aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-17 08:23:01 +0200
committerDanilo M. <danix@danix.xyz>2026-09-17 08:23:01 +0200
commitb7692a3c33e6e66261c292d497a8cf1c9b8b07ba (patch)
tree2fa335b4e16d3241edcf507ce12618d40cad943d
parente704369034b444b72ae68d834073adfd0849452f (diff)
downloadconky-theme-udt-b7692a3c33e6e66261c292d497a8cf1c9b8b07ba.tar.gz
conky-theme-udt-b7692a3c33e6e66261c292d497a8cf1c9b8b07ba.zip
docs: add the weather widget design
One vertical card merging idea1.png's two: conditions on top, a sunrise-to-sunset arc at the bottom, with the times at the arc's ends behind sunrise/sunset glyphs. Decisions worth recording rather than rediscovering: - The fetch runs from `${execi}` in conky.text, not a systemd timer, so refresh is tied to the dashboard's lifetime. conky.text is empty in this config because Cairo covers it, so whether execi fires at all was probed rather than assumed: it does, even when the command prints nothing. - Inconsolata Nerd Font already carries the Weather Icons range and the two sunrise/sunset glyphs, so no new font. Confirmed by rendering the codepoints and looking at them; an fc-list query alone had wrongly reported them missing, and a missing glyph in conky is an invisible blank rather than an error. - The cache is written atomically, since the widget reads it on an unrelated 2s cadence and would otherwise parse a half-written file. - A bad key returns HTTP 200 with a JSON error body, so the fetch checks .cod before replacing the cache, and never overwrites good data with an error. - Missing and stale states draw the normal card chrome with a notice. An empty cell was rejected: it looks exactly like a crashed widget. Also ignores .superpowers/, the brainstorming session directory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-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.