From 6ad2e017e8e7e998d00a6b9e0b663c630ad2d533 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Sun, 23 Aug 2026 10:38:09 -0400 Subject: [PATCH] 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 --- .../modules/powermenu/PowerMenu.qml | 13 ++++-- tests/quickshell/powermenu-contract | 45 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100755 tests/quickshell/powermenu-contract diff --git a/config/dot/quickshell/modules/powermenu/PowerMenu.qml b/config/dot/quickshell/modules/powermenu/PowerMenu.qml index e700d99..fd3cac6 100644 --- a/config/dot/quickshell/modules/powermenu/PowerMenu.qml +++ b/config/dot/quickshell/modules/powermenu/PowerMenu.qml @@ -31,10 +31,15 @@ PanelWindow { property int currentIndex: 0 - // The session is uwsm-managed, so logging out means stopping the uwsm unit - // rather than killing the compositor; the hyprctl branch is only there for - // a session started without it. - readonly property string logoutScript: 'if command -v uwsm >/dev/null 2>&1; then exec uwsm stop; else exec hyprctl dispatch "hl.dsp.exit()"; fi' + // A uwsm-managed session logs out by stopping the uwsm unit; a plain + // "Hyprland" session must ask the compositor to exit instead. The branch + // used to test merely that the uwsm BINARY existed -- but uwsm is always + // 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: [ { diff --git a/tests/quickshell/powermenu-contract b/tests/quickshell/powermenu-contract new file mode 100755 index 0000000..0446f13 --- /dev/null +++ b/tests/quickshell/powermenu-contract @@ -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'