aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/status
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-21 20:13:23 +0200
committerDanilo M. <danix@danix.xyz>2026-09-21 20:13:23 +0200
commit696f3ad4f65a6516549544af13a9646f57ae523e (patch)
tree10004eac68626f413da40c2287dd1838fbb82f86 /desktop/modules/status
parent34a6a3cd65370fd31877da0c54abcae15d69d3a4 (diff)
downloadquickshell-696f3ad4f65a6516549544af13a9646f57ae523e.tar.gz
quickshell-696f3ad4f65a6516549544af13a9646f57ae523e.zip
feat(status): report a running game as its own presentation state
status.gaming is written by the shell's game detector and presentation is the OR of it and the manual file, but statusctl read only the manual half. A game therefore asserted the presentation effects while the waybar pill still read deactivated. Report gaming as a third state, for presentation only. The watch follows status.gaming as well as its own file, and emits only on a real change so an unchanged rewrite draws nothing. Toggle is refused while a game holds the mode: the detector reasserts the file within its poll, so the write would not stick. set stays allowed, the manual half is worth setting for when the game exits. gaming stays out of MODES. The detector owns that file and nothing here may write it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/status')
-rw-r--r--desktop/modules/status/README.md9
-rwxr-xr-xdesktop/modules/status/statusctl45
-rwxr-xr-xdesktop/modules/status/test-statusctl.sh41
3 files changed, 92 insertions, 3 deletions
diff --git a/desktop/modules/status/README.md b/desktop/modules/status/README.md
index 4d18342..7ac6b93 100644
--- a/desktop/modules/status/README.md
+++ b/desktop/modules/status/README.md
@@ -130,6 +130,15 @@ waybar's built-in `idle_inhibitor`, which cannot be kept alongside it: that
module owns its own inhibitor object, so both would have to be released
before the screen could lock.
+The watch emits three classes, not two. `status.gaming` turns the mode on
+without touching the manual half, so a game would otherwise leave the widget
+reading `deactivated` while the effects were asserted. `gaming` is its own
+class with its own icon, and `toggle` is refused while it holds: the detector
+reasserts the file within its poll, so the write would not stick. `set` is
+still allowed, since the manual half is worth setting for when the game exits.
+The watch therefore follows `status.gaming` as well as its own file, and emits
+only on a real change so an unchanged rewrite draws nothing.
+
## The check
./test-statusctl.sh
diff --git a/desktop/modules/status/statusctl b/desktop/modules/status/statusctl
index 571beef..531951b 100755
--- a/desktop/modules/status/statusctl
+++ b/desktop/modules/status/statusctl
@@ -82,15 +82,34 @@ tooltip() {
case "$1" in
activated) printf '%s: on' "$mode" ;;
deactivated) printf '%s: off' "$mode" ;;
+ gaming) printf '%s: on (game running)' "$mode" ;;
esac
}
+# The shell's game detector owns status.gaming and presentation is the OR of
+# the two, so the CLI has to read it to agree with the shell. It is not a mode
+# in MODES: nothing here may write it, and only presentation is affected.
+gaming_on() {
+ local v=""
+ [[ -e "$DIR/status.gaming" ]] && v="$(tr -d '[:space:]' < "$DIR/status.gaming" 2>/dev/null)"
+ [[ "$v" == "1" ]]
+}
+
# A missing file is the off state, not a distinct condition: read_mode already
# reads it as 0, and this is the same path `get` uses. There is no "down": a
# file cannot report whether a watcher is alive, and a missing one is exactly
# what a fresh session looks like.
+#
+# A running game reports its own state rather than plain activated, so the
+# widget can show why the mode is on and that a click will not turn it off.
state_now() {
- [[ "$(read_mode)" == "1" ]] && printf 'activated' || printf 'deactivated'
+ if [[ "$mode" == "presentation" ]] && gaming_on; then
+ printf 'gaming'
+ elif [[ "$(read_mode)" == "1" ]]; then
+ printf 'activated'
+ else
+ printf 'deactivated'
+ fi
}
case "$action" in
@@ -106,6 +125,15 @@ case "$action" in
esac
;;
toggle)
+ # A running game pins presentation on and the detector reasserts it
+ # within its poll, so a toggle here would flip back on its own. Refuse
+ # it instead of writing a value that does not stick. `set` still works:
+ # it is the explicit verb, and the manual half is worth setting for
+ # when the game exits.
+ if [[ "$mode" == "presentation" ]] && gaming_on; then
+ printf '%s: presentation held on by a running game\n' "${0##*/}" >&2
+ exit 1
+ fi
[[ "$(read_mode)" == "1" ]] && write_mode 0 || write_mode 1
;;
watch)
@@ -123,9 +151,20 @@ case "$action" in
exec 3< <(inotifywait -q -m -e close_write,moved_to,delete --format '%f' "$DIR" 2>/dev/null)
watcher=$!
trap 'kill "$watcher" 2>/dev/null' EXIT TERM INT
+ # Presentation also follows status.gaming, so a game starting or
+ # ending redraws the widget. Emitting only on a real change keeps a
+ # write of an unchanged value from firing a duplicate line.
+ last="$(state_now)"
while read -r changed <&3; do
- [[ "$changed" == "status.$mode" ]] || continue
- emit "$(state_now)"
+ case "$changed" in
+ "status.$mode") ;;
+ status.gaming) [[ "$mode" == "presentation" ]] || continue ;;
+ *) continue ;;
+ esac
+ now="$(state_now)"
+ [[ "$now" == "$last" ]] && continue
+ last="$now"
+ emit "$now"
done
;;
*)
diff --git a/desktop/modules/status/test-statusctl.sh b/desktop/modules/status/test-statusctl.sh
index 7b1b188..38da27c 100755
--- a/desktop/modules/status/test-statusctl.sh
+++ b/desktop/modules/status/test-statusctl.sh
@@ -92,5 +92,46 @@ kill "$watcher2" 2>/dev/null
wait "$watcher2" 2>/dev/null
check "watch reports deactivated when absent" "1" "$(grep -c '"class": *"deactivated"' "$out2")"
+# A running game is reported as its own state, not as plain activated: the
+# widget draws a different icon and the tooltip says why the mode is on.
+rm -f "$tmp/status.presentation"
+printf '1\n' > "$tmp/status.gaming"
+out3="$tmp/watch3.out"
+"$ctl" presentation watch > "$out3" 2>/dev/null &
+watcher3=$!
+sleep 0.5
+kill "$watcher3" 2>/dev/null
+wait "$watcher3" 2>/dev/null
+check "watch reports gaming" "1" "$(grep -c '"class": *"gaming"' "$out3")"
+
+# The gaming file is the shell's, so presentation's own file stays off and
+# `get` keeps reporting the manual half rather than the OR.
+check "get reads the manual half" "0" "$("$ctl" presentation get)"
+
+# Toggling while a game runs is refused: the detector would reassert it, so a
+# write here would not stick.
+"$ctl" presentation toggle 2>/dev/null
+check "toggle refused while gaming" "1" "$?"
+check "toggle wrote nothing" "0" "$("$ctl" presentation get)"
+
+# The refusal is specific to presentation; another mode toggles normally with
+# a game running.
+"$ctl" dnd set 0
+"$ctl" dnd toggle
+check "dnd toggles while gaming" "1" "$("$ctl" dnd get)"
+
+# A game ending redraws the widget, which is why the watch follows
+# status.gaming as well as its own file.
+out4="$tmp/watch4.out"
+"$ctl" presentation watch > "$out4" 2>/dev/null &
+watcher4=$!
+sleep 0.3
+printf '0\n' > "$tmp/status.gaming.tmp"
+mv -f "$tmp/status.gaming.tmp" "$tmp/status.gaming"
+sleep 0.5
+kill "$watcher4" 2>/dev/null
+wait "$watcher4" 2>/dev/null
+check "game ending redraws" "1" "$(grep -c '"class": *"deactivated"' "$out4")"
+
printf '\n%d passed, %d failed\n' "$pass" "$fail"
[[ "$fail" -eq 0 ]]