Emacs: multiple commands in a single binding, without Transient

Published 2024-12-05

tag(s): #tutorial #emacs

I have nothing but love for Transient, it is amazing package. It was a natural choice to build Sharper, my dotnet wrapper.
And lately it's been all the rage, since it is now part of Emacs core, and people are building pretty cool integrations with it.[1]

But you don't need Transient if you want a single binding to show a menu-like setup to invoke commands.
Notice that what I described has a much more limited scope than Transient!!! The latter allows keeping track of state (for example, and originally, toggling Git command flags and persistent options). While I propose an alternative for only command invocations, so at most you would use a prefix arg here and there.
Even if limited, these menus can still be useful, and are much easier to build.

The context

I have a personal keymap bound to F6[2], where, among many other keys...

Then in what I view as complementing the project-* commands, I HAD a pair of bindings: You know, like the "regular" command is in project context, and the "ESC" version[3] is the more generic command.

But then I used a couple times find-grep-dired to put files in a Dired buffer for multi-file replacement using dired-do-find-regexp-and-replace, as described in this manual section.
And I figured I could add a binding for it. The thing is, I don't use this often enough to have a completely dedicated binding. And, both the g and f keys where bound already. And also! is this a find or a grep command?
Other Emacs users will understand my plight :)

An inspiration, and code dive

I use mhtml-mode to write my posts, which has an interesting binding in M-o: it shows a menu where you can choose formatting markup to insert (or wrap the active region), like this:

Screenshot of the bottom of an
    Emacs frame, with a menu in the echo area.
(direct link to image)

I had the idea to make a similar menu for the find-* commands. Then I wouldn't need to recall their names to M-x command-name, nor have three distinct "top level" key bindings. Instead a single key sequence, F6-ESC-f, would be an umbrella for the idea of "put files in a Dired buffer to operate on them".

Time to navigate the source code! A hurdle first: C-h k and then M-o didn't show the Help buffer with a link to the command definition, but the menu. So I tried describe-keymap, but even then I couldn't get exactly to the definition of the menu, but instead of the individual functions.

My next idea was to use describe-mode in mhtml-mode, which in turn took me to html-mode and finally to what I was looking for:


;; code in sgml-mode.el
        
(defvar html-mode-map
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map  sgml-mode-map)
    (define-key map "\C-c6" #'html-headline-6)
    (define-key map "\C-c5" #'html-headline-5)
    ;; edited for brevity
    (define-key map "\C-c\C-v" #'browse-url-of-buffer)
    (define-key map "\M-o" 'facemenu-keymap)
    map)
"Keymap for commands for use in HTML mode.")

;; after navigating to the definition of facemenu-keymap, in facemenu.el

(defvar facemenu-keymap
  (let ((map (make-sparse-keymap "Set face")))
    (define-key map "o" (cons "Other..." 'facemenu-set-face))
    (define-key map "\M-o" 'font-lock-fontify-block)
    map)
  "Keymap for face-changing commands.
`Facemenu-update' fills in the keymap according to the bindings
requested in `facemenu-keybindings'.")
(defalias 'facemenu-keymap facemenu-keymap)
    

Reading the comments in both files, and navigating the code, I found functions and configuration to augment the facemenu at runtime.
But for my purposes, I don't need any of that, just assigning a fixed, predefine keymap to a binding, as a function (hence the defalias...) seems to be enough to get a menu displayed, but you can skip that step if you use keymap-set instead of define-key[4].
Understanding the subtleties between these two assignments is something I still have in my TODO list...

My solution


(use-package dired
  ;; -- removed code for clarity here --

  :config
  ;; What are the differences between the last two commands?
  ;; (info "(emacs) Dired and Find")
  (defvar-keymap hoagie-find-keymap
    :doc "Keymap for Dired find commands."
    :name "Find..."
    "g" '("grep dired" . find-grep-dired)
    "n" '("name dired" . find-name-dired)
    "d" '("dired" . find-dired))
  ;; UPDATE 2024-11-04: I saw this technique in "M-o" for sgml-mode, which in
  ;; turn uses facemenu.el, but it only works correctly if I assign the binding
  ;; "manually" instead through use-package
  (keymap-set hoagie-keymap "ESC f" hoagie-find-keymap)

  ;; -- removed code for clarity here --
    

And this is the modest, but useful, end result:

Screenshot of the bottom of
    an Emacs frame, with a menu in the echo area.
(direct link to image)

So, there you go, you can get a menu-like behaviour, that pops all the associated command names, using only defvar-keymap and keymap-set.

Footnotes
  1. There are a few, but the ones that come to mind are Charles Choi's "Casual *" packages: Casual Calendar, Casual Bookmarks, and a few others. Check his site.
  2. Reachable in "normal" keyboards, but more important, a thumb key in my Dygma Raise configuration. 😏
  3. ESC is also a thumb key in my Raise. 😌
  4. Of course I experimented a bit using the scratch buffer to confirm this.

Share your thoughts (via email)

Back to top

Go to homepage