2023-03-11 Adding my TODOs to agenda

Last month I wrote about my way of managing TODOs. It occurred to me that I could make it even better. Why not add my set of TODOs to look into to my agenda? I have already constructed a regex matching them. Unfortunately, the section of the manual dealing with custom agenda views and search was a bit vague, so I decided to ask on the mailing list. Alas, I got no answers, which didn’t worry me too much – I just decided that I’ll try to find the answer myself.

Another thing I wanted to achieve was harnessing the randomness. I didn’t like the fact that the “middle” tasks were selected at random every time I generated the list, so I came up with the idea of seeding the random number generator with today’s date. Elisp’s random function allows for setting the seed, but only as a global state, so I decided to use cl​’s cl-random, which accepts the seed (more or less) as the second parameter. I paired this with (time-to-days nil) which gives the same integer every day.

This is still not ideal – if I add or remove an item from the TODO list, the count will be different so even on the same day, another set will be selected. I can live with that, though, especially that I don’t add (or remove) stuff from these lists all the time. My workflow is that whenever I find something interesting I’d like to dig deeper into later, I put it in the capture.org file. Then, usually once a day, I go through the new items there and assign them to various buckets.

Anyway, here is the code. An astute reader will notice that a few functions are changed from what what they used to be in the previous post – this is expected, since I now need a separate function producing just the list of strings to be fed either to the agenda or to the command showing just the tasks. Also, it turned ot that instead of the :title property, I need :raw-value, which gives very similar results, but not adorned by string properties etc. (For some reason, it broke in certain circumstances – I’m not sure why, since I didn’t investigate it deeper.)

  (require 'cl-lib)

  (defun org-get-first-random-last (first random last)
    "Return 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."
    (let* ((headlines (cdr (org-map-entries
                            (lambda ()
                              (org-element-property
                               :raw-value
                               (org-element-at-point)))
                            nil
                            'tree
                            'archive 'comment)))
           (length (length headlines))
           (head (seq-take headlines first))
           (tail (seq-drop headlines (- length last)))
           (random-state (cl-make-random-state
                          (time-to-days nil)))
           (belly (cl-loop repeat random
                           collect (seq-elt headlines
                                            (cl-random
                                             length
                                             random-state)))))
      (seq-concatenate 'list head belly tail)))

  (defun org-show-first-random-last (first random last)
    "Show FIRST first headlines, RANDOM random and LAST last ones.
See `org-get-first-random-last'."
    (interactive (let ((arg (prefix-numeric-value current-prefix-arg)))
                   (list arg arg arg)))
    (org-sparse-tree-from-list
     (org-get-first-random-last first random last)))

  (defun org-list-future-tasks (when count)
    "Return a list of future tasks.
  WHEN is a string matching a top headline in `future.org'.
  COUNT is the number of first, random and last tasks."
    (with-current-buffer "future.org"
      (save-excursion
        (goto-char (point-min))
        (let (pos)
          (while
              (progn
                (setq pos (point))
                (org-forward-heading-same-level 1 t)
                (and (/= pos (point))
                     (not (string= when
                                   (org-element-property
                                    :raw-value
                                    (org-element-at-point))))))))
        (org-get-first-random-last count count count))))

And here is my agenda command (with some irrelevant settings cut out):

(defun mbork-agenda-short ()
  "My personal agenda command."
  (interactive)
  (let ((org-agenda-span 15)
        (org-agenda-use-time-grid nil))
    (org-agenda-run-series
     "Agenda, tasks, projects"
     `(((agenda "")
        (search ,(format "{%s}"
                         (replace-regexp-in-string
                          "[{}]" "."
                          (regexp-opt
                           (org-list-future-tasks "Soon" 2))))))))))

You might be wondering about the mysterious replace-regexp-in-string. As I started to use this code, I noticed that it errors out when one of the selected entries contains curly braces. The reason is not hard to find – Org requires the regex to be put within curly braces, and apparently it cannot contain them inside. Maybe they can be escaped somehow – I tried, but failed. So I decided to go the easy route and substitute periods for them, in the hope that making the regex more allowing won’t match too many headlines. (And even if it does, it is not a big problem – it just means that on some days, I will see a few headlines more, something I can definitely live with.)

It is still not perfect – for example, the heading for the six tasks below the agenda proper is the constructed regex instead of something more descriptive. Org mode could allow for a purely decorative entry in the block agenda so that I could make it nicer visually. That, however, is not a necessary thing, and I can definitely live without it.

So, from now on my agenda is richer. We’ll see how that helps me accomplish my goals!

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryOrgMode