summaryrefslogtreecommitdiff
path: root/modules/org-agenda-config.el
blob: c7aac99b061f8578eb79cc9e81d0fb422febdda0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
;;; 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)
  :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, 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)))

(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

;; 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
  :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))

  ;; Modeline display: show upcoming events within 60 minutes
  (setq chime-modeline-lookahead 120)
  (setq chime-modeline-format " ⏰ %s")

  ;; Chime sound: plays when notifications appear
  (setq chime-play-sound t)
  ;; Uses bundled chime.wav by default

  ;; Notification settings
  (setq chime-notification-title "Reminder")
  (setq chime-alert-severity 'medium)

  ;; Don't filter by TODO keywords - notify for all events with timestamps
  (setq chime-keyword-whitelist nil)
  (setq chime-keyword-blacklist nil)

  ;; Only notify for non-done items (default behavior)
  (setq chime-predicate-blacklist
        '(chime-done-keywords-predicate))

  ;; Enable chime-mode automatically
  (chime-mode 1))


(provide 'org-agenda-config)
;;; org-agenda-config.el ends here