123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- ;;; org-choose.el --- decision management for org-mode
- ;; Copyright (C) 2009-2014 Tom Breton (Tehom)
- ;; This file is not part of GNU Emacs.
- ;; Author: Tom Breton (Tehom)
- ;; Keywords: outlines, convenience
- ;; This file is free software; you can redistribute it and/or modify
- ;; it under the terms of the GNU General Public License as published by
- ;; the Free Software Foundation; either version 2, or (at your option)
- ;; any later version.
- ;; This file is distributed in the hope that it will be useful,
- ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;; GNU General Public License for more details.
- ;; You should have received a copy of the GNU General Public License
- ;; along with GNU Emacs; see the file COPYING. If not, write to
- ;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
- ;; Boston, MA 02111-1307, USA.
- ;;; Commentary:
- ;; This is code to support decision management. It lets you treat a
- ;; group of sibling items in org-mode as alternatives in a decision.
- ;; There are no user commands in this file. You use it by:
- ;; * Loading it (manually or by M-x customize-apropos org-modules)
- ;; * Setting up at least one set of TODO keywords with the
- ;; interpretation "choose" by either:
- ;; * Using the file directive #+CHOOSE_TODO:
- ;; * For instance, "#+CHOOSE_TODO: NO(,-) MAYBE(,0) YES"
- ;; * Or by M-x customize-apropos org-todo-keywords
- ;; * Operating on single items with the TODO commands.
- ;; * Use C-S-right to change the keyword set. Use this to change to
- ;; the "choose" keyword set that you just defined.
- ;; * Use S-right to advance the TODO mark to the next setting.
- ;; For "choose", that means you like this alternative more than
- ;; before. Other alternatives will be automatically demoted to
- ;; keep your settings consistent.
- ;; * Use S-left to demote TODO to the previous setting.
- ;; For "choose", that means you don't like this alternative as much
- ;; as before. Other alternatives will be automatically promoted,
- ;; if this item was all that was keeping them down.
- ;; * All the other TODO commands are available and behave essentially
- ;; the normal way.
- ;;; Requires
- (require 'org)
- ;(eval-when-compile
- ; (require 'cl))
- (require 'cl)
- ;;; Body
- ;;; The variables
- (defstruct (org-choose-mark-data. (:type list))
- "The format of an entry in org-choose-mark-data.
- Indexes are 0-based or `nil'.
- "
- keyword
- bot-lower-range
- top-upper-range
- range-length
- static-default
- all-keywords)
- (defvar org-choose-mark-data
- ()
- "Alist of information for choose marks.
- Each entry is an `org-choose-mark-data.'" )
- (make-variable-buffer-local 'org-choose-mark-data)
- ;;;_ , For setup
- ;;;_ . org-choose-filter-one
- (defun org-choose-filter-one (i)
- "Return a list of
- * a canonized version of the string
- * optionally one symbol"
- (if
- (not
- (string-match "(.*)" i))
- (list i i)
- (let*
- (
- (end-text (match-beginning 0))
- (vanilla-text (substring i 0 end-text))
- ;;Get the parenthesized part.
- (match (match-string 0 i))
- ;;Remove the parentheses.
- (args (substring match 1 -1))
- ;;Split it
- (arglist
- (let
- ((arglist-x (org-split-string args ",")))
- ;;When string starts with "," `split-string' doesn't
- ;;make a first arg, so in that case make one
- ;;manually.
- (if
- (string-match "^," args)
- (cons nil arglist-x)
- arglist-x)))
- (decision-arg (second arglist))
- (type
- (cond
- ((string= decision-arg "0")
- 'default-mark)
- ((string= decision-arg "+")
- 'top-upper-range)
- ((string= decision-arg "-")
- 'bot-lower-range)
- (t nil)))
- (vanilla-arg (first arglist))
- (vanilla-mark
- (if vanilla-arg
- (concat vanilla-text "("vanilla-arg")")
- vanilla-text)))
- (if type
- (list vanilla-text vanilla-mark type)
- (list vanilla-text vanilla-mark)))))
- ;;;_ . org-choose-setup-vars
- (defun org-choose-setup-vars (bot-lower-range top-upper-range
- static-default num-items all-mark-texts)
- "Add to org-choose-mark-data according to arguments"
- (let*
- ((tail
- ;;If there's no bot-lower-range or no default, we don't
- ;;have ranges.
- (cdr
- (if (and static-default bot-lower-range)
- (let*
- ;;If there's no top-upper-range, use the last
- ;;item.
- ((top-upper-range
- (or top-upper-range (1- num-items)))
- (lower-range-length
- (1+ (- static-default bot-lower-range)))
- (upper-range-length
- (- top-upper-range static-default))
- (range-length
- (min upper-range-length lower-range-length)))
- (make-org-choose-mark-data.
- :keyword nil
- :bot-lower-range bot-lower-range
- :top-upper-range top-upper-range
- :range-length range-length
- :static-default static-default
- :all-keywords all-mark-texts))
- (make-org-choose-mark-data.
- :keyword nil
- :bot-lower-range nil
- :top-upper-range nil
- :range-length nil
- :static-default (or static-default 0)
- :all-keywords all-mark-texts)))))
- (dolist (text all-mark-texts)
- (pushnew (cons text tail)
- org-choose-mark-data
- :test
- #'(lambda (a b)
- (equal (car a) (car b)))))))
- ;;; org-choose-filter-tail
- (defun org-choose-filter-tail (raw)
- "Return a translation of RAW to vanilla and set appropriate
- buffer-local variables.
- RAW is a list of strings representing the input text of a choose
- interpretation."
- (let
- ((vanilla-list nil)
- (all-mark-texts nil)
- (index 0)
- bot-lower-range top-upper-range range-length static-default)
- (dolist (i raw)
- (destructuring-bind
- (vanilla-text vanilla-mark &optional type)
- (org-choose-filter-one i)
- (cond
- ((eq type 'bot-lower-range)
- (setq bot-lower-range index))
- ((eq type 'top-upper-range)
- (setq top-upper-range index))
- ((eq type 'default-mark)
- (setq static-default index)))
- (incf index)
- (push vanilla-text all-mark-texts)
- (push vanilla-mark vanilla-list)))
- (org-choose-setup-vars bot-lower-range top-upper-range
- static-default index (reverse all-mark-texts))
- (nreverse vanilla-list)))
- ;;; org-choose-setup-filter
- (defun org-choose-setup-filter (raw)
- "A setup filter for choose interpretations."
- (when (eq (car raw) 'choose)
- (cons
- 'choose
- (org-choose-filter-tail (cdr raw)))))
- ;;; org-choose-conform-after-promotion
- (defun org-choose-conform-after-promotion (entry-pos keywords highest-ok-ix)
- "Conform the current item after another item was promoted"
- (unless
- ;;Skip the entry that triggered this by skipping any entry with
- ;;the same starting position. plist uses the start of the
- ;;header line as the position, but map no longer does, so we
- ;;have to go back to the heading.
- (=
- (save-excursion
- (org-back-to-heading)
- (point))
- entry-pos)
- (let
- ((ix
- (org-choose-get-entry-index keywords)))
- ;;If the index of the entry exceeds the highest allowable
- ;;index, change it to that.
- (when (and ix
- (> ix highest-ok-ix))
- (org-todo
- (nth highest-ok-ix keywords))))))
- ;;;_ . org-choose-conform-after-demotion
- (defun org-choose-conform-after-demotion (entry-pos keywords
- raise-to-ix
- old-highest-ok-ix)
- "Conform the current item after another item was demoted."
- (unless
- ;;Skip the entry that triggered this.
- (=
- (save-excursion
- (org-back-to-heading)
- (point))
- entry-pos)
- (let
- ((ix
- (org-choose-get-entry-index keywords)))
- ;;If the index of the entry was at or above the old allowable
- ;;position, change it to the new mirror position if there is
- ;;one.
- (when (and
- ix
- raise-to-ix
- (>= ix old-highest-ok-ix))
- (org-todo
- (nth raise-to-ix keywords))))))
- ;;; org-choose-keep-sensible (the org-trigger-hook function)
- (defun org-choose-keep-sensible (change-plist)
- "Bring the other items back into a sensible state after an item's
- setting was changed."
- (let*
- ( (from (plist-get change-plist :from))
- (to (plist-get change-plist :to))
- (entry-pos
- (set-marker
- (make-marker)
- (plist-get change-plist :position)))
- (kwd-data
- (assoc to org-todo-kwd-alist)))
- (when
- (eq (nth 1 kwd-data) 'choose)
- (let*
- (
- (data
- (assoc to org-choose-mark-data))
- (keywords
- (org-choose-mark-data.-all-keywords data))
- (old-index
- (org-choose-get-index-in-keywords
- from
- keywords))
- (new-index
- (org-choose-get-index-in-keywords
- to
- keywords))
- (highest-ok-ix
- (org-choose-highest-other-ok
- new-index
- data))
- (funcdata
- (cond
- ;;The entry doesn't participate in conformance,
- ;;so give `nil' which does nothing.
- ((not highest-ok-ix) nil)
- ;;The entry was created or promoted
- ((or
- (not old-index)
- (> new-index old-index))
- (list
- #'org-choose-conform-after-promotion
- entry-pos keywords
- highest-ok-ix))
- (t ;;Otherwise the entry was demoted.
- (let
- (
- (raise-to-ix
- (min
- highest-ok-ix
- (org-choose-mark-data.-static-default
- data)))
- (old-highest-ok-ix
- (org-choose-highest-other-ok
- old-index
- data)))
- (list
- #'org-choose-conform-after-demotion
- entry-pos
- keywords
- raise-to-ix
- old-highest-ok-ix))))))
- (if funcdata
- ;;The funny-looking names are to make variable capture
- ;;unlikely. (Poor-man's lexical bindings).
- (destructuring-bind (func-d473 . args-46k) funcdata
- (let
- ((map-over-entries
- (org-choose-get-fn-map-group))
- ;;We may call `org-todo', so let various hooks
- ;;`nil' so we don't cause loops.
- org-after-todo-state-change-hook
- org-trigger-hook
- org-blocker-hook
- org-todo-get-default-hook
- ;;Also let this alist `nil' so we don't log
- ;;secondary transitions.
- org-todo-log-states)
- ;;Map over group
- (funcall map-over-entries
- #'(lambda ()
- (apply func-d473 args-46k))))))))
- ;;Remove the marker
- (set-marker entry-pos nil)))
- ;;; Getting the default mark
- ;;; org-choose-get-index-in-keywords
- (defun org-choose-get-index-in-keywords (ix all-keywords)
- "Return the index of the current entry."
- (if ix
- (position ix all-keywords
- :test #'equal)))
- ;;; org-choose-get-entry-index
- (defun org-choose-get-entry-index (all-keywords)
- "Return index of current entry."
- (let*
- ((state (org-entry-get (point) "TODO")))
- (org-choose-get-index-in-keywords state all-keywords)))
- ;;; org-choose-get-fn-map-group
- (defun org-choose-get-fn-map-group ()
- "Return a function to map over the group"
- #'(lambda (fn)
- (require 'org-agenda) ;; `org-map-entries' seems to need it.
- (save-excursion
- (unless (org-up-heading-safe)
- (error "Choosing is only supported between siblings in a tree, not on top level"))
- (let
- ((level (org-reduced-level (org-outline-level))))
- (save-restriction
- (org-map-entries
- fn
- (format "LEVEL=%d" level)
- 'tree))))))
- ;;; org-choose-get-highest-mark-index
- (defun org-choose-get-highest-mark-index (keywords)
- "Get the index of the highest current mark in the group.
- If there is none, return 0"
- (let*
- ;;Func maps over applicable entries.
- ((map-over-entries
- (org-choose-get-fn-map-group))
- (indexes-list
- (remove nil
- (funcall map-over-entries
- #'(lambda ()
- (org-choose-get-entry-index keywords))))))
- (if
- indexes-list
- (apply #'max indexes-list)
- 0)))
- ;;; org-choose-highest-ok
- (defun org-choose-highest-other-ok (ix data)
- "Return the highest index that any choose mark can sensibly have,
- given that another mark has index IX.
- DATA must be a `org-choose-mark-data.'."
- (let
- ((bot-lower-range
- (org-choose-mark-data.-bot-lower-range data))
- (top-upper-range
- (org-choose-mark-data.-top-upper-range data))
- (range-length
- (org-choose-mark-data.-range-length data)))
- (when (and ix bot-lower-range)
- (let*
- ((delta
- (- top-upper-range ix)))
- (unless
- (< range-length delta)
- (+ bot-lower-range delta))))))
- ;;; org-choose-get-default-mark-index
- (defun org-choose-get-default-mark-index (data)
- "Return the index of the default mark in a choose interpretation.
- DATA must be a `org-choose-mark-data.'."
- (or
- (let
- ((highest-mark-index
- (org-choose-get-highest-mark-index
- (org-choose-mark-data.-all-keywords data))))
- (org-choose-highest-other-ok
- highest-mark-index data))
- (org-choose-mark-data.-static-default data)))
- ;;; org-choose-get-mark-N
- (defun org-choose-get-mark-N (n data)
- "Get the text of the nth mark in a choose interpretation."
- (let*
- ((l (org-choose-mark-data.-all-keywords data)))
- (nth n l)))
- ;;; org-choose-get-default-mark
- (defun org-choose-get-default-mark (new-mark old-mark)
- "Get the default mark IFF in a choose interpretation.
- NEW-MARK and OLD-MARK are the text of the new and old marks."
- (let*
- ((old-kwd-data
- (assoc old-mark org-todo-kwd-alist))
- (new-kwd-data
- (assoc new-mark org-todo-kwd-alist))
- (becomes-choose
- (and
- (or
- (not old-kwd-data)
- (not
- (eq (nth 1 old-kwd-data) 'choose)))
- (eq (nth 1 new-kwd-data) 'choose))))
- (when
- becomes-choose
- (let
- ((new-mark-data
- (assoc new-mark org-choose-mark-data)))
- (if
- new-mark
- (org-choose-get-mark-N
- (org-choose-get-default-mark-index
- new-mark-data)
- new-mark-data)
- (error "Somehow got an unrecognizable mark"))))))
- ;;; Setting it all up
- (eval-after-load 'org
- '(progn
- (add-to-list 'org-todo-setup-filter-hook
- #'org-choose-setup-filter)
- (add-to-list 'org-todo-get-default-hook
- #'org-choose-get-default-mark)
- (add-to-list 'org-trigger-hook
- #'org-choose-keep-sensible)
- (add-to-list 'org-todo-interpretation-widgets
- '(:tag "Choose (to record decisions)" choose)
- 'append)))
- (provide 'org-choose)
- ;;; org-choose.el ends here
|