diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-18 17:20:38 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-18 17:20:38 +0200 |
| commit | f40f9b9fd1369f244c7a064a9ec083104f52a3f9 (patch) | |
| tree | 662185aede16dfd922ef49c888abdfaea0ecd486 /shared | |
| parent | 411d849bdbe41a6c9ae94f4ebe6dc3d798757ee7 (diff) | |
| download | quickshell-f40f9b9fd1369f244c7a064a9ec083104f52a3f9.tar.gz quickshell-f40f9b9fd1369f244c7a064a9ec083104f52a3f9.zip | |
feat(status): read breaktimer phase, state and countdown
The daemon already publishes these three files under XDG_RUNTIME_DIR and
waybar already consumes them, so a second consumer costs three FileViews and
invents no interface.
Read only, deliberately. The daemon loop rewrites state and phase on every
transition, so a second writer would race it, which is why presentation mode
has always called a verb rather than writing the file.
The countdown does not interpolate between the daemon's five second writes: a
local clock would drift against it and keep ticking while the daemon is frozen
outside the work window.
Diffstat (limited to 'shared')
| -rw-r--r-- | shared/Status.qml | 74 |
1 files changed, 74 insertions, 0 deletions
diff --git a/shared/Status.qml b/shared/Status.qml index be090b3..5d799e2 100644 --- a/shared/Status.qml +++ b/shared/Status.qml @@ -36,6 +36,23 @@ Singleton { // the wanted default of locking on. The row inverts it for the label. readonly property bool nolock: nolockFile.value + // Breaktimer, read only. The daemon publishes these three files and owns + // them; the shell calls verbs and never writes them, because the daemon + // loop rewrites state and phase on every transition. + // + // A stopped daemon is read from the state file rather than probed: both + // paths that end it, stop_daemon and the cleanup trap, write "stopped" + // there. A daemon lost to SIGKILL leaves a stale "running" and the drawer + // shows a frozen countdown, which is a visible wrong answer the start + // button resolves. That is cheaper than a liveness probe per repaint. + readonly property string btState: btStateFile.value + readonly property string btPhase: btPhaseFile.value + readonly property int btRemain: btRemainFile.value + + readonly property bool btRunning: root.btState === "running" + || root.btState === "paused" + readonly property bool btPaused: root.btState === "paused" + readonly property int activeCount: (root.dnd ? 1 : 0) + (root.presentation ? 1 : 0) + (root.nolock ? 1 : 0) @@ -121,6 +138,17 @@ Singleton { breakProc.running = true; } + // Seconds as m:ss. The daemon rewrites the remaining seconds every five + // seconds, its tick, so this counts down in five second steps and does + // not interpolate: a local one second timer would be a second clock + // drifting against the first, correcting itself with a visible jump, and + // it would keep ticking while the daemon is frozen outside the work + // window or paused. + function btCountdown() { + const s = Math.max(0, root.btRemain); + return Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0"); + } + // breaktimer owns its own state file; this only calls its verbs. Two // writers on that file would race with its daemon loop, which rewrites it // on every phase change. @@ -175,7 +203,53 @@ Singleton { onLoadFailed: mf.value = false } + // A daemon file holding a word. Same watch and same missing-file rule as + // ModeFile, without a write path: this side only reads. + component WordFile: FileView { + id: wf + + property string value: "stopped" + + function reparse() { + const t = wf.text().trim(); + if (t !== wf.value) wf.value = t; + } + + watchChanges: true + printErrors: false + onFileChanged: wf.reload() + onLoaded: wf.reparse() + // No file means no daemon, which is the stopped state, not an error. + onLoadFailed: wf.value = "stopped" + } + + // The remaining seconds. Validated as an integer rather than trusted: + // the file is written every tick and a read can catch it mid-write, and + // an empty string coerces to 0 silently while NaN would propagate into + // the countdown as "NaN:aN". + component SecondsFile: FileView { + id: sf + + property int value: 0 + + function reparse() { + const n = parseInt(sf.text().trim(), 10); + const v = (isNaN(n) || n < 0) ? 0 : n; + if (v !== sf.value) sf.value = v; + } + + watchChanges: true + printErrors: false + onFileChanged: sf.reload() + onLoaded: sf.reparse() + onLoadFailed: sf.value = 0 + } + ModeFile { id: dndFile; path: root.dir + "/status.dnd" } ModeFile { id: presFile; path: root.dir + "/status.presentation" } ModeFile { id: nolockFile; path: root.dir + "/status.nolock" } + + WordFile { id: btStateFile; path: root.dir + "/breaktimer.state" } + WordFile { id: btPhaseFile; path: root.dir + "/breaktimer.phase" } + SecondsFile { id: btRemainFile; path: root.dir + "/breaktimer.remain" } } |
