Command-line and Emacs Calendar Client
1open Icalendar
2
3module CollectionMap = Map.Make (struct
4 type t = Collection.t
5
6 let compare (Collection.Col a) (Collection.Col b) = String.compare a b
7end)
8
9type t = { path : string; mutable collections : Event.t list CollectionMap.t }
10
11let get_collection_path ~fs calendar_dir (Collection.Col collection_name) =
12 Eio.Path.(fs / calendar_dir.path / collection_name)
13
14let ensure_dir path =
15 try
16 Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 path;
17 Ok ()
18 with Eio.Exn.Io _ as exn ->
19 Error
20 (`Msg
21 (Fmt.str "Failed to create directory %s: %a" (snd path) Eio.Exn.pp exn))
22
23let create ~fs path =
24 match ensure_dir Eio.Path.(fs / path) with
25 | Ok () -> Ok { path; collections = CollectionMap.empty }
26 | Error e -> Error e
27
28let list_collections ~fs calendar_dir =
29 try
30 let dir = Eio.Path.(fs / calendar_dir.path) in
31 let collections =
32 Eio.Path.read_dir dir
33 |> List.filter_map (fun file ->
34 if Eio.Path.is_directory Eio.Path.(dir / file) then
35 Some (Collection.Col file)
36 else None)
37 |> List.sort (fun (Collection.Col a) (Collection.Col b) ->
38 String.compare a b)
39 in
40 Ok collections
41 with Eio.Exn.Io _ as exn ->
42 Error
43 (`Msg
44 (Fmt.str "Failed to list calendar directory %s: %a" calendar_dir.path
45 Eio.Exn.pp exn))
46
47let load_events collection collection_path file_name =
48 let file = Eio.Path.(collection_path / file_name) in
49 let _, file_path = file in
50 match Filename.check_suffix file_name ".ics" with
51 | false -> []
52 | true -> (
53 try
54 let content = Eio.Path.load file in
55 match parse content with
56 | Ok calendar -> Event.events_of_icalendar ~file collection calendar
57 | Error err ->
58 Printf.eprintf "Failed to parse %s: %s\n%!" file_path err;
59 []
60 with Eio.Exn.Io _ as exn ->
61 Fmt.epr "Failed to read file %s: %a\n%!" file_path Eio.Exn.pp exn;
62 [])
63
64let get_collection ~fs calendar_dir collection =
65 match CollectionMap.find_opt collection calendar_dir.collections with
66 | Some events -> Ok events
67 | None -> (
68 let collection_path = get_collection_path ~fs calendar_dir collection in
69 if not (Eio.Path.is_directory collection_path) then Error `Not_found
70 else
71 try
72 let files = Eio.Path.read_dir collection_path in
73 let events =
74 List.flatten
75 @@ List.map (load_events collection collection_path) files
76 in
77 calendar_dir.collections <-
78 CollectionMap.add collection events calendar_dir.collections;
79 Ok events
80 with e ->
81 Error
82 (`Msg
83 (Printf.sprintf "Exception processing directory %s: %s"
84 (snd collection_path) (Printexc.to_string e))))
85
86let ( let* ) = Result.bind
87
88let get_events ~fs calendar_dir =
89 match list_collections ~fs calendar_dir with
90 | Error e -> Error e
91 | Ok ids -> (
92 try
93 let rec process_ids acc = function
94 | [] -> Ok (List.rev acc)
95 | id :: rest -> (
96 match get_collection ~fs calendar_dir id with
97 | Ok cal -> process_ids (cal :: acc) rest
98 | Error `Not_found -> process_ids acc rest
99 | Error (`Msg e) -> Error (`Msg e))
100 in
101 let* collections = process_ids [] ids in
102 Ok (List.flatten collections)
103 with exn ->
104 Error
105 (`Msg
106 (Printf.sprintf "Error getting collections: %s"
107 (Printexc.to_string exn))))
108
109let add_event ~fs calendar_dir event =
110 let collection = Event.get_collection event in
111 let file = Event.get_file event in
112 let collection_path = get_collection_path ~fs calendar_dir collection in
113 let* () = ensure_dir collection_path in
114 let calendar = Event.to_ical_calendar event in
115 let content = Icalendar.to_ics ~cr:true calendar in
116 let* _ =
117 try
118 Eio.Path.save ~create:(`Or_truncate 0o644) file content;
119 Ok ()
120 with Eio.Exn.Io _ as exn ->
121 Error
122 (`Msg
123 (Fmt.str "Failed to write file %s: %a\n%!" (snd file) Eio.Exn.pp exn))
124 in
125 calendar_dir.collections <-
126 CollectionMap.add collection
127 (event
128 ::
129 (match CollectionMap.find_opt collection calendar_dir.collections with
130 | Some lst -> lst
131 | None -> []))
132 calendar_dir.collections;
133 Ok ()
134
135let edit_event ~fs calendar_dir event =
136 let collection = Event.get_collection event in
137 let event_id = Event.get_id event in
138 let collection_path = get_collection_path ~fs calendar_dir collection in
139 let* () = ensure_dir collection_path in
140 let ical_event = Event.to_ical_event event in
141 let file = Event.get_file event in
142 let existing_props, existing_components = Event.to_ical_calendar event in
143 let calendar =
144 (* Replace the event with our updated version *)
145 let filtered_components =
146 List.filter
147 (function
148 | `Event e ->
149 (* Filter out the old event *)
150 let uid = e.Icalendar.uid in
151 snd uid <> event_id
152 | _ -> true)
153 existing_components
154 in
155 (existing_props, `Event ical_event :: filtered_components)
156 in
157 let content = Icalendar.to_ics ~cr:true calendar in
158 let* _ =
159 try
160 Eio.Path.save ~create:(`Or_truncate 0o644) file content;
161 Ok ()
162 with Eio.Exn.Io _ as exn ->
163 Error
164 (`Msg
165 (Fmt.str "Failed to write file %s: %a\n%!" (snd file) Eio.Exn.pp exn))
166 in
167 calendar_dir.collections <-
168 CollectionMap.add collection
169 (event
170 ::
171 (match CollectionMap.find_opt collection calendar_dir.collections with
172 (* filter old version *)
173 | Some lst -> List.filter (fun e -> Event.get_id e = event_id) lst
174 | None -> []))
175 calendar_dir.collections;
176 Ok ()
177
178let delete_event ~fs calendar_dir event =
179 let collection = Event.get_collection event in
180 let event_id = Event.get_id event in
181 let collection_path = get_collection_path ~fs calendar_dir collection in
182 let* () = ensure_dir collection_path in
183 let file = Event.get_file event in
184 let existing_props, existing_components = Event.to_ical_calendar event in
185 let other_events = ref false in
186 let calendar =
187 (* Replace the event with our updated version *)
188 let filtered_components =
189 List.filter
190 (function
191 | `Event e ->
192 (* Filter out the old event *)
193 let uid = e.Icalendar.uid in
194 if snd uid = event_id then false
195 else (
196 other_events := true;
197 true)
198 | _ -> true)
199 existing_components
200 in
201 (existing_props, filtered_components)
202 in
203 let content = Icalendar.to_ics ~cr:true calendar in
204 let* _ =
205 try
206 (match !other_events with
207 | true -> Eio.Path.save ~create:(`Or_truncate 0o644) file content
208 | false -> Eio.Path.unlink file);
209 Ok ()
210 with Eio.Exn.Io _ as exn ->
211 Error
212 (`Msg
213 (Fmt.str "Failed to write file %s: %a\n%!" (snd file) Eio.Exn.pp exn))
214 in
215 calendar_dir.collections <-
216 CollectionMap.add collection
217 (match CollectionMap.find_opt collection calendar_dir.collections with
218 (* filter old version *)
219 | Some lst -> List.filter (fun e -> Event.get_id e = event_id) lst
220 | None -> [])
221 calendar_dir.collections;
222 Ok ()
223
224let get_path t = t.path