aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/specs/2026-09-18-breaktimer-status-design.md221
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.