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