This is 100% AI generated WITHOUT any code review. AI Slop. It just happens to work for me, and might help somebody else.
3-way Google Calendar sync engine. Keeps 3 calendars in sync: when an event is created, modified, or deleted on any of them, mirror events are automatically created/updated/deleted on the other two.
Mirror events show as "Busy" by default (configurable per calendar) — title, description, and attendee details are hidden via visibility: private.
┌─────────────┐
│ Calendar A │ (personal)
└──┬───┬───┬──┘
│ │ │
mirrors │ │ │ mirrors
│ │ │
┌──────────┘ │ └──────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Mirror A │ │ Mirror A │ │ Mirror B │
│ from B │ │ from C │ │ from C │
└──────────┘ └──────────┘ └──────────┘
- Polls each calendar at a configurable interval (default: 300s)
- Detects new, modified, and cancelled events via the Google Calendar API
- Creates mirrors with per-source
colorId(configurable per calendar) and per-targetmirror_style(busyorfull) - Declined events are not mirrored — if you declined the original event, no "busy" block is created (and existing mirrors are cleaned up)
- Recurring events are mirrored with the same RRULE so all instances stay in sync
- Individual cancelled instances of recurring events are handled
- Working location / out-of-office events are skipped
- Mirrors are marked with
extendedProperties.private— the sync engine skips them to avoid loops - SQLite database tracks event mappings and last-sync timestamps per calendar
- A Google Cloud project with the Google Calendar API enabled
- OAuth 2.0 credentials (Desktop application type) from that project
- Your 3 Google account emails added as test users in the OAuth consent screen
- Go to https://console.cloud.google.com/
- Sign in with your personal Google account
- Create a new project (or use an existing one)
- Enable the Google Calendar API (APIs & Services → Library)
- Go to APIs & Services → OAuth consent screen:
- User Type: External
- Fill in app name, support email, developer contact
- Add your 3 email addresses as Test users
- Go to APIs & Services → Credentials → Create Credentials → OAuth client ID:
- Application type: Desktop app
- Download the JSON file as
hipoglos_client_secret.json
No workspace admin permissions needed. The project stays in Testing mode (no verification). All 3 accounts authenticate individually via OAuth.
# 1. Place the OAuth client secret in the project root
cp ~/Downloads/client_secret_*.json hipoglos_client_secret.json
# 2. Run setup — this authenticates all 3 accounts
cargo run -- setup
# Opens OAuth URLs in your browser. Use 3 separate browsers (or
# incognito windows) for the 3 accounts. If the redirect fails,
# the tool prompts you to paste the authorization code manually.
# 3. Start syncing
cargo run -- sync# 1. Place client secret and create data directory
cp ~/Downloads/client_secret_*.json ./hipoglos_client_secret.json
mkdir -p data/tokens
# 2. Pull the pre-built image (or build locally with: docker compose build)
docker compose pull
# 3. Run setup (interactive — needs browser access, uses host networking)
docker compose run --rm --network host hipoglos setup
# 4. Start the sync engine
docker compose up -dconfig.toml is generated by setup and looks like:
poll_interval_seconds = 300
[[calendars]]
email = "your-personal@gmail.com"
calendar_id = "primary"
token_file = "data/tokens/your-personal_at_gmail.com.json"
[[calendars]]
email = "your-work@company1.com"
calendar_id = "primary"
token_file = "data/tokens/your-work_at_company1.com.json"
[[calendars]]
email = "your-work@company2.com"
calendar_id = "primary"
token_file = "data/tokens/your-work_at_company2.com.json"calendar_id—"primary"for the default calendar, or a specific calendar ID from Google Calendarcolor_id— (optional) Google Calendar color ID ("1"–"11") for mirrors originating from this calendar. If unset, a muted color is chosen automatically based on a hash of the emailmirror_style— (optional, default"busy") how mirror events appear when this calendar is a target:"busy"— summary is"Busy", no description, private visibility (safe for work calendars)"full"— summary is"↗ Original Title", includes original description and RSVP status (for personal calendar where you want to see details)
poll_interval_seconds— how often to check for changes (minimum ~10s to avoid rate limits)
Example with custom colors and mirror styles:
[[calendars]]
email = "your-work@company1.com"
calendar_id = "primary"
token_file = "data/tokens/your-work_at_company1.com.json"
color_id = "3" # Grape (purple) for events originating here
[[calendars]]
email = "your-personal@gmail.com"
calendar_id = "primary"
token_file = "data/tokens/your-personal_at_gmail.com.json"
mirror_style = "full" # show event details on personal calendar| Command | Description |
|---|---|
setup |
Authenticate accounts + verify calendar access + write config |
sync |
Start the polling sync loop (runs until stopped) |
status |
Show configured calendars and whether tokens exist |
Mirror events are visually distinct from regular events and have per-target configurable detail level:
busystyle (default) — summary is"Busy", no description, private visibility. Safe for work calendars where you don't want to leak meeting details.fullstyle —↗prefix in the title, per-source color, description shows source calendar and your RSVP status from the original event.- Common to both styles:
- Private visibility — others only see "Busy"
- No notifications (
reminders.useDefault: false) - Declined events — not mirrored at all (no "busy" block when you declined)
Set the RUST_LOG environment variable to control verbosity:
RUST_LOG=debug cargo run -- sync # full debug output
RUST_LOG=info cargo run -- sync # normal output (default)
RUST_LOG=warn cargo run -- sync # errors onlyIn Docker Compose, set it via environment:.
All runtime state is in the data/ directory:
| File | Purpose |
|---|---|
data/sync.db |
SQLite database (event mappings, sync timestamps) |
data/tokens/*.json |
OAuth refresh tokens (one per account) |
Mount data/ as a volume or bind mount to persist across container restarts.
A GitHub Actions workflow (.github/workflows/build.yml) builds and pushes the image to GitHub Container Registry on every push to main.
The image is published as:
ghcr.io/<your-username>/hipoglos:latest
To use the pre-built image, no setup is needed beyond pushing the repo to GitHub. Then on the NAS:
# Pull and run the pre-built image
docker compose pull
docker compose up -dThe docker-compose.yml already references ghcr.io/${GH_USER:-tulku}/hipoglos:latest — set GH_USER to your GitHub username or edit the file directly.