aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-18 16:49:49 +0200
committerDanilo M. <danix@danix.xyz>2026-09-18 16:49:49 +0200
commitcc132f5d02f1bbb544bd22ce4120df70be869069 (patch)
tree837fa6741523dc269e3028d122ddc1755cfead99 /docs
parentf0c01dd6bd4d3c629cc914cc960078b9b9de526d (diff)
downloadquickshell-cc132f5d02f1bbb544bd22ce4120df70be869069.tar.gz
quickshell-cc132f5d02f1bbb544bd22ce4120df70be869069.zip
docs(status): spec breaktimer control and a config file
The daemon already publishes its phase, state and countdown as files under XDG_RUNTIME_DIR, and the status module already shells out to it for presentation mode. Reading those files makes the traffic two-way for the cost of three FileViews, with no new interface to invent. Into status/ rather than a module of its own: breaktimer state is one more thing the desktop is doing, which is what that page already is, and Status.qml is already where breaktimer.sh is called from. The config file is the other half, and stands alone. Every tunable is a shell variable in a tracked script, so the installed copy and the committed one have already drifted over three sound paths pointing into a home directory. One sourced file fixes that with no parser and no format. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.