Funding Open Source Software as a Third Party?

In a Discord chat, we’ve recently talked about how well funding for Blender turned out. At the time of writing, they get $137k per month for development. I cannot say if that’s enough or too little. But it’s not nothing.

Being crowd-funded comes with its perils. Especially with free open-source software like Blender, developers tell that it’s not easy to know which user base to focus on, which UI/UX compromises to make, and how to figure out if the project backers are satisfied with the result.

It’s not trivial when you sell apps, either, but there you at least do know which people to listen to if in doubt: your loyal customers.

We wondered how hard it’d be to set up a successful funding pool for other projects, like Emacs. There’s no clear goal, but there’s a ton of bugs to fix and things to maintain, and a couple extra dollars don’t hurt to make it possible to put in more work and at the same time not starve.

Management of expectations, direction of funds, singling out of developers to support – all that would be extra effort. While there’s a FSF fund, there’s no fund that directs your money towards Emacs development. It could go anywhere, split in many ways. So even the money transfer, which is the easiest part, is not simple in this case. It doesn’t suffice to collect money in a pool and then forward the funds to the FSF. (This set-up is also the prime motivation to even talk about crowd-funding Emacs development: because you can’t already do it properly.)

Now making the money available to the software maintainers requires a platform to manage these people. That’s an extra step that sucks. As if raising funds wouldn’t be hard enough already.

Managing people is not a trivial task, either. What if some Emacs user has enough of the daily shortcomings of the editor, whips up a couple of work-in-progress improvements, but is totally new to the scene? How do you compensate these kinds of efforts? They’re not part of the ‘accepted developer pool’, so they won’t get a share of the funds. Do you even want to compensate contributors that way? Would it turn the FOSS spirit on its head and shift the focus from “how can I make this project better” to “how can I make money from this”?

In the end, I’d imagine that managing expectations of backers would be the easiest part, combined with setting up a way to collect and then forward the money. – Legal implications might prove me wrong quickly. Maybe you need a pro-bono organization as a legal entity to manage and forwards funds correctly. More overhead. But customer expectations could be managed by making clear from the get-go that the pool of funds is not a custom order form for your favorite Emacs feature.

Here’s what I imagine: Backers can leave a vote and tell devs what they are most interested in. But this is not a promise by the devs. It’s a mutual “feeling out”. A telling of needs and wants. It’s by no means intended to bind devs (legally, morally, or otherwise) to implement that stuff. In the spirit of patronage, devs shall receive money and do good deeds and improve the software, but not be forced to fulfill someone else’s wishes. Might even be worth keeping the developers secret to avoid their getting bugged by angry backers.

Is the best case scenario then that this fund attracts hundreds of thousands of dollars per month? Would this create a political problem because now there’s a well-funded “power” that wants to have a say in the development of Emacs, and would there be conflict with the vision of the FSF, and veteran pro-bono Emacs maintainers? I’d be surprised if there would not be any trouble.

So funding open source projects like Emacs as a 3rd party is not an easy task. It’s simple to set up, but it’s by no means a smooth ride to make it work.

The best thing that could happen is if the FSF offered a way to directly fund Emacs development. But that only means the FSF would have to figure out how to split the funds among developers. And their problem would be that there already are a lot of active developers known to the FSF, so this would have to be figured out from the start for everyone involved, lest they create a caste system of paid and unpaid devs. It might be easier when there’s “a lot” of monthly funds to share. But if you only have a single slice of pizza, how many more ways can you cut it before you might as well not bother trying?

The Archive 3rd Anniversary Giveaway

The confetti looks much nicer in 60 FPS on the real page. Mhmmm confetti…

Our note-taking app for macOS, The Archive, turns 3 years old this week. To celebrate, we wanted to do something nice to all the people who supported us during that time.

The thing we came up with is a giveaway. But every customer of the past 3 years wins automatically. The price is 1 free license to freely gift to someone else.

