Parsing Time

Building my ideal Emacs journal

Parsing Time

The pen is mightier than the org?

I have been drifting away from the brilliant org-mode and org-roam knowledge-graph-noting over the past month. A significant part of that, to be fair, is down to owning a shiny new fountain pen that I just love writing with, and will find any excuse to do so. At the same time, something about org-roam, and specifically the dailies feature, just wasn't completely working for me. Hence I switched completely away to a written notebook for a month or so.

As much as I love writing on paper, it is a pain having to flick back through looking for stuff in the middle of a meeting. I need something more searchable. org-roam-dailies seem like a shoe-in for this. However, when writing the actual notes, I found I rarely actually used links in the places that actually would have ended up being useful to me later. Inevitably, I'd end up consult-ripgrep-ing my entire knowledge base of notes for a term that I didn't have the foresight to make into an org-roam link.

Apart from this, I was attempting to track my time through the org-mode agenda as well, with varying success. The clock-table view (obtained by hitting R while point is over the agenda) was handy for filling time sheets, but often it wasn't very accurate. I'd either forget to log time completely, or forget to clock out after finishing, leaving me with 10s of hours of time logged on tasks that I didn't do.

I started cooking up an idea in my head between various showers and daydreaming moments for an Emacs application where I could just log everything I do in a no-nonsense linear way, much like I was in my pen-and-paper notebook. I'd just hit a key combo, type a few words about what I was doing, and Emacs would automatically clock me out of the last task I was doing and then in to the new task. Then I'd be able to put some journal notes in under each entry, particularly under meeting entries or debugging investigations.

This is when I found org-journal. After trying it out, I found that it was already pretty close to what I had in my head, and the simplicity was refreshing (I find that over time I tend to gravitate to the simpler packages - eglot over lsp-mode, for example). It seemed like an ideal base to build from to realise the thing I had been constructing in my mind.

Hacking org-journal

My goals were as follows:

At first I thought I might have to modify the org-journal source code itself to accomplish this, so as a first step I cloned it down and quickly found the org-journal-new-entry function that I would be adding most of my new functionality to.

I quickly realised however that this was unnecessary. I didn't even need to hook in to the org-journal-after-entry-hook - I could just add a custom wrapper function for the whole thing that would do a few bits and pieces before and after the actual org-journal-new-entry function call.

Creating journal entries from tasks

The first thing I set out to add to my wrapper function was functionality to grab a TODO item from the current point of the cursor, and add a link back to that item as the title of the new journal entry.

You can get the various properties of an org heading with the handy org-entry-properties function, which returns a key-value list of properties like the TODO status, priority, or text of the heading. To get the value of one of the properties, you can use something like the following:

(org-entry-get nil "ITEM") ;; gets the title text of the heading
(org-entry-get nil "TODO") ;; gets the todo status of the heading

This functionality was useful for making sure that the cursor was in a TODO heading before clocking in to it and adding the link.

Actually capturing the link to the original TODO item was a little more challenging. I opted to use org-store-link, which puts a link to the current org heading in org-stored-links for recalling later with org-insert-link. It wasn't originally working for me, because the interactive? argument needed to be set to t in order for the org-stored-links variable to get updated. I feel like since I wasn't really using it in an interactive context, perhaps I did the wrong thing here, but the resulting code works well enough for my purposes.

Clocking in and out was simple - I just had to use org-clock-in directly in my code before the org-journal-new-entry function was called and moved the cursor to the new journal entry. There was no need to use org-clock-out at this point, since org-clock-in automatically clocks out the previous task.

Different types of journal entries

For supporting multiple types of entries, I made up some custom symbols that would represent each type: rsws/org-journal-entry-type--task for a task, rsws/org-journal-entry-type--break for a break, et cetera. I used cond expressions, elisp's answer for switch-case statements, to modify the behaviour of the wrapper function subtly for each type:

I hooked up the wonderful general package to create prefixed key bindings for each of these types of entries like so:

(general-define-key
  :prefix "C-c j"
  "j" (lambda () (interactive) (rsws/org-journal-new-entry 'rsws/org-journal-entry-type--note))
  "t" (lambda () (interactive) (rsws/org-journal-new-entry 'rsws/org-journal-entry-type--task))
  "m" (lambda () (interactive) (rsws/org-journal-new-entry 'rsws/org-journal-entry-type--meeting))
  "b" (lambda () (interactive) (rsws/org-journal-new-entry 'rsws/org-journal-entry-type--break)))

Creating journal entries from agenda tasks

Shortly after starting to use this system for my actual work, I found a glaring shortcoming - I couldn't create journal entries from tasks on org-agenda! I pretty much interact with my tasks exclusively through the agenda, so this wouldn't do.

The issue was that my (if (not (org-entry-get nil "TODO")) check doesn't work if run from the agenda. I figured that being inside the agenda was enough of a check for me, so I added a clause to this check to allow it to pass if the buffer-name matched the org-agenda-buffer-name. Now the check passes and I can add journal entries from tasks on the agenda!

Well.. not quite. org-clock-in doesn't work on the agenda, which was a surprise considering I had only ever used clocking in through the agenda before (using key I while point was at a task). A quick C-h k I later and it turns out what I was using was actually org-agenda-clock-in. I guess I'll need to use that buffer-name check again to decide which function to use.

Conclusion

And with that, I'm all done on the first part of this project. Of course, I still have a lot more ideas for improving this - for instance, it would be cool to be given a vertico-style list of tasks to pick from if the point isn't currently over a task and I want to create a task-based journal entry. I'd also like to build some reporting on top of this - what percentage of time do I spent in meetings versus project work for example?

If you're interested, you can find my code for these features on my Github, but in case I've updated it since then, here's my org-journal elisp snippet in full:

(defun rsws/org-journal-new-entry (entry-type)
  "Create a new entry in the journal of the given type"
  ;; Do some initial actions before adding the entry.
  (cond
   ((eq entry-type 'rsws/org-journal-entry-type--task)
    ;; If entry type is a task, check that point is under a TODO heading first
    (if (not (org-entry-get nil "TODO"))
        (user-error "Point is not under a TODO heading")
      ;; Clock in to the task under point and store a link to it.
      (progn
        (org-clock-in)
        (org-store-link nil t))))
   ((eq entry-type 'rsws/org-journal-entry-type--break)
    ;; If entry type is a break, clock out.
    (org-clock-out)))

  ;; Add the entry itself.
  (org-journal-new-entry nil)

  ;; Append some text to the entry title, depending on the type.
  (cond
   ((eq entry-type 'rsws/org-journal-entry-type--note)
    ;; Basic note, just add an emoji
    (insert "āœļø "))
   ((eq entry-type 'rsws/org-journal-entry-type--task)
    ;; For a task, add a link to the task itself
    (progn
      (insert "šŸ› ļø ")
      (org-insert-last-stored-link nil)))
   ((eq entry-type 'rsws/org-journal-entry-type--meeting)
    ;; For a meeting, add an emoji and clock in to this journal entry
    (progn
      (insert "šŸ‘„ ")
      (org-clock-in)))
   ((eq entry-type 'rsws/org-journal-entry-type--break)
    ;; For a break, add emoji and word "break"
    (insert "ā˜• Break"))))

(use-package org-journal
  :defer t
  :custom
  (org-journal-dir "~/notes/journal/")
  (org-journal-enable-agenda-integration t))

Thanks for reading and I hope you got something useful out of this. I'm looking at writing more blog posts around elisp, but I'm also interested in developing a game for Panic's Playdate console in C, so expect a post on my set up process for that in the near future.