2

Is there a way to prompt the user for a key sequence, like read-key-sequence, but only including keys from a specified keymap? It seems like read-key-sequence always reads from all active keymaps.

Prismavoid
  • 41
  • 1
  • read-key-sequence doesn't pay attention to any keymaps. It reads a key sequence that Emacs can recognize - regardless of whether it is bound in any keymap. A key sequence need not be bound to any command. – Drew Aug 27 '21 at 06:25
  • 1
    What is it you're really trying to do? Are you trying to complete key descriptions against the keys bound in some keymap? If so, you can use map-keymap to get key descriptions of all keys in a keymap, and then use those descriptions as completion candidates with completing-read. – Drew Aug 27 '21 at 06:27
  • 1
    @Drew read-key-sequence does pay attention to the currently active keymaps - if you press C-x after running read-key-sequence, it will wait for you to press more keys because C-x is globally bound to a keymap, but if you press an unmapped key or key bound to a command, it will return the sequence. What I am looking for is a command to do the same thing but only taking into account a specific keymap. – Prismavoid Aug 28 '21 at 04:25
  • Yes, when you hit a prefix key, that is, a key bound to a keymap, it waits for the rest of the key sequence. But it does not use the keymap that's bound to C-x. You can type C-x 7, even though that key sequence is unbound. – Drew Aug 28 '21 at 16:55
  • @Drew Yes it does use the keymap bound to C-x, enough to know that C-x 5 is yet another prefix and read one more key after that. Literally one of the first lines in the built-in help: "The sequence is sufficient to specify a non-prefix command in the current local and global maps." It stops at C-x 7 because 7 is not bound in the C-x map, so it has ruled out the prefix case. It doesn't stop at C-x 5 because it looked into the C-x keymap and seen that 5 is bound to yet another keymap, and it will look in there too to see if the next key is also a prefix, and so on. – mtraceur Sep 10 '23 at 16:11

1 Answers1

1

Best I've been able to figure out:

(defun read-key-sequence-in-keymap (keymap &rest arguments)
    (let ((overriding-terminal-local-map nil)
          (overriding-local-map keymap)
          (saved-global-map (current-global-map)))
        (unwind-protect
            (progn
                (use-global-map (make-sparse-keymap))
                (apply 'read-key-sequence arguments))
            (use-global-map saved-global-map))))

Usage Notes

Since read-key-sequence doesn't distinguish if the key sequence reaches a non-prefix bound key or an unbound key, combine it with something like lookup-key if you need to tell those cases apart (note the t argument at the end of lookup-key makes it respect default bindings the same way that read-key-sequence does):

(let ((key-sequence (read-key-sequence-in-keymap my-keymap ...)))
    (lookup-key my-keymap key-sequence t))

Explanation

The big picture: activate the keymap we're interested in, and disable all other keymaps. This limits read-key-sequence to just our keymap.

The details:

Keymaps can come from many different variables, and even from text under point, but overriding-local-map is uniquely special: it does not merely take precedence, it causes most other keymaps to not even get looked at.

The easiest way to see this is in the pseudo code in the Emacs documentation for how keymaps are searched (notice how most of the search is in the "else" branch of if overriding-local-map):

(or (if overriding-terminal-local-map
        (find-in overriding-terminal-local-map))
    (if overriding-local-map
        (find-in overriding-local-map)
      (or (find-in (get-char-property (point) 'keymap))
          (find-in-any emulation-mode-map-alists)
          (find-in-any minor-mode-overriding-map-alist)
          (find-in-any minor-mode-map-alist)
          (if (get-char-property (point) 'local-map)
              (find-in (get-char-property (point) 'local-map))
            (find-in (current-local-map)))))
    (find-in (current-global-map)))

So simply assigning the keymap we're interested in to overriding-local-map is most of the solution, and we just have to clear overriding-terminal-local-map and the global map. The first is trivial - just set it to nil. But the second requires a little extra fiddling:

  1. we have to use use-global-map to set it since it doesn't live in a global variable (there's global-map but that's just a reference to the default global map, not the current one);
  2. Emacs won't let us set it to nil, so we have to create a new empty keymap (by the way, an empty sparse keymap is just '(keymap)); and
  3. we have to make sure we restore the global keymap when we're done.

Questions

Does this break input methods?

My own design intuition based on what I know would be to implement the entry into key-translating functions using something like key-translation-map or a "special event" (aiui: binding that execute as soon as they're read, before the normal key sequence read and lookup is done), and that should be fully compatible with this.

(The Quail library that ships with Emacs uses overriding-terminal-local-map in its implemention of such input methods, but glancing at the source, it seems to only do this temporarily in their own let form, during their own code, with little to no opportunity for other code to run, so that part seems compatible with this.)

Bugs

When read-key-sequence-in-keymap is ran inside a (with-temp-buffer ...), and input comes from a terminal (not GUI) Emacs frame, hitting the Escape key causes the read to wait for more input until I hit any other key.

mtraceur
  • 346
  • 2
  • 13