aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-17 09:11:56 +0200
committerDanilo M. <danix@danix.xyz>2026-09-17 09:11:56 +0200
commit2bdb74a1cbd4b33216d94713de1f1b3c81f631e2 (patch)
treeaae8b839b73110bada9274e4ee12e58e15c2964a
parent04542528f018678b64e0003eab078858c31f196e (diff)
downloadconky-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>
-rw-r--r--conky.conf.in7
-rw-r--r--dashboard.lua1
-rw-r--r--widgets/weather.lua177
3 files changed, 183 insertions, 2 deletions
diff --git a/conky.conf.in b/conky.conf.in
index 9587f91..1eb99b4 100644
--- a/conky.conf.in
+++ b/conky.conf.in
@@ -46,5 +46,8 @@ conky.config = {
default_shade_color = '@BODY_SHADE@',
}
--- Empty by design: Cairo output covers conky.text.
-conky.text = [[]]
+-- Cairo output covers conky.text, so nothing here is visible. The execi still
+-- fires on schedule: verified with a probe config whose only text was an execi
+-- producing no output. This is what refreshes the weather cache, and tying it
+-- to conky means nothing fetches while the dashboard is down.
+conky.text = [[${execi 900 ~/.config/conky/bin/weather-fetch.sh}]]
diff --git a/dashboard.lua b/dashboard.lua
index af6e51c..dfd19ff 100644
--- a/dashboard.lua
+++ b/dashboard.lua
@@ -20,6 +20,7 @@ local COLS, ROWS = 4, 2
local layout = {
{ widget = 'clock', col = 1, row = 1, w = 1, h = 2 },
+ { widget = 'weather', col = 2, row = 1, w = 1, h = 2 },
}
-- ==========================================================================
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