A lot of the changes for this release aren't very user-visible, although they
are important in improving the usability. The main change is the move from the
standard set-transient-map
to my own hydra-set-transient-map
.
This change has allowed to remove the part where the amaranth and pink hydras
intercept a binding which doesn't belong to them and then try to forward it back
to Emacs. It was done in this way, which might be interesting for people who
write Elisp:
(define-key hydra-keymap t 'hydra-intercept)
Binding t
means that when the keymap is active, any binding which doesn't
belong to the keymap will be interpreted as t
. Then, I would use lookup-key
and this-command-keys
and try to call the result. That method was quite
fragile:
- It didn't work for prefix keys while a pink hydra was active.
- It didn't work for some keys in the terminal because of
input-decode-map
.
The new method solves the mentioned issues by not using t
and instead running
this function in the pre-command-hook
:
(defun hydra--clearfun ()
"Disable the current Hydra unless `this-command' is a head."
(if (memq this-command '(handle-switch-frame
keyboard-quit))
(hydra-disable)
(unless (eq this-command
(lookup-key hydra-curr-map
(this-single-command-keys)))
(unless (cl-case hydra-curr-foreign-keys
(warn
(setq this-command 'hydra-amaranth-warn))
(run
t)
(t nil))
(hydra-disable)))))
This approach is actually very similar to what the built-in set-transient-map
does from Emacs 24.4 onward. Of course, changing the a large cog in the Hydra
mechanism can lead to some new bugs, or even old bugs to re-surface. So I really
appreciate the help from @jhonnyseven in testing the new code.
As always, if you find something very broken, you can roll back to the GNU ELPA version
and raise an issue.
Fixes
single command red/blue issue
Fix the uniqueness issue, when a single command is assigned to both a
red and a blue head.
Here's an example:
(defhydra hydra-zoom (global-map "<f2>")
"zoom"
("g" text-scale-increase "in")
("l" text-scale-decrease "out")
("r" (text-scale-set 0) "reset")
("0" (text-scale-set 0) :bind nil :exit t)
("1" (text-scale-set 0) nil :bind nil :exit t))
Here, three heads are assigned (text-scale-set 0)
, however their behavior is different:
- r doesn't exit and has a string hint.
- 0 exits and has an empty hint (so only the key is in the docstring).
- 1 exits and has a nil hint (will not be displayed in the docstring).
The latter two call hydra-zoom/lambda-0-and-exit
, while r
calls hydra-zoom/lambda-r
.
Don't default hydra-repeast--prefix-arg
to 1
See #61 for more info.
Allow hydra-repeat
to take a numeric arg
For hydra-vi
, it's now possible to do this 4j.2...
The line will be forwarded:
- 4 times by 4j
- 4 times by .
- 2 times by 2.
- 2 times by .
See #92 for more info.
Key chord will be disabled for the duration of a hydra
This means that hydras have become much more easy to use with key chords. For
instance, if dj key chord calls a hydra or is part of the hydra, you
won't call the jj key chord by accident with djj.
See #97 for more info.
New Features
Variable as a string docstring spec
You can now use this form in your hydras:
(defvar foo "a b c")
(defhydra bar ()
"
bar %s`foo
"
("a" 't)
("q" nil))
Previously, it would only work for %s(foo)
forms.
:bind property can now also be a keymap
If you remember, you can set :bind
in the body to define in which way the
heads should be bound outside the Hydra. You also assign/override :bind
for
each head. This is especially useful to set :bind
to nil for a few heads that
you don't want to bind outside.
Previously, :bind
could be either a lambda or nil. Now a keymap is also accepted.
Integration tests
In addition to the abundant macro-expansion tests, integration tests are now
also running, both for emacs24
and for emacs-snapshot
. This means that hydra
should be a lot more stable now.
Here's an example test:
(defhydra hydra-simple-1 (global-map "C-c")
("a" (insert "j"))
("b" (insert "k"))
("q" nil))
(ert-deftest hydra-integration-1 ()
(should (string= (hydra-with "|"
(execute-kbd-macro
(kbd "C-c aabbaaqaabbaa")))
"jjkkjjaabbaa|"))
(should (string= (hydra-with "|"
(condition-case nil
(execute-kbd-macro
(kbd "C-c aabb C-g"))
(quit nil))
(execute-kbd-macro "aaqaabbaa"))
"jjkkaaqaabbaa|")))
In the tests above, hydra-simple
is a defined and bound hydra. "|"
represents the buffer text (empty), where |
is the point position.
And (kbd "C-c aabbaaqaabbaa")
represents the key sequence that you can normally press by hand.
Finally, "jjkkjjaabbaa|"
is what the buffer and the point position should look like afterwards.
If you find a hydra bug, it would be really cool to submit a new integration test to make sure that
this bug doesn't happen in the future.
Basic error handling
I really like the use-package feature where it catches load-time errors and issues a message instead of bringing up the debugger. This is really useful, since it's hard to fix the bug with a mostly broken Emacs, in the case when the error happened early in the load process. So the same behavior now happens with defhydra
. In case of an error, defhydra
will be equivalent to a no-op, and the error message will be written to the *Messages*
buffer.
Use a variable instead of a function for the hint
This leads up to the yet unresolved #86, which asks
for heads to be activated conditionally.
For now, you can modify the docstring on your own if you wish.
Here's some code from the expansion of hydra-zoom
to explain what I mean:
(set
(defvar hydra-zoom/hint nil
"Dynamic hint for hydra-zoom.")
'(format
#("zoom: [g]: in, [l]: out."
7 8 (face hydra-face-red)
16 17 (face hydra-face-red))))
;; in head body:
(when hydra-is-helpful
(if hydra-lv
(lv-message
(eval hydra-zoom/hint))
(message
(eval hydra-zoom/hint))))
Eventually, I'll add some automatic stuff to fix #86. But for now, you can
experiment with modifying e.g. hydra-zoom/hint
inside heads, if you want.
Multiple inheritance for Hydra heads
Each hydra, e.g. hydra-foo
will now declare its own heads as a variable
hydra-foo/heads
. It's possible to inherit them like this:
(defhydra hydra-zoom (global-map "<f2>")
"zoom"
("g" text-scale-increase "in")
("s" text-scale-decrease "out"))
(defhydra hydra-arrows ()
("h" backward-char "left")
("j" next-line "down")
("k" previous-line "up")
("l" forward-char "right"))
(defhydra hydra-zoom-child (:inherit (hydra-zoom/heads hydra-arrows/heads)
:color amaranth)
"zoom"
("q" nil))
Here, hydra-zoom-child
inherits the heads of hydra-zoom
and hydra-arrows
.
It adds one more head q to quit. Also, it changes the color to
amaranth, which means that it's only possible to exit it with q or
C-g. This hydra's parents remain at their original (red) color.
See #57 for more info.