1 | ;;; org-autofocus.el --- Mark Forster's Auto Focus system for Org -*- lexical-binding: t; -*- |
---|
2 | |
---|
3 | ;; Copyright (C) 2020 David Owen |
---|
4 | |
---|
5 | ;; Author: David Owen <dsowen@fugue88.ws> |
---|
6 | ;; Keywords: autofocus |
---|
7 | ;; Package-Requires: (org) |
---|
8 | |
---|
9 | ;;; Commentary: |
---|
10 | |
---|
11 | ;; This minor mode works primarily by lightly analyzing the Org Agenda todo list |
---|
12 | ;; and adding some extra text properties, and by the addition of a TOUCHED |
---|
13 | ;; property to tasks' property drawers. |
---|
14 | ;; |
---|
15 | ;; Tasks are identified by looking at the beginning of a line for the TYPE |
---|
16 | ;; property that Org adds. The code does /not/ verify that the property's value |
---|
17 | ;; is "todo". It's assumed that a task runs from the beginning of a line to the |
---|
18 | ;; end of the same line. |
---|
19 | ;; |
---|
20 | ;; A task is "dotted" or "marked" by setting the DISPLAY properties of the first |
---|
21 | ;; character of the line to "*". |
---|
22 | ;; |
---|
23 | ;; A range of tasks is hidden by setting the INVISIBLE property of the entire |
---|
24 | ;; line to T. Because this hides the newline, the range of hidden tasks |
---|
25 | ;; actually appears at the beginning of the line for the following unhidden |
---|
26 | ;; task; see ORG-AF--BEG-OF-TASK. |
---|
27 | ;; |
---|
28 | ;; A marked task is dimmed by adding an overlay with FACE set to |
---|
29 | ;; 'ORG-AGENDA-DIMMED-TODO-FACE. |
---|
30 | ;; |
---|
31 | ;; Properties are added as one property representing the state |
---|
32 | ;; (e.g. ORG-AF-MARKED) set to T for easy property testing, and the CATEGORY |
---|
33 | ;; property set to the state (e.g. 'ORG-AF-MARKED) to handle the actual visual |
---|
34 | ;; characteristics. See variable ORG-AF-MARKED and following functions for an |
---|
35 | ;; example. |
---|
36 | ;; |
---|
37 | ;; Tasks are sorted according to the value of the TOUCHED property; |
---|
38 | ;; least-recently-touched tasks are followed by most-recently-touched tasks. |
---|
39 | ;; Any task that doesn't already have the property is touched at the time of |
---|
40 | ;; sorting. |
---|
41 | ;; |
---|
42 | ;; The user may mark a task by pressing "."; all tasks between it and the |
---|
43 | ;; previous marked task will be hidden, and all previous marked tasks dimmed. |
---|
44 | ;; The idea is that hidden and dimmed tasks are not to be manipulated any |
---|
45 | ;; further, until subsquent marked tasks have been handled in some way. |
---|
46 | ;; |
---|
47 | ;; The user may defer a task by pressing "d"; the task will be unmarked, |
---|
48 | ;; touched, and sorted to the end. |
---|
49 | ;; |
---|
50 | ;; Finally, the user may complete a task by pressing "c" or "t"; the task will |
---|
51 | ;; be unmarked, completed by Org's code, touched, and sorted to the end. |
---|
52 | |
---|
53 | ;;; Known issues: |
---|
54 | |
---|
55 | ;; Keymaps and commands are not sufficiently controlled. A user may manipulate |
---|
56 | ;; a task in a way this minor mode doesn't handle. A user may also manipulate |
---|
57 | ;; tasks that are dimmed. |
---|
58 | |
---|
59 | ;; Redo'ing the agenda loses the minor mode; it's necessary to run ORG-AUTOFOCUS |
---|
60 | ;; every time the minor mode is desired. |
---|
61 | |
---|
62 | ;; I haven't yet found a way to enable this minor mode without using a command |
---|
63 | ;; that wraps the call to ORG-AGENDA with some overriding bindings in place. |
---|
64 | ;; This is the primary reason that redo isn't supported. |
---|
65 | |
---|
66 | ;;; Code: |
---|
67 | |
---|
68 | (require 'org-agenda) |
---|
69 | |
---|
70 | |
---|
71 | (defvar org-af-marked 'org-af-marked) |
---|
72 | (put 'org-af-marked 'org-af-marked t) |
---|
73 | (put 'org-af-marked 'display "*") |
---|
74 | |
---|
75 | (defun org-af--mark (point) |
---|
76 | "Mark the task at POINT." |
---|
77 | (let ((o (make-overlay point (1+ point)))) |
---|
78 | (overlay-put o 'category org-af-marked))) |
---|
79 | |
---|
80 | (defun org-af--unmark (point) |
---|
81 | "Unmark the task at POINT." |
---|
82 | (remove-overlays point (1+ point) org-af-marked t)) |
---|
83 | |
---|
84 | (defun org-af--marked-p (point) |
---|
85 | "Test whether the task at POINT is marked." |
---|
86 | (get-char-property point 'org-af-marked)) |
---|
87 | |
---|
88 | |
---|
89 | (defvar org-af-hidden 'org-af-hidden) |
---|
90 | (put 'org-af-hidden 'org-af-hidden t) |
---|
91 | (put 'org-af-hidden 'invisible t) |
---|
92 | |
---|
93 | |
---|
94 | (defvar org-af-dimmed 'org-af-dimmed) |
---|
95 | (put 'org-af-dimmed 'org-af-dimmed t) |
---|
96 | (put 'org-af-dimmed 'face 'org-agenda-dimmed-todo-face) |
---|
97 | (put 'org-af-dimmed 'evaporate t) |
---|
98 | |
---|
99 | (defun org-af--dim (start end) |
---|
100 | "Dim the task from START to END." |
---|
101 | (let ((o (make-overlay start end))) |
---|
102 | (overlay-put o 'category org-af-dimmed))) |
---|
103 | |
---|
104 | (defun org-af--undim (start end) |
---|
105 | "Undim the task from START to END." |
---|
106 | (remove-overlays start end 'org-af-dimmed t)) |
---|
107 | |
---|
108 | (defun org-af--dimmed-p (point) |
---|
109 | "Test whether the task at POINT is dimmed." |
---|
110 | (get-char-property point 'org-af-dimmed)) |
---|
111 | |
---|
112 | |
---|
113 | (defun org-af--beg-of-task () |
---|
114 | "Move point to the beginning of the task at point. |
---|
115 | Skips over any hidden tasks the are on the same line." |
---|
116 | (beginning-of-line) |
---|
117 | (when (get-text-property (point) 'org-af-hidden) |
---|
118 | (goto-char (next-single-property-change (point) 'org-af-hidden)))) |
---|
119 | |
---|
120 | |
---|
121 | (defun org-af--end-of-task (pos) |
---|
122 | "Return the position of the end of the task at POS (excluding end of line)." |
---|
123 | (next-single-property-change pos 'org-hd-marker)) |
---|
124 | |
---|
125 | |
---|
126 | (defun org-af--next-task (from-pos) |
---|
127 | "Return the position of the next visible task, from FROM-POS." |
---|
128 | (setf from-pos (next-single-property-change from-pos 'org-hd-marker)) |
---|
129 | (while (eq (get-text-property from-pos 'org-todo-blocked) 'invisible) |
---|
130 | ;; The EOL doesn't have an ORG-HD-MARKER property, so search twice to skip |
---|
131 | ;; over it. |
---|
132 | (setf from-pos (next-single-property-change from-pos 'org-hd-marker) |
---|
133 | from-pos (next-single-property-change from-pos 'org-hd-marker))) |
---|
134 | from-pos) |
---|
135 | |
---|
136 | |
---|
137 | (defun org-af--first-task () |
---|
138 | "Return the position of the first visible task." |
---|
139 | (org-af--next-task 1)) |
---|
140 | |
---|
141 | |
---|
142 | (defun org-af--prev-mark (pos) |
---|
143 | "Return the position of the first mark previous to POS, or NIL if none are previous." |
---|
144 | (let* ((prev (previous-single-char-property-change pos 'org-af-marked))) |
---|
145 | (if (= prev 1) |
---|
146 | nil |
---|
147 | (1- prev)))) |
---|
148 | |
---|
149 | |
---|
150 | (defun org-af-mark () |
---|
151 | "Mark or unmark the current task, dim the previous marked task if any, and hide any tasks between." |
---|
152 | (interactive) |
---|
153 | (org-af--beg-of-task) |
---|
154 | |
---|
155 | (let ((inhibit-read-only t) |
---|
156 | (task-point (point)) |
---|
157 | marked) |
---|
158 | |
---|
159 | (unless (org-af--dimmed-p task-point) |
---|
160 | (setq marked (not (org-af--marked-p task-point))) |
---|
161 | (if marked |
---|
162 | (org-af--mark task-point) |
---|
163 | (org-af--unmark task-point)) |
---|
164 | |
---|
165 | (save-excursion |
---|
166 | (let ((prev-marked (org-af--prev-mark task-point)) |
---|
167 | hide-from) |
---|
168 | |
---|
169 | (cond |
---|
170 | (prev-marked |
---|
171 | (let ((eot (org-af--end-of-task prev-marked))) |
---|
172 | (if marked |
---|
173 | (org-af--dim prev-marked eot) |
---|
174 | (org-af--undim prev-marked eot)) |
---|
175 | (setf hide-from (org-af--next-task eot)))) |
---|
176 | |
---|
177 | (t |
---|
178 | (setf hide-from (org-af--first-task)))) |
---|
179 | |
---|
180 | (if marked |
---|
181 | (add-text-properties hide-from task-point |
---|
182 | '(category org-af-hidden)) |
---|
183 | (remove-text-properties hide-from task-point |
---|
184 | '(category org-af-hidden)))))))) |
---|
185 | |
---|
186 | |
---|
187 | (defun org-af--timestamp () |
---|
188 | "Return the current local time, with nanoseconds, formatted as an ISO string." |
---|
189 | (format-time-string "%Y-%m-%d %H:%M:%S.%N" (current-time))) |
---|
190 | |
---|
191 | |
---|
192 | (defun org-af-defer () |
---|
193 | "Defer the current task and update the display, unless NO-REDISPLAY." |
---|
194 | (interactive) |
---|
195 | (org-af--beg-of-task) |
---|
196 | (if (org-af--dimmed-p (point)) |
---|
197 | (message "dimmed; skipping") |
---|
198 | (when (org-af--marked-p (point)) |
---|
199 | (org-af-mark)) |
---|
200 | |
---|
201 | ;; This comes from the guts of the org-agenda-set-property command. |
---|
202 | (let* ((hdmarker (or (org-get-at-bol 'org-hd-marker) |
---|
203 | (org-agenda-error))) |
---|
204 | (buffer (marker-buffer hdmarker)) |
---|
205 | (pos (marker-position hdmarker)) |
---|
206 | (inhibit-read-only t) |
---|
207 | (time (org-af--timestamp))) |
---|
208 | (org-with-remote-undo buffer |
---|
209 | (with-current-buffer buffer |
---|
210 | (widen) |
---|
211 | (goto-char pos) |
---|
212 | (org-show-context 'agenda) |
---|
213 | (org-set-property "TOUCHED" time)))) |
---|
214 | |
---|
215 | (save-excursion |
---|
216 | (let* ((inhibit-read-only t) |
---|
217 | (text (delete-and-extract-region (point) |
---|
218 | (1+ (line-end-position))))) |
---|
219 | (goto-char (point-max)) |
---|
220 | (insert text))))) |
---|
221 | |
---|
222 | |
---|
223 | (defun org-af-complete () |
---|
224 | "Complete to current task. |
---|
225 | The task is unmarked (if marked), completed by Org, touched, then |
---|
226 | sorted to the end of the list." |
---|
227 | (interactive) |
---|
228 | (org-af--beg-of-task) |
---|
229 | (when (org-af--marked-p (point)) |
---|
230 | (org-af-mark)) |
---|
231 | (org-agenda-todo) |
---|
232 | (org-af-defer)) |
---|
233 | |
---|
234 | |
---|
235 | (defvar org-autofocus-mode-map (make-sparse-keymap)) |
---|
236 | (define-key org-autofocus-mode-map "." 'org-af-mark) |
---|
237 | (define-key org-autofocus-mode-map "d" 'org-af-defer) |
---|
238 | (define-key org-autofocus-mode-map "c" 'org-af-complete) |
---|
239 | (define-key org-autofocus-mode-map "t" 'org-af-complete) |
---|
240 | |
---|
241 | |
---|
242 | (defun org-af--touched (entry) |
---|
243 | "Touch ENTRY by setting its ``TOUCHED'' property to the current time." |
---|
244 | (let* ((marker (get-text-property 0 'org-hd-marker entry)) |
---|
245 | (touched (org-entry-get marker "TOUCHED"))) |
---|
246 | (unless touched |
---|
247 | (setq touched (org-af--timestamp)) |
---|
248 | (org-entry-put marker "TOUCHED" touched)) |
---|
249 | touched)) |
---|
250 | |
---|
251 | |
---|
252 | (defun org-af--cmp (a b) |
---|
253 | "Compare A to B. |
---|
254 | If A > B, return +1; if A < B, return -1; else, return NIL." |
---|
255 | (let* ((a-touched (org-af--touched a)) |
---|
256 | (b-touched (org-af--touched b)) |
---|
257 | (c (compare-strings a-touched nil nil b-touched nil nil))) |
---|
258 | (cond |
---|
259 | ((eq c t) nil) |
---|
260 | ((< c 0) -1) |
---|
261 | ((> c 0) +1) |
---|
262 | (t (error "%S and %S compared as %S" a-touched b-touched c))))) |
---|
263 | |
---|
264 | |
---|
265 | (define-minor-mode org-autofocus-mode nil nil " AF" org-autofocus-mode-map) |
---|
266 | |
---|
267 | |
---|
268 | ;;;###autoload |
---|
269 | (defun org-autofocus () |
---|
270 | "Display the Org Agenda Todo list in AutoFocus mode." |
---|
271 | (interactive) |
---|
272 | (let ((org-agenda-cmp-user-defined 'org-af--cmp) |
---|
273 | (org-agenda-sorting-strategy '(user-defined-up))) |
---|
274 | (org-agenda nil "t") |
---|
275 | (org-autofocus-mode))) |
---|
276 | |
---|
277 | |
---|
278 | (provide 'org-autofocus) |
---|
279 | ;;; org-autofocus.el ends here |
---|