aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-17 10:32:51 +0200
committerDanilo M. <danix@danix.xyz>2026-09-17 10:32:51 +0200
commitb571cfae86ff5ce4b7efcf87ca880df3433654a3 (patch)
tree0d2651761245751839ef820a1c2feb8573e046cc
parent9875bfe6b38ec7ace3745ae9540e1f8655d2a116 (diff)
downloadconky-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.md221
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.