org-choose.el 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. ;;;_ org-choose.el --- decision management for org-mode
  2. ;;;_. Headers
  3. ;;;_ , License
  4. ;; Copyright (C) 2009 Tom Breton (Tehom)
  5. ;; Author: Tom Breton (Tehom)
  6. ;; Keywords: outlines, convenience
  7. ;; This file is free software; you can redistribute it and/or modify
  8. ;; it under the terms of the GNU General Public License as published by
  9. ;; the Free Software Foundation; either version 2, or (at your option)
  10. ;; any later version.
  11. ;; This file is distributed in the hope that it will be useful,
  12. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. ;; GNU General Public License for more details.
  15. ;; You should have received a copy of the GNU General Public License
  16. ;; along with GNU Emacs; see the file COPYING. If not, write to
  17. ;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
  18. ;; Boston, MA 02111-1307, USA.
  19. ;;;_ , Commentary:
  20. ; This is code to support decision management. It lets you treat a
  21. ; group of sibling items in org-mode as alternatives in a decision.
  22. ; There are no user commands in this file. You use it by:
  23. ; * Loading it (manually or by M-x customize-apropos org-modules)
  24. ;; * Setting up at least one set of TODO keywords with the
  25. ;; interpretation "choose" by either:
  26. ;; * Using the file directive #+CHOOSE_TODO:
  27. ;; * For instance, "#+CHOOSE_TODO: NO(,-) MAYBE(,0) YES"
  28. ;; * Or by M-x customize-apropos org-todo-keywords
  29. ;; * Operating on single items with the TODO commands.
  30. ;; * Use C-S-right to change the keyword set. Use this to change to
  31. ;; the "choose" keyword set that you just defined.
  32. ;; * Use S-right to advance the TODO mark to the next setting.
  33. ;; For "choose", that means you like this alternative more than
  34. ;; before. Other alternatives will be automatically demoted to
  35. ;; keep your settings consistent.
  36. ;; * Use S-left to demote TODO to the previous setting.
  37. ;; For "choose", that means you don't like this alternative as much
  38. ;; as before. Other alternatives will be automatically promoted,
  39. ;; if this item was all that was keeping them down.
  40. ;; * All the other TODO commands are available and behave essentially
  41. ;; the normal way.
  42. ;;;_ , Requires
  43. (require 'org)
  44. ;(eval-when-compile
  45. ; (require 'cl))
  46. (require 'cl)
  47. ;;;_. Body
  48. ;;;_ , The variables
  49. (defstruct (org-choose-mark-data. (:type list))
  50. "The format of an entry in org-choose-mark-data.
  51. Indexes are 0-based or `nil'.
  52. "
  53. keyword
  54. bot-lower-range
  55. top-upper-range
  56. range-length
  57. static-default
  58. all-keywords)
  59. (defvar org-choose-mark-data
  60. ()
  61. "Alist of information for choose marks.
  62. Each entry is an `org-choose-mark-data.'" )
  63. (make-variable-buffer-local 'org-choose-mark-data)
  64. ;;;_ , For setup
  65. ;;;_ . org-choose-filter-one
  66. (defun org-choose-filter-one (i)
  67. "Return a list of
  68. * a canonized version of the string
  69. * optionally one symbol"
  70. (if
  71. (not
  72. (string-match "(.*)" i))
  73. (list i i)
  74. (let*
  75. (
  76. (end-text (match-beginning 0))
  77. (vanilla-text (substring i 0 end-text))
  78. ;;Get the parenthesized part.
  79. (match (match-string 0 i))
  80. ;;Remove the parentheses.
  81. (args (substring match 1 -1))
  82. ;;Split it
  83. (arglist
  84. (let
  85. ((arglist-x (org-split-string args ",")))
  86. ;;When string starts with "," `split-string' doesn't
  87. ;;make a first arg, so in that case make one
  88. ;;manually.
  89. (if
  90. (string-match "^," args)
  91. (cons nil arglist-x)
  92. arglist-x)))
  93. (decision-arg (second arglist))
  94. (type
  95. (cond
  96. ((string= decision-arg "0")
  97. 'default-mark)
  98. ((string= decision-arg "+")
  99. 'top-upper-range)
  100. ((string= decision-arg "-")
  101. 'bot-lower-range)
  102. (t nil)))
  103. (vanilla-arg (first arglist))
  104. (vanilla-mark
  105. (if vanilla-arg
  106. (concat vanilla-text "("vanilla-arg")")
  107. vanilla-text)))
  108. (if type
  109. (list vanilla-text vanilla-mark type)
  110. (list vanilla-text vanilla-mark)))))
  111. ;;;_ . org-choose-setup-vars
  112. (defun org-choose-setup-vars (bot-lower-range top-upper-range
  113. static-default num-items all-mark-texts)
  114. "Add to org-choose-mark-data according to arguments"
  115. (let*
  116. (
  117. (tail
  118. ;;If there's no bot-lower-range or no default, we don't
  119. ;;have ranges.
  120. (cdr
  121. (if (and static-default bot-lower-range)
  122. (let*
  123. (
  124. ;;If there's no top-upper-range, use the last
  125. ;;item.
  126. (top-upper-range
  127. (or top-upper-range (1- num-items)))
  128. (lower-range-length
  129. (1+ (- static-default bot-lower-range)))
  130. (upper-range-length
  131. (- top-upper-range static-default))
  132. (range-length
  133. (min upper-range-length lower-range-length)))
  134. (make-org-choose-mark-data.
  135. :keyword nil
  136. :bot-lower-range bot-lower-range
  137. :top-upper-range top-upper-range
  138. :range-length range-length
  139. :static-default static-default
  140. :all-keywords all-mark-texts))
  141. (make-org-choose-mark-data.
  142. :keyword nil
  143. :bot-lower-range nil
  144. :top-upper-range nil
  145. :range-length nil
  146. :static-default (or static-default 0)
  147. :all-keywords all-mark-texts)))))
  148. (dolist (text all-mark-texts)
  149. (pushnew (cons text tail)
  150. org-choose-mark-data
  151. :test
  152. #'(lambda (a b)
  153. (equal (car a) (car b)))))))
  154. ;;;_ . org-choose-filter-tail
  155. (defun org-choose-filter-tail (raw)
  156. "Return a translation of RAW to vanilla and set appropriate
  157. buffer-local variables.
  158. RAW is a list of strings representing the input text of a choose
  159. interpretation."
  160. (let
  161. ((vanilla-list nil)
  162. (all-mark-texts nil)
  163. (index 0)
  164. bot-lower-range top-upper-range range-length static-default)
  165. (dolist (i raw)
  166. (destructuring-bind
  167. (vanilla-text vanilla-mark &optional type)
  168. (org-choose-filter-one i)
  169. (cond
  170. ((eq type 'bot-lower-range)
  171. (setq bot-lower-range index))
  172. ((eq type 'top-upper-range)
  173. (setq top-upper-range index))
  174. ((eq type 'default-mark)
  175. (setq static-default index)))
  176. (incf index)
  177. (push vanilla-text all-mark-texts)
  178. (push vanilla-mark vanilla-list)))
  179. (org-choose-setup-vars bot-lower-range top-upper-range
  180. static-default index (reverse all-mark-texts))
  181. (nreverse vanilla-list)))
  182. ;;;_ . org-choose-setup-filter
  183. (defun org-choose-setup-filter (raw)
  184. "A setup filter for choose interpretations."
  185. (when (eq (car raw) 'choose)
  186. (cons
  187. 'choose
  188. (org-choose-filter-tail (cdr raw)))))
  189. ;;;_ . org-choose-conform-after-promotion
  190. (defun org-choose-conform-after-promotion (entry-pos keywords highest-ok-ix)
  191. "Conform the current item after another item was promoted"
  192. (unless
  193. ;;Skip the entry that triggered this by skipping any entry with
  194. ;;the same starting position. Both map and plist use the start
  195. ;;of the header line as the position, so we can just compare
  196. ;;them with `='
  197. (= (point) entry-pos)
  198. (let
  199. ((ix
  200. (org-choose-get-entry-index keywords)))
  201. ;;If the index of the entry exceeds the highest allowable
  202. ;;index, change it to that.
  203. (when (and ix
  204. (> ix highest-ok-ix))
  205. (org-todo
  206. (nth highest-ok-ix keywords))))))
  207. ;;;_ . org-choose-conform-after-demotion
  208. (defun org-choose-conform-after-demotion (entry-pos keywords
  209. raise-to-ix
  210. old-highest-ok-ix)
  211. "Conform the current item after another item was demoted."
  212. (unless
  213. ;;Skip the entry that triggered this.
  214. (= (point) entry-pos)
  215. (let
  216. ((ix
  217. (org-choose-get-entry-index keywords)))
  218. ;;If the index of the entry was at or above the old allowable
  219. ;;position, change it to the new mirror position if there is
  220. ;;one.
  221. (when (and
  222. ix
  223. raise-to-ix
  224. (>= ix old-highest-ok-ix))
  225. (org-todo
  226. (nth raise-to-ix keywords))))))
  227. ;;;_ , org-choose-keep-sensible (the org-trigger-hook function)
  228. (defun org-choose-keep-sensible (change-plist)
  229. "Bring the other items back into a sensible state after an item's
  230. setting was changed."
  231. (let*
  232. ( (from (plist-get change-plist :from))
  233. (to (plist-get change-plist :to))
  234. (entry-pos
  235. (set-marker
  236. (make-marker)
  237. (plist-get change-plist :position)))
  238. (kwd-data
  239. (assoc to org-todo-kwd-alist)))
  240. (when
  241. (eq (nth 1 kwd-data) 'choose)
  242. (let*
  243. (
  244. (data
  245. (assoc to org-choose-mark-data))
  246. (keywords
  247. (org-choose-mark-data.-all-keywords data))
  248. (old-index
  249. (org-choose-get-index-in-keywords
  250. from
  251. keywords))
  252. (new-index
  253. (org-choose-get-index-in-keywords
  254. to
  255. keywords))
  256. (highest-ok-ix
  257. (org-choose-highest-other-ok
  258. new-index
  259. data))
  260. (funcdata
  261. (cond
  262. ;;The entry doesn't participate in conformance,
  263. ;;so give `nil' which does nothing.
  264. ((not highest-ok-ix) nil)
  265. ;;The entry was created or promoted
  266. ((or
  267. (not old-index)
  268. (> new-index old-index))
  269. (list
  270. #'org-choose-conform-after-promotion
  271. entry-pos keywords
  272. highest-ok-ix))
  273. (t ;;Otherwise the entry was demoted.
  274. (let
  275. (
  276. (raise-to-ix
  277. (min
  278. highest-ok-ix
  279. (org-choose-mark-data.-static-default
  280. data)))
  281. (old-highest-ok-ix
  282. (org-choose-highest-other-ok
  283. old-index
  284. data)))
  285. (list
  286. #'org-choose-conform-after-demotion
  287. entry-pos
  288. keywords
  289. raise-to-ix
  290. old-highest-ok-ix))))))
  291. (if funcdata
  292. ;;The funny-looking names are to make variable capture
  293. ;;unlikely. (Poor-man's lexical bindings).
  294. (destructuring-bind (func-d473 . args-46k) funcdata
  295. (let
  296. ((map-over-entries
  297. (org-choose-get-fn-map-group))
  298. ;;We may call `org-todo', so let various hooks
  299. ;;`nil' so we don't cause loops.
  300. org-after-todo-state-change-hook
  301. org-trigger-hook
  302. org-blocker-hook
  303. org-todo-get-default-hook
  304. ;;Also let this alist `nil' so we don't log
  305. ;;secondary transitions.
  306. org-todo-log-states)
  307. ;;Map over group
  308. (funcall map-over-entries
  309. #'(lambda ()
  310. (apply func-d473 args-46k))))))))
  311. ;;Remove the marker
  312. (set-marker entry-pos nil)))
  313. ;;;_ , Getting the default mark
  314. ;;;_ . org-choose-get-index-in-keywords
  315. (defun org-choose-get-index-in-keywords (ix all-keywords)
  316. "Return the index of the current entry."
  317. (if ix
  318. (position ix all-keywords
  319. :test #'equal)))
  320. ;;;_ . org-choose-get-entry-index
  321. (defun org-choose-get-entry-index (all-keywords)
  322. "Return index of current entry."
  323. (let*
  324. ((state (org-entry-get (point) "TODO")))
  325. (org-choose-get-index-in-keywords state all-keywords)))
  326. ;;;_ . org-choose-get-fn-map-group
  327. (defun org-choose-get-fn-map-group ()
  328. "Return a function to map over the group"
  329. #'(lambda (fn)
  330. (require 'org-agenda) ;; `org-map-entries' seems to need it.
  331. (save-excursion
  332. (unless (org-up-heading-safe)
  333. (error "Chosing is only supported between siblings in a tree, not on top level"))
  334. (save-restriction
  335. (org-map-entries fn nil 'tree)))))
  336. ;;;_ . org-choose-get-highest-mark-index
  337. (defun org-choose-get-highest-mark-index (keywords)
  338. "Get the index of the highest current mark in the group.
  339. If there is none, return 0"
  340. (let*
  341. (
  342. ;;Func maps over applicable entries.
  343. (map-over-entries
  344. (org-choose-get-fn-map-group))
  345. (indexes-list
  346. (remove nil
  347. (funcall map-over-entries
  348. #'(lambda ()
  349. (org-choose-get-entry-index keywords))))))
  350. (if
  351. indexes-list
  352. (apply #'max indexes-list)
  353. 0)))
  354. ;;;_ . org-choose-highest-ok
  355. (defun org-choose-highest-other-ok (ix data)
  356. "Return the highest index that any choose mark can sensibly have,
  357. given that another mark has index IX.
  358. DATA must be a `org-choose-mark-data.'."
  359. (let
  360. (
  361. (bot-lower-range
  362. (org-choose-mark-data.-bot-lower-range data))
  363. (top-upper-range
  364. (org-choose-mark-data.-top-upper-range data))
  365. (range-length
  366. (org-choose-mark-data.-range-length data)))
  367. (when (and ix bot-lower-range)
  368. (let*
  369. ((delta
  370. (- top-upper-range ix)))
  371. (unless
  372. (< range-length delta)
  373. (+ bot-lower-range delta))))))
  374. ;;;_ . org-choose-get-default-mark-index
  375. (defun org-choose-get-default-mark-index (data)
  376. "Return the index of the default mark in a choose interpretation.
  377. DATA must be a `org-choose-mark-data.'."
  378. (or
  379. (let
  380. ((highest-mark-index
  381. (org-choose-get-highest-mark-index
  382. (org-choose-mark-data.-all-keywords data))))
  383. (org-choose-highest-other-ok
  384. highest-mark-index data))
  385. (org-choose-mark-data.-static-default data)))
  386. ;;;_ . org-choose-get-mark-N
  387. (defun org-choose-get-mark-N (n data)
  388. "Get the text of the nth mark in a choose interpretation."
  389. (let*
  390. ((l (org-choose-mark-data.-all-keywords data)))
  391. (nth n l)))
  392. ;;;_ . org-choose-get-default-mark
  393. (defun org-choose-get-default-mark (new-mark old-mark)
  394. "Get the default mark IFF in a choose interpretation.
  395. NEW-MARK and OLD-MARK are the text of the new and old marks."
  396. (let*
  397. (
  398. (old-kwd-data
  399. (assoc old-mark org-todo-kwd-alist))
  400. (new-kwd-data
  401. (assoc new-mark org-todo-kwd-alist))
  402. (becomes-choose
  403. (and
  404. (or
  405. (not old-kwd-data)
  406. (not
  407. (eq (nth 1 old-kwd-data) 'choose)))
  408. (eq (nth 1 new-kwd-data) 'choose))))
  409. (when
  410. becomes-choose
  411. (let
  412. ((new-mark-data
  413. (assoc new-mark org-choose-mark-data)))
  414. (if
  415. new-mark
  416. (org-choose-get-mark-N
  417. (org-choose-get-default-mark-index
  418. new-mark-data)
  419. new-mark-data)
  420. (error "Somehow got an unrecognizable mark"))))))
  421. ;;;_ , Setting it all up
  422. (eval-after-load "org"
  423. '(progn
  424. (add-to-list 'org-todo-setup-filter-hook
  425. #'org-choose-setup-filter)
  426. (add-to-list 'org-todo-get-default-hook
  427. #'org-choose-get-default-mark)
  428. (add-to-list 'org-trigger-hook
  429. #'org-choose-keep-sensible)
  430. (add-to-list 'org-todo-interpretation-widgets
  431. '(:tag "Choose (to record decisions)" choose)
  432. 'append)
  433. ))
  434. ;;;_. Footers
  435. ;;;_ , Provides
  436. (provide 'org-choose)
  437. ;;;_ * Local emacs vars.
  438. ;;;_ + Local variables:
  439. ;;;_ + End:
  440. ;;;_ , End
  441. ;;; org-choose.el ends here