151 lines
4.4 KiB
Python
151 lines
4.4 KiB
Python
""" Key-name to escape-sequence tables
|
|
|
|
The keymap binds key *names*; get_key_code turns a name plus its modifier flags
|
|
into the bytes an app expects. Legacy xterm encodings only — the kitty keyboard
|
|
protocol is deliberately not implemented.
|
|
"""
|
|
|
|
# unmodified keys, and the paste brackets, in their default (non-application) form
|
|
_KEY_MAP = {
|
|
"enter": "\r",
|
|
"backspace": "\x7f",
|
|
"tab": "\t",
|
|
"space": " ",
|
|
"escape": "\x1b",
|
|
"up": "\x1b[A",
|
|
"down": "\x1b[B",
|
|
"right": "\x1b[C",
|
|
"left": "\x1b[D",
|
|
"home": "\x1b[H",
|
|
"end": "\x1b[F",
|
|
"pageup": "\x1b[5~",
|
|
"pagedown": "\x1b[6~",
|
|
"delete": "\x1b[3~",
|
|
"f1": "\x1bOP",
|
|
"f2": "\x1bOQ",
|
|
"f3": "\x1bOR",
|
|
"f4": "\x1bOS",
|
|
"f5": "\x1b[15~",
|
|
"f6": "\x1b[17~",
|
|
"f7": "\x1b[18~",
|
|
"f8": "\x1b[19~",
|
|
"f9": "\x1b[20~",
|
|
"f10": "\x1b[21~",
|
|
"f11": "\x1b[23~",
|
|
"f12": "\x1b[24~",
|
|
"bracketed_paste_start": "\x1b[200~",
|
|
"bracketed_paste_end": "\x1b[201~",
|
|
}
|
|
|
|
# DECCKM: cursor keys switch from CSI to SS3 while an app asks for application mode
|
|
_APP_MODE_KEY_MAP = {
|
|
"up": "\x1bOA",
|
|
"down": "\x1bOB",
|
|
"right": "\x1bOC",
|
|
"left": "\x1bOD",
|
|
"home": "\x1bOH",
|
|
"end": "\x1bOF",
|
|
}
|
|
|
|
# xterm modified keys: CSI 1;<mod><letter> / CSI <n>;<mod>~
|
|
_MODIFIABLE_CSI = {
|
|
"up": "\x1b[1;{}A",
|
|
"down": "\x1b[1;{}B",
|
|
"right": "\x1b[1;{}C",
|
|
"left": "\x1b[1;{}D",
|
|
"home": "\x1b[1;{}H",
|
|
"end": "\x1b[1;{}F",
|
|
"delete": "\x1b[3;{}~",
|
|
"pageup": "\x1b[5;{}~",
|
|
"pagedown": "\x1b[6;{}~",
|
|
}
|
|
|
|
# ctrl+<key> for the keys whose control byte is not chr(letter - 'a' + 1)
|
|
_CTRL_KEY_MAP = {
|
|
"@": "\x00",
|
|
"`": "\x00",
|
|
"2": "\x00",
|
|
"[": "\x1b",
|
|
"{": "\x1b",
|
|
"3": "\x1b",
|
|
"\\": "\x1c",
|
|
"|": "\x1c",
|
|
"4": "\x1c",
|
|
"]": "\x1d",
|
|
"}": "\x1d",
|
|
"5": "\x1d",
|
|
"^": "\x1e",
|
|
"~": "\x1e",
|
|
"6": "\x1e",
|
|
"_": "\x1f",
|
|
"/": "\x1f",
|
|
"7": "\x1f",
|
|
"?": "\x7f",
|
|
"8": "\x7f",
|
|
"space": "\x00",
|
|
"enter": "\n",
|
|
"tab": "\t",
|
|
"backspace": "\x08",
|
|
}
|
|
|
|
# the two shifted keys with an encoding of their own; shift is bare text elsewhere
|
|
_SHIFT_KEY_MAP = {
|
|
"tab": "\x1b[Z",
|
|
"enter": "\x1b\r",
|
|
}
|
|
|
|
# keys whose modified form has no unambiguous legacy encoding: ESC CR reads as
|
|
# alt+enter, ctrl+space as NUL. An app that turned modifyOtherKeys on gets them
|
|
# as CSI 27 ; <mod> ; <code> ~ instead
|
|
_OTHER_KEYS = {"enter": 13, "tab": 9, "backspace": 127, "space": 32}
|
|
|
|
|
|
def _modifier_code(ctrl, alt, shift):
|
|
"""xterm modifier parameter: 1 plus a bitmask of shift/alt/ctrl."""
|
|
return 1 + (1 if shift else 0) + (2 if alt else 0) + (4 if ctrl else 0)
|
|
|
|
|
|
def get_key_code(key, ctrl=False, alt=False, shift=False, application_mode=False, new_line_mode=False, modify_other_keys=0):
|
|
"""Escape sequence for a key name and its modifiers, honouring the app's active modes"""
|
|
key_lo = key.lower() if len(key) > 1 else key
|
|
|
|
# the keys with an xterm modified form of their own
|
|
if (ctrl or alt or shift) and key_lo in _MODIFIABLE_CSI:
|
|
return _MODIFIABLE_CSI[key_lo].format(_modifier_code(ctrl, alt, shift))
|
|
|
|
# the ambiguous ones, while the app asked for CSI 27
|
|
if modify_other_keys and (ctrl or alt or shift) and key_lo in _OTHER_KEYS:
|
|
return "\x1b[27;{};{}~".format(_modifier_code(ctrl, alt, shift), _OTHER_KEYS[key_lo])
|
|
|
|
# ctrl folds to a control byte, alt prefixes ESC, shift is bare text
|
|
if ctrl:
|
|
base = key_lo
|
|
if base in _CTRL_KEY_MAP:
|
|
code = _CTRL_KEY_MAP[base]
|
|
elif len(base) == 1 and "a" <= base <= "z":
|
|
code = chr(ord(base) - ord("a") + 1)
|
|
else:
|
|
code = _plain(key, application_mode, new_line_mode)
|
|
return ("\x1b" + code) if alt else code
|
|
|
|
if alt:
|
|
return "\x1b" + _plain(key if len(key) == 1 else key_lo, application_mode, new_line_mode)
|
|
|
|
if shift:
|
|
if key_lo in _SHIFT_KEY_MAP:
|
|
return _SHIFT_KEY_MAP[key_lo]
|
|
return _KEY_MAP.get(key, key)
|
|
|
|
return _plain(key, application_mode, new_line_mode)
|
|
|
|
|
|
def _plain(key, application_mode=False, new_line_mode=False):
|
|
"""Unmodified encoding, honouring DECCKM and LNM; unknown names pass through as text."""
|
|
if application_mode and key in _APP_MODE_KEY_MAP:
|
|
return _APP_MODE_KEY_MAP[key]
|
|
if new_line_mode and key == "enter":
|
|
return "\r\n"
|
|
if key in _KEY_MAP:
|
|
return _KEY_MAP[key]
|
|
return key
|