Recover the startup cursor across Hyprland reloads

Hyprland discards Lua globals and timers during reload, so the pending startup reveal must survive outside Lua state. Preserve the normal cursor theme for this compositor session, recover orphaned blank settings, and repair activation environments before future applications inherit them.

🤖 Generated by GPT-6.1-Sol High in Codex through T3 Code. Reviewed by GPT-6.1-Sol XHigh in Codex through T3 Code.

Co-Authored-By: GPT-6.1-Sol XHigh <codex@openai.com>
This commit is contained in:
Spencer BullandGPT-6.1-Sol XHigh committed 2026-10-10 17:17:28 -05:00
1 parent 77bfaca408
commit f5170dd070
3 files changed
+135 -11

No files matched your search

@@ -67,6 +67,9 @@ with tempfile.TemporaryDirectory() as directory:
time.sleep(0.01)
wait_phase("holding")
subprocess.run(["hyprctl", "reload"], env=env, check=True, capture_output=True, timeout=5)
subprocess.run(["hyprctl", "eval", "assert(omarchy_startup_cursor_pending, 'reload lost the pending cursor reveal')"],
env=env, check=True, capture_output=True, timeout=5)
(stage / "release").write_text("reveal")
wait_phase("revealed")
# Cursor visibility is polled by the compositor independently of the fade.
@@ -78,6 +81,9 @@ with tempfile.TemporaryDirectory() as directory:
if float(green) > 20:
break
assert time.monotonic() < deadline, "the normal cursor did not return with the desktop"
except Exception:
print((stage / "compositor.log").read_text())
raise
finally:
os.killpg(compositor.pid, signal.SIGTERM)
compositor.wait(timeout=10)
+69 -3
View File
@@ -4,11 +4,11 @@ set -euo pipefail
source "$(dirname "$0")/base-test.sh"
require_command lua
lua <<'LUA'
lua - <<'LUA'
package.path = os.getenv("ROOT") .. "/?.lua;" .. package.path
local events, recovery, command = {}, nil, nil
local events, recovery, command, imported = {}, nil, nil, false
local config = { invisible = false, enable_hyprcursor = true, sync_gsettings_theme = true }
local env = { XCURSOR_THEME = "my-xcursor", HYPRCURSOR_THEME = "my-hyprcursor", XCURSOR_PATH = "/my/icons" }
local env = { XCURSOR_THEME = "my-xcursor", HYPRCURSOR_THEME = "my-hyprcursor", XCURSOR_PATH = "/my/icons", HYPRLAND_INSTANCE_SIGNATURE = "test-session", OMARCHY_PATH = os.getenv("ROOT") }
local getenv = os.getenv
os.getenv = function(name) return env[name] or getenv(name) end
local monitors = {}
@@ -25,6 +25,10 @@ hl = {
recovery = callback
end,
exec_cmd = function(value)
if value == "dbus-update-activation-environment --systemd XCURSOR_PATH XCURSOR_THEME" then
assert(env.XCURSOR_THEME ~= "omarchy-startup" and not env.XCURSOR_PATH:match("/default/hypr/cursors"))
imported = true
end
if value == "omarchy-launch-shell" then
assert(config.invisible and env.XCURSOR_THEME == "my-xcursor", "hide the compositor cursor while restoring application settings before launch")
end
@@ -44,7 +48,15 @@ assert(env.XCURSOR_THEME == "my-xcursor" and env.XCURSOR_PATH == "/my/icons", "a
assert(config.invisible, "starting applications must not reveal the cursor")
local previous_recovery = recovery
-- Hyprland reloads the configuration in a fresh Lua state and discards timers.
omarchy_startup_cursor_pending = nil
omarchy_startup_cursor = nil
for name in pairs(config) do config[name] = true end
config.invisible = false
monitors = { {} }
dofile(os.getenv("ROOT") .. "/default/hypr/startup-cursor.lua")
events["config.reloaded"]()
assert(omarchy_startup_cursor_pending and omarchy_startup_cursor.xcursor == "my-xcursor", "a fresh Lua state must retain the pending reveal and the user's cursor")
assert(recovery ~= previous_recovery and config.invisible, "a config reload must retain startup hiding and rearm recovery")
recovery()
assert(command:match("setcursor 'my%-xcursor'"), "restore the Xcursor fallback before revealing the pointer")
@@ -62,5 +74,59 @@ omarchy_startup_cursor_pending = nil
monitors = { {} }
events["config.reloaded"]()
assert(not omarchy_startup_cursor_pending and not config.invisible, "installing the fix in a running compositor must leave its cursor alone")
local popen = io.popen
local gsettings = "'custom-cursor'\n"
io.popen = function(value)
assert(value:match("gsettings get org.gnome.desktop.interface cursor%-theme"))
return { read = function() return gsettings end, close = function() return true end }
end
local function orphan(theme)
imported = false
omarchy_startup_cursor_pending, omarchy_startup_cursor = nil, nil
env.XCURSOR_THEME = "omarchy-startup"
env.OMARCHY_STARTUP_CURSOR_THEME = theme
env.OMARCHY_STARTUP_CURSOR_SESSION = "old-session"
env.XCURSOR_PATH = "/old/worktree/default/hypr/cursors:/my/icons"
env.OMARCHY_STARTUP_CURSOR_PATH = nil
config.invisible, config.enable_hyprcursor, config.sync_gsettings_theme = false, false, true
dofile(os.getenv("ROOT") .. "/default/hypr/startup-cursor.lua")
events["config.reloaded"]()
assert(omarchy_startup_cursor_pending, "an orphan blank theme must be recovered with live monitors")
assert(env.XCURSOR_THEME ~= "omarchy-startup" and env.XCURSOR_PATH == "/my/icons", "recovery must remove the blank theme and old worktree search path")
return omarchy_startup_cursor.xcursor
end
assert(orphan("saved-custom") == "saved-custom", "saved settings take precedence over GSettings")
omarchy_startup_cursor_restore()
assert(command:match("setcursor 'saved%-custom'") and imported, "orphan recovery must also repair activation environments")
local stalled = command
command = nil
recovery()
assert(command == stalled, "the timeout must retry an unfinished restore")
local completion = assert(command:match("hyprctl eval '(.*)'$"))
omarchy_startup_cursor.xcursor = "changed-during-restore"
assert(load(completion))()
assert(omarchy_startup_cursor_pending, "a stale completion must not reveal a different cursor after reload")
omarchy_startup_cursor.xcursor = "saved-custom"
local old_size = omarchy_startup_cursor.size
omarchy_startup_cursor.size = old_size + 8
assert(load(completion))()
assert(omarchy_startup_cursor_pending, "a stale completion must not acknowledge a different cursor size")
omarchy_startup_cursor.size = old_size
omarchy_startup_cursor_restore(true)
assert(not config.invisible and config.sync_gsettings_theme)
assert(orphan(nil) == "custom-cursor", "recover a nonblank user GSettings theme when no snapshot survives")
omarchy_startup_cursor_restore(true)
gsettings = "'omarchy-startup'\n"
assert(orphan(nil) == "default", "a destroyed user preference must fall back to the default alias, never the blank theme")
omarchy_startup_cursor_restore(true)
omarchy_startup_cursor_pending, omarchy_startup_cursor = nil, nil
monitors = {}
env.XCURSOR_THEME, env.XCURSOR_PATH = "next-theme", "/next/icons"
env.OMARCHY_STARTUP_CURSOR_PATH = "/stale/icons"
env.OMARCHY_STARTUP_CURSOR_SESSION = "old-session"
events["config.reloaded"]()
assert(omarchy_startup_cursor.xcursor == "next-theme" and omarchy_startup_cursor.path == "/next/icons", "a new session must honor changed cursor settings instead of inherited snapshots")
io.popen = popen
LUA
pass "the first compositor frame starts blank and the reveal restores user cursor settings"