aboutsummaryrefslogtreecommitdiff
path: root/modules/calendar-sync-org.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-29 05:23:56 -0400
committerCraig Jennings <c@cjennings.net>2026-06-29 05:23:56 -0400
commit54250d958f2829ff0f44a223a1481b6ec55a6d91 (patch)
treea3e86f5aebc39e934fee12b75e81fc56b016e5da /modules/calendar-sync-org.el
parent3ca9a9f17daa3d6258aa9f32a1a56a9f9e19642c (diff)
downloaddotemacs-54250d958f2829ff0f44a223a1481b6ec55a6d91.tar.gz
dotemacs-54250d958f2829ff0f44a223a1481b6ec55a6d91.zip
refactor: split calendar-sync.el into layered modules
Break the 1724-line calendar-sync.el into a thin public face plus four layered libraries, moving every function verbatim so behavior and public names are unchanged: - calendar-sync-ics.el — base parsing: RFC 5545 text cleaning, VEVENT property extraction, attendee/organizer/URL parsing, timezone and timestamp conversion, date arithmetic, single-event parsing. Depends on neither of the other new modules. - calendar-sync-recurrence.el — RRULE/EXDATE/RECURRENCE-ID expansion. - calendar-sync-org.el — Org rendering and atomic file output. - calendar-sync-source.el — sync state and persistence, async .ics fetch, the batch conversion worker, and the Google Calendar API path. calendar-sync.el keeps configuration, the parse orchestrator, sync dispatch, the user commands, the timer, and the C-; g keymap, and requires the four layers. Each layer forward-declares the config defvars it reads, so no layer requires the top module back. The batch worker loads the whole graph, so source forward-declares the two functions it calls there. Every public name is preserved, so all 574 existing calendar-sync tests pass unchanged through the require chain. The four new modules carry the load-graph and package headers and join the header-contract allowlist. Claude-Session: https://claude.ai/code/session_014fyKMTTqLrZpL3rDF3dYc3
Diffstat (limited to 'modules/calendar-sync-org.el')
-rw-r--r--modules/calendar-sync-org.el94
1 files changed, 94 insertions, 0 deletions
diff --git a/modules/calendar-sync-org.el b/modules/calendar-sync-org.el
new file mode 100644
index 000000000..9ea5a129d
--- /dev/null
+++ b/modules/calendar-sync-org.el
@@ -0,0 +1,94 @@
+;;; calendar-sync-org.el --- Org rendering and atomic file output -*- coding: utf-8; lexical-binding: t; -*-
+
+;; Author: Craig Jennings <c@cjennings.net>
+;; Created: 2025-11-16
+
+;;; Commentary:
+;;
+;; Layer: 3 (Domain Workflow).
+;; Category: D.
+;; Load shape: library.
+;; Top-level side effects: none (defuns only).
+;; Runtime requires: subr-x, cj-org-text-lib, calendar-sync-ics.
+;; Direct test load: yes (requires calendar-sync-ics explicitly).
+;;
+;; Output layer of the calendar-sync parser: render a parsed event plist
+;; into an Org entry (heading, property drawer, body) and write generated
+;; content to disk atomically via a same-directory temp file plus rename,
+;; so a reader never sees a half-written calendar.
+
+;;; Code:
+
+(require 'subr-x)
+(require 'cj-org-text-lib)
+(require 'calendar-sync-ics)
+
+;;; Org Rendering
+
+(defun calendar-sync--event-to-org (event)
+ "Convert parsed EVENT plist to org entry string.
+Produces property drawer with LOCATION, ORGANIZER, STATUS, URL when present.
+Description appears as body text after the drawer."
+ (let* ((summary (cj/org-sanitize-heading
+ (or (plist-get event :summary) "(No Title)")))
+ (description (plist-get event :description))
+ (location (plist-get event :location))
+ (start (plist-get event :start))
+ (end (plist-get event :end))
+ (organizer (plist-get event :organizer))
+ (status (plist-get event :status))
+ (url (plist-get event :url))
+ (timestamp (calendar-sync--format-timestamp start end))
+ ;; Build property drawer entries
+ (props '()))
+ ;; Collect non-nil properties
+ (when (and location (not (string-empty-p location)))
+ (push (format ":LOCATION: %s"
+ (cj/org-sanitize-property-value location))
+ props))
+ (when organizer
+ (let ((org-name (or (plist-get organizer :cn)
+ (plist-get organizer :email))))
+ (when org-name
+ (push (format ":ORGANIZER: %s"
+ (cj/org-sanitize-property-value org-name))
+ props))))
+ (when (and status (not (string-empty-p status)))
+ (push (format ":STATUS: %s"
+ (cj/org-sanitize-property-value status))
+ props))
+ (when (and url (not (string-empty-p url)))
+ (push (format ":URL: %s"
+ (cj/org-sanitize-property-value url))
+ props))
+ (setq props (nreverse props))
+ ;; Build output
+ (let ((parts (list timestamp (format "* %s" summary))))
+ ;; Add property drawer if any properties exist
+ (when props
+ (push ":PROPERTIES:" parts)
+ (dolist (prop props)
+ (push prop parts))
+ (push ":END:" parts))
+ ;; Add description as body text (sanitized to prevent org heading conflicts)
+ (when (and description (not (string-empty-p description)))
+ (push (cj/org-sanitize-body-text description) parts))
+ (string-join (nreverse parts) "\n"))))
+
+;;; Atomic File Output
+
+(defun calendar-sync--write-file (content file)
+ "Write CONTENT to FILE atomically.
+Creates parent directories if needed, then writes a temp file in the same
+directory and renames it into place, so org-agenda or chime reading mid-write
+never sees a half-written calendar."
+ (let ((dir (file-name-directory file)))
+ (unless (file-directory-p dir)
+ (make-directory dir t))
+ (let ((tmp (make-temp-file (expand-file-name ".calendar-sync-" dir))))
+ (with-temp-file tmp
+ (insert content))
+ (rename-file tmp file t))))
+
+(provide 'calendar-sync-org)
+;;; calendar-sync-org.el ends here