From 3bb25669ea64a1002576468f38d0d2660ea31962 Mon Sep 17 00:00:00 2001 From: Daniele Fucini Date: Thu, 12 Nov 2026 14:16:36 +0100 Subject: [PATCH] calcurse: sync with Google Calendar via calcurse-caldav Hourly timer started from xmonad, plus UI hooks that sync before load and after exit. calcurse-sync notifies once when the OAuth login expires and restarts the daemon after imports, since it never re-reads its files. --- .config/calcurse/hooks/post-save | 6 +++ .config/calcurse/hooks/pre-load | 5 ++ .config/systemd/user/calcurse-sync.service | 6 +++ .config/systemd/user/calcurse-sync.timer | 7 +++ .config/systemd/user/calcurse-today.service | 1 + .config/xmonad/xmonad.hs | 1 + .local/bin/calcurse-sync | 52 +++++++++++++++++++++ 7 files changed, 78 insertions(+) create mode 100755 .config/calcurse/hooks/post-save create mode 100755 .config/calcurse/hooks/pre-load create mode 100644 .config/systemd/user/calcurse-sync.service create mode 100644 .config/systemd/user/calcurse-sync.timer create mode 100755 .local/bin/calcurse-sync diff --git a/.config/calcurse/hooks/post-save b/.config/calcurse/hooks/post-save new file mode 100755 index 0000000..5d13947 --- /dev/null +++ b/.config/calcurse/hooks/post-save @@ -0,0 +1,6 @@ +#!/bin/sh +# Sync the UI's saved changes once it exits (syncing while it runs would change files under it). +[ "$(ps -o args= -p "$PPID")" = calcurse ] && [ "$(ps -o tty= -p "$PPID" | tr -d ' ')" != '?' ] || exit 0 +# shellcheck disable=SC2016 # $1 expands in the inner shell +setsid -f sh -c 'while kill -0 "$1" 2>/dev/null; do sleep 1; done; systemctl --user start calcurse-sync.service' \ + sh "$PPID" >/dev/null 2>&1 diff --git a/.config/calcurse/hooks/pre-load b/.config/calcurse/hooks/pre-load new file mode 100755 index 0000000..92d76e7 --- /dev/null +++ b/.config/calcurse/hooks/pre-load @@ -0,0 +1,5 @@ +#!/bin/sh +# Sync with Google before the UI loads. Non-interactive calls (-Q, -d, -i, calcurse-caldav's own) run this hook +# too, so act only for the UI: a bare "calcurse" with a terminal (the daemon is bare too, but has none). +[ "$(ps -o args= -p "$PPID")" = calcurse ] && [ "$(ps -o tty= -p "$PPID" | tr -d ' ')" != '?' ] || exit 0 +calcurse-sync --now 2>&1 | systemd-cat -t calcurse-sync diff --git a/.config/systemd/user/calcurse-sync.service b/.config/systemd/user/calcurse-sync.service new file mode 100644 index 0000000..03005b4 --- /dev/null +++ b/.config/systemd/user/calcurse-sync.service @@ -0,0 +1,6 @@ +[Unit] +Description=Sync calcurse with Google Calendar + +[Service] +Type=oneshot +ExecStart=%h/.local/bin/calcurse-sync diff --git a/.config/systemd/user/calcurse-sync.timer b/.config/systemd/user/calcurse-sync.timer new file mode 100644 index 0000000..52f743c --- /dev/null +++ b/.config/systemd/user/calcurse-sync.timer @@ -0,0 +1,7 @@ +[Unit] +Description=Sync calcurse with Google Calendar hourly + +[Timer] +OnActiveSec=1min +OnCalendar=hourly +Persistent=true diff --git a/.config/systemd/user/calcurse-today.service b/.config/systemd/user/calcurse-today.service index 2a8514f..9145abd 100644 --- a/.config/systemd/user/calcurse-today.service +++ b/.config/systemd/user/calcurse-today.service @@ -1,5 +1,6 @@ [Unit] Description=Notify today's calcurse events +After=calcurse-sync.service [Service] Type=oneshot diff --git a/.config/xmonad/xmonad.hs b/.config/xmonad/xmonad.hs index 2bfa0b8..7a7ecc4 100644 --- a/.config/xmonad/xmonad.hs +++ b/.config/xmonad/xmonad.hs @@ -359,6 +359,7 @@ myStartupHook = do spawnOnce "arch-audit-gtk" spawnOnce "calcurse --daemon" spawnOnce "systemctl --user start calcurse-today.timer" + spawnOnce "systemctl --user start calcurse-sync.timer" spawnOnce "systemctl --user start dunst" spawnOnce "lxpolkit" spawnOnce "numlockx" diff --git a/.local/bin/calcurse-sync b/.local/bin/calcurse-sync new file mode 100755 index 0000000..c786ce8 --- /dev/null +++ b/.local/bin/calcurse-sync @@ -0,0 +1,52 @@ +#!/bin/bash +# Sync calcurse with Google Calendar via calcurse-caldav; notify once when the weekly OAuth login expires. +# --now: called from the UI's pre-load hook (skip the UI check, shorter timeout, no daemon restart). +set -uo pipefail + +data=${XDG_DATA_HOME:-$HOME/.local/share}/calcurse +state=${XDG_STATE_HOME:-$HOME/.local/state}/calcurse-sync +expired_flag=$state/login-expired +now=false +[[ ${1:-} == --now ]] && now=true + +# The UI keeps the data in memory, so syncing under it would lose changes; its post-save hook syncs after it exits. +ui_running() { + # shellcheck disable=SC2009 # pgrep can't match "has any terminal" + ps -C calcurse -o tty= | grep -qv '?' +} + +if ! $now && ui_running; then + echo "calcurse UI is running, skipping" + exit 0 +fi + +mkdir -p "$state" +# BROWSER=true: an expired login must not open browser tabs on its own. +out=$(BROWSER=true timeout "$($now && echo 15 || echo 60)" calcurse-caldav 2>&1) +rc=$? +printf '%s\n' "$out" + +if ((rc != 0)); then + if grep -q 'Access token is missing or refresh token is expired' <<<"$out"; then + if [[ ! -e $expired_flag ]]; then + notify-send -a calcurse -u critical -i x-office-calendar "Google Calendar login expired" \ + "Run calcurse-caldav in a terminal, allow access in the browser, then calcurse-caldav --authcode ''." + touch "$expired_flag" + fi + elif grep -q 'Another synchronization instance' <<<"$out"; then + exit 0 + fi + exit "$rc" +fi +rm -f "$expired_flag" + +# A running daemon never re-reads the data files: restart it so imported appointments get alerts. +if ! $now && ! grep -q '^0 items imported, 0 items removed locally' <<<"$out"; then + pidfile=$data/.daemon.pid + if [[ -s $pidfile ]] && pid=$(<"$pidfile") && kill "$pid" 2>/dev/null; then + timeout 5 tail --pid="$pid" -f /dev/null + # Own scope, so the daemon outlives this oneshot service. + systemd-run --user --scope -q calcurse --daemon + echo "restarted calcurse daemon" + fi +fi