summaryrefslogtreecommitdiff
path: root/README.org
blob: f5cc096e59847ce4f19fb91d094bdabca25eb40c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#+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+.

** Quick start

#+begin_src emacs-lisp
(keymap-popup-define my-commands-map
  "My commands."
  :group "Edit"
  "c" ("Comment" comment-dwim)
  "r" ("Rename" rename-file)
  :group "View"
  "g" ("Refresh" revert-buffer)
  "q" ("Quit" quit-window))

;; Use as a normal keymap:
(keymap-set some-mode-map "C-c m" my-commands-map)

;; Or show the popup directly:
(keymap-popup my-commands-map)
#+end_src

Press =h= in the keymap to open the popup. Press =q= to dismiss.

** Features

- =:switch= -- buffer-local toggle with =[on]/[off]= display
- =:keymap= -- sub-menu with stack navigation (=q= / =C-g= pops back)
- =:stay-open= -- command executes without dismissing the popup
- =:inapt-if= -- grays out and blocks entries based on a predicate
- =:c-u= -- prefix argument mode (=C-u= highlights eligible entries)
- =:if= -- conditionally hide entries
- =:group= / =:row= -- column layout
- Dynamic descriptions via lambdas
- =keymap-popup-annotate= -- add popup descriptions to existing keymaps

** Full example

Eval this block, then =M-x kp-test=.  It creates a buffer with
state, a popup with switches, sub-menus, inapt entries, dynamic
descriptions, and prefix argument support.

#+begin_src emacs-lisp
  (require 'keymap-popup)

  ;; Force fresh keymaps on re-eval (defvar won't re-set bound variables)
  (mapc #'makunbound
        (cl-remove-if-not #'boundp '(kp-test--map kp-test--sub-map)))

  ;;; Buffer rendering

  (defvar-local kp-test--name nil)


  (defun kp-test--render ()
    "Redraw the *kp-test* buffer from buffer-local state."
    (let ((inhibit-read-only t))
      (erase-buffer)
      (insert (propertize "keymap-popup live test\n" 'face 'bold)
              (make-string 40 ?-) "\n\n"
              (format "  Name:     %s\n" (or kp-test--name "(not set)"))
              "\n"
              (propertize "Press h for popup, H for child-frame, q to quit.\n" 'face 'shadow))))

  (defun kp-test--refresh ()
    "Refresh the display (stay-open)."
    (interactive)
    (kp-test--render)
    (message "Refreshed"))

  ;;; Commands

  (defun kp-test--greet ()
    "Greet using buffer-local state."
    (interactive)
    (let ((name (or kp-test--name "world"))
          (loud current-prefix-arg))
      (message (if loud
                   (format "%s!!!" (upcase name))
                 (format "Hello, %s." name)))
      (kp-test--render)))

  (defun kp-test--sub-action ()
    (interactive)
    (message "Sub-menu action! prefix=%s" current-prefix-arg))

  ;;; Sub-menu keymap

  (keymap-popup-define kp-test--sub-map
    :group "Sub-menu"
    "s" ("Sub action" kp-test--sub-action)
    "x" ("Greet from sub" kp-test--greet))

  ;;; Root keymap

  (keymap-popup-define kp-test--map
    "Test popup"
    :description "keymap-popup live test"
    :group "Actions"
    "a" ("Greet" kp-test--greet :c-u "SHOUT (C-u)")
    "g" ("Refresh" kp-test--refresh :stay-open t)
    :group "Infixes"
    "v" ("Verbose" :switch kp-test--verbose)
    "n" ((lambda () (concat "Name ="
                           (if (and kp-test--name (not (string-empty-p kp-test--name)))
                               (propertize kp-test--name 'face 'success)
                             (propertize "?" 'face 'warning))))
         (lambda () (interactive)
           (setq-local kp-test--name (read-string "Your name: ")))
         :stay-open t)
    :group "Navigate"
    "s" ("Sub-menu" :keymap kp-test--sub-map)
    "q" ("Quit" quit-window)
    "H" ("Popup (child-frame)" (lambda () (interactive)
                                  (let ((keymap-popup-backend #'keymap-popup-backend-child-frame))
                                    (keymap-popup kp-test--map))))
    :row
    :group "Inapt (entry-level)"
    "m" ("Merge (always blocked)" kp-test--greet :inapt-if (lambda () t))
    "d" ("Dynamic inapt" kp-test--greet
         :inapt-if (lambda () (not kp-test--verbose)))
    :group ("Group inapt (when verbose off)" :inapt-if (lambda () (not kp-test--verbose)))
    "x" ("Group-blocked cmd" kp-test--greet)
    :group ("Toggle (visible when verbose)" :if (lambda () kp-test--verbose))
    "t" ("Verbose-only action" kp-test--greet))

  ;;; Entry point

  (defun kp-test ()
    "Open the *kp-test* buffer and activate the popup.
  h opens side-window popup, H opens child-frame popup."
    (interactive)
    (let ((buf (get-buffer-create "*kp-test*")))
      (with-current-buffer buf
        (setq-local buffer-read-only t)
        (kp-test--render)
        (use-local-map kp-test--map))
      (pop-to-buffer-same-window buf)
      (keymap-popup kp-test--map)))

#+end_src

** Annotating existing keymaps

#+begin_src emacs-lisp
(keymap-popup-annotate dired-mode-map
  :group "Navigate"
  dired-next-line "Next"
  dired-previous-line "Previous"
  :group "Mark"
  dired-mark "Mark"
  dired-unmark "Unmark")
#+end_src

Keys are resolved dynamically via =where-is-internal=, so the popup
always reflects the user's current bindings.