aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-29 19:46:28 -0400
committerCraig Jennings <c@cjennings.net>2026-06-29 19:46:28 -0400
commita200947f67df4e906ba3a2b40cc0be056a2112e7 (patch)
treedb4d77fa8c976ec6f3bbc61b55fc9fb26d1bae78
parent142bbc45c1c64765bb21b0537aa2b7a89300b238 (diff)
downloaddotemacs-a200947f67df4e906ba3a2b40cc0be056a2112e7.tar.gz
dotemacs-a200947f67df4e906ba3a2b40cc0be056a2112e7.zip
refactor(calendar-sync): defer auto-start until first agenda use
calendar-sync ran calendar-sync-start at load, which syncs immediately and then every interval. Every configured calendar resolves its .ics feed URL from a :secret-host in authinfo.gpg, so both the immediate sync and each timer tick decrypt authinfo.gpg. On a cold gpg-agent (after a reboot) that surfaced as a GPG passphrase prompt right after startup, before I'd asked for anything needing a secret. I deferred the whole start, immediate sync and recurring timer alike, to the first org-agenda use via a one-shot org-agenda-mode-hook. The unlock now happens when I actually open the agenda. A manual calendar-sync-start or calendar-sync-now still works on demand.
-rw-r--r--modules/calendar-sync.el22
-rw-r--r--tests/test-calendar-sync--deferred-start.el43
2 files changed, 62 insertions, 3 deletions
diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el
index 1079a72be..297d1fe61 100644
--- a/modules/calendar-sync.el
+++ b/modules/calendar-sync.el
@@ -381,12 +381,28 @@ Syncs all calendars immediately, then every `calendar-sync-interval-minutes'."
;; User can manually sync or it will happen on next timer tick if auto-sync is enabled
))
-;; Start auto-sync if enabled and calendars are configured
-;; Syncs immediately then every calendar-sync-interval-minutes (default: 60 minutes)
+;; Defer auto-sync until calendar data is first needed.
+;;
+;; The :secret-host feed URLs live in authinfo.gpg, and BOTH the immediate sync
+;; and every periodic timer tick resolve them. Calling `calendar-sync-start' at
+;; load (immediate sync + recurring timer) therefore decrypts authinfo.gpg right
+;; after startup, prompting for the GPG passphrase on a cold gpg-agent (e.g.
+;; after a reboot). Defer the whole start to the first org-agenda use, so the
+;; unlock happens when the user actually asks for calendar data. A manual
+;; `calendar-sync-start' / `calendar-sync-now' still works on demand.
+(defun calendar-sync--auto-start-on-first-agenda ()
+ "Start auto-sync on the first org-agenda use, then remove this hook.
+One-shot: deferring `calendar-sync-start' until the agenda is first built keeps a
+cold gpg-agent from being prompted for the authinfo passphrase at startup.
+Removes itself before starting so a `calendar-sync-start' error can't re-fire it."
+ (remove-hook 'org-agenda-mode-hook #'calendar-sync--auto-start-on-first-agenda)
+ (calendar-sync-start))
+
+;; Arm the deferred start when auto-sync is enabled and calendars are configured.
(when (and calendar-sync-auto-start
calendar-sync-calendars
(not noninteractive))
- (calendar-sync-start))
+ (add-hook 'org-agenda-mode-hook #'calendar-sync--auto-start-on-first-agenda))
(provide 'calendar-sync)
diff --git a/tests/test-calendar-sync--deferred-start.el b/tests/test-calendar-sync--deferred-start.el
new file mode 100644
index 000000000..a3a9c0198
--- /dev/null
+++ b/tests/test-calendar-sync--deferred-start.el
@@ -0,0 +1,43 @@
+;;; test-calendar-sync--deferred-start.el --- Deferred auto-start tests -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; calendar-sync arms its auto-sync on the first org-agenda use instead of at
+;; load, so a cold gpg-agent is not prompted for the authinfo passphrase at
+;; startup (the :secret-host feed URLs decrypt authinfo.gpg). These tests cover
+;; the one-shot helper: it starts sync once and removes itself, even when the
+;; start call errors.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'calendar-sync)
+
+;; org-agenda need not be loaded under `make test'; declare the hook special so
+;; the dynamic `let' bindings below shadow it cleanly.
+(defvar org-agenda-mode-hook)
+
+(ert-deftest test-calendar-sync-deferred-start-fires-once-and-unhooks ()
+ "Normal: the one-shot starts sync once and removes itself from the hook."
+ (let ((started 0)
+ (org-agenda-mode-hook (list #'calendar-sync--auto-start-on-first-agenda)))
+ (cl-letf (((symbol-function 'calendar-sync-start)
+ (lambda (&rest _) (setq started (1+ started)))))
+ (calendar-sync--auto-start-on-first-agenda))
+ (should (= started 1))
+ (should-not (member #'calendar-sync--auto-start-on-first-agenda
+ org-agenda-mode-hook))))
+
+(ert-deftest test-calendar-sync-deferred-start-unhooks-even-when-start-errors ()
+ "Error: a start failure still leaves the hook removed, so it cannot re-fire."
+ (let ((org-agenda-mode-hook (list #'calendar-sync--auto-start-on-first-agenda)))
+ (cl-letf (((symbol-function 'calendar-sync-start)
+ (lambda (&rest _) (error "boom"))))
+ (should-error (calendar-sync--auto-start-on-first-agenda)))
+ (should-not (member #'calendar-sync--auto-start-on-first-agenda
+ org-agenda-mode-hook))))
+
+(provide 'test-calendar-sync--deferred-start)
+;;; test-calendar-sync--deferred-start.el ends here