Command-line and Emacs Calendar Client
1open Cmdliner 2open Caledonia_lib 3 4let from_arg = 5 let doc = 6 "Start date in YYYY-MM-DD format or a relative expression (today, \ 7 tomorrow, this-week, next-week, this-month, next-month, +Nd, -Nd, +Nw, \ 8 +Nm)" 9 in 10 let i = Arg.info [ "from"; "f" ] ~docv:"DATE" ~doc in 11 Arg.(value @@ opt (some string) None i) 12 13let to_arg = 14 let doc = 15 "End date in YYYY-MM-DD format or a relative expression (today, tomorrow, \ 16 this-week, next-week, this-month, next-month, +Nd, -Nd, +Nw, +Nm)" 17 in 18 let i = Arg.info [ "to"; "t" ] ~docv:"DATE" ~doc in 19 Arg.(value @@ opt (some string) None i) 20 21let calendar_arg = 22 let doc = "Calendar to filter by" in 23 Arg.( 24 value 25 & opt (some string) None 26 & info [ "calendar"; "c" ] ~docv:"CALENDAR" ~doc) 27 28let format_enum = 29 [ 30 ("text", `Text); 31 ("entries", `Entries); 32 ("json", `Json); 33 ("csv", `Csv); 34 ("ics", `Ics); 35 ("sexp", `Sexp); 36 ] 37 38let format_arg = 39 let doc = "Output format (text, entries, json, csv, ics, sexp)" in 40 Arg.( 41 value 42 & opt (enum format_enum) `Text 43 & info [ "format"; "o" ] ~docv:"FORMAT" ~doc) 44 45let count_arg = 46 let doc = "Maximum number of events to display" in 47 Arg.(value & opt (some int) None & info [ "count"; "n" ] ~docv:"COUNT" ~doc) 48 49let today_arg = 50 let doc = "Show events for today only" in 51 Arg.(value & flag & info [ "today"; "d" ] ~doc) 52 53let tomorrow_arg = 54 let doc = "Show events for tomorrow only" in 55 Arg.(value & flag & info [ "tomorrow" ] ~doc) 56 57let week_arg = 58 let doc = "Show events for the current week" in 59 Arg.(value & flag & info [ "week"; "w" ] ~doc) 60 61let month_arg = 62 let doc = "Show events for the current month" in 63 Arg.(value & flag & info [ "month"; "m" ] ~doc) 64 65let timezone_arg = 66 let doc = 67 "Timezone to use for date calculations (e.g., 'America/New_York', 'UTC', \ 68 'Europe/London') defaulting to the system timezone" 69 in 70 Arg.( 71 value 72 & opt (some string) None 73 & info [ "timezone"; "z" ] ~docv:"TIMEZONE" ~doc) 74 75let sort_field_enum = 76 [ 77 ("start", `Start); 78 ("end", `End); 79 ("summary", `Summary); 80 ("location", `Location); 81 ("calendar", `Calendar); 82 ] 83 84type sort_spec = { 85 field : [ `Start | `End | `Summary | `Location | `Calendar ]; 86 descending : bool; 87} 88 89let parse_sort_spec str = 90 let ( let* ) = Result.bind in 91 let parts = String.split_on_char ':' str in 92 match parts with 93 | [] -> Error (`Msg "Empty sort specification") 94 | field_str :: order_opt -> ( 95 let* descending = 96 match order_opt with 97 | [ "desc" ] | [ "descending" ] -> Ok true 98 | [ "asc" ] | [ "ascending" ] -> Ok false 99 | [] -> Ok false (* Default to ascending *) 100 | _ -> Error (`Msg ("Invalid sort order in: " ^ str)) 101 in 102 match List.assoc_opt field_str sort_field_enum with 103 | Some field -> Ok { field; descending } 104 | None -> 105 Error 106 (`Msg 107 (Printf.sprintf "Invalid sort field '%s'. Valid options are: %s" 108 field_str 109 (String.concat ", " (List.map fst sort_field_enum))))) 110 111let sort_converter = 112 let parse s = parse_sort_spec s in 113 let print ppf spec = 114 let field_str = 115 List.find_map 116 (fun (name, field) -> if field = spec.field then Some name else None) 117 sort_field_enum 118 in 119 let order_str = if spec.descending then ":desc" else "" in 120 Fmt.pf ppf "%s%s" (Option.value field_str ~default:"unknown") order_str 121 in 122 Arg.conv (parse, print) 123 124let default_sort = { field = `Start; descending = false } 125 126let sort_arg = 127 let doc = 128 "Sorting specifications in the format 'field[:order]' where field is one \ 129 of 'start', 'end', 'summary', 'location', 'calendar' and order is one of \ 130 'asc'/'ascending' or 'desc'/'descending' (default: asc). Multiple sort \ 131 specs can be provided for multi-level sorting. When no sort is specified, \ 132 defaults to sorting by start time ascending." 133 in 134 Arg.( 135 value 136 & opt_all sort_converter [ default_sort ] 137 & info [ "sort"; "S" ] ~docv:"SORT" ~doc) 138 139(* Convert sort specs to an event comparator *) 140let create_event_comparator sort_specs = 141 match sort_specs with 142 | [] -> Event.by_start 143 | [ spec ] -> 144 let comp = 145 match spec.field with 146 | `Start -> Event.by_start 147 | `End -> Event.by_end 148 | `Summary -> Event.by_summary 149 | `Location -> Event.by_location 150 | `Calendar -> Event.by_collection 151 in 152 if spec.descending then Event.descending comp else comp 153 | specs -> 154 (* Chain multiple sort specs together *) 155 List.fold_right 156 (fun spec acc -> 157 let comp = 158 match spec.field with 159 | `Start -> Event.by_start 160 | `End -> Event.by_end 161 | `Summary -> Event.by_summary 162 | `Location -> Event.by_location 163 | `Calendar -> Event.by_collection 164 in 165 let comp = if spec.descending then Event.descending comp else comp in 166 Event.chain comp acc) 167 (List.tl specs) 168 (let spec = List.hd specs in 169 let comp = 170 match spec.field with 171 | `Start -> Event.by_start 172 | `End -> Event.by_end 173 | `Summary -> Event.by_summary 174 | `Location -> Event.by_location 175 | `Calendar -> Event.by_collection 176 in 177 if spec.descending then Event.descending comp else comp) 178 179let date_format_manpage_entries = 180 [ 181 `S "DATE FORMATS"; 182 `P "Date format flags:"; 183 `I ("--today, -d", "Show events for today only"); 184 `I ("--tomorrow", "Show events for tomorrow only"); 185 `I ("--week, -w", "Show events for the current week"); 186 `I ("--month, -m", "Show events for the current month"); 187 `I 188 ( "--timezone, -z", 189 "Timezone to use for date calculations (e.g., 'America/New_York', \ 190 'UTC')" ); 191 `P "Relative date formats for --from and --to:"; 192 `I ("today", "Current day"); 193 `I ("tomorrow", "Next day"); 194 `I ("yesterday", "Previous day"); 195 `I ("this-week", "Start of current week"); 196 `I ("next-week", "Start of next week"); 197 `I ("this-month", "Start of current month"); 198 `I ("next-month", "Start of next month"); 199 `I ("+Nd", "N days from today (e.g., +7d for a week from today)"); 200 `I ("-Nd", "N days before today (e.g., -7d for a week ago)"); 201 `I ("+Nw", "N weeks from today (e.g., +4w for 4 weeks from today)"); 202 `I ("+Nm", "N months from today (e.g., +2m for 2 months from today)"); 203 ] 204 205let parse_timezone ~timezone = 206 match timezone with 207 | Some tzid -> ( 208 match Timedesc.Time_zone.make tzid with 209 | Some tz -> tz 210 | None -> failwith ("Invalid timezone: " ^ tzid)) 211 | None -> !Date.default_timezone ()