rscalendar - Google Calendar CLI Tool
A command-line tool for managing Google Calendar events, written in Rust.
Features
- List events with flexible filtering by date and calendar
- Create events with support for all-day and timed events
- Update events by patching individual fields
- Delete events by ID
- Shell completions for bash, zsh, fish, and more
Technology
- Built with Rust using direct Google Calendar API v3 calls via reqwest
- OAuth2 authentication via yup-oauth2
- CLI powered by clap
Installation
Prerequisites
- Rust toolchain (edition 2024)
- Google Cloud project with Calendar API enabled
- OAuth2 credentials (Desktop application type)
Install from crates.io
cargo install rscalendar
This downloads, compiles, and installs the latest published version into ~/.cargo/bin/.
Building from Source
git clone https://github.com/veltzer/rscalendar.git
cd rscalendar
cargo build --release
The binary will be at target/release/rscalendar.
Google Cloud Setup
- Go to Google Cloud Console
- Create a new project (or use an existing one)
- Enable the Google Calendar API under APIs & Services
- Create OAuth2 credentials:
- Go to APIs & Services > Credentials
- Click “Create Credentials” > “OAuth client ID”
- Choose “Desktop application” as the application type
- Download the JSON file
- Place the credentials file at
~/.config/rscalendar/credentials.json
Getting Started
First-Time Setup
After installing rscalendar and placing your OAuth2 credentials, authenticate:
rscalendar auth
This opens your browser for Google OAuth2 consent. The token is cached at ~/.config/rscalendar/token_cache.json for future use.
If you’re on a headless machine:
rscalendar auth --no-browser
This prints the auth URL instead of opening a browser.
Basic Usage
List upcoming events:
rscalendar list
List more events:
rscalendar list --max-results 25
Create an all-day event:
rscalendar create --summary "Team offsite" --start 2026-04-01 --end 2026-04-02
Create a timed event:
rscalendar create --summary "Standup" --start "2026-04-01T09:00:00+03:00" --end "2026-04-01T09:15:00+03:00"
Delete an event by ID (shown in list output):
rscalendar delete --event-id <EVENT_ID>
Authentication
rscalendar uses OAuth2 to access the Google Calendar API on your behalf.
How It Works
- You provide OAuth2 client credentials (a JSON file from Google Cloud Console)
- On first use, rscalendar opens your browser to get consent
- The access token is cached locally for future requests
Files
| File | Location | Purpose |
|---|---|---|
| Credentials | ~/.config/rscalendar/credentials.json | OAuth2 client ID and secret |
| Token cache | ~/.config/rscalendar/token_cache.json | Cached access/refresh tokens |
Commands
Authenticate (opens browser):
rscalendar auth
Authenticate without browser (prints URL):
rscalendar auth --no-browser
Force re-authentication (removes cached token first):
rscalendar auth --force
Scopes
rscalendar requests the https://www.googleapis.com/auth/calendar scope, which provides full read/write access to your Google Calendar.
Commands
rscalendar provides the following commands:
| Command | Description |
|---|---|
| list-calendars | List all accessible calendars |
| list | List all events for a calendar |
| event create | Create a new event |
| event update | Update fields on an existing event |
| event delete | Delete an event |
| calendar create | Create a new public calendar |
| check | Check events against property rules |
| properties add | Add properties to events |
| properties check | Validate event properties against config |
| properties delete | Delete a property from events |
| properties rename | Rename a property key on events |
| properties edit | Edit a property value on events |
| move-events | Move events between calendars |
| auth | Authenticate with Google |
| defconfig | Print default configuration |
| complete | Generate shell completions |
Global Flags
| Flag | Description |
|---|---|
--show-builtin | Show “(built-in)” labels on standard Google Calendar fields |
--json | Output as JSON for processing with tools like jq |
All commands that interact with the calendar require prior authentication via rscalendar auth.
list-calendars
List all calendars accessible to the authenticated user.
Usage
rscalendar list-calendars
Output
For each calendar, shows:
- Name (with
(primary)marker for the default calendar) - id — the calendar ID to use with
--calendar-idor inconfig.toml - role — your access level (
owner,writer,reader,freeBusyReader) - description — calendar description, if set
Example
$ rscalendar list-calendars
My Calendar (primary)
id: user@gmail.com
role: owner
Work
id: abc123@group.calendar.google.com
role: writer
Holidays in Israel
id: en.jewish#holiday@group.v.calendar.google.com
role: reader
Use the id value as --calendar-id or set it as calendar_id in ~/.config/rscalendar/config.toml.
list
List all events for a calendar.
Usage
rscalendar list [OPTIONS]
Options
| Option | Default | Description |
|---|---|---|
--calendar-name <NAME> | from config | Calendar name to query |
Examples
List all events from the default calendar:
rscalendar list
List all events from a specific calendar:
rscalendar list --calendar-name Teaching
event create
Create a new calendar event.
Usage
rscalendar event create --summary <TEXT> --start <TIME> --end <TIME> [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--summary <TEXT> | Yes | Event title |
--start <TIME> | Yes | Start time (RFC3339 or YYYY-MM-DD) |
--end <TIME> | Yes | End time (RFC3339 or YYYY-MM-DD) |
--calendar-id <ID> | No | Calendar ID (default: primary) |
--description <TEXT> | No | Event description |
--location <TEXT> | No | Event location |
Date Handling
- Timed events: Use RFC3339 format, e.g.
2026-04-01T09:00:00+03:00 - All-day events: Use
YYYY-MM-DDformat. The end date is inclusive — rscalendar automatically converts it to Google’s exclusive format.
Examples
Create an all-day event:
rscalendar event create --summary "Holiday" --start 2026-04-01 --end 2026-04-01
Create a timed event with location:
rscalendar event create --summary "Lunch" --start "2026-04-01T12:00:00+03:00" --end "2026-04-01T13:00:00+03:00" --location "Cafe"
event update
Update fields on an existing calendar event. Only the fields you specify are changed; all others remain unchanged.
Usage
rscalendar event update --event-id <ID> [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--event-id <ID> | Yes | Event ID to update |
--calendar-id <ID> | No | Calendar ID (default: primary) |
--summary <TEXT> | No | New event title |
--start <TIME> | No | New start time (RFC3339 or YYYY-MM-DD) |
--end <TIME> | No | New end time (RFC3339 or YYYY-MM-DD) |
--description <TEXT> | No | New description |
--location <TEXT> | No | New location |
At least one field must be provided.
Examples
Rename an event:
rscalendar event update --event-id abc123 --summary "New Title"
Change the time and location:
rscalendar event update --event-id abc123 --start "2026-04-02T10:00:00+03:00" --end "2026-04-02T11:00:00+03:00" --location "Room B"
event delete
Delete a calendar event by ID.
Usage
rscalendar event delete --event-id <ID> [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--event-id <ID> | Yes | Event ID to delete |
--calendar-id <ID> | No | Calendar ID (default: primary) |
Examples
rscalendar event delete --event-id abc123
The event ID can be found in the output of rscalendar list.
check
Check that events have all required properties based on their type property. The rules are defined in the [check] section of config.toml.
Usage
rscalendar check [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--calendar-name <NAME> | No | Calendar name (default: from config) |
How It Works
- Reads the
[check]section from config, which maps eachtypevalue to a list of required property keys - For each event, looks up its
typeproperty - Verifies that all required properties for that type are present
Reports
- Events missing a
typeproperty - Events with a
typevalue not defined in[check] - Events missing required properties for their type
Config
[check]
teaching = ["client", "company", "course"]
working = ["client", "company"]
call = ["client", "company"]
meeting = ["client", "company"]
This means: if an event has type=teaching, it must also have client, company, and course properties set.
Example
$ rscalendar check
Linux Workshop (2026-04-01T09:00:00+03:00):
- missing required property 'course' (required for type 'teaching')
Team Sync (2026-04-02T14:00:00+03:00):
- missing 'type' property
2 issue(s) in 2 event(s) out of 10.
calendar create
Create a new public Google Calendar.
Usage
rscalendar calendar create <NAME>
Details
Creates a new calendar with the given name and sets it to public (readable by anyone). The calendar ID is printed on success — use it with --calendar-id or in config.toml.
Examples
$ rscalendar calendar create Teaching
Created public calendar 'Teaching'
id: abc123@group.calendar.google.com
properties add
Add shared extended properties to events. Validates keys and values against the [properties] section in config.toml.
Usage
rscalendar properties add [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--calendar-name <NAME> | No | Calendar name (default: from config) |
--key <KEY> | No | Property key to set (must be in config). If omitted, prompts for all missing properties |
--value <VALUE> | No | Property value (must be allowed for the key). Requires --key |
--all | No | Apply to all events without prompting |
Modes
Interactive (no –key/–value)
Walks each event and prompts for all missing properties using a numbered menu of allowed values from config:
rscalendar properties add
Single property (–key and –value)
Sets a specific property, prompting per event (y/n/q):
rscalendar properties add --key company --value AgileSparks
Skip prompting with --all:
rscalendar properties add --key company --value AgileSparks --all
Config
Properties must be defined in ~/.config/rscalendar/config.toml:
[properties]
company = ["AgileSparks", "Intel", "Google"]
course = ["Linux Fundamentals", "Advanced Python"]
properties check
Check that all event properties have keys and values defined in the [properties] section of config.toml.
Usage
rscalendar properties check [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--calendar-name <NAME> | No | Calendar name (default: from config) |
What It Checks
- Missing properties — keys defined in config but not present on the event
- Invalid values — property values not in the allowed list for that key
- Unknown properties — keys on the event that are not defined in config
Example
$ rscalendar properties check
Team Meeting (2026-04-01T10:00:00+03:00):
- missing property 'course'
Workshop (2026-04-02T09:00:00+03:00):
- property 'company' has value 'Foo' which is not in allowed values: AgileSparks, Intel, Google
2 issue(s) found across 5 event(s).
properties delete
Delete a shared extended property from events.
Usage
rscalendar properties delete --key <KEY> [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--key <KEY> | Yes | Property key to delete |
--calendar-name <NAME> | No | Calendar name (default: from config) |
--all | No | Apply to all events without prompting |
Details
Interactive by default — prompts (y/n/q) for each event that has the property. Events that don’t have the property are skipped automatically.
The deletion works by sending the property key with a null value to the Google Calendar API, which is the only way to remove an extended property.
Examples
Delete interactively:
rscalendar properties delete --key client
Delete from all events without prompting:
rscalendar properties delete --key client --all
properties rename
Rename a shared extended property key on events. The value is preserved.
Usage
rscalendar properties rename --from <OLD_KEY> --to <NEW_KEY> [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--from <OLD_KEY> | Yes | Current property key name |
--to <NEW_KEY> | Yes | New property key name |
--calendar-name <NAME> | No | Calendar name (default: from config) |
--all | No | Apply to all events without prompting |
Details
Interactive by default — prompts (y/n/q) for each event that has the old key. Events without the old key are skipped.
The rename removes the old key and adds the new key with the same value in a single PATCH request.
Examples
Rename interactively:
rscalendar properties rename --from source_calendar --to client
Rename on all events without prompting:
rscalendar properties rename --from source_calendar --to client --all
properties edit
Interactively edit properties on each event via TUI menus. For each event, you can add, change, or delete properties in a loop before moving to the next event.
Usage
rscalendar properties edit [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--calendar-name <NAME> | No | Calendar name (default: from config) |
How It Works
For each event, the TUI shows:
- The event name and start time
- Current properties
- A numbered menu of actions:
- add — for properties defined in config but not set on the event
- change — for properties already set (pick a new value from the allowed list)
- delete — remove a property from the event
- n — save changes and move to the next event
- q — save changes and quit
Changes are saved when you press n (next) or q (quit). If no changes were made, nothing is sent to the API.
Example Session
Event: Linux Workshop (2026-04-01T09:00:00+03:00)
company: AgileSparks
Actions:
1: change 'company'
2: delete 'company'
3: add 'course'
n: next event
q: quit
choice: 3
Select course:
1: Linux Fundamentals
2: Advanced Python
s: skip this property
choice: 1
company: AgileSparks
course: Linux Fundamentals
Actions:
1: change 'company'
2: delete 'company'
3: change 'course'
4: delete 'course'
n: next event
q: quit
choice: n
saved.
Config
Property keys and allowed values must be defined in ~/.config/rscalendar/config.toml:
[properties]
company = ["AgileSparks", "Intel", "Google"]
course = ["Linux Fundamentals", "Advanced Python"]
move-events
Move events from one calendar to another. Events are created in the target calendar and deleted from the source.
Usage
rscalendar move-events --source <NAME> --target <NAME> [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--source <NAME> | Yes | Source calendar name to move events from |
--target <NAME> | Yes | Target calendar name to move events into |
--interactive | No | Prompt for each event: y=move, n=skip, q=quit |
--dry-run | No | Show what would be done without making changes |
Examples
Preview what would be moved:
rscalendar move-events --source "Client - John" --target Teaching --dry-run
Interactively move events, choosing which ones to move:
rscalendar move-events --source "Client - John" --target Teaching --interactive
Move all events without prompting:
rscalendar move-events --source "Client - John" --target Teaching
auth
Authenticate with Google via OAuth2 and cache the token locally.
Usage
rscalendar auth [OPTIONS]
Options
| Option | Description |
|---|---|
--no-browser | Print the authorization URL instead of opening the browser |
--force | Remove cached token and force re-authentication |
Examples
Normal authentication (opens browser):
rscalendar auth
Headless authentication:
rscalendar auth --no-browser
Re-authenticate:
rscalendar auth --force
defconfig
Print the default configuration file to stdout.
Usage
rscalendar defconfig
Example
To create a fresh config file with all defaults commented out:
rscalendar defconfig > ~/.config/rscalendar/config.toml
complete
Generate shell completion scripts.
Usage
rscalendar complete <SHELL>
Supported Shells
bashzshfishelvishpowershell
Examples
Generate and install bash completions:
rscalendar complete bash > ~/.local/share/bash-completion/completions/rscalendar
Generate and install zsh completions:
rscalendar complete zsh > ~/.zfunc/_rscalendar
Configuration
rscalendar supports an optional TOML config file for setting defaults.
File Location
~/.config/rscalendar/config.toml
The config file is optional. If missing, built-in defaults are used. All config values can be overridden by CLI flags.
Options
| Key | Type | Default | Description |
|---|---|---|---|
calendar_name | string | (none) | Default calendar name for all commands |
no_browser | boolean | false | Don’t open browser during auth |
The [check] Section
The [check] section defines required properties per event type. It is used by the rscalendar check command to validate that events have all expected properties.
[check]
teaching = ["client", "company", "course"]
working = ["client", "company"]
call = ["client", "company"]
meeting = ["client", "company"]
Each key is a type property value; its array lists the property keys that must be present on events of that type.
Example
# Use a specific calendar by default
calendar_name = "Teaching"
# Always print URL instead of opening browser (headless machines)
no_browser = true
# Required properties per event type
[check]
teaching = ["client", "company", "course"]
working = ["client", "company"]
Precedence
CLI flags always win over config file values:
# Uses calendar_name from config.toml
rscalendar list
# Overrides config.toml with the flag value
rscalendar list --calendar-name "Work"
Files Overview
| File | Purpose |
|---|---|
~/.config/rscalendar/config.toml | User preferences |
~/.config/rscalendar/credentials.json | OAuth2 client credentials |
~/.config/rscalendar/token_cache.json | Cached access/refresh tokens |
Design Decisions
Event Tags via Shared Extended Properties
Google Calendar API offers extendedProperties on events for storing custom key-value metadata. There are two scopes:
private— scoped to the OAuth client ID that created them. Only the same client ID can read them back.shared— visible to anyone with access to the event (all attendees, all apps, including public calendar readers via the API).
Why shared?
We chose shared extended properties for the following reasons:
-
Data durability —
privateproperties are tied to the OAuth client ID incredentials.json. If you ever recreate your OAuth credentials in Google Cloud Console (new project, deleted and re-created client ID, etc.), allprivateextended properties become permanently inaccessible. The data isn’t deleted, but there is no way to recover it with a different client ID. Withshared, the data belongs to the event itself and survives credential changes. -
Acceptable visibility tradeoff —
sharedproperties are visible via the API to anyone who can read the event. On public calendars, this means anyone querying the API could see the tags. However, extended properties are not displayed in the Google Calendar web or mobile UI — they are only accessible programmatically. For a personal CLI tool, this is an acceptable tradeoff. -
Interoperability —
sharedproperties can be read and written by any tool or script with access to the calendar, not just rscalendar. This makes it possible to build other tools that interact with the same tags.
Implementation Plan
Tags will be stored as a comma-separated string under a single key in shared extended properties:
{
"extendedProperties": {
"shared": {
"tags": "work,urgent,followup"
}
}
}
The Google Calendar API supports filtering events by extended properties using the sharedExtendedProperty query parameter, enabling efficient tag-based queries without client-side filtering.
Deleting Extended Properties
Google Calendar API does not remove an extended property when you simply omit it from a PATCH request. To delete a property, you must explicitly set its value to null:
{
"extendedProperties": {
"shared": {
"key_to_delete": null
}
}
}
This is handled automatically by rscalendar properties delete.