Decide how to log out by what the session is, not what is installed

The power menu's Log Out ran `uwsm stop` whenever the uwsm binary existed --
which is always, since Panama installs it. In a plain "Hyprland" session there
is no uwsm unit to stop, so the command failed and the button silently did
nothing. Nor is `uwsm check is-active` the test: it reads
graphical-session.target, which the plain session also activates. The one
thing unique to a managed session is uwsm's own compositor unit,
wayland-wm@*.service, so that is now what chooses between stopping the unit
and asking the compositor to exit. A new contract pins the decision -- and
checks the script line rather than the file, because the comment explaining
the wrong tests has to be allowed to name them.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
This commit is contained in:
Gabriel Brown
2026-08-23 10:38:09 -04:00
parent 12f6b2a310
commit 6ad2e017e8
2 changed files with 54 additions and 4 deletions
@@ -31,10 +31,15 @@ PanelWindow {
property int currentIndex: 0 property int currentIndex: 0
// The session is uwsm-managed, so logging out means stopping the uwsm unit // A uwsm-managed session logs out by stopping the uwsm unit; a plain
// rather than killing the compositor; the hyprctl branch is only there for // "Hyprland" session must ask the compositor to exit instead. The branch
// a session started without it. // used to test merely that the uwsm BINARY existed -- but uwsm is always
readonly property string logoutScript: 'if command -v uwsm >/dev/null 2>&1; then exec uwsm stop; else exec hyprctl dispatch "hl.dsp.exit()"; fi' // installed here, so in a plain session `uwsm stop` found no unit to
// stop, failed, and the button did nothing. Nor is `uwsm check is-active`
// the test: it only checks graphical-session.target, which the plain
// session also reaches. What only the managed session has is uwsm's own
// compositor unit, wayland-wm@*.service, so that is what decides.
readonly property string logoutScript: 'if systemctl --user list-units --no-legend --state=active "wayland-wm@*.service" 2>/dev/null | grep -q .; then exec uwsm stop; else exec hyprctl dispatch "hl.dsp.exit()"; fi'
readonly property var entries: [ readonly property var entries: [
{ {
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# The power menu's Log Out, in both kinds of session.
#
# Logging out means `uwsm stop` in a uwsm-managed session and a compositor
# exit in a plain one -- and the script has to tell the two apart by what the
# SESSION is, not by what is installed. It once branched on the uwsm binary
# existing; uwsm is always installed here, so in a plain session `uwsm stop`
# found no unit to stop, failed, and the button silently did nothing. The only
# thing unique to the managed session is uwsm's own compositor unit.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
menu="$repo_dir/config/dot/quickshell/modules/powermenu/PowerMenu.qml"
fail() {
printf 'powermenu contract: %s\n' "$1" >&2
exit 1
}
[[ -r "$menu" ]] || fail "$menu is missing"
# The script itself, not the file: the comment above it names the wrong
# tests while explaining them, and a contract must not fail over prose.
script_line="$(rg -N 'logoutScript:' "$menu" | head -1)"
[[ -n "$script_line" ]] || fail 'the logout script is gone'
# The branch condition is the session's own unit, nothing weaker.
grep -Fq 'wayland-wm@*.service' <<<"$script_line" \
|| fail 'logout does not decide by the uwsm compositor unit'
grep -Fq 'command -v uwsm' <<<"$script_line" \
&& fail 'logout branches on the uwsm binary existing, which is true in both kinds of session'
# `uwsm check is-active` is the tempting wrong test: it reads
# graphical-session.target, which the plain session also activates.
grep -Fq 'uwsm check is-active' <<<"$script_line" \
&& fail 'logout branches on is-active, which the plain session also passes'
# Both destinations survive.
grep -Fq 'uwsm stop' <<<"$script_line" \
|| fail 'a managed session has no way to stop its unit'
grep -Fq 'hl.dsp.exit()' <<<"$script_line" \
|| fail 'a plain session has no way to exit the compositor'
printf 'powermenu contract: ok\n'