aboutsummaryrefslogtreecommitdiffstats
path: root/widgets
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-13 11:35:00 +0200
committerDanilo M. <danix@danix.xyz>2026-10-13 11:35:00 +0200
commit3943e08dabe684b409a3ac99c9371fbeb5df5cdb (patch)
tree7dcedbb2d64a04e59ab239a8c40c04f0f0cf29aa /widgets
parentb2fe3f493efc12dd300f0e2b2ede5660ee70c193 (diff)
downloadconky-theme-udt-3943e08dabe684b409a3ac99c9371fbeb5df5cdb.tar.gz
conky-theme-udt-3943e08dabe684b409a3ac99c9371fbeb5df5cdb.zip
feat: system, gpu, disks and network move to the quickshell islandHEADmaster
The quickshell island's performance panel now shows CPU, RAM, temperatures, the GPU, disks and network, and warns from the status pill when something runs hot, fills up, or the public IP drifts from the pinned one. Two dashboards drawing the same numbers would drift apart, so the four widgets go, with the disks and public IP samplers. The island keeps writing ~/.cache/udt/pubip.txt in the same format. The freed space is left empty for the user to rearrange. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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