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.
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.