diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-09-17-weather-widget-design.md | 239 |
2 files changed, 240 insertions, 0 deletions
@@ -1,3 +1,4 @@ HANDOFF.md weather.env conky.conf +.superpowers/ diff --git a/docs/superpowers/specs/2026-09-17-weather-widget-design.md b/docs/superpowers/specs/2026-09-17-weather-widget-design.md new file mode 100644 index 0000000..412adac --- /dev/null +++ b/docs/superpowers/specs/2026-09-17-weather-widget-design.md @@ -0,0 +1,239 @@ +# Weather widget design + +Date: 2026-09-17 +Status: approved, not yet implemented + +A single vertical card for the conky Lua dashboard: current conditions on top, +a sunrise-to-sunset arc at the bottom. Shaped after `idea1.png`, which splits +the same information across two cards; this merges them into one 1x2 cell, as +requested. + +This builds on the v1 design +(`2026-09-16-conky-lua-dashboard-design.md`), whose Weather section specified +the data source and the secrets handling. That section stands. What follows +adds the card, the arc, and the decisions that section deferred. + +## What it shows + +Top to bottom, inside the padded rect `card.card()` returns: + +| Band | Content | +|-----------|------------------------------------------------------| +| Header | condition glyph, then the temperature with the city beneath it | +| Condition | OWM's own `weather[0].description`, first letter capitalised | +| rule | hairline in `colors.rule` | +| Stats | `FEELS LIKE`, `HUMIDITY`, `WIND` (direction arrow + speed) | +| rule | hairline | +| Sun arc | curve, baseline, sun dot, sunrise and sunset times at the ends | + +Three stat rows, as in `idea1.png`. Wind direction rides inside the existing +wind row as an arrow glyph before the speed rather than claiming a fourth row. + +Sizes derive from the inner rect the way `clock.lua` derives its numerals, not +fixed pixels, so the card still composes on the 1920-wide monitor. + +## Architecture + + bin/weather-fetch.sh + reads ~/.config/udt/weather.env (KEY, CITY, COUNTRY, UNITS) + curl api.openweathermap.org/data/2.5/weather + writes ~/.cache/udt/weather.json (whole response) + + conky.conf.in + conky.text = ${execi 900 ~/.config/conky/bin/weather-fetch.sh} + + lib/weather.lua parses the cache, owns the domain tables + widgets/weather.lua draws the card + +The dashboard never blocks on the network: the widget only ever reads a local +file, and the fetch happens on conky's own `execi` schedule. + +### Why `${execi}` and not a systemd timer + +Refresh is tied to the dashboard's lifetime. Nothing fetches while conky is +down, which is exactly when nobody is looking at the card, and there is no +second installation step or unit file to keep in sync. `install.sh` already +owns the rendered config. + +**`conky.text` is empty in this config**, because Cairo output covers it, so +whether `execi` fires at all had to be checked rather than assumed. It does: +a probe config whose only text was an `execi` producing no output still ran the +command on schedule (verified 2026-09-17, conky 1.22 on this host, two +invocations over a 12s run at `execi 2`). The text block therefore changes from +`[[]]` to the single `execi` line, which stays invisible beneath the Cairo +layer. + +### Why a separate `lib/weather.lua` + +`lib/data.lua` parses `/proc` and `/sys`. Weather is a different source with +its own domain tables (condition ids, Beaufort, compass points) and its own +test file. It keeps data.lua's testable shape: functions take the file +*contents* as a string and return a table, so the tests need no filesystem. + +### Cache writes are atomic + +The fetch writes `weather.json.tmp` and renames it over the target. The widget +reads that file on an unrelated 2-second cadence, so a curl killed mid-write +would otherwise hand the parser a truncated response. Rename is atomic within a +filesystem; the temp file therefore lives in the same directory as the target. + +### JSON parsing without a JSON library + +Lua patterns over the handful of fields the card draws, matching scalars by key +(`"temp":([%d%.%-]+)`). OWM's current-weather response is flat and known. +Adding a dependency to read six numbers fails the ladder. This is why caching +the whole response costs nothing: the parser only looks at what it needs, and a +later field is already on disk. + +The parser returns `nil` on anything it cannot read rather than raising, since +a Lua error in this project is a blank screen. + +## The arc + +The only non-trivial drawing in the widget. + +A Bezier curve spanning the band's width via `cairo_curve_to`, with a baseline +beneath it. The sun's position along it: + + t = (now - sunrise) / (sunset - sunrise) clamped to 0..1 + +The dot is placed by **evaluating the Bezier at `t`**, not by computing a point +on a circle. The curve is already the path; evaluating it keeps the dot on the +curve if the control points are ever adjusted, where a separately derived +circle would drift off it. + +Behaviour outside daylight: + +- Before sunrise `t` is negative, after sunset greater than 1. The clamp parks + the dot at the corresponding end. +- At night the dot takes `colors.label` rather than the bright fill, so a + parked dot does not read as "the sun is up". + +OWM returns *today's* sunrise and sunset, so between midnight and sunrise the +numerator is negative. The clamp is the whole handling. Multi-day astronomy +buys nothing for a dot on an arc. + +Sunrise and sunset times sit at the ends of the arc, each behind a Nerd Font +glyph (U+E34C sunrise, U+E34D sunset) rather than a bare time. Both glyphs were +rendered from `Inconsolata Nerd Font` and inspected before being chosen: they +are a sun with an up arrow and a sun with a down arrow, visually distinct at +the size used. + +## Fonts + +No new font. `Inconsolata Nerd Font`, already `card.FONT_MONO`, carries the +Weather Icons range the reference script used (U+E3xx) and the two +sunrise/sunset glyphs. Confirmed by rendering the actual codepoints and looking +at the result, not by a fontconfig query alone: an early `fc-list` check +reported the glyphs missing, which was the query's fault, and a missing glyph +in conky is an invisible blank rather than an error. + +## Domain tables, ported from the polybar script + +`/data/udt-backup/polybar/modules/weather/openweathermap-simple.sh` is the +reference. Its accumulated knowledge ports; none of its code does. + +**Condition id to icon**, by upper bound: `<=232` thunderstorm, `<=311` light +drizzle, `<=321` heavy drizzle, `<=531` rain, `<=622` snow, `<=771` fog, `781` +tornado, `800` clear, `801` few clouds, `<=804` overcast, anything else an +error glyph. + +**Day and night variants** for the ids that have them (thunderstorm, both +drizzles, rain, clear, few clouds), selected by comparing now against +`sys.sunrise` and `sys.sunset` from the same response. Snow, fog and tornado +have a single icon in the reference and keep one here. + +**Beaufort thresholds** for the wind glyph, in km/h: 1, 5, 11, 19, 28, 38, 49, +61, 74, 88, 102, 117. + +**Wind direction** is new, not in the reference: `wind.deg` binned to eight +compass points, each an arrow glyph. Bin boundaries are offset by half a step +so that north spans 348.75 to 11.25 degrees rather than starting at zero. + +Units: OWM `metric` gives m/s for wind, so the card converts to km/h +(`* 3.6`). The reference's knots conversion and its `MIN_WIND` suppression are +dropped; the card always shows the wind row. + +## Failure and staleness + +One code path, three states, all drawing the normal card chrome so the +dashboard keeps its shape: + +| State | Card shows | +|---|---| +| `weather.env` missing | `no weather data`, then `set ~/.config/udt/weather.env` | +| cache missing or unparseable | `no weather data`, then `waiting for first fetch` | +| cache older than 45 minutes | the data, drawn normally, plus a dim `stale <age>` by the city | + +An empty cell was rejected: it is indistinguishable from a crashed widget, +which is a failure mode this project has already fought once. Silently showing +old values was rejected as a correctness bug. + +The 45-minute threshold is three missed fetches at the 15-minute interval, so a +single transient failure does not flag the card. + +The fetch script exits non-zero with a message on stderr when the key is +missing or curl fails, and **leaves any existing cache untouched** rather than +overwriting it with an error body. OWM returns HTTP 200 with a JSON error body +for a bad key, so the script checks that `.cod` is 200 before replacing the +cache. That check is carried over from the reference script, which learned it +the same way. + +## Secrets and personal data + +Nothing sensitive enters the repo. + +`~/.config/udt/weather.env` holds `KEY`, `CITY`, `COUNTRY` and `UNITS`. It is +outside the repo, already covered by `.gitignore`, and does not exist yet, so +the "set weather.env" state above is what the card shows on first run. A +`weather.env.example` with placeholder values ships in the repo. + +The location lives in that file rather than in the source, so no real location +appears in committed code. + +The API key hardcoded in the reference script must be treated as exposed and +revoked: that file is mode 755 under a world-readable path. This design never +carries a key in-repo. + +## Tests + +`test/test_weather.lua`, in the style of the existing two, with a fixture OWM +response under `test/fixtures/` whose key and city are placeholders. + +- condition id to icon at every range boundary: 232/233, 311/312, 321/322, + 531/532, 622/623, 771/772, 781, 800, 801, 804, and an unknown id +- day and night selection for the ids that have both, by moving `now` across + sunrise and sunset +- Beaufort binning at each threshold and just either side of it +- `wind.deg` to arrow for all eight points, including the wraparound at 348.75 + and 360 degrees +- sun position `t` at sunrise, midday and sunset, and the clamp before dawn and + after dusk +- truncated and garbage JSON returning nil rather than raising +- staleness classification at the 45-minute boundary + +Run with the existing two: + + lua test/test_data.lua && lua test/test_layout.lua && lua test/test_weather.lua + +The card itself is verified by screenshot, which means looking at the PNG. + +## Files + +| Path | Change | +|---|---| +| `bin/weather-fetch.sh` | new, fetch and cache | +| `lib/weather.lua` | new, parse and domain tables | +| `widgets/weather.lua` | new, the card | +| `test/test_weather.lua` | new | +| `test/fixtures/weather.json` | new | +| `weather.env.example` | new | +| `conky.conf.in` | `conky.text` gains the `execi` line | +| `dashboard.lua` | one row in the `layout` table | +| `install.sh` (UDT repo) | symlink `bin/` alongside `lib/` and `widgets/`; also prefix its conky restart with `[workspace special:dash silent]`, which it currently lacks, so a reinstall does not pop the dashboard open | +| `README.md` | the widget, and the `weather.env` setup step | + +## Out of scope + +Forecast, hourly or daily. The current-weather endpoint has none of it, and the +card has no room. A forecast card would be its own widget and its own design. |