The only condition is that you purchased the app before I uploaded everything today, and that’s it. And that you enter before April rolls around, because then we’ll take down the Claim-O-Matic.

Go to the give-away page and enter your email, then you get a link you can share with the ultimate recipient!


It was really fun to create the website for this giveaway. I also find myself staring at the confetti because it’s so soothing to see how it travels downward. I hope you enjoy the experience, too, and that you know someone who would benefit from a tasteful note-taking app in 2021 with no strings attached.

The Beauty of Hacking Swift: Make Union of Set Algebra Types More Obvious

I found it weird to form the union of two CharacterSet instances by calling the union method on one element:

CharacterSet.urlHostAllowed.union(.urlPathAllowed)

This chains nicely, but what pops out to me looking at the line of code is the CharacterSet, then something about URLs, and then I scan the line to see what kind of statement this is – some lengthy stuff that looks like chained method calls at first glance due to the length and the many dots.

Instead, I prefer a static method/function:

extension SetAlgebra {
    static func union(_ sets: Self...) -> Self {
        guard let first = sets.first else { return self.init() }
        return sets.dropFirst().reduce(first) { $0.union($1) }
    }
}

That way, I can write:

CharacterSet.union(.urlHostAllowed, .urlPathAllowed)

… and thus I can highlight the union operator in my code instead of bringing attention to the first set in the union. This gets even better when you add more sets into the union.

This is no segue to hate OOP and method calls. I think the traditional OOP style works fine for a lot of cases. But functions work better for some operator tasks. (I’m not ready to make this a generic free function just yet, but think it’s a sensible next step.)

This kind of customization is by no means ground-breaking. But it’s one of the things we can do in Swift that make working with Swift enjoyable. It’s the ‘hacking’ with the language. I don’t have to cook my own unionizing type, I can just amend the existing code a bit to suit my, well, tastes. Because functionally, the code is equivalent. And the reducer is probably harder to read if you don’t know how reducers work (hello past-me from 5 years ago!). But the surface API is much nicer and doesn’t require any arcane skills.

With recent events on my computer, this reminds me a lot of the hackability of Emacs Lisp and thus the editor I’m using to write this while it’s running, and of course Smalltalk and its IDE, and Ruby. It works differently, but it’s a similar kind of joy of hacking.

MacSymbolicator: Tool to Symbolicate Your Crash Reports

I am not good at reading crash logs of my apps. Some errors are obvious, like index out of bounds exceptions. Others require actual symbolication of the crash log to reveal the symbol aka function name in the stack trace. You can do this in the command line and interactively explore the crash reasons like a caveperson.

Or you use a converter app.

I didn’t have any for quite a while, but was frustrated enough again today to search for an app.

MacSymbolicator 2.0 makes symbolication of crash reports super easy.

Enter MacSymbolicator 2.0!

It’s a window with two drag targets: one for the crash, and the other for the dSYM to resolve symbols from memory addresses.

The app even tries to find the dSYM for you. In my case, the crash log was for a previous app version but MacSymbolicator pulled up the dSYM for a newer version. Easy to fix, though:

  • head to your .xcarchive by revealing it in Finder from Xcode’s “Organizer” window,
  • show its package contents,
  • go to the dSYMs/ subdirectory,
  • drag the app’s .dsym file into the right-hand pane.

It’s open source and just works. Lovely. Big shoutout to Mahdi Bchatnia, creator of this tool!

Visit his website for a binary download of the app, or GitHub for the source.

Refactoring Case Change Code to Idiomatic Emacs Lisp

I asked Xah Lee for feedback on my case changing functions. He’s fluent in Emacs Lisp, so I figured if he wanted to, he would’ve used my approach years ago. So there must be something I miss.

The factoring of the small helper functions don’t seem to be bad, but there are other reasons to design the text editor you use every day in one way or another:

Ergonomics: Limit modifier key usage

“[C]onsider the fact there are 100 often used commands but only say 50 good key spots” – with the default Emacs key bindings re-bound to my functions, I need 3 keys and hold a modifier to access them (M-c, M-u, M-d).

