Show visual effects over fullscreen windows
This commit is contained in:
+25
-1
@@ -537,6 +537,26 @@ Item {
|
|||||||
|
|
||||||
// -------------------------------------------------------------- effects
|
// -------------------------------------------------------------- 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
|
// `flash` is drawn by the shell itself (Flash.qml). Everything else is a
|
||||||
// script: ~/.config/attention-required/effects/<type> if you wrote one,
|
// script: ~/.config/attention-required/effects/<type> if you wrote one,
|
||||||
// otherwise effects/<type> shipped with the plugin.
|
// otherwise effects/<type> shipped with the plugin.
|
||||||
@@ -545,11 +565,15 @@ Item {
|
|||||||
if (!type) return
|
if (!type) return
|
||||||
effect = Rules.withDefaults(root.config, effect)
|
effect = Rules.withDefaults(root.config, effect)
|
||||||
if (root.overlays[type]) {
|
if (root.overlays[type]) {
|
||||||
|
root.keepCompositing(effect)
|
||||||
root.overlays[type].trigger(effect, notif, rule)
|
root.overlays[type].trigger(effect, notif, rule)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// The shake is a time-driven screen shader; keep frames coming while it runs.
|
// 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 = {
|
var payload = {
|
||||||
effect: effect,
|
effect: effect,
|
||||||
notification: {
|
notification: {
|
||||||
|
|||||||
Executable
+56
@@ -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"
|
||||||
Reference in New Issue
Block a user