From e827ba10e495fb33cca6b5e02861bc1ddf64c76e Mon Sep 17 00:00:00 2001 From: Alan Silva Date: Tue, 8 Sep 2026 22:50:10 +0100 Subject: [PATCH] Show visual effects over fullscreen windows --- Service.qml | 26 +++++++++++++++++++++- bin/ar-composite | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100755 bin/ar-composite diff --git a/Service.qml b/Service.qml index b08f146..4344376 100644 --- a/Service.qml +++ b/Service.qml @@ -537,6 +537,26 @@ Item { // -------------------------------------------------------------- effects + // A direct-scanned fullscreen window bypasses Hyprland's compositor, which + // also bypasses our layer-shell surfaces and screen shader. Keep compositor + // rendering enabled just long enough for visual effects to be seen. + function visualEffectDuration(effect) { + var type = String(effect.type || "") + var duration = Number(effect.duration) + if (!isFinite(duration) || duration <= 0) duration = type === "airplane" ? 7 : 1 + if (type === "banner") { + var speed = Number(effect.speed) + if (!isFinite(speed) || speed <= 0) speed = 4 + return duration + 2 / speed + } + if (type === "confetti") return duration + 4.5 + return duration + } + + function keepCompositing(effect) { + Quickshell.execDetached([root.pluginDir + "/bin/ar-composite", String(visualEffectDuration(effect))]) + } + // `flash` is drawn by the shell itself (Flash.qml). Everything else is a // script: ~/.config/attention-required/effects/ if you wrote one, // otherwise effects/ shipped with the plugin. @@ -545,11 +565,15 @@ Item { if (!type) return effect = Rules.withDefaults(root.config, effect) if (root.overlays[type]) { + root.keepCompositing(effect) root.overlays[type].trigger(effect, notif, rule) return } // The shake is a time-driven screen shader; keep frames coming while it runs. - if (type === "nudge") ticker.run(Number(effect.duration) > 0 ? Number(effect.duration) : 1) + if (type === "nudge") { + root.keepCompositing(effect) + ticker.run(Number(effect.duration) > 0 ? Number(effect.duration) : 1) + } var payload = { effect: effect, notification: { diff --git a/bin/ar-composite b/bin/ar-composite new file mode 100755 index 0000000..a99bce6 --- /dev/null +++ b/bin/ar-composite @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# +# Temporarily turn off Hyprland direct scanout. A direct-scanned fullscreen +# window bypasses the compositor, so it also bypasses screen shaders and +# layer-shell overlays. Each invocation extends one shared hold and restores +# the exact previous setting after the last visual effect is gone. +set -uo pipefail + +seconds=${1:-1} +number='^[0-9]*\.?[0-9]+$' +[[ $seconds =~ $number ]] || seconds=1 +command -v hyprctl >/dev/null 2>&1 || exit 0 +command -v jq >/dev/null 2>&1 || exit 0 + +runtime=${XDG_RUNTIME_DIR:-/tmp}/attention-required +mkdir -p "$runtime" || exit 0 +state=$runtime/composite-state +lock=$runtime/composite.lock + +milliseconds=$(awk -v seconds="$seconds" 'BEGIN { printf "%d", (seconds + 0.3) * 1000 }') +now() { date +%s%3N; } +direct_scanout() { hyprctl -j getoption render:direct_scanout 2>/dev/null | jq -r '.int // empty'; } +set_direct_scanout() { + hyprctl -q eval "hl.config({ render = { direct_scanout = $1 } })" >/dev/null 2>&1 \ + || hyprctl -q keyword render:direct_scanout "$1" >/dev/null 2>&1 +} + +exec 9>"$lock" +flock 9 + +current=$(direct_scanout) +[[ $current =~ ^[0-2]$ ]] || exit 0 +until=$(( $(now) + milliseconds )) +previous=$current +if [[ -s $state ]]; then + IFS=$'\t' read -r saved_previous saved_until < "$state" || true + [[ $saved_previous =~ ^[0-2]$ ]] && previous=$saved_previous + [[ $saved_until =~ ^[0-9]+$ && $saved_until -gt $until ]] && until=$saved_until +fi + +printf '%s\t%s\n' "$previous" "$until" > "$state" +set_direct_scanout 0 || { rm -f "$state"; exit 0; } +flock -u 9 + +# Let parallel invocations extend the deadline. The final one restores the +# setting only if nothing else has changed it in the meantime. +sleep "$(awk -v deadline="$until" -v current="$(now)" 'BEGIN { printf "%.3f", (deadline - current) / 1000 + 0.05 }')" +flock 9 + +IFS=$'\t' read -r previous until < "$state" || exit 0 +[[ $until =~ ^[0-9]+$ && $(now) -ge $until ]] || exit 0 +current=$(direct_scanout) +if [[ $current == 0 && $previous =~ ^[0-2]$ ]]; then + set_direct_scanout "$previous" || true +fi +rm -f "$state"