From c64bf67e15b075182deb4eebead8315c9081ac46 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 18 Sep 2026 09:26:32 +0200 Subject: feat: add a calendar card reading khal The month grid with the week's appointments below it, colour-coded by calendar. Events come from a cache file rather than a live call: khal costs about 200ms of Python startup, which is two orders of magnitude over the 2-second draw budget, so bin/calendar-sample.sh writes the cache on a 5-minute execi and the widget only parses. Calendar colours are read out of khal's own config by the sampler and mapped onto the board's palette roles, so the card follows a scheme change instead of pinning three literal hues. khal will not emit an ISO date: {start-date} uses the user's dateformat, which carries no year here, and a strftime spec inside the field raises. The sampler asks for {start-date-long} and data.calendar_date recovers the numbers by position, resolving day/month by magnitude and reading a genuinely ambiguous pair day-first. A 2-digit year is refused rather than guessed at. Co-Authored-By: Claude Opus 5 --- test/fixtures/calendar_cache | 10 ++++++++ test/test_data.lua | 61 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 test/fixtures/calendar_cache (limited to 'test') diff --git a/test/fixtures/calendar_cache b/test/fixtures/calendar_cache new file mode 100644 index 0000000..c31ab94 --- /dev/null +++ b/test/fixtures/calendar_cache @@ -0,0 +1,10 @@ +generated 1789700000 +today 2026-09-18 +color personal light green +color birthdays light magenta +color work light blue +event 18.09.2026|13:00|13:30|personal|Dentist +event 18.09.2026|||birthdays|A birthday +event 24.09.2026|14:30|15:30|work|Team sync | notes +event 26.09.2026|||birthdays|Another birthday +event 02.10.2026|09:00|10:00|work|Review diff --git a/test/test_data.lua b/test/test_data.lua index 34021c5..00616d6 100644 --- a/test/test_data.lua +++ b/test/test_data.lua @@ -322,4 +322,65 @@ assert(data.iface_addr_parse('5: br0: mtu 1500 state DOWN\ assert(data.iface_addr_parse('') == nil, 'empty gives nil') assert(data.iface_addr_parse(nil) == nil, 'nil gives nil') +-- === Calendar ============================================================= +-- The fixture uses generic event titles. This repository is public: a real +-- appointment names a real person or a real appointment, and neither belongs +-- in a committed file. +local cal = data.calendar_parse(read('test/fixtures/calendar_cache')) +assert(cal, 'calendar cache parses') +assert(cal.today[1] == 2026 and cal.today[2] == 9 and cal.today[3] == 18, + 'today parses') +assert(cal.colors.personal == 'light green', 'calendar colour, got ' + .. tostring(cal.colors.personal)) +assert(#cal.events == 5, 'event count, got ' .. tostring(#cal.events)) + +-- A timed event keeps its times; an all-day one has neither, and the card +-- shows 'all day' rather than an invented 00:00. +assert(cal.events[1].start == '13:00' and cal.events[1].finish == '13:30', + 'timed event keeps its times') +assert(cal.events[2].start == nil and cal.events[2].finish == nil, + 'all-day event has no times') + +-- A title containing a pipe survives: only the first four separators split. +assert(cal.events[3].title == 'Team sync | notes', + 'title keeps its pipe, got ' .. tostring(cal.events[3].title)) + +-- No 'generated' key means the sampler never ran, which is not the same as +-- having run and found nothing. +assert(data.calendar_parse('today 2026-09-18\n') == nil, 'no generated key: nil') +assert(data.calendar_parse('') == nil, 'empty cache: nil') +assert(data.calendar_parse(nil) == nil, 'nil cache: nil') +local empty = data.calendar_parse('generated 1\ntoday 2026-09-18\n') +assert(empty and #empty.events == 0, 'ran with no events: empty list, not nil') + +-- calendar_date: the year is the 4-digit group wherever it sits, and the +-- day/month pair is resolved by magnitude. +local function ymd(s) + local t = data.calendar_date(s) + return t and table.concat(t, '-') or nil +end +assert(ymd('18.09.2026') == '2026-9-18', 'day-first, got ' .. tostring(ymd('18.09.2026'))) +assert(ymd('09/18/2026') == '2026-9-18', 'month-first resolved by magnitude') +assert(ymd('2026-09-18') == '2026-9-18', 'ISO, year leading') +-- Genuinely ambiguous: both below 13. Day-first, matching khal's defaults. +assert(ymd('03.04.2026') == '2026-4-3', 'ambiguous reads day-first') +assert(ymd('18.09.26') == nil, 'two-digit year is refused, not guessed') +assert(ymd('nonsense') == nil, 'unparseable date: nil') +assert(ymd('18.13.2026') == nil, 'impossible month: nil') + +-- month_days: first calendar of a day wins, and only the asked-for month. +local days = data.calendar_month_days(cal.events, 2026, 9) +assert(days[18] == 'personal', 'first event of the day owns it, got ' + .. tostring(days[18])) +assert(days[24] == 'work' and days[26] == 'birthdays', 'later days marked') +assert(days[2] == nil, 'October event does not mark September') + +-- month_shape: September 2026 has 30 days and starts on a Tuesday. +-- Monday = 1, so Tuesday = 2. +local ndays, first = data.month_shape(2026, 9) +assert(ndays == 30 and first == 2, 'september 2026, got ' .. ndays .. '/' .. first) +-- February in a leap year, and a December that must roll into January. +assert((data.month_shape(2024, 2)) == 29, 'leap february has 29 days') +assert((data.month_shape(2026, 12)) == 31, 'december rolls over correctly') + print('test_data: all assertions passed') -- cgit v1.2.3