Reducing Duplication in my Emacs Config

Learning from One Small Refactor

By on ::    

Earlier this week, I realized that I wanted some keyboard bindings to open a few of my common org-mode files.

I have the following files:

agenda.org
At the end of the day, this is where I put tomorrows expected work.
elfeed.org
This is the org file that manages my elfeed's feed list; It's analogue to an OPML 📖 file.
index.org
This is the index into the multitude of org files that comprise my Org Roam repository.
permanent/card_index.org
Following the slip-case method, I'm capturing ideas and how they stack in my virtual slip-case.
permanent/bibliographic_index.org
This is a list of all the bibliographic cards that I've filed away.
troubleshoot.org
Sometimes I have long-running problems that I'm poking it. I record that work in the Troubleshooting.org file.

Note: the filenames aren’t important, but help provide some context.

Pre-Refactor Code

Prior to tonight’s work, I mapped function keys (e.g. F2, F3, etc.) to some of these files. It was in an ad-hoc fashion.

Below is the code I used for opening these org files:

(defun gorg (&optional org_file_basename)
  "Jump to the given ORG_FILE_BASENAME or toggle it's org-sidebar.

If no ORG_FILE_BASENAME is given default to `agenda.org'. I chose
`gorg' as the mnemonic Goto Org."
  (interactive)
  ;; Need to know the location on disk for the buffer
  (unless org_file_basename (setq org_file_basename "agenda.org"))
  (setq org_filename (concat org-directory "/" org_file_basename))
  (let ((current_filename (if (equal major-mode 'dired-mode) default-directory (buffer-file-name))))
    (if (equal current_filename (expand-file-name org_filename))
        (progn (org-sidebar-toggle))
      (progn (find-file org_filename) (delete-other-windows)))))

Note: I’m also making use of the org-sidebar package; But that’s not important to the refactoring.

And here are the key mappings for those files:

(global-set-key (kbd "<f2>") 'gorg)
(global-set-key (kbd "<f3>") `(lambda () (interactive) (gorg "index.org")))
(global-set-key (kbd "<f4>") `(lambda () (interactive) (gorg "permanent/card_index.org")))
(global-set-key (kbd "<f5>") `(lambda () (interactive) (gorg "troubleshooting.org")))
(global-set-key (kbd "<f6>") `(lambda () (interactive) (gorg "permanent/bibliographic_index.org")))

At this time, it is my understanding that in Emacs I cannot bind a parameterized function to a keyboard shortcut. That is to say the following would not work: (global-set-key (kbd "<f6>") 'gorg "permanent/bibliographic_index.org")

The key definitions were passable. But there wasn’t a mnemonic with the function key and filename.

I thought about C-c o a for command to open the agenda.org file. And decided to change the key combinations. Below are the changes:

(global-set-key (kbd "C-c o a") 'gorg)
(global-set-key (kbd "C-c o i") `(lambda () (interactive) (gorg "index.org")))
(global-set-key (kbd "C-c o c") `(lambda () (interactive) (gorg "permanent/card_index.org")))
(global-set-key (kbd "C-c o t") `(lambda () (interactive) (gorg "troubleshooting.org")))
(global-set-key (kbd "C-c o b") `(lambda () (interactive) (gorg "permanent/bibliographic_index.org")))
(global-set-key (kbd "C-c o e") `(lambda () (interactive) (gorg "elfeed.org")))

Better, but my brain wanted to reduce duplication.

An Observation

In other configurations, I’d seen mode-maps. I wondered about creating a mode map. I searched and found it in the documentation:

Some prefix keymaps are stored in variables with names:

  • ctl-x-map is the variable name for the map used for characters that follow C-x.
  • help-map is for characters that follow C-h.
  • esc-map is for characters that follow ESC. Thus, all Meta characters are actually defined by this map.
  • ctl-x-4-map is for characters that follow C-x 4.
  • mode-specific-map is for characters that follow C-c.

Curious about what all was mapped to mode-specific-map, I looked it up. I use the helpful package’s helfpul-variable. The function for C-c o i was registered as #<anonymous-function>. I had bound the key combination to a lambda, which is an anonymous function.

If I wanted Emacs to best reinforce my mnemonic, I wanted to move away from the anonymous function and to something meaningful.

Post-Refactor

What I wanted was to loop through an array of key/value pairs. The key would be the keyboard shortcut and the value would be the name of the relative name of the file.

I left the above gorg function defined as is. The following code is the macro I wrote:

(defmacro gorg-sexp-eval (sexp &rest key value)
  `(eval (read (format ,(format "%S" sexp) ,@key ,@value))))

(dolist (the-map  '(("a" . "agenda.org")
                    ("b" . "permanent/bibliographic_index.org")
                    ("c" . "permanent/card_index.org")
                    ("e" . "elfeed.org")
                    ("i" . "index.org")
                    ("t" . "troubleshooting.org")))
  ;; Create a function for element in the above alist.  The `car'
  ;; (e.g. "a"), will be used for the kbd shortcut.  The `cdr'
  ;; (e.g. "agenda.org") will be the filename sent to `gorg'
  (gorg-sexp-eval
   (progn (defun gorg--%1$s-%2$s ()
      "Invoke `gorg' with %2$s"
      (interactive)
      (gorg "%2$s"))
          (global-set-key (kbd "C-c o %1$s") 'gorg--%1$s-%2$s))
   (car the-map) (cdr the-map)))

Originally, I had two calls to the gorg-sexp-eval macro, but factored that away by using progn to wrap the method definition and the keybinding.

Now when I look at the mode-specific-map and see that gorg--i-index.org is registered to the C-c o i keyboard combination.

Updates

update

Based on feedback on Reddit, I reworked the code.

In this work I have two considerations. First, is the code should be legible. One commenter rightly pointed out that I was jumping through some hoops with the defmacro. As a Ruby developer, I always look at the eval function with trepedation. It’s presence usually means something’s not quite right.

I learned a less convoluted way to do what I wanted to do. Here’s the code I’m going with:

(defmacro go-org-file-fn (file)
  "Define a function to go to Org FILE."
  (let* ((fn-name (intern (concat "go-org-file-" file)))
         (docstring (concat "Go to Org file at: " file)))
    `(defun ,fn-name ()
       ,docstring
       (interactive)
       (gorg ,file))))

(global-set-key (kbd "C-c o i") (go-org-file-fn "index.org"))
(global-set-key (kbd "C-c o a") (go-org-file-fn "agenda.org"))
(global-set-key (kbd "C-c o b") (go-org-file-fn "permanent/bibliographic_index.org"))
(global-set-key (kbd "C-c o c") (go-org-file-fn "permanent/card_index.org"))
(global-set-key (kbd "C-c o e") (go-org-file-fn "elfeed.org"))
(global-set-key (kbd "C-c o i") (go-org-file-fn "index.org"))

What’s happening? The go-org-file-fn returns a named function. Each of the global-set-key calls binds the keyboard combination to the named function.

Now, when I type C-c o ? I get a description of the key bindings. They look like:

Global Bindings Starting With C-c o:
key             binding
---             -------

C-c o a         go-org-file-agenda.org
C-c o b         go-org-file-permanent/bibliographic_index.org
C-c o c         go-org-file-permanent/card_index.org
C-c o e         go-org-file-elfeed.org
C-c o i         go-org-file-index.org

Were I to use an anonymous function they would look like:

Global Bindings Starting With C-c o:
key             binding
---             -------

C-c o a         #<anonymous-function>
C-c o b         #<anonymous-function>
C-c o c         #<anonymous-function>
C-c o e         #<anonymous-function>
C-c o i         #<anonymous-function>

The named binding is much nicer. Yes there’s still duplication, but the next step would be a loop and iteration. Which might obfuscate what was going on.

As it turns out, I’m more concerned about the legibility than removing all of the duplication.

A commenter also reminded me of Emacs 📖 ’s bookmark system. It’s not quite what I want in this moment, but I think it’s going to be quite close going forward. I’m overloading behavior in this gorg function; I’ll continue to think on how I’m using it.

Conclusion

I spent more time than I would have thought. I cribbed the conceptual macro from the Modus Themes’s manual.

It took a bit of time to stumble upon splitting the key/values via the car and cdr functions. I may have done things wrong, but I believe the gorg-sexp-eval macro wanted strings for each parameter.

All of this is to say, I learned something new today, and want to share it with you.