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 `To 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 | [] -> () 50 | calendars -> filters := Event.in_calendars calendars :: !filters); 51 (match query_text with 52 | Some text -> 53 if summary then filters := Event.summary_contains text :: !filters; 54 if description then filters := Event.description_contains text :: !filters; 55 if location then filters := Event.location_contains text :: !filters; 56 if not (summary || description || location) then 57 filters := 58 Event.or_filter 59 [ 60 Event.summary_contains text; 61 Event.description_contains text; 62 Event.location_contains text; 63 ] 64 :: !filters 65 | None -> ()); 66 if recurring then filters := Event.recurring_only () :: !filters; 67 if non_recurring then filters := Event.non_recurring_only () :: !filters; 68 (match id with 69 | Some id -> filters := Event.with_id id :: !filters 70 | None -> ()); 71 let filter = Event.and_filter !filters in 72 let comparator = Query_args.create_event_comparator sort in 73 let* events = Calendar_dir.get_events ~fs calendar_dir in 74 let events = 75 Event.query events ~filter ~from ~to_ ~comparator ?limit:count () 76 in 77 if events = [] then print_endline "No events found." 78 else print_endline (Event.format_events ~tz ~format events); 79 Ok () 80 81let query_text_arg = 82 let doc = 83 "Text to search for in the fields summary, description, and/or location." 84 in 85 Arg.(value & pos 0 (some string) None & info [] ~docv:"TEXT" ~doc) 86 87let summary_arg = 88 let doc = "Search in event summaries only" in 89 Arg.(value & flag & info [ "summary"; "s" ] ~doc) 90 91let description_arg = 92 let doc = "Search in event descriptions only" in 93 Arg.(value & flag & info [ "description"; "D" ] ~doc) 94 95let location_arg = 96 let doc = "Search in event locations only" in 97 Arg.(value & flag & info [ "location"; "l" ] ~doc) 98 99let recurring_arg = 100 let doc = "Search for recurring events only" in 101 Arg.(value & flag & info [ "recurring"; "r" ] ~doc) 102 103let non_recurring_arg = 104 let doc = "Search for non-recurring events only" in 105 Arg.(value & flag & info [ "non-recurring"; "R" ] ~doc) 106 107let id_arg = 108 let doc = "Search for an event with a specific ID" in 109 Arg.(value & opt (some string) None & info [ "id"; "i" ] ~docv:"ID" ~doc) 110 111let cmd ~fs calendar_dir = 112 let run query_text from_str to_str calendars count format summary description 113 location id today tomorrow week month recurring non_recurring timezone 114 sort () = 115 match 116 run ?from_str ?to_str ~calendar:calendars ?count ?query_text ~summary 117 ~description ~location ~id ~format ~today ~tomorrow ~week ~month 118 ~recurring ~non_recurring ?timezone ~sort ~fs calendar_dir 119 with 120 | Error (`Msg msg) -> 121 Printf.eprintf "Error: %s\n%!" msg; 122 1 123 | Ok () -> 0 124 in 125 let term = 126 Term.( 127 const run $ query_text_arg $ from_arg $ to_arg $ calendar_arg $ count_arg 128 $ format_arg $ summary_arg $ description_arg $ location_arg $ id_arg 129 $ today_arg $ tomorrow_arg $ week_arg $ month_arg $ recurring_arg 130 $ non_recurring_arg $ timezone_arg $ sort_arg) 131 in 132 let doc = "Search calendar events for specific text" in 133 let man = 134 [ 135 `S Manpage.s_description; 136 `P 137 "Search calendar events for text in summary, description, or location \ 138 fields. By default, the search looks across all text fields in all \ 139 events regardless of date. You can specify specific fields to search \ 140 in using the --summary, --description, or --location flags. You can \ 141 use date flags to show events for a specific time period, and filter \ 142 events with the --sort option.\n\ 143 \ The search text is optional, so you search events according \ 144 to other query criteria."; 145 `S Manpage.s_examples; 146 `I ("Search for 'meeting' in all events:", "caled search meeting"); 147 `I 148 ( "Search for 'interview' in event summaries only:", 149 "caled search --summary interview" ); 150 `I 151 ( "Search for 'conference' in a specific calendar:", 152 "caled search --calendar work conference" ); 153 `I 154 ( "Search for 'workshop' in event descriptions for today only:", 155 "caled search --description --today workshop" ); 156 `I 157 ( "Search for 'project' in events this month:", 158 "caled search --month project" ); 159 `I 160 ( "Search for 'workshop' in event descriptions within a date range:", 161 "caled search --description --from 2025-03-27 --to 2025-04-01 \ 162 workshop" ); 163 `I 164 ("Search for recurring events only:", "caled search --recurring meeting"); 165 `I 166 ( "Search for non-recurring events only:", 167 "caled search --non-recurring appointment" ); 168 `I ("Find all recurring events:", "caled search --recurring"); 169 `I 170 ( "Find all events in a specific calendar:", 171 "caled search --calendar work" ); 172 `I 173 ( "Sort results by location and then summary:", 174 "caled search --sort location --sort summary" ); 175 `I 176 ( "Sort results by end time in descending order:", 177 "caled search --sort end:desc" ); 178 `S Manpage.s_options; 179 ] 180 @ date_format_manpage_entries 181 @ [ `S Manpage.s_see_also ] 182 in 183 let exit_info = 184 [ Cmd.Exit.info ~doc:"on success." 0; Cmd.Exit.info ~doc:"on error." 1 ] 185 in 186 let info = Cmd.info "search" ~doc ~man ~exits:exit_info in 187 Cmd.v info term