From e45448297093e5868276c066a73be52a973dc8ac Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 26 May 2026 14:11:06 +0200 Subject: [PATCH] feat(caldav): add CalDAV sync script with calendar.vim cache converter Installs vdirsyncer + khal, writes vdirsyncer/khal configs, creates ics-to-calendarim converter to populate calendar.vim local JSON cache, and sets up a systemd user timer for 15-minute periodic sync. Co-Authored-By: Claude Sonnet 4.6 --- .../optional-Modules/apps/caldav-sync.sh | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100755 setup/modules/optional-Modules/apps/caldav-sync.sh diff --git a/setup/modules/optional-Modules/apps/caldav-sync.sh b/setup/modules/optional-Modules/apps/caldav-sync.sh new file mode 100755 index 0000000..9039ef7 --- /dev/null +++ b/setup/modules/optional-Modules/apps/caldav-sync.sh @@ -0,0 +1,192 @@ +#!/bin/bash +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" + +log "Installing CalDAV sync stack..." +sudo pacman -S --noconfirm --needed vdirsyncer khal python-icalendar + +# ── Credentials ─────────────────────────────────────────────────────────────── +read -rp "CalDAV server URL (e.g. https://dav.example.com/cal/): " CALDAV_URL +read -rp "Username : " CALDAV_USER +read -rsp "Password : " CALDAV_PASS; echo +read -rp "Calendar display name [Personal] : " CAL_NAME +CAL_NAME="${CAL_NAME:-Personal}" + +CALDAV_DIR="$HOME/.local/share/calendars" +mkdir -p "$CALDAV_DIR" + +# ── vdirsyncer ──────────────────────────────────────────────────────────────── +log "Writing ~/.config/vdirsyncer/config..." +mkdir -p ~/.config/vdirsyncer ~/.local/share/vdirsyncer/status +cat > ~/.config/vdirsyncer/config << EOF +[general] +status_path = "$HOME/.local/share/vdirsyncer/status/" + +[pair calendars] +a = "local_cals" +b = "remote_cals" +collections = ["from b"] +metadata = ["color", "displayname"] + +[storage local_cals] +type = "filesystem" +path = "$CALDAV_DIR" +fileext = ".ics" + +[storage remote_cals] +type = "caldav" +url = "$CALDAV_URL" +username = "$CALDAV_USER" +password = "$CALDAV_PASS" +EOF +chmod 600 ~/.config/vdirsyncer/config + +log "Discovering CalDAV collections (confirm any prompts with y)..." +yes | vdirsyncer discover calendars || true + +log "Running initial sync..." +vdirsyncer sync + +# ── khal (CLI calendar companion) ──────────────────────────────────────────── +log "Writing ~/.config/khal/config..." +mkdir -p ~/.config/khal +cat > ~/.config/khal/config << EOF +[calendars] + [[personal]] + path = $CALDAV_DIR/* + color = light blue + +[sqlite] +path = ~/.local/share/khal/khal.db + +[locale] +timeformat = %H:%M +dateformat = %Y-%m-%d +datetimeformat = %Y-%m-%d %H:%M +EOF + +# ── ICS → calendar.vim cache converter ─────────────────────────────────────── +log "Installing ics-to-calendarim converter..." +mkdir -p ~/.local/bin +cat > ~/.local/bin/ics-to-calendarim << 'PYEOF' +#!/usr/bin/env python3 +""" +Convert vdirsyncer ICS files to calendar.vim local event cache. +Cache lives at ~/.cache/calendar.vim/local/ in the format: + calendarList – list of calendars (JSON) + ///0 – events per month (JSON) +""" +import json, sys +from pathlib import Path +from datetime import datetime, date, timezone + +try: + from icalendar import Calendar as iCal +except ImportError: + sys.exit("python-icalendar is required: sudo pacman -S python-icalendar") + +CALDAV_DIR = Path.home() / ".local/share/calendars" +CACHE_DIR = Path.home() / ".cache/calendar.vim/local" + +COLORS = ["#4CAF50", "#2196F3", "#FF5722", "#9C27B0", "#FF9800"] + +def to_naive(dt): + if isinstance(dt, datetime): + if dt.tzinfo is not None: + dt = dt.astimezone(timezone.utc).replace(tzinfo=None) + return dt.strftime("%Y-%m-%dT%H:%M:%S"), True, dt.year, dt.month + if isinstance(dt, date): + return dt.strftime("%Y-%m-%d"), False, dt.year, dt.month + return None, False, None, None + +def main(): + CACHE_DIR.mkdir(parents=True, exist_ok=True) + cal_items = [] + + for idx, cal_dir in enumerate(sorted(d for d in CALDAV_DIR.iterdir() if d.is_dir())): + cal_id = cal_dir.name + cal_summary = cal_dir.name.replace("_", " ").title() + cal_items.append({ + "id": cal_id, + "summary": cal_summary, + "backgroundColor": COLORS[idx % len(COLORS)], + "foregroundColor": "#ffffff", + }) + + by_month: dict = {} + for ics in cal_dir.glob("*.ics"): + try: + cal = iCal.from_ical(ics.read_bytes()) + except Exception as e: + print(f"warning: skipping {ics.name}: {e}", file=sys.stderr) + continue + for comp in cal.walk("VEVENT"): + dtstart = comp.get("DTSTART") + dtend = comp.get("DTEND") or comp.get("DTSTART") + if not dtstart: + continue + s_str, is_timed, yr, mo = to_naive(dtstart.dt) + e_str, _, _, _ = to_naive(dtend.dt) + if not s_str: + continue + uid = str(comp.get("UID", ics.stem)) + summary = str(comp.get("SUMMARY", "")) + key = (yr, mo) + by_month.setdefault(key, []).append({ + "id": uid, + "summary": summary, + "start": {"dateTime": s_str} if is_timed else {"date": s_str}, + "end": {"dateTime": e_str} if is_timed else {"date": e_str}, + }) + + for (yr, mo), events in by_month.items(): + out = CACHE_DIR / cal_id / f"{yr:04d}" / f"{mo:02d}" + out.mkdir(parents=True, exist_ok=True) + (out / "0").write_text(json.dumps({"items": events})) + + (CACHE_DIR / "calendarList").write_text(json.dumps({"items": cal_items})) + print(f"calendar.vim cache updated: {len(cal_items)} calendar(s).") + +if __name__ == "__main__": + main() +PYEOF +chmod +x ~/.local/bin/ics-to-calendarim + +log "Running initial calendar.vim cache build..." +~/.local/bin/ics-to-calendarim + +# ── systemd user timer ──────────────────────────────────────────────────────── +log "Installing vdirsyncer systemd user timer (every 15 min)..." +mkdir -p ~/.config/systemd/user + +cat > ~/.config/systemd/user/vdirsyncer.service << EOF +[Unit] +Description=Sync CalDAV and rebuild calendar.vim cache +After=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/vdirsyncer sync +ExecStartPost=$HOME/.local/bin/ics-to-calendarim +EOF + +cat > ~/.config/systemd/user/vdirsyncer.timer << EOF +[Unit] +Description=Run vdirsyncer every 15 minutes + +[Timer] +OnBootSec=2min +OnUnitActiveSec=15min +Unit=vdirsyncer.service + +[Install] +WantedBy=timers.target +EOF + +systemctl --user daemon-reload +systemctl --user enable --now vdirsyncer.timer + +log "CalDAV sync configured. Events will appear in calendar.vim automatically." +log "Manual sync: vdirsyncer sync && ics-to-calendarim" +log "CLI calendar: khal list / khal interactive" +log "Timer status: systemctl --user status vdirsyncer.timer"