diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-17 10:32:51 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-17 10:32:51 +0200 |
| commit | b571cfae86ff5ce4b7efcf87ca880df3433654a3 (patch) | |
| tree | 0d2651761245751839ef820a1c2feb8573e046cc | |
| parent | 9875bfe6b38ec7ace3745ae9540e1f8655d2a116 (diff) | |
| download | conky-theme-udt-b571cfae86ff5ce4b7efcf87ca880df3433654a3.tar.gz conky-theme-udt-b571cfae86ff5ce4b7efcf87ca880df3433654a3.zip | |
docs: add the system widgets design
Four cards: CPU/RAM with every temperature, GPU, filesystems, and the
user cache. Three were asked for; cache became its own card because it
is a different refresh cycle and sharing the disks card would have made
the densest card in the dashboard.
Two findings that shaped it, both from probing the hardware rather than
assuming:
- The Arc B580 exposes no utilisation figure. The xe driver has no
gpu_busy_percent, intel_gpu_top refuses the device outright, and
gputop prints per-process rows with ANSI escapes, which is not an
interface to build on. The card shows temperatures and fan speed and
no load bar, rather than inventing a number.
- /data is on a spinning disk with no hwmon chip, so there is no sda
temperature to show. Reading SMART would need smartctl as root and
would spin up a sleeping disk each poll.
Also: the four NFS mounts are two filesystems behind two servers, so
showing all four printed every number twice. One mount per server.
Both new samplers follow the weather fetch's cache-and-read pattern,
which exists precisely so an unreachable NFS server costs a stale cache
instead of freezing the draw hook.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| -rw-r--r-- | docs/superpowers/specs/2026-09-17-system-widgets-design.md | 221 |
1 files changed, 221 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-09-17-system-widgets-design.md b/docs/superpowers/specs/2026-09-17-system-widgets-design.md new file mode 100644 index 0000000..eea3a88 --- /dev/null +++ b/docs/superpowers/specs/2026-09-17-system-widgets-design.md @@ -0,0 +1,221 @@ +# System widgets design + +Date: 2026-09-17 +Status: approved, not yet implemented + +Four cards covering the machine itself: CPU and memory with every temperature, +the GPU, filesystem capacity, and the user cache. + +The original request was three widgets (CPU/RAM/temps, GPU, disks). Cache +became a fourth during design: it is a different kind of data on a much slower +refresh cycle, and sharing a card with the filesystems would have made the +densest card in the dashboard. + +This follows the v1 design (`2026-09-16-conky-lua-dashboard-design.md`) and the +weather widget (`2026-09-17-weather-widget-design.md`), whose cache-and-read +pattern and fluid-widget convention both carry over unchanged. + +## The four cards + +| Widget | Shows | Source | +|---|---|---| +| `system` | CPU load, 16 per-core bars, RAM used/total, and every temperature: Tctl, Tccd1, motherboard x2, NVMe | `/proc/stat`, `/proc/meminfo`, hwmon | +| `gpu` | Arc B580 package temp, VRAM temp, fan RPM | hwmon `xe` | +| `disks` | `/`, `/home`, `/data`, plus one NFS mount per server, with usage bars | cache file | +| `cache` | `~/.cache` total and its four largest subdirectories | cache file | + +## Host bindings + +These are specific to this machine and will not port. They are stated here +because the alternative is rediscovering them from a wrong reading. + + k10temp temp1 Tctl CPU package + temp3 Tccd1 CCD die + nvme temp1 Composite WD_BLACK SN850X + xe temp2 pkg Arc B580 package + temp3 vram Arc B580 memory + fan1 Arc B580 fan RPM + gigabyte_wmi temp2, temp3 motherboard, X870 Eagle WiFi 7 + +**hwmon is globbed by its `name` file, never by index.** Indices drift across +kernel and hardware reorders and a stale one silently reports a different chip. +`lib/data.lua` already has `hwmon_dir()` for this. + +The `gigabyte_wmi` chip exposes five unlabelled temperatures. The old conky +config showed `temp2` and `temp3`, chosen empirically; that choice is carried +over, and the uncertainty carried over with it. They are labelled "board" +rather than given a specific meaning they may not have. + +`Tccd1` is read from sysfs (`temp3_input`), not through the `sensors` command +the old config shelled out to. Same number, no subprocess. + +### What the hardware does not expose + +**The Arc B580 reports no utilisation.** The `xe` driver exposes no +`gpu_busy_percent`, unlike `amdgpu`. `intel_gpu_top` refuses the device +("Detected Xe device which is not supported"), and `gputop` can read per-engine +load but prints per-process rows with ANSI escapes and would need a sampler and +a parser for a format that is not a stable interface. The GPU card therefore +shows temperatures and fan speed, and no load bar. This is honest about the +hardware rather than inventing a number. + +The AMD integrated GPU does expose `gpu_busy_percent`, but is deliberately not +shown: the discrete card is the one in use. + +**`sda` reports no temperature.** `/data` sits on a WD spinning disk with no +hwmon chip; reading its SMART temperature would need `smartctl` (not installed) +running as root, and would spin up a sleeping disk on every poll. The NVMe +composite temperature is freely readable and is shown; `sda` simply has none. + +## Data + +### Additions to `lib/data.lua` + +Most of what these widgets need already exists and is tested: `cpu_times`, +`mem_info`, `millidegrees`, `slurp`, `hwmon_dir`, `new_cpu_counter`. Three +functions are added, in the same shape as the rest, taking file *contents* as a +string so the tests need no filesystem: + +- `per_cpu_times(stat)` — the `cpu0..cpuN` lines rather than only the + aggregate, returning a list of `{total, idle}`. The existing + `new_cpu_counter` then works per core with no change, which is exactly why it + was written as an instance holding its own previous sample. +- `sensor(chip, file)` — `hwmon_dir` plus `millidegrees`, so a widget writes + `data.sensor('k10temp', 'temp1_input')` instead of building paths. Returns + nil when the chip or file is absent. +- `df_parse(text)` and `du_parse(text)` — parse the two cache files below. + +Sensor bindings stay in the widgets. They are host-specific, and `data.lua` is +the part that is not. + +### The two sampler scripts + +Same pattern as `bin/weather-fetch.sh`, for the same reason: the draw hook +never runs a subprocess and never blocks. + + bin/disks-sample.sh df -P over the local mounts and the two NFS mounts + -> ~/.cache/udt/disks.txt every 60s + + bin/cache-sample.sh du -sh ~/.cache and its children + -> ~/.cache/udt/cachesize.txt every 900s + +Both write a temp file in the same directory and rename over the target, under +`umask 077`, exactly as the weather fetch does. Rename is atomic within a +filesystem; the widget reads on the 2-second draw cadence and would otherwise +see a half-written file. + +`conky.text` gains two more `${execi}` entries alongside the weather one. That +block renders nothing (Cairo covers it) but still fires, which was verified with +a probe config before the weather widget relied on it. + +**Why the disks sampler exists at all**: `statfs` on an NFS path blocks when the +server is unreachable. Called from the draw hook that would freeze the whole +dashboard, which is the one failure this project has worked hardest to avoid. +In the sampler it costs a stale cache and nothing else, and the widget shows the +age exactly as the weather card does. + +**Why the cache sampler is separate and slower**: `du -sh ~/.cache` walks the +tree and takes about 100ms warm. That is fine every 15 minutes and unthinkable +every 2 seconds. The old conky config used 900s for the same reason. + +### NFS: one mount per server + +Four NFS mounts are configured, but they are two filesystems: + + Library, Slackware one server, same export, identical figures + shared, backup_danix another server, same export, identical figures + +Showing all four repeats each number twice. The card shows `Library` and +`shared`, one per server. + +## Drawing + +All four follow the fluid convention documented in the README: sizes derive +from `inner.w` with clamps, the slack is distributed deliberately, and an +element that cannot fit drops out rather than overlapping. + +### A shared bar primitive + +`lib/card.lua` gains `card.bar(cr, x, y, w, h, frac, colors, colour)`: a track +with a filled portion. Three of the four widgets need it (per-core load, RAM, +filesystem usage), which is the third use that earns the abstraction rather +than each widget drawing its own rectangles. + +It takes an explicit colour so a bar can turn `colors.critical` past a +threshold, which is what makes a filesystem at 84% readable at a glance. + +### The per-core row + +Sixteen thin vertical bars across the card width is the one novel layout. Bar +width derives from the cell, and below the width where sixteen would be thinner +than about 3px the widget drops to the aggregate bar alone rather than drawing a +grey smear. + +## Staleness and failure + +Each widget draws its card chrome and a notice rather than vanishing, exactly +as the weather card does, because an empty cell is indistinguishable from a +crashed widget. + +- A missing sensor shows `--`, not 0. Zero degrees is a plausible reading. +- A missing cache file shows `no data` and the sampler that should have written + it. +- A cache older than ten times its sampling interval shows a dim age marker + beside the heading, so an unreachable NFS server or a dead sampler is visible + rather than silently showing yesterday's numbers. + +## Tests + +`test/test_data.lua` extends with fixtures under `test/fixtures/`: + +- a `/proc/stat` with per-core lines, checking `per_cpu_times` returns one entry + per core and that `new_cpu_counter` gives a sane percentage per core across + two samples +- a `df -P` capture including the NFS rows, checking `df_parse` handles an NFS + device field containing a colon (`server:/export`, which a naive split on + punctuation would break), a mount at 100%, and a trailing blank line. `df -P` + guarantees one line per mount with the mountpoint last, which is why the + parser takes the last field rather than the sixth +- a `du -sh` capture, checking `du_parse` orders by size and handles the + human-readable suffixes rather than sorting them as strings: `1.1G` must + outrank `245M`, which a string sort gets backwards. `K`, `M`, `G` all occur + in the current cache and `T` occurs in `df` output, so all four are converted +- truncated and empty inputs to both parsers returning an empty table rather + than raising + +Run with the others: + + lua test/test_data.lua && lua test/test_layout.lua && lua test/test_weather.lua + +The cards themselves are verified by rendering them offscreen at several cell +sizes and looking at the PNG, then in the live dashboard. + +## Files + +| Path | Change | +|---|---| +| `widgets/system.lua` | new | +| `widgets/gpu.lua` | new | +| `widgets/disks.lua` | new | +| `widgets/cache.lua` | new | +| `bin/disks-sample.sh` | new | +| `bin/cache-sample.sh` | new | +| `lib/data.lua` | `per_cpu_times`, `sensor`, `df_parse`, `du_parse` | +| `lib/card.lua` | `card.bar` | +| `test/test_data.lua` | fixtures and assertions for the new parsers | +| `conky.conf.in` | two more `${execi}` entries | +| `dashboard.lua` | four rows in the `layout` table | +| `README.md` | the widgets, the samplers, the host bindings | + +## Out of scope + +**Disk I/O rates.** The old config showed read/write throughput via conky's +`${diskio}`, which is a built-in the Lua side does not get; it would mean +parsing `/proc/diskstats` and holding per-draw deltas. Worth doing, but it is +its own piece of work and the card is already full. + +**GPU utilisation.** See above: the hardware does not expose it through any +interface stable enough to build on. + +**Network.** Named in the original plan, still unspecified, and unaffected by +any of this. |
