summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThanos Apollo <public@thanosapollo.org>2026-04-26 02:02:32 +0300
committerThanos Apollo <public@thanosapollo.org>2026-04-26 02:02:32 +0300
commitdb768f7410675916827d0e850971e4114b3ab450 (patch)
tree6c1e3c3da3b8f052ec2c4aff268f9ffcb8c976d3
parent368c7aeb3bceb067bf36b93f72b7ecfe97bc45e7 (diff)
keymap-popup: Add sub-menus, C-u prefix mode, column rows, and review fixes
-rw-r--r--keymap-popup.el239
-rw-r--r--tests/keymap-popup-tests.el150
2 files changed, 290 insertions, 99 deletions
diff --git a/keymap-popup.el b/keymap-popup.el
index 95a1b3f..d3cdb22 100644
--- a/keymap-popup.el
+++ b/keymap-popup.el
@@ -3,7 +3,7 @@
;; Author: Thanos Apollo
;; Version: 0.1.0
;; Package-Requires: ((emacs "29.1"))
-;; Keywords: convenience keymaps
+;; Keywords: convenience
;; URL: https://thanosapollo.org/projects/keymap-popup
;; This program is free software; you can redistribute it and/or modify
@@ -50,13 +50,18 @@
"Face for infix values in the popup."
:group 'keymap-popup)
+(defface keymap-popup-submenu
+ '((t :inherit font-lock-type-face))
+ "Face for sub-menu entries in the popup."
+ :group 'keymap-popup)
+
;;; Parsers
(defun keymap-popup--extract-props (plist)
"Extract known properties from PLIST.
-Recognized keys: :if, :reader, :prompt, :stay-open."
+Recognized keys: :if, :reader, :prompt, :stay-open, :c-u."
(cl-loop for (k v) on plist by #'cddr
- when (memq k '(:if :reader :prompt :stay-open))
+ when (memq k '(:if :reader :prompt :stay-open :c-u))
append (list k v)))
(defun keymap-popup--parse-entry (key spec)
@@ -74,6 +79,10 @@ SPEC is (DESCRIPTION COMMAND-OR-TYPE &rest PROPS)."
`(: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)
+ ,@(keymap-popup--extract-props (cdr rest))))
(_
`(:key ,key :description ,description :type suffix
:command ,second
@@ -162,7 +171,9 @@ Each row is a list of group plists with :name and :entries."
(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))))))
+ ('option (intern (format "%s--set-%s" map-name (plist-get entry :variable))))
+ ('keymap (let ((target (plist-get entry :target)))
+ `(lambda () (interactive) (keymap-popup ',target))))))
;;; Macro helpers
@@ -174,41 +185,33 @@ MAP-NAME is used to derive generated command names."
append (list (plist-get entry :key)
(if (symbolp cmd) `#',cmd cmd))))
+(defun keymap-popup--quote-if-needed (form)
+ "Quote FORM unless it is a lambda, in which case return as-is."
+ (if (and (consp form) (eq (car form) 'lambda))
+ form
+ `',form))
+
(defun keymap-popup--build-entry-form (entry)
"Build a `list' form for a single ENTRY that evaluates lambdas properly."
- (let* ((desc (plist-get entry :description))
- (desc-form (if (and (consp desc) (eq (car desc) 'lambda))
- desc
- `',desc))
- (type (plist-get entry :type)))
- (pcase type
- ('suffix
- (let* ((cmd (plist-get entry :command))
- (cmd-form (if (and (consp cmd) (eq (car cmd) 'lambda))
- cmd
- `',cmd))
- (if-pred (plist-get entry :if))
- (stay-open (plist-get entry :stay-open)))
- `(list :key ,(plist-get entry :key)
- :description ,desc-form
- :type 'suffix
- :command ,cmd-form
- ,@(when if-pred (list :if if-pred))
- ,@(when stay-open (list :stay-open t)))))
- ('switch
- (let ((if-pred (plist-get entry :if)))
- `(list :key ,(plist-get entry :key)
- :description ,desc-form
- :type 'switch
- :variable ',(plist-get entry :variable)
- ,@(when if-pred (list :if if-pred)))))
- ('option
- (let ((if-pred (plist-get entry :if)))
- `(list :key ,(plist-get entry :key)
- :description ,desc-form
- :type 'option
- :variable ',(plist-get entry :variable)
- ,@(when if-pred (list :if if-pred))))))))
+ (let* ((type (plist-get entry :type))
+ (key (plist-get entry :key))
+ (desc-form (keymap-popup--quote-if-needed
+ (plist-get entry :description)))
+ (type-props (pcase type
+ ('suffix `(:command ,(keymap-popup--quote-if-needed
+ (plist-get entry :command))
+ ,@(when (plist-get entry :stay-open)
+ '(:stay-open t))))
+ ('keymap `(:target ',(plist-get entry :target)))
+ (_ `(:variable ',(plist-get entry :variable)))))
+ (if-pred (plist-get entry :if)))
+ `(list :key ,key
+ :description ,desc-form
+ :type ',type
+ ,@type-props
+ ,@(when if-pred (list :if if-pred))
+ ,@(when-let* ((c-u (plist-get entry :c-u)))
+ (list :c-u c-u)))))
(defun keymap-popup--build-descriptions-form (rows)
"Build a `list' form that constructs descriptions at load time.
@@ -338,15 +341,22 @@ Updates both the keymap and the popup descriptions."
"If DESC is a function, call it; otherwise return as-is."
(if (functionp desc) (funcall desc) desc))
-(defun keymap-popup--render-entry (entry)
- "Render ENTRY into a formatted line, or nil if :if hides it."
+(defun keymap-popup--render-entry (entry &optional prefix-mode)
+ "Render ENTRY into a formatted line, or nil if :if hides it.
+When PREFIX-MODE is non-nil, entries with :c-u are highlighted and
+their :c-u description is shown; other entries are dimmed."
(when (or (null (plist-get entry :if))
(funcall (plist-get entry :if)))
- (let* ((desc (keymap-popup--resolve-description
- (plist-get entry :description)))
+ (let* ((raw-desc (keymap-popup--resolve-description
+ (plist-get entry :description)))
+ (type (plist-get entry :type))
+ (desc (if (eq type 'keymap)
+ (propertize raw-desc 'face 'keymap-popup-submenu)
+ raw-desc))
+ (c-u-desc (plist-get entry :c-u))
(key-str (propertize (plist-get entry :key)
'face 'keymap-popup-key))
- (value-str (pcase (plist-get entry :type)
+ (value-str (pcase type
('switch (propertize
(if (symbol-value (plist-get entry :variable))
" [on]" " [off]")
@@ -355,17 +365,27 @@ Updates both the keymap and the popup descriptions."
(format " =%s"
(symbol-value (plist-get entry :variable)))
'face 'keymap-popup-value))
- (_ ""))))
- (format " %s %s%s" key-str desc value-str))))
-
-(defun keymap-popup--render-group-lines (group)
+ (_ "")))
+ (c-u-str (when c-u-desc
+ (if prefix-mode
+ (format " (%s)" c-u-desc)
+ (propertize (format " (%s)" c-u-desc)
+ 'face 'shadow))))
+ (line (format " %s %s%s%s" key-str desc value-str
+ (or c-u-str ""))))
+ (if (and prefix-mode (not c-u-desc))
+ (propertize line 'face 'shadow)
+ line))))
+
+(defun keymap-popup--render-group-lines (group &optional prefix-mode)
"Render GROUP into a list of lines (strings).
+When PREFIX-MODE is non-nil, pass it to entry rendering.
Returns nil if the group has no visible entries."
(let* ((header (when-let* ((raw-name (plist-get group :name))
(name (keymap-popup--resolve-description raw-name)))
(propertize name 'face 'keymap-popup-group-header)))
(lines (cl-loop for entry in (plist-get group :entries)
- for line = (keymap-popup--render-entry entry)
+ for line = (keymap-popup--render-entry entry prefix-mode)
when line collect line)))
(when lines
(if header (cons header lines) lines))))
@@ -406,12 +426,13 @@ Shorter columns are padded with blank lines."
padded-cols
separator)))))
-(defun keymap-popup--rows-to-columns (rows)
+(defun keymap-popup--rows-to-columns (rows &optional prefix-mode)
"Render each row of ROWS into its list of column line-lists.
+When PREFIX-MODE is non-nil, pass it to group rendering.
Returns a list of ((col-lines ...) ...) per row, filtering empty groups."
(mapcar (lambda (row)
(cl-loop for group in row
- for lines = (keymap-popup--render-group-lines group)
+ for lines = (keymap-popup--render-group-lines group prefix-mode)
when lines collect lines))
rows))
@@ -424,14 +445,15 @@ Returns a list of ((col-lines ...) ...) per row, filtering empty groups."
when (nth i cols)
maximize (keymap-popup--column-width (nth i cols))))))
-(defun keymap-popup--render (docstring rows)
+(defun keymap-popup--render (docstring rows &optional prefix-mode)
"Render DOCSTRING and ROWS into a complete popup string.
ROWS is a list of rows, each row a list of groups.
+When PREFIX-MODE is non-nil, highlight :c-u entries and dim others.
Column widths are aligned across all rows."
(let* ((doc (when docstring
(concat (propertize docstring 'face 'font-lock-doc-face)
"\n")))
- (rendered-rows (keymap-popup--rows-to-columns rows))
+ (rendered-rows (keymap-popup--rows-to-columns rows prefix-mode))
(col-widths (keymap-popup--global-col-widths rendered-rows))
(sections (cl-loop for cols in rendered-rows
when cols
@@ -458,23 +480,31 @@ Returns the entry plist, or nil."
(when-let* ((entry (keymap-popup--find-entry-by-key descriptions key-str)))
(memq (plist-get entry :type) '(switch option))))
+(defun keymap-popup--keymap-target (descriptions key-str)
+ "Return the target map symbol if KEY-STR is a :keymap entry in DESCRIPTIONS."
+ (when-let* ((entry (keymap-popup--find-entry-by-key descriptions key-str)))
+ (when (eq (plist-get entry :type) 'keymap)
+ (plist-get entry :target))))
+
(defun keymap-popup--stay-open-p (descriptions key-str)
- "Return non-nil if KEY-STR should keep the popup open.
+ "Return non-nil if KEY-STR should keep the popup open in DESCRIPTIONS.
True for infixes and suffixes with :stay-open."
(when-let* ((entry (keymap-popup--find-entry-by-key descriptions key-str)))
(or (memq (plist-get entry :type) '(switch option))
(plist-get entry :stay-open))))
-(defun keymap-popup--refresh-buffer (buf map-symbol)
- "Re-render the popup buffer BUF for MAP-SYMBOL."
- (let* ((descriptions (get map-symbol 'keymap-popup--descriptions))
- (docstring (documentation-property map-symbol 'variable-documentation))
- (content (keymap-popup--render docstring descriptions)))
+(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
+prefix argument highlighting."
+ (let ((content (keymap-popup--render docstring descriptions prefix-mode)))
(with-current-buffer buf
(let ((inhibit-read-only t))
(erase-buffer)
(insert content)
- (goto-char (point-min))))))
+ (goto-char (point-min))))
+ (when (and win (window-live-p win))
+ (fit-window-to-buffer win))))
(defun keymap-popup--prepare-buffer (map-symbol)
"Create and populate the popup buffer for MAP-SYMBOL."
@@ -485,32 +515,70 @@ True for infixes and suffixes with :stay-open."
(setq-local buffer-read-only t)
(setq-local cursor-type nil)
(setq-local mode-line-format nil))
- (keymap-popup--refresh-buffer buf map-symbol)
+ (keymap-popup--refresh-buffer
+ buf nil
+ (get map-symbol 'keymap-popup--descriptions)
+ (documentation-property map-symbol 'variable-documentation))
buf))
-(defun keymap-popup--dismiss-p (key _cmd)
- "Return non-nil if KEY should dismiss the popup.
-The keys `q' and `C-g' always dismiss."
- (memq key '(?q ?\C-g)))
-
-(defun keymap-popup--read-loop (buf win map-symbol keymap descriptions)
- "Read keys until a suffix or dismiss key is pressed.
-MAP-SYMBOL identifies the described keymap. KEYMAP is the live
-keymap for command lookup. DESCRIPTIONS is the stored metadata.
-Stay-open keys (infixes and :stay-open suffixes) execute and
-re-render BUF in WIN. Returns the command to dispatch, or nil."
- (cl-loop for key = (read-key)
+(defun keymap-popup--read-loop (buf win keymap descriptions docstring)
+ "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.
+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 keymap key-str)
- when (keymap-popup--dismiss-p key cmd) return nil
- when (and cmd (keymap-popup--stay-open-p descriptions key-str))
+ 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 (equal 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)))
+ ;; q: pop stack or dismiss
+ else when (eq key ?q)
+ 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))
+ ;; 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)
- (keymap-popup--refresh-buffer buf map-symbol)
- (when (and win (window-live-p win))
- (fit-window-to-buffer win)))
- when (and cmd (not (keymap-popup--stay-open-p descriptions key-str)))
- return 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)))))
;;;###autoload
(defun keymap-popup (map-symbol)
@@ -518,22 +586,23 @@ re-render BUF in WIN. Returns the command to dispatch, or nil."
Display in a bottom side window. Infix keys (switches/options)
execute and re-render without closing. Suffix keys and dismiss
keys close the popup."
- (interactive)
(let* ((buf (keymap-popup--prepare-buffer map-symbol))
(keymap (symbol-value map-symbol))
- (descriptions (get map-symbol 'keymap-popup--descriptions)))
+ (descriptions (get map-symbol 'keymap-popup--descriptions))
+ (docstring (documentation-property map-symbol 'variable-documentation)))
(unwind-protect
(let* ((win (display-buffer buf
'(display-buffer-in-side-window
(side . bottom)
(window-height . fit-window-to-buffer))))
(_ (when win (fit-window-to-buffer win)))
- (final-cmd (keymap-popup--read-loop
- buf win map-symbol keymap descriptions)))
+ (result (keymap-popup--read-loop
+ buf win keymap descriptions docstring)))
(when (and win (window-live-p win))
(delete-window win))
- (when final-cmd
- (call-interactively final-cmd)))
+ (when result
+ (let ((current-prefix-arg (cdr result)))
+ (call-interactively (car result)))))
(when (buffer-live-p buf)
(kill-buffer buf)))))
diff --git a/tests/keymap-popup-tests.el b/tests/keymap-popup-tests.el
index a5192a4..8284c51 100644
--- a/tests/keymap-popup-tests.el
+++ b/tests/keymap-popup-tests.el
@@ -292,20 +292,6 @@
"Signal user-error when symbol has no descriptions."
(should-error (keymap-popup 'nonexistent-symbol) :type 'user-error))
-(ert-deftest keymap-popup-test-dismiss-c-g ()
- "C-g always dismisses."
- (should (keymap-popup--dismiss-p ?\C-g #'ignore))
- (should (keymap-popup--dismiss-p ?\C-g nil)))
-
-(ert-deftest keymap-popup-test-dismiss-q ()
- "q always dismisses the popup, even if bound in the keymap."
- (should (keymap-popup--dismiss-p ?q nil))
- (should (keymap-popup--dismiss-p ?q #'quit-window)))
-
-(ert-deftest keymap-popup-test-no-dismiss-other ()
- "Other keys do not dismiss."
- (should-not (keymap-popup--dismiss-p ?c nil))
- (should-not (keymap-popup--dismiss-p ?c #'ignore)))
;;; Column layout tests
@@ -488,5 +474,141 @@
(should-not (string-match-p "Reply" (buffer-string))))
(kill-buffer buf))))
+;;; Macro edge cases
+
+(ert-deftest keymap-popup-test-if-on-switch ()
+ "Switch with :if is hidden from popup when predicate returns nil."
+ (eval '(define-described-keymap keymap-popup--test-if-sw
+ "v" ("Verbose" :switch keymap-popup--test-if-sw-var
+ :if (lambda () nil)))
+ t)
+ ;; Keybinding still exists
+ (should (keymap-lookup keymap-popup--test-if-sw "v"))
+ ;; But hidden in popup
+ (let ((buf (keymap-popup--prepare-buffer 'keymap-popup--test-if-sw)))
+ (unwind-protect
+ (with-current-buffer buf
+ (should-not (string-match-p "Verbose" (buffer-string))))
+ (kill-buffer buf))))
+
+(ert-deftest keymap-popup-test-if-on-option ()
+ "Option with :if is hidden from popup when predicate returns nil."
+ (eval '(define-described-keymap keymap-popup--test-if-opt
+ "n" ("Count" :option keymap-popup--test-if-opt-var
+ :reader read-number :prompt "N: "
+ :if (lambda () nil)))
+ t)
+ (should (keymap-lookup keymap-popup--test-if-opt "n"))
+ (let ((buf (keymap-popup--prepare-buffer 'keymap-popup--test-if-opt)))
+ (unwind-protect
+ (with-current-buffer buf
+ (should-not (string-match-p "Count" (buffer-string))))
+ (kill-buffer buf))))
+
+(ert-deftest keymap-popup-test-stay-open-in-descriptions ()
+ "Suffix with :stay-open stores the flag in descriptions."
+ (eval '(define-described-keymap keymap-popup--test-stay
+ "g" ("Refresh" ignore :stay-open t))
+ t)
+ (let* ((descs (get 'keymap-popup--test-stay 'keymap-popup--descriptions))
+ (entry (keymap-popup--find-entry-by-key descs "g")))
+ (should (plist-get entry :stay-open))))
+
+(ert-deftest keymap-popup-test-popup-key-with-docstring ()
+ "Docstring and :popup-key work together."
+ (eval '(define-described-keymap keymap-popup--test-pkdoc
+ "My commands."
+ :popup-key "?"
+ :group "Actions"
+ "c" ("Comment" ignore))
+ t)
+ (should (functionp (keymap-lookup keymap-popup--test-pkdoc "?")))
+ (should (null (keymap-lookup keymap-popup--test-pkdoc "h")))
+ (should (string-match-p "My commands"
+ (documentation-property 'keymap-popup--test-pkdoc
+ 'variable-documentation))))
+
+(ert-deftest keymap-popup-test-dynamic-group-name ()
+ "Group name can be a function called at render time."
+ (eval '(define-described-keymap keymap-popup--test-dyngrp
+ :group (lambda () "Dynamic Group")
+ "c" ("Comment" ignore))
+ t)
+ (let ((buf (keymap-popup--prepare-buffer 'keymap-popup--test-dyngrp)))
+ (unwind-protect
+ (with-current-buffer buf
+ (should (string-match-p "Dynamic Group" (buffer-string))))
+ (kill-buffer buf))))
+
+;;; Keymap entry tests
+
+(ert-deftest keymap-popup-test-parse-keymap-entry ()
+ "A :keymap entry parses with target symbol."
+ (let ((result (keymap-popup--parse-entry "a" '("Metadata" :keymap my-sub-map))))
+ (should (equal (plist-get result :type) 'keymap))
+ (should (equal (plist-get result :target) 'my-sub-map))))
+
+(ert-deftest keymap-popup-test-keymap-target ()
+ "Detect :keymap entries in descriptions."
+ (let ((descs (list (list (list :name nil
+ :entries (list (list :key "c" :type 'suffix :command 'ignore)
+ (list :key "a" :type 'keymap
+ :target 'my-sub)))))))
+ (should (eq (keymap-popup--keymap-target descs "a") 'my-sub))
+ (should (null (keymap-popup--keymap-target descs "c")))))
+
+(ert-deftest keymap-popup-test-keymap-entry-gets-submenu-face ()
+ "Keymap entries render with the submenu face."
+ (let* ((rows (list (list (list :name nil
+ :entries (list (list :key "a" :description "Sub"
+ :type 'keymap :target 'x))))))
+ (output (keymap-popup--render nil rows)))
+ (should (string-match-p "Sub" output))
+ (let ((pos (string-match "Sub" output)))
+ (should (eq (get-text-property pos 'face output) 'keymap-popup-submenu)))))
+
+;;; C-u rendering tests
+
+(ert-deftest keymap-popup-test-c-u-desc-in-normal-mode ()
+ "In normal mode, :c-u description appears in shadow face."
+ (let* ((rows (list (list (list :name nil
+ :entries (list (list :key "s" :description "Submit"
+ :type 'suffix :command 'ignore
+ :c-u "force push"))))))
+ (output (keymap-popup--render nil rows)))
+ (should (string-match-p "(force push)" output))
+ (let ((pos (string-match "(force push)" output)))
+ (should (eq (get-text-property pos 'face output) 'shadow)))))
+
+(ert-deftest keymap-popup-test-c-u-desc-in-prefix-mode ()
+ "In prefix mode, :c-u entries are normal and non-c-u entries are dimmed."
+ (let* ((rows (list (list (list :name nil
+ :entries (list (list :key "s" :description "Submit"
+ :type 'suffix :command 'ignore
+ :c-u "force")
+ (list :key "g" :description "Refresh"
+ :type 'suffix :command 'ignore))))))
+ (output (keymap-popup--render nil rows t)))
+ ;; "force" should NOT be in shadow (it's highlighted in prefix mode)
+ (let ((pos (string-match "(force)" output)))
+ (should pos)
+ (should-not (eq (get-text-property pos 'face output) 'shadow)))
+ ;; "Refresh" line should be dimmed
+ (let ((pos (string-match "Refresh" output)))
+ (should (eq (get-text-property pos 'face output) 'shadow)))))
+
+;;; Stay-open detection tests
+
+(ert-deftest keymap-popup-test-stay-open-p ()
+ "Stay-open detects infixes and :stay-open suffixes."
+ (let ((descs (list (list (list :name nil
+ :entries (list (list :key "c" :type 'suffix :command 'ignore)
+ (list :key "g" :type 'suffix :command 'ignore
+ :stay-open t)
+ (list :key "v" :type 'switch :variable 'x)))))))
+ (should-not (keymap-popup--stay-open-p descs "c"))
+ (should (keymap-popup--stay-open-p descs "g"))
+ (should (keymap-popup--stay-open-p descs "v"))))
+
(provide 'keymap-popup-tests)
;;; keymap-popup-tests.el ends here