diff options
| -rw-r--r-- | AGENTS.md | 17 | ||||
| -rw-r--r-- | desktop/modules/status/README.md | 59 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-09-15-status-registry.md | 40 |
3 files changed, 111 insertions, 5 deletions
@@ -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. --- |
