aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--DESIGN.md11
-rw-r--r--README.md5
-rwxr-xr-xbin/breaktimer-sample.sh70
-rw-r--r--conky.conf.in5
-rw-r--r--dashboard.lua2
-rw-r--r--lib/card.lua2
-rw-r--r--lib/data.lua74
-rw-r--r--test/fixtures/breaktimer_cache6
-rw-r--r--test/test_data.lua62
-rw-r--r--widgets/breaktimer.lua79
10 files changed, 9 insertions, 307 deletions
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