aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md17
-rw-r--r--desktop/modules/status/README.md59
-rw-r--r--docs/superpowers/plans/2026-09-15-status-registry.md40
3 files changed, 111 insertions, 5 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 1b6e8a8..963bda2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -212,6 +212,23 @@ changing that component. The ones that generalise:
inhibitor it asserts a second, independent one, so idle resumes only when
both release. Replace it with a `custom/` module reading the owning
component's state.
+- **A `FileView` that writes the file it watches sees its own write.**
+ `watchChanges` fires `fileChanged` on `setText()` as well as on an external
+ change, so a handler that writes in response to a change loops. The status
+ registry compares the reparsed value against the current one and assigns
+ only on a difference, which makes the self-write a no-op.
+- **`IdleInhibitor` needs a non-null `window`.** It has no window of its own
+ and does nothing without one. A singleton therefore cannot assert an
+ inhibitor unaided: `shell.qml` hands it the keepalive `PanelWindow`, which
+ is the one window that exists for the whole session.
+- **A layer-surface idle inhibitor is not observable via `hyprctl clients` or
+ `hyprctl layers`.** `hyprctl clients | grep -ci inhibit` counts the
+ per-toplevel `inhibitingIdle` field only, and an inhibitor asserted on a
+ `PanelWindow` layer surface appears in neither listing, so the count does not
+ move even when the inhibitor is asserted. Check it behaviorally: run a
+ throwaway `hypridle -c` with a short timeout whose `on-timeout` writes a
+ marker, confirm the marker does not appear while presentation is on, and
+ that it appears within a few seconds of turning presentation off.
## Theme
diff --git a/desktop/modules/status/README.md b/desktop/modules/status/README.md
new file mode 100644
index 0000000..0bb376f
--- /dev/null
+++ b/desktop/modules/status/README.md
@@ -0,0 +1,59 @@
+# status
+
+Desktop modes as state: `dnd` and `presentation`, owned by the `Status`
+singleton and stored as files under `$XDG_RUNTIME_DIR`.
+
+## The files are the interface
+
+ $XDG_RUNTIME_DIR/status.dnd
+ $XDG_RUNTIME_DIR/status.presentation
+
+Each holds `0` or `1`; a missing file means off. That directory is tmpfs, so a
+reboot resets every mode and there is no cleanup code. A shell restart does
+not: the files outlive the process and the singleton reads them back.
+
+Anything can read a mode with `cat`. `statusctl` is the convenience, not the
+mechanism, which is why it keeps working while quickshell is down.
+
+## statusctl
+
+ statusctl <mode> get prints 0 or 1
+ statusctl <mode> set 0|1
+ statusctl <mode> toggle
+ statusctl <mode> watch waybar JSON on every change
+
+The repo copy is the source; the user installs it to `~/bin`. `watch` watches
+the directory rather than the file, because an atomic write replaces the file
+and a watch on the old inode dies with it.
+
+Setting a mode with `statusctl` records the state without firing its effects.
+The shell sees the change through its own `FileView` watch and asserts them,
+so the effects follow either way. If the shell is down, the state is recorded
+and reasserted when it returns.
+
+## Effects
+
+`dnd` has none of its own. It is state the notification daemon reads.
+
+`presentation` sets `dnd`, asserts a Wayland idle inhibitor, and pauses
+breaktimer. Turning it off restores `dnd` to the value it had before rather
+than clearing it, so hand-set DND survives a presentation.
+
+breaktimer owns `$XDG_RUNTIME_DIR/breaktimer.state`. This module calls
+`breaktimer.sh pause|resume` and never writes that file: its daemon loop
+rewrites it on every phase change, and two writers would race.
+
+## Waybar
+
+`custom/presentation` reads `statusctl presentation watch`. It replaces
+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 check
+
+ ./test-statusctl.sh
+
+Points `XDG_RUNTIME_DIR` at a temporary directory, so it never touches live
+modes. Covers the file format, the atomic write, the toggle, the unknown-mode
+error and both watch states.
diff --git a/docs/superpowers/plans/2026-09-15-status-registry.md b/docs/superpowers/plans/2026-09-15-status-registry.md
index 614952c..c98769d 100644
--- a/docs/superpowers/plans/2026-09-15-status-registry.md
+++ b/docs/superpowers/plans/2026-09-15-status-registry.md
@@ -67,7 +67,7 @@ Do not re-probe these; they are measured, not assumed.
Two claims come from the documentation and have not been observed running. Record what actually happens in the task report, and add an `AGENTS.md` trap in Task 8 for whichever bites.
- `FileView` with `watchChanges: true` is documented to fire `fileChanged` on its own `setText()`. If so, the singleton sees its own writes and must not re-enter. Task 1 handles this with a value comparison rather than a re-entrancy flag; confirm the comparison is actually needed.
-- `IdleInhibitor` is documented to need a non-null `window` to do anything. Confirm that assigning the keepalive window is sufficient and that `hyprctl clients` count changes.
+- `IdleInhibitor` is documented to need a non-null `window` to do anything. Confirm that assigning the keepalive window is sufficient. The `hyprctl clients` count does not change: the inhibit lands on a layer surface, which that listing ignores, so check it behaviorally with a throwaway `hypridle -c` (Task 4 Step 4).
---
@@ -709,18 +709,33 @@ This needs the user's running shell, not the transient smoke instance. Ask the u
```bash
statusctl_path=desktop/modules/status/statusctl
+cat > /tmp/hypridle-check.conf <<'EOF'
+listener {
+ timeout = 8
+ on-timeout = touch /tmp/hypridle-fired
+}
+EOF
+rm -f /tmp/hypridle-fired
+hypridle -c /tmp/hypridle-check.conf &
+idle_pid=$!
+
bash "$statusctl_path" presentation set 1
sleep 1
echo "dnd now: $(bash "$statusctl_path" dnd get) (expect 1)"
~/bin/breaktimer.sh status
-hyprctl clients | grep -ci inhibit
+sleep 10
+test -e /tmp/hypridle-fired && echo "inhibitor FAILED: fired while presenting" || echo "inhibitor held"
+
bash "$statusctl_path" presentation set 0
sleep 1
echo "dnd now: $(bash "$statusctl_path" dnd get) (expect 0)"
~/bin/breaktimer.sh status
+sleep 10
+test -e /tmp/hypridle-fired && echo "inhibitor released, idle fired" || echo "inhibitor STUCK"
+kill "$idle_pid"
```
-Expected: `dnd now: 1`, breaktimer reports `paused`, the inhibitor count rises by one, then `dnd now: 0` and breaktimer reports `running`. Record the actual inhibitor counts in the task report; if the count does not change, the `IdleInhibitor` assumption is wrong and Task 8 gets a trap saying so.
+Expected: `dnd now: 1`, breaktimer reports `paused`, `inhibitor held`, then `dnd now: 0`, breaktimer reports `running` and `inhibitor released, idle fired`. The check is behavioral because `hyprctl clients | grep -ci inhibit` counts the per-toplevel `inhibitingIdle` field only; the inhibitor is asserted on the keepalive layer surface, which that listing does not cover, so the count never moves. Record the marker results in the task report.
- [ ] **Step 5: Commit**
@@ -1087,10 +1102,25 @@ Ask the user to click the waybar glyph and confirm the drawer's Status page swit
- [ ] **Step 4: Confirm only one inhibitor is asserted**
```bash
-hyprctl clients | grep -ci inhibit
+cat > /tmp/hypridle-check.conf <<'EOF'
+listener {
+ timeout = 8
+ on-timeout = touch /tmp/hypridle-fired
+}
+EOF
+rm -f /tmp/hypridle-fired
+hypridle -c /tmp/hypridle-check.conf &
+idle_pid=$!
+bash desktop/modules/status/statusctl presentation set 1
+sleep 10
+test -e /tmp/hypridle-fired && echo "inhibitor FAILED: fired while presenting" || echo "inhibitor held"
+bash desktop/modules/status/statusctl presentation set 0
+sleep 10
+test -e /tmp/hypridle-fired && echo "inhibitor released, idle fired" || echo "second inhibitor still held"
+kill "$idle_pid"
```
-Ask the user to run this with presentation mode off, then on. Expected: the count rises by exactly one, not two. Two would mean the built-in waybar module is still running.
+Ask the user to run this. Expected: `inhibitor held` while presentation is on, then `inhibitor released, idle fired` within a few seconds of turning it off. `second inhibitor still held` would mean the built-in waybar module is still running. `hyprctl clients | grep -ci inhibit` is not the check: a layer-surface inhibitor does not appear in that listing.
---