CLI to manage calendars.
Important
This README documents Calendula v0.2.0. If you are running v0.1.0, refer to the v0.1.0 README. The MIGRATION.md guide walks v0.1 users through the breaking changes.
- Features
- RFC coverage
- Installation
- Configuration
- Usage
- AI policy
- License
- Social
- Contributing
- Sponsoring
- Shared API:
calendar,event,todo,journalanditemwork the same whichever backend serves the account. - One family per component kind:
event(VEVENT),todo(VTODO) andjournal(VJOURNAL) each render the columns their kind is read by;itemkeeps the raw, unfiltered view. - Protocol-specific APIs:
caldav,gcal,pimdirandvdireach expose what only that backend has. - CalDAV: talk to any standard calendar server, with basic or bearer authentication.
- Google Calendar: the native API v3, where Google's CalDAV bridge is crippled, with the iCalendar document synthesized both ways.
gcaladds sharing, free/busy, recurrence expansion and quick add. - vdir: read and write a local vdir home, one directory per calendar.
- pimdir: read and stage writes against a local pimdir store, the offline cache a sync engine fills.
- Agenda view:
event agendadraws a cal(1)-style grid marking the days that carry an event. - Composer:
-i/--interactiveonbuild,createandupdateopens the item in the commanditem.composernames, your editor included, and what it leaves in the file is the decision. - Build without an account:
<family> buildapplies a source and the composer and prints the iCalendar rather than sending it, on a machine holding no configuration at all. - Discovery: an email address is enough to find a provider's server, through SRV records,
.well-knownand the provider configuration documents. - Interactive wizard:
calendula configurediscovers an account, tests it, and saves it; a first run with no configuration is offered it. - Multi-account: one TOML file, one block per account, several files deep-merged when you want secrets apart.
- JSON output: every command switches to JSON with
--json, for scripts and other tools, andcalendula json-schemadescribes the shape each one returns. - Full standard, blocking client with TLS support:
- Rustls with ring crypto (requires
rustls-ringfeature, enabled by default) - Rustls with aws crypto (requires
rustls-awsfeature) - Native TLS (requires
native-tlsfeature)
- Rustls with ring crypto (requires
Tip
Each backend sits behind its own cargo feature (caldav, gcal, vdir, pimdir), all enabled by default. Build with --no-default-features and pick the ones you need.
| RFC | What is covered |
|---|---|
| 4791 | CalDAV: calendar collections, calendar object resources, and the calendar-query REPORT with its time-range filter |
| 4918 | WebDAV: the PROPFIND, PROPPATCH, MKCOL, GET, PUT and DELETE methods CalDAV builds on |
| 5397 | Current-user-principal, the first step of the CalDAV discovery walk |
| 5545 | iCalendar: parsing and editing the event, to-do and journal components a calendar holds |
| 6764 | CalDAV service discovery: the _caldav and _caldavs SRV records, and .well-known/caldav |
| 6578 | Collection synchronization, whose sync token a CalDAV calendar listing reports |
| 7617 | HTTP Basic authentication |
| 6750 | HTTP Bearer authentication, for a provider-issued or broker-refreshed API token |
As root:
curl -sSL https://raw.githubusercontent.com/pimalaya/calendula/master/install.sh | sudo shAs a regular user:
curl -sSL https://raw.githubusercontent.com/pimalaya/calendula/master/install.sh | PREFIX=~/.local shThese commands install the latest binary from the GitHub releases section.
For a more up-to-date version than the latest release, check out the releases GitHub workflow and look for the Artifacts section. These pre-built binaries are built from the master branch.
Note
Such binaries are built with the default cargo features. If you need specific features, please use another installation method.
cargo install --locked --git https://github.com/pimalaya/calendula.gitWith only the local backends, and no network code at all:
cargo install --locked --git https://github.com/pimalaya/calendula.git \
--no-default-features \
--features vdir,pimdir,rustls-ringIf you have the Flakes feature enabled:
nix profile install github:pimalaya/calendulaOr run without installing:
nix run github:pimalaya/calendulagit clone https://github.com/pimalaya/calendula
cd calendula
nix runThe configuration is loaded from the first existing path among:
$XDG_CONFIG_HOME/calendula/config.toml$HOME/.config/calendula/config.toml$HOME/.calendularc
Override the path with calendula -c <PATH> or CALENDULA_CONFIG=<PATH>; multiple paths can be passed at once, separated by :. The first is the base and the rest are deep-merged on top. The full field reference lives in config.sample.toml.
Run calendula configure to launch the wizard. It asks one question, taking an email address, a server URL or a local folder path, and the shape of what you type decides the rest.
An address is discovered: every reachable server is offered, and picking one prompts only its credentials. A URL is taken as the CalDAV context root, which is how a self-hosted server publishing no SRV record gets configured. A folder is detected as a vdir home or a pimdir store.
The wizard tests the account before showing you anything, then offers to save it: to a new configuration file, or appended to the one you already have, leaving its comments and formatting untouched.
A bare calendula finding no configuration offers the wizard, as does any command needing an account; once you have one, bare calendula prints the help.
Redirect it instead to keep the document yourself:
calendula configure > ~/.config/calendula/config.tomlApple exposes calendars via CalDAV, but you cannot use your regular password. You need to generate an app-specific password (required once two-factor authentication is on):
[accounts.example]
caldav.discover = "icloud.com"
caldav.server = "https://caldav.icloud.com/"
# The home URL is usually of this shape:
#caldav.home = "https://caldav.icloud.com/<id>/calendars/"
caldav.auth.basic.username = "example@icloud.com"
caldav.auth.basic.password.raw = "***"
calendar.default = "home"Use the gcal backend, which speaks the Calendar API v3 directly. Both routes need OAuth 2.0, and an access token expires within the hour, so point the token at a broker such as Ortie rather than pasting one in.
[accounts.example]
gcal.auth.token.command = ["ortie", "token", "show"]
# Your email address for the primary calendar, or a
# "...@group.calendar.google.com" value for a secondary one. Run
# `calendula calendar list` to read them.
calendar.default = "example@gmail.com"Google stores events as JSON and exposes no iCalendar representation of one, so calendula synthesizes the document you read and re-projects what you write.
Properties with a well-defined iCalendar slot are authoritative in both directions, and provider-scoped ones surface as read-only X-GOOGLE-* properties.
Google-only fields (the event colour, the guest permissions, working-location and out-of-office blocks) survive an update untouched, and everything else is stashed verbatim on the server so it round-trips instead of being dropped on the next write.
Only events project: a VTODO or VJOURNAL is refused by name, since Google models neither.
Google is also reachable over CalDAV, but only in a crippled form: bearer tokens are the sole authentication, MKCALENDAR is refused outright, and the discovery entry point is off-spec, so each calendar has to be addressed by hand and none can be created.
If you want it anyway, set caldav.home to the base URL and make the calendar id the <CALENDAR-ID>/events segment:
[accounts.example]
caldav.home = "https://apidata.googleusercontent.com/caldav/v2"
caldav.auth.bearer.token.command = ["ortie", "token", "show"]
# Primary calendar: "<your-email>/events".
calendar.default = "example@gmail.com/events"Not supported yet: Microsoft offers no CalDAV for calendars, only the Graph API. Native Graph support is planned.
Not supported: Proton exposes no calendar API, neither CalDAV nor through Proton Bridge (which proxies mail only). Calendars are reachable only from Proton's own web and mobile apps.
Standard CalDAV with the mailbox address and its app password. If caldav.discover / caldav.server return a 404, point caldav.home straight at the calendar home-set to skip the discovery walk:
[accounts.example]
caldav.home = "https://caldav.fastmail.com/dav/calendars/user/example@fastmail.com/"
caldav.auth.basic.username = "example@fastmail.com"
caldav.auth.basic.password.raw = "***"Run calendula calendar list once connected to read the calendar ids (the ID column), then set calendar.default to the one you want.
Standard CalDAV with the mailbox address and its password.
[accounts.posteo]
caldav.discover = "posteo.de"
caldav.server = "https://posteo.de:8443/"
# The home URL is usually of this shape:
#caldav.home = "https://posteo.de:8443/calendars/<username>/"
caldav.auth.basic.username = "example@posteo.net"
caldav.auth.basic.password.raw = "***"
calendar.default = "default"No server is involved, so nothing needs discovering. Point calendula at a directory and it works offline.
A vdir home is one directory per calendar, holding one .ics file per item. This is what vdirsyncer writes, and what most local tools read:
[accounts.local]
vdir.home-dir = "~/.local/share/vdirsyncer/calendars"
calendar.default = "personal"A pimdir store is the offline cache a sync engine fills: a SQLite index plus content-addressed bodies, shared with the other Pimalaya clients reading the same store.
It is a cache, not a server, so calendars come from the sync and the collection verbs refuse here. Writes are staged for the next sync to push:
[accounts.cached]
pimdir.root = "~/.local/state/neverest/example"
# Usually left unset: a store synced by one account is read as that one.
#pimdir.account = "personal"Run calendula pimdir status to see which account you are reading, how much of each calendar is downloaded, and how many creations are still queued. An item that is listed but not downloaded reads as "body not fetched" until a sync hydrates it.
Run calendula --help for the full command tree, and calendula <command> --help for any subcommand's arguments and its JSON output shape (printed when the global --json flag is set). calendula json-schema --dir ./schemas writes the JSON Schema of every --json payload.
A few real command lines:
calendula calendar list
calendula event list --calendar personal --from 2026-08-01 --to 2026-08-31
calendula event agenda -3
calendula todo list --calendar tasks
calendula journal list --calendar notes
calendula item read --calendar personal event-1.ics
calendula event build -i -o event.ics
calendula event create --calendar personal -i
calendula todo update --calendar tasks -i todo-1.ics
calendula pimdir status
calendula gcal free-busy --from 2026-08-10 --to 2026-08-14
calendula gcal quick-add "Lunch with Ada tomorrow at noon"This project is licensed under either of:
- Chat on Matrix
- News on Mastodon or RSS
- Mail at pimalaya.org@posteo.net
Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:
- 2022 → 2023: NGI Assure
- 2023 → 2024: NGI Zero Entrust
- 2024 → 2026: NGI Zero Core
- 2026 → 2027: NGI Zero Commons Fund
This program is part of Pimalaya, free software funded entirely by grants and donations. If you find it useful, consider sponsoring its development:
