aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-09-16-conky-lua-dashboard-design.md
blob: 08b434bc1fd2051c55c99c918e0e95afdeb7ceb5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# 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.

**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.