Emacs Hydra menu
Table of Contents
Words….
It's been a while since I've written anything meaningful. I suppose I've just been feeling a bit lazy, but to my defense, I've spent a lot of time training on my bike 🚴. I needed a change of pace. Don’t get me wrong—I love programming and the exhilaration of being in the zone where everything flows effortlessly. But sometimes, I lose it all. Inspiration fades, and more than anything, I find it hard to sit down in front of the computer and actually get something done.
My daytime job as a software engineer often drains the energy right out of me. One way I try to cope is by taking deeper breaths—usually while cycling. Anyway, I realize I need to do something to kickstart my motivation. So here I am, trying to get myself moving again, having changed the spark plugs and added some starting fuel. Trying to jumpstart my writing once more… Honestly, it’s not going great yet.
Motivation
One of my main problems when using emacs is remebering the key shortcuts or even functions that I need to run. Don't take wrong its both a benefit and a curse, maybe it boils down to me not having a perfect memory. Though I guess im not the only one. The reason for the last statement is based on all the different meta-menu shortcuts that you can build. So what do I mean by that? Well, there are several different menu system that can be used and several different ways of searching for the right functionality. For example hydra which maps a popup menu, and this can be mapped to certain major-mode, and each item is based on a key and a function. Lets make a really short example
(defhydra hydra-zoom (:color amarath :title fisk :separator "🪓")
"🐛 zoom 🐛"
("g" text-scale-increase "in")
("l" text-scale-decrease "out")
("q" nil "quit"))
As the result shows it returns a hydra-zooom/body
, which is a interactive function and shows a
nice menu as in , the red frame outlines the popup.
This of course could be connected to a key-shortcut or run
interactivly. Each of the defined hydra menu will automatically have
a <name>/body
function, which then can be bound to some key for
example the above example could be bound to:
(bind-key "C-c c z" #'hydra-zoom/body)
Another nice way to deal with this, is to map a major mode to some
hydra-popup, with a certain key-binding M-SPC
. That means , we
get a different popup depending on what major-mode is running. .
This can be done using major-mode-hydra. The following example shows
how to initialize major-mode-hydra
(use-package major-mode-hydra
:straight t
:config
(global-set-key (kbd "M-SPC") #'major-mode-hydra)
(setq hydra-hint-display-type 'posframe)
)
Lets make a simple example using the major mode: text-mode . The idea is to create a major mode that maps to
M-SPC
when in text-mode.
I added some different options in the following example:
(major-mode-hydra-define text-mode
(:color red :title "Ꙫ Text mode Ꙫ" :separator "⨝" :quit-key "q" :foreign-keys warn :hint t :timeout 15)
(
"🐛 zoom 🐛"
(("g" text-scale-increase "in" :color blue :column "vertical")
("l" text-scale-decrease "out")
("d" whitespace-mode "Debug on error" :toggle (bound-and-true-p whitespace-mode ))
("q" nil "quit")
)
"Options"
(("f" flyspell-mode "Flyspell mode" :toggle (bound-and-true-p flyspell-mode))
("v" visual-line-mode "visual line mode" :toggle (bound-and-true-p visual-line-mode))
("b" abbrev-mode "abbrev mode" :toggle (bound-and-true-p abbrev-mode))
("a" auto-fill-mode "auto fill mode" :toggle (and (boundp 'auto-fill-function) auto-fill-function))
("n" display-line-numbers-mode "line number" :toggle t)
("h" highlight-changes-mode "Highlight changes" :toggle t (bound-and-true-p highlight-changes-mode))
)
"Face"
(("w" writeroom-mode "Writer room" :toggle ))
))
This creates a hydra function called:
(major-mode-hydras/text-mode/body)
, which when called will bring up
the popup, since we also are using major-mode-hydra which has mapped
M-Spc
, each time we use that shortcut it will bring up the menu.
But if we want to use this menu in a different mode, for example
org-mode we could do map a key to the function as in:
(add-hook 'org-mode-hook
(lambda ()
(local-set-key (kbd "<f6>") #'major-mode-hydras/text-mode/body)))
Now it is linked to M-Spc
in text-mode and F6
in org-mode (noting that org-mode could have a different menu mapped to M-Spc
).
major-mode-hydra
also extends the possibility to use toggles, which
can be very useful. In the above example I added a couple of differnt
toggles for example whitespace-mode
which
shows the whitespaces in the current text. Its also possible to change
the layout in the popup. The above creates three columns with headings
Zoom, options ,/face/.
Some of the properties needs some explanation.
- timeout
- Timeout before the menu disappears
- exit
- After executing the command, the menu will dissapear
- Color
- Color actually have a side effect except for just coloring
the field. It is an aggregation of
foreign-keys
andexit
- red
- blue
- exit t
- amaranth
- Foreign-keys warn
- teal
- foreign-keys warn, exit t
- pink
- foreign-keys run
- foreign-keys
- If a key is hit that is not part of the defined, what will happen?
- warn
- A warning
- nil
- Hydra state will stop and the foreign key will do whatefer it was suppose to do.
- run
- Will not stop the hydra state, and try to run the foreign key, this can be somewhat confusing.
The template for creating a layout can be something like the following yas-snippets.
# -*- mode: snippet -*-
# name: major-mode-hydra menu
# key: <hydra
# group: col
# --
(major-mode-hydra-define ${1:text-mode}
(:color ${2:$$(yas-choose-value '("red" "blue" "amaranth" "teal" "pink"))} :title "${3:My title}" :separator "${4:―}")
(
${5:<hydra-column}$0
));
Now for the column snippet
# -*- mode: snippet -*-
# name: Hydra column
# key: <hydra-column
# group: col
# --
"${1:Column header}"
(("${2:key}" ${3:function} "${4:description}")
${5:("${6:key}" ${7:function} "${8:description}")}
)
${9:<hydra-column}$0
This snippets are kind of neat, since it provides a recursive way of implement different columns.
Adding this and pressing M-<space>=
will open the above popup-menu
above if in text-mode. This of course can be chained with other
menus. For example; you have a some options where you want to
change the theme or font or something else, this could potentially be done
in another menu with other option, more on this in the later section.
But lets first take a look at how to extend this the hydra
Extending Hydra
To be able to extend major-mode-hydra/....
with additional options there is a macro
called pretty-hydra-define+
where you can define your
extensions. For example lets make take the above example where we
created major-mode-hydras/text-mode
hydra. We could extend this by
adding the following lines
(pretty-hydra-define+ major-mode-hydras/text-mode ()
("Face"
(("C-e" ediff-buffers "Ediff buffers")
("C-f" ediff-current-file "Ediff current file"))
))
Chaining Hydra
As discussed in the previous section maybe we want to some chaining of different menus. Think of it as a menu with submenus , obviously you can do alot of things with this.
Lets consider creating a project
menu.
The idea is to have a menu where the most common options
from the the main menu, and then create a sub menu.
So what are the common things I want to do with a project.
Since I use many different modes, i dont want it to be directly
connected to a mode, instead I will use some key, as i did before (F6).
So lets define the functions that i usually use in projects
- Open
- I want to open a project using
o
, this is achieved throughprojectile-open-project
- Switch open
- Switch to an already open project using
s
, this is achieved throughprojectile-switch-open-project
. - Find file
- Find a file in the current project
f
, usingcounsel-projectile-find-file
- Find file with content
- I use this quite a bit
counsel-projectile-rg
"r"
That f
(pretty-hydra-define col-project-main-menu (:foreign-key warn :title "Project" :quit "q" :separator "")
("Project"
(("o" counsel-projectile-switch-project "Open Project" :color teal)
("s" projectile-switch-open-project "Switch Project" :color teal)
("f" counsel-projectile-find-file "Find file "))
"Files"
(("g" magit-status "Git status" :color teal)
("r" counsel-rg "Ripgrep")
("d" (dired-jump t) "Dired" :color teal)
("l" org-store-link "Store link" :color teal)
)
"Sub Menus"
(("TAB" hydra-tab-bar/body "Tabs" :color teal)
("t" major-mode-hydras/text-mode/body "Text Menu" :color teal)
("c" hydra-org-clock/body "Hydra clock" :color teal )
)))
Obviously this should not be connected to a mode, so we need some kind of key-binding.
(bind-key "C-c c SPC" #'col-project-main-menu/body)
Great, now we can open the menu whenever we hit C-c c SPC
. But
there is a small issue, we need the new sub menu for TAB
and c
, we have
defined it. But there is no implementation. Lets make that.
(defhydra hydra-tab-bar (:color amaranth)
"Tab Bar Operations"
("c" tab-new "Create a new tab" :column "Creation")
("d" dired-other-tab "Open Dired in another tab")
("f" find-file-other-tab "Find file in another tab")
("0" tab-close "Close current tab")
("m" tab-move "Move current tab" :column "Management")
("r" tab-rename "Rename Tab")
("<return>" tab-bar-select-tab-by-name "Select tab by name" :column "Navigation")
("l" tab-next "Next Tab")
("j" tab-previous "Previous Tab")
("<right>" tab-next "Next Tab" )
("<left>" tab-previous "Previous Tab")
("q" nil "Exit" :exit t))
The only problem is that i really want this to have a short cut too.
(bind-key "C-c c TAB" #'hydra-tab-bar/body)
Let finally add the last piece of the puzzle. For this I was lazy, there is actually a small community wiki with different examples. So instead of creating a complete new I just copied it from Hydra wiki.
(defhydra hydra-org-clock (:color blue :hint nil)
"
Clock In/out^ ^Edit^ ^Summary (_?_)
-----------------------------------------
_i_n _e_dit _g_oto entry
_c_ontinue _q_uit _d_isplay
_o_ut ^ ^ _r_eport
"
("i" org-clock-in)
("o" org-clock-out)
("c" org-clock-in-last)
("e" org-clock-modify-effort-estimate)
("q" org-clock-cancel)
("g" org-clock-goto)
("d" org-clock-display)
("r" org-clock-report)
("?" (org-info "Clocking commands")))
Nice, we could continue extending this. But for now this is good enough as an example
Summary
Obviously hydra
can be a useful tool especially since creating
shortcuts for everything drives you crazy. Though, I would recommend
taking your time and plan what you want todo and how the menu(s)
should behave. Using major-mode-hydra
makes life easier, but it is easy
to make a mess of everything.
There are however some issues with hydra that I'm missing. For example
some functions needs a input argument.
But maybe the next the next package can help out with this.
Lets take a look at Transient which is another tool for creating menus.