;;; pkg-calendar.el --- Google Calendar & Tasks integration -*- lexical-binding: t -*- ;; Copyright (C) 2026 Cytrogen ;;; Commentary: ;; Google Calendar and Google Tasks integration for Emacs. ;; - org-gcal: bidirectional Google Calendar sync ;; - calfw + calfw-org: visual calendar views ;; - org-gtasks: Google Tasks sync (optional, from site-lisp/) ;; ;; Credentials are stored in ~/.emacs.d/calendar-secrets.el (gitignored) ;;; Code: ;;; ============================================================ ;;; Credentials ;;; ============================================================ (defvar my/calendar-client-id nil) (defvar my/calendar-client-secret nil) (defvar my/calendar-email nil) (defvar my/calendar-secrets-file (expand-file-name "calendar-secrets.el" user-emacs-directory)) (defun my/calendar--load-secrets () "Load calendar OAuth credentials from external file." (when (file-exists-p my/calendar-secrets-file) (load-file my/calendar-secrets-file))) (my/calendar--load-secrets) ;;; ============================================================ ;;; Calendar org file ;;; ============================================================ (defvar my/calendar-org-file (expand-file-name "calendar.org" org-directory)) ;;; ============================================================ ;;; org-gcal - Google Calendar sync ;;; ============================================================ (unless (package-installed-p 'org-gcal) (package-refresh-contents) (package-install 'org-gcal)) ;; Set credentials before require to avoid org-gcal's load-time warning (when (and my/calendar-client-id my/calendar-client-secret my/calendar-email) (setq org-gcal-client-id my/calendar-client-id org-gcal-client-secret my/calendar-client-secret org-gcal-fetch-file-alist `((,my/calendar-email . ,my/calendar-org-file)))) ;; Encrypt OAuth token store with user's GPG key (when my/calendar-email (setq plstore-encrypt-to (list my/calendar-email))) ;; Use absolute path for oauth2-auto plstore (setq oauth2-auto-plstore (expand-file-name "oauth2-auto.plist" user-emacs-directory)) ;; Workaround: oauth2-auto bug where file-equal-p fails on new files, ;; causing false "BUG: Attempted to write" errors. ;; See https://github.com/rhaps0dy/emacs-oauth2-auto/issues/6 (with-eval-after-load 'oauth2-auto (defun oauth2-auto--insert-break-on-secret-entries (&rest _args) "Disabled: workaround for oauth2-auto issue #6.")) ;; Use system pinentry for GPG decryption (loopback fails in async callbacks) (setq epg-pinentry-mode nil) (require 'org-gcal) ;;; ============================================================ ;;; calfw - Visual calendar ;;; ============================================================ (unless (package-installed-p 'calfw) (package-refresh-contents) (package-install 'calfw)) (unless (package-installed-p 'calfw-org) (package-install 'calfw-org)) (require 'calfw) (require 'calfw-org) ;;; ============================================================ ;;; org-gtasks - Google Tasks (optional) ;;; ============================================================ (let ((gtasks-path (expand-file-name "site-lisp/org-gtasks" user-emacs-directory))) (when (file-directory-p gtasks-path) (add-to-list 'load-path gtasks-path) (let ((gtasks-el (expand-file-name "org-gtasks.el" gtasks-path))) (when (file-exists-p gtasks-el) (load-file gtasks-el))) (when (and (fboundp 'org-gtasks-register-account) my/calendar-client-id my/calendar-client-secret) (let ((gtasks-dir (expand-file-name "gtasks/" org-directory))) (unless (file-directory-p gtasks-dir) (make-directory gtasks-dir t)) (org-gtasks-register-account :name "Google" :directory gtasks-dir :client-id my/calendar-client-id :client-secret my/calendar-client-secret))))) ;;; ============================================================ ;;; Auto-sync (debounced) ;;; ============================================================ (defvar my/calendar--last-sync-time 0) (defvar my/calendar-sync-interval 300 "Minimum seconds between automatic syncs.") (defun my/calendar--maybe-sync () "Auto-sync Google Calendar if enough time has elapsed." (when (and my/calendar-client-id my/calendar-client-secret (> (- (float-time) my/calendar--last-sync-time) my/calendar-sync-interval)) (setq my/calendar--last-sync-time (float-time)) (ignore-errors (org-gcal-fetch)))) (add-hook 'org-agenda-mode-hook #'my/calendar--maybe-sync) ;;; ============================================================ ;;; Capture template integration ;;; ============================================================ (defun my/calendar--add-capture-templates (&rest _) "Append calendar capture templates after org-structure templates." (add-to-list 'org-capture-templates '("G" "Calendar (日历)") t) (add-to-list 'org-capture-templates `("Ge" "日历事件" entry (file ,my/calendar-org-file) ,(concat "* %^{事件}\n" " :PROPERTIES:\n" " :calendar-id: " (or my/calendar-email "") "\n" " :END:\n" " %^{开始时间}T--%^{结束时间}T\n %?") :empty-lines 1) t)) (advice-add 'my/apply-org-structure :after #'my/calendar--add-capture-templates) ;;; ============================================================ ;;; Agenda custom views ;;; ============================================================ (with-eval-after-load 'org-agenda (setq org-agenda-custom-commands (append org-agenda-custom-commands '(("c" . "Calendar (日历)") ("cw" "本周日历" agenda "" ((org-agenda-span 'week) (org-agenda-files (list my/calendar-org-file)) (org-agenda-overriding-header "本周日历事件"))) ("cm" "本月日历" agenda "" ((org-agenda-span 'month) (org-agenda-files (list my/calendar-org-file)) (org-agenda-overriding-header "本月日历事件"))) ("cd" "今日总览" agenda "" ((org-agenda-span 'day) (org-agenda-overriding-header "今日安排"))))))) ;;; ============================================================ ;;; Interactive Commands ;;; ============================================================ (defun my/calendar-open () "Sync Google Calendar and open calfw visual calendar." (interactive) (when (and my/calendar-client-id my/calendar-client-secret) (ignore-errors (org-gcal-fetch))) (calfw-org-open-calendar nil "Google Calendar" "medium purple")) (defun my/calendar-sync () "Force sync Google Calendar events." (interactive) (if (and my/calendar-client-id my/calendar-client-secret) (progn (setq my/calendar--last-sync-time (float-time)) (org-gcal-fetch) (message "Google Calendar 同步中...")) (message "日历凭据未配置,请编辑 calendar-secrets.el"))) (defun my/calendar-push () "Push current org entry to Google Calendar." (interactive) (if (and my/calendar-client-id my/calendar-client-secret) (org-gcal-post-at-point) (message "日历凭据未配置,请编辑 calendar-secrets.el"))) (defun my/gtasks-sync () "Sync Google Tasks." (interactive) (if (fboundp 'org-gtasks-push-fetch) (org-gtasks-push-fetch "Google") (message "org-gtasks 未安装。请 git clone 到 site-lisp/org-gtasks/"))) (defun my/calendar-status () "Show current calendar integration status." (interactive) (message (concat "Calendar: " (if my/calendar-client-id "OAuth已配置" "OAuth未配置") " | Email: " (or my/calendar-email "未设置") " | org-gcal: " (if (featurep 'org-gcal) "已加载" "未加载") " | calfw: " (if (featurep 'calfw) "已加载" "未加载") " | org-gtasks: " (if (fboundp 'org-gtasks-register-account) "已加载" "未安装")))) (provide 'pkg-calendar) ;;; pkg-calendar.el ends here