Command-line and Emacs Calendar Client
1type format = [ `Text | `Entries | `Json | `Csv | `Ics | `Sexp ] 2 3let format_date ?tz date = 4 let dt = Date.ptime_to_timedesc ?tz date in 5 let y = Timedesc.year dt in 6 let m = Timedesc.month dt in 7 let d = Timedesc.day dt in 8 let weekday = 9 match Timedesc.weekday dt with 10 | `Mon -> "Mon" 11 | `Tue -> "Tue" 12 | `Wed -> "Wed" 13 | `Thu -> "Thu" 14 | `Fri -> "Fri" 15 | `Sat -> "Sat" 16 | `Sun -> "Sun" 17 in 18 Printf.sprintf "%04d-%02d-%02d %s" y m d weekday 19 20let format_time ?tz date = 21 let dt = Date.ptime_to_timedesc ?tz date in 22 let h = Timedesc.hour dt in 23 let m = Timedesc.minute dt in 24 Printf.sprintf "%02d:%02d" h m 25 26let format_datetime ?tz date = 27 let tz_str = 28 match tz with 29 | Some tz -> Printf.sprintf "(%s)" (Timedesc.Time_zone.name tz) 30 | None -> "" 31 in 32 Printf.sprintf "%s %s%s" (format_date ?tz date) (format_time ?tz date) tz_str 33 34let next_day day ~next = 35 let y1, m1, d1 = Ptime.to_date day in 36 let y2, m2, d2 = Ptime.to_date next in 37 y1 == y2 && m1 == m2 && d1 == d2 - 1 38 39(* exosed from icalendar *) 40 41let weekday_strings = 42 [ 43 (`Monday, "MO"); 44 (`Tuesday, "TU"); 45 (`Wednesday, "WE"); 46 (`Thursday, "TH"); 47 (`Friday, "FR"); 48 (`Saturday, "SA"); 49 (`Sunday, "SU"); 50 ] 51 52let freq_strings = 53 [ 54 (`Daily, "DAILY"); 55 (`Hourly, "HOURLY"); 56 (`Minutely, "MINUTELY"); 57 (`Monthly, "MONTHLY"); 58 (`Secondly, "SECONDLY"); 59 (`Weekly, "WEEKLY"); 60 (`Yearly, "YEARLY"); 61 ] 62 63let date_to_str (y, m, d) = Printf.sprintf "%04d%02d%02d" y m d 64 65let datetime_to_str ptime utc = 66 let date, ((hh, mm, ss), _) = Ptime.to_date_time ptime in 67 Printf.sprintf "%sT%02d%02d%02d%s" (date_to_str date) hh mm ss 68 (if utc then "Z" else "") 69 70let timestamp_to_ics ts buf = 71 Buffer.add_string buf 72 @@ 73 match ts with 74 | `Utc ts -> datetime_to_str ts true 75 | `Local ts -> datetime_to_str ts false 76 | `With_tzid (ts, _str) -> (* TODO *) datetime_to_str ts false 77 78let recurs_to_ics (freq, count_or_until, interval, l) buf = 79 let write_rulepart key value = 80 Buffer.add_string buf key; 81 Buffer.add_char buf '='; 82 Buffer.add_string buf value 83 in 84 let int_list l = String.concat "," @@ List.map string_of_int l in 85 let recur_to_ics = function 86 | `Byminute byminlist -> write_rulepart "BYMINUTE" (int_list byminlist) 87 | `Byday bywdaylist -> 88 let wday (weeknumber, weekday) = 89 (if weeknumber = 0 then "" else string_of_int weeknumber) 90 ^ List.assoc weekday weekday_strings 91 in 92 write_rulepart "BYDAY" (String.concat "," @@ List.map wday bywdaylist) 93 | `Byhour byhrlist -> write_rulepart "BYHOUR" (int_list byhrlist) 94 | `Bymonth bymolist -> write_rulepart "BYMONTH" (int_list bymolist) 95 | `Bymonthday bymodaylist -> 96 write_rulepart "BYMONTHDAY" (int_list bymodaylist) 97 | `Bysecond byseclist -> write_rulepart "BYSECOND" (int_list byseclist) 98 | `Bysetposday bysplist -> write_rulepart "BYSETPOS" (int_list bysplist) 99 | `Byweek bywknolist -> write_rulepart "BYWEEKNO" (int_list bywknolist) 100 | `Byyearday byyrdaylist -> 101 write_rulepart "BYYEARDAY" (int_list byyrdaylist) 102 | `Weekday weekday -> 103 write_rulepart "WKST" (List.assoc weekday weekday_strings) 104 in 105 write_rulepart "FREQ" (List.assoc freq freq_strings); 106 (match count_or_until with 107 | None -> () 108 | Some x -> ( 109 Buffer.add_char buf ';'; 110 match x with 111 | `Count c -> write_rulepart "COUNT" (string_of_int c) 112 | `Until enddate -> 113 (* TODO cleanup *) 114 Buffer.add_string buf "UNTIL="; 115 timestamp_to_ics enddate buf)); 116 (match interval with 117 | None -> () 118 | Some i -> 119 Buffer.add_char buf ';'; 120 write_rulepart "INTERVAL" (string_of_int i)); 121 List.iter 122 (fun recur -> 123 Buffer.add_char buf ';'; 124 recur_to_ics recur) 125 l 126 127let format_alt ~fs ~calendar_dir ~format ~start ~end_ ?tz event = 128 let open Event in 129 match format with 130 | `Text -> 131 let id = get_id event in 132 let start_date = " " ^ format_date ?tz start in 133 let start_time = 134 match is_date event with 135 | true -> "" 136 | false -> " " ^ format_time ?tz start 137 in 138 let end_date, end_time = 139 match end_ with 140 | None -> ("", "") 141 | Some end_ -> ( 142 match (is_date event, next_day start ~next:end_) with 143 | true, true -> ("", "") 144 | true, _ -> (" - " ^ format_date ?tz end_, "") 145 | false, true -> ("", " - " ^ format_time ?tz end_) 146 | false, _ -> 147 (" - " ^ format_date ?tz end_, " " ^ format_time ?tz end_)) 148 in 149 let summary = 150 match get_summary event with 151 | Some summary when summary <> "" -> " " ^ summary 152 | _ -> "" 153 in 154 let location = 155 match get_location event with 156 | Some loc when loc <> "" -> " @" ^ loc 157 | _ -> "" 158 in 159 Printf.sprintf "%-45s%s%s%s%s%s%s" id start_date start_time end_date 160 end_time summary location 161 | `Entries -> 162 let format_opt label f opt = 163 Option.map (fun x -> Printf.sprintf "%s: %s\n" label (f x)) opt 164 |> Option.value ~default:"" 165 in 166 let format timezone datetime = 167 match is_date event with 168 | true -> format_date ?tz datetime 169 | false -> ( 170 format_datetime ?tz datetime 171 ^ match timezone with None -> "" | Some t -> " (" ^ t ^ ")") 172 in 173 let start_str = 174 format_opt "Start" (format (get_start_timezone event)) (Some start) 175 in 176 let end_str = format_opt "End" (format (get_end_timezone event)) end_ in 177 let location_str = format_opt "Location" Fun.id (get_location event) in 178 let description_str = 179 format_opt "Description" Fun.id (get_description event) 180 in 181 let rrule_str = 182 Option.map 183 (fun r -> 184 let buf = Buffer.create 128 in 185 recurs_to_ics r buf; 186 Printf.sprintf "%s: %s\n" "Reccurence" (Buffer.contents buf)) 187 (get_recurrence event) 188 |> Option.value ~default:"" 189 in 190 let summary_str = format_opt "Summary" Fun.id (get_summary event) in 191 let file_str = 192 format_opt "File" Fun.id 193 (Some 194 (snd 195 (get_file_path ~fs 196 ~calendar_dir_path:(Calendar_dir.get_path calendar_dir) 197 event))) 198 in 199 Printf.sprintf "%s%s%s%s%s%s%s" summary_str start_str end_str location_str 200 description_str rrule_str file_str 201 | `Json -> 202 let open Yojson.Safe in 203 let json = 204 `Assoc 205 [ 206 ("id", `String (get_id event)); 207 ( "summary", 208 match get_summary event with 209 | Some summary -> `String summary 210 | None -> `Null ); 211 ("start", `String (format_datetime ?tz start)); 212 ( "end", 213 match end_ with 214 | Some e -> `String (format_datetime ?tz e) 215 | None -> `Null ); 216 ( "location", 217 match get_location event with 218 | Some loc -> `String loc 219 | None -> `Null ); 220 ( "description", 221 match get_description event with 222 | Some desc -> `String desc 223 | None -> `Null ); 224 ( "calendar", 225 match get_collection event with 226 | Collection.Col cal -> `String cal ); 227 ] 228 in 229 to_string json 230 | `Csv -> 231 let summary = 232 match get_summary event with Some summary -> summary | None -> "" 233 in 234 let start = format_datetime ?tz start in 235 let end_str = 236 match end_ with Some e -> format_datetime ?tz e | None -> "" 237 in 238 let location = 239 match get_location event with Some loc -> loc | None -> "" 240 in 241 let cal_id = 242 match get_collection event with Collection.Col cal -> cal 243 in 244 Printf.sprintf "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"" summary start end_str 245 location cal_id 246 | `Ics -> 247 let cal_props = [] in 248 let event_ical = Event.to_icalendar event in 249 Icalendar.to_ics ~cr:true (cal_props, [ `Event event_ical ]) 250 | `Sexp -> 251 let summary = 252 match get_summary event with Some summary -> summary | None -> "" 253 in 254 let start_date, start_time = 255 let dt = Date.ptime_to_timedesc ?tz start in 256 let y = Timedesc.year dt in 257 let m = Timedesc.month dt in 258 let d = Timedesc.day dt in 259 let h = Timedesc.hour dt in 260 let min = Timedesc.minute dt in 261 let s = Timedesc.second dt in 262 let dow = 263 match Timedesc.weekday dt with 264 | `Mon -> "monday" 265 | `Tue -> "tuesday" 266 | `Wed -> "wednesday" 267 | `Thu -> "thursday" 268 | `Fri -> "friday" 269 | `Sat -> "saturday" 270 | `Sun -> "sunday" 271 in 272 ( Printf.sprintf "(%04d %02d %02d %s)" y m d dow, 273 Printf.sprintf "(%02d %02d %02d)" h min s ) 274 in 275 let end_str = 276 match end_ with 277 | Some end_date -> 278 let dt = Date.ptime_to_timedesc ?tz end_date in 279 let y = Timedesc.year dt in 280 let m = Timedesc.month dt in 281 let d = Timedesc.day dt in 282 let h = Timedesc.hour dt in 283 let min = Timedesc.minute dt in 284 let s = Timedesc.second dt in 285 let dow = 286 match Timedesc.weekday dt with 287 | `Mon -> "monday" 288 | `Tue -> "tuesday" 289 | `Wed -> "wednesday" 290 | `Thu -> "thursday" 291 | `Fri -> "friday" 292 | `Sat -> "saturday" 293 | `Sun -> "sunday" 294 in 295 Printf.sprintf "((%04d %02d %02d %s) (%02d %02d %02d))" y m d dow h 296 min s 297 | None -> "nil" 298 in 299 let location = 300 match get_location event with 301 | Some loc -> Printf.sprintf "\"%s\"" (String.escaped loc) 302 | None -> "nil" 303 in 304 let description = 305 match get_description event with 306 | Some desc -> Printf.sprintf "\"%s\"" (String.escaped desc) 307 | None -> "nil" 308 in 309 let calendar = 310 match get_collection event with 311 | Collection.Col cal -> Printf.sprintf "\"%s\"" (String.escaped cal) 312 in 313 let id = get_id event in 314 Printf.sprintf 315 "((:id \"%s\" :summary \"%s\" :start (%s %s) :end %s :location %s \ 316 :description %s :calendar %s))" 317 (String.escaped id) (String.escaped summary) start_date start_time 318 end_str location description calendar 319 320let format_event ~fs ~calendar_dir ?(format = `Text) ?tz event = 321 format_alt ~fs ~calendar_dir ~format ~start:(Event.get_start event) 322 ~end_:(Event.get_end event) ?tz event 323 324let format_instance ~fs ~calendar_dir ?(format = `Text) ?tz instance = 325 let open Recur in 326 format_alt ~fs ~calendar_dir ~format ~start:instance.start ~end_:instance.end_ 327 ?tz instance.event 328 329let format_events ~fs ~calendar_dir ?(format = `Text) ?tz events = 330 match format with 331 | `Json -> 332 let json_events = 333 List.map 334 (fun e -> 335 Yojson.Safe.from_string 336 (format_event ~fs ~calendar_dir ~format:`Json ?tz e)) 337 events 338 in 339 Yojson.Safe.to_string (`List json_events) 340 | `Csv -> 341 "\"Summary\",\"Start\",\"End\",\"Location\",\"Calendar\"\n" 342 ^ String.concat "\n" 343 (List.map (format_event ~fs ~calendar_dir ~format:`Csv ?tz) events) 344 | `Sexp -> 345 "(" 346 ^ String.concat "\n " 347 (List.map 348 (fun e -> format_event ~fs ~calendar_dir ~format:`Sexp ?tz e) 349 events) 350 ^ ")" 351 | _ -> 352 String.concat "\n" 353 (List.map 354 (fun e -> format_event ~fs ~calendar_dir ~format ?tz e) 355 events) 356 357let format_instances ~fs ~calendar_dir ?(format = `Text) ?tz instances = 358 match format with 359 | `Json -> 360 let json_instances = 361 List.map 362 (fun e -> 363 Yojson.Safe.from_string 364 (format_instance ~fs ~calendar_dir ~format:`Json ?tz e)) 365 instances 366 in 367 Yojson.Safe.to_string (`List json_instances) 368 | `Csv -> 369 "\"Summary\",\"Start\",\"End\",\"Location\",\"Calendar\"\n" 370 ^ String.concat "\n" 371 (List.map 372 (format_instance ~fs ~calendar_dir ~format:`Csv ?tz) 373 instances) 374 | `Sexp -> 375 "(" 376 ^ String.concat "\n " 377 (List.map 378 (fun e -> format_instance ~fs ~calendar_dir ~format:`Sexp ?tz e) 379 instances) 380 ^ ")" 381 | _ -> 382 String.concat "\n" 383 (List.map 384 (fun e -> format_instance ~fs ~calendar_dir ~format ?tz e) 385 instances)