summaryrefslogtreecommitdiff
path: root/modules
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
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')
-rw-r--r--modules/archived/org-gcal-config.el (renamed from modules/org-gcal-config.el)26
-rw-r--r--modules/calendar-sync.el438
-rw-r--r--modules/host-environment.el2
-rw-r--r--modules/org-capture-config.el4
-rw-r--r--modules/org-roam-config.el2
-rw-r--r--modules/user-constants.el5
6 files changed, 456 insertions, 21 deletions
diff --git a/modules/org-gcal-config.el b/modules/archived/org-gcal-config.el
index 4eca5e7e..9f43c1c8 100644
--- a/modules/org-gcal-config.el
+++ b/modules/archived/org-gcal-config.el
@@ -137,19 +137,7 @@ Useful after changing `cj/org-gcal-sync-interval-minutes'."
: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)))))
-
+ ;; Configure org-gcal settings (no authinfo.gpg decryption here - deferred to :config)
;; identify calendar to sync and it's destination
(setq org-gcal-fetch-file-alist `(("craigmartinjennings@gmail.com" . ,gcal-file)))
@@ -165,6 +153,18 @@ Useful after changing `cj/org-gcal-sync-interval-minutes'."
(setq org-gcal-managed-update-existing-mode "gcal") ;; GCal wins on conflicts
:config
+ ;; Retrieve credentials from authinfo.gpg when org-gcal is first loaded
+ ;; This happens on first use (e.g., C-; g s), not during daemon startup
+ (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)))))
;; Plstore caching is now configured globally in auth-config.el
;; to ensure it loads before org-gcal needs it
diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el
new file mode 100644
index 00000000..8450b282
--- /dev/null
+++ b/modules/calendar-sync.el
@@ -0,0 +1,438 @@
+;;; calendar-sync.el --- Simple Google Calendar sync via .ics -*- lexical-binding: t; -*-
+
+;; Author: Craig Jennings <c@cjennings.net>
+;; Created: 2025-11-16
+
+;;; Commentary:
+
+;; Simple, reliable one-way sync from Google Calendar to Org mode.
+;; Downloads .ics file from Google Calendar private URL and converts
+;; to Org format. No OAuth, no API complexity, just file conversion.
+;;
+;; Features:
+;; - Pure Emacs Lisp .ics parser (no external dependencies)
+;; - Timer-based automatic sync (every 15 minutes, configurable)
+;; - Self-contained in .emacs.d (no cron, portable across machines)
+;; - Read-only (can't corrupt Google Calendar)
+;; - Works with chime.el for event notifications
+;;
+;; Setup:
+;; 1. Get your Google Calendar private .ics URL:
+;; - Open Google Calendar → Settings → Your Calendar → Integrate calendar
+;; - Copy the "Secret address in iCal format" URL
+;;
+;; 2. Configure in your init.el:
+;; (setq calendar-sync-ics-url "https://calendar.google.com/calendar/ical/YOUR_PRIVATE_URL/basic.ics")
+;; (require 'calendar-sync)
+;; (calendar-sync-start)
+;;
+;; 3. Add to org-agenda (optional):
+;; (add-to-list 'org-agenda-files calendar-sync-file)
+;;
+;; Usage:
+;; - M-x calendar-sync-now ; Manual sync
+;; - M-x calendar-sync-start ; Start auto-sync
+;; - M-x calendar-sync-stop ; Stop auto-sync
+;; - M-x calendar-sync-toggle ; Toggle auto-sync
+
+;;; Code:
+
+(require 'org)
+(require 'user-constants) ; For gcal-file path
+
+;;; Configuration
+
+(defgroup calendar-sync nil
+ "Simple Google Calendar sync via .ics."
+ :group 'calendar
+ :prefix "calendar-sync-")
+
+(defcustom calendar-sync-ics-url nil
+ "Google Calendar private .ics URL.
+Get this from Google Calendar Settings → Integrate calendar → Secret address in iCal format."
+ :type '(choice (const :tag "Not configured" nil)
+ (string :tag "iCal URL"))
+ :group 'calendar-sync)
+
+(defcustom calendar-sync-interval (* 15 60)
+ "Sync interval in seconds.
+Default: 15 minutes (900 seconds)."
+ :type 'integer
+ :group 'calendar-sync)
+
+(defcustom calendar-sync-file
+ gcal-file
+ "Location of synced calendar file.
+Defaults to gcal-file from user-constants."
+ :type 'file
+ :group 'calendar-sync)
+
+;;; Internal state
+
+(defvar calendar-sync--timer nil
+ "Timer object for automatic syncing.")
+
+(defvar calendar-sync--last-sync-time nil
+ "Time of last successful sync.")
+
+(defvar calendar-sync--last-error nil
+ "Last sync error message, if any.")
+
+(defvar calendar-sync--last-timezone-offset nil
+ "Timezone offset in seconds from UTC at last sync.
+Used to detect timezone changes (e.g., when traveling).")
+
+(defvar calendar-sync--state-file
+ (expand-file-name "data/calendar-sync-state.el" user-emacs-directory)
+ "File to persist sync state across Emacs sessions.")
+
+;;; Timezone Detection
+
+(defun calendar-sync--current-timezone-offset ()
+ "Get current timezone offset in seconds from UTC.
+Returns negative for west of UTC, positive for east.
+Example: -21600 for CST (UTC-6), -28800 for PST (UTC-8)."
+ (car (current-time-zone)))
+
+(defun calendar-sync--timezone-name ()
+ "Get human-readable timezone name.
+Returns string like 'CST' or 'PST'."
+ (cadr (current-time-zone)))
+
+(defun calendar-sync--format-timezone-offset (offset)
+ "Format timezone OFFSET (in seconds) as human-readable string.
+Example: -21600 → 'UTC-6' or 'UTC-6:00'."
+ (if (null offset)
+ "unknown"
+ (let* ((hours (/ offset 3600))
+ (minutes (abs (mod (/ offset 60) 60)))
+ (sign (if (>= hours 0) "+" "-"))
+ (abs-hours (abs hours)))
+ (if (= minutes 0)
+ (format "UTC%s%d" sign abs-hours)
+ (format "UTC%s%d:%02d" sign abs-hours minutes)))))
+
+(defun calendar-sync--timezone-changed-p ()
+ "Return t if timezone has changed since last sync."
+ (and calendar-sync--last-timezone-offset
+ (not (= (calendar-sync--current-timezone-offset)
+ calendar-sync--last-timezone-offset))))
+
+;;; State Persistence
+
+(defun calendar-sync--save-state ()
+ "Save sync state to disk for persistence across sessions."
+ (let ((state `((timezone-offset . ,calendar-sync--last-timezone-offset)
+ (last-sync-time . ,calendar-sync--last-sync-time)))
+ (dir (file-name-directory calendar-sync--state-file)))
+ (unless (file-directory-p dir)
+ (make-directory dir t))
+ (with-temp-file calendar-sync--state-file
+ (prin1 state (current-buffer)))))
+
+(defun calendar-sync--load-state ()
+ "Load sync state from disk."
+ (when (file-exists-p calendar-sync--state-file)
+ (condition-case err
+ (with-temp-buffer
+ (insert-file-contents calendar-sync--state-file)
+ (let ((state (read (current-buffer))))
+ (setq calendar-sync--last-timezone-offset
+ (alist-get 'timezone-offset state))
+ (setq calendar-sync--last-sync-time
+ (alist-get 'last-sync-time state))))
+ (error
+ (message "calendar-sync: Error loading state: %s" (error-message-string err))))))
+
+;;; .ics Parsing
+
+(defun calendar-sync--split-events (ics-content)
+ "Split ICS-CONTENT into individual VEVENT blocks.
+Returns list of strings, each containing one VEVENT block."
+ (let ((events '())
+ (start 0))
+ (while (string-match "BEGIN:VEVENT\\(.\\|\n\\)*?END:VEVENT" ics-content start)
+ (push (match-string 0 ics-content) events)
+ (setq start (match-end 0)))
+ (nreverse events)))
+
+(defun calendar-sync--get-property (event property)
+ "Extract PROPERTY value from EVENT string.
+Returns nil if property not found."
+ (when (string-match (format "^%s:\\(.*\\)$" property) event)
+ (match-string 1 event)))
+
+(defun calendar-sync--convert-utc-to-local (year month day hour minute second)
+ "Convert UTC datetime to local time.
+Returns list (year month day hour minute) in local timezone."
+ (let* ((utc-time (encode-time second minute hour day month year 0))
+ (local-time (decode-time utc-time)))
+ (list (nth 5 local-time) ; year
+ (nth 4 local-time) ; month
+ (nth 3 local-time) ; day
+ (nth 2 local-time) ; hour
+ (nth 1 local-time)))) ; minute
+
+(defun calendar-sync--parse-timestamp (timestamp-str)
+ "Parse iCal timestamp string TIMESTAMP-STR.
+Returns (year month day hour minute) or (year month day) for all-day events.
+Converts UTC times (ending in Z) to local time.
+Returns nil if parsing fails."
+ (cond
+ ;; DateTime format: 20251116T140000Z or 20251116T140000
+ ((string-match "\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)T\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)\\(Z\\)?" timestamp-str)
+ (let* ((year (string-to-number (match-string 1 timestamp-str)))
+ (month (string-to-number (match-string 2 timestamp-str)))
+ (day (string-to-number (match-string 3 timestamp-str)))
+ (hour (string-to-number (match-string 4 timestamp-str)))
+ (minute (string-to-number (match-string 5 timestamp-str)))
+ (second (string-to-number (match-string 6 timestamp-str)))
+ (is-utc (match-string 7 timestamp-str)))
+ (if is-utc
+ (calendar-sync--convert-utc-to-local year month day hour minute second)
+ (list year month day hour minute))))
+ ;; Date format: 20251116
+ ((string-match "\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)" timestamp-str)
+ (list (string-to-number (match-string 1 timestamp-str))
+ (string-to-number (match-string 2 timestamp-str))
+ (string-to-number (match-string 3 timestamp-str))))
+ (t nil)))
+
+(defun calendar-sync--format-timestamp (start end)
+ "Format START and END timestamps as org timestamp.
+START and END are lists from `calendar-sync--parse-timestamp'.
+Returns string like '<2025-11-16 Sun 14:00-15:00>' or '<2025-11-16 Sun>'."
+ (let* ((year (nth 0 start))
+ (month (nth 1 start))
+ (day (nth 2 start))
+ (start-hour (nth 3 start))
+ (start-min (nth 4 start))
+ (end-hour (and end (nth 3 end)))
+ (end-min (and end (nth 4 end)))
+ (date-str (format-time-string
+ "<%Y-%m-%d %a"
+ (encode-time 0 0 0 day month year)))
+ (time-str (when (and start-hour end-hour)
+ (format " %02d:%02d-%02d:%02d"
+ start-hour start-min end-hour end-min))))
+ (concat date-str time-str ">")))
+
+(defun calendar-sync--parse-event (event-str)
+ "Parse single VEVENT string EVENT-STR into plist.
+Returns plist with :summary :description :location :start :end.
+Returns nil if event lacks required fields (DTSTART, SUMMARY)."
+ (let ((summary (calendar-sync--get-property event-str "SUMMARY"))
+ (description (calendar-sync--get-property event-str "DESCRIPTION"))
+ (location (calendar-sync--get-property event-str "LOCATION"))
+ (dtstart (calendar-sync--get-property event-str "DTSTART"))
+ (dtend (calendar-sync--get-property event-str "DTEND")))
+ (when (and summary dtstart)
+ (let ((start-parsed (calendar-sync--parse-timestamp dtstart))
+ (end-parsed (and dtend (calendar-sync--parse-timestamp dtend))))
+ (when start-parsed
+ (list :summary summary
+ :description description
+ :location location
+ :start start-parsed
+ :end end-parsed))))))
+
+(defun calendar-sync--event-to-org (event)
+ "Convert parsed EVENT plist to org entry string."
+ (let* ((summary (plist-get event :summary))
+ (description (plist-get event :description))
+ (location (plist-get event :location))
+ (start (plist-get event :start))
+ (end (plist-get event :end))
+ (timestamp (calendar-sync--format-timestamp start end))
+ (parts (list (format "* %s" summary))))
+ (push timestamp parts)
+ (when description
+ (push description parts))
+ (when location
+ (push (format "Location: %s" location) parts))
+ (string-join (nreverse parts) "\n")))
+
+(defun calendar-sync--event-start-time (event)
+ "Extract comparable start time from EVENT plist.
+Returns time value suitable for comparison, or 0 if no start time."
+ (let ((start (plist-get event :start)))
+ (if start
+ (apply #'encode-time
+ 0 ; second
+ (or (nth 4 start) 0) ; minute
+ (or (nth 3 start) 0) ; hour
+ (nth 2 start) ; day
+ (nth 1 start) ; month
+ (nth 0 start) ; year
+ nil)
+ 0)))
+
+(defun calendar-sync--parse-ics (ics-content)
+ "Parse ICS-CONTENT and return org-formatted string.
+Returns nil if parsing fails.
+Events are sorted chronologically by start time."
+ (condition-case err
+ (let* ((events (calendar-sync--split-events ics-content))
+ (parsed-events (delq nil (mapcar #'calendar-sync--parse-event events)))
+ (sorted-events (sort parsed-events
+ (lambda (a b)
+ (time-less-p (calendar-sync--event-start-time a)
+ (calendar-sync--event-start-time b)))))
+ (org-entries (mapcar #'calendar-sync--event-to-org sorted-events)))
+ (if org-entries
+ (concat "# Google Calendar Events\n\n"
+ (string-join org-entries "\n\n")
+ "\n")
+ nil))
+ (error
+ (setq calendar-sync--last-error (error-message-string err))
+ (message "calendar-sync: Parse error: %s" calendar-sync--last-error)
+ nil)))
+
+;;; Sync functions
+
+(defun calendar-sync--fetch-ics (url)
+ "Fetch .ics file from URL using curl.
+Returns .ics content as string, or nil on error.
+Uses curl instead of url-retrieve-synchronously to avoid daemon mode hanging."
+ (condition-case err
+ (with-temp-buffer
+ (let ((exit-code (call-process "curl" nil t nil
+ "-s" ; Silent
+ "-L" ; Follow redirects
+ "-m" "10" ; Max 10 seconds
+ url)))
+ (if (= exit-code 0)
+ (buffer-string)
+ (setq calendar-sync--last-error (format "curl exited with code %d" exit-code))
+ (message "calendar-sync: Fetch error: %s" calendar-sync--last-error)
+ nil)))
+ (error
+ (setq calendar-sync--last-error (error-message-string err))
+ (message "calendar-sync: Fetch error: %s" calendar-sync--last-error)
+ nil)))
+
+(defun calendar-sync--write-file (content)
+ "Write CONTENT to `calendar-sync-file'.
+Creates parent directories if needed."
+ (let ((dir (file-name-directory calendar-sync-file)))
+ (unless (file-directory-p dir)
+ (make-directory dir t)))
+ (with-temp-file calendar-sync-file
+ (insert content))
+ (message "calendar-sync: Updated %s" calendar-sync-file))
+
+;;;###autoload
+(defun calendar-sync-now ()
+ "Sync Google Calendar now.
+Downloads .ics file and updates org file.
+Tracks timezone for automatic re-sync on timezone changes."
+ (interactive)
+ (if (not calendar-sync-ics-url)
+ (message "calendar-sync: Please set calendar-sync-ics-url")
+ (message "calendar-sync: Syncing...")
+ (let* ((ics-content (calendar-sync--fetch-ics calendar-sync-ics-url))
+ (org-content (and ics-content (calendar-sync--parse-ics ics-content))))
+ (if org-content
+ (progn
+ (calendar-sync--write-file org-content)
+ (setq calendar-sync--last-sync-time (current-time))
+ (setq calendar-sync--last-timezone-offset (calendar-sync--current-timezone-offset))
+ (setq calendar-sync--last-error nil)
+ (calendar-sync--save-state)
+ (message "calendar-sync: Sync complete"))
+ (message "calendar-sync: Sync failed (see *Messages* for details)")))))
+
+;;; Timer management
+
+(defun calendar-sync--sync-timer-function ()
+ "Function called by sync timer.
+Checks for timezone changes and triggers re-sync if detected."
+ (when (calendar-sync--timezone-changed-p)
+ (let ((old-tz (calendar-sync--format-timezone-offset
+ calendar-sync--last-timezone-offset))
+ (new-tz (calendar-sync--format-timezone-offset
+ (calendar-sync--current-timezone-offset))))
+ (message "calendar-sync: Timezone change detected (%s → %s), re-syncing..."
+ old-tz new-tz)))
+ (calendar-sync-now))
+
+;;;###autoload
+(defun calendar-sync-start ()
+ "Start automatic calendar syncing.
+Syncs immediately, then every `calendar-sync-interval' seconds."
+ (interactive)
+ (when calendar-sync--timer
+ (cancel-timer calendar-sync--timer))
+ (if (not calendar-sync-ics-url)
+ (message "calendar-sync: Please set calendar-sync-ics-url before starting")
+ ;; Sync immediately
+ (calendar-sync-now)
+ ;; Start timer for future syncs
+ (setq calendar-sync--timer
+ (run-at-time calendar-sync-interval
+ calendar-sync-interval
+ #'calendar-sync--sync-timer-function))
+ (message "calendar-sync: Auto-sync started (every %d minutes)"
+ (/ calendar-sync-interval 60))))
+
+;;;###autoload
+(defun calendar-sync-stop ()
+ "Stop automatic calendar syncing."
+ (interactive)
+ (when calendar-sync--timer
+ (cancel-timer calendar-sync--timer)
+ (setq calendar-sync--timer nil)
+ (message "calendar-sync: Auto-sync stopped")))
+
+;;;###autoload
+(defun calendar-sync-toggle ()
+ "Toggle automatic calendar syncing on/off."
+ (interactive)
+ (if calendar-sync--timer
+ (calendar-sync-stop)
+ (calendar-sync-start)))
+
+;;; Keybindings
+
+;; Calendar sync prefix and keymap
+(defvar-keymap cj/calendar-map
+ :doc "Keymap for calendar synchronization operations"
+ "s" #'calendar-sync-now
+ "t" #'calendar-sync-toggle
+ "S" #'calendar-sync-start
+ "x" #'calendar-sync-stop)
+
+;; Only set up keybindings if cj/custom-keymap exists (not in test environment)
+(when (boundp 'cj/custom-keymap)
+ (keymap-set cj/custom-keymap "g" cj/calendar-map)
+
+ (with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-; g" "calendar sync menu"
+ "C-; g s" "sync now"
+ "C-; g t" "toggle auto-sync"
+ "C-; g S" "start auto-sync"
+ "C-; g x" "stop auto-sync")))
+
+;;; Initialization
+
+;; Load saved state from previous session
+(calendar-sync--load-state)
+
+;; Check for timezone change on startup
+(when (and calendar-sync-ics-url
+ (calendar-sync--timezone-changed-p))
+ (let ((old-tz (calendar-sync--format-timezone-offset
+ calendar-sync--last-timezone-offset))
+ (new-tz (calendar-sync--format-timezone-offset
+ (calendar-sync--current-timezone-offset))))
+ (message "calendar-sync: Timezone changed since last session (%s → %s)"
+ old-tz new-tz)
+ (message "calendar-sync: Run `calendar-sync-now' or start auto-sync to update")
+ ;; Note: We don't auto-sync here to avoid blocking Emacs startup
+ ;; User can manually sync or it will happen on next timer tick if auto-sync is enabled
+ ))
+
+(provide 'calendar-sync)
+;;; calendar-sync.el ends here
diff --git a/modules/host-environment.el b/modules/host-environment.el
index 6900a8df..3cec5df1 100644
--- a/modules/host-environment.el
+++ b/modules/host-environment.el
@@ -112,7 +112,7 @@ Tries multiple methods in order of reliability:
(when (string-match ".*/zoneinfo/\\(.+\\)" target)
(match-string 1 target))))
- ;; Default to nil - lets org-gcal use its default
+ ;; Default to nil if detection fails
nil))
(provide 'host-environment)
diff --git a/modules/org-capture-config.el b/modules/org-capture-config.el
index f41d0228..5d569002 100644
--- a/modules/org-capture-config.el
+++ b/modules/org-capture-config.el
@@ -84,10 +84,6 @@ Intended to be called within an org capture template."
'(("t" "Task" entry (file+headline inbox-file "Inbox")
"* TODO %?" :prepend t)
- ("a" "Appointment" entry (file gcal-file)
- "* %?\n:PROPERTIES:\n:calendar-id:craigmartinjennings@gmail.com\n:END:\n:org-gcal:\n%^T--%^T\n:END:\n\n"
- :jump-to-captured t)
-
("e" "Event" entry (file+headline schedule-file "Scheduled Events")
"* %?%:description
SCHEDULED: %^t%(cj/org-capture-event-content)
diff --git a/modules/org-roam-config.el b/modules/org-roam-config.el
index a33dd633..e8132776 100644
--- a/modules/org-roam-config.el
+++ b/modules/org-roam-config.el
@@ -87,7 +87,7 @@
(lambda ()
(when (and (member org-state org-done-keywords)
(not (member org-last-state org-done-keywords))
- ;; Don't run for gcal.org - it's managed by org-gcal
+ ;; Don't run for gcal.org - it's synced from Google Calendar
(not (string= (buffer-file-name) (expand-file-name gcal-file))))
(cj/org-roam-copy-todo-to-today)))))
diff --git a/modules/user-constants.el b/modules/user-constants.el
index eafd08e8..c4c7a106 100644
--- a/modules/user-constants.el
+++ b/modules/user-constants.el
@@ -127,8 +127,9 @@ Used by transcription module and other audio-related functionality.")
(defvar schedule-file (expand-file-name "schedule.org" org-dir)
"The location of the org file containing scheduled events.")
-(defvar gcal-file (expand-file-name "gcal.org" org-dir)
- "The location of the org file containing Google Calendar information.")
+(defvar gcal-file (expand-file-name "data/gcal.org" user-emacs-directory)
+ "The location of the org file containing Google Calendar information.
+Stored in .emacs.d/data/ so each machine syncs independently from Google Calendar.")
(defvar reference-file (expand-file-name "reference.org" org-dir)
"The location of the org file containing reference information.")