~cytrogen/.emacs.d

ref: df10ed13470f3f18a090490bd2680abe65f24ba5 .emacs.d/config/pkg-calendar.el -rw-r--r-- 8.0 KiB
df10ed13 — Cytrogen feat(calendar): 添加 Google Calendar/Tasks 双向同步 a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
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