• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#   Copyright 2000-2008 Michael Hudson-Doyle <micahel@gmail.com>
2#                       Armin Rigo
3#
4#                        All Rights Reserved
5#
6#
7# Permission to use, copy, modify, and distribute this software and
8# its documentation for any purpose is hereby granted without fee,
9# provided that the above copyright notice appear in all copies and
10# that both that copyright notice and this permission notice appear in
11# supporting documentation.
12#
13# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
14# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
16# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
17# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
18# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
19# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20
21"""
22Keymap contains functions for parsing keyspecs and turning keyspecs into
23appropriate sequences.
24
25A keyspec is a string representing a sequence of key presses that can
26be bound to a command. All characters other than the backslash represent
27themselves. In the traditional manner, a backslash introduces an escape
28sequence.
29
30pyrepl uses its own keyspec format that is meant to be a strict superset of
31readline's KEYSEQ format. This means that if a spec is found that readline
32accepts that this doesn't, it should be logged as a bug. Note that this means
33we're using the `\\C-o' style of readline's keyspec, not the `Control-o' sort.
34
35The extension to readline is that the sequence \\<KEY> denotes the
36sequence of characters produced by hitting KEY.
37
38Examples:
39`a'      - what you get when you hit the `a' key
40`\\EOA'  - Escape - O - A (up, on my terminal)
41`\\<UP>' - the up arrow key
42`\\<up>' - ditto (keynames are case-insensitive)
43`\\C-o', `\\c-o'  - control-o
44`\\M-.'  - meta-period
45`\\E.'   - ditto (that's how meta works for pyrepl)
46`\\<tab>', `\\<TAB>', `\\t', `\\011', '\\x09', '\\X09', '\\C-i', '\\C-I'
47   - all of these are the tab character.
48"""
49
50_escapes = {
51    "\\": "\\",
52    "'": "'",
53    '"': '"',
54    "a": "\a",
55    "b": "\b",
56    "e": "\033",
57    "f": "\f",
58    "n": "\n",
59    "r": "\r",
60    "t": "\t",
61    "v": "\v",
62}
63
64_keynames = {
65    "backspace": "backspace",
66    "delete": "delete",
67    "down": "down",
68    "end": "end",
69    "enter": "\r",
70    "escape": "\033",
71    "f1": "f1",
72    "f2": "f2",
73    "f3": "f3",
74    "f4": "f4",
75    "f5": "f5",
76    "f6": "f6",
77    "f7": "f7",
78    "f8": "f8",
79    "f9": "f9",
80    "f10": "f10",
81    "f11": "f11",
82    "f12": "f12",
83    "f13": "f13",
84    "f14": "f14",
85    "f15": "f15",
86    "f16": "f16",
87    "f17": "f17",
88    "f18": "f18",
89    "f19": "f19",
90    "f20": "f20",
91    "home": "home",
92    "insert": "insert",
93    "left": "left",
94    "page down": "page down",
95    "page up": "page up",
96    "return": "\r",
97    "right": "right",
98    "space": " ",
99    "tab": "\t",
100    "up": "up",
101}
102
103
104class KeySpecError(Exception):
105    pass
106
107
108def parse_keys(keys: str) -> list[str]:
109    """Parse keys in keyspec format to a sequence of keys."""
110    s = 0
111    r: list[str] = []
112    while s < len(keys):
113        k, s = _parse_single_key_sequence(keys, s)
114        r.extend(k)
115    return r
116
117
118def _parse_single_key_sequence(key: str, s: int) -> tuple[list[str], int]:
119    ctrl = 0
120    meta = 0
121    ret = ""
122    while not ret and s < len(key):
123        if key[s] == "\\":
124            c = key[s + 1].lower()
125            if c in _escapes:
126                ret = _escapes[c]
127                s += 2
128            elif c == "c":
129                if key[s + 2] != "-":
130                    raise KeySpecError(
131                        "\\C must be followed by `-' (char %d of %s)"
132                        % (s + 2, repr(key))
133                    )
134                if ctrl:
135                    raise KeySpecError(
136                        "doubled \\C- (char %d of %s)" % (s + 1, repr(key))
137                    )
138                ctrl = 1
139                s += 3
140            elif c == "m":
141                if key[s + 2] != "-":
142                    raise KeySpecError(
143                        "\\M must be followed by `-' (char %d of %s)"
144                        % (s + 2, repr(key))
145                    )
146                if meta:
147                    raise KeySpecError(
148                        "doubled \\M- (char %d of %s)" % (s + 1, repr(key))
149                    )
150                meta = 1
151                s += 3
152            elif c.isdigit():
153                n = key[s + 1 : s + 4]
154                ret = chr(int(n, 8))
155                s += 4
156            elif c == "x":
157                n = key[s + 2 : s + 4]
158                ret = chr(int(n, 16))
159                s += 4
160            elif c == "<":
161                t = key.find(">", s)
162                if t == -1:
163                    raise KeySpecError(
164                        "unterminated \\< starting at char %d of %s"
165                        % (s + 1, repr(key))
166                    )
167                ret = key[s + 2 : t].lower()
168                if ret not in _keynames:
169                    raise KeySpecError(
170                        "unrecognised keyname `%s' at char %d of %s"
171                        % (ret, s + 2, repr(key))
172                    )
173                ret = _keynames[ret]
174                s = t + 1
175            else:
176                raise KeySpecError(
177                    "unknown backslash escape %s at char %d of %s"
178                    % (repr(c), s + 2, repr(key))
179                )
180        else:
181            ret = key[s]
182            s += 1
183    if ctrl:
184        if len(ret) == 1:
185            ret = chr(ord(ret) & 0x1F)  # curses.ascii.ctrl()
186        elif ret in {"left", "right"}:
187            ret = f"ctrl {ret}"
188        else:
189            raise KeySpecError("\\C- followed by invalid key")
190
191    result = [ret], s
192    if meta:
193        result[0].insert(0, "\033")
194    return result
195
196
197def compile_keymap(keymap, empty=b""):
198    r = {}
199    for key, value in keymap.items():
200        if isinstance(key, bytes):
201            first = key[:1]
202        else:
203            first = key[0]
204        r.setdefault(first, {})[key[1:]] = value
205    for key, value in r.items():
206        if empty in value:
207            if len(value) != 1:
208                raise KeySpecError("key definitions for %s clash" % (value.values(),))
209            else:
210                r[key] = value[empty]
211        else:
212            r[key] = compile_keymap(value, empty)
213    return r
214