Command-line and Emacs Calendar Client
1open Icalendar
2
3type event_id = string
4
5type t = {
6 collection : Collection.t;
7 file_name : string;
8 event : event;
9 calendar : calendar;
10}
11
12type date_error = [ `Msg of string ]
13
14let generate_uuid () =
15 let uuid = Uuidm.v4_gen (Random.State.make_self_init ()) () in
16 Uuidm.to_string uuid
17
18let default_prodid = `Prodid (Params.empty, "-//Freumh//Caledonia//EN")
19
20let create ~summary ~start ?end_ ?location ?description ?recurrence collection =
21 let uuid = generate_uuid () in
22 let uid = (Params.empty, uuid) in
23 let file_name = uuid ^ ".ics" in
24 let dtstart = (Params.empty, start) in
25 let dtend_or_duration = end_ in
26 let rrule = Option.map (fun r -> (Params.empty, r)) recurrence in
27 let now = Ptime_clock.now () in
28 let props = [ `Summary (Params.empty, summary) ] in
29 let props =
30 match location with
31 | Some loc -> `Location (Params.empty, loc) :: props
32 | None -> props
33 in
34 let props =
35 match description with
36 | Some desc -> `Description (Params.empty, desc) :: props
37 | None -> props
38 in
39 let event =
40 {
41 dtstamp = (Params.empty, now);
42 uid;
43 dtstart;
44 dtend_or_duration;
45 rrule;
46 props;
47 alarms = [];
48 }
49 in
50 let calendar =
51 let props = [ default_prodid ] in
52 let components = [ `Event event ] in
53 (props, components)
54 in
55 { collection; file_name; event; calendar }
56
57let edit ?summary ?start ?end_ ?location ?description ?recurrence t =
58 let now = Ptime_clock.now () in
59 let uid = t.event.uid in
60 let dtstart =
61 match start with None -> t.event.dtstart | Some s -> (Params.empty, s)
62 in
63 let dtend_or_duration =
64 match end_ with None -> t.event.dtend_or_duration | Some _ -> end_
65 in
66 let rrule =
67 match recurrence with
68 | None -> t.event.rrule
69 | Some r -> Some (Params.empty, r)
70 in
71 let props =
72 List.filter
73 (function
74 | `Summary _ -> ( match summary with None -> true | Some _ -> false)
75 | `Location _ -> ( match location with None -> true | Some _ -> false)
76 | `Description _ -> (
77 match description with None -> true | Some _ -> false)
78 | _ -> true)
79 t.event.props
80 in
81 let props =
82 match summary with
83 | Some summary -> `Summary (Params.empty, summary) :: props
84 | None -> props
85 in
86 let props =
87 match location with
88 | Some loc -> `Location (Params.empty, loc) :: props
89 | None -> props
90 in
91 let props =
92 match description with
93 | Some desc -> `Description (Params.empty, desc) :: props
94 | None -> props
95 in
96 let alarms = t.event.alarms in
97 let event =
98 {
99 dtstamp = (Params.empty, now);
100 uid;
101 dtstart;
102 dtend_or_duration;
103 rrule;
104 props;
105 alarms;
106 }
107 in
108 let collection = t.collection in
109 let file_name = t.file_name in
110 let calendar = t.calendar in
111 { collection; file_name; event; calendar }
112
113let events_of_icalendar collection ~file_name calendar =
114 List.filter_map
115 (function
116 | `Event event -> Some { collection; file_name; event; calendar }
117 | _ -> None)
118 (snd calendar)
119
120let to_ical_event t = t.event
121let to_ical_calendar t = t.calendar
122let get_id t = snd t.event.uid
123
124let get_summary t =
125 match
126 List.filter_map
127 (function `Summary (_, s) when s <> "" -> Some s | _ -> None)
128 t.event.props
129 with
130 | s :: _ -> Some s
131 | _ -> None
132
133let get_ical_start event =
134 Date.ptime_of_ical (snd event.dtstart)
135
136let get_start t = get_ical_start t.event
137
138let get_ical_end event =
139 match event.dtend_or_duration with
140 | Some (`Dtend (_, d)) -> Some (Date.ptime_of_ical d)
141 | Some (`Duration (_, span)) -> (
142 let start = get_ical_start event in
143 match Ptime.add_span start span with
144 | Some t -> Some t
145 | None ->
146 failwith
147 (Printf.sprintf "Invalid duration calculation: %s + %s"
148 (Ptime.to_rfc3339 start)
149 (Printf.sprintf "%.2fs" (Ptime.Span.to_float_s span))))
150 | None -> None
151
152let get_end t = get_ical_end t.event
153
154let get_start_timezone t =
155 match t.event.dtstart with
156 | _, `Datetime (`With_tzid (_, (_, tzid))) -> Some tzid
157 | _ -> None
158
159let get_end_timezone t =
160 match t.event.dtend_or_duration with
161 | Some (`Dtend (_, `Datetime (`With_tzid (_, (_, tzid))))) -> Some tzid
162 | _ -> None
163
164let get_duration t =
165 match t.event.dtend_or_duration with
166 | Some (`Duration (_, span)) -> Some span
167 | Some (`Dtend (_, e)) ->
168 let span = Ptime.diff (Date.ptime_of_ical e) (get_start t) in
169 Some span
170 | None -> None
171
172let is_date t =
173 match (t.event.dtstart, t.event.dtend_or_duration) with
174 | (_, `Date _), _ -> true
175 | _, Some (`Dtend (_, `Date _)) -> true
176 | _ -> false
177
178let get_location t =
179 match
180 List.filter_map
181 (function `Location (_, s) when s <> "" -> Some s | _ -> None)
182 t.event.props
183 with
184 | s :: _ -> Some s
185 | _ -> None
186
187let get_description t =
188 match
189 List.filter_map
190 (function `Description (_, s) when s <> "" -> Some s | _ -> None)
191 t.event.props
192 with
193 | s :: _ -> Some s
194 | _ -> None
195
196let get_recurrence t = Option.map (fun r -> snd r) t.event.rrule
197let get_collection t = t.collection
198
199let get_file_path ~fs ~calendar_dir_path t =
200 Eio.Path.(
201 fs / calendar_dir_path
202 / (match t.collection with Col s -> s)
203 / t.file_name)
204
205let get_recurrence_ids t =
206 let _, recurrence_ids =
207 match
208 List.partition (function `Event _ -> true | _ -> false) (snd t.calendar)
209 with
210 | `Event hd :: tl, _ ->
211 (hd, List.map (function `Event e -> e | _ -> assert false) tl)
212 | _ -> assert false
213 in
214 recurrence_ids
215
216type comparator = t -> t -> int
217
218let by_start e1 e2 =
219 let t1 = get_start e1 in
220 let t2 = get_start e2 in
221 Ptime.compare t1 t2
222
223let by_end e1 e2 =
224 match (get_end e1, get_end e2) with
225 | Some t1, Some t2 -> Ptime.compare t1 t2
226 | Some _, None -> 1
227 | None, Some _ -> -1
228 | None, None -> 0
229
230let by_summary e1 e2 =
231 match (get_summary e1, get_summary e2) with
232 | Some s1, Some s2 -> String.compare s1 s2
233 | Some _, None -> 1
234 | None, Some _ -> -1
235 | None, None -> 0
236
237let by_location e1 e2 =
238 match (get_location e1, get_location e2) with
239 | Some l1, Some l2 -> String.compare l1 l2
240 | Some _, None -> 1
241 | None, Some _ -> -1
242 | None, None -> 0
243
244let by_collection e1 e2 =
245 match (get_collection e1, get_collection e2) with
246 | Collection.Col c1, Collection.Col c2 -> String.compare c1 c2
247
248let descending comp e1 e2 = -1 * comp e1 e2
249
250let chain comp1 comp2 e1 e2 =
251 let result = comp1 e1 e2 in
252 if result <> 0 then result else comp2 e1 e2
253
254let clone_with_event t event =
255 let collection = t.collection in
256 let file_name = t.file_name in
257 let calendar = t.calendar in
258 { collection; file_name; event; calendar }
259
260let expand_recurrences ~from ~to_ event =
261 let rule = get_recurrence event in
262 match rule with
263 (* If there's no recurrence we just return the original event. *)
264 | None ->
265 (* Include the original event instance only if it falls within the query range. *)
266 let start = get_start event in
267 let end_ = match get_end event with None -> start | Some e -> e in
268 if
269 Ptime.compare start to_ < 0
270 &&
271 (* end_ > f, meaning we don't include events that end at the exact start of our range.
272 This is handy to exclude date events that end at 00:00 the next day. *)
273 match from with Some f -> Ptime.compare end_ f > 0 | None -> true
274 then [ event ]
275 else []
276 | Some _ ->
277 let rec collect generator acc =
278 match generator () with
279 | None -> List.rev acc
280 | Some recur ->
281 let start = get_ical_start recur in
282 let end_ = match get_ical_end recur with None -> start | Some e -> e in
283 (* if start >= to then we're outside our (exclusive) date range and we terminate *)
284 if Ptime.compare start to_ >= 0 then List.rev acc
285 (* if end > from then, *)
286 else if
287 match from with
288 | Some f -> Ptime.compare end_ f > 0
289 | None -> true
290 (* we include the event *)
291 then collect generator (clone_with_event event recur :: acc)
292 (* otherwise we iterate till the event is in range *)
293 else collect generator acc
294 in
295 let generator =
296 let ical_event = to_ical_event event in
297 let recurrence_ids = get_recurrence_ids event in
298 recur_events ~recurrence_ids ical_event
299 in
300 collect generator []