@@ 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