summaryrefslogtreecommitdiff
path: root/modules/org-agenda-config.el
diff options
context:
space:
mode:
Diffstat (limited to 'modules/org-agenda-config.el')
-rw-r--r--modules/org-agenda-config.el196
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