Revisiting Hydra Menu for Org Roam Lookup in Emacs

A General Solution (Thusfar)

Previously, I wrote about Adding Hydra Menu for Org Roam Lookup in Emacs. Since then, I’ve explored the solution.

update: Over on org-roams Slack channel, @nobiot provided some clarity on exploring the solution. Neither @nobiot nor myself are experienced Elisp developers.

As a recap, I wanted two key behaviors from Org Roam.

First, I wanted a way to narrow the scope of my initial search when finding a file. I have two Role Playing Game (RPG 🔍) campaigns, and want a quick way to limit the search to a given campaign. Here’s that initial solution.

Second, I wanted a way to narrow the scope of linking to a file. When I’m running a game via Zoom, I’m often taking notes while playing. When I create a new concept (e.g. an Non-Player Character (NPC 🔍) or location) I want to create an org-roam note for that concept. I also want the lookup to have a similar filter.

Working Through the Solution

Here’s what I have. There are four general areas:

Permanent
Non-project information; Concepts that span work and play.
RPGs
The campaigns I'm running or building.
Work
Stuff that pays the bills.
General Org
Other org-mode utilities.

Within each area, I have a few concepts. Let’s dive into the RPG area.

I have four menu options, two options for each of the two subjects: The World of Ardu and the Thel Sector. Each of these subjects have two actions; Thus four menu options.

The Find action launches a prompt to search the scoped concern. That is “Thel Sector Find” will open a prompt limiting the file search to the Thel Sector files. By convention the key for each of these actions is the upper case of the related insert action.

The insert action is where the magic lives. When I select that action, I open a similar file search as above. When I select a file, it creates a link in the originating buffer. If I don’t find a matching file, I’m prompted to create a quick note.

update: Based on my directory structure fixed the permanent,bibliographies and permanent,cards to account for the comma separation of the roam tags.

Emacs Code for Org Subject Menu

You can find also find this code on Github. Below is that code.

(defun xah-filter-list (@predicate @sequence)
  "Return a new list such that @PREDICATE is true on all members of @SEQUENCE.
URL `http://ergoemacs.org/emacs/elisp_filter_list.html'
Version 2016-07-18"
  (delete
   "e3824ad41f2ec1ed"
   (mapcar
    (lambda ($x)
      (if (funcall @predicate $x)
          $x
        "e3824ad41f2ec1ed" ))
    @sequence)))

(defmacro org-roam-inserter-fn (project)
  "Define a function to wrap the `org-roam-inser' with a filter for the given PROJECT."
  (let* ((fn-name (intern (concat "org-roam-insert--filter-with--"
                                  (replace-regexp-in-string "\\W+" "-" project))))
         (docstring (concat "Insert an `org-roam' file for: " project)))
    `(defun ,fn-name (&optional lowercase completions description link-type)
       ,docstring
       (interactive "P")
       (let* ((filter (lambda(completions) (xah-filter-list
                                            (lambda(item) (string-match-p (concat "\\W" ,project "\\W") (first item)))
                                            completions))))
         (org-roam-insert lowercase completions filter description link-type)))))

(defmacro go-roam-find-file-project-fn (project)
  "Define a function to find an `org-roam' file within the given PROJECT."
  (let* ((fn-name (intern (concat "org-roam-find-file--" (replace-regexp-in-string "\\W+" "-" project))))
         (docstring (concat "Find an `org-roam' file for: " project)))
    `(defun ,fn-name (&optional initial-prompt completions)
       ,docstring
       (interactive)
       (let* ((filter (lambda(completions) (xah-filter-list
                                            (lambda(item) (string-match-p (concat "\\W" ,project "\\W") (first item)))
                                            completions))))
         (org-roam-find-file initial-prompt completions filter)))))

(go-roam-find-file-project-fn "thel-sector")
(go-roam-find-file-project-fn "ardu")
(go-roam-find-file-project-fn "permanent,bibliographies")
(go-roam-find-file-project-fn "permanent,cards")
(go-roam-find-file-project-fn "hesburgh-libraries")
(go-roam-find-file-project-fn "samvera")
(org-roam-inserter-fn "thel-sector")
(org-roam-inserter-fn "ardu")
(org-roam-inserter-fn "permanent,bibliographies")
(org-roam-inserter-fn "permanent,cards")
(org-roam-inserter-fn "hesburgh-libraries")
(org-roam-inserter-fn "samvera")

(defvar jnf-org-subject-menu--title (with-faicon "book" "Org Subject Menu" 1 -0.05))
(pretty-hydra-define jnf-org-subject-menu (:foreign-keys warn :title jnf-org-subject-menu--title :quit-key "q")
  (
   "Permanent"
   (("b" org-roam-insert--filter-with--permanent-bibliographies "Bibliography")
    ("B" org-roam-find-file--permanent-bibliographies " └─ Find")
    ("c" org-roam-insert--filter-with--permanent-cards "Card")
    ("C" org-roam-find-file--permanent-cards " └─ Find"))
   "RPGs"
   (("a" org-roam-insert--filter-with--ardu "Ardu, World of")
    ("A" org-roam-find-file--ardu " └─ Find")
    ("t" org-roam-insert--filter-with--thel-sector "Thel Sector")
    ("T" org-roam-find-file--thel-sector " └─ Find"))
   "Work"
   (("h" org-roam-insert--filter-with--hesburgh-libraries "Hesburgh Libraries")
    ("H" org-roam-find-file--hesburgh-libraries " └─ Find")
    ("s" org-roam-insert--filter-with--samvera "Samvera")
    ("S" org-roam-find-file--samvera " └─ Find"))
   "General Org"
   (("i" org-roam-insert "Insert Unfiltered")
    ("I" org-roam-find-file " └─ Find")
    ("O" gorg "Agenda")
    ("R" org-roam-jump-to-index "Roam Index"))
))

(global-set-key (kbd "s-1") 'jnf-org-subject-menu/body) ;; Deprecated
(global-set-key (kbd "s-i") 'jnf-org-subject-menu/body)

update

If you dig into the code, you’ll see quite a bit of duplication. I spent just a bit of time trying to remove the duplication.

The one that sticks out most is the filter lambda. Those two lambdas are duplicates, and have a rather large contextual concern. Below is a copy:

(lambda(completions) (
    xah-filter-list
    (lambda(item) (string-match-p (concat "\\W" ,project "\\W") (first item)))
    completions)
))

Let’s step through this from the inside out.

The following code (lambda(item) (string-match-p (concat "\\W" ,project "\\W") (first item)) creates is the filter. The given item is list from a completion result alist. The first item of the list is a string. The lambda returns true if there’s a match. Let’s replace this logic with the symbol predicate-matching-function.

Stepping out we have (xah-filter-list predicate-matching-function completions). In this section I’m calling xah-filter-list with the function predicate-matching-function and completions, an alist of candidate results. xah-filter-list returns a new alist in which each completion is a match. We use the predicate-matching-function to determine if its a match. Let’s replace this with completions-filtering-function.

Stepping out again, we now have lambda(completions) (completions-filtering-function); This is what we will use as the filter-fn parameter for both org-roam-find-file and org-roam-insert. Both methods require that filter-fn be a function that takes one parameter; eg. the alist of completions.

I could refactor this, but at this stage it’s good enough for my needs.

And if I start refactoring, I’ll need to look at consolidating the two macros that each generate a function. And I might look at my menu generation.

Wrapping Things Up

I love the malleability of Emacs. I love that its a text editor; What I really love is that I can build on the vast array of functions to help me organize according to my needs.