-- 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