#+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 my-mode-map
    "My mode commands."
    :group "Actions"
    "c" ("Comment" my-comment)
    "r" ("Reply" my-reply)
    "x" ("Toggle" my-toggle)
    :group "Navigate"
    "g" ("Refresh" my-refresh)
    "q" ("Quit" quit-window))

  (define-derived-mode my-mode special-mode "My"
    (use-local-map my-mode-map))
#+end_src

This produces a real =defvar-keymap=.  Keys work directly in the mode.
Press =h= for the popup.

** Infixes

#+begin_src emacs-lisp
  (keymap-popup-define my-list-map
    :group "Options"
    "V" ("Verbose" :switch my-verbose-var)
    "n" ("Limit" :option my-limit-var :reader read-number :prompt "Limit: ")
    :group "Actions"
    "l" ("List" (lambda () (interactive)
                  (my-fetch :verbose my-verbose-var
                            :limit my-limit-var))))
#+end_src

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

** Sub-menus

#+begin_src emacs-lisp
  (keymap-popup-define my-metadata-map
    :group "Add"
    "l" ("Label" my-add-label)
    "a" ("Assignee" my-add-assignee)
    "m" ("Milestone" my-set-milestone))

  (keymap-popup-define my-view-map
    :group "Actions"
    "c" ("Comment" my-comment)
    "a" ("Metadata" :keymap my-metadata-map)
    :group "Navigate"
    "q" ("Quit" quit-window))
#+end_src

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

** Inheritance

#+begin_src emacs-lisp
  (keymap-popup-define view-base-map
    :group "Common"
    "g" ("Refresh" my-refresh)
    "b" ("Browse" my-browse)
    "q" ("Quit" quit-window))

  (keymap-popup-define issue-view-map
    :parent view-base-map
    :group "Issue"
    "c" ("Comment" my-comment)
    "x" ("Close" my-close))

  (keymap-popup-define pull-view-map
    :parent view-base-map
    :group "Pull Request"
    "c" ("Comment" my-comment)
    "m" ("Merge" my-merge))
#+end_src

Both =issue-view-map= and =pull-view-map= inherit Refresh, Browse, and
Quit from =view-base-map=.  The popup shows entries from both child
and parent.

** Conditional and inapt entries

#+begin_src emacs-lisp
  (keymap-popup-define my-map
    :group "Actions"
    "c" ("Comment" my-comment)
    ;; hidden when verbose is off
    "d" ("Debug info" my-debug :if (lambda () my-verbose-var))
    ;; visible but grayed out when merge is not possible
    "m" ("Merge" my-merge :inapt-if (lambda () (not (my-can-merge-p)))))
#+end_src

** Prefix argument

#+begin_src emacs-lisp
  (keymap-popup-define my-map
    :group "Actions"
    "s" ("Submit" my-submit :c-u "force push")
    "g" ("Refresh" my-refresh))
#+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 suffix 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