Xah bound this oft used function to b to change the case (he’s using modal input, like vim, so it’s really just that key, no modifiers involved). b b b is the worst it gets: hit an easy to reach key three times in a row to cycle through all options, and a single b is the best case.

Holding modifier keys while typing a letter is commonplace, but not as good. Put what you use most often at your fingertips.

This reminds me a bit about the appeal of programmable keyboards with multiple layers. Some folk swear by the ability to enter a specific context and make the keys mean something different while they stay there. Modal key maps in editors aren’t much different from a pragmatic point of view.

Could it pay off to enable optional “editing mode” key bindings?

Weird Lisp

To recap, here’s the core function I introduced that would enable Elisp programmers to act on words:

(defun ct/word-boundary-at-point-or-region (&optional callback)
  (let ((deactivate-mark nil)
        $p1 $p2)
    (if (use-region-p)
        (setq $p1 (region-beginning)
              $p2 (region-end))
      (save-excursion
        (skip-chars-backward "[:alpha:]")
        (setq $p1 (point))
        (skip-chars-forward "[:alpha:]")
        (setq $p2 (point))))
    (when callback
      (funcall callback $p1 $p2))
    (list $p1 $p2)))

Xah’s main criticism is that this command’s name is bad and that it introduces complex patterns.

Passing functions apparently isn’t that common in Emacs Lisp. Of course it’s doable. “Function pointers”, let’s call these, are used to wire keyboard shortcuts all the time. It seems that people prefer to write simple functions with input and output.

I really, really like that the function above takes an optional callback and passes the result to it, too, if present, because then you can decorate functions easily. Since I discovered “East-Oriented Programming”, I was hooked by the concept. Really helped me solidify object-oriented programming a bit more.

But Lisp isn’t an OO language.

Choice of language also ties into the other criticism: naming the function. It felt odd, and sure enough, at least 1 other person I respect for their experience agrees.

Here’s a call-site:

