diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/specs/2026-09-18-breaktimer-status-design.md | 221 |
1 files changed, 221 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-09-18-breaktimer-status-design.md b/docs/superpowers/specs/2026-09-18-breaktimer-status-design.md new file mode 100644 index 0000000..364397e --- /dev/null +++ b/docs/superpowers/specs/2026-09-18-breaktimer-status-design.md @@ -0,0 +1,221 @@ +# Breaktimer in the Status Module + +The break reminder daemon already runs every day and already talks to this +shell in one direction: presentation mode calls `breaktimer.sh pause`. This +makes the traffic two-way. The drawer reads the daemon's phase and countdown, +and offers the verbs the waybar module's mouse bindings offer today. + +It also gives the daemon a config file, which it has never had. + +Two repos change. They are independent: the config file is useful with no +drawer, and the drawer works against the unmodified script. Neither half +blocks the other. + +## The daemon as it stands + +`breaktimer.sh` is a bash daemon holding a three-phase state machine, working +to breaking or longbreak and back. It publishes four files under +`$XDG_RUNTIME_DIR`: + + breaktimer.pid daemon pid + breaktimer.state running | paused | stopped + breaktimer.phase working | breaking | longbreak | stopped + breaktimer.remain seconds left in the current phase + +and takes seven verbs: `start`, `stop`, `restart`, `pause`, `resume`, +`toggle`, `status`. + +The files are already the interface, the same arrangement the status registry +and the notification daemon use. `waybar-breaktimer.sh` is a consumer of them +and nothing more. Quickshell becomes a second consumer, and the waybar module +keeps working unchanged beside it. + +## Part one: a config file + +### Why + +Every tunable is a shell variable at the top of the script: durations, the +work-hours window, urgencies, and six sound paths. Changing one means editing +a tracked file, and the repository copy and the installed copy have already +drifted apart because of it. The installed `~/bin/breaktimer.sh` points +`SOUND_MICRO`, `SOUND_LONG` and `SOUND_BACK` at files under a home directory; +the committed copy leaves all three empty. That drift is the whole argument: +personal values are living as uncommittable edits to a shared script, and the +next `cp` from the repository silently reverts them. + +### The change + +One line, after the configuration block: + +```bash +[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/breaktimer.conf" ] && \ + . "${XDG_CONFIG_HOME:-$HOME/.config}/breaktimer.conf" +``` + +The defaults stay in the script exactly as they are. The file overrides them +and need only name what it changes. Bash sources files natively, so there is +no parser, no format, and no validation code: a syntax error in the config is +a bash error at start, reported on stderr, which is the correct and loudest +possible failure. + +The file is read once, where the script reads its variables, which means the +`run` subcommand reads it too when `start` re-execs itself through `setsid`. + +### Scope of a change + +Config is read at daemon start. Editing it while the daemon runs changes +nothing until `breaktimer.sh restart`. This is stated in the README rather +than engineered around: a re-read each tick would have to decide what happens +to a phase already counting down under the old duration, and there is no +answer to that which is worth the code. + +### What moves into the file + +Nothing is moved by this project. The script keeps its defaults; the user's +three sound paths move into their own config file, which is not in the +repository. The effect is that the committed script stops carrying a home +directory in it, and the installed copy stops differing from the committed +one. + +### Documentation + +The README gains a Configuration section naming the overridable variables and +showing a short example. The existing Sounds section, which today tells the +reader to edit the script, points at the config file instead. + +## Part two: the drawer + +### Where it goes + +Into the existing `status` module, not a module of its own. Breaktimer state +is one more thing the desktop is currently doing, alongside do not disturb, +presentation and the screen lock, and the status page is already a column of +exactly that. A separate module would mean a second tile for one row of +content, and `shared/Status.qml` is already the thing that shells out to +`breaktimer.sh`. + +### Reading + +`shared/Status.qml` gains three `FileView`s over `breaktimer.state`, +`breaktimer.phase` and `breaktimer.remain`, exposed as: + + btState string running | paused | stopped + btPhase string working | breaking | longbreak | stopped + btRemain int seconds, 0 when absent or unparseable + +They follow `ModeFile`'s pattern, with `watchChanges`, `printErrors: false`, +a reload on change and a missing file treated as the off state rather than an +error. They differ in parsing a word or an integer instead of `0` or `1`, and +in being read-only: nothing in the shell writes these files. That rule already +exists in the singleton and is the reason presentation mode calls a verb +rather than writing `breaktimer.state`, because the daemon loop rewrites that +file on every phase change and two writers would race. + +`btRemain` guards its parse. An empty or malformed file reads 0, the same +defensive shape `waybar-breaktimer.sh` uses, and the same class of check the +notmuch count uses elsewhere in this shell. + +### Detecting a stopped daemon + +The waybar module tests liveness with `kill -0` on the pid file. QML cannot +send a signal, and shelling out per repaint to learn a fact the state file +already carries would be absurd. + +`breaktimer.state` is sufficient. Both paths that end the daemon write +`stopped` to it: `stop_daemon` for an explicit stop, and the `cleanup` trap +for a `TERM`. The one case they miss is a daemon killed with `KILL` or lost to +a crash, which leaves a stale `running`. The drawer would then show a frozen +countdown, which is a visible and self-explaining wrong answer, and one the +user resolves with the start button. No liveness probe for that. + +### Countdown granularity + +The daemon rewrites `breaktimer.remain` every five seconds, its tick. The +drawer therefore counts down in five second steps. + +It does not interpolate. A local one-second timer counting between the +daemon's writes would be a second clock drifting against the first, correcting +itself with a visible jump every five seconds, and it would keep ticking while +the daemon is frozen outside the work window or paused. The panel is opened +occasionally, not watched, and a five second step on a thirty minute block is +not a legibility problem. The tile has the same granularity for the same +reason. + +### The row + +`BreakRow.qml`, one new file in `desktop/modules/status/`, a sibling of +`LockRow.qml` and `SnoozeRow.qml`, appended to `StatusPage.qml` behind a +divider in the same shape as the rows already there. + +It shows the phase in words and the countdown, and offers two controls: + +| control | when | calls | +|---|---|---| +| pause/resume switch | daemon running | `breaktimer.sh toggle` | +| start/stop button | always | `breaktimer.sh start` or `stop` | + +The switch reflects `btState === "paused"`. It carries the same +`onValueChanged` resync `StatusRow` carries, because the shared `Switch` +writes `checked` on click and drops the declarative binding, so an external +change, from the waybar module's own bindings or a terminal, has to move it +back into agreement. + +While the daemon is stopped the row shows that, the switch is disabled, and +the button reads Start. + +Phase wording is English, matching the rest of the drawer. The daemon's own +notifications stay Italian; they are the daemon's, not the shell's, and this +project does not touch them. + +### The tile + +`StatusTile.qml` prints one line chosen by priority. Breaktimer joins the +chain below every mode, so an active mode still owns the line and breaktimer +replaces only the idle `All clear`: + + presentation -> "Presenting" + dnd -> "Do not disturb" + nolock -> "No lock" + running -> "Break in 12:35" | "Back in 2:10" | "Paused" + otherwise -> "All clear" + +The working phase counts to the next break, the two break phases count to the +return, and a paused daemon shows no number because the number is frozen and a +frozen countdown reads as a bug. A stopped daemon falls through to `All +clear`, which is true: nothing is being tracked. + +### Verbs + +`runBreaktimer(verb)` already exists in the singleton and already logs a +non-zero exit. It gains callers, not code. Its `Process` is reused across +verbs, which means each call must set `running = false` immediately before +`running = true`, because assigning `true` to an already-running `Process` +does nothing. The function does this today and keeps doing it. + +## Not in this project + +**Config editing from the drawer.** The deliverable is the config file, and it +is hand-edited. A QML form writing bash syntax has to quote correctly and to +decide what a malformed field does to a daemon that will not start, which is +its own project, and the durations change about once a year. + +**A `breaktimerctl`.** The script is already its own CLI with seven verbs. +Wrapping it would add a layer that translates nothing. + +**Live config reload.** See the scope note above. + +**Touching the waybar module.** It reads the same files and keeps working. The +two consumers do not know about each other. + +## Checks + +**`breaktimer` repo.** One script in the style of `test-statusctl.sh`, pointing +`XDG_CONFIG_HOME` and `XDG_RUNTIME_DIR` at temporary directories so it never +touches a live daemon or a real config. It covers the two cases that matter: +a config file overriding a value takes effect, and a missing config file still +yields the built-in defaults. The second is the regression that would hurt, +since every user without a config file is exercising it. + +**quickshell.** Visual, and the user has the screen. The daemon verbs are +observable without the drawer: `breaktimer.sh status` prints state, phase and +remaining seconds, so a control that does not work is visible in one command. |
