Fix: Restore former window fullscreen state after screensaver exit (#3284)
* fix(screensaver): open in own special workspace instead of active - The screensaver window rule sets fullscreen, and a workspace holds only one fullscreen window - Launching it on the active workspace drops the fullscreen window already there, and leaves it windowed after the screensaver quits * Keep a showing special workspace open through the screensaver A monitor shows one special workspace at a time, so opening the screensaver on its own replaced a visible scratchpad, and it stayed hidden after the screensaver closed. Open it on the special workspace that is already showing instead; it covers that just as well, and the workspace is still there when it goes. The test's stubbed event stream follows the one in #10870. Co-Authored-By: Willem van Ede <37050539+WillemCR@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hand focus back after the screensaver closes Hyprland focuses a monitor when its special workspace empties, so on more than one monitor keyboard focus ended up wherever the last screensaver happened to close, rather than where it was. A detached reader on the launcher's event stream waits for the last one to go and focuses the original monitor again; the launcher itself still exits straight away, as the idle service expects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep a remapped screensaver off the regular workspaces The terminal occasionally maps its window again as it closes, after its special workspace is gone. The launcher's workspace only applies to the first map, so the window landed on the focused monitor's regular workspace, where its fullscreen rule took fullscreen from the window there. On Hyprland 0.56.2 with two monitors this happened in 4 of 45 cycles, each time losing the user's fullscreen. A class rule now sends any later map to a hidden special workspace; the launcher's exec rule still wins on the first map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Check for screensavers before waiting on their close The focus watcher only looked for remaining screensavers after reading a closewindow, so one that never mapped, or closed while the launcher was still waiting on another monitor, left it blocked until some unrelated window closed, and then it pulled focus back to the old monitor. It now checks before each wait, and matches the exact class, passed through the environment so the screensaver's pgrep and pkill never see it in argv. Co-Authored-By: Codex Medium <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: David Heinemeier Hansson <david@hey.com> Co-authored-by: Willem van Ede <37050539+WillemCR@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Codex Medium <noreply@openai.com>
This commit is contained in:
3 files changed
+129
-3
No files matched your search
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
require_command jq
|
||||
require_command lua
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
cleanup() {
|
||||
[[ -f $tmpdir/socat.pid ]] && kill "$(<"$tmpdir/socat.pid")" 2>/dev/null || true
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$tmpdir/bin"
|
||||
mkfifo "$tmpdir/events"
|
||||
# Held open for writing so the event reader never sees end of file between events.
|
||||
exec {events}<>"$tmpdir/events"
|
||||
|
||||
cat >"$tmpdir/bin/hyprctl" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"$TEST_DIR/calls"
|
||||
case "$*" in
|
||||
'monitors -j')
|
||||
printf '[{"name":"DP-1","specialWorkspace":{"name":""}},{"name":"DP-2","specialWorkspace":{"name":"special:scratchpad"}}]\n'
|
||||
;;
|
||||
'clients -j')
|
||||
cat "$TEST_DIR/clients.json"
|
||||
;;
|
||||
*exec_cmd*)
|
||||
count=$(($(wc -l <"$TEST_DIR/spawned") + 1))
|
||||
printf '%s\n' "$count" >>"$TEST_DIR/spawned"
|
||||
printf 'openwindow>>%s,1,org.omarchy.screensaver,foot\n' "$count" >"$TEST_DIR/events"
|
||||
;;
|
||||
esac
|
||||
SH
|
||||
cat >"$tmpdir/bin/socat" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$$" >"$TEST_DIR/socat.pid"
|
||||
exec cat "$TEST_DIR/events"
|
||||
SH
|
||||
printf '#!/bin/bash\nexit 1\n' >"$tmpdir/bin/pgrep"
|
||||
printf '#!/bin/bash\nexit 1\n' >"$tmpdir/bin/omarchy-toggle-enabled"
|
||||
printf '#!/bin/bash\necho DP-1\n' >"$tmpdir/bin/omarchy-hyprland-monitor-focused"
|
||||
printf '#!/bin/bash\necho foot.desktop\n' >"$tmpdir/bin/xdg-terminal-exec"
|
||||
chmod +x "$tmpdir/bin/"*
|
||||
|
||||
: >"$tmpdir/calls"
|
||||
: >"$tmpdir/spawned"
|
||||
printf '[{"class":"org.omarchy.screensaver","mapped":true}]\n' >"$tmpdir/clients.json"
|
||||
|
||||
PATH="$tmpdir/bin:$PATH" TEST_DIR="$tmpdir" XDG_RUNTIME_DIR="$tmpdir" HYPRLAND_INSTANCE_SIGNATURE=test \
|
||||
timeout 10 "$ROOT/bin/omarchy-launch-screensaver" force
|
||||
|
||||
mapfile -t spawns < <(grep exec_cmd "$tmpdir/calls")
|
||||
(( ${#spawns[@]} == 2 )) || fail "a screensaver opens on each monitor" "$(<"$tmpdir/calls")"
|
||||
[[ ${spawns[0]} == *"[workspace special:screensaver-DP-1]"* ]] ||
|
||||
fail "the screensaver opens on its own special workspace, leaving a fullscreen window alone" "${spawns[0]}"
|
||||
pass "the screensaver opens on its own special workspace, leaving a fullscreen window alone"
|
||||
[[ ${spawns[1]} == *"[workspace special:scratchpad]"* ]] ||
|
||||
fail "the screensaver shares a special workspace that is already showing" "${spawns[1]}"
|
||||
pass "the screensaver shares a special workspace that is already showing"
|
||||
|
||||
# Emptying a special workspace focuses its monitor; the last screensaver to close must not keep focus.
|
||||
: >"$tmpdir/calls"
|
||||
printf 'closewindow>>1\n' >&"$events"
|
||||
sleep 0.5
|
||||
grep -q 'hl.dsp.focus' "$tmpdir/calls" && fail "focus waits until the last screensaver has closed" "$(<"$tmpdir/calls")"
|
||||
pass "focus waits until the last screensaver has closed"
|
||||
printf '[{"class":"org.omarchy.screensaver","mapped":false}]\n' >"$tmpdir/clients.json"
|
||||
printf 'closewindow>>2\n' >&"$events"
|
||||
for (( attempt = 0; attempt < 100; attempt++ )); do
|
||||
grep -q 'hl.dsp.focus({ monitor = "DP-1" })' "$tmpdir/calls" && break
|
||||
sleep 0.05
|
||||
done
|
||||
grep -q 'hl.dsp.focus({ monitor = "DP-1" })' "$tmpdir/calls" ||
|
||||
fail "focus returns to the monitor that had it once the screensaver closes" "$(<"$tmpdir/calls")"
|
||||
pass "focus returns to the monitor that had it once the screensaver closes"
|
||||
|
||||
# Screensavers can close while the launcher is still waiting on another monitor, consuming their events.
|
||||
kill "$(<"$tmpdir/socat.pid")"
|
||||
: >"$tmpdir/calls"
|
||||
: >"$tmpdir/spawned"
|
||||
PATH="$tmpdir/bin:$PATH" TEST_DIR="$tmpdir" XDG_RUNTIME_DIR="$tmpdir" HYPRLAND_INSTANCE_SIGNATURE=test \
|
||||
timeout 10 "$ROOT/bin/omarchy-launch-screensaver" force
|
||||
for (( attempt = 0; attempt < 100; attempt++ )); do
|
||||
(( $(grep -c 'hl.dsp.focus({ monitor = "DP-1" })' "$tmpdir/calls") == 3 )) && break
|
||||
sleep 0.05
|
||||
done
|
||||
(( $(grep -c 'hl.dsp.focus({ monitor = "DP-1" })' "$tmpdir/calls") == 3 )) ||
|
||||
fail "focus returns without waiting for a close that has already happened" "$(<"$tmpdir/calls")"
|
||||
pass "focus returns without waiting for a close that has already happened"
|
||||
|
||||
# The launcher's workspace only holds for the first map. A terminal mapped again as it closes falls back to
|
||||
# the class rule, which must keep it off the regular workspaces where its fullscreen rule would take over.
|
||||
fallback=$(OMARCHY_PATH="$ROOT" lua <<'LUA'
|
||||
package.path = os.getenv("OMARCHY_PATH") .. "/?.lua;" .. package.path
|
||||
hl = setmetatable({
|
||||
window_rule = function(rule)
|
||||
if rule.match.class == "org.omarchy.screensaver" and rule.workspace then print(rule.workspace) end
|
||||
end,
|
||||
}, { __index = function() return function() return {} end end })
|
||||
require("default.hypr.helpers")
|
||||
require("default.hypr.apps.system")
|
||||
LUA
|
||||
)
|
||||
[[ $fallback == "special:screensaver silent" ]] ||
|
||||
fail "a screensaver mapped again as it closes stays off the regular workspaces" "workspace rule: ${fallback:-none}"
|
||||
pass "a screensaver mapped again as it closes stays off the regular workspaces"
|
||||
Reference in new issue
Block a user