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