2023-02-18 My approach to TODOs

Some time ago I described my productivity system. I also mentioned that it is still evolving. One of the aspects where it needed some evolution is handling TODOs – and this is precisely the stuff I am working on right now.

Now the main problem I’ve been having for a long time now is that I tend to add a lot of TODOs to my list, which grows much faster than I deal with them. The worst thing here is that when I have so many TODOs lumped together, important and urgent things often get mixed up with unimportant and not urgent ones, etc.

So, I devised a way to handle this situation. I decided that there are basically 4 types of TODOs (at least in my life). One is just the tasks that need to be done before certain date (or time), like paying the bills or, say, booking holidays. These ones are generally easy to handle with a calendar (either a paper one or an electronic one). Some of them are recurring (bills), some are one-time (holidays), but every decent tool supports both types, and Org Mode (which is my tool of choice, of course) is no exception.

Then there is the other end of the spectrum, which is things I’d like to do “some day” (like watching some mildly interesting video lecture). I can do them today, or in week’s time, or next year, or never, and that’s still ok. These could be just TODO items in some Org file. Many interesting articles I find on the web are of this category, and generally I have no problem with the fact that this list grows faster than I tick these off the list.

There is also a middle ground of things I’d like to do in the near future – maybe in a week or two, maybe in a few months – but sooner rather than later. This is the list I would really prefer to keep at bay.

The last type is things I’d like to do every once in a while, but not necessarily on a regular basis. This may seem strange, but it does make sense. An example would be reading an inspirational blog post – it takes just a few minutes, but it doesn’t make any sense to read it regularly every day, every month or even every year – just being reminded of it from time to time is good enough. (This is the category I want to consider later. Small steps!)

And so, I decided to do something about my ever-growing TODO list. I created a new Org file, called future.org, with two headlines, Soon and Some day. I configured both as refile targets, using the following setting:

(setq org-refile-targets
      '((org-agenda-files . (:tag . "refile"))))

This way, every heading with a :refile: tag in my agenda files is a potential refile target (which makes sense, since I have very few places I want to refile things to, but they are scattered across files and levels).

The last (though not least!) piece of the puzzle is some Elisp code to tell me what to look at when I have some spare time. I explicitly don’t want things from future.org to be shown in my agenda (so these tasks don’t get timestamps), but I want to have a way to see tasks I might want to do (and check how many of them there are in both lists). The way I want it is to be shown the first few ones (so that I get a nudge to deal with the oldest ones – otherwise they will show up again and again and pester me), the last few ones (which are the ones I added most recently, so they may be still fresh in my memory, and I get a nudge to deal with them and not let my list to grow uncontrollably), and a few random ones (so that a bit of serendipity is thrown in, too).

(require 'cl-lib)

(defun org-sparse-tree-from-list (headlines)
  "Show a sparse tree of headlines matching strings in HEADLINES.
Only select them from the current subtree.  This is not optimal -
for example, if one of the strings in HEADLINES matches
a substring of a headline, it is still shown."
  (save-restriction
    (org-narrow-to-subtree)
    (org-occur (regexp-opt headlines))
    (org-remove-occur-highlights nil nil t)))

(defun org-show-first-random-last (first random last)
  "Show FIRST first headlines, RANDOM random and LAST last ones.
For simplicity, the random ones are chosen from all of them,
including the first/last ones.  Also, headlines on all levels are
considered, effectively flattening the current subtree for the
purpose of finding the ones to show."
  (interactive (let ((arg (prefix-numeric-value current-prefix-arg)))
                 (list arg arg arg)))
  (let* ((headlines (cdr (org-map-entries
                          (lambda ()
                            (org-element-property
                             :title
                             (org-element-at-point)))
                          nil
                          'tree
                          'archive 'comment)))
         (length (length headlines))
         (head (seq-take headlines first))
         (tail (seq-drop headlines (- length last)))
         (belly (cl-loop repeat random
                         collect (seq-random-elt headlines))))
    (org-sparse-tree-from-list
     (seq-concatenate 'list head belly tail))))

There are probably many ways of doing this, and the one I’ve chosen is definitely not optimal performance-wise – for example, it needlessly traverses the headline list many times. This doesn’t matter that much – since I’m going to use it interactively (and no more often than a few times a day at most), even if it takes a second or two, I’m ok with that. (And I’ve just tested my code on a file with more than a thousand entries, and it didn’t even need a second.)

As mentioned in the docstrings, this code has a few oversimplifications. If a headline title is a substring of another one, both will match and be shown. All levels are effectively flattened (this could be remedied pretty easily, but I don’t intend to run this on nested trees, so it doesn’t matter to me).

There are a few things about this code I’d like to highlight. The first one is that I decided to use the sparse tree mechanism Org mode provides – specifically, the regex variant (org-occur), but I didn’t need (nor want) to have the highlighting of the matched texts (it wouldn’t make sense in my use case anyway). Finding the necessary functions took me a few minutes of C-h magic.

Another one is the interactive clause which uses the prefix argument to decide how many entries should be shown. Since the “default” value of the prefix argument is one, M-x org-show-first-random-last will show just 3 entries (the first one, the last one and a random one – of course, if the random one happens to be the first or the last, only two will be shown, and if there is only one entry in the list, it will be the only one shown). If I want more entries, I can say C-u M-x org-show-first-random-last to get 12, or, say, C-u 2 M-x org-show-first-random-last to get 6 etc.

Yet another one is the very useful regexp-opt function. It accepts a list of strings and returns a regex matching every string on the list and nothing else. It is not something you would use often, but when you do, it is indispensable.

Lastly, notice the cl-loop macro I used. Since I knew the number of times I want to select a headline at random and I wanted the list of them, I figured it is the easiest way to do this. (Thanks to Irreal for reminding me about the loop macro!) I could also use dotimes with push, of course, which would be probably more Lispy, but a bit more verbose, too.

And that’s it for today! As usual, Emacs (and Org mode) can pretty easily accomodate for various workflows and needs in a very time-efficient way (it took me about half an hour to research Org sparse trees, decide to use org-occur and implement my solution). Let me also remind you that you can also learn to write such little useful functions – and one of the best ways to learn it is to start with the late Robert J. Chassell’s Introduction to programming in Emacs Lisp. You can also follow it with my book about Emacs Lisp.

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryOrgMode