org-choose.el (14183B)
1 ;;; org-choose.el --- decision management for org-mode 2 3 ;; Copyright (C) 2009-2014, 2021 Tom Breton (Tehom) 4 5 ;; This file is not part of GNU Emacs. 6 7 ;; Author: Tom Breton (Tehom) 8 ;; Keywords: outlines, convenience 9 10 ;; This file is free software; you can redistribute it and/or modify 11 ;; it under the terms of the GNU General Public License as published by 12 ;; the Free Software Foundation; either version 3, or (at your option) 13 ;; any later version. 14 15 ;; This file is distributed in the hope that it will be useful, 16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 ;; GNU General Public License for more details. 19 20 ;; You should have received a copy of the GNU General Public License 21 ;; along with GNU Emacs; see the file COPYING. If not, write to 22 ;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, 23 ;; Boston, MA 02111-1307, USA. 24 25 ;;; Commentary: 26 27 ;; This is code to support decision management. It lets you treat a 28 ;; group of sibling items in org-mode as alternatives in a decision. 29 30 ;; There are no user commands in this file. You use it by: 31 ;; * Loading it (manually or by M-x customize-apropos org-modules) 32 33 ;; * Setting up at least one set of TODO keywords with the 34 ;; interpretation "choose" by either: 35 36 ;; * Using the file directive #+CHOOSE_TODO: 37 38 ;; * For instance, "#+CHOOSE_TODO: NO(,-) MAYBE(,0) YES" 39 40 ;; * Or by M-x customize-apropos org-todo-keywords 41 42 ;; * Operating on single items with the TODO commands. 43 44 ;; * Use C-S-right to change the keyword set. Use this to change to 45 ;; the "choose" keyword set that you just defined. 46 47 ;; * Use S-right to advance the TODO mark to the next setting. 48 49 ;; For "choose", that means you like this alternative more than 50 ;; before. Other alternatives will be automatically demoted to 51 ;; keep your settings consistent. 52 53 ;; * Use S-left to demote TODO to the previous setting. 54 55 ;; For "choose", that means you don't like this alternative as much 56 ;; as before. Other alternatives will be automatically promoted, 57 ;; if this item was all that was keeping them down. 58 59 ;; * All the other TODO commands are available and behave essentially 60 ;; the normal way. 61 62 ;;; Requires 63 64 (require 'org) 65 ;(eval-when-compile 66 ; (require 'cl)) 67 (require 'cl) 68 69 ;;; Body 70 ;;; The variables 71 72 (defstruct (org-choose-mark-data. (:type list)) 73 "The format of an entry in org-choose-mark-data. 74 Indexes are 0-based or `nil'. 75 " 76 keyword 77 bot-lower-range 78 top-upper-range 79 range-length 80 static-default 81 all-keywords) 82 83 (defvar org-choose-mark-data 84 () 85 "Alist of information for choose marks. 86 87 Each entry is an `org-choose-mark-data.'" ) 88 (make-variable-buffer-local 'org-choose-mark-data) 89 ;;;_ , For setup 90 ;;;_ . org-choose-filter-one 91 92 (defun org-choose-filter-one (i) 93 "Return a list of 94 * a canonized version of the string 95 * optionally one symbol" 96 97 (if 98 (not 99 (string-match "(.*)" i)) 100 (list i i) 101 (let* 102 ( 103 (end-text (match-beginning 0)) 104 (vanilla-text (substring i 0 end-text)) 105 ;;Get the parenthesized part. 106 (match (match-string 0 i)) 107 ;;Remove the parentheses. 108 (args (substring match 1 -1)) 109 ;;Split it 110 (arglist 111 (let 112 ((arglist-x (org-split-string args ","))) 113 ;;When string starts with "," `split-string' doesn't 114 ;;make a first arg, so in that case make one 115 ;;manually. 116 (if 117 (string-match "^," args) 118 (cons nil arglist-x) 119 arglist-x))) 120 (decision-arg (second arglist)) 121 (type 122 (cond 123 ((string= decision-arg "0") 124 'default-mark) 125 ((string= decision-arg "+") 126 'top-upper-range) 127 ((string= decision-arg "-") 128 'bot-lower-range) 129 (t nil))) 130 (vanilla-arg (first arglist)) 131 (vanilla-mark 132 (if vanilla-arg 133 (concat vanilla-text "("vanilla-arg")") 134 vanilla-text))) 135 (if type 136 (list vanilla-text vanilla-mark type) 137 (list vanilla-text vanilla-mark))))) 138 139 ;;;_ . org-choose-setup-vars 140 (defun org-choose-setup-vars (bot-lower-range top-upper-range 141 static-default num-items all-mark-texts) 142 "Add to org-choose-mark-data according to arguments" 143 (let* 144 ((tail 145 ;;If there's no bot-lower-range or no default, we don't 146 ;;have ranges. 147 (cdr 148 (if (and static-default bot-lower-range) 149 (let* 150 ;;If there's no top-upper-range, use the last 151 ;;item. 152 ((top-upper-range 153 (or top-upper-range (1- num-items))) 154 (lower-range-length 155 (1+ (- static-default bot-lower-range))) 156 (upper-range-length 157 (- top-upper-range static-default)) 158 (range-length 159 (min upper-range-length lower-range-length))) 160 (make-org-choose-mark-data. 161 :keyword nil 162 :bot-lower-range bot-lower-range 163 :top-upper-range top-upper-range 164 :range-length range-length 165 :static-default static-default 166 :all-keywords all-mark-texts)) 167 (make-org-choose-mark-data. 168 :keyword nil 169 :bot-lower-range nil 170 :top-upper-range nil 171 :range-length nil 172 :static-default (or static-default 0) 173 :all-keywords all-mark-texts))))) 174 (dolist (text all-mark-texts) 175 (pushnew (cons text tail) 176 org-choose-mark-data 177 :test 178 (lambda (a b) 179 (equal (car a) (car b))))))) 180 181 ;;; org-choose-filter-tail 182 (defun org-choose-filter-tail (raw) 183 "Return a translation of RAW to vanilla and set appropriate 184 buffer-local variables. 185 186 RAW is a list of strings representing the input text of a choose 187 interpretation." 188 (let 189 ((vanilla-list nil) 190 (all-mark-texts nil) 191 (index 0) 192 bot-lower-range top-upper-range range-length static-default) 193 (dolist (i raw) 194 (destructuring-bind 195 (vanilla-text vanilla-mark &optional type) 196 (org-choose-filter-one i) 197 (cond 198 ((eq type 'bot-lower-range) 199 (setq bot-lower-range index)) 200 ((eq type 'top-upper-range) 201 (setq top-upper-range index)) 202 ((eq type 'default-mark) 203 (setq static-default index))) 204 (cl-incf index) 205 (push vanilla-text all-mark-texts) 206 (push vanilla-mark vanilla-list))) 207 208 (org-choose-setup-vars bot-lower-range top-upper-range 209 static-default index (reverse all-mark-texts)) 210 (nreverse vanilla-list))) 211 212 ;;; org-choose-setup-filter 213 214 (defun org-choose-setup-filter (raw) 215 "A setup filter for choose interpretations." 216 (when (eq (car raw) 'choose) 217 (cons 218 'choose 219 (org-choose-filter-tail (cdr raw))))) 220 221 ;;; org-choose-conform-after-promotion 222 (defun org-choose-conform-after-promotion (entry-pos keywords highest-ok-ix) 223 "Conform the current item after another item was promoted" 224 (unless 225 ;;Skip the entry that triggered this by skipping any entry with 226 ;;the same starting position. plist uses the start of the 227 ;;header line as the position, but map no longer does, so we 228 ;;have to go back to the heading. 229 (= 230 (save-excursion 231 (org-back-to-heading) 232 (point)) 233 entry-pos) 234 (let 235 ((ix 236 (org-choose-get-entry-index keywords))) 237 ;;If the index of the entry exceeds the highest allowable 238 ;;index, change it to that. 239 (when (and ix 240 (> ix highest-ok-ix)) 241 (org-todo 242 (nth highest-ok-ix keywords)))))) 243 ;;;_ . org-choose-conform-after-demotion 244 (defun org-choose-conform-after-demotion (entry-pos keywords 245 raise-to-ix 246 old-highest-ok-ix) 247 "Conform the current item after another item was demoted." 248 (unless 249 ;;Skip the entry that triggered this. 250 (= 251 (save-excursion 252 (org-back-to-heading) 253 (point)) 254 entry-pos) 255 (let 256 ((ix 257 (org-choose-get-entry-index keywords))) 258 ;;If the index of the entry was at or above the old allowable 259 ;;position, change it to the new mirror position if there is 260 ;;one. 261 (when (and 262 ix 263 raise-to-ix 264 (>= ix old-highest-ok-ix)) 265 (org-todo 266 (nth raise-to-ix keywords)))))) 267 268 ;;; org-choose-keep-sensible (the org-trigger-hook function) 269 (defun org-choose-keep-sensible (change-plist) 270 "Bring the other items back into a sensible state after an item's 271 setting was changed." 272 (let* 273 ( (from (plist-get change-plist :from)) 274 (to (plist-get change-plist :to)) 275 (entry-pos 276 (set-marker 277 (make-marker) 278 (plist-get change-plist :position))) 279 (kwd-data 280 (assoc to org-todo-kwd-alist))) 281 (when 282 (eq (nth 1 kwd-data) 'choose) 283 (let* 284 ( 285 (data 286 (assoc to org-choose-mark-data)) 287 (keywords 288 (org-choose-mark-data.-all-keywords data)) 289 (old-index 290 (org-choose-get-index-in-keywords 291 from 292 keywords)) 293 (new-index 294 (org-choose-get-index-in-keywords 295 to 296 keywords)) 297 (highest-ok-ix 298 (org-choose-highest-other-ok 299 new-index 300 data)) 301 (funcdata 302 (cond 303 ;;The entry doesn't participate in conformance, 304 ;;so give `nil' which does nothing. 305 ((not highest-ok-ix) nil) 306 ;;The entry was created or promoted 307 ((or 308 (not old-index) 309 (> new-index old-index)) 310 (list 311 #'org-choose-conform-after-promotion 312 entry-pos keywords 313 highest-ok-ix)) 314 (t ;;Otherwise the entry was demoted. 315 (let 316 ( 317 (raise-to-ix 318 (min 319 highest-ok-ix 320 (org-choose-mark-data.-static-default 321 data))) 322 (old-highest-ok-ix 323 (org-choose-highest-other-ok 324 old-index 325 data))) 326 (list 327 #'org-choose-conform-after-demotion 328 entry-pos 329 keywords 330 raise-to-ix 331 old-highest-ok-ix)))))) 332 (if funcdata 333 ;;The funny-looking names are to make variable capture 334 ;;unlikely. (Poor-man's lexical bindings). 335 (destructuring-bind (func-d473 . args-46k) funcdata 336 (let 337 ((map-over-entries 338 (org-choose-get-fn-map-group)) 339 ;;We may call `org-todo', so let various hooks 340 ;;`nil' so we don't cause loops. 341 org-after-todo-state-change-hook 342 org-trigger-hook 343 org-blocker-hook 344 org-todo-get-default-hook 345 ;;Also let this alist `nil' so we don't log 346 ;;secondary transitions. 347 org-todo-log-states) 348 ;;Map over group 349 (funcall map-over-entries 350 (lambda () 351 (apply func-d473 args-46k)))))))) 352 ;;Remove the marker 353 (set-marker entry-pos nil))) 354 355 ;;; Getting the default mark 356 ;;; org-choose-get-index-in-keywords 357 (defun org-choose-get-index-in-keywords (ix all-keywords) 358 "Return the index of the current entry." 359 (if ix 360 (position ix all-keywords 361 :test #'equal))) 362 363 ;;; org-choose-get-entry-index 364 (defun org-choose-get-entry-index (all-keywords) 365 "Return index of current entry." 366 (let* 367 ((state (org-entry-get (point) "TODO"))) 368 (org-choose-get-index-in-keywords state all-keywords))) 369 370 ;;; org-choose-get-fn-map-group 371 372 (defun org-choose-get-fn-map-group () 373 "Return a function to map over the group" 374 (lambda (fn) 375 (require 'org-agenda) ;; `org-map-entries' seems to need it. 376 (save-excursion 377 (unless (org-up-heading-safe) 378 (error "Choosing is only supported between siblings in a tree, not on top level")) 379 (let 380 ((level (org-reduced-level (org-outline-level)))) 381 (save-restriction 382 (org-map-entries 383 fn 384 (format "LEVEL=%d" level) 385 'tree)))))) 386 387 ;;; org-choose-get-highest-mark-index 388 389 (defun org-choose-get-highest-mark-index (keywords) 390 "Get the index of the highest current mark in the group. 391 If there is none, return 0" 392 (let* 393 ;;Func maps over applicable entries. 394 ((map-over-entries 395 (org-choose-get-fn-map-group)) 396 (indexes-list 397 (remove nil 398 (funcall map-over-entries 399 (lambda () 400 (org-choose-get-entry-index keywords)))))) 401 (if 402 indexes-list 403 (apply #'max indexes-list) 404 0))) 405 406 ;;; org-choose-highest-ok 407 408 (defun org-choose-highest-other-ok (ix data) 409 "Return the highest index that any choose mark can sensibly have, 410 given that another mark has index IX. 411 DATA must be a `org-choose-mark-data.'." 412 (let 413 ((bot-lower-range 414 (org-choose-mark-data.-bot-lower-range data)) 415 (top-upper-range 416 (org-choose-mark-data.-top-upper-range data)) 417 (range-length 418 (org-choose-mark-data.-range-length data))) 419 (when (and ix bot-lower-range) 420 (let* 421 ((delta 422 (- top-upper-range ix))) 423 (unless 424 (< range-length delta) 425 (+ bot-lower-range delta)))))) 426 427 ;;; org-choose-get-default-mark-index 428 429 (defun org-choose-get-default-mark-index (data) 430 "Return the index of the default mark in a choose interpretation. 431 432 DATA must be a `org-choose-mark-data.'." 433 (or 434 (let 435 ((highest-mark-index 436 (org-choose-get-highest-mark-index 437 (org-choose-mark-data.-all-keywords data)))) 438 (org-choose-highest-other-ok 439 highest-mark-index data)) 440 (org-choose-mark-data.-static-default data))) 441 442 ;;; org-choose-get-mark-N 443 (defun org-choose-get-mark-N (n data) 444 "Get the text of the nth mark in a choose interpretation." 445 446 (let* 447 ((l (org-choose-mark-data.-all-keywords data))) 448 (nth n l))) 449 450 ;;; org-choose-get-default-mark 451 452 (defun org-choose-get-default-mark (new-mark old-mark) 453 "Get the default mark IFF in a choose interpretation. 454 NEW-MARK and OLD-MARK are the text of the new and old marks." 455 (let* 456 ((old-kwd-data 457 (assoc old-mark org-todo-kwd-alist)) 458 (new-kwd-data 459 (assoc new-mark org-todo-kwd-alist)) 460 (becomes-choose 461 (and 462 (or 463 (not old-kwd-data) 464 (not 465 (eq (nth 1 old-kwd-data) 'choose))) 466 (eq (nth 1 new-kwd-data) 'choose)))) 467 (when 468 becomes-choose 469 (let 470 ((new-mark-data 471 (assoc new-mark org-choose-mark-data))) 472 (if 473 new-mark 474 (org-choose-get-mark-N 475 (org-choose-get-default-mark-index 476 new-mark-data) 477 new-mark-data) 478 (error "Somehow got an unrecognizable mark")))))) 479 480 ;;; Setting it all up 481 482 (eval-after-load 'org 483 '(progn 484 (add-to-list 'org-todo-setup-filter-hook 485 #'org-choose-setup-filter) 486 (add-to-list 'org-todo-get-default-hook 487 #'org-choose-get-default-mark) 488 (add-to-list 'org-trigger-hook 489 #'org-choose-keep-sensible) 490 (add-to-list 'org-todo-interpretation-widgets 491 '(:tag "Choose (to record decisions)" choose) 492 'append))) 493 494 (provide 'org-choose) 495 496 ;;; org-choose.el ends here