Conditional hydra menus

| categories: hydra, emacs | tags:

Usually the hydra menu is hard coded in the defhydra macro. Sometimes, you would like conditional options, that is, depending on some condition we get different options when we run the hydra and not when it was defined. This is an open issue in hydra. Here we explore a way to achieve that. The idea is to construct the code for the hydra, then eval it, and run the hydra. In this example we make the conditional menu depend on whether we are on an even or odd numbered line. I use the `' syntax for defining the list of code. ` is a variation of ' (quote) that enables you to use the , operator to evaluate that element while in data mode. So, here is our first idea:

(defun my-hydra-1 ()
  (interactive)
  (eval
   `(defhydra my-hydra-1 (:color blue) "My hydra"
      ,(if (evenp (line-number-at-pos))
           '("e" (message-box "Even line") "Even")
         '("o" (message-box "Odd line") "Odd"))
      ,(when t '("a" (message-box "always true") "always"))
      ;; This does not work. you must return a legitimate hydra menu item
      ;;      ,(when nil '("n" (message-box "never") "never"))
      ))
  (my-hydra-1/body))

(my-hydra-1)
(my-hydra-1)
my-hydra

As long as it is not expensive to compute the conditionals, this seems like an easy enough way to get conditional options in a hydra. One limitation of the previous approach is our menu conditionals must return a hydra menu, and not nil. Here is an alternative approach to writing the function that solves the issue of the nil return in the last function. Here we build up the code list using append. It might seem like a macro should be used here, but I have not figured out how to get the macro to run the conditionals at the run-time. Note, we cannot use funcall on the defhydra because that is a macro.

(defun my-hydra-2 ()
  (interactive)
  (let ((conditionals '((if (evenp (line-number-at-pos))
                            '("e" (message-box "Even second") "Even")
                          '("o" (message-box "Odd second") "Odd"))
                        (when t '("a" (message-box "always true") "always"))
                        (when nil '("n" (message-box "never") "never")))))
    (eval
     (append
      '(defhydra my-hydra-2 (:color blue) "My hydra")
      (loop for cond in conditionals
            with result = (eval cond)
            if (eval cond)
            collect (eval cond))))
    (my-hydra-2/body)))

(my-hydra-2)
(my-hydra-2)

That works too. Let us try another type of syntax where the conditional statements have a cons cell with a conditional statement, and a hydra menu option for when the statement is true. This is functionally similar to our first method, but has some advantages in brevity and less quoting. We add a conditional hint here too (at some expense of additional quoting).

(defun my-hydra-3 ()
  (interactive)
  (let ((conditionals
         `(((evenp (line-number-at-pos)) . ("e" (message-box "Even second") ,(format "Even: %s" (line-number-at-pos))))
           ((oddp (line-number-at-pos)) . ("o" (message-box "Odd second") ,(format "Odd: %s" (line-number-at-pos))))
           (t . ("a" (message-box "always true") "always"))
           (nil . ("n" (message-box "never") "never")))))
    (eval
     (append
      '(defhydra my-hydra-3 (:color blue) "My hydra")
      (loop for cond in conditionals
            if (eval (car  cond))
            collect (cdr cond))))
    (my-hydra-3/body)))

(my-hydra-3)
(my-hydra-3)

I cannot figure out how to abstract this much further. There is a little redundancy in names, e.g. in the defhydra and at the end, but it is not too bad, which would usually be handled by a macro. I tried some defmacros to try this, but I could not figure out how to get the conditionals to expand at the right times, which is at run time, and not at macro expansion time. I need a macro that generates a function that has the call to defhydra in it! Maybe next year ;)

Copyright (C) 2015 by John Kitchin. See the License for information about copying.

org-mode source

Org-mode version = 8.2.10

Discuss on Twitter