# Mirage weather cards: design Date: 2026-10-18 ## Goal With most system cards moved to quickshell, the conky dashboard keeps the time-and-place cards. This adds a fuller weather set, adapted from the Conky-Mirage theme by alexsson-xexpanderx (https://github.com/alexsson-xexpanderx/Conky-themes/tree/master/Conky-Mirage), drawn in this repository's own design language. Mirage's tiles come over as ideas and maths, not as code. Mirage's look (glass cut from the wallpaper, wallpaper-derived palette, animated icons) does not. ## Scope In: - Five weather cards: `now`, `hourly`, `week`, `details`, `sky`. - The current `weather` card is removed; `now` and `sky` cover it. - Data moves from OpenWeatherMap to Open-Meteo (no key; hourly, daily, UV and air quality available). - Credit to Mirage in each new widget's header comment and in README.md. Out (deliberately): - Animated icons. The dashboard stays at its 2s update interval. - Wallpaper glass, wallpaper palette, Mirage's fold-to-strip, its 12h option. - Mirage's system tile. - IP-based geolocation. Location comes from `weather.env` only. - A second conky instance. Everything runs in the existing `dashboard.lua` instance on the Hyprland special workspace `dash`. ## Look The hybrid rule: DESIGN.md wins wherever it has an answer, and Mirage supplies only what DESIGN.md calls a card's variable band. - Palette roles, fonts (`FONT_MONO`, `FONT_HEAVY`, `FONT_UI`) and chrome come from `lib/card.lua`. - Icons are Nerd Font weather glyphs, as today. Moon phases use the Nerd Font `moon_*` glyph set (28 phases). Every new codepoint is rendered and looked at before it is trusted (DESIGN.md, Glyphs). - Every size derives from the card's rect. ## Cards | Card | Header left | Big value (top-right) | Rows | Variable band | |---|---|---|---|---| | `now` | condition glyph | current temperature, `body` | feels like, high / low, rain chance, last update | the condition glyph, large, in the leftover height | | `hourly` | glyph + "Next 24h" | next hour's temperature, `body` | none | temperature curve over 24 hours; every 3rd hour labelled with glyph and temperature; rain-chance bars under the curve in `value`, half alpha, with a minimum height so a low chance still shows | | `week` | glyph + "Week" | none (band carries the value) | one per day: day name, glyph, min, range bar, max | the range bar in each row, positioned on the week's overall min..max scale | | `details` | glyph + "Details" | none | wind (speed + compass point), humidity, UV, AQI | a small wind compass with a needle at the wind direction | | `sky` | glyph + "Sky" | moon-phase glyph, `heading` | sunrise, sunset, day length, moon illumination % | the daylight arc, moved from today's `widgets/weather.lua` | ### Colour thresholds Measured quantities use the `ok` / `warning` / `critical` roles: | Quantity | ok | warning | critical | |---|---|---|---| | UV index | < 3 | 3 to < 8 | >= 8 | | European AQI | <= 40 | <= 80 | > 80 | Humidity, wind and temperatures stay in `value` / `body`. This restores a `critical` user to the board, closing the open question in HANDOFF about DESIGN.md's three-states rule. ### Failure states - No cache, or a cache that fails to parse: every card keeps its chrome and draws `no weather data`. - A missing field: `--`, never `0`. - A stale cache (older than `STALE_AFTER`, 45 min): cards draw, and `now` shows the age, as the current card does. - No air-quality line: AQI row shows `--`; the rest of the card is unaffected. ## Data ### Fetcher: `bin/weather-fetch.sh` Rewritten in bash with curl and jq, the tools it already uses. 1. Reads `~/.config/udt/weather.env`: `CITY`, `COUNTRY`, `UNITS` (`metric` or `imperial`). `KEY` is gone. 2. Geocodes once via `https://geocoding-api.open-meteo.com/v1/search` (name = `CITY`, filtered by `COUNTRY` code), caching `lat lon` in `~/.cache/udt/weather-loc`. The cache is reused while `CITY`/`COUNTRY` are unchanged (the cache file records them). 3. Calls `https://api.open-meteo.com/v1/forecast` with `timeformat=unixtime`, `timezone=auto`, `forecast_days=7`, `current` (temperature, apparent temperature, weather code, is_day, humidity, wind speed and direction), `hourly` (temperature, weather code, precipitation probability, is_day, uv_index), `daily` (weather code, min, max, precipitation probability max, sunrise, sunset). `imperial` adds `temperature_unit=fahrenheit` and `wind_speed_unit=mph`; `metric` uses `kmh`. 4. Calls `https://air-quality-api.open-meteo.com/v1/air-quality` with `current=european_aqi`. Failure here is not fatal: the `air` line is omitted. 5. jq flattens both responses into the line format below, written to a temp file and renamed onto `~/.cache/udt/weather.txt` (atomic, as today). A forecast failure leaves the previous cache in place and exits non-zero. ### Cache format: `~/.cache/udt/weather.txt` Tab-separated, one record per line, first field the record type. Numbers as Open-Meteo emits them; epochs in seconds. ``` units metric current sun air hour day ``` `hour` covers the 24 hours starting at the current hour; `day` covers 7 days starting today. `sun` is today's. Line-oriented so the Lua side parses with `gmatch` and needs no JSON parser. ### Lua: `lib/weather.lua` Rewritten for Open-Meteo, same contract as today: pure functions on strings and numbers, nothing raises, nil on bad input. - `ICON` / `icon(code, is_day)`: WMO weather code to Nerd Font glyph. - `parse(src)`: returns `{units, current, sun, air, hours, days}` or nil when `current`, `sun` or at least one `hour` is missing. - `moon(epoch)`: returns `phase` (0..1, 0 = new) and `illumination` (0..1), from the synodic month against a known new moon. `moon_icon(phase)` picks one of the 28 glyphs. - `uv_role(uv)`, `aqi_role(aqi)`: threshold role names per the table above. - Kept from today: `compass`, `sun_t`, `is_day`, `STALE_AFTER`, `is_stale`, `age_str`. Removed: OWM-specific `kmh` and `beaufort`, unless a card ends up using them. The UV shown in `details` is the `hour` record for the current hour. ## Widgets New files in `widgets/`: `now.lua`, `hourly.lua`, `week.lua`, `details.lua`, `sky.lua`. Each reads the cache through `lib/weather.lua` (same read-and-cache pattern as today's `weather.lua`, which is deleted). The daylight arc moves to `sky.lua`. `lib/card.lua` gets ICON entries for the new card titles if needed. ## Layout `now` takes the old `weather` slot (col 1, row 7, w 3, h 5). `hourly`, `week`, `details` and `sky` go into the open right side of the 16x12 grid (cols 10 to 16). Exact cells are set in the plan and adjusted on the live board. ## Config and docs - `weather.env.example`: drop `KEY` and its OpenWeatherMap notes. - README.md: weather section rewritten for Open-Meteo and the five cards, Mirage credit, layout example updated from `weather` to the new names. - `conky.conf.in`: unchanged (`texeci 900` on the same script). ## Testing - `test/test_weather.lua` rewritten against a fixture `test/fixtures/weather.txt` (location-free; any names use `Example City`): - WMO code to glyph, day and night, unknown code fallback. - `parse`: full fixture, missing `air`, missing `current` (nil), garbage input (nil), empty string (nil). - `moon`: known full and new moon dates within one day of tolerance. - `uv_role` / `aqi_role` at each threshold boundary. - Fetcher: run once by hand against the live API and inspect the cache. - Each card rendered offscreen with `test/render.lua` at its layout size, and the live board checked after `./restart.sh` for size beside neighbours.