diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/calendar-sync.el | 92 | ||||
| -rw-r--r-- | modules/org-export-config.el | 52 | ||||
| -rw-r--r-- | modules/org-reveal-config.el | 162 |
3 files changed, 227 insertions, 79 deletions
diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el index 6753e5fe..fb1c0f22 100644 --- a/modules/calendar-sync.el +++ b/modules/calendar-sync.el @@ -818,27 +818,18 @@ Returns list (year month day hour minute) in local timezone." SOURCE-TZ is a timezone name like 'Europe/Lisbon' or 'Asia/Yerevan'. Returns list (year month day hour minute) in local timezone, or nil on error. -Uses the system `date` command for reliable timezone conversion." +Uses Emacs built-in timezone support (encode-time/decode-time with ZONE +argument) for fast, subprocess-free conversion. Uses the same system +TZ database as the `date' command." (when (and source-tz (not (string-empty-p source-tz))) (condition-case err - (let* ((date-input (format "%04d-%02d-%02d %02d:%02d" - year month day hour minute)) - ;; Use date command: convert from source-tz to local - ;; TZ= sets output timezone (local), TZ=\"...\" in -d sets input timezone - (cmd (format "date -d 'TZ=\"%s\" %s' '+%%Y %%m %%d %%H %%M' 2>/dev/null" - source-tz date-input)) - (result (string-trim (shell-command-to-string cmd))) - (parts (split-string result " "))) - (if (= 5 (length parts)) - (list (string-to-number (nth 0 parts)) - (string-to-number (nth 1 parts)) - (string-to-number (nth 2 parts)) - (string-to-number (nth 3 parts)) - (string-to-number (nth 4 parts))) - ;; date command failed (invalid timezone, etc.) - (cj/log-silently "calendar-sync: Failed to convert timezone %s: %s" - source-tz result) - nil)) + (let* ((abs-time (encode-time 0 minute hour day month year source-tz)) + (local (decode-time abs-time))) + (list (nth 5 local) ; year + (nth 4 local) ; month + (nth 3 local) ; day + (nth 2 local) ; hour + (nth 1 local))) ; minute (error (cj/log-silently "calendar-sync: Error converting timezone %s: %s" source-tz (error-message-string err)) @@ -1310,25 +1301,67 @@ Creates parent directories if needed." (with-temp-file file (insert content))) +;;; Debug Logging + +(defun calendar-sync--debug-p () + "Return non-nil if calendar-sync debug logging is enabled. +Checks `cj/debug-modules' for symbol `calendar-sync' or t (all)." + (and (boundp 'cj/debug-modules) + (or (eq cj/debug-modules t) + (memq 'calendar-sync cj/debug-modules)))) + ;;; Single Calendar Sync (defun calendar-sync--sync-calendar (calendar) "Sync a single CALENDAR asynchronously. CALENDAR is a plist with :name, :url, and :file keys. -Updates calendar state and saves to disk on completion." +Updates calendar state and saves to disk on completion. +Logs timing for each phase to *Messages* for performance diagnosis." (let ((name (plist-get calendar :name)) (url (plist-get calendar :url)) - (file (plist-get calendar :file))) + (file (plist-get calendar :file)) + (fetch-start (float-time))) ;; Mark as syncing (calendar-sync--set-calendar-state name '(:status syncing)) (cj/log-silently "calendar-sync: [%s] Syncing..." name) (calendar-sync--fetch-ics url (lambda (ics-content) - (let ((org-content (and ics-content (calendar-sync--parse-ics ics-content)))) - (if org-content + (let ((fetch-elapsed (- (float-time) fetch-start))) + (if (null ics-content) (progn - (calendar-sync--write-file org-content file) + (cj/log-silently "calendar-sync: [%s] Fetch failed" name) + (calendar-sync--set-calendar-state + name + (list :status 'error + :last-sync (plist-get (calendar-sync--get-calendar-state name) :last-sync) + :last-error "Fetch failed")) + (calendar-sync--save-state) + (message "calendar-sync: [%s] Sync failed (see *Messages*)" name)) + (when (calendar-sync--debug-p) + (cj/log-silently "calendar-sync: [%s] Fetched %dKB in %.1fs" + name (/ (length ics-content) 1024) fetch-elapsed)) + (let* ((parse-start (float-time)) + (org-content (calendar-sync--parse-ics ics-content)) + (parse-elapsed (- (float-time) parse-start))) + (if (null org-content) + (progn + (cj/log-silently "calendar-sync: [%s] Parse failed (%.1fs)" name parse-elapsed) + (calendar-sync--set-calendar-state + name + (list :status 'error + :last-sync (plist-get (calendar-sync--get-calendar-state name) :last-sync) + :last-error "Parse failed")) + (calendar-sync--save-state) + (message "calendar-sync: [%s] Sync failed (see *Messages*)" name)) + (when (calendar-sync--debug-p) + (cj/log-silently "calendar-sync: [%s] Parsed in %.1fs" name parse-elapsed)) + (let ((write-start (float-time))) + (calendar-sync--write-file org-content file) + (when (calendar-sync--debug-p) + (cj/log-silently "calendar-sync: [%s] Wrote %s in %.2fs" + name (file-name-nondirectory file) + (- (float-time) write-start)))) (calendar-sync--set-calendar-state name (list :status 'ok @@ -1337,14 +1370,9 @@ Updates calendar state and saves to disk on completion." (setq calendar-sync--last-timezone-offset (calendar-sync--current-timezone-offset)) (calendar-sync--save-state) - (message "calendar-sync: [%s] Sync complete → %s" name file)) - (calendar-sync--set-calendar-state - name - (list :status 'error - :last-sync (plist-get (calendar-sync--get-calendar-state name) :last-sync) - :last-error "Parse failed")) - (calendar-sync--save-state) - (message "calendar-sync: [%s] Sync failed (see *Messages*)" name))))))) + (let ((total-elapsed (- (float-time) fetch-start))) + (message "calendar-sync: [%s] Sync complete (%.1fs total) → %s" + name total-elapsed file)))))))))) (defun calendar-sync--sync-all-calendars () "Sync all configured calendars asynchronously. diff --git a/modules/org-export-config.el b/modules/org-export-config.el index 33e170f5..4451eddd 100644 --- a/modules/org-export-config.el +++ b/modules/org-export-config.el @@ -13,14 +13,14 @@ ;; - Texinfo: GNU documentation and Info files ;; ;; Extended via Pandoc: -;; - Additional formats: DOCX, EPUB, reveal.js presentations -;; - Self-contained HTML exports with embedded resources +;; - Additional formats: DOCX, self-contained HTML5 ;; - Custom PDF export with Zathura integration ;; ;; Key features: ;; - UTF-8 encoding enforced across all backends ;; - Subtree export as default scope -;; - reveal.js presentations with CDN or local embedding options +;; +;; Note: reveal.js presentations are handled by org-reveal-config.el (C-; p) ;; ;;; Code: @@ -79,47 +79,7 @@ (setq org-pandoc-options '((standalone . t) (mathjax . t))) - ;; Configure reveal.js specific options - (setq org-pandoc-options-for-revealjs - '((standalone . t) - (variable . "revealjs-url=https://cdn.jsdelivr.net/npm/reveal.js") - (variable . "theme=black") ; or white, league, beige, sky, night, serif, simple, solarized - (variable . "transition=slide") ; none, fade, slide, convex, concave, zoom - (variable . "slideNumber=true") - (variable . "hash=true") - (self-contained . t))) ; This embeds CSS/JS when possible - - ;; Custom function for self-contained reveal.js export - (defun my/org-pandoc-export-to-revealjs-standalone () - "Export to reveal.js with embedded dependencies." - (interactive) - (let* ((org-pandoc-options-for-revealjs - (append org-pandoc-options-for-revealjs - '((self-contained . t) - (embed-resources . t)))) ; pandoc 3.0+ option - (html-file (org-pandoc-export-to-revealjs))) - (when html-file - (browse-url-of-file html-file) - (message "Opened reveal.js presentation: %s" html-file)))) - - ;; Alternative: Download and embed local reveal.js - (defun my/org-pandoc-export-to-revealjs-local () - "Export to reveal.js using local copy of reveal.js." - (interactive) - (let* ((reveal-dir (expand-file-name "reveal.js" user-emacs-directory)) - (org-pandoc-options-for-revealjs - `((standalone . t) - (variable . ,(format "revealjs-url=%s" reveal-dir)) - (variable . "theme=black") - (variable . "transition=slide") - (variable . "slideNumber=true")))) - (unless (file-exists-p reveal-dir) - (cj/log-silently "Downloading reveal.js...") - (shell-command - (format "git clone https://github.com/hakimel/reveal.js.git %s" reveal-dir))) - (org-pandoc-export-to-revealjs))) - - ;; Configure specific format options (your existing config) + ;; Configure specific format options (setq org-pandoc-options-for-latex-pdf '((pdf-engine . "pdflatex"))) (setq org-pandoc-options-for-html5 '((html-q-tags . t) (self-contained . t))) @@ -134,12 +94,10 @@ (start-process "zathura-pdf" nil "zathura" pdf-file) (message "Opened %s in Zathura" pdf-file)))) - ;; Updated menu entries with reveal.js options + ;; Pandoc export menu entries (setq org-pandoc-menu-entry '((?4 "to html5 and open" org-pandoc-export-to-html5-and-open) (?$ "as html5" org-pandoc-export-as-html5) - (?r "to reveal.js (CDN) and open" org-pandoc-export-to-revealjs-and-open) - (?R "to reveal.js (self-contained)" my/org-pandoc-export-to-revealjs-standalone) (?< "to markdown" org-pandoc-export-to-markdown) (?d "to docx and open" org-pandoc-export-to-docx-and-open) (?z "to pdf and open (Zathura)" my/org-pandoc-export-to-pdf-and-open)))) diff --git a/modules/org-reveal-config.el b/modules/org-reveal-config.el new file mode 100644 index 00000000..3ab80315 --- /dev/null +++ b/modules/org-reveal-config.el @@ -0,0 +1,162 @@ +;;; org-reveal-config.el --- Reveal.js Presentation Configuration -*- lexical-binding: t; coding: utf-8; -*- +;; author: Craig Jennings <c@cjennings.net> + +;;; Commentary: +;; Integrates ox-reveal for creating reveal.js presentations from Org files. +;; +;; Fully offline workflow using a local reveal.js clone (managed by +;; scripts/setup-reveal.sh) and self-contained HTML export. +;; +;; Keybindings (C-; p prefix): +;; - C-; p e : Export to self-contained HTML and open in browser +;; - C-; p p : Start live preview (re-exports on save) +;; - C-; p s : Stop live preview +;; - C-; p h : Insert #+REVEAL_ header block at top of current buffer +;; - C-; p n : Create new presentation file (prompts for title and location) + +;;; Code: + +;; Forward declarations for byte-compiler (ox-reveal loaded via use-package) +(defvar org-reveal-root) +(defvar org-reveal-single-file) +(defvar org-reveal-plugins) +(defvar org-reveal-highlight-css) +(defvar org-reveal-init-options) +(declare-function org-reveal-export-to-html "ox-reveal") + +;; --------------------------------- Constants --------------------------------- + +(defconst cj/reveal-root + (expand-file-name "reveal.js" user-emacs-directory) + "Local reveal.js installation directory.") + +(defconst cj/reveal-default-theme "black" + "Default reveal.js theme for new presentations.") + +(defconst cj/reveal-default-transition "slide" + "Default reveal.js slide transition for new presentations.") + +;; --------------------------------- ox-reveal --------------------------------- + +(use-package ox-reveal + :after ox + :config + (setq org-reveal-root (concat "file://" cj/reveal-root)) + (setq org-reveal-single-file t) + (setq org-reveal-plugins '(highlight notes search zoom)) + (setq org-reveal-highlight-css "%r/plugin/highlight/monokai.css") + (setq org-reveal-init-options "slideNumber:true, hash:true")) + +;; ----------------------------- Private Helpers ------------------------------- + +(defun cj/--reveal-header-template (title) + "Return the reveal.js header block string for TITLE." + (unless (stringp title) + (user-error "Title must be a string")) + (format "#+TITLE: %s +#+AUTHOR: %s +#+DATE: %s +#+REVEAL_ROOT: %s +#+REVEAL_THEME: %s +#+REVEAL_TRANS: %s +#+REVEAL_INIT_OPTIONS: slideNumber:true, hash:true +#+REVEAL_PLUGINS: (highlight notes search zoom) +#+REVEAL_HIGHLIGHT_CSS: %%r/plugin/highlight/monokai.css +#+OPTIONS: toc:nil num:nil + +" title (user-full-name) (format-time-string "%Y-%m-%d") + (concat "file://" cj/reveal-root) + cj/reveal-default-theme + cj/reveal-default-transition)) + +(defun cj/--reveal-title-to-filename (title) + "Convert TITLE to a slug-based .org filename. +Downcases TITLE, replaces whitespace runs with hyphens, appends .org." + (concat (replace-regexp-in-string "[[:space:]]+" "-" (downcase title)) + ".org")) + +(defun cj/--reveal-preview-export-on-save () + "Export current org buffer to reveal.js HTML silently. +Intended for use as a buffer-local `after-save-hook'." + (when (derived-mode-p 'org-mode) + (let ((inhibit-message t)) + (org-reveal-export-to-html)))) + +;; ----------------------------- Public Functions ------------------------------ + +(defun cj/reveal-export () + "Export current Org buffer to self-contained reveal.js HTML and open in browser." + (interactive) + (unless (derived-mode-p 'org-mode) + (user-error "Not in an Org buffer")) + (let ((html-file (org-reveal-export-to-html))) + (when html-file + (browse-url-of-file html-file) + (message "Opened presentation: %s" html-file)))) + +(defun cj/reveal-preview-start () + "Start live preview: re-export to HTML on every save. +Opens the presentation in a browser on first call. Subsequent saves +re-export silently; refresh the browser to see changes." + (interactive) + (unless (derived-mode-p 'org-mode) + (user-error "Not in an Org buffer")) + (add-hook 'after-save-hook #'cj/--reveal-preview-export-on-save nil t) + (let ((html-file (org-reveal-export-to-html))) + (when html-file + (browse-url-of-file html-file))) + (message "Live preview started — save to re-export, refresh browser to update")) + +(defun cj/reveal-preview-stop () + "Stop live preview by removing the after-save-hook." + (interactive) + (remove-hook 'after-save-hook #'cj/--reveal-preview-export-on-save t) + (message "Live preview stopped")) + +(defun cj/reveal-insert-header () + "Insert a #+REVEAL_ header block at the top of the current Org buffer." + (interactive) + (unless (derived-mode-p 'org-mode) + (user-error "Not in an Org buffer")) + (let ((title (read-from-minibuffer "Presentation title: "))) + (save-excursion + (goto-char (point-min)) + (insert (cj/--reveal-header-template title))) + (message "Inserted reveal.js headers"))) + +(defun cj/reveal-new () + "Create a new reveal.js presentation file. +Prompts for a title and save location, then opens the file with +reveal.js headers pre-filled." + (interactive) + (let* ((title (read-from-minibuffer "Presentation title: ")) + (default-dir (expand-file-name "~/")) + (file (read-file-name "Save presentation to: " default-dir nil nil + (cj/--reveal-title-to-filename title)))) + (when (file-exists-p file) + (user-error "File already exists: %s" file)) + (find-file file) + (insert (cj/--reveal-header-template title)) + (insert "* Slide 1\n\n") + (save-buffer) + (message "New presentation: %s" file))) + +;; -------------------------------- Keybindings -------------------------------- + +(global-set-key (kbd "C-; p e") #'cj/reveal-export) +(global-set-key (kbd "C-; p p") #'cj/reveal-preview-start) +(global-set-key (kbd "C-; p s") #'cj/reveal-preview-stop) +(global-set-key (kbd "C-; p h") #'cj/reveal-insert-header) +(global-set-key (kbd "C-; p n") #'cj/reveal-new) + +(with-eval-after-load 'which-key + (which-key-add-key-based-replacements + "C-; p" "presentations" + "C-; p e" "export & open" + "C-; p p" "start live preview" + "C-; p s" "stop live preview" + "C-; p h" "insert headers" + "C-; p n" "new presentation")) + +(provide 'org-reveal-config) +;;; org-reveal-config.el ends here |
