| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 | ;;;_ org-choose.el --- decision management for org-mode;;;_. Headers;;;_ , License;; Copyright (C) 2009  Tom Breton (Tehom);; 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)     ));;;_. Footers;;;_ , Provides(provide 'org-choose);;;_ * Local emacs vars.;;;_  + Local variables:;;;_  + End:;;;_ , End;;; org-choose.el ends here
 |