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