You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Pressing the logout binding (CTRL + SHIFT + ESCAPE, defined in modules/omarchy/home-manager/hyprland/bindings.nix:46) runs the hyprexit shellscript from modules/omarchy/packages.nix:45, which calls:
exec hyprctl dispatch exit
Hyprland responds with:
error: return hl.dispatch(exit):1: hl.dispatch: expected a dispatcher
(e.g. hl.dsp.window.close())
…and the compositor stays alive. Nothing reaches plasma-login-manager,
so no greeter ever comes back.
Root cause
The Hyprland config in this repo runs in Lua mode:
Under configType = \"lua\" (Hyprland 0.55+, the new Lua API), the
legacy hyprlang dispatcher syntax is no longer accepted. hyprctl dispatch <X> does not look up a dispatcher named <X> anymore;
instead, the daemon wraps the argument and runs it through the Lua VM
as:
returnhl.dispatch(<X>)
So hyprctl dispatch exit becomes:
returnhl.dispatch(exit)
…where exit is parsed as a bare Lua identifier, evaluates to nil in the binding's scope, and hl.dispatch rejects nil with the
exact error shown above. The other hl.dsp.* callsites in bindings.nix (e.g. hl.dsp.window.close(), hl.dsp.focus({...}))
work because they construct real dispatcher objects; only the exit
codepath, which is hidden behind hyprexit → hyprctl dispatch exit, slipped through.
This is the same class of breakage we already patched around in modules/nixpkgs/overlays/waybar.nix for waybar (PR Alexays/Waybar#5013)
and that the upstream Hyprland discussions #14255 and #14282
document.
Background — correct dispatcher API under Lua mode
Three equivalent ways to spell "exit" from Lua mode:
In a keybind handler (preferred): pass the dispatcher object
directly to hl.bind:
hl.bind(\"CTRL + SHIFT + ESCAPE\", hl.dsp.exit())
Inside a Lua callback: explicitly invoke hl.dispatch:
From hyprctl / a shellscript: quote the Lua expression so hl.dispatch(...) gets a real call, not a bare identifier:
hyprctl dispatch 'hl.dsp.exit()'
The thing that doesn't work anywhere on Lua mode is dispatch exit
without hl.dsp. — that's the legacy hyprlang spelling and is the
exact pattern emitting the error.
How to cleanly log out to plasma-login-manager
programs.hyprland.withUWSM is not enabled in this repo, so the
session model is:
plasma-login-greeter
└── plasmalogin-helper (PAM session leader, lives in user scope)
└── wayland-session = hyprland (the Hyprland binary itself,
launched via the .desktop
installed by programs.hyprland)
When Hyprland's exit dispatcher fires, the hyprland process
returns 0, the helper sees HELPER_SUCCESS, and PLM's Display::slotHelperFinished runs Display::stop → Seat::display Stopped → Seat::createDisplay, which spawns a fresh greeter. This
is exactly the path the thebeast-dm-recovery test guards against
regressing in hosts/thebeast/tests/dm-recovery.nix.
So the right primitive for this repo's non-UWSM setup is unchanged: make Hyprland exit cleanly via its own dispatcher. We just need
to spell the dispatcher in a form Lua mode accepts. We don't need loginctl terminate-user (it trips HELPER_AUTH_ERROR per the existing
comment in packages.nix) and we don't need uwsm stop (UWSM isn't
running).
If we ever flip programs.hyprland.withUWSM = true, the logout
primitive changes to uwsm stop (and hl.dsp.exit() becomes
discouraged per the Hyprland wiki, because it skips UWSM's ordered
shutdown). That's a follow-up, not a fix for this bug. See open
questions below.
Proposed fix
Minimal, surgical change to keep the existing hyprexit indirection (which the dm-recovery test pins to hyprctl dispatch exit):
--- a/modules/omarchy/packages.nix+++ b/modules/omarchy/packages.nix
@@
(writeShellScriptBin \"hyprexit\" ''
- exec \${hyprland}/bin/hyprctl dispatch exit+ # Under configType=\"lua\" (set in+ # modules/omarchy/home-manager/hyprland/default.nix) the legacy+ # \`hyprctl dispatch exit\` lowers to \`hl.dispatch(exit)\` and+ # Lua parses \`exit\` as a bare identifier (= nil), so+ # hl.dispatch rejects it. The Lua-mode spelling is+ # \`hl.dsp.exit()\`.+ exec \${hyprland}/bin/hyprctl dispatch 'hl.dsp.exit()'
'')
The dm-recovery test in hosts/thebeast/tests/dm-recovery.nix:86
currently asserts \"hyprctl dispatch exit\" in script. After the fix
the literal substring is still present (hyprctl dispatch 'hl.dsp.exit()' contains it), so the test stays green — but we should
tighten the assertion to the exact Lua-mode form so a future regression
back to legacy syntax is caught:
- assert \"hyprctl dispatch exit\" in script, (+ assert \"hyprctl dispatch 'hl.dsp.exit()'\" in script, (
\"hyprexit must dispatch a clean hyprland exit so the \"
...
)
Optional follow-up (nice but not required): drop the script
indirection and call the dispatcher directly from the keybind so we
stop round-tripping through hyprctl's string parser entirely:
The pkill -TERM steam; sleep 1; hyprexit prelude in the keybind
exists because steam refuses to die cleanly under wayland teardown.
Worth checking if jovian's steam unit (gamescope-session-plus)
needs the same kill on the gamer-session logout path, or if its
systemd scope handles it.
Symptom
Pressing the logout binding (
CTRL + SHIFT + ESCAPE, defined inmodules/omarchy/home-manager/hyprland/bindings.nix:46) runs thehyprexitshellscript frommodules/omarchy/packages.nix:45, which calls:Hyprland responds with:
…and the compositor stays alive. Nothing reaches plasma-login-manager,
so no greeter ever comes back.
Root cause
The Hyprland config in this repo runs in Lua mode:
Under
configType = \"lua\"(Hyprland 0.55+, the new Lua API), thelegacy hyprlang dispatcher syntax is no longer accepted.
hyprctl dispatch <X>does not look up a dispatcher named<X>anymore;instead, the daemon wraps the argument and runs it through the Lua VM
as:
So
hyprctl dispatch exitbecomes:…where
exitis parsed as a bare Lua identifier, evaluates tonilin the binding's scope, andhl.dispatchrejectsnilwith theexact error shown above. The other
hl.dsp.*callsites inbindings.nix(e.g.hl.dsp.window.close(),hl.dsp.focus({...}))work because they construct real dispatcher objects; only the
exitcodepath, which is hidden behind
hyprexit→hyprctl dispatch exit, slipped through.This is the same class of breakage we already patched around in
modules/nixpkgs/overlays/waybar.nixfor waybar (PR Alexays/Waybar#5013)and that the upstream Hyprland discussions
#14255 and
#14282
document.
Background — correct dispatcher API under Lua mode
Three equivalent ways to spell "exit" from Lua mode:
directly to
hl.bind:hl.dispatch:hyprctl/ a shellscript: quote the Lua expression sohl.dispatch(...)gets a real call, not a bare identifier:hyprctl dispatch 'hl.dsp.exit()'The thing that doesn't work anywhere on Lua mode is
dispatch exitwithout
hl.dsp.— that's the legacy hyprlang spelling and is theexact pattern emitting the error.
How to cleanly log out to plasma-login-manager
programs.hyprland.withUWSMis not enabled in this repo, so thesession model is:
When Hyprland's
exitdispatcher fires, thehyprlandprocessreturns 0, the helper sees
HELPER_SUCCESS, and PLM'sDisplay::slotHelperFinishedrunsDisplay::stop→Seat::display Stopped→Seat::createDisplay, which spawns a fresh greeter. Thisis exactly the path the
thebeast-dm-recoverytest guards againstregressing in
hosts/thebeast/tests/dm-recovery.nix.So the right primitive for this repo's non-UWSM setup is unchanged:
make Hyprland exit cleanly via its own dispatcher. We just need
to spell the dispatcher in a form Lua mode accepts. We don't need
loginctl terminate-user(it trips HELPER_AUTH_ERROR per the existingcomment in
packages.nix) and we don't needuwsm stop(UWSM isn'trunning).
If we ever flip
programs.hyprland.withUWSM = true, the logoutprimitive changes to
uwsm stop(andhl.dsp.exit()becomesdiscouraged per the Hyprland wiki, because it skips UWSM's ordered
shutdown). That's a follow-up, not a fix for this bug. See open
questions below.
Proposed fix
Minimal, surgical change to keep the existing
hyprexitindirection (which the dm-recovery test pins tohyprctl dispatch exit):The dm-recovery test in
hosts/thebeast/tests/dm-recovery.nix:86currently asserts
\"hyprctl dispatch exit\" in script. After the fixthe literal substring is still present (
hyprctl dispatch 'hl.dsp.exit()'contains it), so the test stays green — but we shouldtighten the assertion to the exact Lua-mode form so a future regression
back to legacy syntax is caught:
Optional follow-up (nice but not required): drop the script
indirection and call the dispatcher directly from the keybind so we
stop round-tripping through
hyprctl's string parser entirely:(Skipping that for now keeps the dm-recovery test's
"hyprexit is the single chokepoint" invariant intact.)
Open questions / links
programs.hyprland.withUWSM = trueand switchhyprexittouwsm stop? Pros: UWSM does the systemd-graphical-session.target teardown in the right order; matches the Hyprland
wiki's recommendation for systemd users. Cons: known
PLM+UWSM-on-NixOS regression where the greeter doesn't come back
(discourse SDDM gets black screen after logout from Hyprland UWSM
,
[SOLVED] Exit Hyprland on NixOS always give me black screen hyprwm/Hyprland#9475), so this would need its own dm-recovery
subtest before we trust it.
pkill -TERM steam; sleep 1; hyprexitprelude in the keybindexists because steam refuses to die cleanly under wayland teardown.
Worth checking if jovian's steam unit (
gamescope-session-plus)needs the same kill on the gamer-session logout path, or if its
systemd scope handles it.
hyprctl dispatchlegacy syntax rejected under Lua)hl.dispatchrequires a dispatcher object, not a bare identifier)modules/nixpkgs/overlays/waybar.nix)