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:
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.
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
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
[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
[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
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
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-statesare 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-todois 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
DONEsub-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 to
TODOthen. 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-donecount of sub-items with other states, like
DOING, 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 had
n-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
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
[n/m]cookies from the heading line;
- Handle both percent and fractional cookies separately and update the state via
The regex handling and the two cookie variants make the code a bit longer. Please see below for the implementation.
If you paste this into your
init.el, you’ll get my whole implementation:
- Change item state to
TODOif no sub-items are
DONE, or if the cookie reports
- Change item state to
DOINGwhen one but not all sub-items are
DONE, or when the cookie contains a value above 0% and below 100% (aka for
n < mand
n > 0).
- Change item state to
DONEwhen all sub-items are
DONE, or if the cookie reports
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)
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?