Customizing the Emacs Context Menu
03 Jan 2023 Charles Choi
(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.
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
- 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-nameis version controlled
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
textutilto convert Org markup to rich text format
- ox-slack - Exports Org markup to Slack markup
- ox-gfm - dependency required by ox-slack
context-menu-mode turned on in both
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 (
capitalize-region) are invoked in each
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 variable
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
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
:visibleproperty documented in Extended Menu Items in the function
cc/context-menu-addons. Didn’t work. Elisp experts, definitely would like your feedback.
- Having the function
cc/context-menu-addonshooking into a global variable
content-menu-functionsseems 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!
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.