diff options
| author | Craig Jennings <c@cjennings.net> | 2025-10-12 11:47:26 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-10-12 11:47:26 -0500 |
| commit | 092304d9e0ccc37cc0ddaa9b136457e56a1cac20 (patch) | |
| tree | ea81999b8442246c978b364dd90e8c752af50db5 /modules/org-agenda-config.el | |
changing repositories
Diffstat (limited to 'modules/org-agenda-config.el')
| -rw-r--r-- | modules/org-agenda-config.el | 290 |
1 files changed, 290 insertions, 0 deletions
diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el new file mode 100644 index 00000000..b2cb1c10 --- /dev/null +++ b/modules/org-agenda-config.el @@ -0,0 +1,290 @@ +;;; org-agenda-config --- Org-Agenda/Todo Config -*- lexical-binding: t; coding: utf-8; -*- +;; author: Craig Jennings <c@cjennings.net> +;; +;;; Commentary: +;; +;; Agenda views are tied to the F8 (fate) key. +;; +;; "We are what we repeatedly do. +;; Excellence, then, is not an act, but a habit" +;; -- Aristotle +;; +;; "...watch your actions, they become habits; +;; watch your habits, they become character; +;; watch your character, for it becomes your destiny." +;; -- Lao Tzu +;; +;; +;; 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. +;; +;; M-f8 - TASK LIST containing all tasks from just the current org-mode buffer. +;; +;; 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. + +;;; 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 + (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))) + + ;; 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))) + + ;; build org-agenda-list for the first time after emacs init completes. + (add-hook 'emacs-startup-hook #'cj/build-org-agenda-list)) + +;; ------------------------ 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. + +DIRECTORY is a string of the path to begin the search." + (interactive "D") + (setq org-agenda-files + (append (directory-files-recursively directory + "^[Tt][Oo][Dd][Oo]\\.[Oo][Rr][Gg]$" t) + 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. +Reports elapsed time in the messages buffer." + (interactive) + (let ((start-time (current-time))) + ;; reset org-agenda-files to inbox-file, schedule-file, contacts-file + (setq org-agenda-files (list inbox-file schedule-file gcal-file contacts-file)) + + (cj/add-files-to-org-agenda-files-list projects-dir) + (cj/add-files-to-org-agenda-files-list code-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))) + +(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) + (org-agenda "a" "t")) +(global-set-key (kbd "C-<f8>") #'cj/todo-list-all-agenda-files) + +;; ------------------------- Agenda List Current Buffer ------------------------ +;; an agenda listing tasks from just the current buffer. + +(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) + (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) + +;; -------------------------------- Main Agenda -------------------------------- +;; my custom agenda command from all available agenda targets. adapted from: +;; https://blog.aaronbieber.com/2016/09/24/an-agenda-for-life-with-org-mode.html + +(defvar cj/main-agenda-hipri-title "HIGH PRIORITY UNRESOLVED TASKS" + "String to announce the high priority section of the main agenda.") + +(defvar cj/main-agenda-overdue-title "OVERDUE" + "String to announce the overdue section of the main agenda.") + +(defvar cj/main-agenda-schedule-title "SCHEDULE" + "String to announce the schedule section of the main agenda.") + +(defvar cj/main-agenda-tasks-title "PRIORITY B" + "String to announce the schedule section of the main agenda.") + +(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)))) + (if (string= (org-entry-get nil "STYLE") "habit") + subtree-end + nil))) + +(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))) + (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)))) + (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))))) + (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))) + (pri-value (* 1000 (- org-lowest-priority priority))) + (pri-current (org-get-priority (thing-at-point 'line t)))) + (if (= pri-value pri-current) + subtree-end + nil))) + +(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 + '(("d" "Daily Agenda with Tasks" + ((alltodo "" + ((org-agenda-skip-function #'cj/org-agenda-skip-subtree-if-not-overdue) + (org-agenda-overriding-header cj/main-agenda-overdue-title) + (org-agenda-prefix-format " %i %-15:c%?-15t% s"))) + (tags "PRIORITY=\"A\"" + ((org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done)) + (org-agenda-overriding-header cj/main-agenda-hipri-title) + (org-agenda-prefix-format " %i %-15:c%?-15t% s"))) + (agenda "" + ((org-agenda-start-day "0d") + (org-agenda-span 8) + (org-agenda-start-on-weekday nil) + (org-agenda-overriding-header cj/main-agenda-schedule-title) + (org-agenda-prefix-format " %i %-15:c%?-15t% s"))) + (alltodo "" + ((org-agenda-skip-function '(or (cj/org-skip-subtree-if-habit) + (cj/org-skip-subtree-if-priority ?A) + (cj/org-skip-subtree-if-priority ?C) + (cj/org-skip-subtree-if-priority ?D) + (cj/org-skip-subtree-if-keyword '("PROJECT")) + (org-agenda-skip-if nil '(scheduled deadline)))) + (org-agenda-overriding-header cj/main-agenda-tasks-title) + (org-agenda-prefix-format " %i %-15:c%?-15t% s")))) + ((org-agenda-compact-blocks nil))))) + + +(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\" +- 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) + +;; ------------------------- Add Timestamp To Org Entry ------------------------ +;; simply adds a timestamp to put the org entry on an agenda + +(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 + +(use-package alert + :config + (setq alert-fade-time 10) ;; seconds to vanish alert + (setq alert-default-style 'libnotify)) ;; works well with dunst + +(use-package org-alert + :after alert org-agenda + :commands (org-alert-enable org-alert-check) + :bind + ("C-c A" . org-alert-check) + :config + ;; Set org-alert settings + (setq org-alert-interval 300) ;; seconds between agenda checks (5 minutes) + (setq org-alert-notify-cutoff 10) ;; minutes before a deadline to notify + (setq org-alert-notify-after-event-cutoff 5) ;; stop alerts 5 mins after deadline + (setq org-alert-notification-title "Reminder")) + +;; Enable org-alert timer with message +(defun cj/org-alert-enable-with-message () + (org-alert-enable) + (message "org-alert timer enabled with interval %d seconds" org-alert-interval)) + +;; Alert when idle post Emacs startup +(add-hook 'emacs-startup-hook + (lambda () + (run-with-idle-timer 1 nil #'cj/org-alert-enable-with-message))) + + +(provide 'org-agenda-config) +;;; org-agenda-config.el ends here |
