diff options
| -rw-r--r-- | DESIGN.md | 12 | ||||
| -rw-r--r-- | README.md | 49 | ||||
| -rwxr-xr-x | bin/cache-sample.sh | 33 | ||||
| -rwxr-xr-x | bin/slackware-sample.sh | 52 | ||||
| -rw-r--r-- | conky.conf.in | 4 | ||||
| -rw-r--r-- | dashboard.lua | 2 | ||||
| -rw-r--r-- | lib/card.lua | 4 | ||||
| -rw-r--r-- | lib/data.lua | 48 | ||||
| -rw-r--r-- | test/fixtures/du_output | 5 | ||||
| -rw-r--r-- | test/fixtures/slackware_cache | 5 | ||||
| -rw-r--r-- | test/test_data.lua | 38 | ||||
| -rw-r--r-- | widgets/breaktimer.lua | 2 | ||||
| -rw-r--r-- | widgets/cache.lua | 83 | ||||
| -rw-r--r-- | widgets/slackware.lua | 117 |
14 files changed, 33 insertions, 421 deletions
@@ -20,7 +20,7 @@ bottom: - **Variable band.** One element may absorb the height left over, chosen per card: the daylight arc in `weather`. Or the type itself fills the cell. -Failure states keep the chrome and draw a notice in it ("no cache data", `--` +Failure states keep the chrome and draw a notice in it ("no calendar data", `--` for a missing sensor). An empty cell is indistinguishable from a crashed widget, so a card never draws nothing. A missing sensor is `--`, never `0`: zero is a legitimate reading. @@ -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 `cache` card does this; a card that cannot afford the + The `breaktimer` 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,11 +52,11 @@ One language for every measured quantity, so state is read rather than decoded: | State | Role | Example | |---|---|---| -| fine | `ok` | a fresh cache, a current changelog | -| busy | `warning` | a cache past 25% | -| hot / full | `critical` | a cache past 75%, a stale changelog | +| fine | `ok` | breaktimer working | +| busy | `warning` | breaktimer paused | +| hot / full | `critical` | a quantity past its upper threshold | -- Proportions (cache usage) use **25% / 75%**. +- 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 @@ -63,55 +63,39 @@ in the source, so no real location appears in committed files. ## System widgets -Three cards cover the machine itself: - -- **Cache**: `~/.cache` total and its four largest subdirectories. -- **Slackware**: distribution version, package count, kernel, install age, and - time since the last `slackpkg` ChangeLog update. See below. - CPU, RAM, temperatures, the GPU, disks, network and the public address used to be cards here. They moved to the performance panel of the quickshell island (`desktop/island/` in the quickshell repo), which also warns from the status -pill. The island keeps writing `~/.cache/udt/pubip.txt`. +pill. The island keeps writing `~/.cache/udt/pubip.txt`. The cache and +Slackware cards followed them to quickshell. The updates card, a copy of nvtable's table, moved to the updates page of the quickshell drawer (`desktop/modules/updates/`), which sorts each pending update into the action it needs and covers Slackware itself. -The cache card reads a cache file that a sampler writes; it never calls `du` -itself. Sampling is scheduled by conky itself: `conky.text` carries -a threaded `${texeci}` per sampler, even though that block renders nothing under Cairo. +## Samplers + +Cards read cache files that samplers write; none shells out from a draw. +Sampling is scheduled by conky itself: `conky.text` carries a threaded +`${texeci}` per sampler, even though that block renders nothing under Cairo. ${texeci 900 ~/.config/conky/bin/weather-fetch.sh} - ${texeci 900 ~/.config/conky/bin/cache-sample.sh} - ${texeci 900 ~/.config/conky/bin/slackware-sample.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 cache and Slackware samplers run every 15 minutes, because `du -sh -~/.cache` walks the tree and takes about 100ms warm, which is fine on a slow -cycle and unthinkable on the 2-second draw. The calendar -sampler runs every five minutes: khal costs about 200ms of Python startup, -which rules out calling it from a draw, and the cache has to notice both a -vdirsyncer run and the day rolling over. Tying them to -conky means nothing samples while the dashboard is down. +The calendar sampler runs every five minutes: khal costs about 200ms of Python +startup, which rules out calling it from a draw, and the cache has to notice +both a vdirsyncer run and the day rolling over. Tying them to conky means +nothing samples while the dashboard is down. To run a sampler by hand: - ./bin/cache-sample.sh; echo "exit: $?" - ./bin/slackware-sample.sh; echo "exit: $?" ./bin/calendar-sample.sh; echo "exit: $?" All print nothing and exit 0 on success, and all write a temp file and rename it over the target, so the widget never reads a half-written file. -### Slackware - -The big top-right value is the time since `/var/lib/slackpkg/ChangeLog.txt` -last changed, coloured `ok` under 24 hours, `warning` to a week, `critical` -beyond, the same three-role language the cache card uses. Below it: distribution version, package count, kernel, and install age -(the root filesystem's birth time, where the filesystem records one). - ### Calendar The month grid with the week's appointments under it. A day carrying an event @@ -186,9 +170,8 @@ Anything with a comfortable range and an uncomfortable one is coloured by the same three roles, so a card reads without learning a new scheme: `ok` below the first threshold, `warning` between them, `critical` above the second. -Cache usage uses 25% and 75%. The Slackware card's ChangeLog age uses the same -three roles on an hour scale (24 / 168) instead of a percentage. These are -starting values, and sit in one table per widget so they are easy to retune. +Thresholds are starting values, and sit in one table per widget so they are +easy to retune. ## Changing the layout @@ -252,8 +235,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 cache card does -this. A card that cannot afford the measuring pass can still key off width with +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 a clamp, `math.max(lo, math.min(hi, inner.w * k))`; the clock and weather cards do. diff --git a/bin/cache-sample.sh b/bin/cache-sample.sh deleted file mode 100755 index 202bc63..0000000 --- a/bin/cache-sample.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -# Sample ~/.cache sizes into a cache file, for widgets/cache.lua. -# -# du walks the tree and costs about 100ms warm, which is fine every 15 minutes -# and unthinkable on a 2-second draw. The old conky config used the same -# interval for the same reason. - -set -u - -CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/udt" -CACHE="$CACHE_DIR/cachesize.txt" -TARGET="${XDG_CACHE_HOME:-$HOME/.cache}" - -umask 077 -mkdir -p "$CACHE_DIR" - -TMP="$CACHE.tmp.$$" -trap 'rm -f "$TMP"' EXIT - -# Total first, then the four largest children. `sort -h` compares human sizes -# numerically; the Lua side converts them again because it needs the ratio to -# the total, not just the order. -{ - du -sh "$TARGET" 2>/dev/null - du -sh "$TARGET"/* 2>/dev/null | sort -hr | head -4 -} > "$TMP" - -if [ ! -s "$TMP" ]; then - echo "cache-sample: du produced nothing" >&2 - exit 1 -fi - -mv -f "$TMP" "$CACHE" diff --git a/bin/slackware-sample.sh b/bin/slackware-sample.sh deleted file mode 100755 index 066c520..0000000 --- a/bin/slackware-sample.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -# Sample Slackware facts into a cache file, for widgets/slackware.lua. -# -# Conky's Lua has no lfs, so it cannot stat a file for an mtime, and counting -# 2800 package files every two seconds would be a directory walk per draw. -# Both belong here. - -set -u - -CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/udt" -CACHE="$CACHE_DIR/slackware.txt" - -umask 077 -mkdir -p "$CACHE_DIR" - -TMP="$CACHE.tmp.$$" -trap 'rm -f "$TMP"' EXIT - -VERSION=$(cat /etc/slackware-version 2>/dev/null || echo unknown) - -# /var/log/packages is a symlink to /var/lib/pkgtools/packages. GNU ls follows -# it either way, so the trailing slash is belt and braces rather than the load -# bearing part; what would break this is adding -d, which lists the link itself -# and reports one package. -# -# The real trap is `ls` being aliased: the user's shell aliases it to `ls -lh`, -# and a long-format listing counts a "total" header line as a package. This -# script is not interactive so aliases do not apply, but do not move this -# counting into anything that is. -PACKAGES=$(ls -1 /var/log/packages/ 2>/dev/null | wc -l) - -# Epochs, never formatted dates. The shell function this replaces rebuilds a -# date string from `ls -l` output with the year hardcoded, which breaks every -# January and on any file older than six months, when ls prints a year instead -# of a time and the field offsets shift. -CHANGELOG=$(stat -c %Y /var/lib/slackpkg/ChangeLog.txt 2>/dev/null || echo 0) - -# Filesystem birth time: the install date. Not every filesystem records it, -# and those that do not report 0, which the widget shows as '--'. -BIRTH=$(stat -c %W / 2>/dev/null || echo 0) - -KERNEL=$(uname -r) - -{ - echo "version $VERSION" - echo "packages $PACKAGES" - echo "changelog $CHANGELOG" - echo "birth $BIRTH" - echo "kernel $KERNEL" -} > "$TMP" - -mv -f "$TMP" "$CACHE" diff --git a/conky.conf.in b/conky.conf.in index 1bec680..3e31272 100644 --- a/conky.conf.in +++ b/conky.conf.in @@ -52,7 +52,7 @@ conky.config = { -- Cairo output covers conky.text, so nothing here is visible. The execi still -- fires on schedule: verified with a probe config whose only text was an execi --- producing no output. This is what runs the weather, disks and cache +-- 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. @@ -60,4 +60,4 @@ conky.config = { -- 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 900 ~/.config/conky/bin/cache-sample.sh}${texeci 900 ~/.config/conky/bin/slackware-sample.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 5 ~/.config/conky/bin/breaktimer-sample.sh}${texeci 1800 ~/.config/conky/bin/immich-fetch.sh}]] diff --git a/dashboard.lua b/dashboard.lua index fd3e2e5..53b0ab8 100644 --- a/dashboard.lua +++ b/dashboard.lua @@ -26,8 +26,6 @@ local layout = { { widget = 'calendar', col = 2.5, row = 1, w = 3, h = 6 }, -- Clustered near weather and clock { widget = 'breaktimer', col = 4, row = 7, w = 2, h = 3 }, - { widget = 'cache', col = 12, row = 7, w = 2, h = 3 }, - { widget = 'slackware', col = 12, row = 10, 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 0a93c88..0869ce9 100644 --- a/lib/card.lua +++ b/lib/card.lua @@ -215,8 +215,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 = { - cache = '\u{F0ABA}', -- folder-clock - slackware = '\u{F318}', -- linux-slackware breaktimer = '\u{F13AB}', -- timer calendar = '\u{F0E17}', -- calendar-month immich = '\u{F02F9}', -- image-multiple @@ -315,7 +313,7 @@ end -- -- This counts codepoints, not rendered width, so it does not account for a -- double-width CJK glyph. That is the right trade here: the strings it cuts --- are cache directory names and hardware model strings. Measure with +-- are calendar titles and place names. Measure with -- M.measure when true rendered width matters. function M.truncate(s, limit) s = tostring(s or '') diff --git a/lib/data.lua b/lib/data.lua index 6b32f07..1b4c64f 100644 --- a/lib/data.lua +++ b/lib/data.lua @@ -16,55 +16,13 @@ function M.slurp(path) return s end --- Human-readable size to bytes: '1.1G' -> 1181116006. --- --- Needed because du -sh is what the sampler runs (its output is also what the --- card displays), and 'biggest' has to be decided numerically: sorted as --- strings, '245M' beats '1.1G'. -local SUFFIX = { K = 1024, M = 1024^2, G = 1024^3, T = 1024^4, P = 1024^5 } - -function M.human_bytes(s) - if type(s) ~= 'string' then return nil end - local n, suf = s:match('^%s*([%d%.]+)%s*([KMGTP]?)') - n = tonumber(n) - if not n then return nil end - return n * (SUFFIX[suf] or 1) -end - --- Parse `du -sh` output: the first line is the total, the rest are children --- already sorted largest-first by the sampler. Returns nil when there is --- nothing usable, so the widget can draw its "no data" state. -function M.du_parse(text) - if type(text) ~= 'string' or text == '' then return nil end - local lines = {} - for line in text:gmatch('[^\n]+') do lines[#lines + 1] = line end - if #lines == 0 then return nil end - - local function entry(line) - local label, path = line:match('^(%S+)%s+(.+)$') - if not label then return nil end - return { label = label, bytes = M.human_bytes(label) or 0, - name = path:match('([^/]+)/?$') or path, path = path } - end - - local total = entry(lines[1]) - if not total then return nil end - local items = {} - for i = 2, #lines do - local e = entry(lines[i]) - if e then items[#items + 1] = e end - end - return { total = total, items = items } -end - -- Parse 'key value' lines into a table of strings. -- --- Values stay strings and are converted by the caller. The sampler writes --- epochs as integers and the widget decides whether an age reads as hours or --- days; a parser that guessed would have to know that. +-- Values stay strings and are converted by the caller; a parser that guessed +-- types would have to know what each key means. -- -- Only the first space separates, so a value keeps its own spaces: --- 'version Slackware 15.0+' yields 'Slackware 15.0+', not 'Slackware'. +-- 'title Long break' yields 'Long break', not 'Long'. function M.kv_parse(text) local out = {} if type(text) ~= 'string' then return out end diff --git a/test/fixtures/du_output b/test/fixtures/du_output deleted file mode 100644 index a45b787..0000000 --- a/test/fixtures/du_output +++ /dev/null @@ -1,5 +0,0 @@ -1.6G /home/you/.cache -1.1G /home/you/.cache/mozilla -245M /home/you/.cache/pip -131M /home/you/.cache/go-build -85M /home/you/.cache/opencode diff --git a/test/fixtures/slackware_cache b/test/fixtures/slackware_cache deleted file mode 100644 index 3f2b2ac..0000000 --- a/test/fixtures/slackware_cache +++ /dev/null @@ -1,5 +0,0 @@ -version Slackware 15.0+ -packages 1234 -changelog 1758000000 -birth 1700000000 -kernel 6.12.1 diff --git a/test/test_data.lua b/test/test_data.lua index df6775b..9066241 100644 --- a/test/test_data.lua +++ b/test/test_data.lua @@ -20,30 +20,6 @@ local function read(path) return s end --- === Cache files ========================================================== --- du -sh: total first, then children largest-first. The suffixes must be --- converted, not string-sorted: '1.1G' outranks '245M' numerically and loses --- to it alphabetically. -local cache = data.du_parse(read('test/fixtures/du_output')) -assert(cache.total, 'a total is parsed') -assert(cache.total.label == '1.6G', 'total label, got ' .. tostring(cache.total.label)) -assert(#cache.items == 4, 'four children, got ' .. tostring(#cache.items)) -assert(cache.items[1].name == 'mozilla', 'largest child, got ' .. tostring(cache.items[1].name)) -assert(cache.items[1].label == '1.1G', 'its label, got ' .. tostring(cache.items[1].label)) -assert(cache.items[2].name == 'pip', 'second child, got ' .. tostring(cache.items[2].name)) - --- Sizes are compared as bytes, which is what makes 'biggest' meaningful. -assert(cache.items[1].bytes > cache.items[2].bytes, '1.1G must outrank 245M') -assert(cache.items[1].bytes > 1e9, 'a G suffix is about a billion bytes') -assert(cache.items[4].bytes < 1e8, 'an M suffix is far smaller') - --- Share of the total drives the highlight colour. -assert(cache.items[1].bytes / cache.total.bytes > 0.5, - 'mozilla dominates this fixture, which is what the highlight keys on') - -assert(data.du_parse('') == nil, 'empty input gives nil') -assert(data.du_parse(nil) == nil, 'nil gives nil') - -- === card.truncate ======================================================== -- Truncation is by CHARACTER, not byte: slicing a UTF-8 string mid-sequence -- emits an invalid byte that Cairo draws as a replacement box, and the name @@ -66,16 +42,10 @@ assert(card.truncate(nil, 10) == '', 'nil truncates to empty') assert(card.truncate('abc', 0) == '', 'a zero limit gives empty') -- === kv_parse ============================================================= --- The slackware sampler writes 'key value' lines. The format is deliberately --- dumber than the data: epochs stay integers rather than becoming formatted --- dates, so the widget decides how to render an age and the shell never --- reconstructs a date string. -local kv = data.kv_parse(read('test/fixtures/slackware_cache')) -assert(kv.version == 'Slackware 15.0+', 'version, got ' .. tostring(kv.version)) --- The value keeps its internal spaces: only the FIRST space is the separator. -assert(kv.packages == '1234', 'packages, got ' .. tostring(kv.packages)) -assert(kv.changelog == '1758000000', 'changelog, got ' .. tostring(kv.changelog)) -assert(kv.kernel == '6.12.1', 'kernel, got ' .. tostring(kv.kernel)) +-- Samplers write 'key value' lines; only the FIRST space is the separator. +local kv = data.kv_parse('title Long break\nremain 125\n') +assert(kv.title == 'Long break', 'value keeps its spaces, got ' .. tostring(kv.title)) +assert(kv.remain == '125', 'remain, got ' .. tostring(kv.remain)) -- A key the sampler did not write reads nil, which is how the widget tells -- "the sampler ran but this fact was unavailable" from "no sampler". diff --git a/widgets/breaktimer.lua b/widgets/breaktimer.lua index 6fed30e..fa8649b 100644 --- a/widgets/breaktimer.lua +++ b/widgets/breaktimer.lua @@ -25,7 +25,7 @@ function M.draw(cr, rect, colors) local bt = data.breaktimer_parse(data.slurp(CACHE) or '') - -- No cache at all: the sampler has not run. Name it, as the slackware card + -- 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'. diff --git a/widgets/cache.lua b/widgets/cache.lua deleted file mode 100644 index c6391ca..0000000 --- a/widgets/cache.lua +++ /dev/null @@ -1,83 +0,0 @@ --- Cache: ~/.cache total and its four largest subdirectories. --- --- Reads a cache file written by bin/cache-sample.sh. du walks the tree and --- costs about 100ms, which cannot happen on a 2-second draw. - -local card = require 'lib.card' -local data = require 'lib.data' - -local CACHE = (os.getenv('XDG_CACHE_HOME') or (os.getenv('HOME') .. '/.cache')) - .. '/udt/cachesize.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 - -- Fluid type. One base size S drives every element. The height budget makes - -- the content fill the cell; the measured width fit pulls S down only where - -- the header or a row would overrun the card. A tall cell gets larger type - -- instead of empty space, and nothing collides. - local LABEL_F, ROW_F = 0.30, 0.32 - - local c = data.du_parse(data.slurp(CACHE) or '') - if not c then - card.notice(cr, inner, colors, 'CACHE', 'no cache data', 'bin/cache-sample.sh') - return - end - - local items = c.items or {} - local names = {} - for i, e in ipairs(items) do - -- By character, not byte: a cache directory can carry an accented name and - -- a byte slice cuts one in half, which Cairo draws as a replacement box. - names[i] = card.truncate(e.name, 18) - end - - local row_n = #items - local S_h = inner.h * 0.94 / (1.78 + row_n * 0.64) - local groups = { - { { card.title_str(card.ICON.cache, 'CACHE'), card.FONT_MONO, LABEL_F }, - { c.total.label, card.FONT_HEAVY, 1.0 } }, - } - for i = 1, row_n do - groups[#groups + 1] = { { names[i], card.FONT_MONO, ROW_F }, - { items[i].label, card.FONT_MONO, ROW_F } } - end - local S = clamp(math.min(S_h, 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_MONO, label_size, false) - card.title(cr, inner.x, inner.y + label_size, card.ICON.cache, 'CACHE', colors) - - -- The total shares the header line with the label, set on the same baseline - -- and right-aligned: the width carries the number instead of an otherwise - -- empty row beneath it. - card.font(cr, card.FONT_HEAVY, S, false) - card.rgba(cr, colors.body) - card.text_right(cr, inner.x + inner.w, inner.y + S, c.total.label) - - local ey = inner.y + S + label_size * 2.6 - local step = row_size * 2.0 - - card.font(cr, card.FONT_MONO, row_size, false) - for i, e in ipairs(items) do - if ey + step > inner.y + inner.h then break end - -- The biggest offender is the point of the card, so it carries colour: - -- critical when it is over half the total, warning otherwise. The rest - -- stay in the ordinary value colour so the eye lands on the one to delete. - local colour = colors.value - if i == 1 then - local share = c.total.bytes > 0 and (e.bytes / c.total.bytes) or 0 - colour = share > 0.5 and colors.critical or colors.warning - end - card.rgba(cr, colors.label) - card.text(cr, inner.x, ey, names[i]) - card.rgba(cr, colour) - card.text_right(cr, inner.x + inner.w, ey, e.label) - ey = ey + step - end -end - -return M diff --git a/widgets/slackware.lua b/widgets/slackware.lua deleted file mode 100644 index 1971ca1..0000000 --- a/widgets/slackware.lua +++ /dev/null @@ -1,117 +0,0 @@ --- Slackware: the distribution version, package count, kernel, install age, and --- how long since slackpkg's ChangeLog last changed. --- --- Everything comes from a cache file written by bin/slackware-sample.sh: --- conky's Lua cannot stat a file for an mtime, and counting the package --- directory every two seconds would be a walk per draw. - -local card = require 'lib.card' -local data = require 'lib.data' - -local CACHE = (os.getenv('XDG_CACHE_HOME') or (os.getenv('HOME') .. '/.cache')) - .. '/udt/slackware.txt' - --- Hours. A ChangeLog under a day old is current, under a week is ordinary, and --- past that is worth noticing. This extends the shell prompt's binary green/red --- into the board's three-state language rather than adding a fourth convention. -local WARN_H, CRIT_H = 24, 168 - -local SUBTITLE = 'last slackpkg update' - -local M = {} - --- An age in seconds as a short string: hours below two days, then days. Short --- because it is the card's big value and shares its line with the label. -local function age_str(seconds) - if not seconds then return '--' end - -- A future timestamp yields a negative age. Clock skew and a mirror's dated - -- file both produce it, and '-8000h' on the dashboard reads as a bug. - if seconds < 0 then seconds = 0 end - local hours = seconds / 3600 - if hours < 48 then return string.format('%dh', math.floor(hours)) end - return string.format('%dd', math.floor(hours / 24)) -end - -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, SUB_F = 0.30, 1.0, 0.32, 0.26 - - local kv = data.kv_parse(data.slurp(CACHE) or '') - - -- No cache at all: the sampler has not run. Name it, as the cache card does, - -- rather than leaving a blank cell that is indistinguishable from a crash. - if not kv.version then - card.notice(cr, inner, colors, 'SLACKWARE', 'no slackware data', - 'bin/slackware-sample.sh') - return - end - - -- The ChangeLog age, the card's headline. A cache that exists without this - -- key is a different failure from no cache: the sampler ran and the - -- ChangeLog is what was missing, so the rows still draw and only this reads - -- '--'. - local changelog = tonumber(kv.changelog) - local age_s = (changelog and changelog > 0) and (os.time() - changelog) or nil - local age_txt = age_str(age_s) - local age_colour = colors.label - if age_s then - age_colour = card.threshold(math.max(age_s, 0) / 3600, WARN_H, CRIT_H, colors) - end - - -- 'Slackware 15.0+' -> '15.0+': the header already says which distribution. - local version = (kv.version or ''):gsub('^Slackware%s+', '') - - local birth = tonumber(kv.birth) - local install_age = (birth and birth > 0) - and string.format('%dd', math.floor((os.time() - birth) / 86400)) or '--' - - local rows = { - { 'VERSION', version ~= '' and version or '--' }, - { 'PACKAGES', kv.packages or '--' }, - { 'KERNEL', kv.kernel or '--' }, - { 'AGE', install_age }, - } - - -- Fluid type: the height budget grows the content to fill the cell, the - -- measured width fit pulls it back where a row would overrun. - local groups = { - { { card.title_str(card.ICON.slackware, 'SLACKWARE'), card.FONT_MONO, LABEL_F }, - { age_txt, card.FONT_HEAVY, BIG_F } }, - { { SUBTITLE, card.FONT_UI, SUB_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, age_colour) - card.text_right(cr, inner.x + inner.w, inner.y + S * BIG_F, age_txt) - - card.font(cr, card.FONT_MONO, label_size, false) - card.title(cr, inner.x, inner.y + label_size, card.ICON.slackware, - 'SLACKWARE', colors) - - card.font(cr, card.FONT_UI, S * SUB_F, false) - card.rgba(cr, colors.label, 0.65) - card.text(cr, inner.x, inner.y + label_size + S * SUB_F * 1.5, SUBTITLE) - - 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 |
