summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/org-agenda-config.el416
1 files changed, 311 insertions, 105 deletions
diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el
index 520f2850..becd8f7a 100644
--- a/modules/org-agenda-config.el
+++ b/modules/org-agenda-config.el
@@ -14,119 +14,287 @@
;; watch your character, for it becomes your destiny."
;; -- Lao Tzu
;;
+;; KEYBINDINGS:
+;; f8 - MAIN AGENDA which organizes all tasks and events into:
+;; - overdue tasks (past scheduled/deadline dates)
+;; - all unfinished priority A tasks
+;; - the 8-day schedule, including the habit consistency graph
+;; - all priority B tasks
+;; C-u f8 - Force rebuild of agenda file list (ignores cache)
;;
-;; f8 - MAIN AGENDA which organizes all tasks and events into:
-;; - all unfinished priority A tasks
-;; - the weekly schedule, including the habit consistency graph
-;; - all priority B tasks
-;;
-;; C-f8 - TASK LIST containing all tasks from all agenda targets.
+;; C-f8 - TASK LIST containing all tasks from all agenda targets.
+;; C-u C-f8 - Force rebuild of agenda file list before showing task list
;;
;; M-f8 - TASK LIST containing all tasks from just the current org-mode buffer.
;;
+;; PERFORMANCE OPTIMIZATION:
+;; The agenda file list is cached to avoid expensive directory scans on every
+;; F8 press. The cache is automatically rebuilt every 5 minutes (configurable
+;; via `cj/org-agenda-rebuild-interval'). Use C-u f8 to force an immediate
+;; rebuild if you've added new todo.org files.
+;;
;; NOTE:
;; Files that contain information relevant to the agenda will be found in the
-;; following places: the schedule-file, org-roam notes tagged as 'Projects' and
-;; project todo.org files found in project-dir and code-dir.
+;; following places: the schedule-file, gcal-file, and project todo.org files
+;; found in projects-dir (searches up to 3 levels deep).
;;; Code:
-(require 'user-constants)
-(use-package org-agenda
- :ensure nil ;; built-in
- :after (org org-roam)
- :config
- (setq org-agenda-prefix-format '((agenda . " %i %-25:c%?-12t% s")
- (timeline . " % s")
- (todo . " %i %-25:c")
- (tags . " %i %-12:c")
- (search . " %i %-12:c")))
- (setq org-agenda-dim-blocked-tasks 'invisible)
- (setq org-agenda-skip-scheduled-if-done nil)
- (setq org-agenda-remove-tags t)
- (setq org-agenda-compact-blocks t)
-
- ;; display the agenda from the bottom
+(require 'seq) ;; for seq-filter to disable tramp-archive handlers
+
+;; Forward declarations for user-constants variables
+(eval-when-compile
+ (defvar inbox-file)
+ (defvar schedule-file)
+ (defvar gcal-file)
+ (defvar projects-dir))
+
+;; Forward declarations for org-mode variables
+(defvar org-agenda-files)
+(defvar org-done-keywords)
+(defvar org-lowest-priority)
+(defvar org-agenda-dim-blocked-tasks)
+(defvar org-agenda-use-tag-inheritance)
+(defvar org-agenda-ignore-properties)
+(defvar org-agenda-inhibit-startup)
+(defvar org-agenda-include-diary)
+(defvar org-agenda-skip-deadline-if-done)
+(defvar org-agenda-skip-scheduled-if-done)
+(defvar org-agenda-skip-scheduled-if-deadline-is-shown)
+(defvar org-habit-show-habits-only-for-today)
+(defvar org-habit-graph-column)
+(defvar org-habit-show-graphs)
+(defvar diary-file)
+
+;; Forward declarations for org-mode functions
+(declare-function org-end-of-subtree "org" (invisible-ok))
+(declare-function org-get-todo-state "org" (&optional string))
+(declare-function org-entry-get "org" (pom property &optional inherit literal-nil))
+(declare-function org-time-string-to-absolute "org" (s &optional daynr prefer buffer pos))
+(declare-function org-get-priority "org" (s))
+(declare-function org-agenda "org-agenda" (&optional arg keys restriction))
+(declare-function org-end-of-line "org" (&optional n))
+(declare-function org-agenda-todo-nextset "org-agenda" ())
+(declare-function org-agenda-todo-previousset "org-agenda" ())
+(declare-function thing-at-point "thingatpt" (thing &optional no-properties))
+
+;; Forward declarations for seq (for filtering file-name-handler-alist)
+(declare-function seq-filter "seq" (pred sequence))
+
+;; Forward declarations for alert/org-alert variables
+(defvar alert-fade-time)
+(defvar alert-default-style)
+(defvar org-alert-interval)
+(defvar org-alert-notify-cutoff)
+(defvar org-alert-notify-after-event-cutoff)
+(defvar org-alert-notification-title)
+
+;; ----------------------------- Configuration ---------------------------------
+
+(defvar cj/org-agenda-rebuild-interval 300
+ "Interval in seconds between automatic org-agenda-files rebuilds.
+Set to nil to disable automatic rebuilding. Default is 300 seconds (5 minutes).")
+
+;; ------------------------------ Cache Variables ------------------------------
+
+(defvar cj/org-agenda-files-cache nil
+ "Cached list of org-agenda files to avoid expensive directory scans.")
+
+(defvar cj/org-agenda-rebuild-timer nil
+ "Timer object for periodic org-agenda-files rebuilding.")
+
+;; ========================= PERFORMANCE SETTINGS ==============================
+;; Set these EARLY, before org-agenda loads, for maximum performance
+
+;; CRITICAL: dim-blocked-tasks is VERY slow - checks all dependencies
+(setopt org-agenda-dim-blocked-tasks nil)
+
+;; Disable expensive features
+(setopt org-agenda-use-tag-inheritance nil) ;; don't inherit tags (much faster)
+(setopt org-agenda-ignore-properties '(effort appt category)) ;; skip property lookups
+(setopt org-agenda-inhibit-startup t) ;; don't run startup hooks per file
+
+;; CRITICAL: Disable diary integration - diary is VERY slow
+;; This must be set BEFORE org-agenda loads AND in the config block
+(setopt org-agenda-include-diary nil)
+(setopt diary-file "/dev/null") ;; Extra safety: point diary to /dev/null
+
+;; Skip completed items (fewer items to process)
+(setopt org-agenda-skip-deadline-if-done t)
+(setopt org-agenda-skip-scheduled-if-done t)
+(setopt org-agenda-skip-scheduled-if-deadline-is-shown t)
+
+;; CRITICAL PERFORMANCE: Habit graphs are VERY expensive to render
+;; Habit consistency graphs can add 5-10 seconds to agenda generation
+;; Options:
+;; - org-habit-show-graphs nil = No graphs (FAST - habits still show)
+;; - org-habit-show-graphs t = Show graphs (SLOW but shows consistency)
+;; - org-habit-show-habits-only-for-today t = Graphs only for today (MEDIUM)
+
+(setopt org-habit-show-habits nil)
+(setopt org-habit-show-habits-only-for-today t)
+(setopt org-habit-graph-column 50)
+
+;; Display agenda from the bottom (must be set before org-agenda loads)
+(with-eval-after-load 'org-agenda
(add-to-list 'display-buffer-alist
'("\\*Org Agenda\\*"
(display-buffer-reuse-mode-window display-buffer-below-selected)
(dedicated . t)
- (window-height . fit-window-to-buffer)))
+ (window-height . fit-window-to-buffer))))
+
+(use-package org-agenda
+ :ensure nil ;; built-in
+ :after org
+ :config
+ (require 'user-constants)
+ (setopt org-agenda-prefix-format '((agenda . " %i %-25:c%?-12t% s")
+ (timeline . " % s")
+ (todo . " %i %-25:c")
+ (tags . " %i %-12:c")
+ (search . " %i %-12:c")))
+
+ (setopt org-agenda-remove-tags t)
+ (setopt org-agenda-compact-blocks t)
+ (setopt org-agenda-span 'day) ;; show only today by default (override in custom commands)
+ (setopt org-agenda-start-on-weekday nil) ;; start on current day
+ (setopt org-agenda-use-time-grid nil) ;; disable time grid
+ (setopt org-agenda-show-log nil)
+
+ ;; Ensure diary stays disabled (set again in case something re-enabled it)
+ (setopt org-agenda-include-diary nil)
;; reset s-left/right each time org-agenda is enabled
- (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)))
+ (add-hook 'org-agenda-mode-hook #'cj/org-agenda-mode-keys))
+
+;; ----------------------- Org Agenda Mode Keybindings ------------------------
- ;; build org-agenda-list for the first time after emacs init completes.
- (add-hook 'emacs-startup-hook #'cj/build-org-agenda-list))
+(defun cj/org-agenda-mode-keys ()
+ "Set up keybindings for org-agenda-mode."
+ (local-set-key (kbd "s-<right>") #'org-agenda-todo-nextset)
+ (local-set-key (kbd "s-<left>") #'org-agenda-todo-previousset))
;; ------------------------ Add Files To Org Agenda List -----------------------
;; finds files named 'todo.org' (case insensitive) and adds them to
;; org-agenda-files list.
-(defun cj/add-files-to-org-agenda-files-list (directory)
- "Search for files named \\='todo.org\\=' add them to org-project-files.
+(defun cj/find-todo-org-files (directory)
+ "Find all todo.org files in DIRECTORY using fast wildcard matching.
+Searches up to 3 levels deep for files named todo.org (case insensitive).
+Returns a list of absolute file paths.
+
+Note: Temporarily disables TRAMP archive file name handlers to prevent
+file-expand-wildcards from trying to look inside .zip, .tar.gz, etc. files,
+which would cause significant slowdown."
+ (when (and directory (file-directory-p directory))
+ (let ((dir (file-name-as-directory (expand-file-name directory)))
+ (found-files '())
+ ;; Temporarily disable tramp-archive to prevent looking inside archives
+ (file-name-handler-alist
+ (seq-filter (lambda (handler)
+ (not (eq (cdr handler) 'tramp-archive-file-name-handler)))
+ file-name-handler-alist)))
+ ;; Search at multiple depth levels (faster than recursive search)
+ ;; Level 0: todo.org directly in directory
+ (setq found-files (append (file-expand-wildcards (concat dir "[Tt][Oo][Dd][Oo].[Oo][Rr][Gg]")) found-files))
+ ;; Level 1: todo.org one level deep
+ (setq found-files (append (file-expand-wildcards (concat dir "*/[Tt][Oo][Dd][Oo].[Oo][Rr][Gg]")) found-files))
+ found-files)))
-DIRECTORY is a string of the path to begin the search."
+(defun cj/add-files-to-org-agenda-files-list (directory)
+ "Search for files named `todo.org' and add them to `org-agenda-files'.
+DIRECTORY is a string of the path to begin the search.
+Modifies the global `org-agenda-files' variable as a side effect."
(interactive "D")
(setq org-agenda-files
- (append (directory-files-recursively directory
- "^[Tt][Oo][Dd][Oo]\\.[Oo][Rr][Gg]$" t)
+ (append (cj/find-todo-org-files directory)
org-agenda-files)))
;; ---------------------------- Rebuild Org Agenda ---------------------------
;; builds the org agenda list from all agenda targets.
-;; 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.
+;; agenda targets is the schedule, contacts, project todos, and inbox.org
+
+(defun cj/build-org-agenda-list (&optional force)
+ "Build the org agenda file list, using cache when possible.
+With optional FORCE argument (or when called interactively with prefix arg),
+ignores the cache and forces a full rebuild.
+The file list is cached in `cj/org-agenda-files-cache' to avoid expensive
+directory scans on every agenda display. The cache is automatically refreshed
+every `cj/org-agenda-rebuild-interval' seconds.
+
+Begins with inbox-file, schedule-file, and gcal-file.
+Then adds all todo.org files from projects-dir.
Reports elapsed time in the messages buffer."
- (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
+ (interactive "P")
+ (if (and (not force) cj/org-agenda-files-cache)
+ (progn
+ ;; Use cached file list
+ (setq org-agenda-files cj/org-agenda-files-cache)
+ (message "Using cached org-agenda-files (%d files)" (length org-agenda-files)))
+ ;; Force rebuild or no cache exists
+ (let ((start-time (current-time)))
+ ;; Reset org-agenda-files to core files
+ (setq org-agenda-files (list inbox-file schedule-file gcal-file))
+
+ ;; Add all todo.org files from projects
+ (cj/add-files-to-org-agenda-files-list projects-dir)
+
+ ;; Cache the result
+ (setq cj/org-agenda-files-cache org-agenda-files)
+
+ (message "Rebuilt org-agenda-files in %.3f sec (%d files)"
+ (float-time (time-subtract (current-time) start-time))
+ (length org-agenda-files)))))
+
+;; ------------------------- Periodic Rebuild Timer ----------------------------
+
+(defun cj/start-org-agenda-rebuild-timer ()
+ "Start the periodic timer to rebuild org-agenda-files.
+The timer interval is controlled by `cj/org-agenda-rebuild-interval'.
+If the interval is nil, no periodic rebuilding occurs."
+ (when cj/org-agenda-rebuild-timer
+ (cancel-timer cj/org-agenda-rebuild-timer)
+ (setq cj/org-agenda-rebuild-timer nil))
+ (when cj/org-agenda-rebuild-interval
+ (setq cj/org-agenda-rebuild-timer
+ (run-with-timer cj/org-agenda-rebuild-interval
+ cj/org-agenda-rebuild-interval
+ (lambda () (cj/build-org-agenda-list t))))))
+
+;; Initial build on startup (after 1 second idle) and start periodic timer
(add-hook 'emacs-startup-hook
- (lambda ()
- (run-with-idle-timer 1 nil #'cj/build-org-agenda-list)))
-
-(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)
- (cj/build-org-agenda-list)
+ (lambda ()
+ (run-with-idle-timer 1 nil
+ (lambda ()
+ (cj/build-org-agenda-list t)
+ (cj/start-org-agenda-rebuild-timer)))))
+
+;;;###autoload
+(defun cj/todo-list-all-agenda-files (&optional force-rebuild)
+ "Display an `org-agenda' todo list from all agenda files.
+With prefix argument FORCE-REBUILD, forces a complete rebuild of the
+agenda file list instead of using the cache."
+ (interactive "P")
+ (cj/build-org-agenda-list force-rebuild)
(org-agenda "a" "t"))
-(global-set-key (kbd "C-<f8>") #'cj/todo-list-all-agenda-files)
+
+;;;###autoload (keymap-global-set "C-<f8>" #'cj/todo-list-all-agenda-files)
;; ------------------------- Agenda List Current Buffer ------------------------
;; an agenda listing tasks from just the current buffer.
+;;;###autoload
(defun cj/todo-list-from-this-buffer ()
- "Displays an \\='org-agenda\\=' todo list built from the current 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)
(let ((org-agenda-files (list buffer-file-name)))
(org-agenda "a" "t"))
- (message (concat "Your org agenda request based on '" (buffer-name (current-buffer))
- "' failed because it's not an org buffer."))))
-(global-set-key (kbd "M-<f8>") #'cj/todo-list-from-this-buffer)
+ (message (format "Your org agenda request based on '%s' failed because it's not an org buffer."
+ (buffer-name)))))
+
+;;;###autoload (keymap-global-set "M-<f8>" #'cj/todo-list-from-this-buffer)
;; -------------------------------- Main Agenda --------------------------------
;; my custom agenda command from all available agenda targets. adapted from:
@@ -144,6 +312,18 @@ If the current buffer isn't an org buffer, inform the user."
(defvar cj/main-agenda-tasks-title "PRIORITY B"
"String to announce the schedule section of the main agenda.")
+;; Cache "today" to avoid recalculating for every heading
+(defvar cj/org-agenda-today-absolute nil
+ "Cached absolute day number for today, reset each agenda generation.")
+
+(defun cj/org-agenda-reset-today-cache ()
+ "Reset the cached `today' value before generating agenda."
+ (setq cj/org-agenda-today-absolute
+ (org-time-string-to-absolute (format-time-string "%Y-%m-%d"))))
+
+;; Reset cache before each agenda generation
+(add-hook 'org-agenda-mode-hook #'cj/org-agenda-reset-today-cache)
+
(defun cj/org-skip-subtree-if-habit ()
"Skip an agenda entry if it has a STYLE property equal to \"habit\"."
(let ((subtree-end (save-excursion (org-end-of-subtree t))))
@@ -155,30 +335,31 @@ If the current buffer isn't an org buffer, inform the user."
"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."
+before today, is not marked as done, and is not a habit.
+Performance: Uses cached `today' value to avoid recalculation per heading."
(let* ((subtree-end (save-excursion (org-end-of-subtree t)))
- (todo-state (org-get-todo-state))
- (style (org-entry-get nil "STYLE"))
- (deadline (org-entry-get nil "DEADLINE"))
- (scheduled (org-entry-get nil "SCHEDULED"))
- (today (org-time-string-to-absolute (format-time-string "%Y-%m-%d")))
- (deadline-day (and deadline (org-time-string-to-absolute deadline)))
- (scheduled-day (and scheduled (org-time-string-to-absolute scheduled))))
+ (todo-state (org-get-todo-state)))
(if (or (not todo-state) ; no todo keyword
- (member todo-state org-done-keywords) ; done/completed tasks
- (string= style "habit"))
- subtree-end ; skip if done or habit
- (let ((overdue (or (and deadline-day (< deadline-day today))
- (and scheduled-day (< scheduled-day today)))))
+ (member todo-state org-done-keywords)) ; done/completed tasks
+ subtree-end ; skip if done
+ ;; Only check dates for non-done tasks
+ (let* ((deadline (org-entry-get nil "DEADLINE"))
+ (scheduled (org-entry-get nil "SCHEDULED"))
+ (today (or cj/org-agenda-today-absolute
+ (org-time-string-to-absolute (format-time-string "%Y-%m-%d"))))
+ (deadline-day (and deadline (org-time-string-to-absolute deadline)))
+ (scheduled-day (and scheduled (org-time-string-to-absolute scheduled)))
+ (overdue (or (and deadline-day (< deadline-day today))
+ (and scheduled-day (< scheduled-day today)))))
(if overdue
nil ; do not skip, keep this entry
subtree-end))))) ; skip if not overdue
(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)))
+ ;; org-get-priority returns value * 1000, so we must match that scale
(pri-value (* 1000 (- org-lowest-priority priority)))
(pri-current (org-get-priority (thing-at-point 'line t))))
(if (= pri-value pri-current)
@@ -187,14 +368,13 @@ 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)
subtree-end
nil)))
-(setq org-agenda-custom-commands
+(setopt org-agenda-custom-commands
'(("d" "Daily Agenda with Tasks"
((alltodo ""
((org-agenda-skip-function #'cj/org-agenda-skip-subtree-if-not-overdue)
@@ -206,8 +386,9 @@ KEYWORDS must be a list of strings."
(org-agenda-prefix-format " %i %-15:c%?-15t% s")))
(agenda ""
((org-agenda-start-day "0d")
- (org-agenda-span 8)
+ (org-agenda-span 7) ;; 7-day view
(org-agenda-start-on-weekday nil)
+ (org-agenda-include-diary nil) ;; explicitly disable diary in this view
(org-agenda-overriding-header cj/main-agenda-schedule-title)
(org-agenda-prefix-format " %i %-15:c%?-15t% s")))
(alltodo ""
@@ -222,38 +403,63 @@ KEYWORDS must be a list of strings."
((org-agenda-compact-blocks nil)))))
-(defun cj/main-agenda-display ()
+;;;###autoload
+(defun cj/main-agenda-display (&optional force-rebuild)
"Display the main daily org-agenda view.
-
-This uses all org-agenda targets and presents three sections:
+With prefix argument FORCE-REBUILD, forces a complete rebuild of the
+agenda file list instead of using the cache.
+This uses all org-agenda targets and presents four sections:
+- Overdue tasks (past scheduled/deadline dates)
- All unfinished priority A tasks
-- Today's schedule, including habits with consistency graphs
-- All priority B and C unscheduled/undeadlined tasks
+- Today's schedule (7-8 days), including habits
+- All priority B unscheduled/undeadlined tasks
-The agenda is rebuilt from all sources before display, including:
-- inbox-file and schedule-file
-- Org-roam nodes tagged as \"Project\"
-- All todo.org files in projects-dir and code-dir"
- (interactive)
- (cj/build-org-agenda-list)
- (org-agenda "a" "d"))
-(global-set-key (kbd "<f8>") #'cj/main-agenda-display)
+The agenda file list is cached and rebuilt automatically every
+`cj/org-agenda-rebuild-interval' seconds (default 5 minutes).
+
+Performance: Temporarily increases GC threshold during generation to reduce
+GC pauses, then restores it after 10 seconds.
+
+The agenda is built from all sources including:
+- inbox-file, schedule-file, and gcal-file
+- All todo.org files in projects-dir"
+ (interactive "P")
+ ;; Save current GC threshold and temporarily disable GC during agenda generation
+ (let ((gc-cons-threshold-original gc-cons-threshold))
+ (setq gc-cons-threshold most-positive-fixnum)
+
+ ;; Generate the agenda
+ (cj/build-org-agenda-list force-rebuild)
+ (org-agenda "a" "d")
+
+ ;; Restore GC threshold after 10 seconds (gives user time to view agenda)
+ (run-at-time 10 nil
+ (lambda ()
+ (setq gc-cons-threshold gc-cons-threshold-original)
+ (garbage-collect)))))
+
+;;;###autoload (keymap-global-set "<f8>" #'cj/main-agenda-display)
+
+;; Set keybindings immediately when module loads
+(keymap-global-set "<f8>" #'cj/main-agenda-display)
+(keymap-global-set "C-<f8>" #'cj/todo-list-all-agenda-files)
+(keymap-global-set "M-<f8>" #'cj/todo-list-from-this-buffer)
;; ------------------------- Add Timestamp To Org Entry ------------------------
;; simply adds a timestamp to put the org entry on an agenda
+(defvar cj/timeformat "%Y-%m-%d %a"
+ "Time format for org entry timestamps.")
+
(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")
(org-end-of-line)
(save-excursion
(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