1 ;;;_ org-choose.el --- decision management for org-mode
5 ;; Copyright (C) 2009 Tom Breton (Tehom)
7 ;; Author: Tom Breton (Tehom)
8 ;; Keywords: outlines, convenience
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 2, or (at your option)
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.
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.
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.
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)
33 ;; * Setting up at least one set of TODO keywords with the
34 ;; interpretation "choose" by either:
36 ;; * Using the file directive #+CHOOSE_TODO:
38 ;; * For instance, "#+CHOOSE_TODO: NO(,-) MAYBE(,0) YES"
40 ;; * Or by M-x customize-apropos org-todo-keywords
42 ;; * Operating on single items with the TODO commands.
44 ;; * Use C-S-right to change the keyword set. Use this to change to
45 ;; the "choose" keyword set that you just defined.
47 ;; * Use S-right to advance the TODO mark to the next setting.
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.
53 ;; * Use S-left to demote TODO to the previous setting.
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.
59 ;; * All the other TODO commands are available and behave essentially
73 (defstruct (org-choose-mark-data.
(:type list
))
74 "The format of an entry in org-choose-mark-data.
75 Indexes are 0-based or `nil'.
84 (defvar org-choose-mark-data
86 "Alist of information for choose marks.
88 Each entry is an `org-choose-mark-data.'" )
89 (make-variable-buffer-local 'org-choose-mark-data
)
91 ;;;_ . org-choose-filter-one
93 (defun org-choose-filter-one (i)
95 * a canonized version of the string
96 * optionally one symbol"
100 (string-match "(.*)" i
))
104 (end-text (match-beginning 0))
105 (vanilla-text (substring i
0 end-text
))
106 ;;Get the parenthesized part.
107 (match (match-string 0 i
))
108 ;;Remove the parentheses.
109 (args (substring match
1 -
1))
113 ((arglist-x (org-split-string args
",")))
114 ;;When string starts with "," `split-string' doesn't
115 ;;make a first arg, so in that case make one
118 (string-match "^," args
)
121 (decision-arg (second arglist
))
124 ((string= decision-arg
"0")
126 ((string= decision-arg
"+")
128 ((string= decision-arg
"-")
131 (vanilla-arg (first arglist
))
134 (concat vanilla-text
"("vanilla-arg
")")
137 (list vanilla-text vanilla-mark type
)
138 (list vanilla-text vanilla-mark
)))))
140 ;;;_ . org-choose-setup-vars
141 (defun org-choose-setup-vars (bot-lower-range top-upper-range
142 static-default num-items all-mark-texts
)
143 "Add to org-choose-mark-data according to arguments"
148 ;;If there's no bot-lower-range or no default, we don't
151 (if (and static-default bot-lower-range
)
154 ;;If there's no top-upper-range, use the last
157 (or top-upper-range
(1- num-items
)))
159 (1+ (- static-default bot-lower-range
)))
161 (- top-upper-range static-default
))
163 (min upper-range-length lower-range-length
)))
166 (make-org-choose-mark-data.
168 :bot-lower-range bot-lower-range
169 :top-upper-range top-upper-range
170 :range-length range-length
171 :static-default static-default
172 :all-keywords all-mark-texts
))
174 (make-org-choose-mark-data.
179 :static-default
(or static-default
0)
180 :all-keywords all-mark-texts
)))))
182 (dolist (text all-mark-texts
)
183 (pushnew (cons text tail
)
187 (equal (car a
) (car b
)))))))
192 ;;;_ . org-choose-filter-tail
193 (defun org-choose-filter-tail (raw)
194 "Return a translation of RAW to vanilla and set appropriate
195 buffer-local variables.
197 RAW is a list of strings representing the input text of a choose
203 bot-lower-range top-upper-range range-length static-default
)
206 (vanilla-text vanilla-mark
&optional type
)
207 (org-choose-filter-one i
)
209 ((eq type
'bot-lower-range
)
210 (setq bot-lower-range index
))
211 ((eq type
'top-upper-range
)
212 (setq top-upper-range index
))
213 ((eq type
'default-mark
)
214 (setq static-default index
)))
216 (push vanilla-text all-mark-texts
)
217 (push vanilla-mark vanilla-list
)))
219 (org-choose-setup-vars bot-lower-range top-upper-range
220 static-default index
(reverse all-mark-texts
))
221 (nreverse vanilla-list
)))
223 ;;;_ . org-choose-setup-filter
225 (defun org-choose-setup-filter (raw)
226 "A setup filter for choose interpretations."
227 (when (eq (car raw
) 'choose
)
230 (org-choose-filter-tail (cdr raw
)))))
232 ;;;_ . org-choose-conform-after-promotion
233 (defun org-choose-conform-after-promotion (entry-pos keywords highest-ok-ix
)
234 "Conform the current item after another item was promoted"
237 ;;Skip the entry that triggered this by skipping any entry with
238 ;;the same starting position. Both map and plist use the start
239 ;;of the header line as the position, so we can just compare
241 (= (point) entry-pos
)
244 (org-choose-get-entry-index keywords
)))
245 ;;If the index of the entry exceeds the highest allowable
246 ;;index, change it to that.
248 (> ix highest-ok-ix
))
250 (nth highest-ok-ix keywords
))))))
251 ;;;_ . org-choose-conform-after-demotion
252 (defun org-choose-conform-after-demotion (entry-pos keywords
255 "Conform the current item after another item was demoted."
258 ;;Skip the entry that triggered this.
259 (= (point) entry-pos
)
262 (org-choose-get-entry-index keywords
)))
263 ;;If the index of the entry was at or above the old allowable
264 ;;position, change it to the new mirror position if there is
269 (>= ix old-highest-ok-ix
))
271 (nth raise-to-ix keywords
))))))
273 ;;;_ , org-choose-keep-sensible (the org-trigger-hook function)
274 (defun org-choose-keep-sensible (change-plist)
275 "Bring the other items back into a sensible state after an item's
276 setting was changed."
278 ( (from (plist-get change-plist
:from
))
279 (to (plist-get change-plist
:to
))
283 (plist-get change-plist
:position
)))
285 (assoc to org-todo-kwd-alist
)))
287 (eq (nth 1 kwd-data
) 'choose
)
291 (assoc to org-choose-mark-data
))
293 (org-choose-mark-data.-all-keywords data
))
295 (org-choose-get-index-in-keywords
299 (org-choose-get-index-in-keywords
303 (org-choose-highest-other-ok
308 ;;The entry doesn't participate in conformance,
309 ;;so give `nil' which does nothing.
310 ((not highest-ok-ix
) nil
)
311 ;;The entry was created or promoted
314 (> new-index old-index
))
316 #'org-choose-conform-after-promotion
319 (t ;;Otherwise the entry was demoted.
325 (org-choose-mark-data.-static-default
328 (org-choose-highest-other-ok
333 #'org-choose-conform-after-demotion
337 old-highest-ok-ix
))))))
340 ;;The funny-looking names are to make variable capture
341 ;;unlikely. (Poor-man's lexical bindings).
342 (destructuring-bind (func-d473 . args-46k
) funcdata
345 (org-choose-get-fn-map-group))
346 ;;We may call `org-todo', so let various hooks
347 ;;`nil' so we don't cause loops.
348 org-after-todo-state-change-hook
351 org-todo-get-default-hook
352 ;;Also let this alist `nil' so we don't log
353 ;;secondary transitions.
356 (funcall map-over-entries
358 (apply func-d473 args-46k
))))))))
361 (set-marker entry-pos nil
)))
365 ;;;_ , Getting the default mark
366 ;;;_ . org-choose-get-index-in-keywords
367 (defun org-choose-get-index-in-keywords (ix all-keywords
)
368 "Return the index of the current entry."
371 (position ix all-keywords
374 ;;;_ . org-choose-get-entry-index
375 (defun org-choose-get-entry-index (all-keywords)
376 "Return index of current entry."
379 ((state (org-entry-get (point) "TODO")))
380 (org-choose-get-index-in-keywords state all-keywords
)))
382 ;;;_ . org-choose-get-fn-map-group
384 (defun org-choose-get-fn-map-group ()
385 "Return a function to map over the group"
388 (require 'org-agenda
) ;; `org-map-entries' seems to need it.
390 (unless (org-up-heading-safe)
391 (error "Chosing is only supported between siblings in a tree, not on top level"))
393 ((level (org-reduced-level (org-outline-level))))
397 (format "LEVEL=%d" level
)
400 ;;;_ . org-choose-get-highest-mark-index
402 (defun org-choose-get-highest-mark-index (keywords)
403 "Get the index of the highest current mark in the group.
404 If there is none, return 0"
408 ;;Func maps over applicable entries.
410 (org-choose-get-fn-map-group))
414 (funcall map-over-entries
416 (org-choose-get-entry-index keywords
))))))
419 (apply #'max indexes-list
)
423 ;;;_ . org-choose-highest-ok
425 (defun org-choose-highest-other-ok (ix data
)
426 "Return the highest index that any choose mark can sensibly have,
427 given that another mark has index IX.
428 DATA must be a `org-choose-mark-data.'."
433 (org-choose-mark-data.-bot-lower-range data
))
435 (org-choose-mark-data.-top-upper-range data
))
437 (org-choose-mark-data.-range-length data
)))
438 (when (and ix bot-lower-range
)
441 (- top-upper-range ix
)))
443 (< range-length delta
)
444 (+ bot-lower-range delta
))))))
446 ;;;_ . org-choose-get-default-mark-index
448 (defun org-choose-get-default-mark-index (data)
449 "Return the index of the default mark in a choose interpretation.
451 DATA must be a `org-choose-mark-data.'."
457 (org-choose-get-highest-mark-index
458 (org-choose-mark-data.-all-keywords data
))))
459 (org-choose-highest-other-ok
460 highest-mark-index data
))
461 (org-choose-mark-data.-static-default data
)))
465 ;;;_ . org-choose-get-mark-N
466 (defun org-choose-get-mark-N (n data
)
467 "Get the text of the nth mark in a choose interpretation."
470 ((l (org-choose-mark-data.-all-keywords data
)))
473 ;;;_ . org-choose-get-default-mark
475 (defun org-choose-get-default-mark (new-mark old-mark
)
476 "Get the default mark IFF in a choose interpretation.
477 NEW-MARK and OLD-MARK are the text of the new and old marks."
482 (assoc old-mark org-todo-kwd-alist
))
484 (assoc new-mark org-todo-kwd-alist
))
490 (eq (nth 1 old-kwd-data
) 'choose
)))
491 (eq (nth 1 new-kwd-data
) 'choose
))))
496 (assoc new-mark org-choose-mark-data
)))
499 (org-choose-get-mark-N
500 (org-choose-get-default-mark-index
503 (error "Somehow got an unrecognizable mark"))))))
505 ;;;_ , Setting it all up
507 (eval-after-load "org"
509 (add-to-list 'org-todo-setup-filter-hook
510 #'org-choose-setup-filter
)
511 (add-to-list 'org-todo-get-default-hook
512 #'org-choose-get-default-mark
)
513 (add-to-list 'org-trigger-hook
514 #'org-choose-keep-sensible
)
515 (add-to-list 'org-todo-interpretation-widgets
516 '(:tag
"Choose (to record decisions)" choose
)
524 (provide 'org-choose
)
526 ;;;_ * Local emacs vars.
527 ;;;_ + Local variables:
531 ;;; org-choose.el ends here