Keymap Popup User Manual

Table of Contents


1 Introduction

keymap-popup’ defines a normal Emacs keymap with embedded descriptions, and renders it as a popup menu on demand. One definition, two uses: direct key dispatch and an interactive menu.

Requires Emacs 29.1 or later (for ‘defvar-keymap’). No external dependencies.


2 Installation


2.1 From source

(add-to-list 'load-path "/path/to/keymap-popup")
(require 'keymap-popup)

2.2 With use-package

(use-package keymap-popup
  :ensure t)

3 Quick start

(keymap-popup-define my-commands-map
  "My commands."
  :group "Edit"
  "c" ("Comment" comment-dwim)
  "r" ("Rename" rename-file)
  :group "View"
  "g" ("Refresh" revert-buffer)
  "b" ("Bury buffer" bury-buffer))

(keymap-set some-mode-map "C-c m" my-commands-map)

The macro auto-binds ‘h’ inside the keymap, so ‘C-c m h’ opens the popup. ‘q’ or ‘C-g’ dismisses it.


4 Defining keymaps


4.1 Macro syntax

(keymap-popup-define MAP-NAME
  DOCSTRING
  :popup-key KEY  :exit-key KEY
  :parent KEYMAP  :description STRING-OR-FN
  :persistent BOOL
  BINDING...)

Only ‘MAP-NAME’ and the bindings are required; every keyword above is optional and may appear in any order. ‘keymap-popup-annotate’ takes the same keywords but does not apply defaults for ‘:popup-key’ or ‘:exit-key’ – it never binds keys unless asked.

A binding takes one of three forms:

KEY (DESC COMMAND     [PROP VALUE...])    ; ordinary command
KEY (DESC :switch VAR [PROP VALUE...])    ; toggle
KEY (DESC :keymap MAP [PROP VALUE...])    ; sub-menu
  • KEY’ – ‘key-valid-p’ string.
  • DESC’ – string or zero-arg function (called at render time in the user’s buffer, so it sees buffer-local state).
  • COMMAND’ – command symbol or a ‘(lambda () (interactive) ...)’.
  • PROP’ – one of ‘:if’, ‘:inapt-if’, ‘:stay-open’, ‘:c-u’ (see Special bindings).

4.2 Groups and rows

(keymap-popup-define my-feed-map
  :group "Navigation"
  "n" ("Next" next-line :stay-open t)
  "p" ("Previous" previous-line :stay-open t)
  :group "Tags"
  "r" ("Toggle read" my-feed-toggle-read :stay-open t)
  :row
  :group "Feeds"
  "A" ("Add feed" my-feed-add)
  "D" ("Remove feed" my-feed-remove))

:row’ between groups starts a fresh row of columns beneath the first. Groups may attach their own predicates: ‘:group ("Bulk" :if PRED :inapt-if PRED)’.


4.3 Dynamic descriptions

(keymap-popup-define my-encryption-map
  :description (lambda ()
                 (format "Encryption: %s" my-chat-encryption))
  "o" ("OMEMO" my-chat-encryption-set-omemo)
  "g" ("OpenPGP" my-chat-encryption-set-openpgp)
  "p" ("Plaintext" my-chat-encryption-set-plaintext))

The header is re-rendered after each command.


4.4 Sharing bindings with ‘:parent

Same semantics as ‘defvar-keymap’’s ‘:parent’. The popup walks the parent chain, so parent bindings appear in the child’s menu under their original group.

If a key is bound in both parent and child, the child wins for both dispatch and display. Shadowed parent entries are dropped from the popup, and a parent group whose entries are all shadowed disappears entirely.

(keymap-popup-define my-items-mode-map
  :parent my-dashboard-common-map
  :group "Items"
  "RET" ("Open" my-items-open)
  "q" ("Back" my-items-back))

4.5 Adding and removing entries at runtime

(keymap-popup-add-entry my-commands-map
                        "l" "Log message" #'my-log-message
                        "Edit")

(keymap-popup-remove-entry my-commands-map "l")

Signatures:

  • (keymap-popup-add-entry KEYMAP KEY DESC COMMAND &optional GROUP)
  • (keymap-popup-remove-entry KEYMAP KEY)

Both mutate the keymap and its descriptions in place. Re-adding a key replaces the prior entry (in whatever group it was in), matching ‘keymap-set’’s semantics.


5 Showing the popup

;;;###autoload
(defun yeetube ()
  (interactive)
  (keymap-popup yeetube-mode-map))

C-g’ always dismisses, regardless of the configured ‘:exit-key’.


5.1 Display backends

keymap-popup-backend’ selects ‘keymap-popup-backend-side-window’ (default) or ‘keymap-popup-backend-child-frame’. Bind it locally for a one-off floating popup:

(defun my-commands-floating ()
  (interactive)
  (let ((keymap-popup-backend #'keymap-popup-backend-child-frame))
    (keymap-popup my-commands-map)))

5.2 Persistent popups

By default the popup dismisses after a suffix command runs. ‘:persistent t’ keeps it open until the exit key or ‘C-g’:

(keymap-popup-define my-map :persistent t ...)
(keymap-popup-annotate dired-mode-map :popup-key "?" :persistent t ...)
(setopt keymap-popup-persistent t)        ; global default

Per-keymap setting wins over the global one.


5.3 Dismissing programmatically

Call ‘keymap-popup-dismiss’ from a command to close the popup before transitioning elsewhere:

(defun my-open-detail ()
  (interactive)
  (keymap-popup-dismiss)
  (pop-to-buffer "*detail*"))

6 Special bindings

Predicate quick reference:


6.1 Sub-menus with ‘:keymap

(keymap-popup-define my-dashboard-mode-map
  :group "Navigate"
  "n" ("Nodes" :keymap my-dashboard-nodes-map)
  "t" ("Themata" :keymap my-dashboard-themata-map)
  :group "More"
  "x" ("Import/Export" :keymap my-dashboard-io-map))

Sub-menu entries use the ‘keymap-popup-submenu’ face. Nesting is unlimited; the exit key pops one level.


6.2 Toggles with ‘:switch

(keymap-popup-define yeetube-mode-map
  :group "Actions"
  "o" ("Settings" :switch yeetube--show-settings)
  :group "Settings"
  "V" ("No video" :switch yeetube-mpv-no-video
       :if (lambda () yeetube--show-settings)))

The variable is auto-declared via ‘defvar’ + ‘make-variable-buffer-local’; each buffer gets its own value on first set. Switches always keep the popup open – no need to add ‘:stay-open t’.


6.3 Stay-open commands with ‘:stay-open

(keymap-popup-define my-mark-map
  :group "Mark"
  "m" ("Mark" dired-mark :stay-open t)
  "u" ("Unmark" dired-unmark :stay-open t)
  "U" ("Unmark all" dired-unmark-all-marks))

For an entire popup, prefer ‘:persistent t’ (see Persistent popups).


6.4 Disabling entries with ‘:inapt-if

(keymap-popup-define yeetube-mode-map
  :group "Settings"
  "p" ("Toggle pause" yeetube-mpv-toggle-pause
       :inapt-if (lambda () (not (get-process yeetube-mpv--process-name)))))

;; Group-level applies to every entry in the group:
(keymap-popup-define my-bulk-map
  :group ("Bulk" :inapt-if (lambda () (not (my-marked-files))))
  "c" ("Chmod" my-do-chmod)
  "z" ("Compress" my-do-compress))

Inapt entries render in ‘keymap-popup-inapt’ and print “Command unavailable” when pressed.


6.5 Hiding entries with ‘:if

(keymap-popup-define yeetube-mode-map
  "o" ("Settings" :switch yeetube--show-settings)
  :group ("Playback" :if (lambda () yeetube--show-settings))
  "p" ("Toggle pause" yeetube-mpv-toggle-pause)
  "v" ("Toggle video" yeetube-mpv-toggle-video))

6.6 Prefix-arg variants with ‘:c-u

(keymap-popup-define my-greet-map
  :group "Actions"
  "a" ("Greet" my-greet :c-u "SHOUT (C-u)"))

The ‘:c-u’ label renders in ‘shadow’ alongside the description as a hint. Pressing ‘C-u’ enters prefix-arg mode: ‘:c-u’ entries switch to ‘warning’, all other entries dim with ‘shadow’, and ‘universal-argument-map’ takes over so further ‘C-u’, digits, and ‘-’ compose the prefix as usual. Pressing the key then runs the command with ‘current-prefix-arg’ set, exactly like any ‘interactive "P"’ command.


7 Prompting and re-entry

Suffix commands dismiss the popup when they return. For commands that prompt (‘read-string’, ‘completing-read’, …) and should leave the popup visible, either set ‘:persistent t’ on the keymap or re-invoke ‘keymap-popup’ at the end of the command:

(defun my-edit-set-title ()
  (interactive)
  (let ((new (read-string "Title: ")))
    (my-db-set-title new))
  (keymap-popup my-edit-map))

(keymap-popup-define my-edit-map
  :group "Edit"
  "t" ("Title" my-edit-set-title))

The same pattern works with a lambda in the binding itself:

(keymap-popup-define my-settings-map
  :group "Settings"
  "n" ((lambda () (format "Limit: %d" my--limit))
       (lambda ()
         (interactive)
         (setq-local my--limit (read-number "Limit: "))
         (keymap-popup my-settings-map))))

8 Annotating existing keymaps

keymap-popup-annotate’ attaches descriptions to a keymap you don’t own (like ‘dired-mode-map’) without rebinding its keys. Keys are resolved at popup time via ‘where-is-internal’, so the popup tracks the user’s current bindings.

(keymap-popup-annotate dired-mode-map
  :popup-key "?"
  :persistent t
  :description (lambda ()
                 (format "Dired: %s" (abbreviate-file-name default-directory)))
  :group "File"
  dired-find-file "Open"
  dired-do-copy "Copy"
  dired-do-rename "Rename"
  dired-do-delete "Delete"
  :group "Navigate"
  dired-up-directory "Up"
  dired-previous-line "Prev"
  dired-next-line "Next"
  :group "More"
  "M" ("Mark operations" :keymap my-dired-mark-map))

Entries are ‘COMMAND-SYMBOL DESCRIPTION’ (the key comes from the user’s binding). String-key entries are allowed too – that’s how sub-menus attach to an annotated keymap.


9 Customization


9.1 User options

  • keymap-popup-backend’ – display function; built-ins are ‘keymap-popup-backend-side-window’ (default) and ‘keymap-popup-backend-child-frame’.
  • keymap-popup-display-action’ – ‘display-buffer’ action for the side-window backend.
  • keymap-popup-buffer-parameters’ – buffer-local parameters applied to the popup buffer.
  • keymap-popup-child-frame-parameters’ – frame parameters for the child-frame backend.
  • keymap-popup-persistent’ – global default for ‘:persistent’.
  • keymap-popup-default-popup-key’ – default ‘:popup-key’ (‘h’).
  • keymap-popup-default-exit-key’ – default ‘:exit-key’ (‘q’).

9.2 Faces

  • keymap-popup-key’ – key string (inherits ‘help-key-binding’).
  • keymap-popup-group-header’ – group header.
  • keymap-popup-value’ – ‘[on]’ / ‘[off]’ and inline values.
  • keymap-popup-submenu’ – sub-menu entries.
  • keymap-popup-inapt’ – disabled entries (inherits ‘shadow’).

10 When to use this vs ‘transient

transient’ is its own abstraction: a class hierarchy built on EIEIO, with prefix/suffix/infix objects and a custom dispatch loop. The menu is not an Emacs keymap. It fits codebases that already lean on EIEIO and benefit from typed, extensible class machinery (magit being the canonical example).

keymap-popup’ is just a ‘defvar-keymap’ plus metadata, displayed via the built-in ‘set-transient-map’ primitive (which predates the ‘transient’ package by years and is unrelated to it, despite the naming collision). Use it when you want a real keymap that also knows how to draw itself: bindings show up in ‘where-is’ and ‘describe-key’, and the popup does not block ‘M-x’. There is no class hierarchy to learn, and state lives wherever you choose to put it.


11 Contributing

Source is a single file, ‘keymap-popup.el’, with tests in ‘tests/keymap-popup-tests.el’. Run them with:

emacs --batch -l ert -l keymap-popup.el \
              -l tests/keymap-popup-tests.el \
              -f ert-run-tests-batch-and-exit

Appendix A GNU Free Documentation License