Customizing the Emacs Context Menu
03 Jan 2023 Charles Choi
Summary
(best played full screen, no audio)
Novice Elisp programmer (me) succeeds in customizing Emacs right mouse button menu behavior. Uses this knowledge to copy common actions seen in native (in particular macOS) applications.
This post captures how I got here.
Background
Context menus triggered by a right mouse button event are a commonly used GUI pattern. Emacs provides support for this via context-menu-mode
which was introduced in version 28. Customizing context-menu-mode
requires some understanding how menus are structured. I won’t go into too much detail describing how Emacs menus work here, but hopefully will impart enough about it to get you going.
What I Did
Requirements
- Context menu should only show menu actions based on context. Different criteria for context are enumerated below and can be used in combinatorial fashion for a particular action.
- buffer mode (major and minor)
- region is selected (active) or not
buffer-file-name
is version controlled
Preconditions
My customized context menu relies on these packages being installed. YMMV. Also, you must use Emacs version ≧ 28.
- osx-dictionary - Dictionary lookup using the macOS-installed dictionary
- google-this - Search for term in Google
- reveal-in-folder - Reveal current file in native OS folder.
- Export an Org Region to RTF - Elisp snippet for using macOS command line utility
textutil
to convert Org markup to rich text format - ox-slack - Exports Org markup to Slack markup
- ox-gfm - dependency required by ox-slack
Turn on context-menu-mode
I’ve got context-menu-mode
turned on in both prog-mode-hook
and text-mode-hook
which should cover most all my programming (Python, ObjC, Swift, Elisp, C, etc.) and text (Org, Markdown) modes via inheritance. It is also turned on for dired and shell modes.
1 2 3 4 |
|
At this point, you should be able to right mouse button click anywhere on a buffer see a menu. But that menu is quite unexciting. We’ll address this next.
Let’s Make a Menu
A common context menu action is to take selected text and either capitalize, upper, or lower case it, as illustrated below.
We’ll implement the above as a sub-menu with the user-defined variable cc/transform-text-menu
whose value is a sparse keymap. The code below illustrates how this is implemented.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Note that the actual commands to execute the transformations (upcase-region
, downcase-region
, capitalize-region
) are invoked in each menu-item
call.
Note that at this point cc/transform-text-menu
only defines the sub-menu but it is not attached to anything yet.
Let’s Make a Menu, Part 2
Let’s make another sub-menu with the user-defined variable cc/org-emphasize-menu
, this time to format (emphasize) selected text in Org-mode as shown below.
The source to achieve this is described below. Note that the actual emphasis commands (cc/org-emphasize-bold|italic|code|…
) in this menu are built on-top of org-emphasize
, which generalizes what type of emphasis (bold, italic, code, etc.) to use as a function argument.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
|
With the above source, we now have a variable cc/org-emphasis-menu
that holds the emphasis sub-menu. We are ready to hook this into the context menu!
Customizing the Context Menu
Customizing the context menu requires:
- Defining a new function (in this case
cc/context-menu-addons
) to capture the customized context menu behavior. - Hooking that new function (
cc/context-menu-addons
) into the variablecontext-menu-functions
via theadd-hook
function.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
(defun cc/context-menu-addons (menu click) "CC context menu additions" (save-excursion (mouse-set-point click) (define-key-after menu [open-in-finder] '(menu-item "Open in Finder" reveal-in-folder-this-buffer :help "Open file (buffer) in Finder")) (when (region-active-p) (define-key-after menu [osx-dictionary-lookup] '(menu-item "Look up" osx-dictionary-search-word-at-point :help "Look up in dictionary")) (define-key-after menu [occur-word-at-mouse] '(menu-item "Occur" occur-word-at-mouse :help "Occur"))) (when (and (bound-and-true-p buffer-file-name) (vc-registered (buffer-file-name))) (define-key-after menu [vc-separator] '(menu-item "--single-line")) (define-key-after menu [magit-status] '(menu-item "Magit Status" magit-status :help "Magit Status")) (define-key-after menu [ediff-revision] '(menu-item "Ediff revision…" cc/ediff-revision :help "Ediff this file with revision"))) (when (region-active-p) (define-key-after menu [transform-text-separator] '(menu-item "--single-line")) (define-key-after menu [tranform-text] (list 'menu-item "Transform" cc/transform-text-menu))) (when (and (derived-mode-p 'org-mode) (region-active-p)) (define-key-after menu [org-emphasize] (list 'menu-item "Org Emphasize" cc/org-emphasize-menu)) (define-key-after menu [org-export-to-slack] '(menu-item "Copy as Slack" org-slack-export-to-clipboard-as-slack :help "Copy as Slack to clipboard")) (define-key-after menu [copy-as-rtf] '(menu-item "Copy as RTF" dm/copy-as-rtf :help "Copy as RTF to clipboard"))) (when (region-active-p) (define-key-after menu [google-search] '(menu-item "Search with Google" google-this-noconfirm :help "Search Google with region")))) menu) ;; hook into context menu (add-hook 'context-menu-functions #'cc/context-menu-addons)
Note that the implementation of cc/context-menu-addons
has logic to query context information such as buffer mode, whether text is selected (region-active-p
), and source control (vc-registered
) to determine if a menu-item
is to be rendered.
Extras: Clean up Ediff UX
IMHO, Ediff has terrible UX out of the box, especially when quitting where it leaves a bunch of buffers strewn about. The code below helps mitigate some of that behavior thanks to this posted comment in StackExchange.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Also note that my Ediff variables are set as follows:
1 2 3 |
|
Observations
Going through this exercise, a couple of observations:
- Customizing an Emacs menu requires a lot of boilerplate. Maybe because I’m naive to Elisp and Emacs conventions, there might be a cleaner implementation, but it seems like I gotta work too hard (both learning and implementing) to customize a menu.
- I tried to implement context-dependent logic in the
:visible
property documented in Extended Menu Items in the functioncc/context-menu-addons
. Didn’t work. Elisp experts, definitely would like your feedback. - Having the function
cc/context-menu-addons
hooking into a global variablecontent-menu-functions
seems like a very monolithic design. Maybe there’s a cleaner pattern here? - Embark looks awesome, but sometimes you just want a mouse.
- Beyond thrilled to have gotten here. Hope you feel likewise!
Acknowledgments
Thanks to Philip Kaludercic’s post that helped considerably in my understanding of how to customize a context menu.
Thanks to deaddyfreaddy for providing informal code review on Reddit for an earlier version of this post. This current post has been amended to reflect his feedback.
References
- https://www.gnu.org/software/emacs/manual/html_node/emacs/Menu-Mouse-Clicks.html
- https://ruzkuku.com/texts/emacs-mouse.html
- https://emacs.stackexchange.com/a/17089
- https://www.gnu.org/software/emacs/manual/html_node/elisp/Extended-Menu-Items.html
- https://github.com/oantolin/embark
- https://gist.github.com/danielmartin/3c5d3a3a8cd24a3556379c5251651748