summaryrefslogtreecommitdiff
path: root/modules/org-gcal-config.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-11-16 18:09:17 -0600
committerCraig Jennings <c@cjennings.net>2025-11-16 18:09:17 -0600
commitda0bd6883a4032054aef4b59c338f60796a0fd99 (patch)
treee95369646441b35058c89dfb9c31bad9410243fa /modules/org-gcal-config.el
parent6280a13c87412a6ff50bbaa43e821c518bd2bd0e (diff)
feat(calendar-sync): Add automatic timezone detection and chronological sorting
Implemented calendar-sync.el as a complete replacement for org-gcal, featuring: **Core Functionality:** - One-way sync from Google Calendar to Org (via .ics URL) - UTC to local timezone conversion for all event timestamps - Chronological event sorting (past → present → future) - Non-blocking sync using curl (works reliably in daemon mode) **Automatic Timezone Detection:** - Detects timezone changes when traveling between timezones - Tracks timezone offset in seconds (-21600 for CST, -28800 for PST, etc.) - Triggers automatic re-sync when timezone changes detected - Shows informative messages: "Timezone change detected (UTC-6 → UTC-8)" **State Persistence:** - Saves sync state to ~/.emacs.d/data/calendar-sync-state.el - Persists timezone and last sync time across Emacs sessions - Enables detection even after closing Emacs before traveling **User Features:** - Interactive commands: calendar-sync-now, calendar-sync-start/stop - Keybindings: C-; g s (sync), C-; g a (start auto-sync), C-; g x (stop) - Optional auto-sync every 15 minutes (disabled by default) - Clear status messages for all operations **Code Quality:** - Comprehensive test coverage: 51 ERT tests (100% passing) - Refactored UTC conversion into separate function - Clean separation of concerns (parsing, conversion, formatting, sorting) - Well-documented with timezone behavior guide and changelog **Migration:** - Removed org-gcal-config.el (archived in modules/archived/) - Updated init.el to use calendar-sync - Moved gcal.org to .emacs.d/data/ for machine-independent syncing - Removed org-gcal appointment capture template Files modified: modules/calendar-sync.el:442, tests/test-calendar-sync.el:577 Files created: data/calendar-sync-state.el, tests/testutil-calendar-sync.el Documentation: docs/calendar-sync-timezones.md, docs/calendar-sync-changelog.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'modules/org-gcal-config.el')
-rw-r--r--modules/org-gcal-config.el213
1 files changed, 0 insertions, 213 deletions
diff --git a/modules/org-gcal-config.el b/modules/org-gcal-config.el
deleted file mode 100644
index 4eca5e7e..00000000
--- a/modules/org-gcal-config.el
+++ /dev/null
@@ -1,213 +0,0 @@
-;;; org-gcal-config.el --- Google Calendar synchronization for Org-mode -*- lexical-binding: t; coding: utf-8; -*-
-;;
-;; Author: Craig Jennings <c@cjennings.net>
-;;
-;;; Commentary:
-;;
-;; Bidirectional synchronization between Google Calendar and Org-mode using org-gcal.
-;; - Credential management via authinfo.gpg
-;; - Automatic archival of past events
-;; - Automatic removal of cancelled events, but with TODOs added for visibility
-;; - System timezone configuration via functions in host-environment
-;; - No notifications on syncing
-;; - Events are managed by Org (changes in org file push back to Google Calendar)
-;; This is controlled by org-gcal-managed-newly-fetched-mode and
-;; org-gcal-managed-update-existing-mode set to "org"
-;; - Automatic sync timer (configurable via cj/org-gcal-sync-interval-minutes)
-;; Default: 30 minutes, set to nil to disable
-;; See: https://github.com/kidd/org-gcal.el?tab=readme-ov-file#sync-automatically-at-regular-times
-;; - Validates existing oath2-auto.plist file or creates it to avoid the issue mentioned here:
-;; https://github.com/kidd/org-gcal.el?tab=readme-ov-file#note
-;;
-;; Prerequisites:
-;; 1. Create OAuth 2.0 credentials in Google Cloud Console
-;; See: https://github.com/kidd/org-gcal.el?tab=readme-ov-file#installation
-;; 2. Store credentials in ~/.authinfo.gpg with this format:
-;; machine org-gcal login YOUR_CLIENT_ID password YOUR_CLIENT_SECRET
-;; 3. Define `gcal-file' in user-constants (location of org file to hold sync'd events).
-;;
-;; Usage:
-;; - Manual sync: C-; g s (or M-x org-gcal-sync)
-;; - Toggle auto-sync on/off: C-; g t
-;; - Restart auto-sync (e.g., after changing interval): C-; g r
-;; - Clear sync lock (if sync gets stuck): C-; g c
-;;
-;; Note:
-;; This configuration creates oauth2-auto.plist on first run to prevent sync errors.
-;; Passphrase caching is enabled.
-;;
-;;; Code:
-
-(require 'host-environment)
-(require 'user-constants)
-
-;; Forward declare org-gcal internal variables and functions
-(eval-when-compile
- (defvar org-gcal--sync-lock))
-(declare-function org-gcal-reload-client-id-secret "org-gcal")
-
-;; User configurable sync interval
-(defvar cj/org-gcal-sync-interval-minutes 30
- "Interval in minutes for automatic Google Calendar sync.
-Set to nil to disable automatic syncing.
-Changes take effect after calling `cj/org-gcal-restart-auto-sync'.")
-
-;; Internal timer object
-(defvar cj/org-gcal-sync-timer nil
- "Timer object for automatic org-gcal sync.
-Use `cj/org-gcal-start-auto-sync' and `cj/org-gcal-stop-auto-sync' to control.")
-
-(defun cj/org-gcal-clear-sync-lock ()
- "Clear the org-gcal sync lock.
-Useful when a sync fails and leaves the lock in place, preventing future syncs."
- (interactive)
- (setq org-gcal--sync-lock nil)
- (message "org-gcal sync lock cleared"))
-
-(defun cj/org-gcal-convert-all-to-org-managed ()
- "Convert all org-gcal events in current buffer to Org-managed.
-
-Changes all events with org-gcal-managed property from `gcal' to `org',
-enabling bidirectional sync so changes push back to Google Calendar."
- (interactive)
- (let ((count 0))
- (save-excursion
- (goto-char (point-min))
- (while (re-search-forward "^:org-gcal-managed: gcal$" nil t)
- (replace-match ":org-gcal-managed: org")
- (setq count (1+ count))))
- (when (> count 0)
- (save-buffer))
- (message "Converted %d event(s) to Org-managed" count)))
-
-(defun cj/org-gcal-start-auto-sync ()
- "Start automatic Google Calendar sync timer.
-Uses the interval specified in `cj/org-gcal-sync-interval-minutes'.
-Does nothing if interval is nil or timer is already running."
- (interactive)
- (when (and cj/org-gcal-sync-interval-minutes
- (not (and cj/org-gcal-sync-timer
- (memq cj/org-gcal-sync-timer timer-list))))
- (let ((interval-seconds (* cj/org-gcal-sync-interval-minutes 60)))
- (setq cj/org-gcal-sync-timer
- (run-with-timer
- 120 ;; Initial delay: 2 minutes after startup
- interval-seconds
- (lambda ()
- (condition-case err
- (org-gcal-sync)
- (error (message "org-gcal: Auto-sync failed: %s" err))))))
- (message "org-gcal: Auto-sync started (every %d minutes)"
- cj/org-gcal-sync-interval-minutes))))
-
-(defun cj/org-gcal-stop-auto-sync ()
- "Stop automatic Google Calendar sync timer."
- (interactive)
- (when (and cj/org-gcal-sync-timer
- (memq cj/org-gcal-sync-timer timer-list))
- (cancel-timer cj/org-gcal-sync-timer)
- (setq cj/org-gcal-sync-timer nil)
- (message "org-gcal: Auto-sync stopped")))
-
-(defun cj/org-gcal-toggle-auto-sync ()
- "Toggle automatic Google Calendar sync timer on/off."
- (interactive)
- (if (and cj/org-gcal-sync-timer
- (memq cj/org-gcal-sync-timer timer-list))
- (cj/org-gcal-stop-auto-sync)
- (cj/org-gcal-start-auto-sync)))
-
-(defun cj/org-gcal-restart-auto-sync ()
- "Restart automatic Google Calendar sync timer.
-Useful after changing `cj/org-gcal-sync-interval-minutes'."
- (interactive)
- (cj/org-gcal-stop-auto-sync)
- (cj/org-gcal-start-auto-sync))
-
-;; Deferred library required by org-gcal
-(use-package deferred
- :ensure t)
-
-;; OAuth2 authentication library required by org-gcal
-(use-package oauth2-auto
- :ensure t)
-
-(use-package org-gcal
- :vc (:url "https://github.com/cjennings/org-gcal" :rev :newest)
- :defer t ;; unless idle timer is set below
-
- :init
- ;; Retrieve credentials from authinfo.gpg BEFORE package loads
- ;; This is critical - org-gcal checks these variables at load time
- (require 'auth-source)
- (let ((credentials (car (auth-source-search :host "org-gcal" :require '(:user :secret)))))
- (when credentials
- (setq org-gcal-client-id (plist-get credentials :user))
- ;; The secret might be a function, so we need to handle that
- (let ((secret (plist-get credentials :secret)))
- (setq org-gcal-client-secret
- (if (functionp secret)
- (funcall secret)
- secret)))))
-
- ;; identify calendar to sync and it's destination
- (setq org-gcal-fetch-file-alist `(("craigmartinjennings@gmail.com" . ,gcal-file)))
-
- (setq org-gcal-up-days 30) ;; Look 30 days back
- (setq org-gcal-down-days 60) ;; Look 60 days forward
- (setq org-gcal-auto-archive t) ;; auto-archive old events
- (setq org-gcal-notify-p nil) ;; nil disables; t enables notifications
- (setq org-gcal-remove-api-cancelled-events t) ;; auto-remove cancelled events
- (setq org-gcal-update-cancelled-events-with-todo t) ;; todo cancelled events for visibility
-
- ;; Google Calendar is authoritative - avoids sync conflicts
- (setq org-gcal-managed-newly-fetched-mode "gcal") ;; New events from GCal stay GCal-managed
- (setq org-gcal-managed-update-existing-mode "gcal") ;; GCal wins on conflicts
-
- :config
- ;; Plstore caching is now configured globally in auth-config.el
- ;; to ensure it loads before org-gcal needs it
-
- ;; set org-gcal timezone based on system timezone
- (setq org-gcal-local-timezone (cj/detect-system-timezone))
-
- ;; Reload client credentials (should already be loaded by org-gcal, but ensure it's set)
- (org-gcal-reload-client-id-secret)
-
- ;; Auto-save gcal files after sync completes
- (defun cj/org-gcal-save-files-after-sync (&rest _)
- "Save all org-gcal files after sync completes."
- (dolist (entry org-gcal-fetch-file-alist)
- (let* ((file (cdr entry))
- (buffer (get-file-buffer file)))
- (when (and buffer (buffer-modified-p buffer))
- (with-current-buffer buffer
- (save-buffer)
- (message "Saved %s after org-gcal sync" (file-name-nondirectory file)))))))
-
- ;; Advise org-gcal--sync-unlock which is called when sync completes
- (advice-add 'org-gcal--sync-unlock :after #'cj/org-gcal-save-files-after-sync))
-
-;; Start automatic sync timer based on user configuration
-;; Set cj/org-gcal-sync-interval-minutes to nil to disable
-;; (cj/org-gcal-start-auto-sync)
-
-;; Google Calendar keymap and keybindings
-(defvar-keymap cj/gcal-map
- :doc "Keymap for Google Calendar operations"
- "s" #'org-gcal-sync
- "t" #'cj/org-gcal-toggle-auto-sync
- "r" #'cj/org-gcal-restart-auto-sync
- "c" #'cj/org-gcal-clear-sync-lock)
-(keymap-set cj/custom-keymap "g" cj/gcal-map)
-
-(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements
- "C-; g" "gcal menu"
- "C-; g s" "sync"
- "C-; g t" "toggle auto-sync"
- "C-; g r" "restart auto-sync"
- "C-; g c" "clear sync lock"))
-
-(provide 'org-gcal-config)
-;;; org-gcal-config.el ends here