dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

org-depend.el (15871B)


      1 ;;; org-depend.el --- TODO dependencies for Org-mode
      2 ;; Copyright (C) 2008-2021 Free Software Foundation, Inc.
      3 ;;
      4 ;; Author: Carsten Dominik <carsten.dominik@gmail.com>
      5 ;; Keywords: outlines, hypermedia, calendar, wp
      6 ;; Homepage: https://git.sr.ht/~bzg/org-contrib
      7 ;; Version: 0.08
      8 ;;
      9 ;; This file is not part of GNU Emacs.
     10 ;;
     11 ;; This file is free software; you can redistribute it and/or modify
     12 ;; it under the terms of the GNU General Public License as published by
     13 ;; the Free Software Foundation; either version 3, or (at your option)
     14 ;; any later version.
     15 
     16 ;; This program is distributed in the hope that it will be useful,
     17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     19 ;; GNU General Public License for more details.
     20 
     21 ;; You should have received a copy of the GNU General Public License
     22 ;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
     23 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     24 ;;
     25 ;;; Commentary:
     26 ;;
     27 ;; WARNING: This file is just a PROOF OF CONCEPT, not a supported part
     28 ;;          of Org-mode.
     29 ;;
     30 ;; This is an example implementation of TODO dependencies in Org-mode.
     31 ;; It uses the new hooks in version 5.13 of Org-mode,
     32 ;; `org-trigger-hook' and `org-blocker-hook'.
     33 ;;
     34 ;; It implements the following:
     35 ;;
     36 ;; Triggering
     37 ;; ----------
     38 ;;
     39 ;; 1) If an entry contains a TRIGGER property that contains the string
     40 ;;    "chain-siblings(KEYWORD)", then switching that entry to DONE does
     41 ;;    do the following:
     42 ;;    - The sibling following this entry switched to todo-state KEYWORD.
     43 ;;    - The sibling also gets a TRIGGER property "chain-sibling(KEYWORD)",
     44 ;;      property, to make sure that, when *it* is DONE, the chain will
     45 ;;      continue.
     46 ;;
     47 ;; 2) If an entry contains a TRIGGER property that contains the string
     48 ;;    "chain-siblings-scheduled", then switching that entry to DONE does
     49 ;;    the following actions, similarly to "chain-siblings(KEYWORD)":
     50 ;;    - The sibling receives the same scheduled time as the entry
     51 ;;      marked as DONE (or, in the case, in which there is no scheduled
     52 ;;      time, the sibling does not get any either).
     53 ;;    - The sibling also gets the same TRIGGER property
     54 ;;      "chain-siblings-scheduled", so the chain can continue.
     55 ;;
     56 ;; 3) If the TRIGGER property contains the string
     57 ;;    "chain-find-next(KEYWORD[,OPTIONS])", then switching that entry
     58 ;;    to DONE do the following:
     59 ;;    - All siblings are of the entry are collected into a temporary
     60 ;;      list and then filtered and sorted according to OPTIONS
     61 ;;    - The first sibling on the list is changed into KEYWORD state
     62 ;;    - The sibling also gets the same TRIGGER property
     63 ;;      "chain-find-next", so the chain can continue.
     64 ;;
     65 ;;    OPTIONS should be a comma separated string without spaces, and
     66 ;;    can contain following options:
     67 ;;
     68 ;;    - from-top      the candidate list is all of the siblings in
     69 ;;                    the current subtree
     70 ;;
     71 ;;    - from-bottom   candidate list are all siblings from bottom up
     72 ;;
     73 ;;    - from-current  candidate list are all siblings from current item
     74 ;;                    until end of subtree, then wrapped around from
     75 ;;                    first sibling
     76 ;;
     77 ;;    - no-wrap       candidate list are siblings from current one down
     78 ;;
     79 ;;    - todo-only     Only consider siblings that have a todo keyword
     80 ;;    -
     81 ;;    - todo-and-done-only
     82 ;;                    Same as above but also include done items.
     83 ;;
     84 ;;    - priority-up   sort by highest priority
     85 ;;    - priority-down sort by lowest priority
     86 ;;    - effort-up     sort by highest effort
     87 ;;    - effort-down   sort by lowest effort
     88 ;;
     89 ;;    Default OPTIONS are from-top
     90 ;;
     91 ;;
     92 ;; 4) If the TRIGGER property contains any other words like
     93 ;;    XYZ(KEYWORD), these are treated as entry id's with keywords.  That
     94 ;;    means Org-mode will search for an entry with the ID property XYZ
     95 ;;    and switch that entry to KEYWORD as well.
     96 ;;
     97 ;; Blocking
     98 ;; --------
     99 ;;
    100 ;; 1) If an entry contains a BLOCKER property that contains the word
    101 ;;    "previous-sibling", the sibling above the current entry is
    102 ;;    checked when you try to mark it DONE.  If it is still in a TODO
    103 ;;    state, the current state change is blocked.
    104 ;;
    105 ;; 2) If the BLOCKER property contains any other words, these are
    106 ;;    treated as entry id's.  That means Org-mode will search for an
    107 ;;    entry with the ID property exactly equal to this word.  If any
    108 ;;    of these entries is not yet marked DONE, the current state change
    109 ;;    will be blocked.
    110 ;;
    111 ;; 3) Whenever a state change is blocked, an org-mark is pushed, so that
    112 ;;    you can find the offending entry with `C-c &'.
    113 ;;
    114 ;;; Example:
    115 ;;
    116 ;; When trying this example, make sure that the settings for TODO keywords
    117 ;; have been activated, i.e. include the following line and press C-c C-c
    118 ;; on the line before working with the example:
    119 ;;
    120 ;; #+TYP_TODO: TODO NEXT | DONE
    121 ;;
    122 ;; * TODO Win a million in Las Vegas
    123 ;;   The "third" TODO (see above) cannot become a TODO without this money.
    124 ;;
    125 ;;   :PROPERTIES:
    126 ;;     :ID: I-cannot-do-it-without-money
    127 ;;   :END:
    128 ;;
    129 ;; * Do this by doing a chain of TODO's
    130 ;; ** NEXT This is the first in this chain
    131 ;;    :PROPERTIES:
    132 ;;      :TRIGGER: chain-siblings(NEXT)
    133 ;;    :END:
    134 ;;
    135 ;; ** This is the second in this chain
    136 ;;
    137 ;; ** This is the third in this chain
    138 ;;    :PROPERTIES:
    139 ;;      :BLOCKER: I-cannot-do-it-without-money
    140 ;;    :END:
    141 ;;
    142 ;; ** This is the forth in this chain
    143 ;;    When this is DONE, we will also trigger entry XYZ-is-my-id
    144 ;;   :PROPERTIES:
    145 ;;     :TRIGGER: XYZ-is-my-id(TODO)
    146 ;;   :END:
    147 ;;
    148 ;; ** This is the fifth in this chain
    149 ;;
    150 ;; * Start writing report
    151 ;;   :PROPERTIES:
    152 ;;     :ID: XYZ-is-my-id
    153 ;;   :END:
    154 ;;
    155 ;;
    156 
    157 (require 'org)
    158 (eval-when-compile
    159   (require 'cl))
    160 
    161 (defcustom org-depend-tag-blocked t
    162   "Whether to indicate blocked TODO items by a special tag."
    163   :group 'org
    164   :type 'boolean)
    165 
    166 (defcustom org-depend-find-next-options
    167   "from-current,todo-only,priority-up"
    168   "Default options for chain-find-next trigger"
    169   :group 'org
    170   :type 'string)
    171 
    172 (defmacro org-depend-act-on-sibling (trigger-val &rest rest)
    173   "Perform a set of actions on the next sibling, if it exists,
    174 copying the sibling spec TRIGGER-VAL to the next sibling."
    175   `(catch 'exit
    176      (save-excursion
    177        (goto-char pos)
    178        ;; find the sibling, exit if no more siblings
    179        (condition-case nil
    180            (outline-forward-same-level 1)
    181          (error (throw 'exit t)))
    182        ;; mark the sibling TODO
    183        ,@rest
    184        ;; make sure the sibling will continue the chain
    185        (org-entry-add-to-multivalued-property
    186         nil "TRIGGER" ,trigger-val))))
    187 
    188 (defvar org-depend-doing-chain-find-next nil)
    189 
    190 (defun org-depend-trigger-todo (change-plist)
    191   "Trigger new TODO entries after the current is switched to DONE.
    192 This does two different kinds of triggers:
    193 
    194 - If the current entry contains a TRIGGER property that contains
    195   \"chain-siblings(KEYWORD)\", it goes to the next sibling, marks it
    196   KEYWORD and also installs the \"chain-sibling\" trigger to continue
    197   the chain.
    198 - If the current entry contains a TRIGGER property that contains
    199   \"chain-siblings-scheduled\", we go to the next sibling and copy
    200   the scheduled time from the current task, also installing the property
    201   in the sibling.
    202 - Any other word (space-separated) like XYZ(KEYWORD) in the TRIGGER
    203   property is seen as an entry id.  Org-mode finds the entry with the
    204   corresponding ID property and switches it to the state TODO as well."
    205 
    206   ;; Refresh the effort text properties
    207   (org-refresh-properties org-effort-property 'org-effort)
    208   ;; Get information from the plist
    209   (let* ((type (plist-get change-plist :type))
    210 	 (pos (plist-get change-plist :position))
    211 	 (from (plist-get change-plist :from))
    212 	 (to (plist-get change-plist :to))
    213 	 (org-log-done nil) ; IMPORTANT!: no logging during automatic trigger!
    214 	 trigger triggers tr p1 p2 kwd id)
    215     (catch 'return
    216       (unless (eq type 'todo-state-change)
    217 	;; We are only handling todo-state-change....
    218 	(throw 'return t))
    219       (unless (and (member from org-not-done-keywords)
    220 		   (member to org-done-keywords))
    221 	;; This is not a change from TODO to DONE, ignore it
    222 	(throw 'return t))
    223 
    224       ;; OK, we just switched from a TODO state to a DONE state
    225       ;; Lets see if this entry has a TRIGGER property.
    226       ;; If yes, split it up on whitespace.
    227       (setq trigger (org-entry-get pos "TRIGGER")
    228 	    triggers (and trigger (split-string trigger)))
    229 
    230       ;; Go through all the triggers
    231       (while (setq tr (pop triggers))
    232 	(cond
    233 	 ((and (not org-depend-doing-chain-find-next)
    234 	       (string-match "\\`chain-find-next(\\b\\(.+?\\)\\b\\(.*\\))\\'" tr))
    235 	  ;; smarter sibling selection
    236 	  (let* ((org-depend-doing-chain-find-next t)
    237 		 (kwd (match-string 1 tr))
    238 		 (options (match-string 2 tr))
    239 		 (options (if (or (null options)
    240 				  (equal options ""))
    241 			      org-depend-find-next-options
    242 			    options))
    243 		 (todo-only (string-match "todo-only" options))
    244 		 (todo-and-done-only (string-match "todo-and-done-only"
    245 						   options))
    246 		 (from-top (string-match "from-top" options))
    247 		 (from-bottom (string-match "from-bottom" options))
    248 		 (from-current (string-match "from-current" options))
    249 		 (no-wrap (string-match "no-wrap" options))
    250 		 (priority-up (string-match "priority-up" options))
    251 		 (priority-down (string-match "priority-down" options))
    252 		 (effort-up (string-match "effort-up" options))
    253 		 (effort-down (string-match "effort-down" options)))
    254 	    (save-excursion
    255 	      (org-back-to-heading t)
    256 	      (let ((this-item (point)))
    257 		;; go up to the parent headline, then advance to next child
    258 		(org-up-heading-safe)
    259 		(let ((end (save-excursion (org-end-of-subtree t)
    260 					   (point)))
    261 		      (done nil)
    262 		      (items '()))
    263 		  (outline-next-heading)
    264 		  (while (not done)
    265 		    (if (not (looking-at org-complex-heading-regexp))
    266 			(setq done t)
    267 		      (let ((todo-kwd (match-string 2))
    268 			    (tags (match-string 5))
    269 			    (priority (org-get-priority (or (match-string 3) "")))
    270 			    (effort (when (or effort-up effort-down)
    271 				      (let ((effort (get-text-property (point) 'org-effort)))
    272 					(when effort
    273 					  (org-duration-to-minutes effort))))))
    274 			(push (list (point) todo-kwd priority tags effort)
    275 			      items))
    276 		      (unless (org-goto-sibling)
    277 			(setq done t))))
    278 		  ;; massage the list according to options
    279 		  (setq items
    280 			(cond (from-top (nreverse items))
    281 			      (from-bottom items)
    282 			      ((or from-current no-wrap)
    283 			       (let* ((items (nreverse items))
    284 				      (pos (cl-position this-item items :key #'cl-first))
    285 				      (items-before (cl-subseq items 0 pos))
    286 				      (items-after (cl-subseq items pos)))
    287 				 (if no-wrap items-after
    288 				   (append items-after items-before))))
    289 			      (t (nreverse items))))
    290 		  (setq items (cl-remove-if
    291 			       (lambda (item)
    292 				 (or (equal (first item) this-item)
    293 				     (and (not todo-and-done-only)
    294 					  (member (second item) org-done-keywords))
    295 				     (and (or todo-only
    296 					      todo-and-done-only)
    297 					  (null (second item)))))
    298 			       items))
    299 		  (setq items
    300 			(sort
    301 			 items
    302 			 (lambda (item1 item2)
    303 			   (let* ((p1 (third item1))
    304 				  (p2 (third item2))
    305 				  (e1 (fifth item1))
    306 				  (e2 (fifth item2))
    307 				  (p1-lt (< p1 p2))
    308 				  (p1-gt (> p1 p2))
    309 				  (e1-lt (and e1 (or (not e2) (< e1 e2))))
    310 				  (e2-gt (and e2 (or (not e1) (> e1 e2)))))
    311 			     (cond (priority-up
    312 				    (or p1-gt
    313 					(and (equal p1 p2)
    314 					     (or (and effort-up e1-lt)
    315 						 (and effort-down e2-gt)))))
    316 				   (priority-down
    317 				    (or p1-lt
    318 					(and (equal p1 p2)
    319 					     (or (and effort-up e1-lt)
    320 						 (and effort-down e2-gt)))))
    321 				   (effort-up
    322 				    (or e2-gt (and (equal e1 e2) p1-gt)))
    323 				   (effort-down
    324 				    (or e1-lt (and (equal e1 e2) p1-gt))))))))
    325 		  (when items
    326 		    (goto-char (first (first items)))
    327 		    (org-entry-add-to-multivalued-property nil "TRIGGER" tr)
    328 		    (org-todo kwd)))))))
    329 	 ((string-match "\\`chain-siblings(\\(.*?\\))\\'" tr)
    330 	  ;; This is a TODO chain of siblings
    331 	  (setq kwd (match-string 1 tr))
    332           (org-depend-act-on-sibling (format "chain-siblings(%s)" kwd)
    333                                      (org-todo kwd)))
    334 	 ((string-match "\\`\\(\\S-+\\)(\\(.*?\\))\\'" tr)
    335 	  ;; This seems to be ENTRY_ID(KEYWORD)
    336 	  (setq id (match-string 1 tr)
    337 		kwd (match-string 2 tr)
    338 		p1 (org-find-entry-with-id id))
    339 	  ;; First check current buffer, then all files.
    340 	  (if p1
    341 	      ;; There is an entry with this ID, mark it TODO.
    342 	      (save-excursion
    343 		(goto-char p1)
    344 		(org-todo kwd))
    345 	    (when (setq p2 (org-id-find id))
    346 	      (save-excursion
    347 		(with-current-buffer (find-file-noselect (car p2))
    348 		  (goto-char (cdr p2))
    349 		  (org-todo kwd))))))
    350          ((string-match "\\`chain-siblings-scheduled\\'" tr)
    351           (let ((time (org-get-scheduled-time pos)))
    352             (when time
    353               (org-depend-act-on-sibling
    354                "chain-siblings-scheduled"
    355                (org-schedule nil time))))))))))
    356 
    357 (defun org-depend-block-todo (change-plist)
    358   "Block turning an entry into a TODO.
    359 This checks for a BLOCKER property in an entry and checks
    360 all the entries listed there.  If any of them is not done,
    361 block changing the current entry into a TODO entry.  If the property contains
    362 the word \"previous-sibling\", the sibling above the current entry is checked.
    363 Any other words are treated as entry id's. If an entry exists with the
    364 this ID property, that entry is also checked."
    365   ;; Get information from the plist
    366   (let* ((type (plist-get change-plist :type))
    367 	 (pos (plist-get change-plist :position))
    368 	 (from (plist-get change-plist :from))
    369 	 (to (plist-get change-plist :to))
    370 	 (org-log-done nil) ; IMPORTANT!: no logging during automatic trigger
    371 	 blocker blockers bl p1 p2
    372 	 (proceed-p
    373 	  (catch 'return
    374             ;; If this is not a todo state change, or if this entry is
    375             ;; DONE, do not block
    376             (when (or (not (eq type 'todo-state-change))
    377                       (member from (cons 'done org-done-keywords))
    378                       (member to (cons 'todo org-not-done-keywords))
    379                       (not to))
    380               (throw 'return t))
    381 
    382 	    ;; OK, the plan is to switch from nothing to TODO
    383 	    ;; Lets see if we will allow it.  Find the BLOCKER property
    384 	    ;; and split it on whitespace.
    385 	    (setq blocker (org-entry-get pos "BLOCKER")
    386 		  blockers (and blocker (split-string blocker)))
    387 
    388 	    ;; go through all the blockers
    389 	    (while (setq bl (pop blockers))
    390 	      (cond
    391 	       ((equal bl "previous-sibling")
    392 		;; the sibling is required to be DONE.
    393 		(catch 'ignore
    394 		  (save-excursion
    395 		    (goto-char pos)
    396 		    ;; find the older sibling, exit if no more siblings
    397 		    (unless (org-get-last-sibling)
    398 		      (throw 'ignore t))
    399 		    ;; Check if this entry is not yet done and block
    400 		    (unless (org-entry-is-done-p)
    401 		      ;; return nil, to indicate that we block the change!
    402 		      (org-mark-ring-push)
    403 		      (throw 'return nil)))))
    404 	       ((setq p1 (org-find-entry-with-id bl))
    405 		;; there is an entry with this ID, check it out
    406 		(save-excursion
    407 		  (goto-char p1)
    408 		  (unless (org-entry-is-done-p)
    409 		    ;; return nil, to indicate that we block the change!
    410 		    (org-mark-ring-push)
    411 		    (throw 'return nil))))
    412 	       ((setq p2 (org-id-find bl))
    413 		(save-excursion
    414 		  (with-current-buffer (find-file-noselect (car p2))
    415 		    (goto-char (cdr p2))
    416 		    (unless (org-entry-is-done-p)
    417 		      (org-mark-ring-push)
    418 		      (throw 'return nil)))))))
    419 	    ;; Return t to indicate that we are not blocking.
    420 	    t)))
    421     (when org-depend-tag-blocked
    422       (org-toggle-tag "blocked" (if proceed-p 'off 'on)))
    423 
    424     proceed-p))
    425 
    426 (add-hook 'org-trigger-hook 'org-depend-trigger-todo)
    427 (add-hook 'org-blocker-hook 'org-depend-block-todo)
    428 
    429 (provide 'org-depend)
    430 
    431 ;;; org-depend.el ends here