diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-15 12:25:21 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-15 12:25:21 +0200 |
| commit | fceeb5dc23c5e5a951f725e7682eab6814191c47 (patch) | |
| tree | e18cb3c9a699740922abf92bbdf9670817bda6ed | |
| parent | 180accd0a4dde00a25c1766c6b962809998d2ad7 (diff) | |
| download | quickshell-fceeb5dc23c5e5a951f725e7682eab6814191c47.tar.gz quickshell-fceeb5dc23c5e5a951f725e7682eab6814191c47.zip | |
docs(status): document the module and record its traps
A FileView fires its own fileChanged on setText, so a handler that writes
in response to a change loops unless it compares first. IdleInhibitor has
no window of its own and does nothing without one, so the singleton is
handed the keepalive window by shell.qml.
Both were read from the documentation while designing and confirmed while
implementing.
| -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. --- |
