Skip to content

tulku/hipoglos

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hipoglos

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.

How it works

           ┌─────────────┐
           │  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-target mirror_style (busy or full)
  • 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

Prerequisites

  • 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

Creating GCP credentials

  1. Go to https://console.cloud.google.com/
  2. Sign in with your personal Google account
  3. Create a new project (or use an existing one)
  4. Enable the Google Calendar API (APIs & Services → Library)
  5. 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
  6. 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.

Setup

Local / direct

# 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

Docker

# 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 -d

Configuration

config.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 Calendar
  • color_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 email
  • mirror_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

Commands

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 event styling

Mirror events are visually distinct from regular events and have per-target configurable detail level:

  • busy style (default) — summary is "Busy", no description, private visibility. Safe for work calendars where you don't want to leak meeting details.
  • full style 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)

Logging

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 only

In Docker Compose, set it via environment:.

Data & persistence

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.

Publishing the Docker image

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 -d

The docker-compose.yml already references ghcr.io/${GH_USER:-tulku}/hipoglos:latest — set GH_USER to your GitHub username or edit the file directly.

About

Calendar Sync tool, written in Rust.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors