Persistent prefix keymaps in Emacs

Or: Further Musings on the Tedium of Long Key-Chords. In the past I’ve covered various bespoke approaches to the problem of repeating long key sequences: Transient, Hydra, repeat-mode (and repeat-mode’s helpers) require progressively less forethought and custom elisp chops to set up. Today we continue our aggressive descent into laziness with the simplest way yet to use any Emacs key prefix as a springboard for one-key access to commands.

A quick glossary
  • keymap: A “keymap” is an Emacs data structure that maps a collection of keybindings (i.e. keyboard shortcuts) to commands.

  • key-chord: This is a sequence of keys, some or all of which can require modifier keys to be held down. Examples:

    • C-x C-f (control + x, followed by control + f), which runs the find-file command
    • C-c @ C-t (control + c, @, followed by control + t), which runs the command outline-hide-body.

    Needless to say, these can get pretty tedious for repeated invocations, even if you avoid the modifiers by using Vim-style leader keys.

  • prefix-key: Any “incomplete” part of a key-chord that will cause Emacs to wait for further keyboard input. In the above examples, C-x, C-c and C-c @ are all prefix keys.

repeat-mode, Hydra, Hercules and Transient all enable variations of the following interaction:

  • You (the user) define a special keymap – a set of mappings from key bindings to commands,
  • where the bindings are short and mnemonic/ergonomic,
  • where the commands are related at some conceptual level in your mind,
  • which are displayed in some kind of menu or table,
  • and can be invoked in quick succession if required without exiting this menu. Additionally,
  • this menu (and associated editor state) can be enabled by using a dedicated keybinding, or
  • (repeat-mode only) by calling any one of these commands the long way.

The price you pay for this convenience is that you have to create this command group and specify the menu elements manually. This is not a tall order if you only need a few such menus, and the results can be pretty neat. In this situation these packages solve the tedium issue almost incidentally, since easing repetition is not even their primary purpose.

Figure 1: Pictured: A Transient for assorted tasks. Not pictured: Its long and messy definition.

Figure 1: Pictured: A Transient for assorted tasks. Not pictured: Its long and messy definition.

As I’ve written at length before, Emacs’ built-in repeat-mode straddles the divide between requiring a bespoke menu with associated elisp configuration and a minimally specified approach to repeating commands.

But Emacs is full of prefix maps, and we still don’t have the ability to enter a persistent state – on the fly and without premeditation – where an arbitrary keymap can be “activated” until we are done running or repeating commands in this map.

As it turns out, a 90% solution for this is actually really straightforward, no repeat-mode needed. With some inspiration from Embark:

(defun repeated-prefix-help-command ()
  (interactive)
  (when-let* ((keys (this-command-keys-vector))
              (prefix (seq-take keys (1- (length keys))))
              (orig-keymap (key-binding prefix 'accept-default))
              (keymap (copy-keymap orig-keymap))
              (exit-func (set-transient-map keymap t #'which-key-abort)))
    (define-key keymap [remap keyboard-quit]
      (lambda () (interactive) (funcall exit-func)))
    (which-key--create-buffer-and-show nil keymap)))

(setq prefix-help-command #'repeated-prefix-help-command)

That’s all of it. Here’s how it works:

  1. When you’ve typed any prefix key (say C-c @) and Emacs is waiting for further input, press C-h.
  2. Emacs now remembers whatever you’ve already typed, activates a “repeat state” or repeat-mode, as it were. This is basically modal editing with bespoke modes. and a which-key prompt/menu with available keybindings.
  3. You can now continue to call available commands in sequence without having to re-type the annoying prefix key.
  4. This state persists until you exit it with C-g (bound to keyboard-quit).
Note:

which-key is only a visual aid here – this works if you replace it with a different prompter or if you remove it from the code entirely.

Here is a demo of creating a “repeat state” on the fly, in this case for the ever annoying outline-minor-mode commands on the C-c @ prefix. (The keypresses are shown at the top of the window.)

In principle, repeated-prefix-help-command functions similar to repeat-mode, but with one difference. The latter requires that you specify beforehand which prefix keymaps should automatically enter the “repeat state”, and define maps that group together related commands if they don’t exist already. The above method lacks this automaticity, but is more flexible: you can enter this state at any time (and for any keymap) with an additional keypress (here C-h).

repeated-prefix-help-command also has an advantage over repeat-mode: pressing keys that are not bound in the “repeat state” will cause repeat-mode to end, and you will have to start over with the full, long key sequence to enter it again. Although this can be remedied with some monkey patching. See repeat-help. The keymap we activate above stays active until we call keyboard-quit, so we can freely mix regular Emacs editing commands and the ones we have temporary one-key access to.

We could, of course, use both approaches together. We are approaching peak laziness, but I don’t think we’re there yet!