diff options
| -rw-r--r-- | README.md | 23 | ||||
| -rwxr-xr-x | bin/updates-sample.sh | 39 | ||||
| -rw-r--r-- | conky.conf.in | 4 | ||||
| -rw-r--r-- | dashboard.lua | 3 | ||||
| -rw-r--r-- | lib/card.lua | 1 | ||||
| -rw-r--r-- | lib/data.lua | 39 | ||||
| -rw-r--r-- | test/fixtures/updates_cache | 8 | ||||
| -rw-r--r-- | test/test_data.lua | 17 | ||||
| -rw-r--r-- | widgets/updates.lua | 195 |
9 files changed, 6 insertions, 323 deletions
@@ -68,14 +68,16 @@ 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. -- **Updates**: nvtable's table of tracked packages behind upstream, scrolled - with the mouse wheel. 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`. +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. @@ -85,7 +87,6 @@ a threaded `${texeci}` per sampler, even though that block renders nothing under ${texeci 900 ~/.config/conky/bin/slackware-sample.sh} ${texeci 300 ~/.config/conky/bin/calendar-sample.sh} ${texeci 1800 ~/.config/conky/bin/immich-fetch.sh} - ${texeci 600 ~/.config/conky/bin/updates-sample.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 @@ -100,7 +101,6 @@ To run a sampler by hand: ./bin/cache-sample.sh; echo "exit: $?" ./bin/slackware-sample.sh; echo "exit: $?" ./bin/calendar-sample.sh; echo "exit: $?" - ./bin/updates-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. @@ -180,21 +180,6 @@ The key sits in a plain file, like the weather key, because the fetch runs unattended from conky: reading it from a GPG-backed store would raise a pinentry whenever the agent's cache has expired. -### Updates - -nvtable's table of tracked packages that need action: where each is -installed, the version its SlackBuild ships and, when that is not it, the -upstream release. The big value is how many installed packages are behind -upstream. Installed versions read red when behind and green when caught up, -a shipped `.info` behind upstream reads as a warning. Rows that do not fit -scroll with the mouse wheel, with a thin scrollbar at the right edge. - -`bin/updates-sample.sh` runs every ten minutes and caches `nvtable -C -E` -in `~/.cache/udt/updates.txt`. It also starts `nvchecker` in the background -at most every 30 minutes, detached so conky never waits on the network; its -results show up on the next sample. `nvtable` is looked up in `~/bin` too, -since conky launched by the compositor lacks the login shell's `PATH`. - ### One colour language across every card Anything with a comfortable range and an uncomfortable one is coloured by the diff --git a/bin/updates-sample.sh b/bin/updates-sample.sh deleted file mode 100755 index 0c9ee3a..0000000 --- a/bin/updates-sample.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -# Sample nvtable's package-update table into a cache file, for -# widgets/updates.lua. Replaces the table the login shell used to print. -# -# nvchecker itself is slow (one network request per stanza), so it is started -# in the background at most every 30 minutes, fully detached: conky's execi -# waits for stdout to close, and a child holding it would stall the dashboard. -# nvtable then reads whatever results the last completed run left, so a fresh -# nvchecker run shows up on the next sample. - -set -u - -CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/udt" -CACHE="$CACHE_DIR/updates.txt" -STAMP="$CACHE_DIR/nvchecker.stamp" -NVCHECK_EVERY=1800 - -# conky launched from the compositor does not inherit the login shell's PATH. -PATH="$HOME/bin:$HOME/.local/bin:$PATH" - -command -v nvtable >/dev/null || { echo "updates-sample: nvtable not found" >&2; exit 1; } - -mkdir -p "$CACHE_DIR" - -if command -v nvchecker >/dev/null; then - last=$(cat "$STAMP" 2>/dev/null || echo 0) - now=$(date +%s) - if (( now - ${last:-0} >= NVCHECK_EVERY )); then - echo "$now" > "$STAMP" - setsid nvchecker </dev/null >/dev/null 2>&1 & - fi -fi - -TMP="$CACHE.tmp.$$" -trap 'rm -f "$TMP"' EXIT - -# -C and -E: plain ASCII, which the widget parses and colours itself. -# Keep the old cache when nvtable fails, rather than replacing it with nothing. -nvtable -C -E > "$TMP" 2>/dev/null && mv -f "$TMP" "$CACHE" diff --git a/conky.conf.in b/conky.conf.in index bf7fedc..1bec680 100644 --- a/conky.conf.in +++ b/conky.conf.in @@ -58,6 +58,6 @@ conky.config = { -- in the steps the daemon counts in. -- -- texeci, not execi: execi runs in conky's main loop and blocks the draw until --- the script exits. At 0.1s while visible, a 0.7s nvtable or an Immich +-- 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}${texeci 600 ~/.config/conky/bin/updates-sample.sh}]] +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}]] diff --git a/dashboard.lua b/dashboard.lua index f0c34e6..fd3e2e5 100644 --- a/dashboard.lua +++ b/dashboard.lua @@ -31,9 +31,6 @@ local layout = { -- 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 }, - -- Bottom centre, in the open band between breaktimer and cache. Wide - -- because nvtable's rows are; it scrolls when they outgrow the height. - { widget = 'updates', col = 6, row = 7, w = 6, h = 6 }, } -- ========================================================================== diff --git a/lib/card.lua b/lib/card.lua index 2749a6e..0a93c88 100644 --- a/lib/card.lua +++ b/lib/card.lua @@ -220,7 +220,6 @@ M.ICON = { breaktimer = '\u{F13AB}', -- timer calendar = '\u{F0E17}', -- calendar-month immich = '\u{F02F9}', -- image-multiple - updates = '\u{F03D5}', -- package-up } -- The gap between a title's icon and its text. One constant so the measured diff --git a/lib/data.lua b/lib/data.lua index 804398e..6b32f07 100644 --- a/lib/data.lua +++ b/lib/data.lua @@ -303,43 +303,4 @@ function M.immich_parse(text) return out end --- nvtable's plain table (`nvtable -C -E`), as bin/updates-sample.sh caches it: --- --- [personal] foo-bin 1.14.9 -> 1.14.9 !! ~ 1.14.10 --- <repo> <name> <installed> -> <shipped> <marker> [~] [upstream] --- --- followed by a summary line with the counts. The marker is '!!' (installed --- behind upstream), 'ok' or '-' (not installed); '~' flags the shipped .info --- as behind. Upstream appears only when shipped does not already show it. --- --- Returns the rows as { repo, name, installed, shipped, upstream, behind, --- bump } with '--' turned into nil, plus .upgrade/.bump/.current/.missing --- counts, or nil when the text has neither rows nor a summary. -function M.nvtable_parse(text) - if type(text) ~= 'string' then return nil end - local out, seen = {}, false - local function v(s) return s ~= '--' and s or nil end - for line in text:gmatch('[^\n]+') do - local repo, name, inst, ship, rest = - line:match('^%s*%[(.-)%]%s+(%S+)%s+(%S+)%s+%->%s+(%S+)%s*(.*)$') - if repo then - local mark, tail = rest:match('^(%S*)%s*(.*)$') - local bump = tail:match('^~') ~= nil - if bump then tail = tail:gsub('^~%s*', '') end - out[#out + 1] = { - repo = v(repo), name = name, installed = v(inst), shipped = v(ship), - upstream = tail:match('^(%S+)'), behind = mark == '!!', bump = bump, - } - seen = true - elseif line:match('to upgrade') then - out.upgrade = tonumber(line:match('(%d+) to upgrade')) - out.bump = tonumber(line:match('(%d+) repos? to bump')) - out.current = tonumber(line:match('(%d+) current')) - out.missing = tonumber(line:match('(%d+) not installed')) or 0 - seen = true - end - end - return seen and out or nil -end - return M diff --git a/test/fixtures/updates_cache b/test/fixtures/updates_cache deleted file mode 100644 index 130dda0..0000000 --- a/test/fixtures/updates_cache +++ /dev/null @@ -1,8 +0,0 @@ - [personal] foo-bin 1.14.9 -> 1.14.9 !! ~ 1.14.10 - [SBo] bar 2.1.278 -> 2.1.201 !! 2.1.284 - [personal] baz 3.16.30 -> 3.16.31 !! - [--] qux 0.12.1 -> -- !! 0.12.1_beta.2 - [my-sbo] quux -- -> 0.28 - - [my-sbo] corge 2.3.0 -> 2.3.0 ok - - 3 to upgrade · 1 repo to bump · 46 current · 18 not installed diff --git a/test/test_data.lua b/test/test_data.lua index 37a302e..df6775b 100644 --- a/test/test_data.lua +++ b/test/test_data.lua @@ -226,23 +226,6 @@ assert(data.immich_parse('generated 1\nscope week\n').week, 'scope week parses') print('test_data: all assertions passed') --- nvtable_parse: nvtable -C -E rows. The marker column is positional, so an --- empty installed or repo column ('--') must not shift it. -local up = data.nvtable_parse(read('test/fixtures/updates_cache')) -assert(up and #up == 6, 'updates rows, got ' .. tostring(up and #up)) -local r = up[1] -assert(r.repo == 'personal' and r.name == 'foo-bin' and r.installed == '1.14.9' - and r.shipped == '1.14.9' and r.behind and r.bump and r.upstream == '1.14.10', - 'updates row 1') -assert(up[2].behind and not up[2].bump and up[2].upstream == '2.1.284', 'updates row 2') -assert(up[3].upstream == nil and up[3].shipped == '3.16.31', 'updates row 3') -assert(up[4].shipped == nil and up[4].repo == nil, 'updates row 4: -- is nil') -assert(up[5].installed == nil and not up[5].behind, 'updates row 5: not installed') -assert(not up[6].behind and not up[6].bump, 'updates row 6: current') -assert(up.upgrade == 3 and up.bump == 1 and up.current == 46 and up.missing == 18, - 'updates summary') -assert(data.nvtable_parse('') == nil, 'updates: empty cache is nil') - -- Emoji fall back to card.FONT_EMOJI: the toy API has no per-glyph fallback, -- so without the run split "x🦷" measured a tofu box in Noto Sans. do diff --git a/widgets/updates.lua b/widgets/updates.lua deleted file mode 100644 index b50dcf3..0000000 --- a/widgets/updates.lua +++ /dev/null @@ -1,195 +0,0 @@ --- Updates: nvtable's table of tracked packages that need action, the one the --- login shell used to print. Wide and short, so it scrolls: the mouse wheel --- steps through rows that do not fit. --- --- Everything comes from the cache bin/updates-sample.sh writes; running --- nvtable here would cost ~0.7s 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/updates.txt' - --- Packages behind upstream. One is worth a glance, ten is a backlog. -local WARN_N, CRIT_N = 1, 10 - --- First row shown, moved by the wheel. max_offset is remembered from the last --- draw so the mouse handler can clamp without re-reading the cache. -local offset, max_offset = 0, 0 - -local M = {} - --- nvtable's status dots, in the board's colours: 🔴 installed behind upstream, --- 🟡 shipped .info behind, 🟢 current, ⚪ not installed (a ring). -local function dot(cr, x, y, radius, colour, hollow) - cairo_new_path(cr) - cairo_arc(cr, x, y, radius, 0, 2 * math.pi) - card.rgba(cr, colour) - if hollow then - cairo_set_line_width(cr, math.max(1, radius * 0.35)) - cairo_stroke(cr) - else - cairo_fill(cr) - end -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 up = data.nvtable_parse(data.slurp(CACHE) or '') - if not up then - card.notice(cr, inner, colors, 'UPDATES', 'no update data', - 'bin/updates-sample.sh') - return - end - - local big = up.upgrade and tostring(up.upgrade) or '--' - local sub = {} - if up.bump then sub[#sub + 1] = up.bump .. (up.bump == 1 and ' repo' or ' repos') .. ' to bump' end - if up.current then sub[#sub + 1] = up.current .. ' current' end - if up.missing and up.missing > 0 then sub[#sub + 1] = up.missing .. ' not installed' end - sub = table.concat(sub, ' · ') - - -- Header sized as if the card held ~12 rows: the table scrolls, so letting - -- the header grow with the cell's height would only squeeze the rows. - local S = clamp(math.min(inner.h * 0.94 / (1.5 + 12 * 0.66), - card.fit_unit(cr, inner.w * 0.96, { - { { card.title_str(card.ICON.updates, 'UPDATES'), card.FONT_MONO, LABEL_F }, - { big, card.FONT_HEAVY, BIG_F } }, - { { sub, card.FONT_UI, SUB_F } }, - }, 100)), 10, 72) - local label_size = S * LABEL_F - - card.font(cr, card.FONT_HEAVY, S * BIG_F, false) - card.rgba(cr, card.threshold(up.upgrade, WARN_N, CRIT_N, colors)) - card.text_right(cr, inner.x + inner.w, inner.y + S * BIG_F, big) - - card.font(cr, card.FONT_MONO, label_size, false) - card.title(cr, inner.x, inner.y + label_size, card.ICON.updates, 'UPDATES', 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, sub) - - local top = inner.y + S * BIG_F + S * ROW_F * 1.2 - local bottom = inner.y + inner.h - - if #up == 0 then - card.font(cr, card.FONT_UI, S * ROW_F, false) - card.rgba(cr, colors.ok) - card.text(cr, inner.x, top + S * ROW_F * 1.5, 'everything current') - return - end - - -- Column widths in characters. The font is monospaced, so padding with - -- spaces aligns columns exactly and one advance converts chars to pixels. - local wl, wn, wi, ws, wu = 2, 1, 2, 2, 0 - for _, r in ipairs(up) do - wl = math.max(wl, #(r.repo or '--')) - wn = math.max(wn, #r.name) - wi = math.max(wi, #(r.installed or '--')) - ws = math.max(ws, #(r.shipped or '--')) - wu = math.max(wu, #(r.upstream or '')) - end - -- repo, gap, name, gap, installed, ' → ', shipped, gap, two dots, gap, - -- upstream - local chars = wl + 2 + wn + 2 + wi + 3 + ws + 2 + 3 + 2 + wu - - -- Row type: the header's row ratio, shrunk until the widest row fits - -- beside the scrollbar gutter. - local gutter = 10 - card.font(cr, card.FONT_MONO, 100, false) - local per_unit = card.advance(cr, '0') / 100 - local row_size = math.max(8, math.min(S * ROW_F, (inner.w - gutter) / (chars * per_unit))) - local cw = per_unit * row_size - local step = row_size * 1.7 - local rad = row_size * 0.28 - local mid = row_size * 0.32 -- baseline to the digits' vertical centre - - -- The legend takes the bottom line; the rows scroll above it. - bottom = bottom - row_size * 2 - - local visible = math.max(1, math.floor((bottom - top) / step)) - max_offset = math.max(0, #up - visible) - offset = clamp(offset, 0, max_offset) - - local x_name = inner.x + (wl + 2) * cw - local x_inst = inner.x + inner.w - gutter - (wi + 3 + ws + 2 + 5 + wu) * cw - local x_ship = x_inst + (wi + 3) * cw - local x_mark = x_ship + (ws + 2) * cw - local x_upst = x_mark + 5 * cw - - card.font(cr, card.FONT_MONO, row_size, false) - local y = top + row_size - for i = offset + 1, math.min(#up, offset + visible) do - local r = up[i] - card.rgba(cr, colors.label, 0.65) - card.text(cr, inner.x, y, r.repo or '--') - card.rgba(cr, colors.value) - card.text(cr, x_name, y, r.name) - - -- Installed: behind upstream is the critical state, caught up is ok. - if r.installed then card.rgba(cr, r.behind and colors.critical or colors.ok) - else card.rgba(cr, colors.label, 0.5) end - card.text(cr, x_inst, y, string.format('%' .. wi .. 's', r.installed or '--')) - card.rgba(cr, colors.label, 0.65) - card.text(cr, x_inst + wi * cw, y, ' → ') - -- Shipped: a .info behind upstream is packaging work waiting, a warning. - if r.shipped then card.rgba(cr, r.bump and colors.warning or colors.value) - else card.rgba(cr, colors.label, 0.5) end - card.text(cr, x_ship, y, r.shipped or '--') - -- Installed state, then a second dot when the .info is behind too: the - -- two are independent, as nvtable shows them. - if not r.installed then dot(cr, x_mark + cw / 2, y - mid, rad, colors.label, true) - else dot(cr, x_mark + cw / 2, y - mid, rad, r.behind and colors.critical or colors.ok) end - if r.bump then dot(cr, x_mark + cw * 2.5, y - mid, rad, colors.warning) end - card.font(cr, card.FONT_MONO, row_size, false) - if r.upstream then - card.rgba(cr, colors.highlight) - card.text(cr, x_upst, y, r.upstream) - end - y = y + step - end - - -- Legend, on the card's bottom line. - local lx, ly = inner.x, inner.y + inner.h - row_size * 0.2 - card.font(cr, card.FONT_UI, row_size * 0.85, false) - for _, l in ipairs({ - { colors.critical, 'upstream newer' }, { colors.warning, 'repo behind' }, - { colors.ok, 'current' }, { colors.label, 'not installed', true }, - }) do - dot(cr, lx + rad, ly - mid, rad, l[1], l[3]) - lx = lx + rad * 2 + cw * 0.7 - card.rgba(cr, colors.label, 0.65) - card.text(cr, lx, ly, l[2]) - lx = lx + card.advance(cr, l[2]) + cw * 2.5 - end - - -- Scrollbar, only when there is something to scroll. - if max_offset > 0 then - local track_h = visible * step - local thumb_h = math.max(row_size, track_h * visible / #up) - local thumb_y = top + (track_h - thumb_h) * offset / max_offset - local bx = inner.x + inner.w - 3 - card.rounded_path(cr, bx, top, 3, track_h, 1.5) - card.rgba(cr, colors.rule, 0.35) - cairo_fill(cr) - card.rounded_path(cr, bx, thumb_y, 3, thumb_h, 1.5) - card.rgba(cr, colors.highlight, 0.8) - cairo_fill(cr) - end -end - --- The wheel steps one row. It shows at once because the dashboard draws at --- 0.1s while visible (see dashboard.lua). -function M.mouse(event) - if event.type ~= 'mouse_scroll' then return false end - offset = math.max(0, math.min(max_offset, - offset + (event.direction == 'up' and -1 or 1))) - return true -end - -return M |
