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