# 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/.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-.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. **Source: OpenWeatherMap**, via the current-weather endpoint. Chosen over `wttr.in` because a working reference implementation already exists, with a condition mapping worth keeping. Two prior scripts were examined: `~/Programming/waybar-weather.py` is **rejected**. It scrapes `weather.com` HTML with `pyquery`, a third-party dependency, and screen-scraping breaks whenever the markup changes. `/data/udt-backup/polybar/modules/weather/openweathermap-simple.sh` is the **reference**. What is worth porting is its accumulated domain knowledge, not its code: - The OWM condition-id mapping: `<=232` thunderstorm, `<=311` light drizzle, `<=321` heavy drizzle, `<=531` rain, `<=622` snow, `<=771` fog, `781` tornado, `800` clear, `801` few clouds, `<=804` overcast. - Day/night icon selection by comparing now against `sys.sunrise` and `sys.sunset` from the same response. - The Beaufort thresholds for the wind-force icon. That becomes a table in `data.lua`. Nothing downstream of the data ports. `%{F...}` and `%{T...}` are polybar formatting tokens, meaningless to Cairo; the hardcoded hex colours are replaced by UDT roles; the `bc` unit conversions become Lua arithmetic; and the compact one-line output becomes a card. So the script shrinks to fetch-and-cache: curl the endpoint, write the JSON to a cache file, nothing else. `data.lua` parses that file and owns the icon mapping. OWM's free tier allows 60 calls/minute, so a 15-minute refresh is far inside it. #### Secrets and personal data stay outside the repo The reference script hardcodes a **live API key** in plaintext, along with `CITY_NAME` and `COUNTRY_CODE`. None of that is copied here. The key and location are read from `~/.config/udt/weather.env`, outside the repo, following the same reasoning as UDT's `~/.config/udt/roles.conf`: local state, never committed, and switching machines does not dirty the tree. The fetch script exits with a clear message when the file or key is missing, rather than silently querying without one. The key in the backup script must be treated as exposed and rotated: that file is mode 755 under a world-readable path. This is flagged for the user; the design simply never carries a key in-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 Deferred by design, does not block v1. - 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. Resolved: the weather source is OpenWeatherMap, see "Weather". ## License GPLv2 only, matching UDT.