2026-09-10 14:16:45 +03:00
# calcalist
Aggregate several calendars into one, and keep them in step.
calcalist mirrors any number of CalDAV calendars, Google calendars and iCal
feeds into a single calendar you subscribe to. The point is to subscribe to one
calendar instead of several: an event created or edited in the aggregate travels
back to the calendar it came from, so the aggregate is somewhere to work rather
than a read-only noticeboard. CalDAV and Google are read/write in both
directions; iCal feeds are read-only, because the protocol is.
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
Pre-1.0. The command line and the configuration format are settled.
2026-09-10 14:16:45 +03:00
## Install
```sh
cargo build --release
# target/release/calcalist
```
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
One runtime dependency: ** `pimsync` 0.5.x on `PATH` **, which carries the CalDAV
and iCal legs. It is needed only if some endpoint is one of those — a
Google-only setup needs nothing else, because calcalist talks to Google itself.
pimsync is in nixpkgs, and this repo's `devbox.json` pins 0.5.11.
2026-09-10 14:16:45 +03:00
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
Then check the environment, which is worth doing before writing any config:
2026-09-10 14:16:45 +03:00
```sh
calcalist doctor
```
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
## Configure
2026-09-10 14:16:45 +03:00
One TOML file at `$XDG_CONFIG_HOME/calcalist/calcalist.toml` , or wherever
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
`--config` points. It holds **no secrets and no sync state** , so it is safe to
copy between machines. Credentials come from `*_command` fields: each is run
through `sh -c` and the first line of its output is used. Point them at
`secret-tool` , `pass` , `gpg -d` , or anything else that prints the secret.
2026-09-10 14:16:45 +03:00
```toml
version = 1
```
### Endpoints
Each `[[endpoint]]` is one calendar, with an `id` you refer to it by elsewhere.
**CalDAV**
```toml
[[endpoint]]
id = "posteo"
type = "caldav"
url = "https://posteo.de:8443/calendars/you/abc123/work/"
username = "you@posteo .de"
secret_command = "secret-tool lookup service posteo user you@posteo .de"
```
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
`url` must name the calendar collection itself, not the server root. Rather than
digging it out of a provider's web interface, ask the server — this prints a
ready-to-paste block per calendar, with ids taken from the URL for you to
rename:
```sh
calcalist discover https://posteo.de:8443/calendars/you/ \
--username you@posteo .de --secret-command "secret-tool lookup service posteo"
```
2026-09-10 14:16:45 +03:00
**Google**
```toml
[[endpoint]]
id = "gcal"
type = "google"
calendar_id = "you@gmail .com"
client_id = "1234-abcd.apps.googleusercontent.com"
client_secret_command = "secret-tool lookup service google-calcalist"
# account = "you@gmail.com" # only when logged in to more than one account
```
`calendar_id` is `primary` , or the calendar's own address — the "Calendar ID"
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
under its settings in Google Calendar. See [Authorising
Google](#authorising -google) for `client_id` and the secret.
2026-09-10 14:16:45 +03:00
**iCal feed**
```toml
[[endpoint]]
id = "holidays"
type = "webcal"
url = "https://example.org/holidays.ics"
```
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
Always read-only. Give exactly one of `url` or `url_command` ; the latter is for
a feed whose address is itself a credential, such as Google's "secret address in
iCal format", which grants read access to anyone holding it.
2026-09-10 14:16:45 +03:00
### Aggregates
Each `[[aggregate]]` composes endpoints: several `sources` mirrored into one
`target` .
```toml
[[aggregate]]
id = "unified"
target = "posteo"
sources = ["gcal", "holidays"]
default_sink = "gcal"
# Defaults, all optional:
conflict = "source_wins" # the only policy; the origin calendar wins
propagate_deletes = true # deleting a mirror deletes the original
max_delete_fraction = 0.2 # abort if more of the aggregate than this vanishes
```
The rules, all checked before anything is synchronised, with every problem
reported in one pass:
- `target` must exist and be writable, so never an iCal feed.
- `target` must not also be one of its own `sources` .
- `sources` must be non-empty, and each must exist and be named once.
- `default_sink` , if given, must be one of the `sources` and must be writable.
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
Leaving it out is a deliberate mode — see [Getting events to the right
calendar](#getting -events-to-the-right-calendar).
Make an aggregate's own events a first-class mode
An aggregate with no default_sink already left events created in it alone, but
treated doing so as a failure: it reported a skip per event per cycle saying no
sink was configured, as though something had gone wrong. Nothing had. An
aggregate is also a calendar, and holding events of its own is a legitimate way
to use one.
Skipped::NoSink is replaced by a kept_local count, reported plainly. `@local`
joins the routing markers, so the mode also works per-event where a
default_sink is configured — which was not previously expressible. Unlike every
other marker it is deliberately not stripped: the others have done their job
once the event reaches its source, whereas this one never leaves, so it has to
stay legible for the next cycle to reach the same decision. `local` is
therefore a reserved endpoint id, and configuring one is refused.
This also fixes a real defect. `retarget` rebuilds the new target from the
recorded links, which cover derived events only, so an event belonging to the
aggregate itself did not follow the move — it stayed on the calendar being left
behind while everything around it moved on, quietly. It is now carried across,
since there is nothing to re-derive it from, and the new target's scheduling
rule is applied on the way: this is a write to an aggregate like any other, and
a guest list carried live onto a server that schedules would mail everyone on
it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 14:30:25 +03:00
- No endpoint may be called `local` , which is reserved for the routing marker.
2026-09-10 14:16:45 +03:00
### A complete example
A Google calendar and a public holiday feed, aggregated into a CalDAV calendar:
```toml
version = 1
[[endpoint]]
id = "gcal"
type = "google"
calendar_id = "primary"
client_id = "1234-abcd.apps.googleusercontent.com"
client_secret_command = "secret-tool lookup service google-calcalist"
[[endpoint]]
id = "holidays"
type = "webcal"
url = "https://www.officeholidays.com/ics/germany"
[[endpoint]]
id = "posteo"
type = "caldav"
url = "https://posteo.de:8443/calendars/you/abc123/unified/"
username = "you@posteo .de"
secret_command = "secret-tool lookup service posteo user you@posteo .de"
[[aggregate]]
id = "unified"
target = "posteo"
sources = ["gcal", "holidays"]
default_sink = "gcal"
```
## Authorising Google
Google requires an OAuth client of your own. This is the fiddliest part of the
setup, so in full:
1. At [console.cloud.google.com ](https://console.cloud.google.com ), create a
project — any name.
2. **APIs & Services → Library** , find **Google Calendar API** , enable it.
3. **OAuth consent screen** : choose **External** . Fill in the required fields.
Under **Test users** , add your own Google address. Without this, authorising
fails with `403: access_denied` even though the account is your own.
4. **Credentials → Create credentials → OAuth client ID** , and for
**Application type** choose **Desktop app** .
Desktop app matters. calcalist listens on a loopback port it picks at run
time and hands Google that address as the redirect, so there is no redirect
URI to register and no domain to verify. A Web application client asks for
both and cannot be made to work here.
5. Copy the client ID into `client_id` , and put the client secret somewhere
`client_secret_command` can read it:
```sh
secret-tool store --label="calcalist Google client secret" service google-calcalist
```
6. Authorise:
```sh
calcalist google login gcal
```
A browser opens; approve the request. calcalist asks only for
`https://www.googleapis.com/auth/calendar` . The refresh token is written
`0600` into the state directory, never into your configuration.
**One login covers every endpoint on that account.** The authorisation is filed
under the account it was granted for, not the endpoint that asked, so a second
Google calendar on the same account needs no login of its own. Set `account` on
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
an endpoint only when calcalist is logged in to more than one account.
2026-09-10 14:16:45 +03:00
**While the consent screen is in Testing, Google expires refresh tokens after
seven days.** An installation that worked last week then stops with nothing
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
having changed locally. Publish the consent screen to production to end this;
`calcalist doctor` reports it either way.
2026-09-10 14:16:45 +03:00
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
## Getting events to the right calendar
2026-09-10 14:16:45 +03:00
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
An event mirrored from a source goes back to that source when you edit it —
calcalist tracks where each one came from. An event you *create* in the
aggregate belongs to nothing yet, and goes to the aggregate's `default_sink` .
2026-09-10 14:16:45 +03:00
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
To send one somewhere else, put the endpoint's id after an `@` **on a line of
its own** in the event's notes:
2026-09-10 14:16:45 +03:00
```
Dentist, bring the referral
@gcal
```
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
The marker is removed before the event arrives. `calcalist status` prints the
markers that would actually work, so you need not remember what is in the
config; a marker naming anything else is refused and reported rather than
quietly filed under the default.
**To keep an event in the aggregate itself**, tag it `@local` — it then stays
put even though a `default_sink` would have taken it, and the marker is
deliberately left in place so later cycles decide the same way. Leaving
`default_sink` out of the aggregate entirely makes that the rule for every
untagged event, so routing becomes opt-in. Such events live only in that
calendar and cannot be rebuilt from a source, but they are ordinary events on
that server, visible to every client subscribed to it.
A category matching an endpoint id works too. Note that *any* category on a new
event is read as a routing instruction, so the notes marker is the safer form.
## What it does to your calendars
- **Deleting an event in the aggregate deletes it at its source**, unless
`propagate_deletes = false` . A cycle that would remove more than
`max_delete_fraction` of the aggregate is refused until you pass `--force` —
with a floor of three, so removing one or two events is never blocked.
- **Edited in both places since the last sync, the source wins.** The conflict
is reported.
- **Alarms are always kept**, with no option to strip them. The tool exists so
you subscribe to one calendar rather than several, so dropping reminders would
disarm every one you have.
- **Mirroring never mails your guests.** On a Google target attendees are kept
and Google is told not to notify; on a CalDAV target, which cannot be told
that, the guest list is carried as inert data instead. Routing an event *you*
created is the exception: that server invites its guests for real, which is
the point of creating it.
## Run it
2026-09-10 14:16:45 +03:00
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
```sh
calcalist doctor # environment and configuration
calcalist sync # one cycle
2026-09-10 14:16:45 +03:00
```
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
To run it regularly, units ship in [systemd/ ](systemd/ ):
```sh
install -Dm644 systemd/calcalist.service ~/.config/systemd/user/calcalist.service
install -Dm644 systemd/calcalist.timer ~/.config/systemd/user/calcalist.timer
systemctl --user daemon-reload
systemctl --user enable --now calcalist.timer
2026-09-10 14:16:45 +03:00
```
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
The service expects the binary at `~/.local/bin/calcalist` ; edit `ExecStart` if
yours is elsewhere. Only one cycle runs at a time — a timer firing while you are
running `sync` by hand is refused, naming the process that holds the lock. A
cycle that could not reach an endpoint exits non-zero on purpose, so a lapsed
token shows up as a failed unit rather than passing unnoticed;
`journalctl --user -u calcalist` has the report. If your secrets come from a
keyring needing an unlocked session, the timer only works while you are logged
in.
2026-09-10 14:16:45 +03:00
## Commands
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
`--help` has the flags.
2026-09-10 14:16:45 +03:00
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
| Command | |
|---|---|
| `sync` | One cycle. `--dry-run` reports against current remote state without writing; `--force` overrides the mass-deletion guard. |
| `status` | Endpoints, aggregates, how much is mirrored, and the routing markers that work. |
| `doctor` | Checks the environment and configuration. Run it first. |
| `discover` | Lists a CalDAV server's calendars as endpoint blocks to paste. |
| `google login` | Authorises an account. |
| `aggregate retarget` | Moves an aggregate to a different target calendar. `sync` refuses on its own when the target changes under it, and points here. |
| `prune` | Reports local mirrors of endpoints no longer configured; removes them under `--force` . |
2026-09-10 14:16:45 +03:00
Exit codes: `0` all well, `1` something failed or an endpoint could not be
Finish M2: run lock, systemd units, discover; drop the web UI
Designing the configuration UI in full made the case against building it. Its
audience would be people who find TOML hard, but with bring-your-own OAuth
client settled, every user must first create a Google Cloud project, configure
a consent screen and put a secret in a keyring — a far higher bar than editing
thirty lines of config. Anyone who clears it can edit the file; anyone who
cannot never reaches the file. Against that stood three dependencies, five
modules, an auth.rs refactor and a security surface guarding something that
reads the config and touches the keyring. `doctor` and `status` had already
absorbed most of what it was for. The reasoning is recorded in TODO.md and
SPECS.md rather than left as an apparent oversight.
The run lock is not a UI feature and closes a gap that already existed: nothing
stopped a timer firing into a hand-run cycle, and two cycles interleaving
writes over the same vdirs is what the design otherwise avoids. flock is used
rather than a pid file because the kernel releases it however the process ends,
so a crash cannot leave a lock to clear by hand — which also means a lock we
failed to take is held by a live process, so the pid in it is worth reporting.
The one idea worth keeping from the UI design was collection discovery, which
needed no web layer. `calcalist discover` prints a ready-to-paste endpoint
block per calendar a server offers, removing the most error-prone field in the
config. pimsync's discovery output is undocumented, so the format was
established against a real server first. Two things it teaches: everything
arrives on stdout including failures, and a pair has two storages, so pimsync
reports the scratch vdir's contents too — parsing anchors on the heading naming
the server, or a probe directory's leftovers would be offered as the user's
calendars.
Verified against Posteo as well as Radicale: all four calendars found, the
first matching the URL already configured.
Also fixes a real defect in the test harness rather than its symptom. Ports were
chosen by binding one and letting go, so two tests could pick the same number —
and the loser's readiness check then succeeded against the winner's server,
silently sharing it. Startup now confirms the child we spawned is the one alive,
retries on another port if not, and waits for a real HTTP response rather than
an open socket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:17:34 +03:00
reached.
2026-09-10 14:16:45 +03:00
## Where things are kept
Everything machine-local lives under `$XDG_STATE_HOME/calcalist` :
```
state.json event mappings, content hashes, sync cursors
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
lock held for the duration of a cycle
2026-09-10 14:16:45 +03:00
pimsync.conf generated on every sync; edits are overwritten
pimsync-status/ pimsync's own record of what it has seen
vdir/< endpoint-id > / the local mirror of each endpoint
google/accounts/< account > .json refresh tokens, 0600
```
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
None of it belongs in a backup or a dotfiles repository — it is a cache, and a
lost state file costs a re-materialisation rather than data. Copy the
2026-09-10 14:16:45 +03:00
configuration between machines; leave this behind.
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
## Known gaps
2026-09-10 14:16:45 +03:00
- **A failing `pimsync sync` stops the whole CalDAV leg.** Google endpoints are
isolated from each other, but pimsync is a single process covering every
CalDAV and feed pair, and a failure does not say which pair it belongs to.