Multi-key selection in Emacs
The org-mks
function has been around for a long time. It stands for multi-key selection and lives in the org-macs.el library since 2013, but without much use outside org-capture
and org-insert-structure-template
. The interface is simple, allowing users to select from a list of choices by pressing keys, so here’s an example shared on the mailing list.
(let ((entry (org-mks `(("a" "A entry" ,#'identity) ("b" "B entry" ,(lambda (x) (* 2 x)))) "A or B"))) (funcall (nth 2 entry) 10))
When you run it, a buffer pops up with:
A or B [a] A entry [b] B entry
Pressing a prints 10
and pressing b prints 20
. There are, of course, other ways of doing this within Emacs, but for simple applications I think org-mks
fits the bill well. For example, you can write ("j" "Description")
to use j as a prefix for a multi-letter selection, which probably explains how the function received its name.
(defun key-pressed (key) (message "You pressed %s" key)) (let* ((entry (org-mks '(("j" "Press j") ("jj" "Press j") ("jjj" "Press j" key-pressed) ("jk" "Press k" key-pressed) ("k" "Press k" key-pressed) ("q" "Abort" (lambda (_) nil))) "Select a key" "Keys: ")) (action (nth 2 entry))) (funcall action (car entry)))
And there is one last argument, an alist with ("key" "description")
entries, which returns the bare key only.
(let ((entry (org-mks '(("j" "Roll dice" (lambda () (+ (random 6) 1)))) "Select a key" "Key: " '(("q" "Abort"))))) (cond ((equal entry "q") (user-error "Abort")) (t (funcall (nth 2 entry)))))
It is not possible, however, to pass a prefix argument to it, or to the read-key
function. In other words, they won’t accept C-u j as an input, so I came up with the following solution.
(defun read-choice (prompt choices) "Prompt user to choose an action from a list of choices. Choices should be a list of (KEY DESCRIPTION ACTION) triples. Similar to org-mks, but accepts a prefix arg." (let (action current-prefix-arg) (catch 'exit (save-window-excursion (let (case-fold-search) (pop-to-buffer "*Read choice*" t t) (setq-local cursor-type nil) (erase-buffer) (insert "Select a key\n\n") (pcase-dolist (`(,key ,description) choices) (insert "[" key "] " description "\n")) (goto-char (point-min)) (fit-window-to-buffer) (let ((event (key-description (vector (read-key prompt))))) (when (equal event "C-g") (throw 'exit (user-error "Abort"))) (if (equal event "C-u") (let* ((key (key-description (vector (read-key prompt)))) (selection (assoc key choices))) (setq action (nth 2 selection) current-prefix-arg '(4))) (let ((selection (assoc event choices))) ;; Return description if action is not found (setq action (or (nth 2 selection) (nth 1 selection)))))))) (cond ((stringp action) (message action)) ((commandp action) (call-interactively action)) (t (eval action))))))
Note the difference between j and C-u j.
(read-choice "Key: " '(("j" "Press j" (lambda (arg) (interactive "P") (print (or current-prefix-arg "No ARG")))) ("q" "Abort" keyboard-quit)))
Note also that if you don’t need to run commands, Emacs comes with a read-multiple-choice
function. Either way, I hope you take the right pill.
(read-multiple-choice "Select" '((?r "red") (?b "blue")))