(defun ct/capitalize-word-at-point ()
  (interactive)
  (ct/word-boundary-at-point-or-region #'upcase-initials-region))

The offending line is (ct/word-boundary-at-point-or-region #'upcase-initials-region)).

In Ruby, for example, I wouldn’t mind a block for this, because it’d include do...end in the syntax, so while it’s still not ideal, it still reads like “act on this”:

word_boundary_at_point_or_region do |region|
    upcase_initials(region)
end

That’s not how I’d write idiomatic Ruby, but a mostly literal translation still works better.

Same in Swift or Objective-C, where I learned to value named parameters, adding something non-descript like “handle” helps:

wordBoundaryAtPointOrRegion(handle: { region in
    upcaseInitials(region)
})

Emacs Lisp doesn’t seem to favor function composition operators, like piping or applying, so we don’t get to express it like this: (ct/word-boundary-at-point-or-region |> upcase-initials-region).

Idiomatic back-and-forth Lisp seems to favor this classic approach of the returned value instead:

(defun ct/capitalize-word-at-point ()
  (interactive)
  (let* (($bounds (ct/word-boundary-at-point-or-region))
         ($p1 (car $bounds))
         ($p2 (cadr $bounds)))
    (upcase-initials-region $p1 $p2)))

Simplifying, the fundamental element of Emacs Lisp is the list, not the function. With Ruby, it’s objects all the way down, so you design code differently.

I remember I asked Avdi Grimm one day about code he published, and how he’d approach using OOP not in a web app but a long-running native application. His anwer stuck with me, it was: “First, I’d consider the language I’m using.” – And with it, the environment and standard library etc, but first, the language.

I can pass functions around in Emacs Lisp just fine. Should I do it? Maybe not always.

It’s not like repeating the 3 lines of unpacking the bounds into two points is a huge pain. I can repeat that part. Extracting that repeated code into a “apply 2 items from the list as parameters to a function” helper doesn’t make much sense, yet that’s basically what I did here.

Since I don’t like the name I ended up with, there’s splitting it into 2 variants like (ct/word-boundary-at-point-or-region) to return the values, and (ct/apply-word-boundary-at-point-or-region) for the callback forwarding. Also weird. I’ll try to roll with the simpler, albeit longer code.

Change Case of Word at Point in Emacs, But for Real This Time

At the moment, I’m proof-reading and editing the book manuscript of my pal Sascha for the new edition of the Zettelkasten Method book. As with most things text these days, I’m doing that with Emacs.

Something that continually drives me bonkers is how Emacs handles upcasing, downcasing, and capitalization of words by default. The functions that are called for the default key bindings are upcase-word, downcase-word, and capitalize-word. These sounds super useful to fix typos. The default behavior is odd, though: They only change the case of the whole word when you select the word first. Otherwise they change the case of the remainder of the word beginning at the character at the insertion point. The docstrings say as much: “Capitalize from point to the end of word, moving over.” Why?

Anything before the insertion point is ignored; and when capitalizing, only 1 character is actually changed.

So the functions are aware of my intention to change the word. Why don’t they start at its beginning?

I can understand the underlying functions involved here that act on the region aka selection of the user. They usually expect 2 parameters, the start and end of the region where the effect should be applied. That’s super useful to compose effects with other functions because of its general nature.

The “convenient” behavior of the key-bound functions to change the case for the remainder of the word puzzles me, though. Is it because you can pass numerical parameters to it to continue from point onward N words forward or backward? I dont’ know. Even then, why not start at the beginning while we’re at it? I would understand not acting on words at all without a selection, and just changing the case of character at the insertion point’s location then. But this?!

Xah Lee, who seemingly has done every conceivable thing you can do to Emacs in the past 20 years, implemented his own ‘toggle letter case’ function that does what maybe not every Emacs Lisp programmer, but any writer would expect: to act on the whole word.

He opted to figure out word boundaries via the [:alpha:] regular expression. That’s maybe not always enough, but it’s good enough for typing text. And, unless “thing at point”, it is consistent. Every mode can redefine what a “word” means in its context. (Which is useful on its own, but not helping to keep downcasing predictable.)

I changed Xah’s code a bit, because I don’t want to cycle through cases interactively. I’d rather hit M-u for ALL CAPS upcasing once.

Imagine a helper function ct/word-boundary-at-point-or-region that returns 2 character locations: the start and end of either the current region (i.e. selection in emacs) or the word below the insertion point.

The return value could be (100 110) for a 10 character word that starts at offset 100. The position of the insertion point notably doesn’t matter.

You can upcase a word like this, using car to get the first element of the returned list value, and cadr (aka (car (cdr x)))) to get the last element.1

Here’s a function that would utilize this to fetch both points and then capitalize the region:

(defun ct/capitalize-word-at-point ()
  (interactive)
  (let* (($bounds (ct/word-boundary-at-point-or-region))
         ($p1 (car $bounds))
         ($p2 (cadr $bounds)))
    (upcase-initials-region $p1 $p2)))

I’d have to copy and paste that for all three case-changing functions I need. I’d rather extract the common theme here and change the approach to an adapter of sorts, if you pardon the OOP terminology when using Lisp.

The actual implementation of ct/word-boundary-at-point-or-region thus is implemented in a way to figure out the start and end points of a word, return these, but then also forward these to a callback, if that is provided.

Here’s the implementation, mostly copies from Xah’s excellent code:

(defun ct/word-boundary-at-point-or-region (&optional callback)
  "Return the boundary (beginning and end) of the word at point, or region, if any.
  Forwards the points to CALLBACK as (CALLBACK p1 p2), if present.

URL: https://christiantietze.de/posts/2021/03/change-case-of-word-at-point/"
  (let ((deactivate-mark nil)
        $p1 $p2)
    (if (use-region-p)
        (setq $p1 (region-beginning)
              $p2 (region-end))
      (save-excursion
        (skip-chars-backward "[:alpha:]")
        (setq $p1 (point))
        (skip-chars-forward "[:alpha:]")
        (setq $p2 (point))))
    (when callback
      (funcall callback $p1 $p2))
    (list $p1 $p2)))

Let me walk you through the parts here in case Lisp is odd for you to read:

  • When a region is active, use the region beginning and end points for $p1 and $p2.
  • When no region is active, move the insertion point to the beginning of the word, save that as $p1, skip to the end of the word, save that offset as $p2. (And restore the original position thanks to the save-excursion decorator.)
  • If a callback function is given, pass the two points.
  • Always return a tuple of points via (list $p1 $p2).

Now I can get the tuple of points if I need, or I can tell the function to call another function and forward these points.

The implementation thus shrinks down to one-liners:

(defun ct/capitalize-word-at-point ()
  (interactive)
  (ct/word-boundary-at-point-or-region #'upcase-initials-region))
(defun ct/downcase-word-at-point ()
  (interactive)
  (ct/word-boundary-at-point-or-region #'downcase-region))
(defun ct/upcase-word-at-point ()
  (interactive)
  (ct/word-boundary-at-point-or-region #'upcase-region))

;; Set global shortcuts
(global-set-key (kbd "M-c") #'ct/capitalize-word-at-point)
(global-set-key (kbd "M-u") #'ct/upcase-word-at-point)
(global-set-key (kbd "M-d") #'ct/downcase-word-at-point)

I prefer these one-liners over repeatedly unpacking 2 points frm a tuple that was returned.

The actual capitalization should maybe be implemented a bit different, though: upcase-initials-region only changes the case of the initials and leaves the remainder untouched, unlike capitalize-word which lowercases the rest. "fizzBUZZ" thus becomes "FizzBUZZ". My expectation is for the whole word to change, not just the initial characters, so I prefer to downcase the whole word first and then capitalize the initials for my current task:

(defun ct/capitalize-region (p1 p2)
  (downcase-region p1 p2)
  (upcase-initials-region p1 p2))
(defun ct/capitalize-word-at-point ()
  (interactive)
  (ct/word-boundary-at-point-or-region #'ct/capitalize-region))

I have to say I really like function composition.

By the way, I also evaluated to train myself to expand the selection to the current word first and then call the built-in case changing functions. There’s tools for that. But that sucks, and the default behavior of the built-in functions still is odd.


  1. If you’re new to Lisp, using only cdr is like a dropFirst call on an array, still returning a list, but with 1 element in this case. car then fetches this element. And cadr is a shorthand for this common combination to fetch the butt of a list, so to speak. 

#CamelCaseHashtags Could Improve Accessibility

If you’re using hashtags on Twitter, Instagram, or in your Zettelkasten – consider CamelCasing 1 the hashtags if there’s more than one word involved. That acually seems to help screen-readers with pronunciation.

Not that many Instagram photo hashtag lists are worthy of being read aloud. But it’s nice to know that this can help for actual textual content down the line. (Examples from trending tags I pulled on German Instagram and web comics include, but are not limited, to #sundayBumDay, #towardsMoreRealityOnInstagram, and wherever the word boundaries in #inspoforallgirls and #turtleneckfashionstatementaboutthefragilityofspacetime and #whoagonetoofarnow are hidden.)

Found out about this on Twitter by @ThatBlindLad.


  1. No need to split hairs about PascalCase vs camelCase since the capitalization of the 2nd word onward matters. 

How to Add Backlinks in Nanoc Static Site Generator

Since I’ve added backlinks to the bottom of posts today, I figured I might as well share the code.

I’m using static site generator nanoc. It has a preprocessing section in the compilation step where you can add new items and modify items that are read from disk further. I got the basis for this code from Denis Defreyne, creator of nanoc. (Check out his code.)

Denis’s code is actually very well suited to publish a set of notes with [[wiki link]] style links. If you want to create a static site wiki, definitely check out his approach.

This is a regular blog, though, so I modified the code a bit to match absolute URLs and relative URLs that involve the /posts path component. It also deals with the fact that some source files are called title-of-the-post.md, and others are title-of-the-post/index.md. I used to put them into folders to group them with images. But years later I wasn’t happy with that convention because now all files were called “index.md”. So nowadays I’m mixing both and only put images into a subdirectory of the same name as the post.

A lot of tweaks later, the code is surprisingly long, but works pretty well:

preprocess do
  def find_backlinks
    backlinks_to = {}
    skipped_notes = ["/posts/overview"]
    domain = config[:base_url]
    # Start with the domain, or be inside a (/...) Markdown inline link or a [...]: reference style link
    link_regex = /(?<=#{domain}\/|\(\/|\: \/)(.*?)(?:="|\)|\s|$)/

    def remove_trailing_slash(str)
      if str[-1..] == "/"
        str[...-1]
      else
        str
      end
    end

    def trim_index(str)
      if i = (str =~ %r{/index.*})
        str[...i]
      else
        str
      end
    end

    @items.find_all('/posts/**/*').each do |origin|
      next if origin.binary?
      next if skipped_notes.include?(origin.identifier.without_exts)
      # transform links into a format that looks like identifier.without_ext
      linked_paths = origin.raw_content
                       .scan(link_regex)
                       .map { |matches| matches[0] }
                       .map { "/" + remove_trailing_slash(_1) }
                       .map { trim_index(_1) }
      linked_paths.each do |linked_path|
        backlinks_to[linked_path] ||= []
        backlinks_to[linked_path] << origin.identifier
      end
    end

    @items.find_all('/posts/**/*').each do |target|
      next if target.binary?
      key = trim_index(target.identifier.without_exts)
      next if skipped_notes.include?(key)
      target[:backlinks] = backlinks_to.fetch(key, [])
    end
  end

  # call this before adding e.g. the tag index to limit the amount of @items
  find_backlinks
end

Then to render backlinks below the post, this is part of the layout template:

<% if @item[:backlinks].any? %>
  <aside id="backlinks">
    <h2>Links to this article</h2>
    <ul class="entries">
      <% @item[:backlinks].map { |id| @items[id] }.sort_by { _1[:title] }.each do |note| %>
        <li class="entry">
          <h3 class="title"><%= link_to(note[:title], note) %></h3>
          <%= Post::excerpt_for(note, no_readmore: true) %>
        </li>
    <% end %>
    </ul>
  </aside>
<% end %>

The Post::excerpt_for function is an old helper I wrote in 2013 or so. It takes the first whole paragraph of a post instead of X characters, cutting off mid-sentence. The no_readmore option turns of adding “Continue reading…” links below the excerpt text. You can use the built-in #excerptize text helper for that instead.

Backlinks Added to the Blog

Everytime I mention, think of, and link to Andy Matuschak’s public notes, I really like how each note displays a list of backlinks at the bottom.

In my note-taking, I don’t want backlinks to be added automatically into the content. I can get by with other means just fine to figure out what links to the current note.

But for a hypertextual publication, adding a list of backlinks at the bottom is a neat tool to explore more, to find the beginning of an article series, or related items in general.

So I added a little section to posts on this blog. Not every page, just the posts.

Example: Check out a post with a couple of backlinks, including this post: How Do You Activate Sparkle’s XPC Services?

I’ll keep this around for a while and see how it affects finding the stuff I’m usually looking for on my own blog. More cross-connections make discovery in a publication easier, I’d argue.

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 and org-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 of DONE 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 to TODO 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, like TODO or 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-total and 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 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 are DONE, or if the cookie reports [0/m] or [0%] completion.
  • Change item state to DOING when one but not all sub-items are DONE, or when the cookie contains a value above 0% and below 100% (aka for [n/m] where n < m and n > 0).
  • Change item state to DONE when all sub-items are DONE, 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?


→ Blog Archive