aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-18 14:31:39 +0200
committerDanilo M. <danix@danix.xyz>2026-10-18 14:31:39 +0200
commit28a3edc57047c2d8176e1cc726308c70f34ebc2e (patch)
treeb4783aa6b042f317af2331b3330d667406506438
parent719e1d98d61aa0614db86a9cab79402b304c4852 (diff)
downloadconky-theme-udt-28a3edc57047c2d8176e1cc726308c70f34ebc2e.tar.gz
conky-theme-udt-28a3edc57047c2d8176e1cc726308c70f34ebc2e.zip
feat(now): current-conditions card replaces the OWM weather card
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
-rw-r--r--dashboard.lua2
-rw-r--r--widgets/now.lua51
-rw-r--r--widgets/weather.lua202
3 files changed, 52 insertions, 203 deletions
diff --git a/dashboard.lua b/dashboard.lua
index 53b0ab8..579c08a 100644
--- a/dashboard.lua
+++ b/dashboard.lua
@@ -21,7 +21,7 @@ local COLS, ROWS = 16, 12
local layout = {
{ widget = 'clock', col = 1, row = 1.5, w = 1.5, h = 5 },
-- Under the clock, same column.
- { widget = 'weather', col = 1, row = 7, w = 3, h = 5 },
+ { widget = 'now', col = 1, row = 7, w = 3, h = 5 },
-- In the board's open middle column, left of immich.
{ widget = 'calendar', col = 2.5, row = 1, w = 3, h = 6 },
-- Clustered near weather and clock
diff --git a/widgets/now.lua b/widgets/now.lua
new file mode 100644
index 0000000..8fc6843
--- /dev/null
+++ b/widgets/now.lua
@@ -0,0 +1,51 @@
+-- Now: the current conditions. Replaces the OpenWeatherMap weather card; the
+-- daylight arc it carried moved to the sky card.
+--
+-- One of the forecast cards adapted from the Conky-Mirage theme by
+-- alexsson-xexpanderx (see lib/weather.lua).
+
+local card = require 'lib.card'
+local data = require 'lib.data'
+local weather = require 'lib.weather'
+
+local M = {}
+
+function M.draw(cr, rect, colors)
+ local inner = card.card(cr, rect, colors)
+ local w = weather.parse(data.slurp(weather.CACHE))
+ if not w then
+ -- Closed explicitly: this branch reruns every frame until a fetch lands,
+ -- so an unclosed handle would accumulate one per draw.
+ local envf = io.open((os.getenv('HOME') or '') .. '/.config/udt/weather.env', 'r')
+ local hint = 'set ~/.config/udt/weather.env'
+ if envf then
+ envf:close()
+ hint = 'bin/weather-fetch.sh'
+ end
+ card.notice(cr, inner, colors, 'WEATHER', 'no weather data', hint)
+ return
+ end
+
+ local c, today, now = w.current, w.days[1] or {}, os.time()
+ local rows = {
+ { 'FEELS LIKE', weather.deg(c.feels) },
+ { 'HIGH / LOW', weather.deg(today.max) .. ' / ' .. weather.deg(today.min) },
+ { 'RAIN', weather.num(today.precip, '%') },
+ { 'UPDATED', weather.age_str(c.dt, now),
+ weather.is_stale(c.dt, now) and colors.warning or nil },
+ }
+ local y, S = card.standard(cr, inner, colors, {
+ icon = weather.icon(c.code, c.is_day), title = 'WEATHER',
+ big = weather.deg(c.temp), rows = rows, share = 0.85,
+ })
+
+ -- The condition in words, in the prose face, in what the rows left.
+ local size = S * 0.36
+ if y + size * 1.4 <= inner.y + inner.h then
+ card.font(cr, card.FONT_UI, size, false)
+ card.rgba(cr, colors.body)
+ card.text(cr, inner.x, y + size * 1.4, weather.describe(c.code))
+ end
+end
+
+return M
diff --git a/widgets/weather.lua b/widgets/weather.lua
deleted file mode 100644
index a0c7b3a..0000000
--- a/widgets/weather.lua
+++ /dev/null
@@ -1,202 +0,0 @@
--- 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'
-
--- 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)
- -- Every dimension derives from the band, not fixed pixels: the time labels
- -- take a fraction of the height, the curve's room is what they leave, and the
- -- dot and edge pad follow the same proportions so the arc composes at any
- -- size the cell gives it.
- local label_h = math.max(10, math.min(16, h * 0.16))
- local pad = math.max(4, w * 0.012)
- local x0, x1 = x + pad, x + w - pad
- -- Room under the curve for the time labels.
- local base = y + h - label_h * 1.5
- local top = y + h * 0.08
-
- -- 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 - h * 0.14 }
- local c2 = { x1 - (x1 - x0) * 0.22, top - h * 0.14 }
- 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, math.max(3, math.min(9, h * 0.045)), 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, label_h, false)
- card.rgba(cr, colors.label)
- local ty = y + h - label_h * 0.15
- 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')
- local hint = 'waiting for first fetch'
- if not envf then
- hint = 'set ~/.config/udt/weather.env'
- else
- envf:close()
- end
- card.notice(cr, inner, colors, 'WEATHER', 'no weather data', hint)
- return
- end
-
- -- The card is much taller than idea1's, so the bands are placed against the
- -- rect rather than at fixed offsets: content sits in a block at the top, the
- -- arc gets a bounded band pinned to the bottom, and the slack falls between
- -- them. Letting the arc take all the leftover height instead drew a 794px
- -- cathedral arch on this monitor, with the content crammed into the top
- -- fifth. Scaled, not fixed, so the 1920 monitor gets the same proportions.
- local y = inner.y
-
- -- Type sizes scale with the card, within limits that keep them legible on a
- -- small cell and stop them ballooning on a tall one.
- -- Sizes scale off the WIDTH, not the height: the widget sits in a 1x1 cell
- -- whose height changes with the grid, and scaling off height made the type
- -- shrink to a whisper beside the clock's numerals when the cell got shorter.
- -- Width is the stable dimension here, and it is what constrains the text
- -- anyway, since the stat rows run edge to edge.
- local function clamp(v, lo, hi) return math.max(lo, math.min(hi, v)) end
- local temp_size = clamp(inner.w * 0.135, 40, 84)
- local glyph_size = clamp(temp_size * 0.95, 38, 78)
- local label_size = clamp(inner.w * 0.030, 12, 19)
- local desc_size = clamp(inner.w * 0.038, 15, 24)
- local row_size = clamp(inner.w * 0.032, 13, 20)
- local row_step = row_size * 2.0
-
- -- Header, mirrored to match the other cards: the condition glyph large on
- -- the left, the big temperature and the rest of the text against the right
- -- edge. text_right takes the RIGHT edge as its x, and measures ink rather
- -- than advance, which is what you want for aligning to an edge.
- local glyph = weather.icon(w.id, now, w.sunrise, w.sunset)
- card.font(cr, card.FONT_MONO, glyph_size, false)
- card.rgba(cr, colors.highlight)
- card.text(cr, inner.x, y + glyph_size * 0.95, glyph)
-
- card.font(cr, card.FONT_HEAVY, temp_size, false)
- card.rgba(cr, colors.body)
- card.text_right(cr, inner.x + inner.w, y + temp_size * 0.86,
- string.format('%d\u{00B0}', math.floor(w.temp + 0.5)))
-
- card.font(cr, card.FONT_MONO, label_size, 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_right(cr, inner.x + inner.w, y + temp_size * 1.28, city)
-
- -- Condition, in OWM's own words, first letter capitalised.
- card.font(cr, card.FONT_UI, desc_size, false)
- card.rgba(cr, colors.body)
- local desc = w.description or ''
- desc = desc:sub(1, 1):upper() .. desc:sub(2)
- local dy = y + temp_size * 1.95
- card.text_right(cr, inner.x + inner.w, dy, desc)
-
- -- Rule.
- local ry = dy + desc_size * 1.4
- 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 + row_step
- for _, row in ipairs(rows) do
- card.font(cr, card.FONT_MONO, row_size, 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 + row_step
- end
-
- -- The arc takes what is genuinely left below the stats, with no minimum: a
- -- floor here meant a short cell pulled the curve up THROUGH the stat rows,
- -- drawing the sunrise time over the word WIND. Below the height where it
- -- can be read it is dropped entirely, because no arc beats a broken one.
- -- This is what lets one widget sit in a 2x2 or a 2x3 cell unchanged.
- local bottom = inner.y + inner.h
- local arc_y = sy + row_step * 0.6
- -- The cap is a proportion of the card's width, not a fixed 240px: an arch
- -- taller than about 0.6 of its own width reads as a cathedral door rather
- -- than a day's arc, and the ceiling has to follow a card that is resized.
- local arc_h = math.min(bottom - arc_y, inner.h * 0.34, inner.w * 0.6)
- if arc_h >= 70 then
- draw_arc(cr, inner.x, arc_y, inner.w, arc_h, colors, w, now)
- end
-end
-
-
-return M