|
|
- ;;; hydra.el --- Make bindings that stick around. -*- lexical-binding: t -*-
-
- ;; Copyright (C) 2015-2019 Free Software Foundation, Inc.
-
- ;; Author: Oleh Krehel <ohwoeowho@gmail.com>
- ;; Maintainer: Oleh Krehel <ohwoeowho@gmail.com>
- ;; URL: https://github.com/abo-abo/hydra
- ;; Version: 0.15.0
- ;; Keywords: bindings
- ;; Package-Requires: ((cl-lib "0.5") (lv "0"))
-
- ;; This file is part of GNU Emacs.
-
- ;; GNU Emacs is free software: you can redistribute it and/or modify
- ;; it under the terms of the GNU General Public License as published by
- ;; the Free Software Foundation, either version 3 of the License, or
- ;; (at your option) any later version.
-
- ;; GNU Emacs is distributed in the hope that it will be useful,
- ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;; GNU General Public License for more details.
-
- ;; You should have received a copy of the GNU General Public License
- ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
-
- ;;; Commentary:
- ;;
- ;; This package can be used to tie related commands into a family of
- ;; short bindings with a common prefix - a Hydra.
- ;;
- ;; Once you summon the Hydra (through the prefixed binding), all the
- ;; heads can be called in succession with only a short extension.
- ;; The Hydra is vanquished once Hercules, any binding that isn't the
- ;; Hydra's head, arrives. Note that Hercules, besides vanquishing the
- ;; Hydra, will still serve his original purpose, calling his proper
- ;; command. This makes the Hydra very seamless, it's like a minor
- ;; mode that disables itself automagically.
- ;;
- ;; Here's an example Hydra, bound in the global map (you can use any
- ;; keymap in place of `global-map'):
- ;;
- ;; (defhydra hydra-zoom (global-map "<f2>")
- ;; "zoom"
- ;; ("g" text-scale-increase "in")
- ;; ("l" text-scale-decrease "out"))
- ;;
- ;; It allows to start a command chain either like this:
- ;; "<f2> gg4ll5g", or "<f2> lgllg".
- ;;
- ;; Here's another approach, when you just want a "callable keymap":
- ;;
- ;; (defhydra hydra-toggle (:color blue)
- ;; "toggle"
- ;; ("a" abbrev-mode "abbrev")
- ;; ("d" toggle-debug-on-error "debug")
- ;; ("f" auto-fill-mode "fill")
- ;; ("t" toggle-truncate-lines "truncate")
- ;; ("w" whitespace-mode "whitespace")
- ;; ("q" nil "cancel"))
- ;;
- ;; This binds nothing so far, but if you follow up with:
- ;;
- ;; (global-set-key (kbd "C-c C-v") 'hydra-toggle/body)
- ;;
- ;; you will have bound "C-c C-v a", "C-c C-v d" etc.
- ;;
- ;; Knowing that `defhydra' defines e.g. `hydra-toggle/body' command,
- ;; you can nest Hydras if you wish, with `hydra-toggle/body' possibly
- ;; becoming a blue head of another Hydra.
- ;;
- ;; If you want to learn all intricacies of using `defhydra' without
- ;; having to figure it all out from this source code, check out the
- ;; wiki: https://github.com/abo-abo/hydra/wiki. There's a wealth of
- ;; information there. Everyone is welcome to bring the existing pages
- ;; up to date and add new ones.
- ;;
- ;; Additionally, the file hydra-examples.el serves to demo most of the
- ;; functionality.
-
- ;;; Code:
- ;;* Requires
- (require 'cl-lib)
- (require 'lv)
- (require 'ring)
-
- (defvar hydra-curr-map nil
- "The keymap of the current Hydra called.")
-
- (defvar hydra-curr-on-exit nil
- "The on-exit predicate for the current Hydra.")
-
- (defvar hydra-curr-foreign-keys nil
- "The current :foreign-keys behavior.")
-
- (defvar hydra-curr-body-fn nil
- "The current hydra-.../body function.")
-
- (defvar hydra-deactivate nil
- "If a Hydra head sets this to t, exit the Hydra.
- This will be done even if the head wasn't designated for exiting.")
-
- (defvar hydra-amaranth-warn-message "An amaranth Hydra can only exit through a blue head"
- "Amaranth Warning message. Shown when the user tries to press an unbound/non-exit key while in an amaranth head.")
-
- (defun hydra-set-transient-map (keymap on-exit &optional foreign-keys)
- "Set KEYMAP to the highest priority.
-
- Call ON-EXIT when the KEYMAP is deactivated.
-
- FOREIGN-KEYS determines the deactivation behavior, when a command
- that isn't in KEYMAP is called:
-
- nil: deactivate KEYMAP and run the command.
- run: keep KEYMAP and run the command.
- warn: keep KEYMAP and issue a warning instead of running the command."
- (if hydra-deactivate
- (hydra-keyboard-quit)
- (setq hydra-curr-map keymap)
- (setq hydra-curr-on-exit on-exit)
- (setq hydra-curr-foreign-keys foreign-keys)
- (add-hook 'pre-command-hook 'hydra--clearfun)
- (internal-push-keymap keymap 'overriding-terminal-local-map)))
-
- (defun hydra--clearfun ()
- "Disable the current Hydra unless `this-command' is a head."
- (unless (eq this-command 'hydra-pause-resume)
- (when (or
- (memq this-command '(handle-switch-frame
- keyboard-quit))
- (null overriding-terminal-local-map)
- (not (or (eq this-command
- (lookup-key hydra-curr-map (this-single-command-keys)))
- (cl-case hydra-curr-foreign-keys
- (warn
- (setq this-command 'hydra-amaranth-warn))
- (run
- t)
- (t nil)))))
- (hydra-disable))))
-
- (defvar hydra--ignore nil
- "When non-nil, don't call `hydra-curr-on-exit'.")
-
- (defvar hydra--input-method-function nil
- "Store overridden `input-method-function' here.")
-
- (defun hydra-disable ()
- "Disable the current Hydra."
- (setq hydra-deactivate nil)
- (remove-hook 'pre-command-hook 'hydra--clearfun)
- (unless hydra--ignore
- (if (fboundp 'remove-function)
- (remove-function input-method-function #'hydra--imf)
- (when hydra--input-method-function
- (setq input-method-function hydra--input-method-function)
- (setq hydra--input-method-function nil))))
- (dolist (frame (frame-list))
- (with-selected-frame frame
- (when overriding-terminal-local-map
- (internal-pop-keymap hydra-curr-map 'overriding-terminal-local-map))))
- (unless hydra--ignore
- (when hydra-curr-on-exit
- (let ((on-exit hydra-curr-on-exit))
- (setq hydra-curr-on-exit nil)
- (funcall on-exit)))))
-
- (unless (fboundp 'internal-push-keymap)
- (defun internal-push-keymap (keymap symbol)
- (let ((map (symbol-value symbol)))
- (unless (memq keymap map)
- (unless (memq 'add-keymap-witness (symbol-value symbol))
- (setq map (make-composed-keymap nil (symbol-value symbol)))
- (push 'add-keymap-witness (cdr map))
- (set symbol map))
- (push keymap (cdr map))))))
-
- (unless (fboundp 'internal-pop-keymap)
- (defun internal-pop-keymap (keymap symbol)
- (let ((map (symbol-value symbol)))
- (when (memq keymap map)
- (setf (cdr map) (delq keymap (cdr map))))
- (let ((tail (cddr map)))
- (and (or (null tail) (keymapp tail))
- (eq 'add-keymap-witness (nth 1 map))
- (set symbol tail))))))
-
- (defun hydra-amaranth-warn ()
- "Issue a warning that the current input was ignored."
- (interactive)
- (message hydra-amaranth-warn-message))
-
- ;;* Customize
- (defgroup hydra nil
- "Make bindings that stick around."
- :group 'bindings
- :prefix "hydra-")
-
- (defcustom hydra-is-helpful t
- "When t, display a hint with possible bindings in the echo area."
- :type 'boolean
- :group 'hydra)
-
- (defcustom hydra-default-hint ""
- "Default :hint property to use for heads when not specified in
- the body or the head."
- :type 'sexp
- :group 'hydra)
-
- (declare-function posframe-show "posframe")
- (declare-function posframe-hide "posframe")
- (declare-function posframe-poshandler-window-center "posframe")
-
- (defvar hydra-posframe-show-params
- '(:internal-border-width 1
- :internal-border-color "red"
- :poshandler posframe-poshandler-window-center)
- "List of parameters passed to `posframe-show'.")
-
- (defvar hydra--posframe-timer nil
- "Timer for hiding posframe hint.")
-
- (defun hydra-posframe-show (str)
- (require 'posframe)
- (when hydra--posframe-timer
- (cancel-timer hydra--posframe-timer))
- (setq hydra--posframe-timer nil)
- (apply #'posframe-show
- " *hydra-posframe*"
- :string str
- hydra-posframe-show-params))
-
- (defun hydra-posframe-hide ()
- (require 'posframe)
- (unless hydra--posframe-timer
- (setq hydra--posframe-timer
- (run-with-idle-timer
- 0 nil (lambda ()
- (setq hydra--posframe-timer nil)
- (posframe-hide " *hydra-posframe*"))))))
-
- (defvar hydra-hint-display-alist
- (list (list 'lv #'lv-message #'lv-delete-window)
- (list 'message #'message (lambda () (message "")))
- (list 'posframe #'hydra-posframe-show #'hydra-posframe-hide))
- "Store the functions for `hydra-hint-display-type'.")
-
- (defcustom hydra-hint-display-type 'lv
- "The utility to show hydra hint"
- :type '(choice
- (const message)
- (const lv)
- (const posframe))
- :group 'hydra)
-
- (defcustom hydra-verbose nil
- "When non-nil, hydra will issue some non essential style warnings."
- :type 'boolean)
-
- (defcustom hydra-key-format-spec "%s"
- "Default `format'-style specifier for _a_ syntax in docstrings.
- When nil, you can specify your own at each location like this: _ 5a_."
- :type 'string)
-
- (defcustom hydra-doc-format-spec "%s"
- "Default `format'-style specifier for ?a? syntax in docstrings."
- :type 'string)
-
- (defcustom hydra-look-for-remap nil
- "When non-nil, hydra binding behaves as keymap binding with [remap].
- When calling a head with a simple command, hydra will lookup for a potential
- remap command according to the current active keymap and call it instead if
- found"
- :type 'boolean)
-
- (make-obsolete-variable
- 'hydra-key-format-spec
- "Since the docstrings are aligned by hand anyway, this isn't very useful."
- "0.13.1")
-
- (defface hydra-face-red
- '((t (:foreground "#FF0000" :bold t)))
- "Red Hydra heads don't exit the Hydra.
- Every other command exits the Hydra."
- :group 'hydra)
-
- (defface hydra-face-blue
- '((((class color) (background light))
- :foreground "#0000FF" :bold t)
- (((class color) (background dark))
- :foreground "#8ac6f2" :bold t))
- "Blue Hydra heads exit the Hydra.
- Every other command exits as well.")
-
- (defface hydra-face-amaranth
- '((t (:foreground "#E52B50" :bold t)))
- "Amaranth body has red heads and warns on intercepting non-heads.
- Exitable only through a blue head.")
-
- (defface hydra-face-pink
- '((t (:foreground "#FF6EB4" :bold t)))
- "Pink body has red heads and runs intercepted non-heads.
- Exitable only through a blue head.")
-
- (defface hydra-face-teal
- '((t (:foreground "#367588" :bold t)))
- "Teal body has blue heads and warns on intercepting non-heads.
- Exitable 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)))))
-
- ;;* Find Function
- (eval-after-load 'find-func
- '(defadvice find-function-search-for-symbol
- (around hydra-around-find-function-search-for-symbol-advice
- (symbol type library) activate)
- "Navigate to hydras with `find-function-search-for-symbol'."
- (prog1 ad-do-it
- (when (symbolp symbol)
- ;; The original function returns (cons (current-buffer) (point))
- ;; if it found the point.
- (unless (cdr ad-return-value)
- (with-current-buffer (find-file-noselect library)
- (let ((sn (symbol-name symbol)))
- (when (and (null type)
- (string-match "\\`\\(hydra-[a-z-A-Z0-9]+\\)/\\(.*\\)\\'" sn)
- (re-search-forward (concat "(defhydra " (match-string 1 sn))
- nil t))
- (goto-char (match-beginning 0)))
- (cons (current-buffer) (point)))))))))
-
- ;;* Universal Argument
- (defvar hydra-base-map
- (let ((map (make-sparse-keymap)))
- (define-key map [?\C-u] 'hydra--universal-argument)
- (define-key map [?-] 'hydra--negative-argument)
- (define-key map [?0] 'hydra--digit-argument)
- (define-key map [?1] 'hydra--digit-argument)
- (define-key map [?2] 'hydra--digit-argument)
- (define-key map [?3] 'hydra--digit-argument)
- (define-key map [?4] 'hydra--digit-argument)
- (define-key map [?5] 'hydra--digit-argument)
- (define-key map [?6] 'hydra--digit-argument)
- (define-key map [?7] 'hydra--digit-argument)
- (define-key map [?8] 'hydra--digit-argument)
- (define-key map [?9] 'hydra--digit-argument)
- (define-key map [kp-0] 'hydra--digit-argument)
- (define-key map [kp-1] 'hydra--digit-argument)
- (define-key map [kp-2] 'hydra--digit-argument)
- (define-key map [kp-3] 'hydra--digit-argument)
- (define-key map [kp-4] 'hydra--digit-argument)
- (define-key map [kp-5] 'hydra--digit-argument)
- (define-key map [kp-6] 'hydra--digit-argument)
- (define-key map [kp-7] 'hydra--digit-argument)
- (define-key map [kp-8] 'hydra--digit-argument)
- (define-key map [kp-9] 'hydra--digit-argument)
- (define-key map [kp-subtract] 'hydra--negative-argument)
- map)
- "Keymap that all Hydras inherit. See `universal-argument-map'.")
-
- (defun hydra--universal-argument (arg)
- "Forward to (`universal-argument' ARG)."
- (interactive "P")
- (setq prefix-arg (if (consp arg)
- (list (* 4 (car arg)))
- (if (eq arg '-)
- (list -4)
- '(4)))))
-
- (defun hydra--digit-argument (arg)
- "Forward to (`digit-argument' ARG)."
- (interactive "P")
- (let* ((char (if (integerp last-command-event)
- last-command-event
- (get last-command-event 'ascii-character)))
- (digit (- (logand char ?\177) ?0)))
- (setq prefix-arg (cond ((integerp arg)
- (+ (* arg 10)
- (if (< arg 0)
- (- digit)
- digit)))
- ((eq arg '-)
- (if (zerop digit)
- '-
- (- digit)))
- (t
- digit)))))
-
- (defun hydra--negative-argument (arg)
- "Forward to (`negative-argument' ARG)."
- (interactive "P")
- (setq prefix-arg (cond ((integerp arg) (- arg))
- ((eq arg '-) nil)
- (t '-))))
-
- ;;* Repeat
- (defvar hydra-repeat--prefix-arg nil
- "Prefix arg to use with `hydra-repeat'.")
-
- (defvar hydra-repeat--command nil
- "Command to use with `hydra-repeat'.")
-
- (defun hydra-repeat (&optional arg)
- "Repeat last command with last prefix arg.
- When ARG is non-nil, use that instead."
- (interactive "p")
- (if (eq arg 1)
- (unless (string-match "hydra-repeat$" (symbol-name last-command))
- (setq hydra-repeat--command last-command)
- (setq hydra-repeat--prefix-arg last-prefix-arg))
- (setq hydra-repeat--prefix-arg arg))
- (setq current-prefix-arg hydra-repeat--prefix-arg)
- (funcall hydra-repeat--command))
-
- ;;* Misc internals
- (defun hydra--callablep (x)
- "Test if X is callable."
- (or (functionp x)
- (and (consp x)
- (memq (car x) '(function quote)))))
-
- (defun hydra--make-callable (x)
- "Generate a callable symbol from X.
- If X is a function symbol or a lambda, return it. Otherwise, it
- should be a single statement. Wrap it in an interactive lambda."
- (cond ((or (symbolp x) (functionp x))
- x)
- ((and (consp x) (eq (car x) 'function))
- (cadr x))
- (t
- `(lambda ()
- (interactive)
- ,x))))
-
- (defun hydra-plist-get-default (plist prop default)
- "Extract a value from a property list.
- PLIST is a property list, which is a list of the form
- \(PROP1 VALUE1 PROP2 VALUE2...).
-
- Return the value corresponding to PROP, or DEFAULT if PROP is not
- one of the properties on the list."
- (if (memq prop plist)
- (plist-get plist prop)
- default))
-
- (defun hydra--head-property (h prop &optional default)
- "Return for Hydra head H the value of property PROP.
- Return DEFAULT if PROP is not in H."
- (hydra-plist-get-default (cl-cdddr h) prop default))
-
- (defun hydra--head-set-property (h prop value)
- "In hydra Head H, set a property PROP to the value VALUE."
- (cons (car h) (plist-put (cdr h) prop value)))
-
- (defun hydra--head-has-property (h prop)
- "Return non nil if heads H has the property PROP."
- (plist-member (cdr h) prop))
-
- (defun hydra--body-foreign-keys (body)
- "Return what BODY does with a non-head binding."
- (or
- (plist-get (cddr body) :foreign-keys)
- (let ((color (plist-get (cddr body) :color)))
- (cl-case color
- ((amaranth teal) 'warn)
- (pink 'run)))))
-
- (defun hydra--body-exit (body)
- "Return the exit behavior of BODY."
- (or
- (plist-get (cddr body) :exit)
- (let ((color (plist-get (cddr body) :color)))
- (cl-case color
- ((blue teal) t)
- (t nil)))))
-
- (defun hydra--normalize-body (body)
- "Put BODY in a normalized format.
- Add :exit and :foreign-keys if they are not there.
- Remove :color key. And sort the plist alphabetically."
- (let ((plist (cddr body)))
- (plist-put plist :exit (hydra--body-exit body))
- (plist-put plist :foreign-keys (hydra--body-foreign-keys body))
- (let* ((alist0 (cl-loop for (k v) on plist
- by #'cddr collect (cons k v)))
- (alist1 (assq-delete-all :color alist0))
- (alist2 (cl-sort alist1 #'string<
- :key (lambda (x) (symbol-name (car x))))))
- (append (list (car body) (cadr body))
- (cl-mapcan (lambda (x) (list (car x) (cdr x))) alist2)))))
-
- (defalias 'hydra--imf #'list)
-
- (defun hydra-default-pre ()
- "Default setup that happens in each head before :pre."
- (when (eq input-method-function 'key-chord-input-method)
- (if (fboundp 'add-function)
- (add-function :override input-method-function #'hydra--imf)
- (unless hydra--input-method-function
- (setq hydra--input-method-function input-method-function)
- (setq input-method-function nil)))))
-
- (defvar hydra-timeout-timer (timer-create)
- "Timer for `hydra-timeout'.")
-
- (defvar hydra-message-timer (timer-create)
- "Timer for the hint.")
-
- (defvar hydra--work-around-dedicated t
- "When non-nil, assume there's no bug in `pop-to-buffer'.
- `pop-to-buffer' should not select a dedicated window.")
-
- (defun hydra-keyboard-quit ()
- "Quitting function similar to `keyboard-quit'."
- (interactive)
- (hydra-disable)
- (cancel-timer hydra-timeout-timer)
- (cancel-timer hydra-message-timer)
- (setq hydra-curr-map nil)
- (unless (and hydra--ignore
- (null hydra--work-around-dedicated))
- (funcall
- (nth 2 (assoc hydra-hint-display-type hydra-hint-display-alist))))
- nil)
-
- (defvar hydra-head-format "[%s]: "
- "The formatter for each head of a plain docstring.")
-
- (defvar hydra-key-doc-function 'hydra-key-doc-function-default
- "The function for formatting key-doc pairs.")
-
- (defun hydra-key-doc-function-default (key key-width doc doc-width)
- (cond
- ((equal key " ") (format (format "%%-%ds" (+ 3 key-width doc-width)) doc))
- ((listp doc)
- `(format ,(format "%%%ds: %%%ds" key-width (- -1 doc-width)) ,key ,doc))
- (t (format (format "%%%ds: %%%ds" key-width (- -1 doc-width)) key doc))))
-
- (defun hydra--to-string (x)
- (if (stringp x)
- x
- (eval x)))
-
- (defun hydra--eval-and-format (x)
- (let ((str (hydra--to-string (cdr x))))
- (format
- (if (> (length str) 0)
- (concat hydra-head-format str)
- "%s")
- (car x))))
-
- (defun hydra--hint-heads-wocol (body heads)
- "Generate a hint for the echo area.
- BODY, and HEADS are parameters to `defhydra'.
- Works for heads without a property :column."
- (let (alist)
- (dolist (h heads)
- (let ((val (assoc (cadr h) alist))
- (pstr (hydra-fontify-head h body)))
- (if val
- (setf (cadr val)
- (concat (cadr val) " " pstr))
- (push
- (cons (cadr h)
- (cons pstr (cl-caddr h)))
- alist))))
- (let ((keys (nreverse (mapcar #'cdr alist)))
- (n-cols (plist-get (cddr body) :columns))
- res)
- (setq res
- (if n-cols
- (let ((n-rows (1+ (/ (length keys) n-cols)))
- (max-key-len (apply #'max (mapcar (lambda (x) (length (car x))) keys)))
- (max-doc-len (apply #'max (mapcar (lambda (x)
- (length (hydra--to-string (cdr x)))) keys))))
- `(concat
- "\n"
- (mapconcat #'identity
- (mapcar
- (lambda (x)
- (mapconcat
- (lambda (y)
- (and y
- (funcall hydra-key-doc-function
- (car y)
- ,max-key-len
- (hydra--to-string (cdr y))
- ,max-doc-len))) x ""))
- ',(hydra--matrix keys n-cols n-rows))
- "\n")))
-
-
- `(concat
- (mapconcat
- #'hydra--eval-and-format
- ',keys
- ", ")
- ,(if keys "." ""))))
- (if (cl-every #'stringp
- (mapcar 'cddr alist))
- (eval res)
- res))))
-
- (defun hydra--hint (body heads)
- "Generate a hint for the echo area.
- BODY, and HEADS are parameters to `defhydra'."
- (let* ((sorted-heads (hydra--sort-heads (hydra--normalize-heads heads)))
- (heads-w-col (cl-remove-if-not (lambda (heads) (hydra--head-property (nth 0 heads) :column)) sorted-heads))
- (heads-wo-col (cl-remove-if (lambda (heads) (hydra--head-property (nth 0 heads) :column)) sorted-heads))
- (hint-w-col (when heads-w-col
- (hydra--hint-from-matrix body (hydra--generate-matrix heads-w-col))))
- (hint-wo-col (when heads-wo-col
- (hydra--hint-heads-wocol body (car heads-wo-col)))))
- (if (null hint-w-col)
- hint-wo-col
- (if (stringp hint-wo-col)
- `(concat ,@hint-w-col ,hint-wo-col)
- `(concat ,@hint-w-col ,@(cdr hint-wo-col))))))
-
- (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."
- (let* ((foreign-keys (hydra--body-foreign-keys body))
- (head-exit (hydra--head-property head :exit))
- (head-color
- (if head-exit
- (if (eq foreign-keys 'warn)
- 'teal
- 'blue)
- (cl-case foreign-keys
- (warn 'amaranth)
- (run 'pink)
- (t 'red)))))
- (when (and (null (cadr head))
- (not head-exit))
- (hydra--complain "nil cmd can only be blue"))
- (propertize
- (replace-regexp-in-string "%" "%%" (car head))
- 'face
- (or (hydra--head-property head :face)
- (cl-case head-color
- (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" head)))))))
-
- (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 {}."
- (format
- (if (hydra--head-property head :exit)
- "[%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--strip-align-markers (str)
- "Remove ^ from STR, unless they're escaped: \\^."
- (let ((start 0))
- (while (setq start (string-match "\\\\?\\^" str start))
- (if (eq (- (match-end 0) (match-beginning 0)) 2)
- (progn
- (setq str (replace-match "^" nil nil str))
- (cl-incf start))
- (setq str (replace-match "" nil nil str))))
- str))
-
- (defvar hydra-docstring-keys-translate-alist
- '(("↑" . "<up>")
- ("↓" . "<down>")
- ("→" . "<right>")
- ("←" . "<left>")
- ("⌫" . "DEL")
- ("⌦" . "<deletechar>")
- ("⏎" . "RET")))
-
- (defconst hydra-width-spec-regex " ?-?[0-9]*?"
- "Regex for the width spec in keys and %` quoted sexps.")
-
- (defvar hydra-key-regex "\\[\\|]\\|[-\\[:alnum:] ~.,;:/|?<>={}*+#%@!&^↑↓←→⌫⌦⏎'`()\"$]+?"
- "Regex for the key quoted in the docstring.")
-
- (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."
- (unless (memq 'elisp--witness--lisp (mapcar #'cadr heads))
- (setq docstring (hydra--strip-align-markers docstring))
- (setq docstring (replace-regexp-in-string "___" "_β_" docstring))
- (let ((rest (if (eq (plist-get (cddr body) :hint) 'none)
- ""
- (hydra--hint body heads)))
- (start 0)
- (inner-regex (format "\\(%s\\)\\(%s\\)" hydra-width-spec-regex hydra-key-regex))
- varlist
- offset)
- (while (setq start
- (string-match
- (format
- "\\(?:%%\\( ?-?[0-9]*s?\\)\\(`[a-z-A-Z/0-9]+\\|(\\)\\)\\|\\(?:_%s_\\)\\|\\(?:[?]%s[?]\\)"
- inner-regex
- inner-regex)
- docstring start))
- (cond ((eq ?? (aref (match-string 0 docstring) 0))
- (let* ((key (match-string 6 docstring))
- (head (assoc key heads)))
- (if head
- (progn
- (push (nth 2 head) varlist)
- (setq docstring
- (replace-match
- (or
- hydra-doc-format-spec
- (concat "%" (match-string 3 docstring) "s"))
- t nil docstring)))
- (setq start (match-end 0))
- (warn "Unrecognized key: ?%s?" key))))
- ((eq ?_ (aref (match-string 0 docstring) 0))
- (let* ((key (match-string 4 docstring))
- (key (if (equal key "β") "_" key))
- normal-key
- (head (or (assoc key heads)
- (when (setq normal-key
- (cdr (assoc
- key hydra-docstring-keys-translate-alist)))
- (assoc normal-key heads)))))
- (if head
- (progn
- (push (hydra-fontify-head (if normal-key
- (cons key (cdr head))
- head)
- body)
- varlist)
- (let ((replacement
- (or
- hydra-key-format-spec
- (concat "%" (match-string 3 docstring) "s"))))
- (setq docstring
- (replace-match replacement t nil docstring))
- (setq start (+ start (length replacement)))))
- (setq start (match-end 0))
- (warn "Unrecognized key: _%s_" key))))
-
- (t
- (let* ((varp (if (eq ?` (aref (match-string 2 docstring) 0)) 1 0))
- (spec (match-string 1 docstring))
- (lspec (length spec)))
- (setq offset
- (with-temp-buffer
- (insert (substring docstring (+ 1 start varp
- (length spec))))
- (goto-char (point-min))
- (push (read (current-buffer)) varlist)
- (- (point) (point-min))))
- (when (or (zerop lspec)
- (/= (aref spec (1- (length spec))) ?s))
- (setq spec (concat spec "S")))
- (setq docstring
- (concat
- (substring docstring 0 start)
- "%" spec
- (substring docstring (+ start offset 1 lspec varp))))))))
- (hydra--format-1 docstring rest varlist))))
-
- (defun hydra--format-1 (docstring rest varlist)
- (cond
- ((string= docstring "")
- rest)
- ((listp rest)
- (unless (string-match-p "[:\n]" docstring)
- (setq docstring (concat docstring ":\n")))
- (unless (or (string-match-p "\n\\'" docstring)
- (equal (cadr rest) "\n"))
- (setq docstring (concat docstring "\n")))
- `(concat (format ,(replace-regexp-in-string "\\`\n" "" docstring) ,@(nreverse varlist))
- ,@(cdr rest)))
- ((eq ?\n (aref docstring 0))
- `(format ,(concat (substring docstring 1) rest) ,@(nreverse varlist)))
- (t
- (let ((r `(replace-regexp-in-string
- " +$" ""
- (concat ,docstring
- ,(cond ((string-match-p "\\`\n" rest)
- ":")
- ((string-match-p "\n" rest)
- ":\n")
- (t
- ": "))
- (replace-regexp-in-string
- "\\(%\\)" "\\1\\1" ,rest)))))
- (if (stringp rest)
- `(format ,(eval r))
- `(format ,r))))))
-
- (defun hydra--complain (format-string &rest args)
- "Forward to (`message' FORMAT-STRING ARGS) unless `hydra-verbose' is nil."
- (if hydra-verbose
- (apply #'error format-string args)
- (apply #'message format-string args)))
-
- (defun hydra--doc (body-key body-name heads)
- "Generate a part of Hydra docstring.
- BODY-KEY is the body key binding.
- BODY-NAME is the symbol that identifies the Hydra.
- HEADS is a list of heads."
- (format
- "The heads for the associated hydra are:\n\n%s\n\n%s%s."
- (mapconcat
- (lambda (x)
- (format "\"%s\": `%S'" (car x) (cadr x)))
- heads ",\n")
- (format "The body can be accessed via `%S'" body-name)
- (if body-key
- (format ", which is bound to \"%s\"" body-key)
- "")))
-
- (defun hydra--call-interactively-remap-maybe (cmd)
- "`call-interactively' the given CMD or its remapped equivalent.
- Only when `hydra-look-for-remap' is non nil."
- (let ((remapped-cmd (if hydra-look-for-remap
- (command-remapping `,cmd)
- nil)))
- (if remapped-cmd
- (call-interactively `,remapped-cmd)
- (call-interactively `,cmd))))
-
- (defun hydra--call-interactively (cmd name)
- "Generate a `call-interactively' statement for CMD.
- Set `this-command' to NAME."
- (if (and (symbolp name)
- (not (memq name '(nil body))))
- `(progn
- (setq this-command ',name)
- (hydra--call-interactively-remap-maybe #',cmd))
- `(hydra--call-interactively-remap-maybe #',cmd)))
-
- (defun hydra--make-defun (name body doc head
- keymap body-pre body-before-exit
- &optional body-after-exit)
- "Make a defun wrapper, using NAME, BODY, DOC, HEAD, and KEYMAP.
- NAME and BODY are the arguments to `defhydra'.
- DOC was generated with `hydra--doc'.
- HEAD is one of the HEADS passed to `defhydra'.
- BODY-PRE is added to the start of the wrapper.
- BODY-BEFORE-EXIT will be called before the hydra quits.
- BODY-AFTER-EXIT is added to the end of the wrapper."
- (let ((cmd-name (hydra--head-name head name))
- (cmd (when (car head)
- (hydra--make-callable
- (cadr head))))
- (doc (if (car head)
- (format "Call the head `%S' in the \"%s\" hydra.\n\n%s"
- (cadr head) name doc)
- (format "Call the body in the \"%s\" hydra.\n\n%s"
- name doc)))
- (hint (intern (format "%S/hint" name)))
- (body-foreign-keys (hydra--body-foreign-keys body))
- (body-timeout (plist-get body :timeout))
- (body-idle (plist-get body :idle)))
- `(defun ,cmd-name ()
- ,doc
- (interactive)
- (require 'hydra)
- (hydra-default-pre)
- ,@(when body-pre (list body-pre))
- ,@(if (hydra--head-property head :exit)
- `((hydra-keyboard-quit)
- (setq hydra-curr-body-fn ',(intern (format "%S/body" name)))
- ,@(if body-after-exit
- `((unwind-protect
- ,(when cmd
- (hydra--call-interactively cmd (cadr head)))
- ,body-after-exit))
- (when cmd
- `(,(hydra--call-interactively cmd (cadr head))))))
- (delq
- nil
- `((let ((hydra--ignore ,(not (eq (cadr head) 'body))))
- (hydra-keyboard-quit)
- (setq hydra-curr-body-fn ',(intern (format "%S/body" name))))
- ,(when cmd
- `(condition-case err
- ,(hydra--call-interactively cmd (cadr head))
- ((quit error)
- (message (error-message-string err)))))
- ,(if (and body-idle (eq (cadr head) 'body))
- `(hydra-idle-message ,body-idle ,hint ',name)
- `(hydra-show-hint ,hint ',name))
- (hydra-set-transient-map
- ,keymap
- (lambda () (hydra-keyboard-quit) ,body-before-exit)
- ,(when body-foreign-keys
- (list 'quote body-foreign-keys)))
- ,body-after-exit
- ,(when body-timeout
- `(hydra-timeout ,body-timeout))))))))
-
- (defvar hydra-props-alist nil)
-
- (defun hydra-set-property (name key val)
- "Set hydra property.
- NAME is the symbolic name of the hydra.
- KEY and VAL are forwarded to `plist-put'."
- (let ((entry (assoc name hydra-props-alist))
- plist)
- (when (null entry)
- (add-to-list 'hydra-props-alist (list name))
- (setq entry (assoc name hydra-props-alist)))
- (setq plist (cdr entry))
- (setcdr entry (plist-put plist key val))))
-
- (defun hydra-get-property (name key)
- "Get hydra property.
- NAME is the symbolic name of the hydra.
- KEY is forwarded to `plist-get'."
- (let ((entry (assoc name hydra-props-alist)))
- (when entry
- (plist-get (cdr entry) key))))
-
- (defun hydra-show-hint (hint caller)
- (let ((verbosity (plist-get (cdr (assoc caller hydra-props-alist))
- :verbosity)))
- (cond ((eq verbosity 0))
- ((eq verbosity 1)
- (message (eval hint)))
- (t
- (when hydra-is-helpful
- (funcall
- (nth 1 (assoc hydra-hint-display-type hydra-hint-display-alist))
- (eval hint)))))))
-
- (defmacro hydra--make-funcall (sym)
- "Transform SYM into a `funcall' to call it."
- `(when (and ,sym (symbolp ,sym))
- (setq ,sym `(funcall #',,sym))))
-
- (defun hydra--head-name (h name)
- "Return the symbol for head H of hydra with NAME."
- (let ((str (format "%S/%s" name
- (cond ((symbolp (cadr h))
- (cadr h))
- ((and (consp (cadr h))
- (eq (cl-caadr h) 'function))
- (cadr (cadr h)))
- (t
- (concat "lambda-" (car h)))))))
- (when (and (hydra--head-property h :exit)
- (not (memq (cadr h) '(body nil))))
- (setq str (concat str "-and-exit")))
- (intern str)))
-
- (defun hydra--delete-duplicates (heads)
- "Return HEADS without entries that have the same CMD part.
- In duplicate HEADS, :cmd-name is modified to whatever they duplicate."
- (let ((ali '(((hydra-repeat . nil) . hydra-repeat)))
- res entry)
- (dolist (h heads)
- (if (setq entry (assoc (cons (cadr h)
- (hydra--head-property h :exit))
- ali))
- (setf (cl-cdddr h) (plist-put (cl-cdddr h) :cmd-name (cdr entry)))
- (push (cons (cons (cadr h)
- (hydra--head-property h :exit))
- (plist-get (cl-cdddr h) :cmd-name))
- ali)
- (push h res)))
- (nreverse res)))
-
- (defun hydra--pad (lst n)
- "Pad LST with nil until length N."
- (let ((len (length lst)))
- (if (= len n)
- lst
- (append lst (make-list (- n len) nil)))))
-
- (defmacro hydra-multipop (lst n)
- "Return LST's first N elements while removing them."
- `(if (<= (length ,lst) ,n)
- (prog1 ,lst
- (setq ,lst nil))
- (prog1 ,lst
- (setcdr
- (nthcdr (1- ,n) (prog1 ,lst (setq ,lst (nthcdr ,n ,lst))))
- nil))))
-
- (defun hydra--matrix (lst rows cols)
- "Create a matrix from elements of LST.
- The matrix size is ROWS times COLS."
- (let ((ls (copy-sequence lst))
- res)
- (dotimes (_c cols)
- (push (hydra--pad (hydra-multipop ls rows) rows) res))
- (nreverse res)))
-
- (defun hydra--cell (fstr names)
- "Format a rectangular cell based on FSTR and NAMES.
- FSTR is a format-style string with two string inputs: one for the
- doc and one for the symbol name.
- NAMES is a list of variables."
- (let ((len (cl-reduce
- (lambda (acc it) (max (length (symbol-name it)) acc))
- names
- :initial-value 0)))
- (mapconcat
- (lambda (sym)
- (if sym
- (format fstr
- (documentation-property sym 'variable-documentation)
- (let ((name (symbol-name sym)))
- (concat name (make-string (- len (length name)) ?^)))
- sym)
- ""))
- names
- "\n")))
-
- (defun hydra--vconcat (strs &optional joiner)
- "Glue STRS vertically. They must be the same height.
- JOINER is a function similar to `concat'."
- (setq joiner (or joiner #'concat))
- (mapconcat
- (lambda (s)
- (if (string-match " +$" s)
- (replace-match "" nil nil s)
- s))
- (apply #'cl-mapcar joiner
- (mapcar
- (lambda (s) (split-string s "\n"))
- strs))
- "\n"))
-
- (defvar hydra-cell-format "% -20s %% -8`%s"
- "The default format for docstring cells.")
-
- (defun hydra--table (names rows cols &optional cell-formats)
- "Format a `format'-style table from variables in NAMES.
- The size of the table is ROWS times COLS.
- CELL-FORMATS are `format' strings for each column.
- If CELL-FORMATS is a string, it's used for all columns.
- If CELL-FORMATS is nil, `hydra-cell-format' is used for all columns."
- (setq cell-formats
- (cond ((null cell-formats)
- (make-list cols hydra-cell-format))
- ((stringp cell-formats)
- (make-list cols cell-formats))
- (t
- cell-formats)))
- (hydra--vconcat
- (cl-mapcar
- #'hydra--cell
- cell-formats
- (hydra--matrix names rows cols))
- (lambda (&rest x)
- (mapconcat #'identity x " "))))
-
- (defun hydra-reset-radios (names)
- "Set variables NAMES to their defaults.
- NAMES should be defined by `defhydradio' or similar."
- (dolist (n names)
- (set n (aref (get n 'range) 0))))
-
- ;; Following functions deal with automatic docstring table generation from :column head property
- (defun hydra--normalize-heads (heads)
- "Ensure each head from HEADS have a property :column.
- Set it to the same value as preceding head or nil if no previous value
- was defined."
- (let ((current-col nil))
- (mapcar (lambda (head)
- (if (hydra--head-has-property head :column)
- (setq current-col (hydra--head-property head :column)))
- (hydra--head-set-property head :column current-col))
- heads)))
-
- (defun hydra--sort-heads (normalized-heads)
- "Return a list of heads with non-nil doc grouped by column property.
- Each head of NORMALIZED-HEADS must have a column property."
- (let* ((heads-wo-nil-doc (cl-remove-if-not (lambda (head) (nth 2 head)) normalized-heads))
- (columns-list (delete-dups (mapcar (lambda (head) (hydra--head-property head :column))
- normalized-heads)))
- (get-col-index-fun (lambda (head) (cl-position (hydra--head-property head :column)
- columns-list
- :test 'equal)))
- (heads-sorted (cl-sort heads-wo-nil-doc (lambda (it other)
- (< (funcall get-col-index-fun it)
- (funcall get-col-index-fun other))))))
- ;; this operation partition the sorted head list into lists of heads with same column property
- (cl-loop for head in heads-sorted
- for column-name = (hydra--head-property head :column)
- with prev-column-name = (hydra--head-property (nth 0 heads-sorted) :column)
- unless (equal prev-column-name column-name) collect heads-one-column into heads-all-columns
- and do (setq heads-one-column nil)
- collect head into heads-one-column
- do (setq prev-column-name column-name)
- finally return (append heads-all-columns (list heads-one-column)))))
-
- (defun hydra--pad-heads (heads-groups padding-head)
- "Return a copy of HEADS-GROUPS padded where applicable with PADDING-HEAD."
- (cl-loop for heads-group in heads-groups
- for this-head-group-length = (length heads-group)
- with head-group-max-length = (apply #'max (mapcar (lambda (heads) (length heads)) heads-groups))
- if (<= this-head-group-length head-group-max-length)
- collect (append heads-group (make-list (- head-group-max-length this-head-group-length) padding-head))
- into balanced-heads-groups
- else collect heads-group into balanced-heads-groups
- finally return balanced-heads-groups))
-
- (defun hydra--generate-matrix (heads-groups)
- "Return a copy of HEADS-GROUPS decorated with table formatting information.
- Details of modification:
- 2 virtual heads acting as table header were added to each heads-group.
- Each head is decorated with 2 new properties max-doc-len and max-key-len
- representing the maximum dimension of their owning group.
- Every heads-group have equal length by adding padding heads where applicable."
- (when heads-groups
- (let ((res nil))
- (dolist (heads-group (hydra--pad-heads heads-groups '(" " nil " " :exit t)))
- (let* ((column-name (hydra--head-property (nth 0 heads-group) :column))
- (max-key-len (apply #'max (mapcar (lambda (x) (length (car x))) heads-group)))
- (max-doc-len (apply #'max
- (length column-name)
- (mapcar (lambda (x) (length (hydra--to-string (nth 2 x)))) heads-group)))
- (header-virtual-head `(" " nil ,column-name :column ,column-name :exit t))
- (separator-virtual-head `(" " nil ,(make-string (+ 2 max-doc-len max-key-len) ?-) :column ,column-name :exit t))
- (decorated-heads (copy-tree (apply 'list header-virtual-head separator-virtual-head heads-group))))
- (push (mapcar (lambda (it)
- (hydra--head-set-property it :max-key-len max-key-len)
- (hydra--head-set-property it :max-doc-len max-doc-len))
- decorated-heads) res)))
- (nreverse res))))
-
- (defun hydra-interpose (x lst)
- "Insert X in between each element of LST."
- (let (res y)
- (while (setq y (pop lst))
- (push y res)
- (push x res))
- (nreverse (cdr res))))
-
- (defun hydra--hint-row (heads body)
- (let ((lst (hydra-interpose
- "| "
- (mapcar (lambda (head)
- (funcall hydra-key-doc-function
- (hydra-fontify-head head body)
- (let ((n (hydra--head-property head :max-key-len)))
- (+ n (cl-count ?% (car head))))
- (nth 2 head) ;; doc
- (hydra--head-property head :max-doc-len)))
- heads))))
- (when (stringp (car (last lst)))
- (let ((len (length lst))
- (new-last (replace-regexp-in-string "\s+$" "" (car (last lst)))))
- (when (= 0 (length (setf (nth (- len 1) lst) new-last)))
- (setf (nth (- len 2) lst) "|"))))
- lst))
-
-
- (defun hydra--hint-from-matrix (body heads-matrix)
- "Generate a formatted table-style docstring according to BODY and HEADS-MATRIX.
- HEADS-MATRIX is expected to be a list of heads with following features:
- Each heads must have the same length
- Each head must have a property max-key-len and max-doc-len."
- (when heads-matrix
- (let ((lines (hydra--hint-from-matrix-1 body heads-matrix)))
- `(,@(apply #'append (hydra-interpose '("\n") lines))
- "\n"))))
-
- (defun hydra--hint-from-matrix-1 (body heads-matrix)
- (let* ((first-heads-col (nth 0 heads-matrix))
- (last-row-index (- (length first-heads-col) 1))
- (lines nil))
- (dolist (row-index (number-sequence 0 last-row-index))
- (let ((heads-in-row (mapcar
- (lambda (heads) (nth row-index heads))
- heads-matrix)))
- (push (hydra--hint-row heads-in-row body)
- lines)))
- (nreverse lines)))
-
- (defun hydra-idle-message (secs hint name)
- "In SECS seconds display HINT."
- (cancel-timer hydra-message-timer)
- (setq hydra-message-timer (timer-create))
- (timer-set-time hydra-message-timer
- (timer-relative-time (current-time) secs))
- (timer-set-function
- hydra-message-timer
- (lambda ()
- (hydra-show-hint hint name)
- (cancel-timer hydra-message-timer)))
- (timer-activate hydra-message-timer))
-
- (defun hydra-timeout (secs &optional function)
- "In SECS seconds call FUNCTION, then function `hydra-keyboard-quit'.
- Cancel the previous `hydra-timeout'."
- (cancel-timer hydra-timeout-timer)
- (setq hydra-timeout-timer (timer-create))
- (timer-set-time hydra-timeout-timer
- (timer-relative-time (current-time) secs))
- (timer-set-function
- hydra-timeout-timer
- `(lambda ()
- ,(when function
- `(funcall ,function))
- (hydra-keyboard-quit)))
- (timer-activate hydra-timeout-timer))
-
- ;;* Macros
- ;;;###autoload
- (defmacro defhydra (name body &optional docstring &rest heads)
- "Create a Hydra - a family of functions with prefix NAME.
-
- NAME should be a symbol, it will be the prefix of all functions
- defined here.
-
- BODY has the format:
-
- (BODY-MAP BODY-KEY &rest BODY-PLIST)
-
- DOCSTRING will be displayed in the echo area to identify the
- Hydra. When DOCSTRING starts with a newline, special Ruby-style
- substitution will be performed by `hydra--format'.
-
- Functions are created on basis of HEADS, each of which has the
- format:
-
- (KEY CMD &optional HINT &rest PLIST)
-
- 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 (both are strings passed to `kbd'), and will set
- the transient map so that all following heads can be called
- though KEY only. BODY-KEY can be an empty string.
-
- 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 BODY-PLIST and are allowed to
- override some keys. The keys recognized are :exit, :bind, and :column.
- :exit can be:
-
- - nil (default): this head will continue the Hydra state.
- - t: this head will stop the Hydra state.
-
- :bind can be:
- - nil: this head will not be bound in BODY-MAP.
- - a lambda taking KEY and CMD used to bind a head.
-
- :column is a string that sets the column for all subsequent heads.
-
- 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) (doc-string 3))
- (setq heads (copy-tree heads))
- (cond ((stringp docstring))
- ((and (consp docstring)
- (memq (car docstring) '(hydra--table concat format)))
- (setq docstring (concat "\n" (eval docstring))))
- (t
- (setq heads (cons docstring heads))
- (setq docstring "")))
- (when (keywordp (car body))
- (setq body (cons nil (cons nil body))))
- (setq body (hydra--normalize-body body))
- (condition-case-unless-debug err
- (let* ((keymap-name (intern (format "%S/keymap" name)))
- (body-name (intern (format "%S/body" name)))
- (body-key (cadr body))
- (body-plist (cddr body))
- (base-map (or (eval (plist-get body-plist :base-map))
- hydra-base-map))
- (keymap (copy-keymap base-map))
- (body-map (or (car body)
- (plist-get body-plist :bind)))
- (body-pre (plist-get body-plist :pre))
- (body-body-pre (plist-get body-plist :body-pre))
- (body-before-exit (or (plist-get body-plist :post)
- (plist-get body-plist :before-exit)))
- (body-after-exit (plist-get body-plist :after-exit))
- (body-inherit (plist-get body-plist :inherit))
- (body-foreign-keys (hydra--body-foreign-keys body))
- (body-exit (hydra--body-exit body)))
- (dolist (base body-inherit)
- (setq heads (append heads (copy-sequence (eval base)))))
- (dolist (h heads)
- (let ((len (length h)))
- (cond ((< len 2)
- (error "Each head should have at least two items: %S" h))
- ((= len 2)
- (setcdr (cdr h)
- (list
- (hydra-plist-get-default
- body-plist :hint hydra-default-hint)))
- (setcdr (nthcdr 2 h) (list :exit body-exit)))
- (t
- (let ((hint (cl-caddr h)))
- (unless (or (null hint)
- (stringp hint)
- (consp hint))
- (let ((inherited-hint
- (hydra-plist-get-default
- body-plist :hint hydra-default-hint)))
- (setcdr (cdr h) (cons
- (if (eq 'none inherited-hint)
- nil
- inherited-hint)
- (cddr h))))))
- (let ((hint-and-plist (cddr h)))
- (if (null (cdr hint-and-plist))
- (setcdr hint-and-plist (list :exit body-exit))
- (let* ((plist (cl-cdddr h))
- (h-color (plist-get plist :color)))
- (if h-color
- (progn
- (plist-put plist :exit
- (cl-case h-color
- ((blue teal) t)
- (t nil)))
- (cl-remf (cl-cdddr h) :color))
- (let ((h-exit (hydra-plist-get-default plist :exit 'default)))
- (plist-put plist :exit
- (if (eq h-exit 'default)
- body-exit
- h-exit))))))))))
- (plist-put (cl-cdddr h) :cmd-name (hydra--head-name h name))
- (when (null (cadr h)) (plist-put (cl-cdddr h) :exit t)))
- (let ((doc (hydra--doc body-key body-name heads))
- (heads-nodup (hydra--delete-duplicates heads)))
- (mapc
- (lambda (x)
- (define-key keymap (kbd (car x))
- (plist-get (cl-cdddr x) :cmd-name)))
- heads)
- (hydra--make-funcall body-pre)
- (hydra--make-funcall body-body-pre)
- (hydra--make-funcall body-before-exit)
- (hydra--make-funcall body-after-exit)
- (when (memq body-foreign-keys '(run warn))
- (unless (cl-some
- (lambda (h)
- (hydra--head-property h :exit))
- heads)
- (error
- "An %S Hydra must have at least one blue head in order to exit"
- body-foreign-keys)))
- `(progn
- (set (defvar ,(intern (format "%S/params" name))
- nil
- ,(format "Params of %S." name))
- ',body)
- (set (defvar ,(intern (format "%S/docstring" name))
- nil
- ,(format "Docstring of %S." name))
- ,docstring)
- (set (defvar ,(intern (format "%S/heads" name))
- nil
- ,(format "Heads for %S." name))
- ',(mapcar (lambda (h)
- (let ((j (copy-sequence h)))
- (cl-remf (cl-cdddr j) :cmd-name)
- j))
- heads))
- ;; create keymap
- (set (defvar ,keymap-name
- nil
- ,(format "Keymap for %S." name))
- ',keymap)
- ;; declare heads
- (set
- (defvar ,(intern (format "%S/hint" name)) nil
- ,(format "Dynamic hint for %S." name))
- ',(hydra--format name body docstring heads))
- ;; create defuns
- ,@(mapcar
- (lambda (head)
- (hydra--make-defun name body doc head keymap-name
- body-pre
- body-before-exit
- body-after-exit))
- heads-nodup)
- ;; free up keymap prefix
- ,@(unless (or (null body-key)
- (null body-map)
- (hydra--callablep body-map))
- `((unless (keymapp (lookup-key ,body-map (kbd ,body-key)))
- (define-key ,body-map (kbd ,body-key) nil))))
- ;; bind keys
- ,@(delq nil
- (mapcar
- (lambda (head)
- (let ((name (hydra--head-property head :cmd-name)))
- (when (and (cadr head)
- (or body-key body-map))
- (let ((bind (hydra--head-property head :bind body-map))
- (final-key
- (if body-key
- (vconcat (kbd body-key) (kbd (car head)))
- (kbd (car head)))))
- (cond ((null bind) nil)
- ((hydra--callablep bind)
- `(funcall ,bind ,final-key (function ,name)))
- ((and (symbolp bind)
- (if (boundp bind)
- (keymapp (symbol-value bind))
- t))
- `(define-key ,bind ,final-key (quote ,name)))
- (t
- (error "Invalid :bind property `%S' for head %S" bind head)))))))
- heads))
- ,(hydra--make-defun
- name body doc '(nil body)
- keymap-name
- (or body-body-pre body-pre) body-before-exit
- '(setq prefix-arg current-prefix-arg)))))
- (error
- (hydra--complain "Error in defhydra %S: %s" name (cdr err))
- nil)))
-
- (defmacro defhydra+ (name body &optional docstring &rest heads)
- "Redefine an existing hydra by adding new heads.
- Arguments are same as of `defhydra'."
- (declare (indent defun) (doc-string 3))
- (unless (stringp docstring)
- (setq heads
- (cons docstring heads))
- (setq docstring nil))
- `(defhydra ,name ,(or body (hydra--prop name "/params"))
- ,(or docstring (hydra--prop name "/docstring"))
- ,@(cl-delete-duplicates
- (append (hydra--prop name "/heads") heads)
- :key #'car
- :test #'equal)))
-
- (defun hydra--prop (name prop-name)
- (symbol-value (intern (concat (symbol-name name) prop-name))))
-
- (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))
- (defvar ,(intern (format "%S/names" name))
- ',(mapcar (lambda (h) (intern (format "%S/%S" name (car h))))
- 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)))
- (doc (cadr head))
- (val (or (cl-caddr head) [nil t])))
- `((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))
- (cl-incf i)))
- (error "Val not in range for %S" sym)))
- (set sym
- (aref range
- (if (>= i l)
- 0
- i)))))
-
- (defvar hydra-pause-ring (make-ring 10)
- "Ring for paused hydras.")
-
- (defun hydra-pause-resume ()
- "Quit the current hydra and save it to the stack.
- If there's no active hydra, pop one from the stack and call its body.
- If the stack is empty, call the last hydra's body."
- (interactive)
- (cond (hydra-curr-map
- (ring-insert hydra-pause-ring hydra-curr-body-fn)
- (hydra-keyboard-quit))
- ((zerop (ring-length hydra-pause-ring))
- (funcall hydra-curr-body-fn))
- (t
- (funcall (ring-remove hydra-pause-ring 0)))))
-
- ;; Local Variables:
- ;; outline-regexp: ";;\\([;*]+ [^\s\t\n]\\|###autoload\\)\\|("
- ;; indent-tabs-mode: nil
- ;; End:
-
- (provide 'hydra)
-
- ;;; hydra.el ends here
|