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 disclosure
- 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. - Discovery: an email address is enough to find a provider's server, through SRV records,
.well-knownand the provider configuration documents. - Interactive wizard: bare
calenduladiscovers an account, tests it, and prints a ready-to-save configuration. - 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. - 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, which is how a public configuration and a private one stay separate files. The full field reference lives in config.sample.toml.
Run calendula with no command 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 prints a ready-to-save configuration. Redirect it to keep it, or let the wizard save it for you:
calendula > ~/.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; Google-only fields (the event colour, the guest permissions, working-location and out-of-office blocks) survive an update untouched; provider-scoped ones surface as read-only X-GOOGLE-* properties; 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 as a single source is opened as it.
#pimdir.source = "caldav"Run calendula pimdir status to see which source your writes are attributed to and how much of each calendar is downloaded. 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).
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 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 developed with AI assistance. This section documents how, so users and downstream packagers can make informed decisions.
- Tools: Claude Code (Anthropic), invoked locally with a persistent project-scoped memory and a small set of repo-specific rules.
- Used for: Refactors, mechanical multi-file edits, boilerplate (feature gates, error enums, derive macros, trait impls), test scaffolding, doc polish, exploratory design conversations.
- Not used for: Engineering, critical code, git manipulation (commit, merge, rebase…), real-world tests.
- Verification: Every AI-assisted change is read, compiled, tested, and formatted before commit (
nix develop --command cargo check / cargo test / cargo fmt). Behavioural correctness is verified against the relevant RFC or upstream spec, not assumed from the model output. Tests are never adjusted to fit AI-generated code; the code is adjusted to fit correct behaviour. - Limitations: AI models occasionally produce code that compiles and passes tests but is subtly wrong: off-by-one errors, missed edge cases, plausible but nonexistent APIs, stale RFC references. The verification workflow catches most of this; it does not catch all of it. Bug reports are welcome and taken seriously.
- Last reviewed: 08/08/2026
This project is licensed under either of:
at your option.
- Chat on Matrix
- News on Mastodon or RSS
- Mail at pimalaya.org@posteo.net
Contributions are welcome: start with CONTRIBUTING.md, which opens with the Pimalaya-wide guides to read first.
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
- 2027 in preparation…
If you appreciate the project, feel free to donate using one of the following providers:
