From a33a84164c88d32efd7b04c678388d38001f779b Mon Sep 17 00:00:00 2001 From: physikal Date: Thu, 10 Sep 2026 23:09:32 -0700 Subject: [PATCH 01/10] Preserve full-width windows across the idle screensaver The screensaver used compositor fullscreen mode, which demotes any maximized window underneath. Idle lock then killed the screensaver without restoring that state, so unlock left people tiled. Cover the monitor as a floating overlay instead; session lock remains the security boundary. --- default/hypr/apps/system.lua | 22 +++++++++++++--- test/shell.d/screensaver-window-rules-test.sh | 25 +++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 test/shell.d/screensaver-window-rules-test.sh diff --git a/default/hypr/apps/system.lua b/default/hypr/apps/system.lua index 789e3e03e51..0774bfc224a 100644 --- a/default/hypr/apps/system.lua +++ b/default/hypr/apps/system.lua @@ -31,10 +31,24 @@ o.window("dev.tensaku.Tensaku", { float = true }) o.window("dev.tensaku.Tensaku", { center = true }) o.window("omacalc", { float = true }) --- Fullscreen screensaver. -o.window("org.omarchy.screensaver", { fullscreen = true }) -o.window("org.omarchy.screensaver", { float = true }) -o.window("org.omarchy.screensaver", { animation = "slide" }) +-- Screensaver covers each monitor as a floating overlay. Compositor fullscreen +-- mode would replace any maximized / full-width window underneath, so idle lock +-- left people tiled after unlock. Float + pin keeps that state intact; Hyprland +-- raises floating windows over fullscreen without demoting it. Session lock +-- still provides security and kills these terminals on the way in. +o.window("org.omarchy.screensaver", { + float = true, + pin = true, + size = { "monitor_w", "monitor_h" }, + move = { 0, 0 }, + border_size = 0, + rounding = 0, + tag = "-default-opacity", + opacity = "1 1", + opaque = true, + no_dim = true, + animation = "slide", +}) -- No transparency on media windows. o.window( diff --git a/test/shell.d/screensaver-window-rules-test.sh b/test/shell.d/screensaver-window-rules-test.sh new file mode 100644 index 00000000000..713a3bd3169 --- /dev/null +++ b/test/shell.d/screensaver-window-rules-test.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +screensaver_rules="$ROOT/default/hypr/apps/system.lua" +screensaver_block=$(rg -n -U -o 'o\.window\("org\.omarchy\.screensaver"[^)]*\{[\s\S]*?\}\)' "$screensaver_rules" || true) + +[[ -n $screensaver_block ]] || fail "screensaver window rule is defined" + +# Compositor fullscreen / maximize demotes maximized or full-width windows on +# the workspace. Idle lock would then leave people tiled after unlock. +if rg -q 'fullscreen\s*=\s*true|maximize\s*=\s*true' <<<"$screensaver_block"; then + fail "screensaver must not use compositor fullscreen or maximize" +fi +pass "screensaver avoids compositor fullscreen mode" + +rg -q 'float\s*=\s*true' <<<"$screensaver_block" || fail "screensaver floats over the existing layout" +rg -q 'pin\s*=\s*true' <<<"$screensaver_block" || fail "screensaver stays pinned while covering the monitor" +rg -q 'size\s*=\s*\{\s*"monitor_w",\s*"monitor_h"\s*\}' <<<"$screensaver_block" || + fail "screensaver sizes to the full monitor" +rg -q 'move\s*=\s*\{\s*0,\s*0\s*\}' <<<"$screensaver_block" || + fail "screensaver is anchored to the monitor origin" +pass "screensaver covers the monitor as a floating overlay" From a5125c7bbf1a2e16f051981e015d0e7d64152a71 Mon Sep 17 00:00:00 2001 From: physikal Date: Thu, 10 Sep 2026 23:18:25 -0700 Subject: [PATCH 02/10] Replace float-overlay approach with fullscreen snapshot restore --- test/shell.d/screensaver-window-rules-test.sh | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 test/shell.d/screensaver-window-rules-test.sh diff --git a/test/shell.d/screensaver-window-rules-test.sh b/test/shell.d/screensaver-window-rules-test.sh deleted file mode 100644 index 713a3bd3169..00000000000 --- a/test/shell.d/screensaver-window-rules-test.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" - -screensaver_rules="$ROOT/default/hypr/apps/system.lua" -screensaver_block=$(rg -n -U -o 'o\.window\("org\.omarchy\.screensaver"[^)]*\{[\s\S]*?\}\)' "$screensaver_rules" || true) - -[[ -n $screensaver_block ]] || fail "screensaver window rule is defined" - -# Compositor fullscreen / maximize demotes maximized or full-width windows on -# the workspace. Idle lock would then leave people tiled after unlock. -if rg -q 'fullscreen\s*=\s*true|maximize\s*=\s*true' <<<"$screensaver_block"; then - fail "screensaver must not use compositor fullscreen or maximize" -fi -pass "screensaver avoids compositor fullscreen mode" - -rg -q 'float\s*=\s*true' <<<"$screensaver_block" || fail "screensaver floats over the existing layout" -rg -q 'pin\s*=\s*true' <<<"$screensaver_block" || fail "screensaver stays pinned while covering the monitor" -rg -q 'size\s*=\s*\{\s*"monitor_w",\s*"monitor_h"\s*\}' <<<"$screensaver_block" || - fail "screensaver sizes to the full monitor" -rg -q 'move\s*=\s*\{\s*0,\s*0\s*\}' <<<"$screensaver_block" || - fail "screensaver is anchored to the monitor origin" -pass "screensaver covers the monitor as a floating overlay" From fd267513d49d835fcbf4019d20e654098c8453da Mon Sep 17 00:00:00 2001 From: physikal Date: Thu, 10 Sep 2026 23:22:52 -0700 Subject: [PATCH 03/10] Restore full-width state after the idle screensaver --- bin/omarchy-hyprland-fullscreen-snapshot | 1 + 1 file changed, 1 insertion(+) create mode 100644 bin/omarchy-hyprland-fullscreen-snapshot diff --git a/bin/omarchy-hyprland-fullscreen-snapshot b/bin/omarchy-hyprland-fullscreen-snapshot new file mode 100644 index 00000000000..311c8dd0658 --- /dev/null +++ b/bin/omarchy-hyprland-fullscreen-snapshot @@ -0,0 +1 @@ +PLACEHOLDER \ No newline at end of file From 38385836edb26f5adf5f9a116456daeff8ba3495 Mon Sep 17 00:00:00 2001 From: physikal Date: Thu, 10 Sep 2026 23:24:03 -0700 Subject: [PATCH 04/10] Restore full-width state after the idle screensaver --- bin/omarchy-hyprland-fullscreen-snapshot | 102 ++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/bin/omarchy-hyprland-fullscreen-snapshot b/bin/omarchy-hyprland-fullscreen-snapshot index 311c8dd0658..6c0d11737bf 100644 --- a/bin/omarchy-hyprland-fullscreen-snapshot +++ b/bin/omarchy-hyprland-fullscreen-snapshot @@ -1 +1,101 @@ -PLACEHOLDER \ No newline at end of file +#!/bin/bash + +# omarchy:summary=Save or restore Hyprland fullscreen state around the screensaver +# omarchy:args= +# omarchy:hidden=true +# omarchy:examples=omarchy hyprland fullscreen snapshot save | omarchy hyprland fullscreen snapshot restore + +set -euo pipefail + +usage() { + echo "Usage: omarchy-hyprland-fullscreen-snapshot " >&2 +} + +if (($# != 1)); then + usage + exit 1 +fi + +action=$1 +state_file="${XDG_RUNTIME_DIR:-/tmp}/omarchy-fullscreen-snapshot.json" +lock_file="$state_file.lock" + +# Snapshot every non-screensaver client. The idle screensaver maps as a real +# fullscreen window and demotes maximized / full-width clients underneath; +# restore puts those modes back after the screensaver is gone. +save_snapshot() { + local clients + clients=$(hyprctl clients -j) || return 1 + + jq -c ' + [ + .[] + | select((.class // "") != "org.omarchy.screensaver") + | select((.initialClass // "") != "org.omarchy.screensaver") + | select((.address // "") != "" and (.address // "") != "0x0") + | { + address: .address, + fullscreen: (.fullscreen // 0), + fullscreenClient: (.fullscreenClient // 0) + } + ] + ' <<<"$clients" >"$state_file" +} + +restore_window() { + local address=$1 internal=$2 client=$3 + + hyprctl dispatch "hl.dsp.window.fullscreen_state({ window = \"address:$address\", internal = $internal, client = $client })" >/dev/null 2>&1 || + hyprctl dispatch fullscreenstate "$internal" "$client" "address:$address" >/dev/null 2>&1 || + { + hyprctl dispatch focuswindow "address:$address" >/dev/null 2>&1 || true + hyprctl dispatch fullscreenstate "$internal" "$client" >/dev/null 2>&1 || true + } +} + +restore_snapshot() { + local snapshot clients address internal client current_internal current_client + + [[ -f $state_file ]] || return 0 + + snapshot=$(<"$state_file") + rm -f "$state_file" + + [[ -n $snapshot && $snapshot != "[]" ]] || return 0 + + clients=$(hyprctl clients -j) || return 0 + + while IFS=$'\t' read -r address internal client; do + [[ -n $address ]] || continue + + current_internal=$(jq -r --arg address "$address" ' + .[] | select(.address == $address) | (.fullscreen // 0) + ' <<<"$clients") + [[ -n $current_internal && $current_internal != "null" ]] || continue + + current_client=$(jq -r --arg address "$address" ' + .[] | select(.address == $address) | (.fullscreenClient // 0) + ' <<<"$clients") + + [[ $current_internal == "$internal" && $current_client == "$client" ]] && continue + + restore_window "$address" "$internal" "$client" + done < <(jq -r '.[] | [.address, (.fullscreen|tostring), (.fullscreenClient|tostring)] | @tsv' <<<"$snapshot") +} + +( + flock 9 + + case $action in + save) + save_snapshot + ;; + restore) + restore_snapshot + ;; + *) + usage + exit 1 + ;; + esac +) 9>"$lock_file" From 52c12d1304a794e8e9c0f10bc504ad677b77fee8 Mon Sep 17 00:00:00 2001 From: physikal Date: Thu, 10 Sep 2026 23:24:52 -0700 Subject: [PATCH 05/10] Keep fullscreen screensaver; restore layout via snapshot helper --- default/hypr/apps/system.lua | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/default/hypr/apps/system.lua b/default/hypr/apps/system.lua index 0774bfc224a..6a3bf8c798d 100644 --- a/default/hypr/apps/system.lua +++ b/default/hypr/apps/system.lua @@ -31,24 +31,12 @@ o.window("dev.tensaku.Tensaku", { float = true }) o.window("dev.tensaku.Tensaku", { center = true }) o.window("omacalc", { float = true }) --- Screensaver covers each monitor as a floating overlay. Compositor fullscreen --- mode would replace any maximized / full-width window underneath, so idle lock --- left people tiled after unlock. Float + pin keeps that state intact; Hyprland --- raises floating windows over fullscreen without demoting it. Session lock --- still provides security and kills these terminals on the way in. -o.window("org.omarchy.screensaver", { - float = true, - pin = true, - size = { "monitor_w", "monitor_h" }, - move = { 0, 0 }, - border_size = 0, - rounding = 0, - tag = "-default-opacity", - opacity = "1 1", - opaque = true, - no_dim = true, - animation = "slide", -}) +-- Fullscreen screensaver. Mapping it demotes maximized / full-width windows on +-- the workspace; omarchy-hyprland-fullscreen-snapshot save/restore around the +-- screensaver lifecycle puts those modes back after it exits or lock kills it. +o.window("org.omarchy.screensaver", { fullscreen = true }) +o.window("org.omarchy.screensaver", { float = true }) +o.window("org.omarchy.screensaver", { animation = "slide" }) -- No transparency on media windows. o.window( From 0a42ef0b379405fdc9e12c6c95e05679595fbf39 Mon Sep 17 00:00:00 2001 From: physikal Date: Thu, 10 Sep 2026 23:25:12 -0700 Subject: [PATCH 06/10] Save fullscreen state before launching the screensaver --- bin/omarchy-launch-screensaver | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bin/omarchy-launch-screensaver b/bin/omarchy-launch-screensaver index c66c41da3ee..7d781caefd2 100755 --- a/bin/omarchy-launch-screensaver +++ b/bin/omarchy-launch-screensaver @@ -2,6 +2,10 @@ # omarchy:summary=Launch the Omarchy screensaver in the default terminal on the system with the correct font configuration. +if omarchy-cmd-missing ttfx; then + exit 1 +fi + # Exit early if screensaver is already running pgrep -f '[o]rg.omarchy.screensaver' && exit 0 @@ -32,6 +36,9 @@ hypr_exec() { hyprctl dispatch "hl.dsp.exec_cmd([[$command]])" >/dev/null 2>&1 || hyprctl dispatch exec -- bash -lc "$command" >/dev/null } +# Remember maximized / full-width state before the screensaver demotes it. +omarchy-hyprland-fullscreen-snapshot save || true + SOCKET="$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" # Open Hyprland's event stream before spawning anything, so a terminal that maps From d28f9b05ccf4c928d85cb4a2ecd7a5299f45ce94 Mon Sep 17 00:00:00 2001 From: physikal Date: Thu, 10 Sep 2026 23:25:17 -0700 Subject: [PATCH 07/10] Restore fullscreen state after lock kills the screensaver --- bin/omarchy-system-lock | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/omarchy-system-lock b/bin/omarchy-system-lock index 53e4ca5a0ef..c485d901aff 100755 --- a/bin/omarchy-system-lock +++ b/bin/omarchy-system-lock @@ -24,3 +24,6 @@ pkill -x ttfx 2>/dev/null || true # ttfx handles SIGTERM asynchronously, so keep its terminal alive until it exits. timeout 1s pidwait -x ttfx 2>/dev/null || true pkill -f '[o]rg.omarchy.screensaver' 2>/dev/null || true +# Idle lock kills the screensaver before unlock; restore fullscreen modes now so +# they are already correct when the session lock surface goes away. +omarchy-hyprland-fullscreen-snapshot restore || true From c1140ed11db6ea35156c4efa5110e77d3353b7f1 Mon Sep 17 00:00:00 2001 From: physikal Date: Thu, 10 Sep 2026 23:25:38 -0700 Subject: [PATCH 08/10] Restore fullscreen state when the screensaver exits --- bin/omarchy-screensaver | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/omarchy-screensaver b/bin/omarchy-screensaver index acd3868364a..589448ed5e3 100755 --- a/bin/omarchy-screensaver +++ b/bin/omarchy-screensaver @@ -9,6 +9,8 @@ screensaver_in_focus() { exit_screensaver() { hyprctl eval 'hl.config({ cursor = { invisible = false } })' &>/dev/null || hyprctl keyword cursor:invisible false &>/dev/null || true pkill -x ttfx 2>/dev/null + # Restore before killing our own terminal — pkill may reap this process mid-function. + omarchy-hyprland-fullscreen-snapshot restore || true pkill -f '[o]rg.omarchy.screensaver' 2>/dev/null exit 0 } From c8b6b91f7de29353c3a08a88b9764a38e7b11695 Mon Sep 17 00:00:00 2001 From: physikal Date: Thu, 10 Sep 2026 23:25:56 -0700 Subject: [PATCH 09/10] Add fullscreen snapshot coverage --- .../hyprland-fullscreen-snapshot-test.sh | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/shell.d/hyprland-fullscreen-snapshot-test.sh diff --git a/test/shell.d/hyprland-fullscreen-snapshot-test.sh b/test/shell.d/hyprland-fullscreen-snapshot-test.sh new file mode 100644 index 00000000000..ba9519d8b27 --- /dev/null +++ b/test/shell.d/hyprland-fullscreen-snapshot-test.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +mock_bin="$tmpdir/bin" +call_log="$tmpdir/calls" +runtime="$tmpdir/runtime" +mkdir -p "$mock_bin" "$runtime" + +cat >"$mock_bin/hyprctl" <<'SH' +#!/bin/bash +printf '%s %s\n' "hyprctl" "$*" >>"$CALL_LOG" + +if [[ $1 == "clients" && $2 == "-j" ]]; then + cat "$CLIENTS_JSON" + exit 0 +fi + +exit 0 +SH +chmod +x "$mock_bin/hyprctl" + +clients_before='[ + {"address":"0xaaa","class":"kitty","initialClass":"kitty","fullscreen":1,"fullscreenClient":1}, + {"address":"0xbbb","class":"org.omarchy.screensaver","initialClass":"org.omarchy.screensaver","fullscreen":2,"fullscreenClient":2}, + {"address":"0xccc","class":"firefox","initialClass":"firefox","fullscreen":0,"fullscreenClient":0} +]' + +clients_after='[ + {"address":"0xaaa","class":"kitty","initialClass":"kitty","fullscreen":0,"fullscreenClient":0}, + {"address":"0xccc","class":"firefox","initialClass":"firefox","fullscreen":0,"fullscreenClient":0} +]' + +export CALL_LOG="$call_log" +export XDG_RUNTIME_DIR="$runtime" +export PATH="$mock_bin:$PATH" + +printf '%s\n' "$clients_before" >"$tmpdir/clients.json" +CLIENTS_JSON="$tmpdir/clients.json" "$ROOT/bin/omarchy-hyprland-fullscreen-snapshot" save + +state_file="$runtime/omarchy-fullscreen-snapshot.json" +[[ -f $state_file ]] || fail "save writes a runtime snapshot" +jq -e 'length == 2' "$state_file" >/dev/null || fail "save ignores screensaver clients" +jq -e 'map(select(.address == "0xaaa" and .fullscreen == 1 and .fullscreenClient == 1)) | length == 1' "$state_file" >/dev/null || + fail "save keeps full-width state for ordinary clients" +pass "save snapshots non-screensaver fullscreen state" + +>"$call_log" +printf '%s\n' "$clients_after" >"$tmpdir/clients.json" +CLIENTS_JSON="$tmpdir/clients.json" "$ROOT/bin/omarchy-hyprland-fullscreen-snapshot" restore + +[[ ! -f $state_file ]] || fail "restore consumes the snapshot" +rg -F 'hl.dsp.window.fullscreen_state({ window = "address:0xaaa", internal = 1, client = 1 })' "$call_log" >/dev/null || + fail "restore re-applies full-width on the demoted window" "calls: $(tr '\n' '|' <"$call_log")" +! rg -F 'address:0xccc' "$call_log" >/dev/null || fail "restore skips windows that were already tiled" +! rg -F 'address:0xbbb' "$call_log" >/dev/null || fail "restore never targets the screensaver" +pass "restore re-applies demoted fullscreen modes" + +>"$call_log" +CLIENTS_JSON="$tmpdir/clients.json" "$ROOT/bin/omarchy-hyprland-fullscreen-snapshot" restore +! rg -F 'fullscreen_state' "$call_log" >/dev/null || fail "a second restore is a no-op without a snapshot" +pass "restore is idempotent without a snapshot" + +# Lifecycle wiring: launch saves, dismiss/lock restore. +rg -n 'omarchy-hyprland-fullscreen-snapshot save' "$ROOT/bin/omarchy-launch-screensaver" >/dev/null || + fail "screensaver launch saves fullscreen state before mapping" +rg -n 'omarchy-hyprland-fullscreen-snapshot restore' "$ROOT/bin/omarchy-screensaver" >/dev/null || + fail "screensaver dismiss restores fullscreen state" +rg -n 'omarchy-hyprland-fullscreen-snapshot restore' "$ROOT/bin/omarchy-system-lock" >/dev/null || + fail "system lock restores fullscreen state after killing the screensaver" +pass "screensaver lifecycle saves and restores fullscreen state" From c04cf91d3ffe2a7004da025fe9ff7af92dfed6eb Mon Sep 17 00:00:00 2001 From: physikal Date: Thu, 10 Sep 2026 23:26:11 -0700 Subject: [PATCH 10/10] Expect system lock to restore fullscreen after screensaver kill --- test/shell.d/system-lock-test.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/shell.d/system-lock-test.sh b/test/shell.d/system-lock-test.sh index da79eac045e..cbe66521a2c 100755 --- a/test/shell.d/system-lock-test.sh +++ b/test/shell.d/system-lock-test.sh @@ -11,7 +11,7 @@ mock_bin="$tmpdir/bin" call_log="$tmpdir/calls" mkdir -p "$mock_bin" -for command in omarchy-shell hyprctl pkill timeout; do +for command in omarchy-shell hyprctl pkill timeout omarchy-hyprland-fullscreen-snapshot; do cat >"$mock_bin/$command" <<'SH' #!/bin/bash printf '%s %s\n' "$(basename "$0")" "$*" >>"$CALL_LOG" @@ -25,7 +25,7 @@ SH chmod +x "$mock_bin"/* PATH="$mock_bin:$PATH" CALL_LOG="$call_log" "$ROOT/bin/omarchy-system-lock" -mapfile -t shutdown < <(rg '^(pkill|timeout) ' "$call_log") +mapfile -t shutdown < <(rg '^(pkill|timeout|omarchy-hyprland-fullscreen-snapshot) ' "$call_log") [[ ${shutdown[0]} == "pkill -x ttfx" ]] || fail "system lock stops ttfx before closing its terminal" "calls: ${shutdown[*]}" @@ -33,4 +33,6 @@ mapfile -t shutdown < <(rg '^(pkill|timeout) ' "$call_log") fail "system lock waits for ttfx to exit" "calls: ${shutdown[*]}" [[ ${shutdown[2]} == "pkill -f [o]rg.omarchy.screensaver" ]] || fail "system lock closes the screensaver terminal after ttfx exits" "calls: ${shutdown[*]}" +[[ ${shutdown[3]} == "omarchy-hyprland-fullscreen-snapshot restore" ]] || + fail "system lock restores fullscreen state after closing the screensaver" "calls: ${shutdown[*]}" pass "system lock waits for ttfx before closing its terminal"