From df10ed13470f3f18a090490bd2680abe65f24ba5 Mon Sep 17 00:00:00 2001 From: Cytrogen Date: Wed, 11 Mar 2026 19:23:27 -0400 Subject: [PATCH] =?UTF-8?q?feat(calendar):=20=E6=B7=BB=E5=8A=A0=20Google?= =?UTF-8?q?=20Calendar/Tasks=20=E5=8F=8C=E5=90=91=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - org-gcal 双向同步 Google Calendar 事件到 calendar.org - calfw + calfw-org 可视化日历界面 - org-gtasks 可选集成(从 site-lisp/ 加载) - OAuth2 凭据从外部文件 calendar-secrets.el 加载(已 gitignore) - 自动同步:打开 agenda 时防抖同步(间隔 300 秒) - 添加日历 Capture 模板(advice 注入 org-structure 之后) - 自定义 Agenda 视图:今日/本周/本月日历 - 交互命令:my/calendar-open, my/calendar-sync, my/calendar-push --- config/pkg-calendar.el | 208 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 config/pkg-calendar.el diff --git a/config/pkg-calendar.el b/config/pkg-calendar.el new file mode 100644 index 0000000000000000000000000000000000000000..36cec3c515366a47e353961e00dc8d20ef417792 --- /dev/null +++ b/config/pkg-calendar.el @@ -0,0 +1,208 @@ +;;; 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