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.
I have a personal keymap bound to F6[2], where, among many other keys...
F6-g
invokes project-find-regexp
F6-f
does project-find-file
F6-ESC-g
for rgrep
F6-ESC-f
does find-name-dired
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 :)
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:
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...
(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:
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
.