As we all know, Emacs is so much more than just a text editor. There are quite a few serious applications written on top of it, like Org-mode or mu4e. And many applications – including those two – contain menus (the mu4e main menu or Org-mode’s exporting menu).
Since I am in the (slow) process of writing my own application in Emacs (an in-house tool, so probably of little interest to the general public – but I might be tempted to blog about parts of it one day), I got interested in the problem of coding my own menu.
One of the easiest ways to do that is to display a menu in a temporary buffer and then use read-key
or its ilk. This is probably not the best idea most of the time. One of the reasons Emacs is so brilliant is that it (almost) doesn’t have modal dialog windows. So this blocks the user from doing anything else, for instance.
Conisder how Emacs’ own VC works. If you decide to commit something, and even pop up the *vc-log*
buffer (the one you type your commit message in), and then change your mind, the manual advises you explicitly not to kill that buffer:
To abort a commit, just type ‘C-c C-c’ in that buffer. You can switch buffers and do other editing. As long as you don’t try to make another commit, the entry you were editing remains in the ‘vc-log’ buffer, and you can go back to that buffer at any time to complete the commit.
The Emacs way has always been to use dedicated buffer with a major mode binding keys from your menu to the actions you want to launch from the menu. (This is how mu4e menu works, for instance.) Of course, there is always the “official” drop-down menu, bound to F10
by default – but I’m less interested in drop-down menus here.
Some Emacs packages deviate from this convention. For instance, Org-mode’s exporting menu is effectively a dialog window. This has all the drawbacks a dialog has, like non-searchability etc., but since I usually spend very little time there, it is rarely a problem. (Many people, however, do not know that you can easily scroll in that window, which is one of the reasons it might benefit from a redesign.)
Interestingly, one of the modern approaches to a menu in Emacs – Oleh Krehel’s hydra – is at first glance very similar to a modal dialog. You fire a hydra, it is displayed at the bottom of the screen, and any key you press is handled by it. And there are lots of hydra users who apparently do like this behavior. What is going on here?
Well, apparently Emacs has an analog of modal dialog windows: key sequences. Even in vanilla Emacs, if you press C-x
, anything you press next is catched and interpreted by the C-x
prefix keymap. Hydra is just an extension of this mechanism (and not the only one: guide-key and which-key are popular alternatives).
The bottom line of what I have said until now is this: if I wanted to make a menu for my Emacs app, hydra is a light-weight solution, probably wort considering. But what if I want something more traditional? (I might not want an external dependency - hydra is not part of Emacs. I might want clickable items or drop-downs etc. The reasons may vary.)
Well, we then want to have a dedicated buffer with the menu. Displaying the menu is the easy part – it’s basically a bunch of insert
calls. The important part (though not much harder) is the association of keybindings with the menu items.
Let’s start with something simple. For the sake of example, let me create a menu which will enable me to save all files, exit Emacs or start another instance of Emacs (this is really a contrived example, but you can easily imagine more useful actions).
(defun emacs-start-menu () "Make Emacs like MS Windows." (interactive) (switch-to-buffer " *Start Menu*") (erase-buffer) (insert "Emacs Start Menu\n" "\n" "* [S]ave all files\n" "* E[x]it Emacs\n" "* Start another [E]macs") (start-menu-mode))
Let’s see what happens here. First we switch to a buffer called " *Start-menu*"
. The convention is to surround the names of “special” buffers (I’m not sure whether this is their “official” name, but I couldn’t find it in the Emacs manual). The space at the beginning signals to Emacs that this is an “ephemeral” buffer – it does not save undo information and does not appear in the buffer list. Of course, you might or might not want to start your menu buffer name with a space, depending on the context. My rule of thumb is to start it with a space if there is some easy way to get to this buffer (like if I bound emacs-start-menu
to some global keybinding, for instance). We then erase anything which might have been previously in the buffer – most probably the very same menu we want to enter, but clearing it and inserting again is easier than checking whether the contents are what they should be, and we have no control over what the user did to our buffer in the meantime.
Then we insert a bunch of text (basically, our menu, or whatever makes sense in our case). Last but not least, we start the start-menu-mode
, which will make our menu actually work.
So, let’s define this now.
(define-derived-mode start-menu-mode special-mode "Emacs Start Menu" "A major mode for a buffer with the Emacs Start Menu.") (define-key start-menu-mode-map (kbd "S") #'save-some-buffers) (define-key start-menu-mode-map (kbd "x") #'save-buffers-kill-emacs) (define-key start-menu-mode-map (kbd "E") (lambda () (interactive) (start-process "another-emacs" " *another-emacs-buffer*" "emacs")))
What we do here is we define a new mode, derived from special-mode
. It is a very, well, special mode, which does two things: first, it makes the buffer read-only (which makes sense in the case of menus), and then, it has some default bindings, like scrolling with SPC
and +DEL=, quitting with q
and digits (without C-
or M-
) working as numeric prefix keys. To learn more, say C-h f special-mode RET
.
When we have our mode, the rest is very easy – we just add a few keybindings to it. The only thing that is non-trivial here is the lambda
expression. I didn’t bother with defining a named command to start another Emacs instance, so I used an anonymous one (not forgetting about the interactive
form).
Is that all for today? Yes. Is that all about menus? No. We could add colors, dynamic elements, clickable options and probably lots of other features. However, this will have to wait.