Emacs Org-Mode: Automatic Item TODO/DOING/DONE State Transitions for Checkbox Changes
In Emacs org-mode, you start with two states for your outline headings by default to manage tasks: TODO
and DONE
.
I recently introduced a new state in between: DOING
. That helped me come back to stuff I had to let lie for a while.
In code, that means at least:
(setq org-todo-keywords
(quote ((sequence "TODO(t)" "DOING(g)" "|" "DONE(d)"))))
I actually have multiple sequences, but these don’t matter for this demonstrations.
Thanks to StackExchange, I had automatic parent-child-updates and “statistics cookie” actions for a while.
Video Demo
Check out a video demo of the TODO/DOING/DONE state transitions [on YouTube.](https://www.youtube.com/watch?v=W9R_JXCWORI]
Cookies in Org-Mode
The “cookies” part is a summary of sub-items: of N TODO
/DONE
items, how many are done? That’s displayed on the parent with a cookie
It works both for checkbox list items and for nested outlines. To illustrate this, let me quote another, very short StackExchange answer:
In this org file:
* TODO Organize party [2/4] - [-] call people [33%] - [ ] Peter - [X] Sarah - [ ] Sam - [X] order food - [ ] think about what music to play - [X] talk to the neighbors
The
[2/4]
and[33%]
are statistics cookies. They describe the completion status of the children of the thing they’re on.They’re pretty useful, because they update automatically as you update the status of children. You can also use them to show the status of child TODO tasks:
* TODO bake a cake [3/6] ** DONE buy ingredients ** DONE start cooking ** DONE realize you forgot eggs, dammit ** TODO drive back to the store and buy eggs ** TODO wait, I needed THREE sticks of butter? ** TODO drive back to the store and just buy a damn cake
The [2/4]
and 33%
and [3/6]
parts are all cookies. See the docs for a longer explanation.
Of course I don’t want to update them manually! org-mode does that for me, and there’s C-c # (invoking org-update-statistics-cookies
) just in case. Cookie updates happen as I complete items, both in checkbox lists and TODO
items.
Automatic Parent-Child State Updates
Very closely tied to this is an addition in my code that, when the cookie is updated, automatically completes the parent item.
* TODO complete these! [2/3]
- [X] first step
- [X] second step
- [ ] last step
Once I hit C-c C-c with the cursor in the last line to tick off the checkbox, the cookie is updated to [3/3]
and the whole item changes to this state:
* DONE complete these! [3/3]
- [X] first step
- [X] second step
- [X] last step
It transitions from TODO
to DONE
automatically.
When I untick a checkbox, it switches back to TODO
again, too.
This works the same when you replace checkboxes with sub-items where some are DONE
and some are still TODO
. The cookie policies shown above apply.
Introducing an Intermediate State to the Auto-Update
With the new DOING
state, I needed the cookie update code to change a bit, because when I set an item to DOING
and ticked off a checkbox, my code would see that it wasn’t yet finished and needed to transition to TODO
. The whole point of the DOING
state is to mark an item as work-in-progress and then keep it in that state.
So I changed the policy to take all three states into account. The implementation for sub-items with their own DONE
states is simpler, so I’ll show it first:
(defun ct/org-summary-todo-cookie (n-done n-not-done)
"Switch header state to DONE when all subentries are DONE, to TODO when none are DONE, and to DOING otherwise"
(let (org-log-done org-log-states) ; turn off logging
(org-todo (cond ((= n-done 0)
"TODO")
((= n-not-done 0)
"DONE")
(t
"DOING")))))
(add-hook 'org-after-todo-statistics-hook #'ct/org-summary-todo-cookie)
It simple because the hook already reports how many sub-items are done, and how many aren’t. (For checkboxes, I am going to need to parse the [x/y]
cookie value.)
org-log-done
andorg-log-states
are declared as local variables, overriding the global settings, and thus effectively turning off org-mode’s logging. I don’t use these and found it worked well to disable it here.org-todo
is a function that takes a new state and in this case updates the parent item. You don’t need to tell the function which item to update. The correct item is being activated, i.e. point moved there if needed, when the hook is called.- The trigger is in
org-after-todo-statistics-hook
, when the cookie is updated.
The three conditions are:
- When the
n-done
count ofDONE
sub-items is 0 after the last change, i.e. when I uncomplete the last completed sub-item, it may be not a work-in-progress anymore. I change the state toTODO
then. This doesn’t activate when there are no sub-items at all, because I need to toggle a sub-items state programmatically to make the hook execute. - When the
n-not-done
count of sub-items with other states, likeTODO
orDOING
, is 0, that means we’ve completed everything. This might be a bit roundabout because of the negation you have to do in your head: When there are no not-done items, all items are done. There’s no other way to express “are all items completed” available here. (If we hadn-total
andn-done
, we could test(= n-total n-done)
.) - The fallback/else clause: When 1 or more, but not all items are complete, then it’s a work in progress, so apply the
DOING
state.
How to Approach Auto-Updating the State (TODO/DOING/DONE) Based on Checkboxed
I already mentioned in passing that it’s a bit more work we have to do to achieve the same for checkboxes, because the built-in hooks don’t provide the same convenience. We don’t get a n-done
count for a “checkbox changed” hook.
Instead, we have to rely on the “cookie updated” hook. This requires use of a cookie in the first place. The sub-item approach above works with and without cookies.
I’ll show the complete code for everything below, but here’s the approach I stole from the aforementioned StackExchange post years ago and adapted to my 3-state requirements:
- Find the affected item’s line;
- Use regular expressions to extract the
[x%]
or[n/m]
cookies from the heading line; - Handle both percent and fractional cookies separately and update the state via
org-todo
like above.
The regex handling and the two cookie variants make the code a bit longer. Please see below for the implementation.
Complete Code
If you paste this into your init.el
, you’ll get my whole implementation:
- Change item state to
TODO
if no sub-items areDONE
, or if the cookie reports[0/m]
or[0%]
completion. - Change item state to
DOING
when one but not all sub-items areDONE
, or when the cookie contains a value above 0% and below 100% (aka for[n/m]
wheren < m
andn > 0
). - Change item state to
DONE
when all sub-items areDONE
, or if the cookie reports[100%]
or[m/m]
.
Please note that none of the hooks this relies on are called if you type the changes. If you type D-O-N-E for DONE
, none of the org-mode facilities will note the state change.
You need to go through the interactive org-todo
state change function (C-c C-t) or the Shift+Arrow_keys based state cycling to trigger hooks on the parent item in the outline.
For checkboxes, you need to tick them off with C-c C-c.
In case you forgot this and now nothing’s up-to-date anymore, you can trigger a cookie refresh by hitting C-c C-c with the cursor inside the cookie. So don’t be afraid.
(defun org-todo-if-needed (state)
"Change header state to STATE unless the current item is in STATE already."
(unless (string-equal (org-get-todo-state) state)
(org-todo state)))
(defun ct/org-summary-todo-cookie (n-done n-not-done)
"Switch header state to DONE when all subentries are DONE, to TODO when none are DONE, and to DOING otherwise"
(let (org-log-done org-log-states) ; turn off logging
(org-todo-if-needed (cond ((= n-done 0)
"TODO")
((= n-not-done 0)
"DONE")
(t
"DOING")))))
(add-hook 'org-after-todo-statistics-hook #'ct/org-summary-todo-cookie)
(defun ct/org-summary-checkbox-cookie ()
"Switch header state to DONE when all checkboxes are ticked, to TODO when none are ticked, and to DOING otherwise"
(let (beg end)
(unless (not (org-get-todo-state))
(save-excursion
(org-back-to-heading t)
(setq beg (point))
(end-of-line)
(setq end (point))
(goto-char beg)
;; Regex group 1: %-based cookie
;; Regex group 2 and 3: x/y cookie
(if (re-search-forward "\\[\\([0-9]*%\\)\\]\\|\\[\\([0-9]*\\)/\\([0-9]*\\)\\]"
end t)
(if (match-end 1)
;; [xx%] cookie support
(cond ((equal (match-string 1) "100%")
(org-todo-if-needed "DONE"))
((equal (match-string 1) "0%")
(org-todo-if-needed "TODO"))
(t
(org-todo-if-needed "DOING")))
;; [x/y] cookie support
(if (> (match-end 2) (match-beginning 2)) ; = if not empty
(cond ((equal (match-string 2) (match-string 3))
(org-todo-if-needed "DONE"))
((or (equal (string-trim (match-string 2)) "")
(equal (match-string 2) "0"))
(org-todo-if-needed "TODO"))
(t
(org-todo-if-needed "DOING")))
(org-todo-if-needed "DOING"))))))))
(add-hook 'org-checkbox-statistics-hook #'ct/org-summary-checkbox-cookie)
Possible Improvements
The hook for checkbox state updates is different. But to figure out if the item holding all checkboxes is complete, a cookie is required. Checkbox-ticking should also trigger the cookie statistics hook, though. So I think both implementations could be merged into one callback.
The basic code is 3 years old now. I guess org-mode v9.4 comes with new stuff that helps dealing with this, but which I haven’t discovered yet.
Are there any suggestions from your side, dear reader?