diff options
| author | Thanos Apollo <public@thanosapollo.org> | 2026-04-28 03:08:48 +0300 |
|---|---|---|
| committer | Thanos Apollo <public@thanosapollo.org> | 2026-04-28 03:22:23 +0300 |
| commit | 8e0024e084ffa7517b313e41f864563e4d80f59f (patch) | |
| tree | 2320447d443117379a6156737232e8335f57e3b5 | |
| parent | 4ffa637cb55119305c7a897dabe7e4e475d38cc8 (diff) | |
refactor: set-transient-map, drop option type, pseudo-key metadata
| -rw-r--r-- | keymap-popup.el | 497 |
1 files changed, 313 insertions, 184 deletions
diff --git a/keymap-popup.el b/keymap-popup.el index 89c4fe7..8ebf22d 100644 --- a/keymap-popup.el +++ b/keymap-popup.el @@ -41,7 +41,7 @@ Common values: (display-buffer-in-side-window (side . bottom)) - frame-wide (display-buffer-below-selected) - current window only" - :type display-buffer--action-custom-type + :type 'sexp :group 'keymap-popup) ;;; Faces @@ -56,7 +56,7 @@ Common values: (defface keymap-popup-value '((t :inherit font-lock-string-face :weight bold)) - "Face for switch/option values in the popup.") + "Face for switch values in the popup.") (defface keymap-popup-submenu '((t :inherit font-lock-type-face)) @@ -66,13 +66,22 @@ Common values: '((t :inherit shadow)) "Face for inapt (disabled) entries in the popup.") +;;; Keymap metadata + +(defun keymap-popup--meta (keymap prop) + "Get popup metadata PROP from KEYMAP via pseudo-key lookup." + (lookup-key keymap (vector prop))) + +(gv-define-setter keymap-popup--meta (val keymap prop) + `(define-key ,keymap (vector ,prop) ,val)) + ;;; Parsers (defun keymap-popup--extract-props (plist) "Extract known properties from PLIST. -Recognized keys: :if, :inapt-if, :reader, :prompt, :stay-open, :c-u." +Recognized keys: :if, :inapt-if, :stay-open, :c-u." (cl-loop for (k v) on plist by #'cddr - when (memq k '(:if :inapt-if :reader :prompt :stay-open :c-u)) + when (memq k '(:if :inapt-if :stay-open :c-u)) append (list k v))) (defun keymap-popup--parse-entry (key spec) @@ -98,10 +107,6 @@ for key-based entries, or (DESCRIPTION &rest PROPS) for annotated ones." `(:key ,key :description ,description :type switch :variable ,(car rest) ,@(keymap-popup--extract-props (cdr rest)))) - (:option - `(:key ,key :description ,description :type option - :variable ,(car rest) - ,@(keymap-popup--extract-props (cdr rest)))) (:keymap `(:key ,key :description ,description :type keymap :target ,(car rest) @@ -183,29 +188,13 @@ Each row is a list of group plists with :name and :entries." (setq-local ,variable (not ,variable)) (message "%s: %s" ,description (if ,variable "on" "off")))))) -(defun keymap-popup--option-forms (map-name entry) - "Return (defvar-local defun) forms for option ENTRY in MAP-NAME." - (let* ((variable (plist-get entry :variable)) - (description (plist-get entry :description)) - (reader (or (plist-get entry :reader) 'read-string)) - (prompt (or (plist-get entry :prompt) (format "%s: " description))) - (fn-name (intern (format "%s--set-%s" map-name variable)))) - (list - `(defvar-local ,variable nil) - `(defun ,fn-name () - ,(format "Set %s." description) - (interactive) - (setq-local ,variable (,reader ,prompt)) - (message "%s: %s" ,description ,variable))))) - (defun keymap-popup--entry-command (map-name entry) "Return the command to bind in MAP-NAME's keymap for ENTRY." (pcase (plist-get entry :type) ('suffix (plist-get entry :command)) ('switch (intern (format "%s--toggle-%s" map-name (plist-get entry :variable)))) - ('option (intern (format "%s--set-%s" map-name (plist-get entry :variable)))) ('keymap (let ((target (plist-get entry :target))) - `(lambda () (interactive) (keymap-popup ',target)))))) + `(lambda () (interactive) (keymap-popup ,target)))))) ;;; Macro helpers @@ -234,8 +223,8 @@ MAP-NAME is used to derive generated command names." (plist-get entry :command)) ,@(when (plist-get entry :stay-open) '(:stay-open t)))) - ('keymap `(:target ',(plist-get entry :target))) - (_ `(:variable ',(plist-get entry :variable))))) + ('keymap `(:target ,(plist-get entry :target))) + ('switch `(:variable ',(plist-get entry :variable))))) (if-pred (plist-get entry :if)) (inapt-if (plist-get entry :inapt-if))) `(list :key ,key @@ -312,7 +301,6 @@ KEYMAP, optional :description STRING-OR-FUNCTION, followed by (infix-forms (cl-loop for entry in all-entries append (pcase (plist-get entry :type) ('switch (keymap-popup--switch-forms name entry)) - ('option (keymap-popup--option-forms name entry)) (_ nil)))) (keymap-pairs (keymap-popup--build-keymap-pairs name all-entries))) `(progn @@ -321,18 +309,16 @@ KEYMAP, optional :description STRING-OR-FUNCTION, followed by ,@(when docstring (list :doc docstring)) ,@(when parent (list :parent parent)) ,@keymap-pairs - ,popup-key (lambda () (interactive) (keymap-popup ',name))) - (put ',name 'keymap-popup--descriptions - ,(keymap-popup--build-descriptions-form rows)) - (put ',name 'keymap-popup--exit-key ,exit-key) - ,@(when parent - `((put ',name 'keymap-popup--parent ',parent))) + ,popup-key (lambda () (interactive) (keymap-popup ,name))) + (setf (keymap-popup--meta ,name 'keymap-popup--descriptions) + ,(keymap-popup--build-descriptions-form rows)) + (setf (keymap-popup--meta ,name 'keymap-popup--exit-key) ,exit-key) ,@(when description - `((put ',name 'keymap-popup--description ,description)))))) + `((setf (keymap-popup--meta ,name 'keymap-popup--description) ,description)))))) ;;;###autoload -(defmacro keymap-popup-annotate (map-symbol &rest body) - "Annotate existing keymap MAP-SYMBOL with popup descriptions. +(defmacro keymap-popup-annotate (keymap &rest body) + "Annotate existing KEYMAP with popup descriptions. BODY is :group keywords and COMMAND-SYMBOL DESCRIPTION pairs. COMMAND-SYMBOL is a function symbol already bound in the keymap. DESCRIPTION is a string or (STRING &rest PROPS). @@ -341,9 +327,10 @@ time, so the popup always reflects the user's current bindings." (declare (indent 1)) (let ((rows (keymap-popup--parse-bindings body))) `(progn - (put ',map-symbol 'keymap-popup--descriptions - ,(keymap-popup--build-descriptions-form rows)) - (put ',map-symbol 'keymap-popup--annotated t)))) + (setf (keymap-popup--meta ,keymap 'keymap-popup--descriptions) + ,(keymap-popup--build-descriptions-form rows)) + ;; t is the default binding in keymaps, lookup-key ignores it. + (setf (keymap-popup--meta ,keymap 'keymap-popup--annotated) 'yes)))) ;;; Public API @@ -379,27 +366,26 @@ Falls back to the first group if GROUP-NAME is not found." (plist-get group :entries)))))) ;;;###autoload -(defun keymap-popup-add-entry (map-symbol key description command &optional group) - "Add KEY binding with DESCRIPTION and COMMAND to MAP-SYMBOL. +(defun keymap-popup-add-entry (keymap key description command &optional group) + "Add KEY binding with DESCRIPTION and COMMAND to KEYMAP. GROUP is the group name to add to (nil for the first group). Updates both the keymap and the popup descriptions." - (or (get map-symbol 'keymap-popup--descriptions) - (user-error "No descriptions for `%s'" map-symbol)) - (keymap-set (symbol-value map-symbol) key command) - (let ((entry (list :key key :description description - :type 'suffix :command command))) - (put map-symbol 'keymap-popup--descriptions - (keymap-popup--add-entry-to-rows - (get map-symbol 'keymap-popup--descriptions) entry group)))) + (let ((descs (keymap-popup--meta keymap 'keymap-popup--descriptions))) + (or descs (user-error "No descriptions in keymap")) + (keymap-set keymap key command) + (let ((entry (list :key key :description description + :type 'suffix :command command))) + (setf (keymap-popup--meta keymap 'keymap-popup--descriptions) + (keymap-popup--add-entry-to-rows descs entry group))))) ;;;###autoload -(defun keymap-popup-remove-entry (map-symbol key) - "Remove KEY binding from MAP-SYMBOL. +(defun keymap-popup-remove-entry (keymap key) + "Remove KEY binding from KEYMAP. Updates both the keymap and the popup descriptions." - (keymap-set (symbol-value map-symbol) key nil) - (put map-symbol 'keymap-popup--descriptions - (keymap-popup--remove-key-from-rows - (get map-symbol 'keymap-popup--descriptions) key))) + (keymap-set keymap key nil) + (setf (keymap-popup--meta keymap 'keymap-popup--descriptions) + (keymap-popup--remove-key-from-rows + (keymap-popup--meta keymap 'keymap-popup--descriptions) key))) ;;; Renderer @@ -430,16 +416,12 @@ KEY-WIDTH pads the key column for alignment." ?\s)) raw-key)) (key-str (propertize padded-key 'face 'keymap-popup-key)) - (value-str (pcase type - ('switch (propertize - (if (symbol-value (plist-get entry :variable)) - " [on]" " [off]") - 'face 'keymap-popup-value)) - ('option (propertize - (format " =%s" - (symbol-value (plist-get entry :variable))) - 'face 'keymap-popup-value)) - (_ ""))) + (value-str (if (eq type 'switch) + (propertize + (if (symbol-value (plist-get entry :variable)) + " [on]" " [off]") + 'face 'keymap-popup-value) + "")) (c-u-str (and c-u-desc (if prefix-mode (propertize (format " (%s)" c-u-desc) @@ -487,7 +469,6 @@ rendered with the inapt face." "Return the visible width of STR, ignoring text properties." (string-width (substring-no-properties str))) - (defun keymap-popup--column-width (col) "Return the max visible width of lines in COL." (cl-loop for line in col @@ -552,15 +533,35 @@ Column widths are aligned across all rows." "\n")))) (concat doc (string-join sections "\n") "\n"))) +;;; Popup state + +(defvar-local keymap-popup--source-buffer nil + "The buffer from which the popup was invoked. +Switch variables are buffer-local there, so rendering must read +`symbol-value' in that buffer's context.") +(defvar-local keymap-popup--active-keymap nil + "The currently displayed keymap in the popup.") +(defvar-local keymap-popup--active-descriptions nil + "Descriptions for the currently active keymap.") +(defvar-local keymap-popup--active-docstring nil + "Docstring for the currently active keymap.") +(defvar-local keymap-popup--stack nil + "Stack of parent state plists for sub-menu navigation.") +(defvar-local keymap-popup--prefix-mode nil + "Non-nil when C-u prefix mode is active.") +(defvar-local keymap-popup--reentering nil + "Non-nil when a sub-menu just popped, preventing cascading exit.") +(defvar-local keymap-popup--hook-fn nil + "The post-command-hook function for this popup session.") + ;;; Popup display -(defun keymap-popup--collect-descriptions (map-symbol) - "Collect descriptions from MAP-SYMBOL and all its parent keymaps. -Walks the parent chain via the `keymap-popup--parent' property, -appending each parent's rows after the child's." - (cl-loop for sym = map-symbol then (get sym 'keymap-popup--parent) - while sym - when (get sym 'keymap-popup--descriptions) +(defun keymap-popup--collect-descriptions (keymap) + "Collect descriptions from KEYMAP and all its parent keymaps. +Walks the native parent chain via `keymap-parent'." + (cl-loop for map = keymap then (keymap-parent map) + while map + when (keymap-popup--meta map 'keymap-popup--descriptions) append it)) (defun keymap-popup--find-entry-by-key (descriptions key-str) @@ -574,9 +575,9 @@ Returns the entry plist, or nil." return entry)))) (defun keymap-popup--infix-p (descriptions key-str) - "Return non-nil if KEY-STR maps to an infix entry in DESCRIPTIONS." + "Return non-nil if KEY-STR maps to a switch entry in DESCRIPTIONS." (and-let* ((entry (keymap-popup--find-entry-by-key descriptions key-str))) - (memq (plist-get entry :type) '(switch option)))) + (eq (plist-get entry :type) 'switch))) (defun keymap-popup--keymap-target (descriptions key-str) "Return the target map symbol if KEY-STR is a :keymap entry in DESCRIPTIONS." @@ -604,11 +605,20 @@ Checks both group-level and entry-level :inapt-if predicates." (defun keymap-popup--stay-open-p (descriptions key-str) "Return non-nil if KEY-STR should keep the popup open in DESCRIPTIONS. -True for infixes and suffixes with :stay-open." +True for switches and suffixes with :stay-open." (and-let* ((entry (keymap-popup--find-entry-by-key descriptions key-str))) - (or (memq (plist-get entry :type) '(switch option)) + (or (eq (plist-get entry :type) 'switch) (plist-get entry :stay-open)))) +(defun keymap-popup--keep-popup-p (descriptions key-str) + "Return non-nil if KEY-STR should keep the popup open. +True for switches, inapt keys, :keymap entries, and C-u. +Suffixes with :stay-open dismiss and reopen instead." + (or (keymap-popup--infix-p descriptions key-str) + (keymap-popup--inapt-p descriptions key-str) + (keymap-popup--keymap-target descriptions key-str) + (equal key-str "C-u"))) + (defun keymap-popup--refresh-buffer (buf win descriptions &optional docstring prefix-mode) "Re-render popup BUF with DESCRIPTIONS, fit WIN. DOCSTRING is shown at the top if non-nil. PREFIX-MODE toggles @@ -622,6 +632,19 @@ prefix argument highlighting." (when (and win (window-live-p win)) (fit-window-to-buffer win)))) +(defun keymap-popup--refresh (buf) + "Re-render popup BUF from its buffer-local state. +Renders in the source buffer's context so `symbol-value' for +switch variables sees the user's buffer-local values." + (when (buffer-live-p buf) + (let ((source (buffer-local-value 'keymap-popup--source-buffer buf)) + (descs (buffer-local-value 'keymap-popup--active-descriptions buf)) + (doc (buffer-local-value 'keymap-popup--active-docstring buf)) + (prefix (buffer-local-value 'keymap-popup--prefix-mode buf))) + (with-current-buffer (if (buffer-live-p source) source buf) + (keymap-popup--refresh-buffer + buf (get-buffer-window buf) descs doc prefix))))) + (defun keymap-popup--resolve-key (entry keymap) "Resolve ENTRY's :command to a key in KEYMAP. Returns entry with :key filled in, or nil if unbound." @@ -641,118 +664,224 @@ Drops entries whose command has no binding." when (keymap-popup--resolve-key entry keymap) collect it))))) -(defun keymap-popup--prepare-buffer (map-symbol) - "Create and populate the popup buffer for MAP-SYMBOL. -Includes descriptions inherited from parent keymaps. -For annotated keymaps, resolves command symbols to key bindings." +(defun keymap-popup--prepare-buffer () + "Create and configure the popup buffer." (let ((buf (get-buffer-create "*keymap-popup*"))) - (or (get map-symbol 'keymap-popup--descriptions) - (user-error "No descriptions for `%s'" map-symbol)) (with-current-buffer buf - (setq-local buffer-read-only t) - (setq-local cursor-type nil) - (setq-local mode-line-format nil)) - (let ((descriptions (keymap-popup--collect-descriptions map-symbol))) - (keymap-popup--refresh-buffer - buf nil - (if (get map-symbol 'keymap-popup--annotated) - (keymap-popup--resolve-descriptions - descriptions (symbol-value map-symbol)) - descriptions) - (get map-symbol 'keymap-popup--description))) + (setq-local buffer-read-only t + cursor-type nil + mode-line-format nil)) buf)) -(defun keymap-popup--read-loop (buf win keymap descriptions docstring exit-key) - "Read keys in BUF displayed in WIN until a suffix or dismiss. -KEYMAP is the live keymap for command lookup. DESCRIPTIONS is the -stored row metadata. DOCSTRING is shown at the top of the popup. -EXIT-KEY is the character that dismisses the popup (default ?q). -Supports nested :keymap entries via a stack of (DESCS . KEYMAP) -pairs. Prefix argument mode is toggled with `universal-argument'. -Returns (CMD . PREFIX-ARG) or nil on dismiss." - (cl-loop with prefix-mode = nil - with stack = nil - with current-descs = descriptions - with current-keymap = keymap - for key = (read-key) - for key-str = (key-description (vector key)) - for cmd = (keymap-lookup current-keymap key-str) - for keymap-target = (keymap-popup--keymap-target current-descs key-str) - ;; C-u: toggle prefix mode - when (eq key ?\C-u) - do (progn - (setq prefix-mode (not prefix-mode)) - (keymap-popup--refresh-buffer - buf win current-descs docstring prefix-mode)) - ;; C-g: cancel prefix -> pop stack -> dismiss - else when (eq key ?\C-g) - do (cond - (prefix-mode - (setq prefix-mode nil) - (keymap-popup--refresh-buffer buf win current-descs docstring)) - (stack - (let ((prev (pop stack))) - (setq current-descs (car prev) - current-keymap (cdr prev))) - (keymap-popup--refresh-buffer buf win current-descs docstring)) - (t (cl-return nil))) - ;; Exit key: pop stack or dismiss - else when (eq key exit-key) - do (if stack - (let ((prev (pop stack))) - (setq current-descs (car prev) - current-keymap (cdr prev)) - (keymap-popup--refresh-buffer buf win current-descs docstring)) - (cl-return nil)) - ;; Keymap: push current, swap to sub-map - else when keymap-target - do (progn - (push (cons current-descs current-keymap) stack) - (setq current-descs (get keymap-target 'keymap-popup--descriptions) - current-keymap (symbol-value keymap-target) - prefix-mode nil) - (keymap-popup--refresh-buffer buf win current-descs nil)) - ;; Inapt: ignore the keypress - else when (and cmd (keymap-popup--inapt-p current-descs key-str)) - do (message "Command unavailable") - ;; Stay-open: execute, re-render - else when (and cmd (keymap-popup--stay-open-p current-descs key-str)) - do (let ((current-prefix-arg (when prefix-mode '(4)))) - (call-interactively cmd) - (setq prefix-mode nil) - (keymap-popup--refresh-buffer buf win current-descs docstring)) - ;; Suffix: return with prefix arg - else when cmd - return (cons cmd (when prefix-mode '(4))))) +(defun keymap-popup--teardown (buf) + "Remove the popup window for BUF, clean up hooks, and kill it." + (when (buffer-live-p buf) + (when-let* ((fn (buffer-local-value 'keymap-popup--hook-fn buf))) + (remove-hook 'post-command-hook fn)) + (when-let* ((win (get-buffer-window buf))) + (delete-window win)) + (kill-buffer buf))) + +(defun keymap-popup--make-keep-pred (buf) + "Return a keep-pred for `set-transient-map'. +Reads state from BUF. Returns non-nil to keep the map active." + (lambda () + (or (buffer-local-value 'keymap-popup--reentering buf) + (and-let* ((keys (this-command-keys-vector)) + (key-str (key-description keys)) + (descs (buffer-local-value 'keymap-popup--active-descriptions buf))) + (keymap-popup--keep-popup-p descs key-str))))) + +(defun keymap-popup--make-post-command-fn (buf) + "Return a post-command-hook function that refreshes BUF. +Clears the reentering flag and consumes prefix-mode after +switch commands. Removes itself when BUF is killed externally." + (let ((fn (make-symbol "keymap-popup--post-command"))) + (fset fn + (lambda () + (if (not (buffer-live-p buf)) + (remove-hook 'post-command-hook fn) + (with-current-buffer buf + (when keymap-popup--reentering + (setq-local keymap-popup--reentering nil)) + (when-let* ((_ keymap-popup--prefix-mode) + (keys (this-command-keys-vector)) + (key-str (key-description keys)) + (_ (keymap-popup--stay-open-p + keymap-popup--active-descriptions key-str))) + (setq-local keymap-popup--prefix-mode nil) + (setq prefix-arg nil))) + (keymap-popup--refresh buf)))) + fn)) + +(defun keymap-popup--make-on-exit (buf) + "Return an on-exit callback for `set-transient-map'. +Pops the sub-menu stack if non-empty, otherwise tears down." + (lambda () + (when (buffer-live-p buf) + (with-current-buffer buf + (if keymap-popup--stack + (pcase-let ((`(:keymap ,km :descriptions ,descs :docstring ,doc) + (pop keymap-popup--stack))) + (setq-local keymap-popup--active-keymap km + keymap-popup--active-descriptions descs + keymap-popup--active-docstring doc + keymap-popup--reentering t + keymap-popup--prefix-mode nil) + (keymap-popup--refresh buf)) + (keymap-popup--teardown buf)))))) + +(defun keymap-popup--inapt-keys (descriptions) + "Return a list of key-strings that may be inapt in DESCRIPTIONS. +Includes keys with entry-level :inapt-if and keys in groups with :inapt-if." + (cl-loop for row in descriptions + append (cl-loop for group in row + for group-pred = (plist-get group :inapt-if) + append (cl-loop for entry in (plist-get group :entries) + when (and (plist-get entry :key) + (or (plist-get entry :inapt-if) + group-pred)) + collect (plist-get entry :key))))) + +(defun keymap-popup--stay-open-suffix-keys (descriptions) + "Return list of key-strings for :stay-open suffix entries in DESCRIPTIONS." + (cl-loop for row in descriptions + append (cl-loop for group in row + append (cl-loop for entry in (plist-get group :entries) + when (and (plist-get entry :key) + (eq (plist-get entry :type) 'suffix) + (plist-get entry :stay-open)) + collect (plist-get entry :key))))) + +(defun keymap-popup--submenu-keys (descriptions) + "Return an alist of (KEY-STRING . TARGET-KEYMAP) for :keymap entries." + (cl-loop for row in descriptions + append (cl-loop for group in row + append (cl-loop for entry in (plist-get group :entries) + when (eq (plist-get entry :type) 'keymap) + collect (cons (plist-get entry :key) + (plist-get entry :target)))))) + +(defun keymap-popup--push-submenu (buf child-keymap) + "Push current popup state and activate CHILD-KEYMAP's transient map." + (with-current-buffer buf + (push (list :keymap keymap-popup--active-keymap + :descriptions keymap-popup--active-descriptions + :docstring keymap-popup--active-docstring) + keymap-popup--stack) + (let* ((raw (keymap-popup--collect-descriptions child-keymap)) + (descs (if (keymap-popup--meta child-keymap 'keymap-popup--annotated) + (keymap-popup--resolve-descriptions raw child-keymap) + raw)) + (doc (keymap-popup--meta child-keymap 'keymap-popup--description)) + (exit-key (or (keymap-popup--meta child-keymap 'keymap-popup--exit-key) ?q))) + (setq-local keymap-popup--active-keymap child-keymap + keymap-popup--active-descriptions descs + keymap-popup--active-docstring doc + keymap-popup--prefix-mode nil) + (keymap-popup--refresh buf) + (set-transient-map + (keymap-popup--build-wrapper-map child-keymap descs buf exit-key) + (keymap-popup--make-keep-pred buf) + (keymap-popup--make-on-exit buf))))) + +(defun keymap-popup--core-overrides (buf exit-key) + "Return alist of core overrides: exit key and C-u prefix toggle." + (list (cons (key-description (vector exit-key)) + (lambda () (interactive))) + (cons "C-u" + (lambda () (interactive) + (with-current-buffer buf + (setq-local keymap-popup--prefix-mode + (not keymap-popup--prefix-mode)) + (setq prefix-arg + (when keymap-popup--prefix-mode '(4)))))))) + +(defun keymap-popup--inapt-overrides (keymap descriptions buf) + "Return alist of inapt key overrides. +Each command checks the predicate dynamically at invocation time." + (mapcar (lambda (key-str) + (cons key-str + (lambda () (interactive) + (let ((descs (buffer-local-value + 'keymap-popup--active-descriptions buf))) + (if (keymap-popup--inapt-p descs key-str) + (progn + (message "Command unavailable") + (when (buffer-local-value + 'keymap-popup--prefix-mode buf) + (setq prefix-arg '(4)))) + (call-interactively + (keymap-lookup keymap key-str))))))) + (keymap-popup--inapt-keys descriptions))) + +(defun keymap-popup--submenu-overrides (descriptions buf) + "Return alist of submenu key overrides." + (mapcar (lambda (pair) + (cons (car pair) + (let ((target (cdr pair))) + (lambda () (interactive) + (keymap-popup--push-submenu buf target))))) + (keymap-popup--submenu-keys descriptions))) + +(defun keymap-popup--stay-open-overrides (keymap descriptions) + "Return alist of stay-open suffix overrides. +Each command dismisses the popup, executes, and reopens." + (mapcar (lambda (key-str) + (cons key-str + (lambda () (interactive) + (call-interactively (keymap-lookup keymap key-str)) + (keymap-popup keymap)))) + (keymap-popup--stay-open-suffix-keys descriptions))) + +(defun keymap-popup--build-wrapper-map (keymap descriptions buf exit-key) + "Build wrapper keymap over KEYMAP with all popup overrides." + (let ((map (make-sparse-keymap))) + (set-keymap-parent map keymap) + (pcase-dolist (`(,key . ,cmd) + (append (keymap-popup--core-overrides buf exit-key) + (keymap-popup--inapt-overrides keymap descriptions buf) + (keymap-popup--submenu-overrides descriptions buf) + (keymap-popup--stay-open-overrides keymap descriptions))) + (keymap-set map key cmd)) + map)) ;;;###autoload -(defun keymap-popup (map-symbol) - "Show popup help for described keymap MAP-SYMBOL. -Display in a bottom side window. Switch and option keys -execute and re-render without closing. Command keys and -dismiss keys close the popup." - (let* ((buf (keymap-popup--prepare-buffer map-symbol)) - (keymap (symbol-value map-symbol)) - (raw-descriptions (keymap-popup--collect-descriptions map-symbol)) - (descriptions (if (get map-symbol 'keymap-popup--annotated) - (keymap-popup--resolve-descriptions raw-descriptions keymap) - raw-descriptions)) - (docstring (get map-symbol 'keymap-popup--description)) - (exit-key (or (get map-symbol 'keymap-popup--exit-key) ?q))) - (unwind-protect - (let* ((win (display-buffer buf - (append keymap-popup-display-action - '((window-height . fit-window-to-buffer))))) - (_ (when win (fit-window-to-buffer win))) - (result (keymap-popup--read-loop - buf win keymap descriptions docstring exit-key))) - (when (and win (window-live-p win)) - (delete-window win)) - (when result - (let ((current-prefix-arg (cdr result))) - (call-interactively (car result))))) - (when (buffer-live-p buf) - (kill-buffer buf))))) +(defun keymap-popup (keymap) + "Show popup help for described KEYMAP. +Activates KEYMAP as a transient map. Switch keys execute and +re-render without closing. Inapt keys are blocked. +Sub-menu keys push a navigation stack. C-u toggles prefix mode." + (or (keymap-popup--meta keymap 'keymap-popup--descriptions) + (user-error "No descriptions in keymap")) + (let* ((source (current-buffer)) + (buf (keymap-popup--prepare-buffer)) + (raw (keymap-popup--collect-descriptions keymap)) + (descriptions (if (keymap-popup--meta keymap 'keymap-popup--annotated) + (keymap-popup--resolve-descriptions raw keymap) + raw)) + (docstring (keymap-popup--meta keymap 'keymap-popup--description)) + (exit-key (or (keymap-popup--meta keymap 'keymap-popup--exit-key) ?q)) + (hook-fn (keymap-popup--make-post-command-fn buf))) + (with-current-buffer buf + (setq-local keymap-popup--source-buffer source + keymap-popup--active-keymap keymap + keymap-popup--active-descriptions descriptions + keymap-popup--active-docstring docstring + keymap-popup--stack nil + keymap-popup--prefix-mode nil + keymap-popup--reentering nil + keymap-popup--hook-fn hook-fn)) + (keymap-popup--refresh buf) + (display-buffer buf (append keymap-popup-display-action + '((window-height . fit-window-to-buffer)))) + (when-let* ((win (get-buffer-window buf))) + (fit-window-to-buffer win)) + (add-hook 'post-command-hook hook-fn) + (set-transient-map + (keymap-popup--build-wrapper-map keymap descriptions buf exit-key) + (keymap-popup--make-keep-pred buf) + (keymap-popup--make-on-exit buf)))) (provide 'keymap-popup) ;;; keymap-popup.el ends here |
