;; Author: Oleh Krehel <ohwoeowho@gmail.com>
;; Maintainer: Oleh Krehel <ohwoeowho@gmail.com>
;; URL: https://github.com/abo-abo/hydra
-;; Version: 0.9.0
+;; Version: 0.10.0
;; Keywords: bindings
;; Package-Requires: ((cl-lib "0.5"))
;;; Code:
;;* Requires
(require 'cl-lib)
+(require 'lv)
(defalias 'hydra-set-transient-map
- (if (fboundp 'set-transient-map)
- 'set-transient-map
- 'set-temporary-overlay-map))
+ (if (fboundp 'set-transient-map)
+ 'set-transient-map
+ (lambda (map keep-pred &optional on-exit)
+ (set-temporary-overlay-map map (hydra--pred on-exit)))))
+
+(defun hydra--pred (on-exit)
+ "Generate a predicate on whether to continue the Hydra state.
+Call ON-EXIT for clean-up.
+This is a compatibility code for Emacs older than 24.4."
+ `(lambda ()
+ (if (lookup-key hydra-curr-map (this-command-keys-vector))
+ t
+ (hydra-cleanup)
+ ,(when on-exit
+ `(funcall ,(hydra--make-callable on-exit)))
+ nil)))
;;* Customize
(defgroup hydra nil
It's the only other way to quit it besides though a blue head.
It's possible to set this to nil.")
+(defcustom hydra-lv t
+ "When non-nil, `lv-message' (not `message') will be used to display hints."
+ :type 'boolean)
+
(defface hydra-face-red
- '((t (:foreground "#7F0055" :bold t)))
+ '((t (:foreground "#FF0000" :bold t)))
"Red Hydra heads will persist indefinitely."
:group 'hydra)
(defface hydra-face-blue
- '((t (:foreground "#758BC6" :bold t)))
+ '((t (:foreground "#0000FF" :bold t)))
"Blue Hydra heads will vanquish the Hydra.")
(defface hydra-face-amaranth
'((t (:foreground "#E52B50" :bold t)))
- "Amaranth Hydra can exit only through a blue head.")
+ "Amaranth body has red heads and warns on intercepting non-heads.
+Vanquishable only through a blue head.")
+
+(defface hydra-face-pink
+ '((t (:foreground "#FF6EB4" :bold t)))
+ "Pink body has red heads and on intercepting non-heads calls them without quitting.
+Vanquishable only through a blue head.")
+
+(defface hydra-face-teal
+ '((t (:foreground "#367588" :bold t)))
+ "Teal body has blue heads an warns on intercepting non-heads.
+Vanquishable only through a blue head.")
+
;;* Fontification
(defun hydra-add-font-lock ()
"Fontify `defhydra' statements."
(font-lock-add-keywords
'emacs-lisp-mode
'(("(\\(defhydra\\)\\_> +\\(.*?\\)\\_>"
+ (1 font-lock-keyword-face)
+ (2 font-lock-type-face))
+ ("(\\(defhydradio\\)\\_> +\\(.*?\\)\\_>"
(1 font-lock-keyword-face)
(2 font-lock-type-face)))))
(plist-get plist prop)
default)))
-(defun hydra--color (h body-color)
- "Return the color of a Hydra head H with BODY-COLOR."
- (if (null (cadr h))
- 'blue
- (or (hydra--head-property h :color) body-color)))
-
-(defun hydra--face (h body-color)
- "Return the face for a Hydra head H with BODY-COLOR."
- (cl-case (hydra--color h body-color)
+(defun hydra--head-color (h body)
+ "Return the color of a Hydra head H with BODY."
+ (let ((color (hydra--head-property h :color))
+ (exit (or (plist-get (cddr body) :exit)
+ (hydra--head-property h :exit 'default)))
+ (nonheads (plist-get (cddr body) :nonheads)))
+ (cond ((null (cadr h))
+ 'blue)
+ ((eq exit t)
+ 'blue)
+ ((eq nonheads 'run)
+ 'pink)
+ ((eq nonheads 'warn)
+ (if (eq exit t)
+ 'teal
+ 'amaranth))
+ ((null color)
+ (hydra--body-color body))
+ (t
+ color))))
+
+(defun hydra--body-color (body)
+ "Return the color of BODY.
+BODY is the second argument to `defhydra'"
+ (let ((color (plist-get (cddr body) :color))
+ (exit (plist-get (cddr body) :exit))
+ (nonheads (plist-get (cddr body) :nonheads)))
+ (cond ((eq nonheads 'warn)
+ (if exit 'teal 'amaranth))
+ ((eq nonheads 'run) 'pink)
+ (exit 'blue)
+ (color color)
+ (t 'red))))
+
+(defun hydra--face (h body)
+ "Return the face for a Hydra head H with BODY."
+ (cl-case (hydra--head-color h body)
(blue 'hydra-face-blue)
(red 'hydra-face-red)
(amaranth 'hydra-face-amaranth)
+ (pink 'hydra-face-pink)
+ (teal 'hydra-face-teal)
(t (error "Unknown color for %S" h))))
-(defun hydra--hint (docstring heads body-color)
- "Generate a hint from DOCSTRING and HEADS and BODY-COLOR.
-It's intended for the echo area, when a Hydra is active."
+(defun hydra-cleanup ()
+ "Clean up after a Hydra."
+ (when (window-live-p lv-wnd)
+ (let ((buf (window-buffer lv-wnd)))
+ (delete-window lv-wnd)
+ (kill-buffer buf))))
+
+(defun hydra-disable ()
+ "Disable the current Hydra."
+ (cond
+ ;; Emacs 25
+ ((functionp hydra-last)
+ (funcall hydra-last))
+
+ ;; Emacs 24.3 or older
+ ((< emacs-minor-version 4)
+ (setq emulation-mode-map-alists
+ (cl-remove-if
+ (lambda (x)
+ (and (consp x)
+ (consp (car x))
+ (equal (cdar x) hydra-curr-map)))
+ emulation-mode-map-alists)))
+
+ ;; Emacs 24.4.1
+ (t
+ (setq overriding-terminal-local-map nil))))
+
+(defun hydra--unalias-var (str prefix)
+ "Return the symbol named STR if it's bound as a variable.
+Otherwise, add PREFIX to the symbol name."
+ (let ((sym (intern-soft str)))
+ (if (boundp sym)
+ sym
+ (intern (concat prefix "/" str)))))
+
+(defun hydra--hint (name body docstring heads)
+ "Generate a hint for the echo area.
+NAME, BODY, DOCSTRING and HEADS are parameters to `defhydra'."
(let (alist)
(dolist (h heads)
(let ((val (assoc (cadr h) alist))
- (pstr (propertize (car h) 'face
- (hydra--face h body-color))))
+ (pstr (hydra-fontify-head h body)))
(unless (and (> (length h) 2)
(null (cl-caddr h)))
(if val
(cons pstr
(and (stringp (cl-caddr h)) (cl-caddr h))))
alist)))))
-
- (format "%s: %s."
- docstring
- (mapconcat
- (lambda (x)
- (format
- (if (cdr x)
- (concat "[%s]: " (cdr x))
- "%s")
- (car x)))
- (nreverse (mapcar #'cdr alist))
- ", "))))
-
-(defun hydra-disable ()
- "Disable the current Hydra."
- (cond
- ;; Emacs 25
- ((functionp hydra-last)
- (funcall hydra-last))
-
- ;; Emacs 24.4.1
- ((boundp 'overriding-terminal-local-map)
- (setq overriding-terminal-local-map nil))
-
- ;; older
- (t
- (while (and (consp (car emulation-mode-map-alists))
- (consp (caar emulation-mode-map-alists))
- (equal (cl-cdaar emulation-mode-map-alists) ',keymap))
- (setq emulation-mode-map-alists
- (cdr emulation-mode-map-alists))))))
+ (mapconcat
+ (lambda (x)
+ (format
+ (if (cdr x)
+ (concat "[%s]: " (cdr x))
+ "%s")
+ (car x)))
+ (nreverse (mapcar #'cdr alist))
+ ", ")))
+
+(defvar hydra-fontify-head-function nil
+ "Possible replacement for `hydra-fontify-head-default'.")
+
+(defun hydra-fontify-head-default (head body)
+ "Produce a pretty string from HEAD and BODY.
+HEAD's binding is returned as a string with a colored face."
+ (propertize (car head) 'face (hydra--face head body)))
+
+(defun hydra-fontify-head-greyscale (head body)
+ "Produce a pretty string from HEAD and BODY.
+HEAD's binding is returned as a string wrapped with [] or {}."
+ (let ((color (hydra--head-color head body)))
+ (format
+ (if (eq color 'blue)
+ "[%s]"
+ "{%s}") (car head))))
+
+(defun hydra-fontify-head (head body)
+ "Produce a pretty string from HEAD and BODY."
+ (funcall (or hydra-fontify-head-function 'hydra-fontify-head-default)
+ head body))
+
+(defun hydra--format (name body docstring heads)
+ "Generate a `format' statement from STR.
+\"%`...\" expressions are extracted into \"%S\".
+NAME, BODY, DOCSTRING and HEADS are parameters of `defhydra'.
+The expressions can be auto-expanded according to NAME."
+ (setq docstring (replace-regexp-in-string "\\^" "" docstring))
+ (let ((rest (hydra--hint name body docstring heads))
+ (body-color (hydra--body-color body))
+ (prefix (symbol-name name))
+ (start 0)
+ varlist)
+ (while (setq start
+ (string-match
+ "\\(?:%\\( ?-?[0-9]*\\)`\\([a-z-A-Z/0-9]+\\)\\)\\|\\(?:_\\([a-z-~A-Z]+\\)_\\)"
+ docstring start))
+ (if (eq ?_ (aref (match-string 0 docstring) 0))
+ (let* ((key (match-string 3 docstring))
+ (head (assoc key heads)))
+ (if head
+ (progn
+ (push (hydra-fontify-head head body) varlist)
+ (setq docstring (replace-match "% 3s" nil nil docstring)))
+ (error "Unrecognized key: _%s_" key)))
+ (push (hydra--unalias-var (match-string 2 docstring) prefix) varlist)
+ (setq docstring (replace-match (concat "%" (match-string 1 docstring) "S") nil nil docstring 0))))
+ (if (eq ?\n (aref docstring 0))
+ `(concat (format ,docstring ,@(nreverse varlist))
+ ,rest)
+ `(format ,(concat docstring ": " rest ".")))))
+
+(defun hydra--message (name body docstring heads)
+ "Generate code to display the hint in the preferred echo area.
+Set `hydra-lv' to choose the echo area.
+NAME, BODY, DOCSTRING, and HEADS are parameters of `defhydra'."
+ (let ((format-expr (hydra--format name body docstring heads)))
+ `(if hydra-lv
+ (lv-message ,format-expr)
+ (message ,format-expr))))
(defun hydra--doc (body-key body-name heads)
"Generate a part of Hydra docstring.
(interactive)
,@(when body-pre (list body-pre))
(hydra-disable)
+ ,@(when (memq color '(blue teal)) '((hydra-cleanup)))
(catch 'hydra-disable
,@(delq nil
- (if (eq color 'blue)
+ (if (memq color '(blue teal))
`(,(when cmd `(call-interactively #',cmd))
,body-post)
`(,(when cmd
`(condition-case err
(prog1 t
(call-interactively #',cmd))
- (error
+ ((quit error)
(message "%S" err)
- (sit-for 0.8)
+ (unless hydra-lv
+ (sit-for 0.8))
nil)))
(when hydra-is-helpful
- (message ,hint))
+ (,hint))
(setq hydra-last
(hydra-set-transient-map
(setq hydra-curr-map ',keymap)
t
- ,@(if (and (not (eq body-color 'amaranth)) body-post)
- `((lambda () ,body-post)))))
+ ,(if (and (not (memq body-color '(amaranth pink teal))) body-post)
+ `(lambda () (hydra-cleanup) ,body-post)
+ `(lambda () (hydra-cleanup)))))
,other-post))))))
-;;* Macros
-;;** hydra-create
-;;;###autoload
-(defmacro hydra-create (body heads &optional method)
- "Create a hydra with a BODY prefix and HEADS with METHOD.
-This will result in `global-set-key' statements with the keys
-being the concatenation of BODY and each head in HEADS. HEADS is
-an list of (KEY FUNCTION &optional HINT).
-
-After one of the HEADS is called via BODY+KEY, it and the other
-HEADS can be called with only KEY (no need for BODY). This state
-is broken once any key binding that is not in HEADS is called.
-
-METHOD is a lambda takes two arguments: a KEY and a COMMAND.
-It defaults to `global-set-key'.
-When `(keymapp METHOD)`, it becomes:
-
- (lambda (key command) (define-key METHOD key command))"
- (declare (indent 1)
- (obsolete defhydra "0.8.0"))
- `(defhydra ,(intern
- (concat
- "hydra-" (replace-regexp-in-string " " "_" body)))
- ,(cond ((hydra--callablep method)
- method)
- ((null method)
- `(global-map ,body))
- (t
- (list method body)))
- "hydra"
- ,@(eval heads)))
+(defun hydra-pink-fallback ()
+ "On intercepting a non-head, try to run it."
+ (let ((keys (this-command-keys))
+ kb)
+ (when (equal keys [backspace])
+ (setq keys "\7f"))
+ (setq kb (key-binding keys))
+ (if kb
+ (if (commandp kb)
+ (condition-case err
+ (call-interactively kb)
+ ((quit error)
+ (message "%S" err)
+ (unless hydra-lv
+ (sit-for 0.8))))
+ (message "Pink Hydra can't currently handle prefixes, continuing"))
+ (message "Pink Hydra could not resolve: %S" keys))))
+
+(defun hydra--handle-nonhead (keymap name body heads)
+ "Setup KEYMAP for intercepting non-head bindings.
+NAME, BODY and HEADS are parameters to `defhydra'."
+ (let ((body-color (hydra--body-color body))
+ (body-post (plist-get (cddr body) :post)))
+ (when (and body-post (symbolp body-post))
+ (setq body-post `(funcall #',body-post)))
+ (when (memq body-color '(amaranth pink teal))
+ (if (cl-some `(lambda (h)
+ (eq (hydra--head-color h body) 'blue))
+ heads)
+ (progn
+ (define-key keymap [t]
+ `(lambda ()
+ (interactive)
+ ,(cond
+ ((eq body-color 'amaranth)
+ '(message "An amaranth Hydra can only exit through a blue head"))
+ ((eq body-color 'teal)
+ '(message "A teal Hydra can only exit through a blue head"))
+ (t
+ '(hydra-pink-fallback)))
+ (hydra-set-transient-map hydra-curr-map t)
+ (when hydra-is-helpful
+ (unless hydra-lv
+ (sit-for 0.8))
+ (,(intern (format "%S/hint" name)))))))
+ (error
+ "An %S Hydra must have at least one blue head in order to exit"
+ body-color))
+ (when hydra-keyboard-quit
+ (define-key keymap hydra-keyboard-quit
+ `(lambda ()
+ (interactive)
+ (hydra-disable)
+ (hydra-cleanup)
+ ,body-post))))))
+;;* Macros
;;** defhydra
;;;###autoload
(defmacro defhydra (name body &optional docstring &rest heads)
BODY-MAP is a keymap; `global-map' is used quite often. Each
function generated from HEADS will be bound in BODY-MAP to
-BODY-KEY + KEY, and will set the transient map so that all
-following heads can be called though KEY only.
+BODY-KEY + KEY (both are strings passed to `kbd'), and will set
+the transient map so that all following heads can be called
+though KEY only.
+
+CMD is a callable expression: either an interactive function
+name, or an interactive lambda, or a single sexp (it will be
+wrapped in an interactive lambda).
+
+HINT is a short string that identifies its head. It will be
+printed beside KEY in the echo erea if `hydra-is-helpful' is not
+nil. If you don't even want the KEY to be printed, set HINT
+explicitly to nil.
The heads inherit their PLIST from the body and are allowed to
override each key. The keys recognized are :color and :bind.
:bind can be:
- nil: this head will not be bound in BODY-MAP.
-- a lambda taking KEY and CMD used to bind a head"
+- a lambda taking KEY and CMD used to bind a head
+
+It is possible to omit both BODY-MAP and BODY-KEY if you don't
+want to bind anything. In that case, typically you will bind the
+generated NAME/body command. This command is also the return
+result of `defhydra'."
(declare (indent defun))
(unless (stringp docstring)
(setq heads (cons docstring heads))
(concat "lambda-" (car x)))))))
heads))
(body-name (intern (format "%S/body" name)))
+ (hint-name (intern (format "%S/hint" name)))
(body-key (unless (hydra--callablep body)
(cadr body)))
- (body-color (if (hydra--callablep body)
- 'red
- (or (plist-get (cddr body) :color)
- 'red)))
+ (body-color (hydra--body-color body))
(body-pre (plist-get (cddr body) :pre))
(body-post (plist-get (cddr body) :post))
(method (or (plist-get body :bind)
(car body)))
- (hint (hydra--hint docstring heads body-color))
(doc (hydra--doc body-key body-name heads)))
- (when (and (or body-pre body-post)
- (version< emacs-version "24.4"))
- (error "At least Emacs 24.4 is needed for :pre and :post"))
(when (and body-pre (symbolp body-pre))
(setq body-pre `(funcall #',body-pre)))
(when (and body-post (symbolp body-post))
(setq body-post `(funcall #',body-post)))
- (when (eq body-color 'amaranth)
- (if (cl-some `(lambda (h)
- (eq (hydra--color h ',body-color) 'blue))
- heads)
- (progn
- (when (cl-some `(lambda (h)
- (eq (hydra--color h ',body-color) 'red))
- heads)
- (warn "Amaranth body color: upgrading all red heads to amaranth"))
- (define-key keymap [t]
- `(lambda ()
- (interactive)
- (message "An amaranth Hydra can only exit through a blue head")
- (hydra-set-transient-map hydra-curr-map t)
- (when hydra-is-helpful
- (sit-for 0.8)
- (message ,hint)))))
- (error "An amaranth Hydra must have at least one blue head in order to exit"))
- (when hydra-keyboard-quit
- (define-key keymap hydra-keyboard-quit
- `(lambda ()
- (interactive)
- (hydra-disable)
- ,body-post))))
+ (hydra--handle-nonhead keymap name body heads)
`(progn
,@(cl-mapcar
(lambda (head name)
(hydra--make-defun
- name (hydra--make-callable (cadr head)) (hydra--color head body-color)
+ name (hydra--make-callable
+ (cadr head)) (hydra--head-color head body)
(format "%s\n\nCall the head: `%S'." doc (cadr head))
- hint keymap
+ hint-name keymap
body-color body-pre body-post))
heads names)
,@(unless (or (null body-key)
(lambda (head name)
(when (or body-key method)
(let ((bind (hydra--head-property head :bind 'default))
- (final-key (if body-key
- (vconcat (kbd body-key) (kbd (car head)))
- (kbd (car head)))))
+ (final-key
+ (if body-key
+ (vconcat (kbd body-key) (kbd (car head)))
+ (kbd (car head)))))
(cond ((null bind) nil)
((eq bind 'default)
(t
(error "Invalid :bind property %S" head))))))
heads names))
- ,(hydra--make-defun body-name nil nil doc hint keymap
+ (defun ,hint-name ()
+ ,(hydra--message name body docstring heads))
+ ,(hydra--make-defun body-name nil nil doc hint-name keymap
body-color body-pre body-post
'(setq prefix-arg current-prefix-arg)))))
+(defmacro defhydradio (name body &rest heads)
+ "Create radios with prefix NAME.
+BODY specifies the options; there are none currently.
+HEADS have the format:
+
+ (TOGGLE-NAME &optional VALUE DOC)
+
+TOGGLE-NAME will be used along with NAME to generate a variable
+name and a function that cycles it with the same name. VALUE
+should be an array. The first element of VALUE will be used to
+inialize the variable.
+VALUE defaults to [nil t].
+DOC defaults to TOGGLE-NAME split and capitalized."
+ (declare (indent defun))
+ `(progn
+ ,@(apply #'append
+ (mapcar (lambda (h)
+ (hydra--radio name h))
+ heads))
+ (defun ,(intern (format "%S/reset-radios" name)) ()
+ ,@(mapcar
+ (lambda (h)
+ (let ((full-name (intern (format "%S/%S" name (car h))))
+ )
+ `(setq ,full-name ,(hydra--quote-maybe
+ (and (cadr h) (aref (cadr h) 0))))))
+ heads))))
+
+(defun hydra--radio (parent head)
+ "Generate a hydradio with PARENT from HEAD."
+ (let* ((name (car head))
+ (full-name (intern (format "%S/%S" parent name)))
+ (val (or (cadr head) [nil t]))
+ (doc (or (cl-caddr head)
+ (mapconcat #'capitalize
+ (split-string (symbol-name name) "-")
+ " "))))
+ `((defvar ,full-name ,(hydra--quote-maybe (aref val 0)) ,doc)
+ (put ',full-name 'range ,val)
+ (defun ,full-name ()
+ (hydra--cycle-radio ',full-name)))))
+
+(defun hydra--quote-maybe (x)
+ "Quote X if it's a symbol."
+ (cond ((null x)
+ nil)
+ ((symbolp x)
+ (list 'quote x))
+ (t
+ x)))
+
+(defun hydra--cycle-radio (sym)
+ "Set SYM to the next value in its range."
+ (let* ((val (symbol-value sym))
+ (range (get sym 'range))
+ (i 0)
+ (l (length range)))
+ (setq i (catch 'done
+ (while (< i l)
+ (if (equal (aref range i) val)
+ (throw 'done (1+ i))
+ (incf i)))
+ (error "Val not in range for %S" sym)))
+ (set sym
+ (aref range
+ (if (>= i l)
+ 0
+ i)))))
+
(provide 'hydra)
;;; Local Variables: