Dotfiles/setup/modules/optional-Modules/apps/caldav-sync.sh

213 lines
7.8 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/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 (per-calendar entries)..."
mkdir -p ~/.config/khal
python3 - "$CALDAV_DIR" << 'PYEOF'
import sys
from pathlib import Path
cal_root = Path(sys.argv[1])
colors = ["light blue", "light green", "light red", "light magenta", "light cyan"]
dirs = sorted(d for d in cal_root.iterdir() if d.is_dir())
lines = ["[calendars]"]
for i, d in enumerate(dirs):
lines += [f" [[{d.name}]]", f" path = {d}", f" color = {colors[i % len(colors)]}"]
lines += [
"", "[sqlite]", "path = ~/.local/share/khal/khal.db",
"", "[locale]", "timeformat = %H:%M", "dateformat = %Y-%m-%d",
"datetimeformat = %Y-%m-%d %H:%M",
]
(Path.home() / ".config/khal/config").write_text("\n".join(lines) + "\n")
PYEOF
# ── 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)
<id>/<YYYY>/<MM>/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"
KHAL_CFG = Path.home() / ".config/khal/config"
COLORS = ["#4CAF50", "#2196F3", "#FF5722", "#9C27B0", "#FF9800"]
KHAL_COLORS = ["light blue", "light green", "light red", "light magenta", "light cyan"]
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}))
# regenerate khal config so new calendars are picked up automatically
cal_dirs = sorted(d for d in CALDAV_DIR.iterdir() if d.is_dir())
khal_lines = ["[calendars]"]
for i, d in enumerate(cal_dirs):
khal_lines += [f" [[{d.name}]]", f" path = {d}",
f" color = {KHAL_COLORS[i % len(KHAL_COLORS)]}"]
khal_lines += ["", "[sqlite]", "path = ~/.local/share/khal/khal.db",
"", "[locale]", "timeformat = %H:%M", "dateformat = %Y-%m-%d",
"datetimeformat = %Y-%m-%d %H:%M"]
KHAL_CFG.write_text("\n".join(khal_lines) + "\n")
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"