Mnemonic keymaps

First published .

Where have all the good keys gone?

It was not long into my own personal Emacs saga when I ran into a problem: finding available keys for my personal keybindings. Every good and reasonable binding seemed to already be used up, at least in some mode or other that I use. For a brief while I resorted to the function keys and painful C-M-S-Super-Hyper-whatever combinations. But that quickly became unsustainable.

It was however another few years before I discovered a solution that I am happy with: mnemonic keymaps. That is, making your own nested keymaps with simple letters that are easy to remember. A variety of newer packages have suggested or encouraged this idea over the last ten years, and I don't claim to have invented it; but so far, I haven't seen anyone else give it a name and explicitly recommend it as a strategy for making your own keybindings.

Inspirations

The idea of mnemonic keymaps seems obvious in retrospect, but I came to it gradually, after learning a few more things in the intervening years.

From evil I learned about the benefits of modal editing: composing sequences of keys, especially normal letter keys, can be a powerful and intuitive way to operate on text. Evil's normal-mode keys are often mnemonic: d w deletes a word, d a " deletes around a quotation, d i ( deletes inside parentheses, and so on. There's a single, easy to remember key for "delete" (d) and then a whole range of other ways to describe what you want to delete, equally easy to remember; and so on for other kinds of operations.

But of course, this only works if you have mentally escaped from the normal Emacs paradigm of one-key-means-one-editing-command, and especially the idea that an unmodified letter key should run self-insert-command. Evil does this by borrowing vim's concept of modes (called "states" in Evil so as not to be confused with Emacs' major and minor modes). You need some kind of idea of "modes" or "states", even if not the evil ones, to make use of this idea.

Using magit reinforced the idea of composition of memorable keys. Magit's system of transient menus, activated in sequence when you press a single key, is one of the things that makes magit feel like magic. In magit, key sequences group related functions together: everything under b has to do with branches, everything under P has to do with pushing, everything under r with reverting, etc.

Somewhere along the way, I also learned that the C-c [a-zA-Z] keys are reserved for users. I hadn't learned this early on and only discovered it when someone complained on the Org mailing list that Org shouldn't use those reserved keys. (RTFM, kids! It's long but it's good.)

Making your own mnemonic keymaps

The idea of mnemonic keymaps is simple: you group related functions into a single keymap, binding them to simple letters that help you remember them, and then bind the whole keymap to a single letter in the user-reserved key range.

For example, my 'mail' keymap is bound to C-c m, and contains the following keys:

Thus I type C-c m c to compose a new email, C-c m f to fetch email, and so on.

The C-c prefix transports us out of the self-insert-mode hegemony and into a tiny "mode" for all things mail-related, freeing us up to use single, easy-to-remember letter combinations to do what we want. Simple, beautiful, and it works across all major modes (so long as they respect the user-reserved keys). I have various other maps that use the same idea:

The great thing about this is that there's no limit: because keymaps can be nested, you can apply this strategy recursively. This means that the space of possible combinations is huge. Even though there are only 52 keys reserved for you at the top level under C-c, you'll never run out of easily-memorable keys if you're careful about how you organize them.

So how do we do this concretely?

Create a keymap

The first step is to create a new keymap. You do this with the make-sparse-keymap function. For example, here's the definition of my mail keymap:

(defvar rwl-mail-map
  (make-sparse-keymap)
  "Keymap for mail-related functions")

Bind the keymap to a user-reserved key

Next, we want to make the new keymap accessible from a user-reserved key using define-key:

(define-key (current-global-map) (kbd "C-c m") rwl-mail-map)

I'm doing this here with the kbd function to make it obvious what the top-level binding is, but if you know that C-c is actually bound to mode-specific-map, you could also do it like this:

(define-key mode-specific-map "m" rwl-mail-map)

Bind functions into the new keymap

Finally, we add bindings in our new keymap for all the functions we want to group together, again using define-key. Here for example are two of the bindings in my mail keymap:

(define-key rwl-mail-map "m" 'start-notmuch-at-inbox)
(define-key rwl-mail-map "c" 'compose-with-sender)

Notice that we are not binding the functions directly here, but using their quoted names. That is, the binding is to a symbol. When Emacs finds a symbol at the end of a key sequence, it calls the function associated with that symbol.

In contrast, when we bound the new keymap into mode-specific-map above, we did not quote its name, but instead bound the key directly to the new keymap value. When Emacs finds a keymap value at the end of a key sequence, it will look up the next key in that keymap.

See the documentation for define-key (C-h f define-key) for more.

A nice freebie

Recent versions of Emacs have added a nice bit of polish: pressing ? in a keymap where it is not bound will automatically call describe-keymap on that keymap. So if you ever forget what you've put into your keymap, you can use C-c /k/ ? (for whatever key /k/ you've bound it to) to get an overview of your keymap.

Update: using define-keymap

If you're using Emacs 29 or later, there's another option, which I recently learned about thanks to this newsletter: instead of the combination of defvar, make-sparse-keymap and define-key I used above, you can use the new macro define-keymap to simplify the declaration. The equivalent definition looks like this:

(global-set-key
 (kbd "C-c m")
 (define-keymap
   :prefix 'rwl-mail-map
   "c" 'compose-with-sender
   "f" 'fetch-mail
   "m" 'start-notmuch-at-inbox))

The (to me, nonobvious) :prefix keyword here must be "a symbol to be used as a prefix command", and if given, the map will be stored as the function value of the symbol. Thus this effectively says: bind the global key C-c m to the command rwl-mail-map, the value of which is the created keymap.