Command-line and Emacs Calendar Client

store file_path in Event module

Ryan Gibb bad3784a 5b56468e

+3 -2
bin/add_cmd.ml
···
in
let collection = Collection.Col collection in
let event =
-
Event.create ~summary ~start ?end_ ?location ?description ?recurrence
-
collection
+
Event.create ~fs
+
~calendar_dir_path:(Calendar_dir.get_path calendar_dir)
+
~summary ~start ?end_ ?location ?description ?recurrence collection
in
let* _ = Calendar_dir.add_event ~fs calendar_dir event in
Printf.printf "Event created with ID: %s\n" (Event.get_id event);
+1 -2
bin/list_cmd.ml
···
Query.query ~fs calendar_dir ?filter ~from ~to_ ~comparator ?limit:count ()
in
if results = [] then print_endline "No events found."
-
else
-
print_endline (Format.format_events ~fs ~calendar_dir ~format ~tz results);
+
else print_endline (Format.format_events ~format ~tz results);
Ok ()
let cmd ~fs calendar_dir =
+1 -2
bin/search_cmd.ml
···
Query.query ~fs calendar_dir ~filter ~from ~to_ ~comparator ?limit:count ()
in
if results = [] then print_endline "No events found."
-
else
-
print_endline (Format.format_events ~tz ~fs ~calendar_dir ~format results);
+
else print_endline (Format.format_events ~tz ~format results);
Ok ()
let query_text_arg =
+1 -1
bin/show_cmd.ml
···
let filter = Query.with_id event_id in
let* results = Query.query_without_recurrence ~fs calendar_dir ~filter () in
if results = [] then print_endline "No events found."
-
else print_endline (Format.format_events ~fs ~calendar_dir ~format results);
+
else print_endline (Format.format_events ~format results);
Ok ()
let event_id_arg =
+11 -21
lib/calendar_dir.ml
···
try
let content = Eio.Path.load file in
match parse content with
-
| Ok calendar ->
-
Event.events_of_icalendar ~file_name collection calendar
+
| Ok calendar -> Event.events_of_icalendar ~file collection calendar
| Error err ->
Printf.eprintf "Failed to parse %s: %s\n%!" file_path err;
[]
···
let add_event ~fs calendar_dir event =
let collection = Event.get_collection event in
-
let file_path =
-
Event.get_file_path ~fs ~calendar_dir_path:calendar_dir.path event
-
in
+
let file = Event.get_file event in
let collection_path = get_collection_path ~fs calendar_dir collection in
let* () = ensure_dir collection_path in
let calendar = Event.to_ical_calendar event in
let content = Icalendar.to_ics ~cr:true calendar in
let* _ =
try
-
Eio.Path.save ~create:(`Or_truncate 0o644) file_path content;
+
Eio.Path.save ~create:(`Or_truncate 0o644) file content;
Ok ()
with Eio.Exn.Io _ as exn ->
Error
(`Msg
-
(Fmt.str "Failed to write file %s: %a\n%!" (snd file_path) Eio.Exn.pp
-
exn))
+
(Fmt.str "Failed to write file %s: %a\n%!" (snd file) Eio.Exn.pp exn))
in
calendar_dir.collections <-
CollectionMap.add collection
···
let collection_path = get_collection_path ~fs calendar_dir collection in
let* () = ensure_dir collection_path in
let ical_event = Event.to_ical_event event in
-
let file_path =
-
Event.get_file_path ~fs ~calendar_dir_path:calendar_dir.path event
-
in
+
let file = Event.get_file event in
let existing_props, existing_components = Event.to_ical_calendar event in
let calendar =
(* Replace the event with our updated version *)
···
let content = Icalendar.to_ics ~cr:true calendar in
let* _ =
try
-
Eio.Path.save ~create:(`Or_truncate 0o644) file_path content;
+
Eio.Path.save ~create:(`Or_truncate 0o644) file content;
Ok ()
with Eio.Exn.Io _ as exn ->
Error
(`Msg
-
(Fmt.str "Failed to write file %s: %a\n%!" (snd file_path) Eio.Exn.pp
-
exn))
+
(Fmt.str "Failed to write file %s: %a\n%!" (snd file) Eio.Exn.pp exn))
in
calendar_dir.collections <-
CollectionMap.add collection
···
let event_id = Event.get_id event in
let collection_path = get_collection_path ~fs calendar_dir collection in
let* () = ensure_dir collection_path in
-
let file_path =
-
Event.get_file_path ~fs ~calendar_dir_path:calendar_dir.path event
-
in
+
let file = Event.get_file event in
let existing_props, existing_components = Event.to_ical_calendar event in
let other_events = ref false in
let calendar =
···
let* _ =
try
(match !other_events with
-
| true -> Eio.Path.save ~create:(`Or_truncate 0o644) file_path content
-
| false -> Eio.Path.unlink file_path);
+
| true -> Eio.Path.save ~create:(`Or_truncate 0o644) file content
+
| false -> Eio.Path.unlink file);
Ok ()
with Eio.Exn.Io _ as exn ->
Error
(`Msg
-
(Fmt.str "Failed to write file %s: %a\n%!" (snd file_path) Eio.Exn.pp
-
exn))
+
(Fmt.str "Failed to write file %s: %a\n%!" (snd file) Eio.Exn.pp exn))
in
calendar_dir.collections <-
CollectionMap.add collection
+7 -9
lib/calendar_dir.mli
···
containing .ics files *)
val create :
-
fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> (t, [> `Msg of string ]) result
+
fs:Eio.Fs.dir_ty Eio.Path.t -> string -> (t, [> `Msg of string ]) result
(** Create a calendar_dir from a directory path. Returns Ok with the
calendar_dir if successful, or Error with a message if the directory cannot
be created or accessed. *)
val list_collections :
-
fs:[> Eio.Fs.dir_ty ] Eio.Path.t ->
+
fs:Eio.Fs.dir_ty Eio.Path.t ->
t ->
(Collection.t list, [> `Msg of string ]) result
(** List available Collection.ts in the calendar_dir. Returns Ok with the list
···
directory cannot be read. *)
val get_collection :
-
fs:[> Eio.Fs.dir_ty ] Eio.Path.t ->
+
fs:Eio.Fs.dir_ty Eio.Path.t ->
t ->
Collection.t ->
(Event.t list, [> `Msg of string | `Not_found ]) result
···
the cache, it will be loaded from disk. *)
val get_events :
-
fs:[> Eio.Fs.dir_ty ] Eio.Path.t ->
-
t ->
-
(Event.t list, [> `Msg of string ]) result
+
fs:Eio.Fs.dir_ty Eio.Path.t -> t -> (Event.t list, [> `Msg of string ]) result
(** Get all events in all collections. This will load any Collection.ts that
haven't been loaded yet. *)
val add_event :
-
fs:[> Eio.Fs.dir_ty ] Eio.Path.t ->
+
fs:Eio.Fs.dir_ty Eio.Path.t ->
t ->
Event.t ->
(unit, [> `Msg of string ]) result
val edit_event :
-
fs:[> Eio.Fs.dir_ty ] Eio.Path.t ->
+
fs:Eio.Fs.dir_ty Eio.Path.t ->
t ->
Event.t ->
(unit, [> `Msg of string ]) result
val delete_event :
-
fs:[> Eio.Fs.dir_ty ] Eio.Path.t ->
+
fs:Eio.Fs.dir_ty Eio.Path.t ->
t ->
Event.t ->
(unit, [> `Msg of string ]) result
+17 -16
lib/event.ml
···
type t = {
collection : Collection.t;
-
file_name : string;
+
file : Eio.Fs.dir_ty Eio.Path.t;
event : event;
calendar : calendar;
}
···
let default_prodid = `Prodid (Params.empty, "-//Freumh//Caledonia//EN")
-
let create ~summary ~start ?end_ ?location ?description ?recurrence collection =
+
let create ~(fs : Eio.Fs.dir_ty Eio.Path.t) ~calendar_dir_path ~summary ~start
+
?end_ ?location ?description ?recurrence collection =
let uuid = generate_uuid () in
let uid = (Params.empty, uuid) in
let file_name = uuid ^ ".ics" in
+
let file =
+
Eio.Path.(
+
fs / calendar_dir_path
+
/ (match collection with Collection.Col s -> s)
+
/ file_name)
+
in
let dtstart = (Params.empty, start) in
let dtend_or_duration = end_ in
let rrule = Option.map (fun r -> (Params.empty, r)) recurrence in
···
let components = [ `Event event ] in
(props, components)
in
-
{ collection; file_name; event; calendar }
+
{ collection; file; event; calendar }
let edit ?summary ?start ?end_ ?location ?description ?recurrence t =
let now = Ptime_clock.now () in
···
}
in
let collection = t.collection in
-
let file_name = t.file_name in
+
let file = t.file in
let calendar = t.calendar in
-
{ collection; file_name; event; calendar }
+
{ collection; file; event; calendar }
-
let events_of_icalendar collection ~file_name calendar =
+
let events_of_icalendar collection ~file calendar =
List.filter_map
(function
-
| `Event event -> Some { collection; file_name; event; calendar }
-
| _ -> None)
+
| `Event event -> Some { collection; file; event; calendar } | _ -> None)
(snd calendar)
let to_ical_event t = t.event
···
let get_recurrence t = Option.map (fun r -> snd r) t.event.rrule
let get_collection t = t.collection
-
-
let get_file_path ~fs ~calendar_dir_path t =
-
Eio.Path.(
-
fs / calendar_dir_path
-
/ (match t.collection with Col s -> s)
-
/ t.file_name)
+
let get_file t = t.file
let get_recurrence_ids t =
let _, recurrence_ids =
···
let clone_with_event t event =
let collection = t.collection in
-
let file_name = t.file_name in
+
let file = t.file in
let calendar = t.calendar in
-
{ collection; file_name; event; calendar }
+
{ collection; file; event; calendar }
let expand_recurrences ~from ~to_ event =
let rule = get_recurrence event in
+4 -5
lib/event.mli
···
type date_error = [ `Msg of string ]
val create :
+
fs:Eio.Fs.dir_ty Eio.Path.t ->
+
calendar_dir_path:string ->
summary:string ->
start:Icalendar.date_or_datetime ->
?end_:
···
(** Edit an existing event. *)
val events_of_icalendar :
-
Collection.t -> file_name:string -> Icalendar.calendar -> t list
+
Collection.t -> file:Eio.Fs.dir_ty Eio.Path.t -> Icalendar.calendar -> t list
val to_ical_event : t -> Icalendar.event
val to_ical_calendar : t -> Icalendar.calendar
···
val get_description : t -> string option
val get_recurrence : t -> Icalendar.recurrence option
val get_collection : t -> Collection.t
-
-
val get_file_path :
-
fs:'a Eio.Path.t -> calendar_dir_path:string -> t -> 'a Eio.Path.t
-
+
val get_file : t -> Eio.Fs.dir_ty Eio.Path.t
val get_recurrence_ids : t -> Icalendar.event list
type comparator = t -> t -> int
+7 -22
lib/format.ml
···
recur_to_ics recur)
l
-
let format_event ~fs ~calendar_dir ?(format = `Text) ?tz event =
+
let format_event ?(format = `Text) ?tz event =
let open Event in
let start = get_start event in
let end_ = Event.get_end event in
···
|> Option.value ~default:""
in
let summary_str = format_opt "Summary" Fun.id (get_summary event) in
-
let file_str =
-
format_opt "File" Fun.id
-
(Some
-
(snd
-
(get_file_path ~fs
-
~calendar_dir_path:(Calendar_dir.get_path calendar_dir)
-
event)))
-
in
+
let file_str = format_opt "File" Fun.id (Some (snd (get_file event))) in
Printf.sprintf "%s%s%s%s%s%s%s" summary_str start_str end_str location_str
description_str rrule_str file_str
| `Json ->
···
(String.escaped id) (String.escaped summary) start_date start_time
end_str location description calendar
-
let format_events ~fs ~calendar_dir ?(format = `Text) ?tz events =
+
let format_events ?(format = `Text) ?tz events =
match format with
| `Json ->
let json_events =
List.map
-
(fun e ->
-
Yojson.Safe.from_string
-
(format_event ~fs ~calendar_dir ~format:`Json ?tz e))
+
(fun e -> Yojson.Safe.from_string (format_event ~format:`Json ?tz e))
events
in
Yojson.Safe.to_string (`List json_events)
| `Csv ->
"\"Summary\",\"Start\",\"End\",\"Location\",\"Calendar\"\n"
-
^ String.concat "\n"
-
(List.map (format_event ~fs ~calendar_dir ~format:`Csv ?tz) events)
+
^ String.concat "\n" (List.map (format_event ~format:`Csv ?tz) events)
| `Sexp ->
"("
^ String.concat "\n "
-
(List.map
-
(fun e -> format_event ~fs ~calendar_dir ~format:`Sexp ?tz e)
-
events)
+
(List.map (fun e -> format_event ~format:`Sexp ?tz e) events)
^ ")"
| _ ->
-
String.concat "\n"
-
(List.map
-
(fun e -> format_event ~fs ~calendar_dir ~format ?tz e)
-
events)
+
String.concat "\n" (List.map (fun e -> format_event ~format ?tz e) events)
+2 -12
lib/format.mli
···
(** Functions for formatting specific event types *)
val format_event :
-
fs:'a Eio.Path.t ->
-
calendar_dir:Calendar_dir.t ->
-
?format:format ->
-
?tz:Timedesc.Time_zone.t ->
-
Event.t ->
-
string
+
?format:format -> ?tz:Timedesc.Time_zone.t -> Event.t -> string
(** Format a single event, optionally using the specified timezone *)
val format_events :
-
fs:'a Eio.Path.t ->
-
calendar_dir:Calendar_dir.t ->
-
?format:format ->
-
?tz:Timedesc.Time_zone.t ->
-
Event.t list ->
-
string
+
?format:format -> ?tz:Timedesc.Time_zone.t -> Event.t list -> string
(** Format a list of events, optionally using the specified timezone *)
+2 -2
lib/query.mli
···
val not_filter : filter -> filter
val query_without_recurrence :
-
fs:[> Eio.Fs.dir_ty ] Eio.Path.t ->
+
fs:Eio.Fs.dir_ty Eio.Path.t ->
Calendar_dir.t ->
?filter:filter ->
?comparator:Event.comparator ->
···
of events, or Error with a message. *)
val query :
-
fs:[> Eio.Fs.dir_ty ] Eio.Path.t ->
+
fs:Eio.Fs.dir_ty Eio.Path.t ->
Calendar_dir.t ->
?filter:filter ->
from:Ptime.t option ->
+30 -20
test/test_query.ml
···
| Error _ -> Alcotest.fail "Error querying events");
()
-
let test_events =
+
let test_events ~fs =
(* Create a test event with specific text in all fields *)
let create_test_event ~collection ~summary ~description ~location ~start =
-
Event.create ~summary ~start
+
Event.create ~fs ~calendar_dir_path ~summary ~start
?description:(if description = "" then None else Some description)
?location:(if location = "" then None else Some location)
(Collection.Col collection)
···
(fun e -> String.equal (Option.get @@ Event.get_summary e) summary)
events
-
let test_case_insensitive_search () =
+
let test_case_insensitive_search ~fs () =
(* Test lowercase query for an uppercase word *)
let lowercase_filter = Query.summary_contains "important" in
let matches =
-
List.filter (fun e -> Query.matches_filter e lowercase_filter) test_events
+
List.filter
+
(fun e -> Query.matches_filter e lowercase_filter)
+
(test_events ~fs)
in
Alcotest.(check bool)
"Lowercase query should match uppercase text in summary" true
···
(* Test uppercase query for a lowercase word *)
let uppercase_filter = Query.description_contains "WEEKLY" in
let matches =
-
List.filter (fun e -> Query.matches_filter e uppercase_filter) test_events
+
List.filter
+
(fun e -> Query.matches_filter e uppercase_filter)
+
(test_events ~fs)
in
Alcotest.(check bool)
"Uppercase query should match lowercase text in description" true
(contains_summary matches "Project Meeting")
-
let test_partial_word_matching () =
+
let test_partial_word_matching ~fs () =
(* Test searching for part of a word *)
let partial_filter = Query.summary_contains "Conf" in
(* Should match "Conference" *)
let matches =
-
List.filter (fun e -> Query.matches_filter e partial_filter) test_events
+
List.filter
+
(fun e -> Query.matches_filter e partial_filter)
+
(test_events ~fs)
in
Alcotest.(check bool)
"Partial query should match full word in summary" true
···
let partial_filter = Query.description_contains "nation" in
(* Should match "International" *)
let matches =
-
List.filter (fun e -> Query.matches_filter e partial_filter) test_events
+
List.filter
+
(fun e -> Query.matches_filter e partial_filter)
+
(test_events ~fs)
in
Alcotest.(check bool)
"Partial query should match within word in description" true
···
"Partial query should match within word in description" true
(contains_summary matches "Conference Call")
-
let test_boolean_logic () =
+
let test_boolean_logic ~fs () =
(* Test AND filter *)
let and_filter =
Query.and_filter
[ Query.summary_contains "Meeting"; Query.description_contains "project" ]
in
let matches =
-
List.filter (fun e -> Query.matches_filter e and_filter) test_events
+
List.filter (fun e -> Query.matches_filter e and_filter) (test_events ~fs)
in
Alcotest.(check int)
"AND filter should match events with both terms" 2
···
[ Query.summary_contains "Workshop"; Query.summary_contains "Conference" ]
in
let matches =
-
List.filter (fun e -> Query.matches_filter e or_filter) test_events
+
List.filter (fun e -> Query.matches_filter e or_filter) (test_events ~fs)
in
Alcotest.(check int)
"OR filter should match events with either term"
···
(* Test NOT filter *)
let not_filter = Query.not_filter (Query.summary_contains "Meeting") in
let matches =
-
List.filter (fun e -> Query.matches_filter e not_filter) test_events
+
List.filter (fun e -> Query.matches_filter e not_filter) (test_events ~fs)
in
Alcotest.(check int)
"NOT filter should match events without the term"
···
]
in
let matches =
-
List.filter (fun e -> Query.matches_filter e complex_filter) test_events
+
List.filter
+
(fun e -> Query.matches_filter e complex_filter)
+
(test_events ~fs)
in
Alcotest.(check int)
"Complex filter should match correctly"
3 (* Three events should match the complex criteria *)
(List.length matches)
-
let test_cross_field_search () =
+
let test_cross_field_search ~fs () =
(* Search for a term that appears in multiple fields across different events *)
let term_filter =
Query.or_filter
···
]
in
let matches =
-
List.filter (fun e -> Query.matches_filter e term_filter) test_events
+
List.filter (fun e -> Query.matches_filter e term_filter) (test_events ~fs)
in
Alcotest.(check int)
"Cross-field search should find all occurrences"
···
]
in
let matches =
-
List.filter (fun e -> Query.matches_filter e term_filter) test_events
+
List.filter (fun e -> Query.matches_filter e term_filter) (test_events ~fs)
in
Alcotest.(check int)
"Cross-field search should find all occurrences of 'conference'"
···
("recurrence expansion", `Quick, test_recurrence_expansion ~fs);
("text search", `Quick, test_text_search ~fs);
("calendar filter", `Quick, test_calendar_filter ~fs);
-
("case insensitive search", `Quick, test_case_insensitive_search);
-
("partial word matching", `Quick, test_partial_word_matching);
-
("boolean logic filters", `Quick, test_boolean_logic);
-
("cross-field searching", `Quick, test_cross_field_search);
+
("case insensitive search", `Quick, test_case_insensitive_search ~fs);
+
("partial word matching", `Quick, test_partial_word_matching ~fs);
+
("boolean logic filters", `Quick, test_boolean_logic ~fs);
+
("cross-field searching", `Quick, test_cross_field_search ~fs);
]
let () =