#+title: keymap-popup.el

A macro that defines a keymap with embedded descriptions and a popup
to display them.

=One definition, two uses: direct key dispatch and interactive menu.=

Requires Emacs 29.1+.

** Usage

#+begin_src emacs-lisp
  (keymap-popup-define demo-text-map
    "Text editing commands."
    :group "Navigate"
    "a" ("Beginning" beginning-of-buffer)
    "e" ("End" end-of-buffer)
    "l" ("Goto line" goto-line)
    :group "Edit"
    "f" ("Fill paragraph" fill-paragraph)
    "s" ("Sort lines" sort-lines)
    ";" ("Comment region" comment-dwim))
#+end_src

Eval this, then call =(keymap-popup 'demo-text-map)= to see the popup.

This produces a real =defvar-keymap=.  Keys work directly when the map
is active.  Press =h= for the popup.

*** Infixes

#+begin_src emacs-lisp
  (keymap-popup-define demo-fill-map
    :group "Options"
    "j" ("Justify" :switch demo-justify)
    "w" ("Fill column" :option demo-fill-col :reader read-number :prompt "Column: ")
    :group "Actions"
    "f" ("Fill" (lambda () (interactive)
                  (let ((fill-column (or demo-fill-col fill-column)))
                    (fill-paragraph (when demo-justify 'full))))))
#+end_src

Switches toggle buffer-local booleans.  Options set buffer-local
values via a reader.  Pressing a switch or option key re-renders
the popup without closing it.

*** Sub-menus

#+begin_src emacs-lisp
  (keymap-popup-define demo-edit-map
    :group "Edit"
    "f" ("Fill paragraph" fill-paragraph)
    "s" ("Sort lines" sort-lines)
    "r" ("Reverse region" reverse-region))

  (keymap-popup-define demo-main-map
    :group "Navigate"
    "a" ("Beginning" beginning-of-buffer)
    "e" ("End" end-of-buffer)
    "x" ("Edit" :keymap demo-edit-map))
#+end_src

Press =x= to enter the sub-menu.  =q= or =C-g= goes back.

*** Inheritance

#+begin_src emacs-lisp
  (keymap-popup-define demo-base-map
    :group "Common"
    "g" ("Goto line" goto-line)
    "r" ("Revert" revert-buffer))

  (keymap-popup-define demo-text-view-map
    :parent demo-base-map
    :group "Text"
    "f" ("Fill paragraph" fill-paragraph)
    "s" ("Sort lines" sort-lines))

  (keymap-popup-define demo-prog-view-map
    :parent demo-base-map
    :group "Code"
    ";" ("Comment" comment-dwim)
    "i" ("Indent region" indent-region))
#+end_src

Both maps inherit Goto line and Revert from =demo-base-map=.  The
popup shows entries from both child and parent.

*** Conditional and inapt entries

#+begin_src emacs-lisp
  (keymap-popup-define demo-cond-map
    :group "Actions"
    "f" ("Fill paragraph" fill-paragraph)
    ;; only shown when region is active
    "s" ("Sort lines" sort-lines :if (lambda () mark-active))
    ;; visible but grayed out in read-only buffers
    "d" ("Delete char" delete-char :inapt-if (lambda () buffer-read-only)))
#+end_src

*** Prefix argument

#+begin_src emacs-lisp
  (keymap-popup-define demo-prefix-map
    :group "Actions"
    "f" ("Fill paragraph" fill-paragraph :c-u "justify")
    "s" ("Sort lines" sort-lines :c-u "reverse"))
#+end_src

In the popup, =C-u= enters prefix mode: entries with =:c-u= are
highlighted, others are dimmed.  The next key dispatches with the
prefix argument.  =C-g= cancels prefix mode.

*** Other features

- =:stay-open t= on a command keeps the popup open after dispatch
- =:popup-key "?"= changes the popup key (default =h=)
- Dynamic descriptions: use a function instead of a string
- Dynamic group names: =:group (lambda () (format "Items (%d)" count))=
- =:row= starts a new row of columns in the popup layout
- =keymap-popup-add-entry= / =keymap-popup-remove-entry= for runtime modification
