diff options
Diffstat (limited to 'modules/org-agenda-config.el')
| -rw-r--r-- | modules/org-agenda-config.el | 196 |
1 files changed, 140 insertions, 56 deletions
diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el index c7aac99b..4be4db9e 100644 --- a/modules/org-agenda-config.el +++ b/modules/org-agenda-config.el @@ -3,6 +3,14 @@ ;; ;;; Commentary: ;; +;; Performance: +;; - Caches agenda file list to avoid scanning projects directory on every view +;; - Cache builds asynchronously 10 seconds after Emacs startup (non-blocking) +;; - First agenda view uses cache if ready, otherwise builds synchronously +;; - Subsequent views are instant (cached) +;; - Cache auto-refreshes after 1 hour +;; - Manual refresh: M-x cj/org-agenda-refresh-files (e.g., after adding projects) +;; ;; Agenda views are tied to the F8 (fate) key. ;; ;; "We are what we repeatedly do. @@ -31,10 +39,20 @@ ;;; Code: (require 'user-constants) +(require 'system-lib) + +;; Load debug functions if enabled +(when (or (eq cj/debug-modules t) + (memq 'org-agenda cj/debug-modules)) + (require 'org-agenda-config-debug + (expand-file-name "org-agenda-config-debug.el" + (file-name-directory load-file-name)) + t)) (use-package org-agenda :ensure nil ;; built-in :after (org) + :demand t :config (setq org-agenda-prefix-format '((agenda . " %i %-25:c%?-12t% s") (timeline . " % s") @@ -57,10 +75,24 @@ (add-hook 'org-agenda-mode-hook (lambda () (local-set-key (kbd "s-<right>") #'org-agenda-todo-nextset) (local-set-key (kbd "s-<left>") - #'org-agenda-todo-previousset))) + #'org-agenda-todo-previousset)))) + +;; ------------------------ Org Agenda File List Cache ------------------------- +;; Cache agenda file list to avoid expensive directory scanning on every view + +(defvar cj/org-agenda-files-cache nil + "Cached agenda files list to avoid expensive directory scanning. +Set to nil to invalidate cache.") + +(defvar cj/org-agenda-files-cache-time nil + "Time when agenda files cache was last built.") + +(defvar cj/org-agenda-files-cache-ttl 3600 + "Time-to-live for agenda files cache in seconds (default: 1 hour).") - ;; build org-agenda-list for the first time after emacs init completes. - (add-hook 'emacs-startup-hook #'cj/build-org-agenda-list)) +(defvar cj/org-agenda-files-building nil + "Non-nil when agenda files are being built asynchronously. +Prevents duplicate builds if user opens agenda before async build completes.") ;; ------------------------ Add Files To Org Agenda List ----------------------- ;; finds files named 'todo.org' (case insensitive) and adds them to @@ -68,7 +100,6 @@ (defun cj/add-files-to-org-agenda-files-list (directory) "Search for files named \\='todo.org\\=' add them to org-project-files. - DIRECTORY is a string of the path to begin the search." (interactive "D") (setq org-agenda-files @@ -77,35 +108,74 @@ DIRECTORY is a string of the path to begin the search." org-agenda-files))) ;; ---------------------------- Rebuild Org Agenda --------------------------- -;; builds the org agenda list from all agenda targets. +;; builds the org agenda list from all agenda targets with caching. ;; agenda targets is the schedule, contacts, project todos, ;; inbox, and org roam projects. -(defun cj/build-org-agenda-list () - "Rebuilds the org agenda list without checking org-roam for projects. - -Begins with the inbox-file, schedule-file, and contacts-file. -Then adds all todo.org files from projects-dir and code-dir. -Reports elapsed time in the messages buffer." +(defun cj/build-org-agenda-list (&optional force-rebuild) + "Build org-agenda-files list with caching. + +When FORCE-REBUILD is non-nil, bypass cache and rebuild from scratch. +Otherwise, returns cached list if available and not expired. + +This function scans projects-dir for todo.org files, so caching +improves performance from several seconds to instant." + (interactive "P") + ;; Check if we can use cache + (let ((cache-valid (and cj/org-agenda-files-cache + cj/org-agenda-files-cache-time + (not force-rebuild) + (< (- (float-time) cj/org-agenda-files-cache-time) + cj/org-agenda-files-cache-ttl)))) + (if cache-valid + ;; Use cached file list (instant) + (progn + (setq org-agenda-files cj/org-agenda-files-cache) + ;; Always show cache-hit message (interactive or background) + (cj/log-silently "Using cached agenda files (%d files)" + (length org-agenda-files))) + ;; Check if async build is in progress + (when cj/org-agenda-files-building + (cj/log-silently "Waiting for background agenda build to complete...")) + ;; Rebuild from scratch (slow - scans projects directory) + (unwind-protect + (progn + (setq cj/org-agenda-files-building t) + (let ((start-time (current-time))) + ;; Reset org-agenda-files to base files + (setq org-agenda-files (list inbox-file schedule-file gcal-file pcal-file)) + + ;; Check all projects for scheduled tasks + (cj/add-files-to-org-agenda-files-list projects-dir) + + ;; Update cache + (setq cj/org-agenda-files-cache org-agenda-files) + (setq cj/org-agenda-files-cache-time (float-time)) + + ;; Always show completion message (interactive or background) + (cj/log-silently "Built agenda files: %d files in %.3f sec" + (length org-agenda-files) + (- (float-time) (float-time start-time))))) + ;; Always clear the building flag, even if build fails + (setq cj/org-agenda-files-building nil))))) + +;; Build cache asynchronously after startup to avoid blocking Emacs +(run-with-idle-timer + 10 ; Wait 10 seconds after Emacs is idle + nil ; Don't repeat + (lambda () + (cj/log-silently "Building org-agenda files cache in background...") + (cj/build-org-agenda-list))) + +(defun cj/org-agenda-refresh-files () + "Force rebuild of agenda files cache. + +Use this after adding new projects or todo.org files. +Bypasses cache and scans directories from scratch." (interactive) - (let ((start-time (current-time))) - ;; reset org-agenda-files to inbox, schedule, and gcal - (setq org-agenda-files (list inbox-file schedule-file gcal-file)) - - ;; check all projects for scheduled tasks - (cj/add-files-to-org-agenda-files-list projects-dir) - - (message "Rebuilt org-agenda-files in %.3f sec" - (float-time (time-subtract (current-time) start-time))))) - -;; Run the above once after Emacs startup when idle for 1 second -;; makes regenerating the list much faster -(add-hook 'emacs-startup-hook - (lambda () - (run-with-idle-timer 1 nil #'cj/build-org-agenda-list))) + (cj/build-org-agenda-list 'force-rebuild)) (defun cj/todo-list-all-agenda-files () "Displays an \\='org-agenda\\=' todo list. - The contents of the agenda will be built from org-project-files and org-roam files that have project in their filetag." (interactive) @@ -118,7 +188,6 @@ files that have project in their filetag." (defun cj/todo-list-from-this-buffer () "Displays an \\='org-agenda\\=' todo list built from the current buffer. - If the current buffer isn't an org buffer, inform the user." (interactive) (if (eq major-mode 'org-mode) @@ -153,7 +222,6 @@ If the current buffer isn't an org buffer, inform the user." (defun cj/org-agenda-skip-subtree-if-not-overdue () "Skip an agenda subtree if it is not an overdue deadline or scheduled task. - An entry is considered overdue if it has a scheduled or deadline date strictly before today, is not marked as done, and is not a habit." (let* ((subtree-end (save-excursion (org-end-of-subtree t))) @@ -176,7 +244,6 @@ before today, is not marked as done, and is not a habit." (defun cj/org-skip-subtree-if-priority (priority) "Skip an agenda subtree if it has a priority of PRIORITY. - PRIORITY may be one of the characters ?A, ?B, or ?C." (let ((subtree-end (save-excursion (org-end-of-subtree t))) (pri-value (* 1000 (- org-lowest-priority priority))) @@ -187,7 +254,6 @@ PRIORITY may be one of the characters ?A, ?B, or ?C." (defun cj/org-skip-subtree-if-keyword (keywords) "Skip an agenda subtree if it has a TODO keyword in KEYWORDS. - KEYWORDS must be a list of strings." (let ((subtree-end (save-excursion (org-end-of-subtree t)))) (if (member (org-get-todo-state) keywords) @@ -224,12 +290,10 @@ KEYWORDS must be a list of strings." (defun cj/main-agenda-display () "Display the main daily org-agenda view. - This uses all org-agenda targets and presents three sections: - All unfinished priority A tasks - Today's schedule, including habits with consistency graphs - All priority B and C unscheduled/undeadlined tasks - The agenda is rebuilt from all sources before display, including: - inbox-file and schedule-file - Org-roam nodes tagged as \"Project\" @@ -244,7 +308,6 @@ The agenda is rebuilt from all sources before display, including: (defun cj/add-timestamp-to-org-entry (s) "Add an event with time S to appear underneath the line-at-point. - This allows a line to show in an agenda without being scheduled or a deadline." (interactive "sTime: ") (defvar cj/timeformat "%Y-%m-%d %a") @@ -253,7 +316,6 @@ This allows a line to show in an agenda without being scheduled or a deadline." (open-line 1) (forward-line 1) (insert (concat "<" (format-time-string cj/timeformat (current-time)) " " s ">" )))) -;;(global-set-key (kbd "M-t") #'cj/add-timestamp-to-org-entry) ;; --------------------------- Notifications / Alerts -------------------------- ;; send libnotify notifications for agenda items @@ -265,39 +327,61 @@ This allows a line to show in an agenda without being scheduled or a deadline." ;; Install CHIME from GitHub using use-package :vc (Emacs 29+) (use-package chime - :vc (:url "https://github.com/cjennings/chime.el" :rev :newest) - :after (alert org-agenda) :demand t + ;; :vc (:url "https://github.com/cjennings/chime.el" :rev :newest) ;; using latest on github + :after alert ; Removed org-agenda - chime.el requires it internally + :ensure nil ;; using local version + :load-path "~/code/chime.el" + :init + ;; Initialize org-agenda-files with base files before chime loads + ;; The full list will be built asynchronously later + (setq org-agenda-files (list inbox-file schedule-file gcal-file pcal-file)) + + ;; Debug mode (keep set to nil, but available for troubleshooting) + (setq chime-debug nil) :bind ("C-c A" . chime-check) :config - ;; Notification times: 5 minutes before and at event time (0 minutes) - ;; This gives two notifications per event without any after-event notifications - (setq chime-alert-time '(5 0)) + ;; Polling interval: check every minute + (setq chime-check-interval 60) - ;; Modeline display: show upcoming events within 60 minutes - (setq chime-modeline-lookahead 120) - (setq chime-modeline-format " ⏰ %s") + ;; Alert intervals: 5 minutes before and at event time + ;; All notifications use medium urgency + (setq chime-alert-intervals '((5 . medium) (0 . medium))) - ;; Chime sound: plays when notifications appear - (setq chime-play-sound t) - ;; Uses bundled chime.wav by default + ;; Day-wide events: notify at 9 AM for birthdays/all-day events + (setq chime-day-wide-time "09:00") - ;; Notification settings - (setq chime-notification-title "Reminder") - (setq chime-alert-severity 'medium) + ;; Modeline display: show upcoming events within 6 hours + (setq chime-modeline-lookahead-minutes (* 6 60)) + + ;; Tooltip settings: show up to 10 upcoming events within 6 days + (setq chime-modeline-tooltip-max-events 20) + (setq chime-tooltip-lookahead-hours (* 7 24)) + + ;; Modeline content: show title and countdown only (omit event time) + (setq chime-notification-text-format "%t %u") - ;; Don't filter by TODO keywords - notify for all events with timestamps - (setq chime-keyword-whitelist nil) - (setq chime-keyword-blacklist nil) + ;; Time-until format: compact style like " in 10m" or " in 1h 37m" + (setq chime-time-left-format-short " in %mm ") ; Under 1 hour: " in 10m" + (setq chime-time-left-format-long " in %hh %mm ") ; 1 hour+: " in 1h 37m" + (setq chime-time-left-format-at-event "now") + + ;; Title truncation: limit long event titles to 25 characters + (setq chime-max-title-length 25) + + ;; Notification title + (setq chime-notification-title "Reminder") - ;; Only notify for non-done items (default behavior) - (setq chime-predicate-blacklist - '(chime-done-keywords-predicate)) + ;; Calendar URL + (setq chime-calendar-url "https://calendar.google.com/calendar/u/0/r") - ;; Enable chime-mode automatically + ;; Enable chime-mode (chime-mode 1)) +;; which-key labels +(with-eval-after-load 'which-key + (which-key-add-key-based-replacements "C-c A" "chime check")) (provide 'org-agenda-config) ;;; org-agenda-config.el ends here |
