Something went wrong. Try again.
Command-line and Emacs Calendar Client
Something went wrong. Try again.
OCaml
at timezone-cli
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322open Icalendar
type t = { calendar_name : string; file : Eio.Fs.dir_ty Eio.Path.t; props : journal_prop list; calendar : calendar;}
let get_id t = List.find_map (function `Uid (_, id) -> Some id | _ -> None) t.props |> Option.value ~default:""
let uuid_gen = lazy (Uuidm.v4_gen (Random.State.make_self_init ()))let generate_uuid () = Uuidm.to_string (Lazy.force uuid_gen ())
let default_prodid = `Prodid (Params.empty, "-//Freumh//Caledonia//EN")
let create ~fs ~calendar_dir_path ?summary ?start ?description ?categories ?status calendar_name = 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 / calendar_name / file_name) in let now = Ptime_clock.now () in let props = [ `Dtstamp (Params.empty, now); `Uid uid ] in let props = match summary with | Some s -> `Summary (Params.empty, s) :: props | None -> props in let props = match start with Some s -> `Dtstart s :: props | None -> props in let props = match description with | Some d -> `Description (Params.empty, d) :: props | None -> props in let props = match categories with | Some cats -> `Categories (Params.empty, cats) :: props | None -> props in let props = match status with Some s -> `Status (Params.empty, s) :: props | None -> props in let calendar = ([ default_prodid ], [ `Journal props ]) in Ok { calendar_name; file; props; calendar }
let edit ?summary ?start ?description ?categories ?status t = let now = Ptime_clock.now () in let props = List.filter_map (function | `Uid _ as uid -> Some uid | `Dtstamp _ -> Some (`Dtstamp (Params.empty, now)) | `Summary _ as prop -> ( match summary with | Some s -> Some (`Summary (Params.empty, s)) | None -> Some prop) | `Dtstart _ as prop -> ( match start with Some s -> Some (`Dtstart s) | None -> Some prop) | `Description _ as prop -> ( match description with | Some d -> Some (`Description (Params.empty, d)) | None -> Some prop) | `Categories _ as prop -> ( match categories with | Some cats -> Some (`Categories (Params.empty, cats)) | None -> Some prop) | `Status _ as prop -> ( match status with | Some s -> Some (`Status (Params.empty, s)) | None -> Some prop) | prop -> Some prop) t.props in let props = let add_if_missing pred make_prop value props = if value <> None && not (List.exists pred props) then make_prop (Option.get value) :: props else props in props |> add_if_missing (function `Summary _ -> true | _ -> false) (fun s -> `Summary (Params.empty, s)) summary |> add_if_missing (function `Dtstart _ -> true | _ -> false) (fun s -> `Dtstart s) start |> add_if_missing (function `Description _ -> true | _ -> false) (fun d -> `Description (Params.empty, d)) description |> add_if_missing (function `Categories _ -> true | _ -> false) (fun cats -> `Categories (Params.empty, cats)) categories |> add_if_missing (function `Status _ -> true | _ -> false) (fun s -> `Status (Params.empty, s)) status in let calendar = (fst t.calendar, [ `Journal props ]) in Ok { t with props; calendar }
let journals_of_icalendar calendar_name ~file calendar = let journals = List.filter_map (function `Journal props -> Some props | _ -> None) (snd calendar) in List.map (fun props -> { calendar_name; file; props; calendar = (fst calendar, [ `Journal props ]) }) journals
let to_ical_journal t = t.propslet to_ical_calendar t = t.calendar
let get_summary t = List.find_map (function `Summary (_, s) -> Some s | _ -> None) t.props
let get_start t = List.find_map (function | `Dtstart (_, `Date date) -> let (y, m, d) = date in Ptime.of_date_time ((y, m, d), ((0, 0, 0), 0)) | `Dtstart (_, `Datetime ts) -> ( match ts with | `Utc t | `Local t | `With_tzid (t, _) -> Some t) | _ -> None) t.props
let get_description t = List.find_map (function `Description (_, d) -> Some d | _ -> None) t.props
let get_categories t = List.find_map (function `Categories (_, cats) -> Some cats | _ -> None) t.props |> Option.value ~default:[]
let get_status t = List.find_map (function `Status (_, s) -> Some s | _ -> None) t.props
let get_calendar_name t = t.calendar_namelet get_file t = t.file
let sexp_of_t t = let open Sexplib.Sexp in let entries = [ Some (List [ Atom "id"; Atom (get_id t) ]); (match get_summary t with | Some s -> Some (List [ Atom "summary"; Atom s ]) | None -> None); (match get_start t with | Some s -> Some (List [ Atom "start"; Atom (Ptime.to_rfc3339 s) ]) | None -> None); (match get_description t with | Some d -> Some (List [ Atom "description"; Atom d ]) | None -> None); (match get_categories t with | [] -> None | cats -> Some (List [ Atom "categories"; List (List.map (fun c -> Atom c) cats) ])); Some (List [ Atom "file"; Atom (snd (get_file t)) ]); Some (List [ Atom "calendar"; Atom (get_calendar_name t) ]); ] in List (List.filter_map Fun.id entries)
type format = [ `Text | `Entries | `Json | `Csv | `Ics | `Sexp ]
let format_prop_value = function | `Related (params, s) -> let reltype = match Icalendar.Params.find Reltype params with | Some `Parent -> "PARENT" | Some `Child -> "CHILD" | Some `Sibling -> "SIBLING" | Some (`Ianatoken t) -> t | Some (`Xname (ns, name)) -> ns ^ ":" ^ name | None -> "PARENT" in Some ("Related-To", s ^ " (" ^ reltype ^ ")") | `Seq (_, n) -> Some ("Sequence", string_of_int n) | `Created (_, t) -> Some ("Created", Ptime.to_rfc3339 t) | `Lastmod (_, t) -> Some ("Last-Modified", Ptime.to_rfc3339 t) | `Iana_prop ("RELATED", params, value) -> let reltype = match Icalendar.Params.find Reltype params with | Some `Parent -> "PARENT" | Some `Child -> "CHILD" | Some `Sibling -> "SIBLING" | Some (`Ianatoken t) -> t | Some (`Xname (ns, name)) -> ns ^ ":" ^ name | None -> "PARENT" in Some ("Related-To", value ^ " (" ^ reltype ^ ")") | `Iana_prop (name, _, value) -> Some (name, value) | `Xprop ((ns, name), _, value) -> Some (ns ^ ":" ^ name, value) | _ -> None
let text_journal_data ?tz journal = let id = get_id journal in let calendar_name = get_calendar_name journal in let summary = match get_summary journal with | Some s when s <> "" -> s | _ -> (match get_description journal with | Some desc -> (match String.split_on_char '\n' desc with | first :: _ when first <> "" -> first | _ -> "") | None -> "") in let date_str = match get_start journal with | Some start -> Format_utils.format_date ?tz start | None -> "" in let categories = get_categories journal in let cats_str = if categories = [] then "" else String.concat "," categories in (calendar_name, date_str, summary, cats_str, id)
let format_journal ?(format = `Text) ?tz journal = match format with | `Text -> let calendar_name, date, summary, cats, id = text_journal_data ?tz journal in Printf.sprintf "%s\t%s\t%s\t%s\t%s" calendar_name date summary cats id | `Entries -> let summary_str = Format_utils.format_opt "Summary" Fun.id (get_summary journal) in let start_str = Format_utils.format_opt "Date" (Format_utils.format_date ?tz) (get_start journal) in let cats = get_categories journal in let cats_str = if cats = [] then "" else Printf.sprintf "Categories: %s\n" (String.concat ", " cats) in let description_str = Format_utils.format_opt "Description" Fun.id (get_description journal) in let status_str = Format_utils.format_opt "Status" (function | `Draft -> "Draft" | `Final -> "Final" | `Cancelled -> "Cancelled" | _ -> "Unknown") (get_status journal) in let other_props_str = List.filter_map format_prop_value journal.props |> List.map (fun (name, value) -> Printf.sprintf "%s: %s\n" name value) |> String.concat "" in let file_str = Format_utils.format_opt "File" Fun.id (Some (snd (get_file journal))) in Printf.sprintf "%s%s%s%s%s%s%s" summary_str start_str cats_str description_str status_str other_props_str file_str | `Json -> let open Yojson.Safe in let json = `Assoc [ ("id", `String (get_id journal)); ("summary", match get_summary journal with Some s -> `String s | None -> `Null); ("start", match get_start journal with Some s -> `String (Ptime.to_rfc3339 s) | None -> `Null); ("categories", `List (List.map (fun c -> `String c) (get_categories journal))); ("description", match get_description journal with Some d -> `String d | None -> `Null); ("status", match get_status journal with | Some `Draft -> `String "draft" | Some `Final -> `String "final" | Some `Cancelled -> `String "cancelled" | _ -> `Null); ("calendar", `String (get_calendar_name journal)); ] in to_string json | `Csv -> let summary = Option.value ~default:"" (get_summary journal) in let date = match get_start journal with Some d -> Format_utils.format_date ?tz d | None -> "" in let cats = String.concat "," (get_categories journal) in let cal_id = get_calendar_name journal in Printf.sprintf "\"%s\",\"%s\",\"%s\",\"%s\"" summary date cats cal_id | `Ics -> let calendar = to_ical_calendar journal in Icalendar.to_ics ~cr:true calendar | `Sexp -> Sexplib.Sexp.to_string (sexp_of_t journal)
let format_journals_with_dynamic_columns ?tz ?get_color journals = if journals = [] then "" else let journal_data = List.map (text_journal_data ?tz) journals in let max_cal_width = Format_utils.max_width (fun (cal, _, _, _, _) -> cal) journal_data in let max_date_width = Format_utils.max_width (fun (_, date, _, _, _) -> date) journal_data in let max_summary_width = Format_utils.max_width (fun (_, _, summary, _, _) -> summary) journal_data in let max_cats_width = Format_utils.max_width (fun (_, _, _, cats, _) -> cats) journal_data in let max_id_width = Format_utils.max_width (fun (_, _, _, _, id) -> id) journal_data in List.map (fun (cal, date, summary, cats, id) -> let color = match get_color with Some f -> f cal | None -> None in Printf.sprintf "%s %s %s %s %s" (Format_utils.pad_to_width ?color max_cal_width cal) (Format_utils.pad_to_width max_date_width date) (Format_utils.pad_to_width max_summary_width summary) (Format_utils.pad_to_width max_cats_width cats) (Format_utils.pad_to_width max_id_width id)) journal_data |> String.concat "\n"
let format_journals ?(format = `Text) ?tz ?get_color journals = match format with | `Text -> format_journals_with_dynamic_columns ?tz ?get_color journals | `Json -> let json_journals = List.map (fun j -> Yojson.Safe.from_string (format_journal ~format:`Json ?tz j)) journals in Yojson.Safe.to_string (`List json_journals) | `Sexp -> "(" ^ String.concat "\n " (List.map (fun j -> format_journal ~format:`Sexp ?tz j) journals) ^ ")" | _ -> String.concat "\n" (List.map (fun j -> format_journal ~format ?tz j) journals)