Extending org-roam

geekery

I was in the middle of the activity I’m about to describe below and listening to the BBC Radio 4 programme A History of the World in 100 Objects about the Olduvai stone chopping tool when I heard something that made me smile. Neil MacGregor said something to the effect that one of the defining features of humans is that we make things more complicated than they need to be. Busted!

I’ve been getting in to using org-roam fairly heavily, and decided that it would be great to be able to use it more easily for making notes on research articles. One thing to note is that org-roam has now been integrated into Doom Emacs as a feature, so you can enable the (org +roam) option in init.el to install and configure it. This means that a lot of the configuration I was doing manually before is now not needed.

Anyway, as I’ve written about before, I use Bookends for all my referencing needs. When I find an interesting article, the first thing I do is to import it (and the PDF of the article) into Bookends. Then (or more often, some time later), I go to Bookends to open the PDF and start reading and taking notes. I wanted to move the reading and taking notes bit into org-roam so that I could benefit from linking between my ideas and the evidence to build up my knowledge and my own ideas. What I needed was some way of automatically grabbing the relevant bibliographic info from Bookends, and then some way of jumping between a Bookends reference, and its associated PDF and org-roam note file, in both directions.

I was messing about with roam-protocol when it struck me that I could just add some additional parameters to a call using this protocol and set up the org-roam-capture-ref-templates to insert those parameters in the right place and create the note with the information pre-filled. Someone hand me a rock and a piece of flint — I’m going in!

I have Keyboard Maestro and use the heck out of it, so that seemed a natural place to start. Bookends has a fully featured AppleScript dictionary, so I was able to use an AppleScript action in Keyboard Maestro to get the relevant information from the selected reference in Bookends. Here’s how the process works:

  1. I select the reference I want to read in Bookends and hit F5 which brings up a palette of Bookends actions. I select ‘Create notes file’ and the macro starts running. It checks first whether the reference I have selected has a PDF. If it doesn’t it throws up a notification and cancels the macro.
  2. Next it checks whether the User4 field is empty. I use this field later to store the filename of the org-roam notes file, so if it is not empty, that means I’ve already got a note on this article, so the macro opens that note puts up a notification, then cancels the macro. If the field is empty, the macro continues. Incidentally, for the ‘open note’ step, I call another macro which can also be called independently from the Bookends macro palette.
  3. The next step involves the ugliest AppleScript in the world, which grabs the relevant information from the reference in Bookends (title, PDF file name, Bookends ID number, citation string for Pandoc, full formatted citation) and saves it in JSON form as key:value pairs, and then stores these as Keyboard Maestro variables. This was harder than I thought because AppleScript’s idea of how to format JSON is not the same as Keyboard Maestro’s format.
  4. At this point I save what will be the note’s filename (the same as the PDF filename but lowercased and with spaces converted to underscores) back to the reference in the User4 field.
  5. In order to send the parameters to Emacs via the roam-protocol URI, I need to percent encode the strings. This part is embarrassing because I couldn’t figure out how to loop through all the values so I have to use the Filter action to process them one by one. Never mind, it gets the job done.
  6. The final few steps combine these encoded strings with the correct parameters, and then use the ‘Open URL’ action to send the full URI.
  7. The relevant template (with a ‘b’ key, see below) then fills in the details. I have it set to :immediate finish t so that it saves the file and I can then open it and make notes in a proper buffer.

These are the templates in my Emacs config for org-roam. The ‘ref’ one can be called from a bookmarklet in my browser to store a simpler note with the website URL in the #+ROAM_KEY: property.

(setq org-roam-capture-ref-templates
        '(("r" "ref" plain (function org-roam-capture--get-point)
           ""
           :file-name "${slug}"
           :head "#+TITLE: ${title}\n#+DATE: %<%Y-%m-%d>\n#+ROAM_KEY: ${ref}\n#+CATEGORY: website\n"
           :unnarrowed t)
          ("b" "bookends" plain (function org-roam-capture--get-point)
           "- tags :: [[open-ext:bookends://sonnysoftware.com/${bid}][Bookends link]]\n- reference :: ${fcite}\n\n%?"
           :file-name "${slug}"
           :head "#+TITLE: ${title}\n#+DATE: %<%Y-%m-%d>\n#+ROAM_KEY: ${ref}\n\
#+BID: ${bid}\n#+PDF_FILE: ${pdf}\n#+CATEGORY: article\n\n"
           :unnarrowed t
           :immediate-finish t)))

I had already set up custom link types (using org-add-link-type) to handle links marked ‘open-ext’ using the macOS command line app ‘open’ which just opens the item using whatever the default app is for that URI (in this case Bookends). So clicking the link opens the matching reference in Bookends.

The final piece of the puzzle is a couple of Emacs Lisp functions which use the #+PDF_FILE: property to get the PDF filename, join it with the Bookends attachment directory, and then open the PDF in a split window in Emacs. Another function performs a similar trick to open the Bookends reference using the Bookends ID via the Bookends URL scheme. Both are bound to keys so that I can trigger them quickly.

I was so flushed with success at ‘making things complicated’ that I decided to make my functions into a separate Emacs package. I then call that by making a (package! ...) call in packages.el in Doom Emacs and pull it down from a private GitHub repository. Amazingly, that worked! The whole thing is so gaffer-taped together and tailored to my own set of tools and workflows that I doubt anyone would be able to use it as is. However, if anyone is interested, I might see if there is some way to package the whole thing up so that people can use it as inspiration for their own workflows.