diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-30 19:56:47 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-30 19:56:47 +0200 |
| commit | e55896ce41d6165105671da5b6f33813106ef1b8 (patch) | |
| tree | 082c5a7dffda63bbe740363fb35ca6dd1927b5ab /widgets | |
| parent | 9f6414838d4fabc174bb193f34225de8d9fc8079 (diff) | |
| download | conky-theme-udt-e55896ce41d6165105671da5b6f33813106ef1b8.tar.gz conky-theme-udt-e55896ce41d6165105671da5b6f33813106ef1b8.zip | |
feat: add an updates card showing nvtable's package table
Moves the nvchecker/nvtable table the login shell printed onto the
dashboard, as a wide card at the bottom centre. bin/updates-sample.sh
caches `nvtable -C -E` every ten minutes and starts nvchecker, fully
detached, at most every 30 minutes. The widget parses the cache and
colours it in the board's language: installed behind upstream is
critical, a shipped .info behind is a warning, with nvtable's status
dots per row and a legend on the bottom line. The mouse wheel scrolls
rows that do not fit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'widgets')
| -rw-r--r-- | widgets/updates.lua | 195 |
1 files changed, 195 insertions, 0 deletions
diff --git a/widgets/updates.lua b/widgets/updates.lua new file mode 100644 index 0000000..b50dcf3 --- /dev/null +++ b/widgets/updates.lua @@ -0,0 +1,195 @@ +-- 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 |
