aboutsummaryrefslogtreecommitdiff
path: root/working/weather-kit-integration
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-19 15:44:24 -0500
committerCraig Jennings <c@cjennings.net>2026-07-19 15:44:24 -0500
commit96bc42fafff425cf7690ad417cc6d4d4a757547b (patch)
tree84bbb487e44d27bad51d7daef82d4a2e2336f59c /working/weather-kit-integration
parente0383618dfef4cf5dae331422c206cde308eb9dc (diff)
downloadarchsetup-96bc42fafff425cf7690ad417cc6d4d4a757547b.tar.gz
archsetup-96bc42fafff425cf7690ad417cc6d4d4a757547b.zip
docs: file interface design references
Record the reconciled weather work and future component direction.\nFile the clock and retro-interface reference materials for later design work.
Diffstat (limited to 'working/weather-kit-integration')
-rw-r--r--working/weather-kit-integration/README.org119
-rwxr-xr-xworking/weather-kit-integration/weather215
-rw-r--r--working/weather-kit-integration/weather-chip.html97
-rw-r--r--working/weather-kit-integration/weather-glyphs.svg81
4 files changed, 512 insertions, 0 deletions
diff --git a/working/weather-kit-integration/README.org b/working/weather-kit-integration/README.org
new file mode 100644
index 0000000..d04899f
--- /dev/null
+++ b/working/weather-kit-integration/README.org
@@ -0,0 +1,119 @@
+#+TITLE: Weather kit — for the Dupre kit / waybar / Wayland UX
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-18
+
+Built to completion elsewhere and handed to archsetup, which owns the Dupre kit,
+waybar, and the Wayland UI/UX and will maintain it from here. This note is the
+integration guide: what's in the package, the data contract, and how each
+surface consumes it.
+
+* What's in the package
+
+| File | Purpose |
+|----------------------+-------------------------------------------------------------------------|
+| =weather= | The fetcher. Python 3, stdlib only. Hits Open-Meteo, normalises, caches. |
+| =weather-glyphs.svg= | The custom condition glyph set (9 symbols, currentColor). |
+| =weather-chip.html= | Reference renderer: the chosen chip, wired to the contract. Port target.|
+| =README.org= | This file. |
+
+* The design decisions it encodes (settled over the 2026-07-18 session)
+
+- *Source: Open-Meteo, =model=best_match=.* No API key, no signup, non-commercial
+ free tier (10k calls/day; a 15-min poll is ~96/day). =best_match= auto-selects
+ the most accurate regional model — NOAA blend in the US, ECMWF/ICON abroad — so
+ the same binary is right on ratio in New Orleans and on velox travelling. For a
+ bar chip there's no perceptible accuracy gap vs the NWS API, and NWS is US-only,
+ so Open-Meteo is the single source. (NWS stays a possible *additive* feature
+ later, only for official US severe-weather alerts.)
+- *One fetcher, one cache, thin renderers.* Every surface shells out to =weather=
+ and renders the JSON. Nobody re-implements the API call or the cache. A shared
+ cache means several widgets polling still hit the API at most once per TTL and
+ all show the identical reading.
+- *Windy is a condition, not a compass reading.* Below the threshold the chip
+ shows the direction vane + speed; at/above it, the gust glyph + speed. Same
+ width either way. Threshold default *15 mph* (Beaufort 4 — where wind stops
+ being ambient). It's a =--windy-threshold= flag, derived at output from
+ =wind_mph=, so changing it takes effect without a cache bust.
+- *Custom glyphs, not Nerd Font.* Seven conditions + a gust mark + a vane, drawn
+ to sit in the instrument aesthetic. =currentColor=, so each surface recolours.
+
+* The contract (=weather --json=)
+
+#+begin_src json
+{
+ "temp_f": 82, "feels_f": 92,
+ "condition": "clear", // clear | partly | cloud | fog | rain | snow | storm
+ "wmo_code": 0,
+ "windy": false, // wind_mph >= threshold
+ "wind_mph": 6, "wind_dir": "WNW", "wind_deg": 293,
+ "location": "New Orleans",
+ "observed": "2026-07-18T01:00", "stale": false
+}
+#+end_src
+
+=condition= is the coupling to the glyphs. Mapping (also in =weather-chip.html=):
+=clear->wx-sun=, =partly->wx-partly=, =cloud->wx-cloud=, =fog->wx-fog=,
+=rain->wx-rain=, =snow->wx-snow=, =storm->wx-storm=; plus =wx-wind= (shown when
+=windy=) and =wx-vane= (shown otherwise, rotated by =wind_deg=). A consumer never
+sees the WMO number unless it wants =wmo_code=.
+
+=stale: true= means the network fetch failed and this is the last good reading —
+render it dimmed, never blank the bar.
+
+* WMO weather-code -> condition (what the fetcher collapses)
+
+| WMO code(s) | condition |
+|--------------------------------------+-----------|
+| 0 | clear |
+| 1, 2 | partly |
+| 3 | cloud |
+| 45, 48 | fog |
+| 51-57, 61-67, 80-82 | rain |
+| 71-77, 85-86 | snow |
+| 95, 96, 99 | storm |
+| (anything unmapped) | cloud |
+
+* Install
+
+Drop =weather= on =PATH= the way the other fleet CLIs travel (=agent-page=, etc.):
+into the rulesets/dotfiles bin so =make install= / stow links it to
+=~/.local/bin/weather= on every machine. It's dependency-free, so nothing else to
+provision.
+
+No coordinates are baked in (nothing personal ships in the repo). Each machine
+sets its own location, resolved in order:
+
+1. =--lat/--lon= flags.
+2. =$WEATHER_LAT= / =$WEATHER_LON= (and optional =$WEATHER_NAME=).
+3. =~/.config/weather/config.json= — ={"lat":29.97,"lon":-90.09,"name":"New Orleans"}=.
+ Add ="use_geo": true= (or pass =--geo=) to prefer =whereami= — the right call
+ on velox, which travels.
+
+* How each surface consumes it
+
+- *waybar* — a custom module:
+ #+begin_src jsonc
+ "custom/weather": { "exec": "weather --format waybar", "return-type": "json",
+ "interval": 900, "on-click": "..." }
+ #+end_src
+ =--format waybar= emits ={text, alt, class, tooltip}=; the stylesheet picks the
+ icon by =alt=/=class= (=.clear=, =.windy=, =.stale=, ...).
+- *Dupre kit / =svg.el=* — =(json-parse-string (shell-command-to-string "weather --json"))=,
+ then render the chip. =weather-chip.html='s =renderChip(reading)= is the exact
+ logic to port: glyph, temp, wind slot (gust vs vane by =windy=).
+- *clock-panel (Python)* — =json.loads(subprocess.run(["weather","--json"],...).stdout)=.
+ No HTTP in clock-panel at all.
+
+* Notes / open choices for the maintainer
+
+- Poll cadence lives with the consumer (waybar =interval=), not the tool; the tool
+ just honours its own cache TTL (=--ttl=, default 900s).
+- =weather-glyphs.svg= is the canonical glyph source. =weather-chip.html= inlines a
+ copy for a standalone demo — if the glyphs change, update both (or have the kit
+ build inline from the =.svg=).
+- The chip currently shows the wind slot always. If it ever reads busy on a packed
+ bar, hiding the slot below the threshold (show only when windy) is a one-line
+ change — the data's already there.
+- A "windy later today" hint would want the hourly endpoint (=hourly=wind_speed_10m=)
+ and a small max-over-next-N-hours; the fetcher is structured to add that as a
+ second field without touching consumers.
diff --git a/working/weather-kit-integration/weather b/working/weather-kit-integration/weather
new file mode 100755
index 0000000..6d0c0f1
--- /dev/null
+++ b/working/weather-kit-integration/weather
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+"""weather -- one fetcher for the whole fleet.
+
+Hits Open-Meteo (model=best_match, so NOAA blend in the US, ECMWF/ICON abroad),
+normalises the reading, caches the last good value, and prints it. Every app
+(waybar, clock-panel, an Emacs svg.el panel) shells out to this and renders the
+JSON -- nobody re-implements the API call, the code mapping, or the cache.
+
+Stdlib only: no pip, no venv. Runs as a bare executable.
+
+Contract (--format json):
+ {
+ "temp_f": 82, "feels_f": 92,
+ "condition": "clear", # clear|partly|cloud|fog|rain|snow|storm
+ "wmo_code": 0,
+ "windy": false, # wind_mph >= threshold (default 15)
+ "wind_mph": 6, "wind_dir": "WNW", "wind_deg": 293,
+ "location": "New Orleans",
+ "observed": "2026-07-18T01:00", "stale": false
+ }
+
+Location resolves in order: --lat/--lon, then $WEATHER_LAT/$WEATHER_LON, then
+~/.config/weather/config.json, then `whereami` (with --geo). No coordinates are
+baked in, so nothing personal ships in the repo -- each machine configures its
+own.
+"""
+import argparse
+import json
+import os
+import subprocess
+import sys
+import time
+import urllib.error
+import urllib.request
+from pathlib import Path
+
+API = "https://api.open-meteo.com/v1/forecast"
+CONFIG = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "weather" / "config.json"
+CACHE = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "weather" / "current.json"
+DEFAULT_TTL = 900 # 15 min -- weather changes slowly; keep API calls rare
+DEFAULT_WINDY = 15 # mph; Beaufort 4, where wind stops being ambient
+
+# WMO weather-interpretation code -> our semantic condition.
+# Ranges collapse to the seven condition glyphs; "windy" is derived from speed.
+_WMO = {
+ 0: "clear",
+ 1: "partly", 2: "partly",
+ 3: "cloud",
+ 45: "fog", 48: "fog",
+ 51: "rain", 53: "rain", 55: "rain", 56: "rain", 57: "rain",
+ 61: "rain", 63: "rain", 65: "rain", 66: "rain", 67: "rain",
+ 80: "rain", 81: "rain", 82: "rain",
+ 71: "snow", 73: "snow", 75: "snow", 77: "snow", 85: "snow", 86: "snow",
+ 95: "storm", 96: "storm", 99: "storm",
+}
+
+_CARDINALS = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
+ "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
+
+
+def condition_for(code):
+ return _WMO.get(int(code), "cloud") # unknown code -> cloud, the safe neutral
+
+
+def cardinal(deg):
+ return _CARDINALS[int((deg % 360) / 22.5 + 0.5) % 16]
+
+
+def resolve_location(args):
+ """Return (lat, lon, name). Raise if nothing configured."""
+ if args.lat is not None and args.lon is not None:
+ return args.lat, args.lon, args.location
+ env_lat, env_lon = os.environ.get("WEATHER_LAT"), os.environ.get("WEATHER_LON")
+ if env_lat and env_lon:
+ return float(env_lat), float(env_lon), args.location or os.environ.get("WEATHER_NAME")
+ if CONFIG.exists():
+ cfg = json.loads(CONFIG.read_text())
+ if args.geo or cfg.get("use_geo"):
+ geo = _whereami()
+ if geo:
+ return geo[0], geo[1], args.location or cfg.get("name")
+ if "lat" in cfg and "lon" in cfg:
+ return cfg["lat"], cfg["lon"], args.location or cfg.get("name")
+ if args.geo:
+ geo = _whereami()
+ if geo:
+ return geo[0], geo[1], args.location
+ raise SystemExit(
+ "weather: no location. Pass --lat/--lon, set $WEATHER_LAT/$WEATHER_LON, "
+ f"write {CONFIG} (\"lat\"/\"lon\"[/\"name\"]), or use --geo on a machine "
+ "that travels."
+ )
+
+
+def _whereami():
+ """Best-effort coords from the `whereami` tool. Only meaningful where the
+ machine actually moves (velox); silently gives up otherwise."""
+ try:
+ out = subprocess.run(["whereami"], capture_output=True, text=True, timeout=20).stdout
+ except (OSError, subprocess.SubprocessError):
+ return None
+ for line in out.splitlines():
+ if "coords:" in line:
+ try:
+ nums = line.split("coords:")[1].split("(")[0]
+ lat, lon = (float(x) for x in nums.replace(",", " ").split()[:2])
+ return lat, lon
+ except (ValueError, IndexError):
+ return None
+ return None
+
+
+def fetch(lat, lon, name, threshold):
+ params = (
+ f"?latitude={lat}&longitude={lon}"
+ "&current=temperature_2m,apparent_temperature,weather_code,"
+ "wind_speed_10m,wind_direction_10m"
+ "&temperature_unit=fahrenheit&wind_speed_unit=mph"
+ "&models=best_match&timezone=auto"
+ )
+ req = urllib.request.Request(API + params, headers={"User-Agent": "weather-cli/1.0"})
+ with urllib.request.urlopen(req, timeout=15) as resp:
+ data = json.load(resp)
+ cur = data["current"]
+ mph = round(cur["wind_speed_10m"])
+ deg = cur["wind_direction_10m"]
+ return {
+ "temp_f": round(cur["temperature_2m"]),
+ "feels_f": round(cur["apparent_temperature"]),
+ "condition": condition_for(cur["weather_code"]),
+ "wmo_code": int(cur["weather_code"]),
+ "windy": mph >= threshold,
+ "wind_mph": mph,
+ "wind_dir": cardinal(deg),
+ "wind_deg": round(deg),
+ "location": name or f"{lat:.2f},{lon:.2f}",
+ "observed": cur["time"],
+ "stale": False,
+ }
+
+
+def read_cache():
+ try:
+ blob = json.loads(CACHE.read_text())
+ return blob["fetched_at"], blob["reading"]
+ except (OSError, ValueError, KeyError):
+ return None, None
+
+
+def write_cache(reading):
+ CACHE.parent.mkdir(parents=True, exist_ok=True)
+ CACHE.write_text(json.dumps({"fetched_at": int(time.time()), "reading": reading}))
+
+
+def get_reading(args):
+ """Cache-first, network-second, stale-cache-on-failure. One shared cache
+ means five widgets polling still hit the API at most once per TTL."""
+ fetched_at, cached = read_cache()
+ if cached and not args.no_cache and fetched_at and (time.time() - fetched_at) < args.ttl:
+ return cached
+ lat, lon, name = resolve_location(args)
+ try:
+ reading = fetch(lat, lon, name, args.windy_threshold)
+ write_cache(reading)
+ return reading
+ except (urllib.error.URLError, OSError, KeyError, ValueError) as e:
+ if cached: # never blank the bar: serve last good, flagged
+ cached["stale"] = True
+ return cached
+ raise SystemExit(f"weather: fetch failed and no cache to fall back on ({e})")
+
+
+def render(reading, fmt):
+ if fmt == "json":
+ return json.dumps(reading)
+ if fmt == "plain":
+ wind = "windy" if reading["windy"] else reading["wind_dir"]
+ s = f"{reading['temp_f']}°F {reading['condition']}, wind {wind} {reading['wind_mph']}"
+ return s + (" (stale)" if reading["stale"] else "")
+ if fmt == "waybar":
+ # Data only; the caller's stylesheet picks the icon by `alt`/`class`.
+ wind = "windy" if reading["windy"] else f"{reading['wind_dir']} {reading['wind_mph']}"
+ return json.dumps({
+ "text": f"{reading['temp_f']}° {reading['wind_mph']}",
+ "alt": reading["condition"],
+ "class": (["stale"] if reading["stale"] else []) + [reading["condition"]]
+ + (["windy"] if reading["windy"] else []),
+ "tooltip": f"{reading['location']}: {reading['temp_f']}°F "
+ f"(feels {reading['feels_f']}°), {reading['condition']}, "
+ f"wind {reading['wind_dir']} {reading['wind_mph']} mph"
+ + (" [stale]" if reading["stale"] else ""),
+ })
+ raise SystemExit(f"weather: unknown format {fmt!r}")
+
+
+def main(argv=None):
+ p = argparse.ArgumentParser(description="Normalised current weather from Open-Meteo.")
+ p.add_argument("--lat", type=float, help="latitude override")
+ p.add_argument("--lon", type=float, help="longitude override")
+ p.add_argument("--location", help="display name for the reading")
+ p.add_argument("--geo", action="store_true", help="geolocate via `whereami` (traveling machines)")
+ p.add_argument("--format", choices=["json", "plain", "waybar"], default="json")
+ p.add_argument("--windy-threshold", type=int, default=DEFAULT_WINDY, help="mph at which windy=true")
+ p.add_argument("--ttl", type=int, default=DEFAULT_TTL, help="cache seconds")
+ p.add_argument("--no-cache", action="store_true", help="force a fresh fetch")
+ args = p.parse_args(argv)
+ reading = get_reading(args)
+ # Derive windy from cached wind_mph so a changed threshold takes effect
+ # without waiting for the cache to expire.
+ reading["windy"] = reading["wind_mph"] >= args.windy_threshold
+ print(render(reading, args.format))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/working/weather-kit-integration/weather-chip.html b/working/weather-kit-integration/weather-chip.html
new file mode 100644
index 0000000..f9e05d4
--- /dev/null
+++ b/working/weather-kit-integration/weather-chip.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Weather chip — reference renderer</title>
+<style>
+:root{
+ --panel:#100f0f; --ground:#151311; --cream:#f3e7c5; --steel:#969385;
+ --gold:#e2a038; --gold-hi:#ffbe54; --amber-edge:#7d5c16;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+body{background:var(--ground);color:var(--cream);font-family:var(--mono);padding:32px 4vw 80px}
+h1{color:var(--cream);font-size:19px;margin-bottom:6px}
+p{color:var(--steel);font-size:12.5px;max-width:820px;line-height:1.6;margin-bottom:8px}
+code{color:var(--gold-hi)}
+.row{display:flex;flex-wrap:wrap;gap:26px;align-items:flex-start;margin-top:26px}
+figure{margin:0;display:flex;flex-direction:column;align-items:center;gap:10px}
+figcaption{color:var(--steel);font-size:10px;letter-spacing:.14em;text-transform:uppercase}
+/* the chip */
+.wx-chip{display:inline-flex;align-items:center;gap:8px;background:#080808;border-radius:8px;
+ padding:6px 11px;box-shadow:inset 0 2px 8px #000, 0 0 0 1px #33290f}
+.wx-chip .temp{color:#e9e9e9;font-size:17px}
+.wx-chip .sep{width:1px;align-self:stretch;background:#33290f;margin:2px 0}
+.wx-chip .spd{color:var(--gold);font-size:13px}
+.wx-chip svg{display:block}
+</style>
+</head>
+<body>
+
+<h1>Weather chip — reference renderer</h1>
+<p>The chosen bar form, wired to the <code>weather</code> CLI contract. Each chip below is built by <code>renderChip(reading)</code> from a contract object (the JSON <code>weather --json</code> prints). This is the reference for the port into the Dupre kit's <code>svg.el</code>: condition glyph, temperature, then the wind slot — which collapses to the gust glyph + speed when <code>windy</code>, or the direction vane + speed when calm. Constant width either way.</p>
+<p>To drive it live: <code>weather --json | (feed into renderChip)</code>. The samples here are static so the condition and calm/windy mapping is visible at a glance.</p>
+
+<!-- the custom glyph set, inlined (same as weather-glyphs.svg) -->
+<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs>
+ <symbol id="wx-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4.3" fill="currentColor"/><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="12" y1="1.5" x2="12" y2="4.6"/><line x1="12" y1="19.4" x2="12" y2="22.5"/><line x1="1.5" y1="12" x2="4.6" y2="12"/><line x1="19.4" y1="12" x2="22.5" y2="12"/><line x1="4.6" y1="4.6" x2="6.8" y2="6.8"/><line x1="17.2" y1="17.2" x2="19.4" y2="19.4"/><line x1="4.6" y1="19.4" x2="6.8" y2="17.2"/><line x1="17.2" y1="6.8" x2="19.4" y2="4.6"/></g></symbol>
+ <symbol id="wx-partly" viewBox="0 0 24 24"><circle cx="8.5" cy="8" r="3.2" fill="currentColor"/><g stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="8.5" y1="1.5" x2="8.5" y2="3.4"/><line x1="1.7" y1="8" x2="3.6" y2="8"/><line x1="3.7" y1="3.2" x2="5" y2="4.5"/><line x1="13.3" y1="3.2" x2="12" y2="4.5"/></g><g fill="currentColor"><circle cx="12" cy="15" r="3.6"/><circle cx="16.5" cy="13" r="4.4"/><circle cx="20" cy="15.6" r="3.1"/><rect x="11" y="15" width="10" height="4" rx="2"/></g></symbol>
+ <symbol id="wx-cloud" viewBox="0 0 24 24"><g fill="currentColor"><circle cx="7.5" cy="13" r="4"/><circle cx="12.5" cy="10.5" r="5.2"/><circle cx="17" cy="13.5" r="3.6"/><rect x="6.5" y="13" width="11.5" height="4.2" rx="2.1"/></g></symbol>
+ <symbol id="wx-rain" viewBox="0 0 24 24"><g fill="currentColor"><circle cx="7.5" cy="9" r="3.6"/><circle cx="12.5" cy="7" r="4.6"/><circle cx="16.5" cy="9.6" r="3.1"/><rect x="6.5" y="9" width="10.5" height="3.8" rx="1.9"/></g><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="8" y1="15.5" x2="6.6" y2="19.5"/><line x1="12" y1="15.5" x2="10.6" y2="19.5"/><line x1="16" y1="15.5" x2="14.6" y2="19.5"/></g></symbol>
+ <symbol id="wx-storm" viewBox="0 0 24 24"><g fill="currentColor"><circle cx="7.5" cy="9" r="3.6"/><circle cx="12.5" cy="7" r="4.6"/><circle cx="16.5" cy="9.6" r="3.1"/><rect x="6.5" y="9" width="10.5" height="3.8" rx="1.9"/></g><path d="M12.5 14 L9 19.5 h3 L10.5 23.5 L15.5 17.5 h-3 L14 14 Z" fill="currentColor"/></symbol>
+ <symbol id="wx-snow" viewBox="0 0 24 24"><g fill="currentColor"><circle cx="7.5" cy="9" r="3.6"/><circle cx="12.5" cy="7" r="4.6"/><circle cx="16.5" cy="9.6" r="3.1"/><rect x="6.5" y="9" width="10.5" height="3.8" rx="1.9"/></g><g stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><g transform="translate(9,17.5)"><line x1="-2" y1="0" x2="2" y2="0"/><line x1="0" y1="-2" x2="0" y2="2"/><line x1="-1.4" y1="-1.4" x2="1.4" y2="1.4"/><line x1="-1.4" y1="1.4" x2="1.4" y2="-1.4"/></g><g transform="translate(15,18.5)"><line x1="-2" y1="0" x2="2" y2="0"/><line x1="0" y1="-2" x2="0" y2="2"/><line x1="-1.4" y1="-1.4" x2="1.4" y2="1.4"/><line x1="-1.4" y1="1.4" x2="1.4" y2="-1.4"/></g></g></symbol>
+ <symbol id="wx-fog" viewBox="0 0 24 24"><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><line x1="3" y1="7" x2="21" y2="7"/><line x1="5" y1="11.5" x2="19" y2="11.5"/><line x1="3" y1="16" x2="18" y2="16"/><line x1="7" y1="20.5" x2="21" y2="20.5"/></g></symbol>
+ <symbol id="wx-wind" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M2.5 8 H13.5 a2.6 2.6 0 1 0 -2.55 -3.1"/><path d="M2.5 12.5 H18 a2.9 2.9 0 1 1 -2.85 3.45"/><path d="M2.5 17 H10.5 a2.3 2.3 0 1 1 -2.25 2.75"/></g></symbol>
+ <symbol id="wx-vane" viewBox="0 0 24 24"><path d="M12 2.4 L15.6 9 L12.9 9 L12.9 21 L11.1 21 L11.1 9 L8.4 9 Z" fill="currentColor" stroke="currentColor" stroke-width=".8" stroke-linejoin="round"/></symbol>
+</defs></svg>
+
+<div class="row" id="chips"></div>
+
+<script>
+// condition -> glyph id. Only "clear" differs from its name (historically the sun).
+const GLYPH = {clear:"wx-sun", partly:"wx-partly", cloud:"wx-cloud",
+ fog:"wx-fog", rain:"wx-rain", snow:"wx-snow", storm:"wx-storm"};
+
+// The whole render: contract object in, chip element out. This is the logic to
+// port to svg.el -- glyph, temp, then wind slot (gust glyph when windy, else the
+// vane rotated by wind_deg). Same footprint either way.
+function renderChip(r){
+ const chip = document.createElement("div");
+ chip.className = "wx-chip";
+ const cond = GLYPH[r.condition] || "wx-cloud";
+ chip.innerHTML =
+ `<svg width="24" height="24" style="color:var(--gold-hi)"><use href="#${cond}"/></svg>` +
+ `<span class="temp">${r.temp_f}°</span>` +
+ `<span class="sep"></span>` +
+ (r.windy
+ ? `<svg width="20" height="20" style="color:var(--gold-hi)"><use href="#wx-wind"/></svg>`
+ : `<svg width="18" height="18" style="color:var(--gold-hi);transform:rotate(${r.wind_deg}deg);transform-origin:center"><use href="#wx-vane"/></svg>`) +
+ `<span class="spd">${r.wind_mph}</span>`;
+ if (r.stale) chip.style.opacity = ".55";
+ return chip;
+}
+
+// Sample readings covering the condition set + the calm/windy collapse + stale.
+const SAMPLES = [
+ [{temp_f:94, condition:"clear", windy:false, wind_mph:9, wind_deg:335}, "clear · calm"],
+ [{temp_f:78, condition:"clear", windy:true, wind_mph:22, wind_deg:293}, "clear · windy"],
+ [{temp_f:61, condition:"rain", windy:false, wind_mph:7, wind_deg:120}, "rain"],
+ [{temp_f:58, condition:"storm", windy:true, wind_mph:31, wind_deg:200}, "storm · windy"],
+ [{temp_f:34, condition:"snow", windy:false, wind_mph:5, wind_deg:20}, "snow"],
+ [{temp_f:66, condition:"partly", windy:false, wind_mph:11, wind_deg:250}, "partly"],
+ [{temp_f:55, condition:"fog", windy:false, wind_mph:3, wind_deg:70}, "fog"],
+ [{temp_f:88, condition:"clear", windy:false, wind_mph:9, wind_deg:335, stale:true}, "stale (dimmed)"],
+];
+
+const host = document.getElementById("chips");
+for (const [reading, label] of SAMPLES){
+ const fig = document.createElement("figure");
+ fig.appendChild(renderChip(reading));
+ const cap = document.createElement("figcaption");
+ cap.textContent = label;
+ fig.appendChild(cap);
+ host.appendChild(fig);
+}
+</script>
+</body>
+</html>
diff --git a/working/weather-kit-integration/weather-glyphs.svg b/working/weather-kit-integration/weather-glyphs.svg
new file mode 100644
index 0000000..0cc3189
--- /dev/null
+++ b/working/weather-kit-integration/weather-glyphs.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- weather-glyphs.svg — custom weather condition glyph set for the Dupre kit.
+ Drawn custom (not Nerd Font / emoji) so they sit in the instrument aesthetic.
+ Every symbol uses currentColor, so a consumer recolours by setting `color`
+ on the <svg>/<use> that references it. 24x24 viewBox throughout.
+
+ Condition ids match the `condition` field of the `weather` CLI:
+ clear -> wx-sun partly -> wx-partly cloud -> wx-cloud
+ fog -> wx-fog rain -> wx-rain snow -> wx-snow storm -> wx-storm
+ Plus two wind marks used by the chip's collapse rule:
+ wx-wind — the "windy" gust glyph (shown when wind_mph >= threshold)
+ wx-vane — the direction arrow (shown below threshold; rotate by wind_deg,
+ 0deg = points north, so the CSS transform is rotate(<wind_deg>deg))
+
+ Usage:
+ <svg width="24" height="24" style="color:#ffbe54"><use href="#wx-sun"/></svg>
+-->
+<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="position:absolute" aria-hidden="true"><defs>
+
+ <symbol id="wx-sun" viewBox="0 0 24 24">
+ <circle cx="12" cy="12" r="4.3" fill="currentColor"/>
+ <g stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
+ <line x1="12" y1="1.5" x2="12" y2="4.6"/><line x1="12" y1="19.4" x2="12" y2="22.5"/>
+ <line x1="1.5" y1="12" x2="4.6" y2="12"/><line x1="19.4" y1="12" x2="22.5" y2="12"/>
+ <line x1="4.6" y1="4.6" x2="6.8" y2="6.8"/><line x1="17.2" y1="17.2" x2="19.4" y2="19.4"/>
+ <line x1="4.6" y1="19.4" x2="6.8" y2="17.2"/><line x1="17.2" y1="6.8" x2="19.4" y2="4.6"/>
+ </g>
+ </symbol>
+
+ <symbol id="wx-partly" viewBox="0 0 24 24">
+ <circle cx="8.5" cy="8" r="3.2" fill="currentColor"/>
+ <g stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
+ <line x1="8.5" y1="1.5" x2="8.5" y2="3.4"/><line x1="1.7" y1="8" x2="3.6" y2="8"/>
+ <line x1="3.7" y1="3.2" x2="5" y2="4.5"/><line x1="13.3" y1="3.2" x2="12" y2="4.5"/>
+ </g>
+ <g fill="currentColor"><circle cx="12" cy="15" r="3.6"/><circle cx="16.5" cy="13" r="4.4"/><circle cx="20" cy="15.6" r="3.1"/><rect x="11" y="15" width="10" height="4" rx="2"/></g>
+ </symbol>
+
+ <symbol id="wx-cloud" viewBox="0 0 24 24">
+ <g fill="currentColor"><circle cx="7.5" cy="13" r="4"/><circle cx="12.5" cy="10.5" r="5.2"/><circle cx="17" cy="13.5" r="3.6"/><rect x="6.5" y="13" width="11.5" height="4.2" rx="2.1"/></g>
+ </symbol>
+
+ <symbol id="wx-rain" viewBox="0 0 24 24">
+ <g fill="currentColor"><circle cx="7.5" cy="9" r="3.6"/><circle cx="12.5" cy="7" r="4.6"/><circle cx="16.5" cy="9.6" r="3.1"/><rect x="6.5" y="9" width="10.5" height="3.8" rx="1.9"/></g>
+ <g stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="8" y1="15.5" x2="6.6" y2="19.5"/><line x1="12" y1="15.5" x2="10.6" y2="19.5"/><line x1="16" y1="15.5" x2="14.6" y2="19.5"/></g>
+ </symbol>
+
+ <symbol id="wx-storm" viewBox="0 0 24 24">
+ <g fill="currentColor"><circle cx="7.5" cy="9" r="3.6"/><circle cx="12.5" cy="7" r="4.6"/><circle cx="16.5" cy="9.6" r="3.1"/><rect x="6.5" y="9" width="10.5" height="3.8" rx="1.9"/></g>
+ <path d="M12.5 14 L9 19.5 h3 L10.5 23.5 L15.5 17.5 h-3 L14 14 Z" fill="currentColor"/>
+ </symbol>
+
+ <symbol id="wx-snow" viewBox="0 0 24 24">
+ <g fill="currentColor"><circle cx="7.5" cy="9" r="3.6"/><circle cx="12.5" cy="7" r="4.6"/><circle cx="16.5" cy="9.6" r="3.1"/><rect x="6.5" y="9" width="10.5" height="3.8" rx="1.9"/></g>
+ <g stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
+ <g transform="translate(9,17.5)"><line x1="-2" y1="0" x2="2" y2="0"/><line x1="0" y1="-2" x2="0" y2="2"/><line x1="-1.4" y1="-1.4" x2="1.4" y2="1.4"/><line x1="-1.4" y1="1.4" x2="1.4" y2="-1.4"/></g>
+ <g transform="translate(15,18.5)"><line x1="-2" y1="0" x2="2" y2="0"/><line x1="0" y1="-2" x2="0" y2="2"/><line x1="-1.4" y1="-1.4" x2="1.4" y2="1.4"/><line x1="-1.4" y1="1.4" x2="1.4" y2="-1.4"/></g>
+ </g>
+ </symbol>
+
+ <symbol id="wx-fog" viewBox="0 0 24 24">
+ <g stroke="currentColor" stroke-width="1.9" stroke-linecap="round">
+ <line x1="3" y1="7" x2="21" y2="7"/><line x1="5" y1="11.5" x2="19" y2="11.5"/><line x1="3" y1="16" x2="18" y2="16"/><line x1="7" y1="20.5" x2="21" y2="20.5"/>
+ </g>
+ </symbol>
+
+ <!-- windy condition: flowing gust lines, each ending in a curl -->
+ <symbol id="wx-wind" viewBox="0 0 24 24">
+ <g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round">
+ <path d="M2.5 8 H13.5 a2.6 2.6 0 1 0 -2.55 -3.1"/>
+ <path d="M2.5 12.5 H18 a2.9 2.9 0 1 1 -2.85 3.45"/>
+ <path d="M2.5 17 H10.5 a2.3 2.3 0 1 1 -2.25 2.75"/>
+ </g>
+ </symbol>
+
+ <!-- direction vane: points UP = north at 0deg; rotate by wind_deg (source bearing) -->
+ <symbol id="wx-vane" viewBox="0 0 24 24">
+ <path d="M12 2.4 L15.6 9 L12.9 9 L12.9 21 L11.1 21 L11.1 9 L8.4 9 Z" fill="currentColor" stroke="currentColor" stroke-width=".8" stroke-linejoin="round"/>
+ </symbol>
+
+</defs></svg>