diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-17 09:11:56 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-17 09:11:56 +0200 |
| commit | 2bdb74a1cbd4b33216d94713de1f1b3c81f631e2 (patch) | |
| tree | aae8b839b73110bada9274e4ee12e58e15c2964a /widgets | |
| parent | 04542528f018678b64e0003eab078858c31f196e (diff) | |
| download | conky-theme-udt-2bdb74a1cbd4b33216d94713de1f1b3c81f631e2.tar.gz conky-theme-udt-2bdb74a1cbd4b33216d94713de1f1b3c81f631e2.zip | |
feat: add the weather card
Draws current conditions plus a sunrise-to-sunset arc in widgets/weather.lua,
placed at col 2 in dashboard.lua's layout. Schedules the periodic fetch via
execi in conky.conf.in, tying refresh to conky's own lifecycle.
The no-data env-file probe now closes its handle explicitly: the branch reruns
every draw until a fetch succeeds, so an unclosed io.open there would leak one
handle per frame.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'widgets')
| -rw-r--r-- | widgets/weather.lua | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/widgets/weather.lua b/widgets/weather.lua new file mode 100644 index 0000000..8a3b72d --- /dev/null +++ b/widgets/weather.lua @@ -0,0 +1,177 @@ +-- Weather: current conditions over a sunrise-to-sunset arc. +-- +-- Shape follows idea1.png, which splits this across two cards; this merges +-- them into one vertical cell. Every colour comes from the palette, every size +-- derives from the rect, so the card composes on either monitor. + +local card = require 'lib.card' +local weather = require 'lib.weather' + +local M = {} + +local CACHE = (os.getenv('XDG_CACHE_HOME') or (os.getenv('HOME') .. '/.cache')) + .. '/udt/weather.json' + +-- Draw the card chrome with a message in it. Used for every no-data state, so +-- the dashboard keeps its shape instead of showing an empty cell, which is +-- indistinguishable from a crashed widget. +local function draw_notice(cr, inner, colors, line1, line2) + card.font(cr, card.FONT_UI, 15, true) + card.rgba(cr, colors.label) + card.text(cr, inner.x, inner.y + 24, line1) + card.font(cr, card.FONT_MONO, 12, false) + card.text(cr, inner.x, inner.y + 48, line2) +end + +-- The daylight arc: a curve, its baseline, the sun, and the two times. +-- +-- The dot is placed by evaluating the curve at t rather than by computing a +-- point on a circle: the curve IS the path, so evaluating it keeps the dot on +-- the arc if the control points are ever adjusted. +local function draw_arc(cr, x, y, w, h, colors, w_data, now) + local pad = 4 + local x0, x1 = x + pad, x + w - pad + -- Room under the curve for the time labels. + local base = y + h - 20 + local top = y + 6 + + -- Cubic Bezier control points, chosen so the curve peaks near the middle at + -- about two thirds of the band height. + local p0 = { x0, base } + local c1 = { x0 + (x1 - x0) * 0.22, top - 10 } + local c2 = { x1 - (x1 - x0) * 0.22, top - 10 } + local p3 = { x1, base } + + -- The baseline: the horizon the sun rises from and sets into. + card.rgba(cr, colors.rule, 0.6) + cairo_set_line_width(cr, 1) + cairo_move_to(cr, x0, base) + cairo_line_to(cr, x1, base) + cairo_stroke(cr) + + -- The arc itself. + card.rgba(cr, colors.rule, 0.9) + cairo_set_line_width(cr, 1.5) + cairo_move_to(cr, p0[1], p0[2]) + cairo_curve_to(cr, c1[1], c1[2], c2[1], c2[2], p3[1], p3[2]) + cairo_stroke(cr) + + local t = weather.sun_t(now, w_data.sunrise, w_data.sunset) or 0 + local day = weather.is_day(now, w_data.sunrise, w_data.sunset) + + -- Evaluate the cubic at t. + local function bez(a, b, c, d) + local u = 1 - t + return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d + end + local sx = bez(p0[1], c1[1], c2[1], p3[1]) + local sy = bez(p0[2], c1[2], c2[2], p3[2]) + + -- At night the dot is parked at an end, so it takes the dim colour: a bright + -- dot sitting on the horizon would read as a sun that is still up. + card.rgba(cr, day and colors.body or colors.label, 1) + cairo_arc(cr, sx, sy, 5, 0, math.pi * 2) + cairo_fill(cr) + + -- The times, each behind its own glyph, at the ends of the arc. + card.font(cr, card.FONT_MONO, 12, false) + card.rgba(cr, colors.label) + local ty = y + h - 2 + card.text(cr, x0, ty, + weather.ICON.sunrise .. ' ' .. os.date('%H:%M', w_data.sunrise)) + card.text_right(cr, x1, ty, + weather.ICON.sunset .. ' ' .. os.date('%H:%M', w_data.sunset)) +end + +function M.draw(cr, rect, colors) + local inner = card.card(cr, rect, colors) + local now = os.time() + + local src = nil + local f = io.open(CACHE, 'r') + if f then src = f:read('*a'); f:close() end + + local w = weather.parse(src) + if not w then + -- Closed explicitly: an unclosed handle here would accumulate one per + -- draw (every update_interval), since this whole branch reruns each + -- frame until a fetch succeeds. + local envf = io.open(os.getenv('HOME') .. '/.config/udt/weather.env', 'r') + if envf then + envf:close() + draw_notice(cr, inner, colors, 'no weather data', 'waiting for first fetch') + else + draw_notice(cr, inner, colors, 'no weather data', 'set ~/.config/udt/weather.env') + end + return + end + + local y = inner.y + + -- Header: condition glyph, then the temperature with the city beneath it. + local glyph = weather.icon(w.id, now, w.sunrise, w.sunset) + card.font(cr, card.FONT_MONO, 36, false) + card.rgba(cr, colors.highlight) + card.text(cr, inner.x, y + 34, glyph) + local gx = inner.x + card.advance(cr, glyph) + 14 + + card.font(cr, card.FONT_HEAVY, 44, false) + card.rgba(cr, colors.body) + card.text(cr, gx, y + 38, string.format('%d\u{00B0}', math.floor(w.temp + 0.5))) + + card.font(cr, card.FONT_MONO, 11, false) + card.rgba(cr, colors.label) + local city = (w.city or ''):upper() + if weather.is_stale(w.dt, now) then + city = city .. ' stale ' .. weather.age_str(w.dt, now) + end + card.text(cr, gx, y + 56, city) + + -- Condition, in OWM's own words, first letter capitalised. + card.font(cr, card.FONT_UI, 13, false) + card.rgba(cr, colors.body) + local desc = w.description or '' + desc = desc:sub(1, 1):upper() .. desc:sub(2) + card.text(cr, inner.x, y + 84, desc) + + -- Rule. + local ry = y + 100 + card.rgba(cr, colors.rule, 0.8) + cairo_set_line_width(cr, 1) + cairo_move_to(cr, inner.x, ry) + cairo_line_to(cr, inner.x + inner.w, ry) + cairo_stroke(cr) + + -- Stats: label left, value right. + local kmh = weather.kmh(w.wind_speed) + local wind_txt = kmh + and string.format('%s %d km/h', weather.arrow(w.wind_deg), math.floor(kmh + 0.5)) + or '--' + local rows = { + { 'FEELS LIKE', w.feels_like and string.format('%d\u{00B0}', math.floor(w.feels_like + 0.5)) or '--' }, + { 'HUMIDITY', w.humidity and (w.humidity .. '%') or '--' }, + { 'WIND', wind_txt }, + } + local sy = ry + 22 + for _, row in ipairs(rows) do + card.font(cr, card.FONT_MONO, 11, false) + card.rgba(cr, colors.label) + card.text(cr, inner.x, sy, row[1]) + card.rgba(cr, colors.value) + card.text_right(cr, inner.x + inner.w, sy, row[2]) + sy = sy + 20 + end + + -- Rule. + local ry2 = sy + 2 + card.rgba(cr, colors.rule, 0.8) + cairo_move_to(cr, inner.x, ry2) + cairo_line_to(cr, inner.x + inner.w, ry2) + cairo_stroke(cr) + + -- The arc fills whatever height is left. + draw_arc(cr, inner.x, ry2 + 10, inner.w, (inner.y + inner.h) - (ry2 + 10), + colors, w, now) +end + +return M |
