Command-line and Emacs Calendar Client
1open Cmdliner 2open Caledonia_lib 3open Query_args 4 5let run ?from_str ?to_str ?calendar ?count ?query_text ~summary ~description 6 ~location ~id ~format ~today ~tomorrow ~week ~month ~recurring 7 ~non_recurring ?timezone ~sort ~fs calendar_dir = 8 let ( let* ) = Result.bind in 9 let filters = ref [] in 10 let tz = Query_args.parse_timezone ~timezone in 11 let* from, to_ = 12 match 13 Date.convert_relative_date_formats ~tz ~today ~tomorrow ~week ~month () 14 with 15 | Some (from, to_) -> 16 let* _ = 17 match (from_str, to_str) with 18 | None, None -> Ok () 19 | _ -> 20 Error 21 (`Msg 22 "Can't specify --from / --to when using --today, --week, \ 23 --month") 24 in 25 Ok (Some from, to_) 26 | None -> ( 27 let* from = 28 match from_str with 29 | None -> Ok None 30 | Some s -> 31 let* d = Date.parse_date s `From in 32 Ok (Some d) 33 in 34 let* to_ = 35 match to_str with 36 | None -> Ok None 37 | Some s -> 38 let* d = Date.parse_date s `From in 39 Ok (Some d) 40 in 41 let max_date = Date.add_years (!Date.get_today ()) 75 in 42 match (from, to_) with 43 | Some f, Some t -> Ok (Some f, Date.to_end_of_day t) 44 | Some f, None -> Ok (Some f, Date.to_end_of_day max_date) 45 | None, Some t -> Ok (None, Date.to_end_of_day t) 46 | None, None -> Ok (None, Date.to_end_of_day max_date)) 47 in 48 (match calendar with 49 | Some collection_id -> 50 filters := 51 Query.in_collections [ Collection.Col collection_id ] :: !filters 52 | None -> ()); 53 (match query_text with 54 | Some text -> 55 if summary then filters := Query.summary_contains text :: !filters; 56 if description then filters := Query.description_contains text :: !filters; 57 if location then filters := Query.location_contains text :: !filters; 58 if not (summary || description || location) then 59 filters := 60 Query.or_filter 61 [ 62 Query.summary_contains text; 63 Query.description_contains text; 64 Query.location_contains text; 65 ] 66 :: !filters 67 | None -> ()); 68 if recurring then filters := Query.recurring_only () :: !filters; 69 if non_recurring then filters := Query.non_recurring_only () :: !filters; 70 (match id with 71 | Some id -> filters := Query.with_id id :: !filters 72 | None -> ()); 73 let filter = Query.and_filter !filters in 74 let comparator = Query_args.create_event_comparator sort in 75 let* results = 76 Query.query ~fs calendar_dir ~filter ~from ~to_ ~comparator ?limit:count () 77 in 78 if results = [] then print_endline "No events found." 79 else print_endline (Event.format_events ~tz ~format results); 80 Ok () 81 82let query_text_arg = 83 let doc = "Text to search for in events (summary, description, location)" in 84 Arg.(value & pos 0 (some string) None & info [] ~docv:"TEXT" ~doc) 85 86let summary_arg = 87 let doc = "Search in event summaries only" in 88 Arg.(value & flag & info [ "summary"; "s" ] ~doc) 89 90let description_arg = 91 let doc = "Search in event descriptions only" in 92 Arg.(value & flag & info [ "description"; "D" ] ~doc) 93 94let location_arg = 95 let doc = "Search in event locations only" in 96 Arg.(value & flag & info [ "location"; "l" ] ~doc) 97 98let recurring_arg = 99 let doc = "Search for recurring events only" in 100 Arg.(value & flag & info [ "recurring"; "r" ] ~doc) 101 102let non_recurring_arg = 103 let doc = "Search for non-recurring events only" in 104 Arg.(value & flag & info [ "non-recurring"; "R" ] ~doc) 105 106let id_arg = 107 let doc = "Search for an event with a specific ID" in 108 Arg.(value & opt (some string) None & info [ "id"; "i" ] ~docv:"ID" ~doc) 109 110let cmd ~fs calendar_dir = 111 let run query_text from_str to_str calendar count format summary description 112 location id today tomorrow week month recurring non_recurring timezone 113 sort = 114 match 115 run ?from_str ?to_str ?calendar ?count ?query_text ~summary ~description 116 ~location ~id ~format ~today ~tomorrow ~week ~month ~recurring 117 ~non_recurring ?timezone ~sort ~fs calendar_dir 118 with 119 | Error (`Msg msg) -> 120 Printf.eprintf "Error: %s\n%!" msg; 121 1 122 | Ok () -> 0 123 in 124 let term = 125 Term.( 126 const run $ query_text_arg $ from_arg $ to_arg $ calendar_arg $ count_arg 127 $ format_arg $ summary_arg $ description_arg $ location_arg $ id_arg 128 $ today_arg $ tomorrow_arg $ week_arg $ month_arg $ recurring_arg 129 $ non_recurring_arg $ timezone_arg $ sort_arg) 130 in 131 let doc = "Search calendar events for specific text" in 132 let man = 133 [ 134 `S Manpage.s_description; 135 `P 136 "Search calendar events for text in summary, description, or location \ 137 fields."; 138 `P 139 "By default, the search looks across all text fields in all events \ 140 regardless of date."; 141 `P 142 "You can narrow the search to a specific date range with date flags or \ 143 --from and --to."; 144 `P 145 "You can specify specific fields to search in using the --summary, \ 146 --description, or --location flags."; 147 `P 148 "You can limit results to only recurring or non-recurring events using \ 149 the --recurring or --non-recurring flags."; 150 `P "Use the --sort option to control the sorting of results."; 151 `P 152 "The search text is optional if you're using other filters. For \ 153 example, you can find all recurring events without specifying any \ 154 search text."; 155 `S Manpage.s_options; 156 ] 157 @ date_format_manpage_entries 158 @ [ 159 `S Manpage.s_examples; 160 `I ("Search for 'meeting' in all events:", "caled search meeting"); 161 `I 162 ( "Search for 'interview' in event summaries only:", 163 "caled search --summary interview" ); 164 `I 165 ( "Search for 'conference' in a specific calendar:", 166 "caled search --calendar work conference" ); 167 `I 168 ( "Search for 'workshop' in event descriptions for today only:", 169 "caled search --description --today workshop" ); 170 `I 171 ( "Search for 'project' in events this month:", 172 "caled search --month project" ); 173 `I 174 ( "Search for 'workshop' in event descriptions within a date range:", 175 "caled search --description --from 2025-03-27 --to 2025-04-01 \ 176 workshop" ); 177 `I 178 ( "Search for recurring events only:", 179 "caled search --recurring meeting" ); 180 `I 181 ( "Search for non-recurring events only:", 182 "caled search --non-recurring appointment" ); 183 `I ("Find all recurring events:", "caled search --recurring"); 184 `I 185 ( "Find all events in a specific calendar:", 186 "caled search --calendar work" ); 187 `I 188 ( "Sort results by location and then summary:", 189 "caled search --sort location --sort summary" ); 190 `I 191 ( "Sort results by end time in descending order:", 192 "caled search --sort end:desc" ); 193 ] 194 in 195 let exit_info = 196 [ Cmd.Exit.info ~doc:"on success." 0; Cmd.Exit.info ~doc:"on error." 1 ] 197 in 198 let info = Cmd.info "search" ~doc ~man ~exits:exit_info in 199 Cmd.v info term