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 := Query.in_calendars calendars :: !filters);
51 (match query_text with
52 | Some text ->
53 if summary then filters := Query.summary_contains text :: !filters;
54 if description then filters := Query.description_contains text :: !filters;
55 if location then filters := Query.location_contains text :: !filters;
56 if not (summary || description || location) then
57 filters :=
58 Query.or_filter
59 [
60 Query.summary_contains text;
61 Query.description_contains text;
62 Query.location_contains text;
63 ]
64 :: !filters
65 | None -> ());
66 if recurring then filters := Query.recurring_only () :: !filters;
67 if non_recurring then filters := Query.non_recurring_only () :: !filters;
68 (match id with
69 | Some id -> filters := Query.with_id id :: !filters
70 | None -> ());
71 let filter = Query.and_filter !filters in
72 let comparator = Query_args.create_event_comparator sort in
73 let* results =
74 Query.query ~fs calendar_dir ~filter ~from ~to_ ~comparator ?limit:count ()
75 in
76 if results = [] then print_endline "No events found."
77 else print_endline (Event.format_events ~tz ~format results);
78 Ok ()
79
80let query_text_arg =
81 let doc =
82 "Text to search for in the fields summary, description, and/or location."
83 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 calendars 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:calendars ?count ?query_text ~summary
116 ~description ~location ~id ~format ~today ~tomorrow ~week ~month
117 ~recurring ~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. By default, the search looks across all text fields in all \
138 events regardless of date. You can specify specific fields to search \
139 in using the --summary, --description, or --location flags. You can \
140 use date flags to show events for a specific time period, and filter \
141 events with the --sort option.\n\
142 \ The search text is optional, so you search events according \
143 to other query criteria.";
144 `S Manpage.s_examples;
145 `I ("Search for 'meeting' in all events:", "caled search meeting");
146 `I
147 ( "Search for 'interview' in event summaries only:",
148 "caled search --summary interview" );
149 `I
150 ( "Search for 'conference' in a specific calendar:",
151 "caled search --calendar work conference" );
152 `I
153 ( "Search for 'workshop' in event descriptions for today only:",
154 "caled search --description --today workshop" );
155 `I
156 ( "Search for 'project' in events this month:",
157 "caled search --month project" );
158 `I
159 ( "Search for 'workshop' in event descriptions within a date range:",
160 "caled search --description --from 2025-03-27 --to 2025-04-01 \
161 workshop" );
162 `I
163 ("Search for recurring events only:", "caled search --recurring meeting");
164 `I
165 ( "Search for non-recurring events only:",
166 "caled search --non-recurring appointment" );
167 `I ("Find all recurring events:", "caled search --recurring");
168 `I
169 ( "Find all events in a specific calendar:",
170 "caled search --calendar work" );
171 `I
172 ( "Sort results by location and then summary:",
173 "caled search --sort location --sort summary" );
174 `I
175 ( "Sort results by end time in descending order:",
176 "caled search --sort end:desc" );
177 `S Manpage.s_options;
178 ]
179 @ date_format_manpage_entries
180 @ [ `S Manpage.s_see_also ]
181 in
182 let exit_info =
183 [ Cmd.Exit.info ~doc:"on success." 0; Cmd.Exit.info ~doc:"on error." 1 ]
184 in
185 let info = Cmd.info "search" ~doc ~man ~exits:exit_info in
186 Cmd.v info term