123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- ;;; org-depend.el --- TODO dependencies for Org-mode
- ;; This file is not part of GNU Emacs.
- ;;
- ;; 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 3, or (at your option)
- ;; any later version.
- ;; GNU Emacs 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., 51 Franklin Street, Fifth Floor,
- ;; Boston, MA 02110-1301, USA.
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;
- ;;; Commentary:
- ;;
- ;; WARNING: This file is just a PROOF OF CONCEPT, not a supported part
- ;; of Org-mode.
- ;;
- ;; This is an example implementation of TODO dependencies in Org-mode.
- ;; It uses the new hooks in version 5.13 of Org-mode,
- ;; `org-trigger-hook' and `org-blocker-hook'.
- ;;
- ;; It implements the following:
- ;;
- ;; Triggering
- ;; ----------
- ;;
- ;; 1) If an entry contains a TRIGGER property that contains the string
- ;; "chain-siblings(KEYWORD)", then switching that entry to DONE does
- ;; do the following:
- ;; - The sibling following this entry switched to todo-state KEYWORD.
- ;; - The sibling also gets a TRIGGER property "chain-sibling(KEYWORD)",
- ;; property, to make sure that, when *it* is DONE, the chain will
- ;; continue.
- ;;
- ;; 2) If the TRIGGER property contains any other words like
- ;; XYZ(KEYWORD), these are treated as entry id's with keywords. That
- ;; means, Org-mode will search for an entry with the ID property XYZ
- ;; and switch that entry to KEYWORD as well.
- ;;
- ;; Blocking
- ;; --------
- ;;
- ;; 1) If an entry contains a BLOCKER property that contains the word
- ;; "previous-sibling", the sibling above the current entry is
- ;; checked when you try to mark it DONE. If it is still in a TODO
- ;; state, the current state change is blocked.
- ;;
- ;; 2) If the BLOCKER property contains any other words, these are
- ;; treated as entry id's. That means, Org-mode will search for an
- ;; entry with the ID property exactly equal to this word. If any
- ;; of these entries is not yet marked DONE, the current state change
- ;; will be blocked.
- ;;
- ;; 3) Whenever a state change is blocked, an org-mark is pushed, so that
- ;; you can find the offending entry with `C-c &'.
- ;;
- ;;; Example:
- ;;
- ;; When trying this example, make sure that the settings for TODO keywords
- ;; have been activated, i.e. include the following line and press C-c C-c
- ;; on the line before working with the example:
- ;;
- ;; #+TYP_TODO: TODO NEXT | DONE
- ;;
- ;; * TODO Win a million in Las Vegas
- ;; The "third" TODO (see above) cannot become a TODO without this money.
- ;;
- ;; :PROPERTIES:
- ;; :ID: I-cannot-do-it-without-money
- ;; :END:
- ;;
- ;; * Do this by doing a chain of TODO's
- ;; ** NEXT This is the first in this chain
- ;; :PROPERTIES:
- ;; :TRIGGER: chain-siblings(NEXT)
- ;; :END:
- ;;
- ;; ** This is the second in this chain
- ;;
- ;; ** This is the third in this chain
- ;; :PROPERTIES:
- ;; :BLOCKER: I-cannot-do-it-without-money
- ;; :END:
- ;;
- ;; ** This is the forth in this chain
- ;; When this is DONE, we will also trigger entry XYZ-is-my-id
- ;; :PROPERTIES:
- ;; :TRIGGER: XYZ-is-my-id(TODO)
- ;; :END:
- ;;
- ;; ** This is the fifth in this chain
- ;;
- ;; * Start writing report
- ;; :PROPERTIES:
- ;; :ID: XYZ-is-my-id
- ;; :END:
- ;;
- ;;
- (require 'org)
- (defun org-depend-trigger-todo (change-plist)
- "Trigger new TODO entries after the current is switched to DONE.
- This does two different kinds of triggers:
- - If the current entry contains a TRIGGER property that contains
- \"chain-siblings(KEYWORD)\", it goes to the next sibling, marks it
- KEYWORD and also installs the \"chain-sibling\" trigger to continue
- the chain.
- - Any other word (space-separated) like XYZ(KEYWORD) in the TRIGGER
- property is seen as an entry id. Org-mode finds the entry with the
- corresponding ID property and switches it to the state TODO as well."
- ;; Get information from the plist
- (let* ((type (plist-get change-plist :type))
- (pos (plist-get change-plist :position))
- (from (plist-get change-plist :from))
- (to (plist-get change-plist :to))
- (org-log-done nil) ; IMPROTANT!: no logging during automatic trigger!
- trigger triggers tr p1 kwd)
- (catch 'return
- (unless (eq type 'todo-state-change)
- ;; We are only handling todo-state-change....
- (throw 'return t))
- (unless (and (member from org-not-done-keywords)
- (member to org-done-keywords))
- ;; This is not a change from TODO to DONE, ignore it
- (throw 'return t))
- ;; OK, we just switched from a TODO state to a DONE state
- ;; Lets see if this entry has a TRIGGER property.
- ;; If yes, split it up on whitespace.
- (setq trigger (org-entry-get pos "TRIGGER")
- triggers (and trigger (org-split-string trigger "[ \t]+")))
- ;; Go through all the triggers
- (while (setq tr (pop triggers))
- (cond
- ((string-match "\\`chain-siblings(\\(.*?\\))\\'" tr)
- ;; This is a TODO chain of siblings
- (setq kwd (match-string 1 tr))
- (catch 'exit
- (save-excursion
- (goto-char pos)
- ;; find the sibling, exit if no more siblings
- (condition-case nil
- (outline-forward-same-level 1)
- (error (throw 'exit t)))
- ;; mark the sibling TODO
- (org-todo kwd)
- ;; make sure the sibling will continue the chain
- (org-entry-add-to-multivalued-property
- nil "TRIGGER" (format "chain-siblings(%s)" kwd)))))
- ((string-match "\\`\\(\\S-+\\)(\\(.*?\\))\\'" tr)
- ;; This seems to be ENTRY_ID(KEYWORD)
- (setq id (match-string 1 tr)
- kwd (match-string 2 tr)
- p1 (org-find-entry-with-id id))
- (when p1
- ;; there is an entry with this ID, mark it TODO
- (save-excursion
- (goto-char p1)
- (org-todo kwd)))))))))
- (defun org-depend-block-todo (change-plist)
- "Block turning an entry into a TODO.
- This checks for a BLOCKER property in an entry and checks
- all the entries listed there. If any of them is not done,
- block changing the current entry into a TODO entry. If the property contains
- the word \"previous-sibling\", the sibling above the current entry is checked.
- Any other words are treated as entry id's. If an entry exists with the
- this ID property, that entry is also checked."
- ;; Get information from the plist
- (let* ((type (plist-get change-plist :type))
- (pos (plist-get change-plist :position))
- (from (plist-get change-plist :from))
- (to (plist-get change-plist :to))
- (org-log-done nil) ; IMPROTANT!: no logging during automatic trigger
- blocker blockers bl p1)
- (catch 'return
- (unless (eq type 'todo-state-change)
- ;; We are not handling this kind of change
- (throw 'return t))
- (unless (and (not from) (member to org-not-done-keywords))
- ;; This is not a change from nothing to TODO, ignore it
- (throw 'return t))
- ;; OK, the plan is to switch from nothing to TODO
- ;; Lets see if we will allow it. Find the BLOCKER property
- ;; and split it on whitespace.
- (setq blocker (org-entry-get pos "BLOCKER")
- blockers (and blocker (org-split-string blocker "[ \t]+")))
- ;; go through all the blockers
- (while (setq bl (pop blockers))
- (cond
- ((equal bl "previous-sibling")
- ;; the sibling is required to be DONE.
- (catch 'ignore
- (save-excursion
- (goto-char pos)
- ;; find the older sibling, exit if no more siblings
- (condition-case nil
- (outline-backward-same-level 1)
- (error (throw 'ignore t)))
- ;; Check if this entry is not yet done and block
- (unless (org-entry-is-done-p)
- ;; return nil, to indicate that we block the change!
- (org-mark-ring-push)
- (throw 'return nil)))))
- ((setq p1 (org-find-entry-with-id bl))
- ;; there is an entry with this ID, check it out
- (save-excursion
- (goto-char p1)
- (unless (org-entry-is-done-p)
- ;; return nil, to indicate that we block the change!
- (org-mark-ring-push)
- (throw 'return nil))))))
- t ; return t to indicate that we are not blocking
- )))
- (add-hook 'org-trigger-hook 'org-depend-trigger-todo)
- (add-hook 'org-blocker-hook 'org-depend-block-todo)
- ;;; org-depend.el ends here
|