kitty/gen-key-constants.py
2021-01-16 20:52:15 +05:30

329 lines
15 KiB
Python

#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
import string
from typing import Dict, List, Any
from pprint import pformat
functional_key_defs = '''# {{{
# kitty XKB macOS
escape Escape -
enter Return -
tab Tab -
backspace BackSpace -
insert Insert -
delete Delete -
left Left -
right Right -
up Up -
down Down -
page_up Page_Up -
page_down Page_Down -
home Home -
end End -
caps_lock Caps_Lock -
scroll_lock Scroll_Lock -
num_lock Num_Lock -
print_screen Print -
pause Pause -
menu Menu -
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 -
f21 F21 -
f22 F22 -
f23 F23 -
f24 F24 -
f25 F25 -
f26 F26 -
f27 F27 -
f28 F28 -
f29 F29 -
f30 F30 -
f31 F31 -
f32 F32 -
f33 F33 -
f34 F34 -
f35 F35 -
kp_0 KP_0 -
kp_1 KP_1 -
kp_2 KP_2 -
kp_3 KP_3 -
kp_4 KP_4 -
kp_5 KP_5 -
kp_6 KP_6 -
kp_7 KP_7 -
kp_8 KP_8 -
kp_9 KP_9 -
kp_decimal KP_Decimal -
kp_divide KP_Divide -
kp_multiply KP_Multiply -
kp_subtract KP_Subtract -
kp_add KP_Add -
kp_enter KP_Enter -
kp_equal KP_Equal -
kp_separator KP_Separator -
kp_left KP_Left -
kp_right KP_Right -
kp_up KP_Up -
kp_down KP_Down -
kp_page_up KP_Page_Up -
kp_page_down KP_Page_Down -
kp_home KP_Home -
kp_end KP_End -
kp_insert KP_Insert -
kp_delete KP_Delete -
left_shift Shift_L -
left_control Control_L -
left_alt Alt_L -
left_super Super_L -
right_shift Shift_R -
right_control Control_R -
right_alt Alt_R -
right_super Super_R -
media_play XF86AudioPlay -
media_pause XF86AudioPause -
media_play_pause - -
media_reverse - -
media_stop XF86AudioStop -
media_fast_forward XF86AudioForward -
media_rewind XF86AudioRewind -
media_track_next XF86AudioNext -
media_track_previous XF86AudioPrev -
media_record XF86AudioRecord -
lower_volume XF86AudioLowerVolume -
raise_volume XF86AudioRaiseVolume -
mute_volume XF86AudioMute -
''' # }}}
shift_map = {x[0]: x[1] for x in '`~ 1! 2@ 3# 4$ 5% 6^ 7& 8* 9( 0) -_ =+ [{ ]} \\| ;: \'" ,< .> /?'.split()}
shift_map.update({x: x.upper() for x in string.ascii_lowercase})
functional_encoding_overrides = {
'insert': 2, 'delete': 3, 'page_up': 5, 'page_down': 6,
'home': 7, 'end': 8, 'tab': 9, 'f1': 11, 'f2': 12, 'f3': 13, 'enter': 13, 'f4': 14,
'f5': 15, 'f6': 17, 'f7': 18, 'f8': 19, 'f9': 20, 'f10': 21,
'f11': 23, 'f12': 24, 'escape': 27, 'backspace': 127
}
different_trailer_functionals = {
'up': 'A', 'down': 'B', 'right': 'C', 'left': 'D', 'end': 'F', 'home': 'H',
'f1': 'P', 'f2': 'Q', 'f3': 'R', 'f4': 'S', 'enter': 'u', 'tab': 'u',
'backspace': 'u', 'escape': 'u'
}
functional_key_names: List[str] = []
name_to_code: Dict[str, int] = {}
name_to_xkb: Dict[str, str] = {}
start_code = 0xe000
for line in functional_key_defs.splitlines():
line = line.strip()
if not line or line.startswith('#'):
continue
parts = line.split()
name = parts[0]
functional_key_names.append(name)
name_to_code[name] = len(name_to_code) + start_code
if parts[1] != '-':
name_to_xkb[name] = parts[1]
last_code = start_code + len(functional_key_names) - 1
ctrl_mapping = {
' ': 0, '@': 0, 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7,
'h': 8, 'i': 9, 'j': 10, 'k': 11, 'l': 12, 'm': 13, 'n': 14, 'o': 15, 'p': 16,
'q': 17, 'r': 18, 's': 19, 't': 20, 'u': 21, 'v': 22, 'w': 23, 'x': 24,
'y': 25, 'z': 26, '[': 27, '\\': 28, ']': 29, '^': 30, '~': 30, '/': 31,
'_': 31, '?': 127, '0': 48, '1': 49, '2': 0, '3': 27, '4': 28,
'5': 29, '6': 30, '7': 31, '8': 127, '9': 57
}
def patch_file(path: str, what: str, text: str, start_marker: str = '/* ', end_marker: str = ' */') -> None:
simple_start_q = f'{start_marker}start {what}{end_marker}'
start_q = f'{start_marker}start {what} (auto generated by gen-key-constants.py do not edit){end_marker}'
end_q = f'{start_marker}end {what}{end_marker}'
with open(path, 'r+') as f:
raw = f.read()
try:
start = raw.index(start_q)
except ValueError:
try:
start = raw.index(simple_start_q)
except ValueError:
raise SystemExit(f'Failed to find "{simple_start_q}" in {path}')
try:
end = raw.index(end_q)
except ValueError:
raise SystemExit(f'Failed to find "{end_q}" in {path}')
raw = raw[:start] + start_q + '\n' + text + '\n' + raw[end:]
f.seek(0)
f.truncate(0)
f.write(raw)
def serialize_dict(x: dict) -> str:
return pformat(x, indent=4).replace('{', '{\n ', 1)
def generate_glfw_header() -> None:
lines = [
'typedef enum {',
f' GLFW_FKEY_FIRST = 0x{start_code:x}u,',
]
klines, pyi, names, knames = [], [], [], []
for name, code in name_to_code.items():
lines.append(f' GLFW_FKEY_{name.upper()} = 0x{code:x}u,')
klines.append(f' ADDC(GLFW_FKEY_{name.upper()});')
pyi.append(f'GLFW_FKEY_{name.upper()}: int')
names.append(f' case GLFW_FKEY_{name.upper()}: return "{name.upper()}";')
knames.append(f' case GLFW_FKEY_{name.upper()}: return PyUnicode_FromString("{name}");')
lines.append(f' GLFW_FKEY_LAST = 0x{last_code:x}u')
lines.append('} GLFWFunctionKey;')
patch_file('glfw/glfw3.h', 'functional key names', '\n'.join(lines))
patch_file('kitty/glfw.c', 'glfw functional keys', '\n'.join(klines))
patch_file('kitty/fast_data_types.pyi', 'glfw functional keys', '\n'.join(pyi), start_marker='# ', end_marker='')
patch_file('glfw/input.c', 'functional key names', '\n'.join(names))
patch_file('kitty/glfw.c', 'glfw functional key names', '\n'.join(knames))
def generate_xkb_mapping() -> None:
lines, rlines = [], []
for name, xkb in name_to_xkb.items():
lines.append(f' case XKB_KEY_{xkb}: return GLFW_FKEY_{name.upper()};')
rlines.append(f' case GLFW_FKEY_{name.upper()}: return XKB_KEY_{xkb};')
patch_file('glfw/xkb_glfw.c', 'xkb to glfw', '\n'.join(lines))
patch_file('glfw/xkb_glfw.c', 'glfw to xkb', '\n'.join(rlines))
def generate_functional_table() -> None:
lines = [
'',
'.. csv-table:: Functional key codes',
' :header: "Name", "CSI", "Name", "CSI"',
''
]
line_items = []
enc_lines = []
tilde_trailers = set()
for name, code in name_to_code.items():
if name in functional_encoding_overrides or name in different_trailer_functionals:
trailer = different_trailer_functionals.get(name, '~')
if trailer == '~':
tilde_trailers.add(code)
code = oc = functional_encoding_overrides.get(name, code)
code = code if trailer in '~u' else 1
enc_lines.append((' ' * 8) + f"case GLFW_FKEY_{name.upper()}: S({code}, '{trailer}');")
if code == 1 and name not in ('up', 'down', 'left', 'right'):
trailer += f' or {oc} ~'
else:
trailer = 'u'
line_items.append(name.upper())
line_items.append(f'``{code}\xa0{trailer}``')
for li in chunks(line_items, 4):
lines.append(' ' + ', '.join(f'"{x}"' for x in li))
lines.append('')
patch_file('docs/keyboard-protocol.rst', 'functional key table', '\n'.join(lines), start_marker='.. ', end_marker='')
patch_file('kitty/key_encoding.c', 'special numbers', '\n'.join(enc_lines))
code_to_name = {v: k.upper() for k, v in name_to_code.items()}
csi_map = {v: name_to_code[k] for k, v in functional_encoding_overrides.items()}
letter_trailer_codes = {
v: functional_encoding_overrides.get(k, name_to_code.get(k))
for k, v in different_trailer_functionals.items() if v in 'ABCDHFPQRSZ'}
text = f'functional_key_number_to_name_map = {serialize_dict(code_to_name)}'
text += f'\ncsi_number_to_functional_number_map = {serialize_dict(csi_map)}'
text += f'\nletter_trailer_to_csi_number_map = {letter_trailer_codes!r}'
text += f'\ntilde_trailers = {tilde_trailers!r}'
patch_file('kitty/key_encoding.py', 'csi mapping', text, start_marker='# ', end_marker='')
def generate_legacy_text_key_maps() -> None:
tests = []
tp = ' ' * 8
shift, alt, ctrl = 1, 2, 4
lines = []
for c, s in shift_map.items():
if c in '\\\'':
c = '\\' + c
lines.append(f" case '{c}': return '{s}';")
patch_file('kitty/key_encoding.c', 'shifted key map', '\n'.join(lines))
def simple(c: str) -> None:
shifted = shift_map.get(c, c)
ctrled = chr(ctrl_mapping.get(c, ord(c)))
for m in range(16):
if m == 0:
tests.append(f'{tp}ae(enc(ord({c!r})), {c!r})')
elif m == shift:
tests.append(f'{tp}ae(enc(ord({c!r}), mods=shift), {shifted!r})')
elif m == alt:
tests.append(f'{tp}ae(enc(ord({c!r}), mods=alt), "\\x1b" + {c!r})')
elif m == ctrl:
tests.append(f'{tp}ae(enc(ord({c!r}), mods=ctrl), {ctrled!r})')
elif m == shift | alt:
tests.append(f'{tp}ae(enc(ord({c!r}), mods=shift | alt), "\\x1b" + {shifted!r})')
elif m == ctrl | alt:
tests.append(f'{tp}ae(enc(ord({c!r}), mods=ctrl | alt), "\\x1b" + {ctrled!r})')
for k in shift_map:
simple(k)
patch_file('kitty_tests/keys.py', 'legacy letter tests', '\n'.join(tests), start_marker='# ', end_marker='')
def chunks(lst: List, n: int) -> Any:
"""Yield successive n-sized chunks from lst."""
for i in range(0, len(lst), n):
yield lst[i:i + n]
def generate_ctrl_mapping() -> None:
lines = [
'.. csv-table:: Emitted bytes when :kbd:`ctrl` is held down and a key is pressed',
' :header: "Key", "Byte", "Key", "Byte", "Key", "Byte"',
''
]
items = []
mi = []
for k in sorted(ctrl_mapping):
prefix = '\\' if k == '\\' else ('SPC' if k == ' ' else '')
items.append(prefix + k)
val = str(ctrl_mapping[k])
items.append(val)
if k in "\\'":
k = '\\' + k
mi.append(f" case '{k}': return {val};")
for line_items in chunks(items, 6):
lines.append(' ' + ', '.join(f'"{x}"' for x in line_items))
lines.append('')
patch_file('docs/keyboard-protocol.rst', 'ctrl mapping', '\n'.join(lines), start_marker='.. ', end_marker='')
patch_file('kitty/key_encoding.c', 'ctrl mapping', '\n'.join(mi))
def main() -> None:
generate_glfw_header()
generate_xkb_mapping()
generate_functional_table()
generate_legacy_text_key_maps()
generate_ctrl_mapping()
if __name__ == '__main__':
main()