| # Copyright 2000-2008 Michael Hudson-Doyle <micahel@gmail.com> | |
| # Armin Rigo | |
| # | |
| # All Rights Reserved | |
| # | |
| # | |
| # Permission to use, copy, modify, and distribute this software and | |
| # its documentation for any purpose is hereby granted without fee, | |
| # provided that the above copyright notice appear in all copies and | |
| # that both that copyright notice and this permission notice appear in | |
| # supporting documentation. | |
| # | |
| # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO | |
| # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | |
| # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, | |
| # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER | |
| # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF | |
| # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | |
| # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |
| """ | |
| Keymap contains functions for parsing keyspecs and turning keyspecs into | |
| appropriate sequences. | |
| A keyspec is a string representing a sequence of key presses that can | |
| be bound to a command. All characters other than the backslash represent | |
| themselves. In the traditional manner, a backslash introduces an escape | |
| sequence. | |
| pyrepl uses its own keyspec format that is meant to be a strict superset of | |
| readline's KEYSEQ format. This means that if a spec is found that readline | |
| accepts that this doesn't, it should be logged as a bug. Note that this means | |
| we're using the '\\C-o' style of readline's keyspec, not the 'Control-o' sort. | |
| The extension to readline is that the sequence \\<KEY> denotes the | |
| sequence of characters produced by hitting KEY. | |
| Examples: | |
| 'a' - what you get when you hit the 'a' key | |
| '\\EOA' - Escape - O - A (up, on my terminal) | |
| '\\<UP>' - the up arrow key | |
| '\\<up>' - ditto (keynames are case-insensitive) | |
| '\\C-o', '\\c-o' - control-o | |
| '\\M-.' - meta-period | |
| '\\E.' - ditto (that's how meta works for pyrepl) | |
| '\\<tab>', '\\<TAB>', '\\t', '\\011', '\\x09', '\\X09', '\\C-i', '\\C-I' | |
| - all of these are the tab character. | |
| """ | |
| _escapes = { | |
| "\\": "\\", | |
| "'": "'", | |
| '"': '"', | |
| "a": "\a", | |
| "b": "\b", | |
| "e": "\033", | |
| "f": "\f", | |
| "n": "\n", | |
| "r": "\r", | |
| "t": "\t", | |
| "v": "\v", | |
| } | |
| _keynames = { | |
| "backspace": "backspace", | |
| "delete": "delete", | |
| "down": "down", | |
| "end": "end", | |
| "enter": "\r", | |
| "escape": "\033", | |
| "f1": "f1", | |
| "f2": "f2", | |
| "f3": "f3", | |
| "f4": "f4", | |
| "f5": "f5", | |
| "f6": "f6", | |
| "f7": "f7", | |
| "f8": "f8", | |
| "f9": "f9", | |
| "f10": "f10", | |
| "f11": "f11", | |
| "f12": "f12", | |
| "f13": "f13", | |
| "f14": "f14", | |
| "f15": "f15", | |
| "f16": "f16", | |
| "f17": "f17", | |
| "f18": "f18", | |
| "f19": "f19", | |
| "f20": "f20", | |
| "home": "home", | |
| "insert": "insert", | |
| "left": "left", | |
| "page down": "page down", | |
| "page up": "page up", | |
| "return": "\r", | |
| "right": "right", | |
| "space": " ", | |
| "tab": "\t", | |
| "up": "up", | |
| } | |
| class KeySpecError(Exception): | |
| pass | |
| def parse_keys(keys: str) -> list[str]: | |
| """Parse keys in keyspec format to a sequence of keys.""" | |
| s = 0 | |
| r: list[str] = [] | |
| while s < len(keys): | |
| k, s = _parse_single_key_sequence(keys, s) | |
| r.extend(k) | |
| return r | |
| def _parse_single_key_sequence(key: str, s: int) -> tuple[list[str], int]: | |
| ctrl = 0 | |
| meta = 0 | |
| ret = "" | |
| while not ret and s < len(key): | |
| if key[s] == "\\": | |
| c = key[s + 1].lower() | |
| if c in _escapes: | |
| ret = _escapes[c] | |
| s += 2 | |
| elif c == "c": | |
| if key[s + 2] != "-": | |
| raise KeySpecError( | |
| "\\C must be followed by `-' (char %d of %s)" | |
| % (s + 2, repr(key)) | |
| ) | |
| if ctrl: | |
| raise KeySpecError( | |
| "doubled \\C- (char %d of %s)" % (s + 1, repr(key)) | |
| ) | |
| ctrl = 1 | |
| s += 3 | |
| elif c == "m": | |
| if key[s + 2] != "-": | |
| raise KeySpecError( | |
| "\\M must be followed by `-' (char %d of %s)" | |
| % (s + 2, repr(key)) | |
| ) | |
| if meta: | |
| raise KeySpecError( | |
| "doubled \\M- (char %d of %s)" % (s + 1, repr(key)) | |
| ) | |
| meta = 1 | |
| s += 3 | |
| elif c.isdigit(): | |
| n = key[s + 1 : s + 4] | |
| ret = chr(int(n, 8)) | |
| s += 4 | |
| elif c == "x": | |
| n = key[s + 2 : s + 4] | |
| ret = chr(int(n, 16)) | |
| s += 4 | |
| elif c == "<": | |
| t = key.find(">", s) | |
| if t == -1: | |
| raise KeySpecError( | |
| "unterminated \\< starting at char %d of %s" | |
| % (s + 1, repr(key)) | |
| ) | |
| ret = key[s + 2 : t].lower() | |
| if ret not in _keynames: | |
| raise KeySpecError( | |
| "unrecognised keyname `%s' at char %d of %s" | |
| % (ret, s + 2, repr(key)) | |
| ) | |
| ret = _keynames[ret] | |
| s = t + 1 | |
| else: | |
| raise KeySpecError( | |
| "unknown backslash escape %s at char %d of %s" | |
| % (repr(c), s + 2, repr(key)) | |
| ) | |
| else: | |
| ret = key[s] | |
| s += 1 | |
| if ctrl: | |
| if len(ret) == 1: | |
| ret = chr(ord(ret) & 0x1F) # curses.ascii.ctrl() | |
| elif ret in {"left", "right"}: | |
| ret = f"ctrl {ret}" | |
| else: | |
| raise KeySpecError("\\C- followed by invalid key") | |
| result = [ret], s | |
| if meta: | |
| result[0].insert(0, "\033") | |
| return result | |
| def compile_keymap(keymap, empty=b""): | |
| r = {} | |
| for key, value in keymap.items(): | |
| if isinstance(key, bytes): | |
| first = key[:1] | |
| else: | |
| first = key[0] | |
| r.setdefault(first, {})[key[1:]] = value | |
| for key, value in r.items(): | |
| if empty in value: | |
| if len(value) != 1: | |
| raise KeySpecError("key definitions for %s clash" % (value.values(),)) | |
| else: | |
| r[key] = value[empty] | |
| else: | |
| r[key] = compile_keymap(value, empty) | |
| return r |