aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--docs/superpowers/specs/2026-09-17-weather-widget-design.md239
2 files changed, 240 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 54cdcff..20ba171 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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.