2023-06-12 Counting time, backwards

Some time ago I thought that I would like to have a feature Org mode doesn’t seem to have. There are things I would like to do once in a while, but I don’t need a reminder with a hard deadline (like a repeating one) – I’d prefer a gentle nudge. What if I could display a list of things I select, each one with information how long ago I did it last? For example, “Mowing the lawn [21 days ago]”. Given that Org mode allows for timestamps with repeaters, and the time a task was done can be recorded in the :LOGBOOK: drawer, I can assume that the most recent timestamp in the task is the date when I last did it.

Some time ago I said to someone that Org mode is “a platform to build productivity workflows” (much like Emacs is a platform to build text-centric workflows”). So, let’s build ourselves a workflow;-).

First of all, I need a function to tell me the “age” (in days) of the most recent timestamp in the current subtree. After a bit of poking around in Org mode function list I came up with this:

(defun org-ago--compute-last-time ()
  "Age in days from the last timestamp in the current subtree."
  (save-excursion
    (save-restriction
      (org-narrow-to-subtree)
      (goto-char (point-min))
      (let ((regex (org-re-timestamp 'all))
            ago found
            (acc most-positive-fixnum))
        (while (re-search-forward regex nil t)
          (setq ago (- (org-time-stamp-to-now (match-string 0))))
          (when (< ago acc)
            (setq acc ago)
            (setq found t)))
        (if found acc -1)))))

I could perhaps make it prettier with the cl-loop macro, but it’s ok for me.

Now I need to map through all headlines in my agenda files, looking for the ones that have, say, a last_entry_threshold property. For every one that does, my code will check if its most recent timestamp was at least as many days ago as that property says. If yes, it will collect that headline (that is, its text and the computed number of days).

(defun org-ago--find-last-time-entries (scope)
  "Find the entries to show the last time."
  (cl-remove-if-not #'identity
   (org-map-entries
    (lambda ()
      (let ((ago (org-compute-last-time)))
        (when (>= ago
                  (string-to-number
                   (org-entry-get (point) "last_entry_threshold")))
          (cons (substring-no-properties (org-get-heading t t t t))
                ago))))
    "+last_entry_threshold>0"
    scope
    'archive 'comment)))

Finally, I want to show these data nicely formatted for the user.

(defun org-ago-list-entries ()
  "List entries requiring a reminder in a temp buffer."
  (interactive)
  (display-message-or-buffer
   (mapconcat
    (lambda (entry)
      (format "%s [%s days ago]" (car entry) (cdr entry)))
    (org-ago--find-last-time-entries 'agenda)
    "\n")))

And that’s it! Before I wrap it up, let me mention two things. First of all, it would be much nicer if I could incorporate this into my agenda. The trick I used earlier won’t work here, since I need Emacs to show me how many days ago a particular headline “last happened”. I don’t think there is a function to include items in an Org mode agenda based on a given predicate function, but I’ll look for some way to do it. The second thing I’d like to mention is that if you envy me and you’d like to be able to make your Emacs life easier by coding little utilities like this, too, I have some good news for you (although I’ll admit that at this moment this is rather an old news…). You can read the excellent book by the late Robert J. Chassell’s entitled Introduction to programming in Emacs Lisp, and if you want, you can follow it up with my book about Emacs Lisp.

Happy coding!

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryOrgMode