aboutsummaryrefslogtreecommitdiffstats
path: root/widgets
diff options
context:
space:
mode:
Diffstat (limited to 'widgets')
-rw-r--r--widgets/disks.lua150
-rw-r--r--widgets/gpu.lua121
-rw-r--r--widgets/network.lua181
-rw-r--r--widgets/system.lua198
4 files changed, 0 insertions, 650 deletions
diff --git a/widgets/disks.lua b/widgets/disks.lua
deleted file mode 100644
index 3922188..0000000
--- a/widgets/disks.lua
+++ /dev/null
@@ -1,150 +0,0 @@
--- Disks: one ring per filesystem, after idea2.png.
---
--- Reads a cache file written by bin/disks-sample.sh and never calls statfs
--- itself: an unreachable NFS server blocks that call, and blocking the draw
--- freezes the whole dashboard.
-
-local card = require 'lib.card'
-local data = require 'lib.data'
-
-local M = {}
-
-local CACHE = (os.getenv('XDG_CACHE_HOME') or (os.getenv('HOME') .. '/.cache'))
- .. '/udt/disks.txt'
-
--- mount -> centre glyph and the small text under it.
---
--- The icon IS the label: no mount name is drawn, because a Slackware S or a
--- house says which filesystem this is faster than the word does. Every
--- codepoint here was taken from the old conky config and re-rendered to
--- confirm it, rather than guessed: an earlier guess at the Slackware mark
--- (F83C) turned out to draw a stomach, and a wrong-but-present codepoint
--- fails plausibly rather than visibly.
---
--- The two NFS shares are told apart by their server's address, which is read
--- from the mount's device field at runtime rather than written here. The
--- addresses belong on the screen, not in a public repo, and deriving them also
--- means the card keeps working if the LAN is renumbered. It fits: at a 50px
--- radius the usable inner width is about 75px and an address measures 58px at
--- 10px type.
---
--- A mount not listed still draws, with a generic disk glyph and the last path
--- segment, so adding one to the sampler needs no change here.
-local GLYPH = {
- ['/'] = '\u{F318}', -- Slackware
- ['/home'] = '\u{F015}', -- house
- ['/data'] = '\u{F02CA}', -- hard disk
-}
-local NFS_GLYPH = '\u{F06F3}' -- network share
-local FALLBACK_GLYPH = '\u{F02CA}'
-
-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_size = clamp(inner.w * 0.030, 10, 16)
-
- local fs = data.df_parse(data.slurp(CACHE) or '')
- if #fs == 0 then
- card.notice(cr, inner, colors, 'DISKS', 'no disk data', 'bin/disks-sample.sh')
- return
- end
-
- card.font(cr, card.FONT_MONO, label_size, false)
- card.title(cr, inner.x, inner.y + label_size, card.ICON.disks, 'DISKS', colors)
-
- local top = inner.y + label_size * 2.2
- local avail_h = (inner.y + inner.h) - top
-
- -- All rings share one row: the cell width shrinks as mounts are added and
- -- the ring radius follows it, so the layout never wraps. Below the radius
- -- where a ring reads, fall back to labelled bars rather than drawing a row
- -- of illegible dots.
- local n = #fs
- local per_row = n
- local cell_w = inner.w / per_row
- -- 0.27 rather than 0.30: two rows sit under each ring now, not one, and the
- -- radius is what gives up the height.
- local r = math.min(cell_w * 0.34, avail_h * 0.27)
-
- if r >= 18 then
- for i, e in ipairs(fs) do
- -- host is non-nil exactly for a network mount, since a local device is
- -- a path and carries no 'server:' prefix.
- local glyph = GLYPH[e.mount] or (e.host and NFS_GLYPH) or FALLBACK_GLYPH
- local sub = e.host
- if not sub and not GLYPH[e.mount] then
- sub = (e.mount:match('([^/]+)/?$') or e.mount):upper()
- end
- local cx = inner.x + cell_w * (i - 0.5)
- local cy = top + r + 6
- local colour = card.threshold(e.pct, 25, 75, colors)
-
- card.ring(cr, cx, cy, r, e.pct / 100, colour, colors)
-
- -- Glyph at the centre. Drawn at 0.55 of the radius rather than 0.7: the
- -- network-share glyph carries a wide horizontal connector that runs past
- -- the ring at larger sizes, which reads as a line joining the rings
- -- together.
- local gsize = r * 0.55
- card.font(cr, card.FONT_MONO, gsize, false)
- card.rgba(cr, colors.label)
- local gw = card.measure(cr, glyph)
- -- Raised when there is text under it, centred when there is not.
- card.text(cr, cx - gw / 2, cy + (sub and gsize * 0.10 or gsize * 0.38), glyph)
-
- -- The address, inside the ring beneath the glyph. Only the NFS shares
- -- have one; the local mounts are identified by their icon alone.
- if sub then
- -- Tied to the card's own label scale rather than a fixed px floor, so
- -- every piece of small mono text on the card tracks the same size.
- card.font(cr, card.FONT_MONO, clamp(r * 0.20, label_size * 0.7, label_size), false)
- card.rgba(cr, colors.label)
- local sw_ = card.measure(cr, sub)
- card.text(cr, cx - sw_ / 2, cy + r * 0.55, sub)
- end
-
- -- Free on top, total beneath, both centred under the ring.
- --
- -- The percentage that used to sit here is gone: the ring already draws
- -- it as an angle, and a number repeating it costs a row in a column
- -- about 75px wide. Free carries the threshold colour because it is the
- -- measured quantity; the total is fixed, so it stays in `value`. One
- -- coloured number per column, as the rest of the board reads.
- local free_size = label_size
- local total_size = label_size * 0.88
- card.font(cr, card.FONT_MONO, free_size, false)
- card.rgba(cr, colour)
- local free_txt = card.human(e.avail) .. ' free'
- local fw = card.measure(cr, free_txt)
- card.text(cr, cx - fw / 2, cy + r + free_size * 1.7, free_txt)
-
- card.font(cr, card.FONT_MONO, total_size, false)
- card.rgba(cr, colors.value)
- local total_txt = card.human(e.size)
- local tw = card.measure(cr, total_txt)
- card.text(cr, cx - tw / 2, cy + r + free_size * 1.7 + total_size * 1.4, total_txt)
- end
- else
- local step = label_size * 2.6
- local bar_h = clamp(label_size * 0.5, 4, 8)
- local ey = top + label_size
- for _, e in ipairs(fs) do
- if ey + step > inner.y + inner.h then break end
- -- The bar fallback has no room for a ring, so it shows the glyph inline
- -- followed by the address or the mount name.
- local glyph = GLYPH[e.mount] or (e.host and NFS_GLYPH) or FALLBACK_GLYPH
- local name = e.host or (e.mount:match('([^/]+)/?$') or e.mount):upper()
- local colour = card.threshold(e.pct, 25, 75, colors)
- card.font(cr, card.FONT_MONO, label_size, false)
- card.rgba(cr, colors.label)
- card.text(cr, inner.x, ey, glyph .. ' ' .. name)
- card.rgba(cr, colour)
- card.text_right(cr, inner.x + inner.w, ey,
- string.format('%s / %s', card.human(e.used), card.human(e.size)))
- card.bar(cr, inner.x, ey + label_size * 0.5, inner.w, bar_h, e.pct / 100, colour, colors)
- ey = ey + step
- end
- end
-end
-
-return M
diff --git a/widgets/gpu.lua b/widgets/gpu.lua
deleted file mode 100644
index abac856..0000000
--- a/widgets/gpu.lua
+++ /dev/null
@@ -1,121 +0,0 @@
--- GPU: Arc B580 temperatures, VRAM in use and fan speed.
---
--- No utilisation figure, deliberately. The xe driver exposes no
--- gpu_busy_percent and intel_gpu_top refuses the device outright ("Detected Xe
--- device which is not supported"). VRAM comes from DRM fdinfo, the per-client
--- counters gputop itself reads (see data.drm_vram).
---
--- The integrated AMD GPU does expose gpu_busy_percent and is deliberately not
--- shown: the discrete card is the one in use.
-
-local card = require 'lib.card'
-local data = require 'lib.data'
-
-local M = {}
-
--- The B580's VRAM. fdinfo reports what clients hold, not the card's size, and
--- xe puts no capacity in sysfs. ponytail: hardcoded like the 'ARC B580' label.
-local VRAM_TOTAL = 12 * 1024 ^ 3
-
--- fdinfo is a popen over every fd on the system, so sample it every ~2s, not
--- every frame.
-local gate = data.new_gate(1.9)
-local vram
-
--- chip, file, label, warn, crit. Package (temp2) is the headline above, so it
--- is not repeated here; these are the rows drawn under it.
-local TEMPS = {
- { 'xe', 'temp3_input', 'VRAM', 80, 90 },
-}
-
-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
-
- if gate() then vram = data.drm_vram() end
-
- local pkg = data.sensor('xe', 'temp2_input')
- local big_txt = pkg and string.format('%d\u{00B0}', pkg) or '--'
-
- -- Fluid type. One base size S drives every element. The cell's height sets
- -- how large the content grows; the measured width fit pulls S back down
- -- only where the header or a row would overrun the card. So a tall cell
- -- gets larger type instead of empty space, and nothing ever collides.
- local LABEL_F, ROW_F = 0.32, 0.34
- local row_n = #TEMPS + 2 -- VRAM + MEM + FAN
- local S_h = inner.h * 0.94 / (2.088 + row_n * 0.646 + 0.3) -- + MEM bar
- local groups = {
- { { card.title_str(card.ICON.gpu, 'GPU'), card.FONT_MONO, LABEL_F },
- { big_txt, card.FONT_HEAVY, 1.0 } },
- { { 'ARC B580', card.FONT_MONO, LABEL_F } },
- }
- for _, t in ipairs(TEMPS) do
- groups[#groups + 1] = { { t[3], card.FONT_MONO, ROW_F },
- { '888\u{00B0}', card.FONT_MONO, ROW_F } }
- end
- groups[#groups + 1] = { { 'MEM', card.FONT_MONO, ROW_F },
- { '100%', card.FONT_MONO, ROW_F } }
- groups[#groups + 1] = { { 'FAN', card.FONT_MONO, ROW_F },
- { '8888 RPM', card.FONT_MONO, ROW_F } }
- 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
-
- local y = inner.y
-
- -- The package temperature is the headline, since there is no load to show.
- -- It shares the header line with the label, set on the same baseline and
- -- right-aligned, so the card's width carries the number rather than leaving
- -- the right half empty under a left-set value.
- card.font(cr, card.FONT_HEAVY, S, false)
- card.rgba(cr, pkg and card.threshold(pkg, 75, 85, colors) or colors.label)
- card.text_right(cr, inner.x + inner.w, y + S, big_txt)
-
- card.font(cr, card.FONT_MONO, label_size, false)
- card.title(cr, inner.x, y + label_size, card.ICON.gpu, 'GPU', colors)
-
- card.rgba(cr, colors.value)
- card.text(cr, inner.x, y + S + label_size * 1.7, 'ARC B580')
-
- local ey = y + S + label_size * 3.4
- local step = row_size * 1.9
-
- card.font(cr, card.FONT_MONO, row_size, false)
- for _, t in ipairs(TEMPS) do
- if ey + step > inner.y + inner.h then break end
- local v = data.sensor(t[1], t[2])
- card.rgba(cr, colors.label)
- card.text(cr, inner.x, ey, t[3])
- card.rgba(cr, v and card.threshold(v, t[4], t[5], colors) or colors.label)
- card.text_right(cr, inner.x + inner.w, ey,
- v and string.format('%d\u{00B0}', v) or '--')
- ey = ey + step
- end
-
- -- VRAM as a fraction of the card's, a bar under the row like RAM in system.
- local bar_h = S * 0.12
- if ey + step + bar_h + S * 0.1 <= inner.y + inner.h then
- local frac = vram and vram / VRAM_TOTAL
- local colour = frac and card.threshold(frac * 100, 25, 75, colors) or colors.label
- card.rgba(cr, colors.label)
- card.text(cr, inner.x, ey, 'MEM')
- card.rgba(cr, colour)
- card.text_right(cr, inner.x + inner.w, ey,
- frac and string.format('%d%%', math.floor(frac * 100 + 0.5)) or '--')
- card.bar(cr, inner.x, ey + row_size * 0.6, inner.w, bar_h, frac, colour, colors)
- ey = ey + step + bar_h + S * 0.1
- end
-
- -- Fan RPM is not a temperature, so it carries no threshold colour: a fast
- -- fan is the cooling working, not a fault.
- if ey + step <= inner.y + inner.h then
- local rpm = data.sensor_raw('xe', 'fan1_input')
- card.rgba(cr, colors.label)
- card.text(cr, inner.x, ey, 'FAN')
- card.rgba(cr, colors.value)
- card.text_right(cr, inner.x + inner.w, ey,
- rpm and string.format('%d RPM', rpm) or '--')
- end
-end
-
-return M
diff --git a/widgets/network.lua b/widgets/network.lua
deleted file mode 100644
index 20b0573..0000000
--- a/widgets/network.lua
+++ /dev/null
@@ -1,181 +0,0 @@
--- Network: throughput as two lines on one chart, with the LAN and public
--- addresses.
---
--- The rates come from the interface's own byte counters, read every draw: two
--- file reads, no subprocess. The public address comes from a cache file,
--- because a network call in the draw hook would freeze the dashboard when it
--- hung.
-
-local card = require 'lib.card'
-local data = require 'lib.data'
-
-local M = {}
-
--- The interface to graph. br0 is this host's bridge, matching the old text
--- config.
---
--- Note what this means: a bridge carries VM-to-host traffic that never reaches
--- the router, so a local copy spikes the graph. That was true of the old
--- config too and is accepted; this constant is the one edit that changes it.
-local IFACE = 'br0'
-
-local CACHE = (os.getenv('XDG_CACHE_HOME') or (os.getenv('HOME') .. '/.cache'))
- .. '/udt/pubip.txt'
-
--- A public address older than this is not shown. Eight sampling intervals: by
--- then the sampler has failed repeatedly, and an address that may no longer be
--- yours displayed with confidence is worse than '--'.
-local STALE_AFTER = 4 * 3600
-
-local STAT = '/sys/class/net/' .. IFACE .. '/statistics/'
-
--- Module state: the history outlives the frame, which is the whole point.
--- It does not outlive a conky restart, so the chart starts empty and fills
--- left to right.
-local rx_counter, tx_counter
-local rx_hist, tx_hist = {}, {}
-local hist_len = 0
-local lan_addr = nil
-
--- Sampled every ~2s whatever the draw rate, so the chart keeps one point per
--- sample rather than per draw. 1.9 not 2: draws jitter by a few ms.
-local gate = data.new_gate(1.9)
-local down, up = nil, nil
-
--- Append to a fixed-length history, dropping the oldest. A plain array with
--- table.remove(1) rather than a circular buffer with an index: the lengths
--- here are a few hundred at most and the draw is every two seconds, so the
--- O(n) shift is free and the array is already in draw order.
-local function push(hist, v, len)
- hist[#hist + 1] = v
- while #hist > len do table.remove(hist, 1) end
-end
-
-local function read_counter(file)
- local s = data.slurp(STAT .. file)
- if not s then return nil end
- return tonumber(s:match('^%s*(%d+)'))
-end
-
--- Bytes per second to a short string. Deliberately not card.human: a rate
--- wants a '/s' and one decimal at most, and reusing card.human would put a
--- suffix meant for capacity onto a speed.
-local function rate_str(bps)
- if not bps then return '--' end
- local units = { 'B', 'K', 'M', 'G' }
- local n, i = bps, 1
- while n >= 1024 and i < #units do n = n / 1024; i = i + 1 end
- if i == 1 then return string.format('%d%s/s', math.floor(n), units[i]) end
- if n >= 100 then return string.format('%.0f%s/s', n, units[i]) end
- return string.format('%.1f%s/s', n, units[i])
-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 interval = (conky_info and conky_info.update_interval) or 2
- rx_counter = rx_counter or data.new_rate_counter(interval)
- tx_counter = tx_counter or data.new_rate_counter(interval)
-
- local sampled, now = gate()
- if sampled then
- down = rx_counter:sample(read_counter('rx_bytes'), now)
- up = tx_counter:sample(read_counter('tx_bytes'), now)
- end
-
- -- Fluid type, as every other card does: one base size S drives everything,
- -- taken as the smaller of a height budget and a measured width fit.
- local LABEL_F, BIG_F, ROW_F = 0.30, 1.0, 0.30
- local groups = {
- { { card.title_str(card.ICON.network, 'NET'), card.FONT_MONO, LABEL_F },
- { '888.8M/s', card.FONT_HEAVY, BIG_F } },
- { { '\u{F0318} 192.000.000.000', card.FONT_MONO, ROW_F } },
- { { '\u{F01DA} 888.8M/s', card.FONT_MONO, ROW_F },
- { '\u{F0552} 888.8M/s', card.FONT_MONO, ROW_F } },
- }
- local fixed_units = 1.0 + ROW_F * 2.2 * 3
- local S = clamp(math.min(inner.h * 0.96 / (fixed_units + 1.2),
- card.fit_unit(cr, inner.w * 0.96, groups, 100)), 10, 72)
- local label_size = S * LABEL_F
- local row_size = S * ROW_F
-
- -- Header: the label left, the download rate as the big value at the right.
- -- Download is the headline because it is the number that moves.
- card.font(cr, card.FONT_HEAVY, S * BIG_F, false)
- card.rgba(cr, colors.body)
- card.text_right(cr, inner.x + inner.w, inner.y + S * BIG_F, rate_str(down))
-
- card.font(cr, card.FONT_MONO, label_size, false)
- card.title(cr, inner.x, inner.y + label_size, card.ICON.network, 'NET', colors)
-
- local ey = inner.y + S * BIG_F + row_size * 1.4
- local step = row_size * 2.2
-
- -- The addresses. LAN is memoized on first success and retried while nil: a
- -- bridge may not be up when conky starts.
- lan_addr = lan_addr or data.iface_addr(IFACE)
- card.font(cr, card.FONT_MONO, row_size, false)
- card.rgba(cr, colors.label)
- card.text(cr, inner.x, ey, '\u{F0318}') -- LAN
- card.rgba(cr, colors.value)
- card.text_right(cr, inner.x + inner.w, ey, lan_addr or '--')
- ey = ey + step
-
- local pub = data.kv_parse(data.slurp(CACHE) or '')
- local fetched = tonumber(pub.fetched)
- local pub_txt = '--'
- if pub.ip and fetched and (os.time() - fetched) < STALE_AFTER then
- pub_txt = pub.ip
- end
- card.rgba(cr, colors.label)
- card.text(cr, inner.x, ey, '\u{F059F}') -- globe
- card.rgba(cr, pub_txt == '--' and colors.label or colors.value)
- card.text_right(cr, inner.x + inner.w, ey, pub_txt)
- ey = ey + step * 1.1
-
- -- The chart takes the room left between the addresses and the rate row.
- local rate_row_h = row_size * 2.4
- local chart_top = ey
- local chart_h = (inner.y + inner.h) - chart_top - rate_row_h
- if chart_h > 12 then
- -- One sample per pixel column, so the chart never interpolates. The
- -- buffer is resized when the card is, keeping what it can: a moved card
- -- should not clear the history.
- local want = math.max(8, math.floor(inner.w))
- if want ~= hist_len then
- hist_len = want
- while #rx_hist > hist_len do table.remove(rx_hist, 1) end
- while #tx_hist > hist_len do table.remove(tx_hist, 1) end
- end
- if sampled and down then push(rx_hist, down, hist_len) end
- if sampled and up then push(tx_hist, up, hist_len) end
-
- -- One ceiling for both series. Scaled independently they would lie about
- -- their relative size, which is the entire reason to draw them together.
- local peak = 0
- for _, v in ipairs(rx_hist) do if v > peak then peak = v end end
- for _, v in ipairs(tx_hist) do if v > peak then peak = v end end
-
- card.plot(cr, inner.x, chart_top, inner.w, chart_h, {
- { values = rx_hist, colour = colors.ok },
- { values = tx_hist, colour = colors.highlight },
- }, peak, colors)
-
- -- The peak, so a full-height line means something in absolute terms.
- card.font(cr, card.FONT_MONO, row_size * 0.85, false)
- card.rgba(cr, colors.label)
- card.text(cr, inner.x, chart_top + row_size * 0.85, rate_str(peak))
- end
-
- -- The live rates, each in its series colour so the line and the number are
- -- unmistakably the same thing.
- local ry = inner.y + inner.h - row_size * 0.4
- card.font(cr, card.FONT_MONO, row_size, false)
- card.rgba(cr, colors.ok)
- card.text(cr, inner.x, ry, '\u{F01DA} ' .. rate_str(down))
- card.rgba(cr, colors.highlight)
- card.text_right(cr, inner.x + inner.w, ry, '\u{F0552} ' .. rate_str(up))
-end
-
-return M
diff --git a/widgets/system.lua b/widgets/system.lua
deleted file mode 100644
index c1cdf67..0000000
--- a/widgets/system.lua
+++ /dev/null
@@ -1,198 +0,0 @@
--- System: CPU load as a per-core equaliser, memory, and every temperature.
---
--- The hwmon bindings and temperature ceilings below are host-specific, which
--- is why they live here rather than in lib/data.lua: that file is the part
--- that ports to another machine, this is the part that does not.
-
-local card = require 'lib.card'
-local data = require 'lib.data'
-
-local M = {}
-
--- chip, file, label, warn, crit.
---
--- Ceilings are per sensor because a single pair cannot serve both: 70C is
--- unremarkable on a CPU package and alarming on an NVMe. Starting values,
--- easy to retune once real numbers under load are known.
-local TEMPS = {
- { 'k10temp', 'temp1_input', 'CPU', 75, 90 },
- { 'k10temp', 'temp3_input', 'CCD1', 75, 90 },
- { 'nvme', 'temp1_input', 'NVME', 60, 70 },
- { 'gigabyte_wmi', 'temp2_input', 'BOARD', 60, 70 },
- { 'gigabyte_wmi', 'temp3_input', 'BOARD', 60, 70 },
-}
-
--- One counter per core, created on first draw and kept for the process's life:
--- load is a delta between samples, so the state has to outlive the frame.
-local counters = {}
-local total_counter = data.new_cpu_counter()
-
--- Sampled every ~2s whatever the draw rate; the values between samples are
--- the last ones read. 1.9 not 2: draws jitter by a few ms either way.
-local gate = data.new_gate(1.9)
-local load, pcts = nil, {}
-
--- Read once, not per frame. Neither can change without a reboot, and
--- re-reading /proc/cpuinfo every two seconds for a constant is waste: the file
--- repeats its model-name line once per thread.
---
--- `false` is the cached miss, distinct from nil meaning "not looked up yet",
--- so an absent DMI table is not re-read on every draw.
-local cpu_name, board = nil, nil
-
-local function hardware()
- if cpu_name == nil then
- cpu_name = data.cpu_model(data.slurp('/proc/cpuinfo') or '') or false
- end
- if board == nil then
- local dmi = '/sys/devices/virtual/dmi/id/'
- board = data.board_name(data.slurp(dmi .. 'board_vendor'),
- data.slurp(dmi .. 'board_name')) or false
- end
- return cpu_name or nil, board or nil
-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
-
- if gate() then
- local stat = data.slurp('/proc/stat') or ''
- load = total_counter:sample(data.cpu_times(stat))
- for i, c in ipairs(data.per_cpu_times(stat)) do
- counters[i] = counters[i] or data.new_cpu_counter()
- pcts[i] = counters[i]:sample(c.total, c.idle) or 0
- end
- end
- local load_txt = load and string.format('%d%%', math.floor(load + 0.5)) or '--'
- local temp_n = #TEMPS
- local cpu_txt, board_txt = hardware()
-
- -- Fluid type. One base size S drives the label, headline, RAM and every
- -- temperature row; the equaliser then absorbs the vertical room left over,
- -- so the card fills its cell and S shrinks only where a row would overrun
- -- the width. card.fit_unit supplies the width side of that.
- local LABEL_F, BIG_F, ROW_F = 0.30, 1.0, 0.32
- local groups = {
- { { card.title_str(card.ICON.system, 'CPU'), card.FONT_MONO, LABEL_F },
- { load_txt, card.FONT_HEAVY, BIG_F } },
- { { cpu_txt or '', card.FONT_MONO, ROW_F } },
- { { board_txt or '', card.FONT_MONO, ROW_F } },
- { { 'RAM', card.FONT_MONO, ROW_F }, { '999.9G / 999.9G', card.FONT_MONO, ROW_F } },
- }
- for _, t in ipairs(TEMPS) do
- groups[#groups + 1] = { { t[3], card.FONT_MONO, ROW_F },
- { '888\u{00B0}', card.FONT_MONO, ROW_F } }
- end
- -- Height the non-equaliser content needs, in units of S, plus a reserve for
- -- the equaliser band. The smaller of that budget and the width fit is S.
- -- The two identification rows come out of the equaliser's budget: it is the
- -- card's designated slack absorber, so it is what gives up the height.
- local id_rows = (cpu_txt and 1 or 0) + (board_txt and 1 or 0)
- local fixed_units = 1.00 + 1.13 + temp_n * ROW_F * 1.8 + 0.10
- + id_rows * ROW_F * 1.5
- local S = clamp(math.min(inner.h * 0.96 / (fixed_units + 1.7),
- card.fit_unit(cr, inner.w * 0.96, groups, 100)), 10, 72)
- local label_size = S * LABEL_F
- local big_size = S * BIG_F
- local row_size = S * ROW_F
-
- local y = inner.y
-
- -- Aggregate load, the headline number. It takes the header line, right
- -- aligned like the big value on every other card, with the label top-left.
- card.font(cr, card.FONT_HEAVY, big_size, false)
- card.rgba(cr, load and card.threshold(load, 25, 75, colors) or colors.label)
- card.text_right(cr, inner.x + inner.w, y + big_size, load_txt)
-
- card.font(cr, card.FONT_MONO, label_size, false)
- card.title(cr, inner.x, y + label_size, card.ICON.system, 'CPU', colors)
-
- -- The hardware this card is about, under the header. Truncated by character,
- -- never by byte: a model string can carry a multi-byte character and half of
- -- one draws as a replacement box.
- -- Two accents rather than one label colour: the rows name two different
- -- pieces of hardware, and giving each its own hue separates them at a glance
- -- without a prefix like 'CPU:' eating the width. `heading` and `highlight`
- -- are the two roles that carry no state meaning, so they can stand for an
- -- identity; ok/warning/critical could not, since those say how something is
- -- doing and these strings never change.
- local id_y = y + big_size + row_size * 0.2
- if cpu_txt or board_txt then
- card.font(cr, card.FONT_MONO, row_size, false)
- local id_limit = math.max(8, math.floor(inner.w / (row_size * 0.55)))
- if cpu_txt then
- card.rgba(cr, colors.heading)
- card.text(cr, inner.x, id_y, card.truncate(cpu_txt, id_limit))
- id_y = id_y + row_size * 1.5
- end
- if board_txt then
- card.rgba(cr, colors.highlight)
- card.text(cr, inner.x, id_y, card.truncate(board_txt, id_limit))
- id_y = id_y + row_size * 1.5
- end
- end
-
- -- The equaliser: one bar per core, rising from a common baseline. It takes
- -- the room left between the header and the RAM/temperature block, so it is
- -- the card's slack absorber.
- -- Below the identification rows when there are any, otherwise where it was.
- local eq_top = (cpu_txt or board_txt) and (id_y + S * 0.10) or (y + big_size + S * 0.30)
- local bar_h = S * 0.18
- local step = row_size * 1.8
- -- The floor was a flat 18px. With the identification rows above it the
- -- equaliser can genuinely run out of room, and a fixed floor means it keeps
- -- height it does not have and the temperature rows draw over it. Zero is a
- -- legitimate outcome: the guard below skips the band when it has no height.
- local eh = clamp((inner.y + inner.h) - eq_top - (S * 1.13 + temp_n * step),
- 0, math.huge)
- local n = #pcts
- if n > 0 and eh >= 8 then
- local gap = math.max(2, inner.w * 0.006)
- local bw = (inner.w - gap * (n - 1)) / n
- -- Below about 3px a row of sixteen bars is an illegible smear; drop to the
- -- aggregate bar instead of drawing one.
- if bw >= 3 then
- for i, pct in ipairs(pcts) do
- card.vbar(cr, inner.x + (i - 1) * (bw + gap), eq_top + eh, bw, eh,
- pct / 100, card.threshold(pct, 25, 75, colors), colors)
- end
- else
- card.bar(cr, inner.x, eq_top + eh - bar_h, inner.w, bar_h, (load or 0) / 100,
- card.threshold(load or 0, 25, 75, colors), colors)
- end
- end
- local ey = eq_top + eh + S * 0.30
-
- -- Memory. MemAvailable, not free: it already excludes reclaimable cache, so
- -- it is the figure a person recognises as "used".
- local mem = data.mem_info(data.slurp('/proc/meminfo') or '')
- if mem then
- local frac = mem.used / mem.total
- card.font(cr, card.FONT_MONO, row_size, false)
- card.rgba(cr, colors.label)
- card.text(cr, inner.x, ey, 'RAM')
- card.rgba(cr, colors.value)
- card.text_right(cr, inner.x + inner.w, ey,
- string.format('%s / %s', card.human(mem.used * 1024), card.human(mem.total * 1024)))
- ey = ey + S * 0.30
- card.bar(cr, inner.x, ey, inner.w, bar_h, frac,
- card.threshold(frac * 100, 25, 75, colors), colors)
- ey = ey + bar_h + S * 0.35
- end
-
- -- Temperatures, each against its own ceiling.
- card.font(cr, card.FONT_MONO, row_size, false)
- for _, t in ipairs(TEMPS) do
- if ey + step > inner.y + inner.h then break end -- out of room: stop
- local v = data.sensor(t[1], t[2])
- card.rgba(cr, colors.label)
- card.text(cr, inner.x, ey, t[3])
- card.rgba(cr, v and card.threshold(v, t[4], t[5], colors) or colors.label)
- card.text_right(cr, inner.x + inner.w, ey,
- v and string.format('%d\u{00B0}', v) or '--')
- ey = ey + step
- end
-end
-
-return M