diff options
Diffstat (limited to 'docs/superpowers')
| -rw-r--r-- | docs/superpowers/specs/2026-09-16-conky-lua-dashboard-design.md | 252 |
1 files changed, 252 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-09-16-conky-lua-dashboard-design.md b/docs/superpowers/specs/2026-09-16-conky-lua-dashboard-design.md new file mode 100644 index 0000000..059205b --- /dev/null +++ b/docs/superpowers/specs/2026-09-16-conky-lua-dashboard-design.md @@ -0,0 +1,252 @@ +# Conky Lua dashboard: design + +Date: 2026-09-16 +Status: approved, pending implementation plan + +## What this is + +A fullscreen Conky dashboard for Hyprland, drawn with Cairo from Lua, living on +a special workspace that a keybind and a waybar launcher toggle. + +It replaces `templates/conky.conf.in` in the `unified-desktop-theme` (UDT) repo. +That template is a 6.3K variables-and-`${execi}` config rendering a flat text +column on the desktop layer. This repo becomes the canonical Conky consumer of +the UDT palette: UDT's `install.sh` keeps rendering the template and restarting +Conky exactly as it does today, only the source of the template moves here. + +The old config is not thrown away. Its hardware discovery, in particular the +hwmon glob-by-name discipline, is the one part that must not regress and is +carried over deliberately (see "Data"). + +## Reference material + +Two mockups in the repo root, both **layout and shape only, not colour**. +Colour comes from the UDT palette in every case. + +`idea1.png` (2560x1600): card grid, thin-bordered rounded rectangles, monospace +labels in small caps over larger values. Supplies the card vocabulary. + +`idea2.png` (307x773): the clock treatment. Very heavy numerals, hour stacked +over minute, left-aligned, tight leading; small-caps weekday beneath; date as +`12 / MAY / 2023` with slash separators. Also shows ring gauges and album art, +useful for later widgets. + +### Deliberately not taken from the mockups + +- **Lumon branding and "UNITED IN SEVERANCE"** — user asked for layout only. +- **Task list with a Sync button** — needs an application backend, not a system + monitor's job. +- **"Sent to Jackie" mic button** — interactive. Conky can draw a button; it + cannot receive a click. +- **`idea1.png`'s vertical right-hand stack** — does not fit the target screen. + See "Screen geometry". + +## Screen geometry + +The primary monitor is **2560x1080**; the secondary is 1920x1080. `idea1.png` is +2560x1600, so its four-card vertical stack cannot be reproduced at 1080px tall. + +This is why the layout is expressed in **grid cells** rather than pixels: the +cell size derives from the actual surface dimensions, so one layout table works +on both monitors and the column count, not a set of hand-tuned offsets, decides +whether the result reads as horizontal or dense. + +v1 targets the primary monitor. Per-monitor layout variants are not built (see +"Explicitly out of scope"). + +## Verified platform facts + +Established by experiment on this machine before the design was fixed, because +each one invalidates a common approach. + +**Conky build:** 1.24.2-pre, with Lua bindings for Cairo and Imlib2, ARGB visual, +and Wayland output. Lua 5.4.9. + +**Cairo on Wayland needs `conky_surface()`.** The idiom in essentially every +Conky-Lua tutorial is: + + cairo_xlib_surface_create(conky_window.display, conky_window.drawable, ...) + +Under `out_to_wayland = true` there is no Xlib drawable: `conky_window.drawable` +and `conky_window.visual` are both **nil**, and Conky emits a deprecation +warning pointing at `conky_surface()` instead. `conky_surface()` was tested and +returns a live surface; Cairo fills, Nerd Font text via `cairo_show_text`, and +`conky_parse()` all work on it. A `grim` screenshot confirmed the output reaches +the screen, so this is not merely "the calls return without error". + +**Conky renders as a real toplevel.** With `own_window_type = 'normal'` the +window appears in `hyprctl clients` with `class=conky`, floating, on a normal +workspace, which is what makes a Hyprland `windowrule` able to place it. The +existing desktop-layer config uses `own_window_type = 'desktop'`, which is a +layer-surface at level 0 and cannot be assigned to a workspace at all. + +**Cairo output covers `conky.text`.** A `conky.text` of `hello` did not appear +under the Cairo drawing. `conky.text` is therefore left empty. + +## Architecture + + conky.conf.in UDT-templated config: @ROLE@ placeholders, lua_load + dashboard.lua entry point; owns the layout table and the grid + lib/card.lua Cairo primitives: rounded card, text, gauge + lib/data.lua /proc and /sys readers + widgets/<name>.lua one file per widget + +`conky.text` is empty; all drawing happens in `lua_draw_hook_post`. + +### Layout: the user owns the order + +A single table at the top of `dashboard.lua` is the layout. Reordering widgets +means editing that table and nothing else. + + -- Layout. Order and position are yours: edit this table, nothing else. + -- col/row are grid cells; w/h span cells. Grid auto-sizes to the screen. + local layout = { + { widget = 'clock', col = 1, row = 1, w = 1, h = 2 }, + { widget = 'system', col = 2, row = 1, w = 2, h = 1 }, + { widget = 'weather', col = 2, row = 2, w = 1, h = 1 }, + { widget = 'network', col = 3, row = 2, w = 1, h = 1 }, + } + +Consequences, and they are the point: + +- A widget receives a rectangle and draws inside it. It never knows where it is, + so moving it cannot break it. +- Cell size is derived from the surface dimensions, so the same table works on + either monitor. +- Adding a widget is a new file in `widgets/` plus one row in the table. + +Grid cells mean cards snap to the grid; a card cannot sit at an arbitrary pixel +offset. This was chosen over an absolute-placement escape hatch because cells +give alignment for free and keep realigning when the grid or the screen changes. + +### Widget interface + +Every widget exports one function: + + function draw(cr, rect, colors) -- rect = {x, y, w, h} + +`rect` is computed by the grid. `colors` is the palette table. A widget draws +only inside its rect; it does not read the layout, other widgets, or global +position. + +### Palette + +No hex literals in Lua. `conky.conf.in` carries the `@HEADING@`, `@LABEL@`, +`@VALUE@`, `@RULE@`, `@BODY@` placeholders that the existing template already +uses, and UDT's `bin/udt-palette` fills them from `palette/roles-<scheme>.conf`. +Lua reads the resulting values from `conky.config` once per draw cycle and hands +them to widgets as `colors`. + +Scheme switching therefore keeps working unchanged: edit +`~/.config/udt/roles.conf`, run `./install.sh`. The `[conky]` role block already +exists in every shipped scheme, so no palette file needs touching. + +### Data + +`lib/data.lua` reads `/proc/stat`, `/proc/meminfo` and `/sys/class/hwmon/*` +directly, replacing the `${execi ... | grep | awk}` pipelines of the old +template. Motivation is correctness and cost: those pipelines spawn several +processes every few seconds. + +**hwmon is globbed by its `name` file, never by a fixed index.** Carried over +verbatim from the old config, where it is the hard-won part: + + /sys/class/hwmon/hwmon*/name -> match exact name -> read tempN_input + +Fixed `${hwmon N}` indices drift across kernel and hardware reorders and then +silently report the wrong chip. Known mapping on this host: CPU TCTL is +`k10temp` temp1; NVMe is `nvme` temp1; motherboard is `gigabyte_wmi` temp2 and +temp3 (that chip exposes five unlabeled temps, chosen empirically); GPU (Intel +Arc B580) is `xe` temp2 package, temp3 vram, fan1 RPM. + +These bindings are host-specific and will not port to another machine. + +Data that genuinely needs a shell stays a shell call, kept behind a `data.lua` +function: `playerctl` for MPRIS now-playing, and the weather fetch below. + +### Weather + +Deferred past the v1 slice, then implemented as a fetch script writing a cache +file that `data.lua` reads. The dashboard never blocks on the network. + +`~/Programming/waybar-weather.py` exists and is the starting point, but it is +not reusable as-is: it scrapes `weather.com` HTML with `pyquery`, a third-party +dependency, and screen-scraping breaks whenever the site's markup changes. The +replacement targets a documented endpoint (`wttr.in` JSON, or an API with a key) +and is reimplemented rather than copied. The location id in that script is +personal data and does not move into this repo. + +## v1 slice: one card, one widget, end to end + +The clock card, shaped after `idea2.png`, and nothing else. + +That single card exercises the entire pipeline: UDT renders the template from +palette roles, Conky loads the Lua, the grid computes a rect, Cairo draws on a +`conky_surface()`, and Hyprland places the window on the special workspace. + +Every widget after it is additive: one file, one table row. + +## Hyprland integration + + own_window_class = 'conky-dash' + +Distinct from the existing desktop-layer instance's `Conky`, so the two can +never match each other's rules. + + windowrulev2 = workspace special:dash silent, class:^(conky-dash)$ + windowrulev2 = fullscreen, class:^(conky-dash)$ + windowrulev2 = noborder, class:^(conky-dash)$ + +Toggle, bound to both a keybind and a waybar launcher: + + hyprctl dispatch togglespecialworkspace dash + +**Lifetime: always running, started with the session, no hidden-state +optimization.** Hiding is a workspace switch, so showing is instant and graph +history survives. Skipping draws while the workspace is hidden was considered +and rejected as unmeasured: Conky at a 2-3s interval is cheap, and the check +costs a `hyprctl` call per cycle. Revisit only if idle CPU actually shows up. + +## Error handling + +**Conky reports a Lua fault as a blank screen and nothing else.** No stderr, no +log line: a typo in a Cairo call yields an empty dashboard. + +So the draw body runs inside `pcall`, and on failure the error string is drawn +onto the surface in the palette's critical colour. Three lines, and every future +failure becomes a readable message instead of a black rectangle. + +This is the only error handling the dashboard needs; a widget that cannot read +its data draws a dash, it does not abort the frame. + +## Verification + +**Parsers:** one runnable check, `assert`-based, run with `lua`. Committed +fixture text for `/proc/stat`, `/proc/meminfo` and a hwmon `temp*_input` is +parsed and the results asserted. Parsers are what break silently on a kernel +change, so they are what gets a test. No framework, no fixtures directory. + +**Rendering:** screenshot loop, the one used to verify `conky_surface()`: + + conky -c ./conky.conf & + grim -g "$(hyprctl clients -j | jq -r '.[]|select(.class=="conky-dash")|"\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"')" cap.png + +Cairo correctness is a visual property; a screenshot is the honest check. + +## Explicitly out of scope + +Per-monitor layout variants. Absolute pixel placement. A configuration UI or +drag-to-place. Click interaction of any kind. The task list and mic button from +`idea1.png`. Light-scheme support, which UDT does not have either. + +## Open items + +Both deferred by design, neither blocks v1. + +- Weather source: `wttr.in` versus a keyed API. Decided when the widget is built. +- Album art via Imlib2 for the media widget, as in `idea2.png`. Needs a check + that Imlib2 bindings work on a Wayland `conky_surface()`, which was not tested. + +## License + +GPLv2 only, matching UDT. |
