From 3e9189a9a8bc6875b1ad3cd5a32959b7895c3e7e Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 15 Sep 2026 11:24:46 +0200 Subject: feat(desktop): add the statusctl CLI and its check statusctl reads and writes the mode files directly rather than going through the shell, so it works while quickshell is down. Setting a mode that way records the state without firing its effects; the shell sees the change through its own watch and reasserts them. The watch listens on the directory, not the file: an atomic write replaces the file, so a watch held on the old inode dies with it. Same trap the mail watcher hit with Xapian, and the reason moved_to is in the event list. An unknown mode exits non-zero rather than reading as off, so a typo cannot masquerade as a mode that happens to be disabled. The watch loop runs inotifywait through process substitution, held open on fd 3, with the PID captured and trapped on EXIT/TERM/INT. Piped straight into the while loop, inotifywait would be a pipeline sibling rather than a child, so a plain kill on statusctl leaves it running, watching a directory nobody reads anymore. Task 7 wires this CLI into waybar's exec, and waybar kills and respawns exec children on every reload the same plain way, so every reload would otherwise leak another watcher for the rest of the session. Process substitution makes inotifywait a real child whose PID the trap can hold and kill. A SIGKILL still orphans it, since KILL cannot be trapped, but nothing in the described path sends one. A failed rename exits non-zero instead of being swallowed. Reporting success on a write that did not land would leave the caller and the shell disagreeing about the mode, with an orphan temp file as the only trace, and this script is the interface the whole registry is read and written through. Verified against an unwritable directory: exit 1, no orphan left, prior value intact. Reading uses a redirect rather than cat piped into tr, so that an unreadable file falling back to off is explicit rather than a side effect of a pipeline discarding cat's exit status. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01A7ThHHh5iTYbVfp3rNAkw2 --- desktop/modules/status/test-statusctl.sh | 87 ++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100755 desktop/modules/status/test-statusctl.sh (limited to 'desktop/modules/status/test-statusctl.sh') diff --git a/desktop/modules/status/test-statusctl.sh b/desktop/modules/status/test-statusctl.sh new file mode 100755 index 0000000..3b8f7ba --- /dev/null +++ b/desktop/modules/status/test-statusctl.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# +# Copyright (C) 2026 Danilo M. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The one runnable check for statusctl. It points XDG_RUNTIME_DIR at a +# temporary directory, so nothing here touches the live modes. +# +# Usage: ./test-statusctl.sh (exit 0 = all passed) + +set -u + +here="$(cd "$(dirname "$0")" && pwd)" +ctl="$here/statusctl" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +export XDG_RUNTIME_DIR="$tmp" + +pass=0 +fail=0 + +check() { + local label="$1" want="$2" got="$3" + if [[ "$want" == "$got" ]]; then + printf 'ok %s\n' "$label" + pass=$((pass + 1)) + else + printf 'FAIL %s: want %q, got %q\n' "$label" "$want" "$got" + fail=$((fail + 1)) + fi +} + +# A mode with no file reads as off. +check "missing file reads 0" "0" "$("$ctl" dnd get)" + +# set writes the file and get reads it back. +"$ctl" dnd set 1 +check "set 1 writes the file" "1" "$(cat "$tmp/status.dnd" | tr -d '[:space:]')" +check "get after set 1" "1" "$("$ctl" dnd get)" + +# toggle flips it. +"$ctl" dnd toggle +check "toggle from 1" "0" "$("$ctl" dnd get)" +"$ctl" dnd toggle +check "toggle from 0" "1" "$("$ctl" dnd get)" + +# set 0 writes rather than removing, so a reader sees an explicit off. +"$ctl" dnd set 0 +check "set 0 writes the file" "0" "$("$ctl" dnd get)" + +# An unknown mode is an error, not a silent success: a typo must not look +# like a mode that is off. +"$ctl" nosuch get >/dev/null 2>&1 +check "unknown mode exits non-zero" "1" "$?" + +# watch prints a line on change, and the class reflects the value. The +# atomic write arrives as a rename, which is why the watch needs moved_to. +out="$tmp/watch.out" +"$ctl" presentation watch > "$out" 2>/dev/null & +watcher=$! +sleep 0.3 +"$ctl" presentation set 1 +sleep 0.5 +kill "$watcher" 2>/dev/null +wait "$watcher" 2>/dev/null +check "watch reports activated" "1" "$(grep -c '"class": *"activated"' "$out")" + +# A missing file is reported as down, distinct from a mode that is off. +rm -f "$tmp/status.presentation" +out2="$tmp/watch2.out" +"$ctl" presentation watch > "$out2" 2>/dev/null & +watcher2=$! +sleep 0.5 +kill "$watcher2" 2>/dev/null +wait "$watcher2" 2>/dev/null +check "watch reports down when absent" "1" "$(grep -c '"class": *"down"' "$out2")" + +printf '\n%d passed, %d failed\n' "$pass" "$fail" +[[ "$fail" -eq 0 ]] -- cgit v1.2.3