Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions bin/omarchy-hyprland-fullscreen-snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/bin/bash

# omarchy:summary=Save or restore Hyprland fullscreen state around the screensaver
# omarchy:args=<save|restore>
# 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 <save|restore>" >&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"
7 changes: 7 additions & 0 deletions bin/omarchy-launch-screensaver
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions bin/omarchy-screensaver
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 3 additions & 0 deletions bin/omarchy-system-lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion default/hypr/apps/system.lua
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ o.window("dev.tensaku.Tensaku", { float = true })
o.window("dev.tensaku.Tensaku", { center = true })
o.window("omacalc", { float = true })

-- Fullscreen screensaver.
-- 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" })
Expand Down
76 changes: 76 additions & 0 deletions test/shell.d/hyprland-fullscreen-snapshot-test.sh
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 4 additions & 2 deletions test/shell.d/system-lock-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -25,12 +25,14 @@ 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[*]}"
[[ ${shutdown[1]} == "timeout 1s pidwait -x ttfx" ]] ||
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"