1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
|
# Calendar panel design
A calendar panel that drops from the island's clock pill: a month grid beside
the selected day's agenda, and an editor that creates, edits and deletes
events in the vdirsyncer calendars. This is sub-project 3 of the roadmap in
`2026-10-07-island-core-design.md`.
The model is Modesty's calendar menu, read for ideas only (GPLv3, this repo is
GPLv2-only).
## Scope
In v1:
- Month grid with event dots, month navigation, today and selection marked.
- Agenda of the selected day.
- Create, edit and delete events: title, calendar, date or date range, start
and end time or all day, simple recurrence, one alarm, location, notes.
- Simple recurrence: none, daily, weekly, monthly, yearly, with an optional
until-date. Edit and delete act on the whole series.
- Live refresh when the calendar files change, whoever changed them.
- A sync after every write.
Not in v1: per-occurrence edits (`RECURRENCE-ID`, `EXDATE`, "this and
following"); rich recurrence rules beyond a plain `FREQ` and `UNTIL`; more than
one alarm per event from the form; a keybind; an upcoming-event hint on the
clock pill. Measured on the live calendars: no event uses `RECURRENCE-ID` or
`EXDATE`, so the cut loses nothing in use today.
## Where it lives
desktop/island/calendar/
calctl Python: list, new, edit, delete; JSON in and out
test_calctl.py plain asserts against temporary vdirs
Cal.js pure date logic for the QML, node-tested
test_cal.js node test for Cal.js
Calendar.qml singleton: calctl list, the file watch, writes
CalendarPanel.qml month grid left, agenda or editor right
EventEditor.qml the form
The island is the only consumer, so the backend sits beside its QML.
## Backend: calctl
`/usr/bin/python3` with `icalendar` and `dateutil`, both already installed for
cal-notif; nothing else outside the standard library. Calendars come from the
khal config's `[calendars]` section (name, path, colour), so there is no
second list to keep in step. khal and cal-notif keep working untouched: all
three read the same vdir.
khal was the roadmap's first idea for reading. It is not used: 0.11 has no JSON
output, and its sqlite cache is a second view of the same files.
Commands:
calctl calendars {default, calendars: [{name, color, path}]}
calctl list <from> <to> occurrences in [from, to), local time
calctl get <uid> the series itself, for the editor
calctl new <event-json> {uid}
calctl edit <uid> <event-json> {uid}
calctl delete <uid> {}
`list` expands recurrence with `dateutil.rrule` and returns one object per
occurrence: uid, calendar, title, start, end, allDay, location, notes, repeat,
until, alarm. `repeat` is `none`, `daily`, `weekly`, `monthly`, `yearly`, or
`custom` for any rule richer than a plain `FREQ` with optional `UNTIL`.
`alarm` is `none`, `10m`, `1h`, `1d`, or `custom` for anything else (several
alarms, another offset, an absolute trigger, an alarm after the start).
Writes:
- `new` writes `<uid>.ics` into the chosen calendar's directory. Timed events
carry a `TZID` for the local zone with its VTIMEZONE; all-day events are
`DATE` values with an exclusive end, per RFC 5545.
- `edit` loads the file and changes only the properties the form owns. Other
properties, VTIMEZONEs, a `custom` rule and `custom` alarms survive. Picking
a preset alarm replaces every VALARM; picking a preset repeat replaces the
RRULE. A calendar change moves the file to the other directory.
- `delete` removes the file.
- Every write goes to a temporary file in the same directory and is renamed
into place, so a reader never sees half a file.
- After a write, `vdirsyncer sync` is started detached, its output appended to
the existing sync log. A failed sync is not an error for calctl: the cron
run every 30 minutes retries.
Every command prints one JSON object on stdout: `{"ok": true, "data": ...}`
or `{"ok": false, "error": "..."}` with exit 1, the error also on stderr.
The shell reads stdout only, so it never has to order an exit code against
a stream. The event JSON is an argument rather than stdin, which keeps the
QML side to a plain `Process` command. `get` exists because the editor edits
the series: an occurrence's start is not the series' DTSTART.
Event JSON, in and out: `calendar`, `title`, `allDay`, `start`, `end`
(`YYYY-MM-DDTHH:MM` local, or `YYYY-MM-DD` all day with an inclusive last
day), `repeat`, `until` (`YYYY-MM-DD` or ""), `alarm`, `location`, `notes`;
out also `uid`, `color` and `override`.
Recurrence is expanded in naive local time, which keeps the wall-clock time
of a TZID event across DST, as RFC 5545 asks. ponytail: a `Z` UNTIL is read
as local time, so an until-date can be off by the UTC offset at its very
end.
## Shell side
`Calendar.qml` is a singleton. It runs `calctl list` for the six weeks the
month grid shows, and again on month navigation. It watches both calendar
directories with `inotifywait` for `close_write,moved_to,delete`, debounced,
the same shape as the mail count's xapian watch, so an edit from khal, the
phone via cron sync, or the panel itself all repaint the grid. Writes go
through it: `create`, `update` and `remove` run calctl and report the stderr
line on failure.
The clock pill gains a click that sets the island's `panel` to `"calendar"`.
The pill opens to about 620px; its flick and VM dots are unchanged at rest.
**Left: the month.** Seven columns, weeks from Monday (khal's
`firstweekday = 0`). Days outside the month dimmed, today in the accent's
text colour, the selected day outlined. A day with events carries a dot per
calendar that has events that day, in that calendar's khal colour mapped to a
Theme colour. Header with the month name and previous/next arrows.
**Right: the agenda.** The selected day's events in start order, all-day
first, each with a calendar-coloured bar, its time range and title. A click
opens it in the editor. "+ New event" at the bottom opens an empty editor for
that day.
**The editor** replaces the agenda. While it is open the month grid is its
date picker: a click sets the date, a shift-click a second day sets an end
date for a multi-day event. Fields:
- title;
- calendar, one chip per calendar;
- start and end time, two `HH:MM` text fields, scroll steps 15 minutes; an
all-day chip hides them;
- repeat: none, day, week, month, year, plus `custom` shown when the event has
a richer rule and not selectable; an optional until-date set by clicking the
grid after choosing "until";
- alarm: none, 10m, 1h, 1d, plus `custom` shown when not a preset and not
selectable; when cal-notif's `overrides.toml` has the event's uid, a note
says cal-notif's own alarm wins;
- location, notes;
- Delete (a second click confirms), Cancel, Save.
A new event defaults to the next whole hour, one hour long, the default
calendar from the khal config, alarm 1h (the most common alarm in the live
calendars, 84 of 97 events with one).
Escape closes the editor first and the panel second.
## Errors
A failed calctl keeps the editor open with its stderr line under the buttons,
so nothing typed is lost. A list that fails keeps the last good month and
shows the line in the agenda.
Sync conflicts follow the vdirsyncer pair's `conflict_resolution = "b wins"`:
an event edited here and on the server before the next sync keeps the server's
version. That is the existing behaviour, documented, not handled.
## Testing
- `test_calctl.py`: list across a month boundary, all-day and multi-day,
yearly expansion of an event created years ago, `custom` detection for a
rich rule and a non-preset alarm, edit preserving unknown properties and a
custom alarm, a calendar move, delete, atomic write leaving no temp file.
Run against temporary vdirs with a temporary khal config; never against the
live calendars.
- A smoke check of the live shell log after wiring.
- The user checks on screen: grid, agenda, create, edit, move between
calendars, repeat, alarm, delete, and an edit made in khal repainting the
panel.
|