From 4506646c66c5c5d05278fe2ada76b482e142f09e Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 30 Oct 2026 17:34:26 +0100 Subject: Remove the breaktimer card breaktimer is parked and no longer runs, so the card only ever showed "no breaktimer data". Drop the widget, its sampler and the texeci that ran it every 5s, the parser and its tests, the icon, and the doc examples that used it. Co-Authored-By: Claude Opus 5.5 --- DESIGN.md | 11 +++--- README.md | 5 ++- bin/breaktimer-sample.sh | 70 ------------------------------------- conky.conf.in | 5 ++- dashboard.lua | 2 -- lib/card.lua | 2 -- lib/data.lua | 74 --------------------------------------- test/fixtures/breaktimer_cache | 6 ---- test/test_data.lua | 62 --------------------------------- widgets/breaktimer.lua | 79 ------------------------------------------ 10 files changed, 9 insertions(+), 307 deletions(-) delete mode 100755 bin/breaktimer-sample.sh delete mode 100644 test/fixtures/breaktimer_cache delete mode 100644 widgets/breaktimer.lua diff --git a/DESIGN.md b/DESIGN.md index 80276cb..60a8a6e 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -38,7 +38,7 @@ Everything else exists to keep those two readable at any cell size. of a width fit and a height budget. `card.fit_unit` measures candidate rows and returns the largest base size that fits the width, and a row that would collide pulls the size down. Bigger cell, bigger type instead of empty space. - The `breaktimer` card does this; a card that cannot afford the + The `immich` card does this; a card that cannot afford the measuring pass may key off width with a clamped factor. - **Roles.** `FONT_MONO` for labels and row text, `FONT_HEAVY` for the big number, `FONT_UI` for prose (a condition description, a notice). Sizes within @@ -52,17 +52,16 @@ One language for every measured quantity, so state is read rather than decoded: | State | Role | Example | |---|---|---| -| fine | `ok` | breaktimer working | -| busy | `warning` | breaktimer paused | +| fine | `ok` | a quantity under its thresholds | +| busy | `warning` | a quantity past its lower threshold | | hot / full | `critical` | a quantity past its upper threshold | - Proportions use **25% / 75%**. - `warning` is the palette's middle role. The board should show all three states in normal use, or the language is not earning its place. - The big value's colour follows what it is: `body` for a plain number, the - threshold roles when it is a measured quantity, the phase's own role in - `breaktimer`, and `heading` when it names rather than measures (the calendar - month). + threshold roles when it is a measured quantity, and `heading` when it names + rather than measures (the calendar month). ## Glyphs diff --git a/README.md b/README.md index 0461f4b..07171c2 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,6 @@ Sampling is scheduled by conky itself: `conky.text` carries a threaded ${texeci 900 ~/.config/conky/bin/weather-fetch.sh} ${texeci 300 ~/.config/conky/bin/calendar-sample.sh} - ${texeci 5 ~/.config/conky/bin/breaktimer-sample.sh} ${texeci 1800 ~/.config/conky/bin/immich-fetch.sh} The calendar sampler runs every five minutes: khal costs about 200ms of Python @@ -244,8 +243,8 @@ cell is left sparse; size it off the height alone and a short cell shrinks the text to a whisper. The robust way is a measured fit: `card.fit_unit()` returns the largest base size at which every row still fits the width, and the widget takes the smaller of that and its height budget. Larger cell, larger type; a row -that would collide pulls the size back down. The breaktimer and -immich cards do this. A card that cannot afford the measuring pass can still key off width with +that would collide pulls the size back down. The immich card +does this. A card that cannot afford the measuring pass can still key off width with a clamp, `math.max(lo, math.min(hi, inner.w * k))`; the clock does. **Decide where the slack goes, and let one element absorb it.** The clock pins diff --git a/bin/breaktimer-sample.sh b/bin/breaktimer-sample.sh deleted file mode 100755 index 2bd6f85..0000000 --- a/bin/breaktimer-sample.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/bash -# Sample breaktimer's state into a cache file, for widgets/breaktimer.lua. -# -# Conky's Lua cannot run a process, so it cannot call `kill -0` to learn whether -# the daemon is alive, and shelling out every two seconds to ask breaktimer.sh -# `status` would parse prose rather than read the four small files the daemon -# already writes under $XDG_RUNTIME_DIR. Those files are the whole interface; -# this script is the one place that interprets them. - -set -u - -CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/udt" -CACHE="$CACHE_DIR/breaktimer.txt" - -umask 077 -mkdir -p "$CACHE_DIR" - -TMP="$CACHE.tmp.$$" -trap 'rm -f "$TMP"' EXIT - -RUNTIME="${XDG_RUNTIME_DIR:-/tmp}" -PID_FILE="$RUNTIME/breaktimer.pid" -STATE_FILE="$RUNTIME/breaktimer.state" -PHASE_FILE="$RUNTIME/breaktimer.phase" -REMAIN_FILE="$RUNTIME/breaktimer.remain" - -# The daemon's own tick. Used only to decide how stale the remain file may be -# before the clock counts as frozen. -TICK=5 - -running=no -if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null; then - running=yes -fi - -state=stopped -phase=stopped -remain=0 -if [ "$running" = yes ]; then - state=$(cat "$STATE_FILE" 2>/dev/null) - phase=$(cat "$PHASE_FILE" 2>/dev/null) - remain=$(cat "$REMAIN_FILE" 2>/dev/null) -fi -[ -z "$state" ] && state=stopped -[ -z "$phase" ] && phase=stopped -case "$remain" in ''|*[!0-9]*) remain=0 ;; esac - -# Outside the work-hours window the daemon leaves the phase at 'working' without -# decrementing, so the remain file stops being rewritten; its age is the honest -# signal that the clock is frozen. Three ticks of slack, so a sample landing a -# moment before the next tick never reads as frozen. A paused daemon freezes the -# file too but reports 'paused' and is handled on its own, and a hung daemon -# freezes it as well, which is a state worth showing rather than hiding. -frozen=no -if [ "$running" = yes ] && [ "$state" = running ] && [ "$phase" = working ] \ - && [ -f "$REMAIN_FILE" ]; then - mt=$(stat -c %Y "$REMAIN_FILE" 2>/dev/null || echo 0) - [ $(( $(date +%s) - mt )) -gt $((TICK * 3)) ] && frozen=yes -fi - -{ - echo "generated $(date +%s)" - echo "running $running" - echo "state $state" - echo "phase $phase" - echo "remain $remain" - echo "frozen $frozen" -} > "$TMP" - -mv -f "$TMP" "$CACHE" diff --git a/conky.conf.in b/conky.conf.in index 3e31272..995a63f 100644 --- a/conky.conf.in +++ b/conky.conf.in @@ -54,10 +54,9 @@ conky.config = { -- fires on schedule: verified with a probe config whose only text was an execi -- producing no output. This is what runs the weather, calendar and other -- samplers, and tying it to conky means nothing fetches while the dashboard is --- down. breaktimer runs at 5s, its daemon's own tick, so the countdown advances --- in the steps the daemon counts in. +-- down. -- -- texeci, not execi: execi runs in conky's main loop and blocks the draw until -- the script exits. At 0.1s while visible, a slow sampler or an Immich -- download stalls the scroll; texeci runs each in its own thread. -conky.text = [[${texeci 900 ~/.config/conky/bin/weather-fetch.sh}${texeci 300 ~/.config/conky/bin/calendar-sample.sh}${texeci 5 ~/.config/conky/bin/breaktimer-sample.sh}${texeci 1800 ~/.config/conky/bin/immich-fetch.sh}]] +conky.text = [[${texeci 900 ~/.config/conky/bin/weather-fetch.sh}${texeci 300 ~/.config/conky/bin/calendar-sample.sh}${texeci 1800 ~/.config/conky/bin/immich-fetch.sh}]] diff --git a/dashboard.lua b/dashboard.lua index 5545ef1..d591824 100644 --- a/dashboard.lua +++ b/dashboard.lua @@ -24,8 +24,6 @@ local layout = { { widget = 'now', col = 1, row = 7, w = 3, h = 3.75 }, -- In the board's open middle column, left of immich. { widget = 'calendar', col = 2.5, row = 1, w = 3, h = 6 }, - -- Clustered near now and clock - { widget = 'breaktimer', col = 4, row = 7, w = 2, h = 3 }, -- Centred on the board. Twice as many rows as columns because cells are -- about twice as wide as tall, so this span comes out square. { widget = 'immich', col = 5.5, row = 1, w = 4, h = 6 }, diff --git a/lib/card.lua b/lib/card.lua index 3dd4b12..221d8d2 100644 --- a/lib/card.lua +++ b/lib/card.lua @@ -224,7 +224,6 @@ end -- fontconfig alone: a missing glyph draws as an invisible blank and a wrong -- codepoint as a plausible neighbour, so neither fails visibly. M.ICON = { - breaktimer = '\u{F13AB}', -- timer calendar = '\u{F0E17}', -- calendar-month immich = '\u{F02F9}', -- image-multiple hourly = '\u{F0150}', -- clock-outline @@ -331,7 +330,6 @@ end -- -- Returns the y where the drawn content ends and S, so a card can put its -- variable band in what is left and size the band's type off the same S. --- The proportions are the breaktimer card's, which this generalises. function M.standard(cr, inner, colors, spec) local LABEL_F, BIG_F, ROW_F = 0.30, 1.0, 0.32 local rows = spec.rows or {} diff --git a/lib/data.lua b/lib/data.lua index 818bbf1..dca039a 100644 --- a/lib/data.lua +++ b/lib/data.lua @@ -162,80 +162,6 @@ function M.month_shape(year, month) return last.day, (first.wday + 5) % 7 + 1 end --- The breaktimer cache written by bin/breaktimer-sample.sh. --- --- Returns a table describing the daemon, or nil when the cache is absent or --- carries no 'generated' key (the sampler never ran). The display strings are --- derived here rather than in the widget so the phase-to-word and --- phase-to-colour decisions can be tested without Cairo: --- --- { running, state, phase, frozen, remain, --- countdown = '24:31' or '--', --- phase_label = 'working' | 'micro-pausa' | 'pausa lunga' | 'in pausa' --- | 'outside working hours' | 'fermo', --- next = 'pausa' | 'lavoro' | '--', --- role = 'ok' | 'heading' | 'highlight' | 'warning' | 'label' } --- --- `frozen` is the sampler's read of whether the clock is ticking: the phase is --- 'working' but the remain file has stopped being rewritten. Outside the work --- window that is normal and reads as such; a hung daemon looks the same, and --- should. --- --- The countdown leads to the next phase, but the daemon keeps the cycle count --- in memory and never writes it, so whether the NEXT break is a micro or a long --- one is not knowable here. 'pausa' is the honest answer; the phase word carries --- the rest. -function M.breaktimer_parse(text) - if type(text) ~= 'string' then return nil end - local kv = M.kv_parse(text) - if not kv.generated then return nil end - - local running = kv.running == 'yes' - local state = kv.state or 'stopped' - local phase = kv.phase or 'stopped' - local remain = tonumber(kv.remain) or 0 - if remain < 0 then remain = 0 end - - local out = { - running = running, - state = state, - phase = phase, - frozen = kv.frozen == 'yes', - remain = remain, - } - - -- Not running and stopped are the same thing on the card. The sampler's - -- liveness check is what turns a dead PID into this reading: the state file - -- can still say 'running' after a crash or a kill -9. - if not running or state == 'stopped' then - out.countdown = '--' - out.phase_label = 'fermo' - out.next = '--' - out.role = 'label' - return out - end - - out.countdown = string.format('%d:%02d', math.floor(remain / 60), remain % 60) - - if state == 'paused' then - out.phase_label = 'in pausa' - out.role = 'warning' - elseif out.frozen then - out.phase_label = 'outside working hours' - out.role = 'label' - else - local labels = { working = 'working', breaking = 'micro-pausa', - longbreak = 'pausa lunga' } - local roles = { working = 'ok', breaking = 'heading', - longbreak = 'highlight' } - out.phase_label = labels[phase] or phase - out.role = roles[phase] or 'value' - end - - out.next = (phase == 'working') and 'pausa' or 'lavoro' - return out -end - -- The Immich memories index written by bin/immich-fetch.sh. -- -- Returns a list of { id, year, taken = { y, m, d, H, M }, place }, plus diff --git a/test/fixtures/breaktimer_cache b/test/fixtures/breaktimer_cache deleted file mode 100644 index 23a147a..0000000 --- a/test/fixtures/breaktimer_cache +++ /dev/null @@ -1,6 +0,0 @@ -generated 1758200000 -running yes -state running -phase working -remain 1471 -frozen no diff --git a/test/test_data.lua b/test/test_data.lua index 01a55cc..c33e5ae 100644 --- a/test/test_data.lua +++ b/test/test_data.lua @@ -125,68 +125,6 @@ assert(ndays == 30 and first == 2, 'september 2026, got ' .. ndays .. '/' .. fir assert((data.month_shape(2024, 2)) == 29, 'leap february has 29 days') assert((data.month_shape(2026, 12)) == 31, 'december rolls over correctly') --- === breaktimer =========================================================== --- The phase-to-word and phase-to-colour decisions live in breaktimer_parse so --- they can be checked here; the card only draws the result. -local bt = data.breaktimer_parse(read('test/fixtures/breaktimer_cache')) -assert(bt, 'breaktimer cache parses') -assert(bt.countdown == '24:31', 'countdown, got ' .. tostring(bt.countdown)) -assert(bt.phase_label == 'working', 'phase label, got ' .. tostring(bt.phase_label)) -assert(bt.next == 'pausa', 'next during work is a break, got ' .. tostring(bt.next)) -assert(bt.role == 'ok', 'working is ok, got ' .. tostring(bt.role)) - --- Every phase has its own word and colour role. Build caches rather than files: --- the sampler's output is six plain lines and the parse is the seam that --- matters. -local function bt_cache(fields) - local out = { 'generated 1', 'running ' .. (fields.running or 'yes'), - 'state ' .. (fields.state or 'running'), - 'phase ' .. (fields.phase or 'working'), - 'remain ' .. (fields.remain or 60), - 'frozen ' .. (fields.frozen or 'no') } - return table.concat(out, '\n') .. '\n' -end - -local b - -b = data.breaktimer_parse(bt_cache({ phase = 'breaking', remain = 125 })) -assert(b.phase_label == 'micro-pausa' and b.role == 'heading' and b.next == 'lavoro', - 'micro-pause: ' .. b.phase_label .. '/' .. b.role .. '/' .. b.next) -assert(b.countdown == '2:05', 'micro-pause countdown, got ' .. b.countdown) - -b = data.breaktimer_parse(bt_cache({ phase = 'longbreak', remain = 600 })) -assert(b.phase_label == 'pausa lunga' and b.role == 'highlight' and b.next == 'lavoro', - 'long pause: ' .. b.phase_label .. '/' .. b.role) - --- Paused freezes the number, but the phase underneath still decides what the --- countdown leads to. -b = data.breaktimer_parse(bt_cache({ state = 'paused', phase = 'working' })) -assert(b.phase_label == 'in pausa' and b.role == 'warning' and b.next == 'pausa', - 'paused: ' .. b.phase_label .. '/' .. b.role .. '/' .. b.next) - --- Frozen: the clock stopped while the phase stayed 'working'. Outside working --- hours, or a hung daemon; both read the same. -b = data.breaktimer_parse(bt_cache({ frozen = 'yes' })) -assert(b.phase_label == 'outside working hours' and b.role == 'label', - 'frozen: ' .. b.phase_label .. '/' .. b.role) -assert(b.next == 'pausa', 'frozen working still leads to a break') - --- A dead daemon: the sampler's liveness check sets running no even when the --- state file was left saying running. -b = data.breaktimer_parse(bt_cache({ running = 'no' })) -assert(b.countdown == '--' and b.phase_label == 'fermo' and b.next == '--' - and b.role == 'label', 'stopped reads as fermo, got ' .. b.phase_label) - --- A negative remain must clamp, not print '-1:59'. -b = data.breaktimer_parse(bt_cache({ remain = -5 })) -assert(b.countdown == '0:00', 'negative remain clamps, got ' .. b.countdown) - --- No 'generated' key means the sampler never ran, which is different from a --- stopped daemon (which still writes a cache). -assert(data.breaktimer_parse('running no\n') == nil, 'no generated key: nil') -assert(data.breaktimer_parse('') == nil, 'empty cache: nil') -assert(data.breaktimer_parse(nil) == nil, 'nil cache: nil') - -- immich_parse: one entry per photo line, place optional, garbage skipped. local im = data.immich_parse(read('test/fixtures/immich_index')) assert(im and #im == 3, 'immich photo count, got ' .. tostring(im and #im)) diff --git a/widgets/breaktimer.lua b/widgets/breaktimer.lua deleted file mode 100644 index fa8649b..0000000 --- a/widgets/breaktimer.lua +++ /dev/null @@ -1,79 +0,0 @@ --- Breaktimer: the phase of breaktimer.sh and the time left in it. --- --- Everything comes from a cache file written by bin/breaktimer-sample.sh: --- conky's Lua cannot run a process, so it can neither call `kill -0` to learn --- whether the daemon is alive nor ask breaktimer.sh itself. The sampler owns --- that; this card owns only how the state looks. --- --- The countdown is the big value, coloured by phase: green for a work block, --- blue during a micro-pause, purple during a long one, yellow when paused, and --- the dim label colour when the clock is frozen. That is the same phase-to- --- colour language the old Waybar module used, mapped onto the palette roles. - -local card = require 'lib.card' -local data = require 'lib.data' - -local CACHE = (os.getenv('XDG_CACHE_HOME') or (os.getenv('HOME') .. '/.cache')) - .. '/udt/breaktimer.txt' - -local M = {} - -function M.draw(cr, rect, colors) - local inner = card.card(cr, rect, colors) - local function clamp(v, lo, hi) return math.max(lo, math.min(hi, v)) end - local LABEL_F, BIG_F, ROW_F = 0.30, 1.0, 0.32 - - local bt = data.breaktimer_parse(data.slurp(CACHE) or '') - - -- No cache at all: the sampler has not run. Name it, as every cached card - -- does, rather than leaving a blank cell that is indistinguishable from a - -- crash. A stopped daemon is NOT this case: the sampler still writes a cache, - -- with running no, which the card draws as 'fermo'. - if not bt then - card.notice(cr, inner, colors, 'BREAKTIMER', 'no breaktimer data', - 'bin/breaktimer-sample.sh') - return - end - - local rows = { - { 'PHASE', bt.phase_label }, - { 'NEXT', bt.next }, - } - - -- Fluid type: the height budget grows the content to fill the cell, the - -- measured width fit pulls it back where the phase label would overrun. - local groups = { - { { card.title_str(card.ICON.breaktimer, 'BREAKTIMER'), card.FONT_MONO, LABEL_F }, - { bt.countdown, card.FONT_HEAVY, BIG_F } }, - } - for _, r in ipairs(rows) do - groups[#groups + 1] = { { r[1], card.FONT_MONO, ROW_F }, - { tostring(r[2]), card.FONT_MONO, ROW_F } } - end - local S = clamp(math.min(inner.h * 0.94 / (1.5 + #rows * 0.66), - card.fit_unit(cr, inner.w * 0.96, groups, 100)), 10, 72) - local label_size = S * LABEL_F - local row_size = S * ROW_F - - card.font(cr, card.FONT_HEAVY, S * BIG_F, false) - card.rgba(cr, colors[bt.role] or colors.value) - card.text_right(cr, inner.x + inner.w, inner.y + S * BIG_F, bt.countdown) - - card.font(cr, card.FONT_MONO, label_size, false) - card.title(cr, inner.x, inner.y + label_size, card.ICON.breaktimer, - 'BREAKTIMER', colors) - - local ey = inner.y + S * BIG_F + row_size * 1.6 - local step = row_size * 2.0 - card.font(cr, card.FONT_MONO, row_size, false) - for _, r in ipairs(rows) do - if ey + step > inner.y + inner.h then break end - card.rgba(cr, colors.label) - card.text(cr, inner.x, ey, r[1]) - card.rgba(cr, colors.value) - card.text_right(cr, inner.x + inner.w, ey, tostring(r[2])) - ey = ey + step - end -end - -return M -- cgit v1.2.3