Nov 10th 2024
Background
Why
For a long time I’ve used python CLI tools (todoman and khal) to interact with my CalDAV calendar and todo lists in Linux. I’m pretty happy with them, but I could see a lot of room for improvements, especially with todoman.
These tools actually don’t interact with CalDAV directory, but instead
work directly on iCalendar (.ics
) file and require vdirsyncer
(now replaced by pimsync) to sync a CalDAV
server to local iCalendar files.
Now, I’m not affraid to say that I’m quite a Python hater for big projects.
So I didn’t want to go out and rewrite todoman
in Python again.
I decided to go with my favorite choice here which was Rust.
Rust iCalendar Libraries
There are two rust libraries that exist:
- ical-rs: cool library but archived in 2024. Seems to be a great library for making iCalendar files, but not for reading or editing ones.
- icalendar: cool library, well maintained but again seems to not focus on editing
Now realistically, I could have been very successful with either library.
The Real Reason Why
I was super interested in this old format, it was cool to be that it had a super defined spec (RFC 5545). I was also taking a programming langauges class, I needed a project for the masters extension that handled parsing.
Results
After probably rewriting this library 3 times, I can say I’m almost happy
with the result. I taught me a LOT about using Rust macros, and also
their disadvantages. I think having a system like macros in Zig would have
been best here. In my opinion, the ideal implementation of this library relies
on code generation in a build.rs
(something I didn’t realize was possible
at the time).
Features
- Full implemenation of every ICalendar type
- Generated methods for every ICalendar property with all allowed types
- Support for X & IANA properties and parameters
Usage
Modify Existing
I built the library around this idea, I would I say I definetly achieved it. Here you can see that in 4 lines of code, I can parse an iCalendar string, edit it and then save it back to a string.
let ics_str = "BEGIN:VCALENDAR...";
let mut vcal = ICalComponent::from_ics(ics_str)?;
let vtodo = vcal.expect_vtodo();
vtodo.summary("New Summary".to_string());
let new_ics_str = vcal.to_ics();
Make New
let dtstamp = Tz::America__New_York.with_ymd_and_hms(1992, 12, 17, 12, 34, 56)?;
let vcal = ICalComponent::vcalendar_with_vtodo(
ICalComponent::empty()
.uid_random()
.dtstamp(dtstamp.into())
.percent_complete(10)
.build()
);
let ics_str = vcal.to_ics();
X & IANA Properties
Convert Value:
let in_ics = r#"BEGIN:VCALENDAR
X-EXAMPLE:19921217T123456
END:VCALENDAR"#;
let mut vcal = ICalComponent::from_ics(&in_ics)?;
let x_example = vcal.get_prop("X-EXAMPLE")?
.convert_value::<ICalDateTime>()?;
Read Later:
let value = vcal.get_prop("X-EXAMPLE")?
.get_as::<ICalDateTime>()?);
println!("{}", value); // 1992-12-17 12:34:56