Preventing checkbox inheritance in org-mode

1. Background

One feature in org-mode that I've wished I could customise for a long time is the checkbox inheritance feature. Org's behaviour is that if I have a list like this…

- [ ] This is a parent item
  - [ ] This is a child nested item

…then, as soon as I check all the child items as [X], the parent item will automatically be updated to [X] too.

This doesn't fit with how I like to use nested checkboxes: generally I do use them to represent a kind of "subtask" concept, but it's not necessarily the full set of items that make up the "parent" task, and completing them doesn't mean I'm done with the parent item. I'd prefer to be able to manually trigger the two checkboxes independently: changing state on the child item shouldn't affect the parent.

2. Fixing it by disabling org-list-struct-fix-box

A lot of org-mode's behaviour can be customised via variables, hooks etc. However, this particular behaviour doesn't seem to be customisable, and when searching I've not found any immediate solutions or ideas that people use to achieve this.

After some poking around in the source this week, I came up with something that seems to work for me: this checkbox behaviour is implemented in a function named org-list-struct-fix-box. That's the only thing this function does, and so if we turn this function into a no-op, then I get the behaviour that I want: updating child checkbox items has no affect on the parent.

To achieve this, I define advice[1] around the original function that prevents the original implementation from being called:

(defadvice org-list-struct-fix-box (around md/noop last activate)
    "Turn org-list-struct-fix-box into a no-op.

By default, if an org list item is checked using the square-bracket
syntax [X], then org will look for a parent checkbox, and if all child items are
checked, it will set [X] on the parent too. This isn't how I personally use
child items -- I'll often use child checkboxes as subtasks, but it's almost
never an exhaustive list of everything that has to be done to close out the
parent -- and so I'd prefer to just control the parent checkbox state manually.

AFAICT org-mode doesn't provide a way to customise this behaviour, /but/ the
behaviour all seems to be implemented in 'org-list-struct-fix-box'. And so I'm
trying something out by turning it into a no-op. It seems to work nicely initially,
but I won't be surprised if it causes an issue at some point because it's very
hacky."
nil)

That's it: all this time and I only had to write one line of code to achieve what I want.

3. Why use advice over defun?

Instead of advice, another approach would be to just redefine the original function using defun. I prefer to use advice though, so I can still see the original function, still jump to its definition, explicitly know that it exists and that I'm modifying it, etc. If I use describe-function on org-list-struct-fix-box, it will tell me that the function is advised, and as I'm using the excellent helpful package, I can see additional information including the definition of both the original function and the advice.

4. Isn't this pretty hacky?

Yes. I've only been using it for a few days. org-list-struct-fix-box doesn't seem to be called in many places though, so hopefully nothing major will break. The most likely issue is that I break third-party packages that depend on the original behaviour, but I don't think any of the packages I'm using are likely to depend on this particular function.

I think this kind of approach can be risky in a prod-environment program that other people depend on, but as it's just my personal config, it's fine – I'm not sure I'd go as far to say it's encouraged, but this "advice" patching concept is one of the tools provided by Emacs to customise a module's behaviour for this exact kind of situation, where you otherwise don't have the ability to achieve the behaviour you want without redefining whole functions.

5. Contributing upstream?

It doesn't seem far-fetched to me that this could be contributed upstream: not as advice, but as a new custom variable that can be used to disable plain checklist inheritance – you might even be able to get away with an implementation that just looks at the value of this new variable in org-list-struct-fix-box.

Another more complex approach would be to support new syntax for opting into the "inheritance" behaviour on a per-list basis, similar to how you can set your checkbox value to [/] to have it automatically show the count of completed child items.

6. Am I missing something?

If you're aware of a way to achieve this without patching, please let me know!

You can find my config in my dotfiles repo.

2023-Aug-25