My Emacs life is better with Hydra

2022-07-14 Thu 00:00

Do you know the Emacs package Hydra? I tried it today, and I loved the way it changed my workflow. I was bragging about it to everyone at work, so I decided to write a blog post. That way I can show more people how fabulous it is :D

What is Hydra? Why should I use it?

Hydra makes it easier to execute a set of commands in succession by shortening the key bindings. A showcase of it pretty much describes it, so here it is!

Example 1: Window management

Let's say that I want to split a window into 4 with equal size, I would need to run the following key strokes.

C-w v ; split vertically
C-w s ; split horizontally
C-w l ; navigate to the left window
C-w s ; split horizontally

When I use Hydra the key strokes becomes like this.

C-w ; start hydra
v   ; split vertically
s   ; split horizontally
l   ; navigate to the left window
s   ; split horizontally
2022-07-14_10-33-43_Kapture.gif

Example 2: Navigating and changing Org mode headlines

I use Org Mode a lot. Even this blog is made by Org mode! I tend to navigate between headlines, and change headline levels after navigating.

2022-07-14_10-44-41_Kapture.gif

How do you set it up?

Everything is pretty much explained in the Hydra docs, but I'll write out some of my ah-ha moments.

But first, this is my complete windows setup.

(defhydra hydra-window (:hint nil)
  "
| Navigation^^      | Placement^^         | Create, Delete^^          | Adjustment^^         |
|^^-----------------+^^-------------------+^^-------------------------+^^--------------------|
| _h_: go left      | _H_: move to left   | _v_: split vertically     | _=_: balance windows |
| _j_: go down      | _J_: move to bottom | _s_: split horizontally   | _+_: increase height |
| _k_: go up        | _K_: move to top    | _q_: delete window        | _-_: decrease height |
| _l_: go right     | _L_: move to right  | _Q_: delete other windows | _>_: increase width  |
| _w_: go to next   | ^^                  | ^^                        | _<_: decrease width  |
| _C-w_: go to next | ^^                  | ^^                        | ^^                   |
"
  ("+"   evil-window-increase-height)
  ("-"   evil-window-decrease-height)
  ("<"   evil-window-decrease-width)
  (">"   evil-window-increase-width)
  ("="   balance-windows)
  ("C-w" evil-window-next nil :color blue)
  ("H"   evil-window-move-far-left)
  ("J"   evil-window-move-very-bottom)
  ("K"   evil-window-move-very-top)
  ("L"   evil-window-move-far-right)
  ("h"   evil-window-left)
  ("j"   evil-window-down)
  ("k"   evil-window-up)
  ("l"   evil-window-right)
  ("q"   evil-window-delete)
  ("Q"   delete-other-windows)
  ("s"   evil-window-split)
  ("v"   evil-window-vsplit)
  ("w"   evil-window-next))

Setting up key bindings

The documentation shows that you can setup key bindings in the first 2 arguments of the defhydra declaration.

This example creates the key binding of hydra-zoom to the global-map with the key <f2>.

(defhydra hydra-zoom (global-map "<f2>")
  "zoom"
  ("g" text-scale-increase "in")
  ("l" text-scale-decrease "out"))

This didn't work for me, probably because I have Evil Mode (Emacs) installed. I needed to setup the key bindings the usual Evil way.

(defhydra hydra-zoom ()
  "zoom"
  ("g" text-scale-increase "in")
  ("l" text-scale-decrease "out"))

(general-nmap 'global
  "<f2>" 'hydra-zoom/body)

You might notice that the key is bound to hydra-zoom/body and not hydra-zoom. This is because the defhydra macro does not create the hydra-zoom function. It creates a function called hydra-zoom/body instead.

After this setup, you should be able to invoke hydra-zoom with the f2 key.

Setting up behaviors of keys

Basic behavior can be set with the color option.

| color    | toggle                     |
|----------+----------------------------|
| red      |                            |
| blue     | :exit t                    |
| amaranth | :foreign-keys warn         |
| teal     | :foreign-keys warn :exit t |
| pink     | :foreign-keys run          |

The option behaves the following way.

  • :exit: when it is set to t, exits Hydra state when key is pressed
  • :foreign-keys: this describes the behavior when a key not defined in Hydra is pressed
    • nil: runs the command mapped to the key, and then exits Hydra state
    • warn: shows a warning message and does not run anything, and stays in the Hydra state
    • run: runs the command mapped to the key, but does not exit Hydra state

An example configuration will be something like this.

(defhydra hydra-zoom (:color amaranth) ; Sets the default color to amaranth
  "zoom"
  ("g" text-scale-increase "in")
  ("l" text-scale-decrease "out")
  ("q" nil "quit" :color blue) ; Overwrites the color to blue
  )

This will result in Hydra looking like this. You can see the colors! The keys behaves according to the chart above too.

2022-07-14_10-05-27_screenshot.png

The warning message looks like this.

2022-07-14_10-08-50_screenshot.png

Setting up custom hints

My window Hydra has this beautiful hint that is easy for me reference.

2022-07-14_09-40-07_screenshot.png

You can set this up with a special docstring defhydra. This is my window Hydra docstring.

(defhydra hydra-window (:hint nil)
  "
| Navigation^^      | Placement^^         | Create, Delete^^          | Adjustment^^         |
|^^-----------------+^^-------------------+^^-------------------------+^^--------------------|
| _h_: go left      | _H_: move to left   | _v_: split vertically     | _=_: balance windows |
| _j_: go down      | _J_: move to bottom | _s_: split horizontally   | _+_: increase height |
| _k_: go up        | _K_: move to top    | _q_: delete window        | _-_: decrease height |
| _l_: go right     | _L_: move to right  | _Q_: delete other windows | _>_: increase width  |
| _w_: go to next   | ^^                  | ^^                        | _<_: decrease width  |
| _C-w_: go to next | ^^                  | ^^                        | ^^                   |
"
  ...)

The special notations I use are

  • _: when the key are surrounded by _, Hydra uses the appropriate color
  • ^: character that becomes an empty string when Hydra shows the hint

You might wonder why I have the ^ notation everywhere. This is just to make the docstring clean. You can also write the hint without ^, but it would look a little crooked. The docstring below produces the exact same hint as the docstring above.

;; Without the ^ notations
(defhydra hydra-window (:hint nil)
  "
| Navigation      | Placement         | Create, Delete          | Adjustment         |
|-----------------+-------------------+-------------------------+--------------------|
| _h_: go left      | _H_: move to left   | _v_: split vertically     | _=_: balance windows |
| _j_: go down      | _J_: move to bottom | _s_: split horizontally   | _+_: increase height |
| _k_: go up        | _K_: move to top    | _q_: delete window        | _-_: decrease height |
| _l_: go right     | _L_: move to right  | _Q_: delete other windows | _>_: increase width  |
| _w_: go to next   |                   |                         | _<_: decrease width  |
| _C-w_: go to next |                   |                         |                    |
"
  ...)

Now it's your turn

Use it! It's amazing! Have fun!