Skip to content

Repository files navigation

📅 Calendula crates.io Matrix Mastodon

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.

Table of contents

Features

  • Shared API: calendar, event, todo, journal and item work the same whichever backend serves the account.
  • One family per component kind: event (VEVENT), todo (VTODO) and journal (VJOURNAL) each render the columns their kind is read by; item keeps the raw, unfiltered view.
  • Protocol-specific APIs: caldav, gcal, pimdir and vdir each 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. gcal adds 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 agenda draws 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-known and the provider configuration documents.
  • Interactive wizard: bare calendula discovers 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-ring feature, enabled by default)
    • Rustls with aws crypto (requires rustls-aws feature)
    • Native TLS (requires native-tls feature)

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 coverage

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

Installation

Pre-built binary

As root:

curl -sSL https://raw.githubusercontent.com/pimalaya/calendula/master/install.sh | sudo sh

As a regular user:

curl -sSL https://raw.githubusercontent.com/pimalaya/calendula/master/install.sh | PREFIX=~/.local sh

These 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

cargo install --locked --git https://github.com/pimalaya/calendula.git

With 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-ring

Nix

If you have the Flakes feature enabled:

nix profile install github:pimalaya/calendula

Or run without installing:

nix run github:pimalaya/calendula

Sources

git clone https://github.com/pimalaya/calendula
cd calendula
nix run

Configuration

The 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.toml

Apple

Apple 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"

Google

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"

Microsoft

Not supported yet: Microsoft offers no CalDAV for calendars, only the Graph API. Native Graph support is planned.

Proton

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.

Fastmail

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.

Posteo

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"

Local calendars

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.

Usage

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"

AI disclosure

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

License

This project is licensed under either of:

at your option.

Social

Contributing

Contributions are welcome: start with CONTRIBUTING.md, which opens with the Pimalaya-wide guides to read first.

Sponsoring

nlnet

Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:

If you appreciate the project, feel free to donate using one of the following providers:

GitHub Ko-fi Buy Me a Coffee Liberapay thanks.dev PayPal

About

CLI to manage calendars

Topics

Resources

Contributing

Security policy

Stars

40 stars

Watchers

2 watching

Forks

Releases

Contributors

Languages