Command-line and Emacs Calendar Client
1type filter = 2 | SummaryContains of string 3 | DescriptionContains of string 4 | LocationContains of string 5 | InCollections of Calendar_dir.collection list 6 | RecurringOnly 7 | NonRecurringOnly 8 | WithId of Event.event_id 9 | And of filter list 10 | Or of filter list 11 | Not of filter 12 13type sort_order = [ `Ascending | `Descending ] 14type sort_by = [ `Start | `End | `Summary | `Location | `Calendar ] 15 16let calendar_to_ptime date = 17 let open CalendarLib in 18 let year = Calendar.year date in 19 let month = Date.int_of_month (Calendar.month date) in 20 let day = Calendar.day_of_month date in 21 let time = Calendar.to_time date in 22 let hour = Time.hour time in 23 let minute = Time.minute time in 24 let second = Time.second time in 25 match 26 Ptime.of_date_time ((year, month, day), ((hour, minute, second), 0)) 27 with 28 | Some t -> t 29 | None -> failwith "Invalid date conversion from Calendar to Ptime" 30 31let ptime_to_calendar ptime = 32 let (year, month, day), ((hour, minute, second), _) = 33 Ptime.to_date_time ptime 34 in 35 let open CalendarLib in 36 let date = Date.make year month day in 37 let time = Time.make hour minute second in 38 Calendar.create date time 39 40let get_today = 41 ref (fun () -> 42 let today_date = CalendarLib.Date.today () in 43 let midnight = CalendarLib.Time.make 0 0 0 in 44 let today_with_midnight = 45 CalendarLib.Calendar.create today_date midnight 46 in 47 calendar_to_ptime today_with_midnight) 48 49(* Convert a midnight timestamp to end-of-day (23:59:59) *) 50let to_end_of_day date = 51 let cal_date = ptime_to_calendar date in 52 let date_only = CalendarLib.Calendar.to_date cal_date in 53 let end_of_day_time = CalendarLib.Time.make 23 59 59 in 54 let end_of_day = CalendarLib.Calendar.create date_only end_of_day_time in 55 calendar_to_ptime end_of_day 56 57let add_days date days = 58 let cal_date = ptime_to_calendar date in 59 let period = CalendarLib.Calendar.Period.day days in 60 let new_date = CalendarLib.Calendar.add cal_date period in 61 calendar_to_ptime new_date 62 63let add_weeks date weeks = 64 let cal_date = ptime_to_calendar date in 65 let period = CalendarLib.Calendar.Period.week weeks in 66 let new_date = CalendarLib.Calendar.add cal_date period in 67 calendar_to_ptime new_date 68 69let add_months date months = 70 let cal_date = ptime_to_calendar date in 71 let period = CalendarLib.Calendar.Period.month months in 72 let new_date = CalendarLib.Calendar.add cal_date period in 73 calendar_to_ptime new_date 74 75let add_years date years = 76 let cal_date = ptime_to_calendar date in 77 let period = CalendarLib.Calendar.Period.year years in 78 let new_date = CalendarLib.Calendar.add cal_date period in 79 calendar_to_ptime new_date 80 81let get_start_of_week date = 82 let cal_date = ptime_to_calendar date in 83 let day_of_week = CalendarLib.Calendar.day_of_week cal_date in 84 let days_to_subtract = 85 match day_of_week with 86 | CalendarLib.Date.Mon -> 0 87 | CalendarLib.Date.Tue -> 1 88 | CalendarLib.Date.Wed -> 2 89 | CalendarLib.Date.Thu -> 3 90 | CalendarLib.Date.Fri -> 4 91 | CalendarLib.Date.Sat -> 5 92 | CalendarLib.Date.Sun -> 6 93 in 94 let monday = 95 CalendarLib.Calendar.add cal_date 96 (CalendarLib.Calendar.Period.day (-days_to_subtract)) 97 in 98 (* Extract the date part and create a new calendar with midnight time *) 99 let monday_date = CalendarLib.Calendar.to_date monday in 100 let midnight = CalendarLib.Time.make 0 0 0 in 101 let monday_at_midnight = CalendarLib.Calendar.create monday_date midnight in 102 calendar_to_ptime monday_at_midnight 103 104let get_start_of_current_week () = get_start_of_week (!get_today ()) 105let get_start_of_next_week () = add_days (get_start_of_current_week ()) 7 106let get_end_of_week date = add_days (get_start_of_week date) 6 107let get_end_of_current_week () = get_end_of_week (!get_today ()) 108let get_end_of_next_week () = get_end_of_week (get_start_of_next_week ()) 109 110let get_start_of_month date = 111 let cal_date = ptime_to_calendar date in 112 (* Extract year and month from calendar date *) 113 let year = CalendarLib.Calendar.year cal_date in 114 let month = CalendarLib.Calendar.month cal_date in 115 (* Create a date for the first of the month *) 116 let month_int = CalendarLib.Date.int_of_month month in 117 let first_day = CalendarLib.Date.make year month_int 1 in 118 let midnight = CalendarLib.Time.make 0 0 0 in 119 let first_of_month = CalendarLib.Calendar.create first_day midnight in 120 calendar_to_ptime first_of_month 121 122let get_start_of_current_month () = get_start_of_month (!get_today ()) 123let get_start_of_next_month () = add_months (get_start_of_current_month ()) 1 124 125let get_end_of_month date = 126 let cal_date = ptime_to_calendar date in 127 let year = CalendarLib.Calendar.year cal_date in 128 let month = CalendarLib.Calendar.month cal_date in 129 let month_int = CalendarLib.Date.int_of_month month in 130 (* Create a calendar for the first of next month *) 131 let next_month_int = if month_int == 12 then 1 else month_int + 1 in 132 let next_month_year = if month_int == 12 then year + 1 else year in 133 let first_of_next_month = 134 CalendarLib.Date.make next_month_year next_month_int 1 135 in 136 let midnight = CalendarLib.Time.make 0 0 0 in 137 let first_of_next_month_cal = 138 CalendarLib.Calendar.create first_of_next_month midnight 139 in 140 (* Subtract one second to get the end of the current month *) 141 let period = CalendarLib.Calendar.Period.second (-1) in 142 let last_of_month = CalendarLib.Calendar.add first_of_next_month_cal period in 143 calendar_to_ptime last_of_month 144 145let get_end_of_current_month () = get_end_of_month (!get_today ()) 146let get_end_of_next_month () = get_end_of_month (get_start_of_next_month ()) 147 148(* Parse a date string that could be ISO format or a relative expression *) 149let parse_date_expression expr parameter = 150 let iso_date_regex = Re.Pcre.regexp "^(\\d{4})-(\\d{2})-(\\d{2})$" in 151 let relative_regex = Re.Pcre.regexp "^([+-])(\\d+)([dwm])$" in 152 match expr with 153 | "today" -> !get_today () 154 | "tomorrow" -> add_days (!get_today ()) 1 155 | "yesterday" -> add_days (!get_today ()) (-1) 156 | "this-week" -> ( 157 match parameter with 158 | `From -> get_start_of_current_week () 159 | `To -> get_end_of_current_week ()) 160 | "next-week" -> ( 161 match parameter with 162 | `From -> get_start_of_next_week () 163 | `To -> get_end_of_next_week ()) 164 | "this-month" -> ( 165 match parameter with 166 | `From -> get_start_of_current_month () 167 | `To -> get_end_of_current_month ()) 168 | "next-month" -> ( 169 match parameter with 170 | `From -> get_start_of_next_month () 171 | `To -> get_end_of_next_month ()) 172 | _ -> 173 (* Try to parse as ISO date *) 174 if Re.Pcre.pmatch ~rex:iso_date_regex expr then 175 try 176 let year = 177 int_of_string 178 (Re.Pcre.get_substring (Re.Pcre.exec ~rex:iso_date_regex expr) 1) 179 in 180 let month = 181 int_of_string 182 (Re.Pcre.get_substring (Re.Pcre.exec ~rex:iso_date_regex expr) 2) 183 in 184 let day = 185 int_of_string 186 (Re.Pcre.get_substring (Re.Pcre.exec ~rex:iso_date_regex expr) 3) 187 in 188 match Ptime.of_date_time ((year, month, day), ((0, 0, 0), 0)) with 189 | Some date -> date 190 | None -> failwith (Printf.sprintf "Invalid date: %s" expr) 191 with e -> 192 failwith 193 (Printf.sprintf "Failed to parse ISO date '%s': %s" expr 194 (Printexc.to_string e)) 195 (* Try to parse as relative expression +Nd, -Nd, etc. *) 196 else if Re.Pcre.pmatch ~rex:relative_regex expr then 197 try 198 let sign = 199 Re.Pcre.get_substring (Re.Pcre.exec ~rex:relative_regex expr) 1 200 in 201 let num = 202 int_of_string 203 (Re.Pcre.get_substring (Re.Pcre.exec ~rex:relative_regex expr) 2) 204 in 205 let unit = 206 Re.Pcre.get_substring (Re.Pcre.exec ~rex:relative_regex expr) 3 207 in 208 let multiplier = if sign = "+" then 1 else -1 in 209 let value = num * multiplier in 210 let today = !get_today () in 211 match unit with 212 | "d" -> add_days today value 213 | "w" -> ( 214 let date = add_weeks today value in 215 match parameter with 216 | `From -> get_start_of_week date 217 | `To -> get_end_of_week date) 218 | "m" -> ( 219 let date = add_months today value in 220 match parameter with 221 | `From -> get_start_of_month date 222 | `To -> get_end_of_month date) 223 | _ -> failwith (Printf.sprintf "Invalid date unit: %s" unit) 224 with e -> 225 failwith 226 (Printf.sprintf "Failed to parse relative date '%s': %s" expr 227 (Printexc.to_string e)) 228 else failwith (Printf.sprintf "Invalid date format: %s" expr) 229 230let convert_relative_date_formats ~today ~tomorrow ~week ~month = 231 if today then 232 let today_date = !get_today () in 233 (* Set the end date to end-of-day to include all events on that day *) 234 let end_of_today = to_end_of_day today_date in 235 Some (today_date, end_of_today) 236 else if tomorrow then 237 let today = !get_today () in 238 let tomorrow_date = add_days today 1 in 239 (* Set the end date to end-of-day to include all events on that day *) 240 let end_of_tomorrow = to_end_of_day tomorrow_date in 241 Some (tomorrow_date, end_of_tomorrow) 242 else if week then 243 let week_start = get_start_of_current_week () in 244 let week_end_date = add_days week_start 6 in 245 (* Sunday is 6 days from Monday *) 246 (* Set the end date to end-of-day to include all events on Sunday *) 247 let end_of_week = to_end_of_day week_end_date in 248 Some (week_start, end_of_week) 249 else if month then 250 let month_start = get_start_of_current_month () in 251 let month_end = get_end_of_month month_start in 252 Some (month_start, month_end) 253 else None 254 255let summary_contains text = SummaryContains text 256let description_contains text = DescriptionContains text 257let location_contains text = LocationContains text 258let in_collections ids = InCollections ids 259let recurring_only () = RecurringOnly 260let non_recurring_only () = NonRecurringOnly 261let with_id id = WithId id 262let and_filter filters = And filters 263let or_filter filters = Or filters 264let not_filter filter = Not filter 265 266let rec matches_filter event = function 267 | SummaryContains text -> 268 let summary = Event.get_summary event in 269 let re = Re.Pcre.regexp ~flags:[ `CASELESS ] (Re.Pcre.quote text) in 270 Re.Pcre.pmatch ~rex:re summary 271 | DescriptionContains text -> ( 272 match Event.get_description event with 273 | Some desc -> 274 let re = Re.Pcre.regexp ~flags:[ `CASELESS ] (Re.Pcre.quote text) in 275 Re.Pcre.pmatch ~rex:re desc 276 | None -> false) 277 | LocationContains text -> ( 278 match Event.get_location event with 279 | Some loc -> 280 let re = Re.Pcre.regexp ~flags:[ `CASELESS ] (Re.Pcre.quote text) in 281 Re.Pcre.pmatch ~rex:re loc 282 | None -> false) 283 | InCollections ids -> ( 284 match Event.get_collection event with 285 | Some id -> List.exists (fun col -> col = id) ids 286 | None -> false) 287 | RecurringOnly -> Event.get_recurrence event <> None 288 | NonRecurringOnly -> Event.get_recurrence event = None 289 | WithId id -> Event.get_id event = id 290 | And filters -> List.for_all (matches_filter event) filters 291 | Or filters -> List.exists (matches_filter event) filters 292 | Not filter -> not (matches_filter event filter) 293 294let compare_events sort_by order e1 e2 = 295 let compare = 296 match sort_by with 297 | `Start -> 298 let t1 = Event.get_start e1 in 299 let t2 = Event.get_start e2 in 300 Ptime.compare t1 t2 301 | `End -> ( 302 match (Event.get_end e1, Event.get_end e2) with 303 | Some t1, Some t2 -> Ptime.compare t1 t2 304 | Some _, None -> 1 305 | None, Some _ -> -1 306 | None, None -> 0) 307 | `Summary -> String.compare (Event.get_summary e1) (Event.get_summary e2) 308 | `Location -> ( 309 match (Event.get_location e1, Event.get_location e2) with 310 | Some l1, Some l2 -> String.compare l1 l2 311 | Some _, None -> 1 312 | None, Some _ -> -1 313 | None, None -> 0) 314 | `Calendar -> ( 315 match (Event.get_collection e1, Event.get_collection e2) with 316 | Some (Calendar_dir.Collection c1), Some (Calendar_dir.Collection c2) 317 -> 318 String.compare c1 c2 319 | Some _, None -> 1 320 | None, Some _ -> -1 321 | None, None -> 0) 322 in 323 match order with `Ascending -> compare | `Descending -> -compare 324 325let get_all_events ~fs calendar_dir = 326 match Calendar_dir.get_collections ~fs calendar_dir with 327 | Ok collections -> 328 let events = 329 List.concat_map 330 (fun (collection, calendar_files) -> 331 List.concat_map 332 (fun calendar_file -> 333 List.filter_map 334 (function 335 | `Event e -> ( 336 match Event.of_icalendar collection e with 337 | Ok event -> Some event 338 | Error (`Msg msg) -> 339 Printf.eprintf "Error parsing event from %s: %s\n%!" 340 calendar_file.Calendar_dir.file_path msg; 341 None) 342 | _ -> None) 343 (snd calendar_file.Calendar_dir.calendar)) 344 calendar_files) 345 collections 346 in 347 Ok events 348 | Error e -> Error e 349 350let query_events ~fs calendar_dir ?filter ?sort_by ?order ?limit () = 351 match get_all_events ~fs calendar_dir with 352 | Ok events -> 353 let filtered_events = 354 match filter with 355 | Some f -> List.filter (fun event -> matches_filter event f) events 356 | None -> events 357 in 358 let sorted_events = 359 match (sort_by, order) with 360 | Some criteria, Some ord -> 361 List.sort (compare_events criteria ord) filtered_events 362 | Some criteria, None -> 363 List.sort (compare_events criteria `Ascending) filtered_events 364 | None, _ -> 365 List.sort (compare_events `Start `Ascending) filtered_events 366 in 367 Ok 368 (match limit with 369 | Some n when n > 0 -> 370 let rec take n lst acc = 371 match (lst, n) with 372 | _, 0 -> List.rev acc 373 | [], _ -> List.rev acc 374 | x :: xs, n -> take (n - 1) xs (x :: acc) 375 in 376 take n sorted_events [] 377 | _ -> sorted_events) 378 | Error e -> Error e 379 380let query ~fs calendar_dir ?filter ~from ~to_ ?sort_by ?order ?limit () = 381 match query_events ~fs calendar_dir ?filter ?sort_by ?order () with 382 | Ok events -> 383 let instances = 384 List.concat_map 385 (fun event -> Recur.expand_event event ~from ~to_) 386 events 387 in 388 let compare_instances criteria ord i1 i2 = 389 match criteria with 390 | `Start -> 391 let c = Ptime.compare i1.Recur.start i2.Recur.start in 392 if ord = `Ascending then c else -c 393 | `End -> ( 394 match (i1.Recur.end_, i2.Recur.end_) with 395 | Some t1, Some t2 -> 396 let c = Ptime.compare t1 t2 in 397 if ord = `Ascending then c else -c 398 | Some _, None -> if ord = `Ascending then 1 else -1 399 | None, Some _ -> if ord = `Ascending then -1 else 1 400 | None, None -> 0) 401 | other -> 402 let c = compare_events other ord i1.Recur.event i2.Recur.event in 403 if ord = `Ascending then c else -c 404 in 405 let sorted_instances = 406 match (sort_by, order) with 407 | Some criteria, Some ord -> 408 List.sort (compare_instances criteria ord) instances 409 | Some criteria, None -> 410 List.sort (compare_instances criteria `Ascending) instances 411 | None, _ -> List.sort (compare_instances `Start `Ascending) instances 412 in 413 Ok 414 (match limit with 415 | Some n when n > 0 -> 416 let rec take n lst acc = 417 match (lst, n) with 418 | _, 0 -> List.rev acc 419 | [], _ -> List.rev acc 420 | x :: xs, n -> take (n - 1) xs (x :: acc) 421 in 422 take n sorted_instances [] 423 | _ -> sorted_instances) 424 | Error e -> Error e