| 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 |
|---|