-- 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