| 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 appropriatebuffer-local variables.RAW is a list of strings representing the input text of a chooseinterpretation."  (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'ssetting 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
 |