dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

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