key_constants.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. #!/usr/bin/env python
  2. # License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
  3. import os
  4. import string
  5. import subprocess
  6. import sys
  7. from pprint import pformat
  8. from typing import Any, Dict, List, Union
  9. if __name__ == '__main__' and not __package__:
  10. import __main__
  11. __main__.__package__ = 'gen'
  12. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  13. functional_key_defs = '''# {{{
  14. # kitty XKB macVK macU
  15. escape Escape 0x35 -
  16. enter Return 0x24 NSCarriageReturnCharacter
  17. tab Tab 0x30 NSTabCharacter
  18. backspace BackSpace 0x33 NSBackspaceCharacter
  19. insert Insert 0x72 Insert
  20. delete Delete 0x75 Delete
  21. left Left 0x7B LeftArrow
  22. right Right 0x7C RightArrow
  23. up Up 0x7E UpArrow
  24. down Down 0x7D DownArrow
  25. page_up Page_Up 0x74 PageUp
  26. page_down Page_Down 0x79 PageDown
  27. home Home 0x73 Home
  28. end End 0x77 End
  29. caps_lock Caps_Lock 0x39 -
  30. scroll_lock Scroll_Lock - ScrollLock
  31. num_lock Num_Lock 0x47 ClearLine
  32. print_screen Print - PrintScreen
  33. pause Pause - Pause
  34. menu Menu 0x6E Menu
  35. f1 F1 0x7A F1
  36. f2 F2 0x78 F2
  37. f3 F3 0x63 F3
  38. f4 F4 0x76 F4
  39. f5 F5 0x60 F5
  40. f6 F6 0x61 F6
  41. f7 F7 0x62 F7
  42. f8 F8 0x64 F8
  43. f9 F9 0x65 F9
  44. f10 F10 0x6D F10
  45. f11 F11 0x67 F11
  46. f12 F12 0x6F F12
  47. f13 F13 0x69 F13
  48. f14 F14 0x6B F14
  49. f15 F15 0x71 F15
  50. f16 F16 0x6A F16
  51. f17 F17 0x40 F17
  52. f18 F18 0x4F F18
  53. f19 F19 0x50 F19
  54. f20 F20 0x5A F20
  55. f21 F21 - F21
  56. f22 F22 - F22
  57. f23 F23 - F23
  58. f24 F24 - F24
  59. f25 F25 - F25
  60. f26 F26 - F26
  61. f27 F27 - F27
  62. f28 F28 - F28
  63. f29 F29 - F29
  64. f30 F30 - F30
  65. f31 F31 - F31
  66. f32 F32 - F32
  67. f33 F33 - F33
  68. f34 F34 - F34
  69. f35 F35 - F35
  70. kp_0 KP_0 0x52 -
  71. kp_1 KP_1 0x53 -
  72. kp_2 KP_2 0x54 -
  73. kp_3 KP_3 0x55 -
  74. kp_4 KP_4 0x56 -
  75. kp_5 KP_5 0x57 -
  76. kp_6 KP_6 0x58 -
  77. kp_7 KP_7 0x59 -
  78. kp_8 KP_8 0x5B -
  79. kp_9 KP_9 0x5C -
  80. kp_decimal KP_Decimal 0x41 -
  81. kp_divide KP_Divide 0x4B -
  82. kp_multiply KP_Multiply 0x43 -
  83. kp_subtract KP_Subtract 0x4E -
  84. kp_add KP_Add 0x45 -
  85. kp_enter KP_Enter 0x4C NSEnterCharacter
  86. kp_equal KP_Equal 0x51 -
  87. kp_separator KP_Separator - -
  88. kp_left KP_Left - -
  89. kp_right KP_Right - -
  90. kp_up KP_Up - -
  91. kp_down KP_Down - -
  92. kp_page_up KP_Page_Up - -
  93. kp_page_down KP_Page_Down - -
  94. kp_home KP_Home - -
  95. kp_end KP_End - -
  96. kp_insert KP_Insert - -
  97. kp_delete KP_Delete - -
  98. kp_begin KP_Begin - -
  99. media_play XF86AudioPlay - -
  100. media_pause XF86AudioPause - -
  101. media_play_pause - - -
  102. media_reverse - - -
  103. media_stop XF86AudioStop - -
  104. media_fast_forward XF86AudioForward - -
  105. media_rewind XF86AudioRewind - -
  106. media_track_next XF86AudioNext - -
  107. media_track_previous XF86AudioPrev - -
  108. media_record XF86AudioRecord - -
  109. lower_volume XF86AudioLowerVolume - -
  110. raise_volume XF86AudioRaiseVolume - -
  111. mute_volume XF86AudioMute - -
  112. left_shift Shift_L 0x38 -
  113. left_control Control_L 0x3B -
  114. left_alt Alt_L 0x3A -
  115. left_super Super_L 0x37 -
  116. left_hyper Hyper_L - -
  117. left_meta Meta_L - -
  118. right_shift Shift_R 0x3C -
  119. right_control Control_R 0x3E -
  120. right_alt Alt_R 0x3D -
  121. right_super Super_R 0x36 -
  122. right_hyper Hyper_R - -
  123. right_meta Meta_R - -
  124. iso_level3_shift ISO_Level3_Shift - -
  125. iso_level5_shift ISO_Level5_Shift - -
  126. ''' # }}}
  127. shift_map = {x[0]: x[1] for x in '`~ 1! 2@ 3# 4$ 5% 6^ 7& 8* 9( 0) -_ =+ [{ ]} \\| ;: \'" ,< .> /?'.split()}
  128. shift_map.update({x: x.upper() for x in string.ascii_lowercase})
  129. functional_encoding_overrides = {
  130. 'insert': 2, 'delete': 3, 'page_up': 5, 'page_down': 6,
  131. 'home': 7, 'end': 8, 'tab': 9, 'f1': 11, 'f2': 12, 'f3': 13, 'enter': 13, 'f4': 14,
  132. 'f5': 15, 'f6': 17, 'f7': 18, 'f8': 19, 'f9': 20, 'f10': 21,
  133. 'f11': 23, 'f12': 24, 'escape': 27, 'backspace': 127
  134. }
  135. different_trailer_functionals = {
  136. 'up': 'A', 'down': 'B', 'right': 'C', 'left': 'D', 'kp_begin': 'E', 'end': 'F', 'home': 'H',
  137. 'f1': 'P', 'f2': 'Q', 'f3': '~', 'f4': 'S', 'enter': 'u', 'tab': 'u',
  138. 'backspace': 'u', 'escape': 'u'
  139. }
  140. macos_ansi_key_codes = { # {{{
  141. 0x1D: ord('0'),
  142. 0x12: ord('1'),
  143. 0x13: ord('2'),
  144. 0x14: ord('3'),
  145. 0x15: ord('4'),
  146. 0x17: ord('5'),
  147. 0x16: ord('6'),
  148. 0x1A: ord('7'),
  149. 0x1C: ord('8'),
  150. 0x19: ord('9'),
  151. 0x00: ord('a'),
  152. 0x0B: ord('b'),
  153. 0x08: ord('c'),
  154. 0x02: ord('d'),
  155. 0x0E: ord('e'),
  156. 0x03: ord('f'),
  157. 0x05: ord('g'),
  158. 0x04: ord('h'),
  159. 0x22: ord('i'),
  160. 0x26: ord('j'),
  161. 0x28: ord('k'),
  162. 0x25: ord('l'),
  163. 0x2E: ord('m'),
  164. 0x2D: ord('n'),
  165. 0x1F: ord('o'),
  166. 0x23: ord('p'),
  167. 0x0C: ord('q'),
  168. 0x0F: ord('r'),
  169. 0x01: ord('s'),
  170. 0x11: ord('t'),
  171. 0x20: ord('u'),
  172. 0x09: ord('v'),
  173. 0x0D: ord('w'),
  174. 0x07: ord('x'),
  175. 0x10: ord('y'),
  176. 0x06: ord('z'),
  177. 0x27: ord('\''),
  178. 0x2A: ord('\\'),
  179. 0x2B: ord(','),
  180. 0x18: ord('='),
  181. 0x32: ord('`'),
  182. 0x21: ord('['),
  183. 0x1B: ord('-'),
  184. 0x2F: ord('.'),
  185. 0x1E: ord(']'),
  186. 0x29: ord(';'),
  187. 0x2C: ord('/'),
  188. 0x31: ord(' '),
  189. } # }}}
  190. functional_key_names: List[str] = []
  191. name_to_code: Dict[str, int] = {}
  192. name_to_xkb: Dict[str, str] = {}
  193. name_to_vk: Dict[str, int] = {}
  194. name_to_macu: Dict[str, str] = {}
  195. start_code = 0xe000
  196. for line in functional_key_defs.splitlines():
  197. line = line.strip()
  198. if not line or line.startswith('#'):
  199. continue
  200. parts = line.split()
  201. name = parts[0]
  202. functional_key_names.append(name)
  203. name_to_code[name] = len(name_to_code) + start_code
  204. if parts[1] != '-':
  205. name_to_xkb[name] = parts[1]
  206. if parts[2] != '-':
  207. name_to_vk[name] = int(parts[2], 16)
  208. if parts[3] != '-':
  209. val = parts[3]
  210. if not val.startswith('NS'):
  211. val = f'NS{val}FunctionKey'
  212. name_to_macu[name] = val
  213. last_code = start_code + len(functional_key_names) - 1
  214. ctrl_mapping = {
  215. ' ': 0, '@': 0, 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7,
  216. 'h': 8, 'i': 9, 'j': 10, 'k': 11, 'l': 12, 'm': 13, 'n': 14, 'o': 15, 'p': 16,
  217. 'q': 17, 'r': 18, 's': 19, 't': 20, 'u': 21, 'v': 22, 'w': 23, 'x': 24,
  218. 'y': 25, 'z': 26, '[': 27, '\\': 28, ']': 29, '^': 30, '~': 30, '/': 31,
  219. '_': 31, '?': 127, '0': 48, '1': 49, '2': 0, '3': 27, '4': 28,
  220. '5': 29, '6': 30, '7': 31, '8': 127, '9': 57
  221. }
  222. def patch_file(path: str, what: str, text: str, start_marker: str = '/* ', end_marker: str = ' */') -> None:
  223. simple_start_q = f'{start_marker}start {what}{end_marker}'
  224. start_q = f'{start_marker}start {what} (auto generated by gen-key-constants.py do not edit){end_marker}'
  225. end_q = f'{start_marker}end {what}{end_marker}'
  226. with open(path, 'r+') as f:
  227. raw = f.read()
  228. try:
  229. start = raw.index(start_q)
  230. except ValueError:
  231. try:
  232. start = raw.index(simple_start_q)
  233. except ValueError:
  234. raise SystemExit(f'Failed to find "{simple_start_q}" in {path}')
  235. try:
  236. end = raw.index(end_q)
  237. except ValueError:
  238. raise SystemExit(f'Failed to find "{end_q}" in {path}')
  239. raw = f'{raw[:start]}{start_q}\n{text}\n{raw[end:]}'
  240. f.seek(0)
  241. f.truncate(0)
  242. f.write(raw)
  243. if path.endswith('.go'):
  244. subprocess.check_call(['go', 'fmt', path])
  245. def serialize_dict(x: Dict[Any, Any]) -> str:
  246. return pformat(x, indent=4).replace('{', '{\n ', 1)
  247. def serialize_go_dict(x: Union[Dict[str, int], Dict[int, str], Dict[int, int]]) -> str:
  248. ans = []
  249. def s(x: Union[int, str]) -> str:
  250. if isinstance(x, int):
  251. return str(x)
  252. return f'"{x}"'
  253. for k, v in x.items():
  254. ans.append(f'{s(k)}: {s(v)}')
  255. return '{' + ', '.join(ans) + '}'
  256. def generate_glfw_header() -> None:
  257. lines = [
  258. 'typedef enum {',
  259. f' GLFW_FKEY_FIRST = 0x{start_code:x}u,',
  260. ]
  261. klines, pyi, names, knames = [], [], [], []
  262. for name, code in name_to_code.items():
  263. lines.append(f' GLFW_FKEY_{name.upper()} = 0x{code:x}u,')
  264. klines.append(f' ADDC(GLFW_FKEY_{name.upper()});')
  265. pyi.append(f'GLFW_FKEY_{name.upper()}: int')
  266. names.append(f' case GLFW_FKEY_{name.upper()}: return "{name.upper()}";')
  267. knames.append(f' case GLFW_FKEY_{name.upper()}: return PyUnicode_FromString("{name}");')
  268. lines.append(f' GLFW_FKEY_LAST = 0x{last_code:x}u')
  269. lines.append('} GLFWFunctionKey;')
  270. patch_file('glfw/glfw3.h', 'functional key names', '\n'.join(lines))
  271. patch_file('kitty/glfw.c', 'glfw functional keys', '\n'.join(klines))
  272. patch_file('kitty/fast_data_types.pyi', 'glfw functional keys', '\n'.join(pyi), start_marker='# ', end_marker='')
  273. patch_file('glfw/input.c', 'functional key names', '\n'.join(names))
  274. patch_file('kitty/glfw.c', 'glfw functional key names', '\n'.join(knames))
  275. def generate_xkb_mapping() -> None:
  276. lines, rlines = [], []
  277. for name, xkb in name_to_xkb.items():
  278. lines.append(f' case XKB_KEY_{xkb}: return GLFW_FKEY_{name.upper()};')
  279. rlines.append(f' case GLFW_FKEY_{name.upper()}: return XKB_KEY_{xkb};')
  280. patch_file('glfw/xkb_glfw.c', 'xkb to glfw', '\n'.join(lines))
  281. patch_file('glfw/xkb_glfw.c', 'glfw to xkb', '\n'.join(rlines))
  282. def generate_functional_table() -> None:
  283. lines = [
  284. '',
  285. '.. csv-table:: Functional key codes',
  286. ' :header: "Name", "CSI", "Name", "CSI"',
  287. ''
  288. ]
  289. line_items = []
  290. enc_lines = []
  291. tilde_trailers = set()
  292. for name, code in name_to_code.items():
  293. if name in functional_encoding_overrides or name in different_trailer_functionals:
  294. trailer = different_trailer_functionals.get(name, '~')
  295. if trailer == '~':
  296. tilde_trailers.add(code)
  297. code = oc = functional_encoding_overrides.get(name, code)
  298. code = code if trailer in '~u' else 1
  299. enc_lines.append((' ' * 8) + f"case GLFW_FKEY_{name.upper()}: S({code}, '{trailer}');")
  300. if code == 1 and name not in ('up', 'down', 'left', 'right'):
  301. trailer += f' or {oc} ~'
  302. else:
  303. trailer = 'u'
  304. line_items.append(name.upper())
  305. line_items.append(f'``{code}\xa0{trailer}``')
  306. for li in chunks(line_items, 4):
  307. lines.append(' ' + ', '.join(f'"{x}"' for x in li))
  308. lines.append('')
  309. patch_file('docs/keyboard-protocol.rst', 'functional key table', '\n'.join(lines), start_marker='.. ', end_marker='')
  310. patch_file('kitty/key_encoding.c', 'special numbers', '\n'.join(enc_lines))
  311. code_to_name = {v: k.upper() for k, v in name_to_code.items()}
  312. csi_map = {v: name_to_code[k] for k, v in functional_encoding_overrides.items()}
  313. letter_trailer_codes: Dict[str, int] = {
  314. v: functional_encoding_overrides.get(k, name_to_code.get(k, 0))
  315. for k, v in different_trailer_functionals.items() if v in 'ABCDEHFPQRSZ'}
  316. text = f'functional_key_number_to_name_map = {serialize_dict(code_to_name)}'
  317. text += f'\ncsi_number_to_functional_number_map = {serialize_dict(csi_map)}'
  318. text += f'\nletter_trailer_to_csi_number_map = {letter_trailer_codes!r}'
  319. text += f'\ntilde_trailers = {tilde_trailers!r}'
  320. patch_file('kitty/key_encoding.py', 'csi mapping', text, start_marker='# ', end_marker='')
  321. text = f'var functional_key_number_to_name_map = map[int]string{serialize_go_dict(code_to_name)}\n'
  322. text += f'\nvar csi_number_to_functional_number_map = map[int]int{serialize_go_dict(csi_map)}\n'
  323. text += f'\nvar letter_trailer_to_csi_number_map = map[string]int{serialize_go_dict(letter_trailer_codes)}\n'
  324. tt = ', '.join(f'{x}: true' for x in tilde_trailers)
  325. text += '\nvar tilde_trailers = map[int]bool{' + f'{tt}' + '}\n'
  326. patch_file('tools/tui/loop/key-encoding.go', 'csi mapping', text, start_marker='// ', end_marker='')
  327. def generate_legacy_text_key_maps() -> None:
  328. tests = []
  329. tp = ' ' * 8
  330. shift, alt, ctrl = 1, 2, 4
  331. def simple(c: str) -> None:
  332. shifted = shift_map.get(c, c)
  333. ctrled = chr(ctrl_mapping.get(c, ord(c)))
  334. call = f'enc(ord({c!r}), shifted_key=ord({shifted!r})'
  335. for m in range(16):
  336. if m == 0:
  337. tests.append(f'{tp}ae({call}), {c!r})')
  338. elif m == shift:
  339. tests.append(f'{tp}ae({call}, mods=shift), {shifted!r})')
  340. elif m == alt:
  341. tests.append(f'{tp}ae({call}, mods=alt), "\\x1b" + {c!r})')
  342. elif m == ctrl:
  343. tests.append(f'{tp}ae({call}, mods=ctrl), {ctrled!r})')
  344. elif m == shift | alt:
  345. tests.append(f'{tp}ae({call}, mods=shift | alt), "\\x1b" + {shifted!r})')
  346. elif m == ctrl | alt:
  347. tests.append(f'{tp}ae({call}, mods=ctrl | alt), "\\x1b" + {ctrled!r})')
  348. for k in shift_map:
  349. simple(k)
  350. patch_file('kitty_tests/keys.py', 'legacy letter tests', '\n'.join(tests), start_marker='# ', end_marker='')
  351. def chunks(lst: List[Any], n: int) -> Any:
  352. """Yield successive n-sized chunks from lst."""
  353. for i in range(0, len(lst), n):
  354. yield lst[i:i + n]
  355. def generate_ctrl_mapping() -> None:
  356. lines = [
  357. '.. csv-table:: Emitted bytes when :kbd:`ctrl` is held down and a key is pressed',
  358. ' :header: "Key", "Byte", "Key", "Byte", "Key", "Byte"',
  359. ''
  360. ]
  361. items = []
  362. mi = []
  363. for k in sorted(ctrl_mapping):
  364. prefix = '\\' if k == '\\' else ('SPC' if k == ' ' else '')
  365. items.append(prefix + k)
  366. val = str(ctrl_mapping[k])
  367. items.append(val)
  368. if k in "\\'":
  369. k = f'\\{k}'
  370. mi.append(f" case '{k}': return {val};")
  371. for line_items in chunks(items, 6):
  372. lines.append(' ' + ', '.join(f'"{x}"' for x in line_items))
  373. lines.append('')
  374. patch_file('docs/keyboard-protocol.rst', 'ctrl mapping', '\n'.join(lines), start_marker='.. ', end_marker='')
  375. patch_file('kitty/key_encoding.c', 'ctrl mapping', '\n'.join(mi))
  376. def generate_macos_mapping() -> None:
  377. lines = []
  378. for k in sorted(macos_ansi_key_codes):
  379. v = macos_ansi_key_codes[k]
  380. lines.append(f' case 0x{k:x}: return 0x{v:x};')
  381. patch_file('glfw/cocoa_window.m', 'vk to unicode', '\n'.join(lines))
  382. lines = []
  383. for name, vk in name_to_vk.items():
  384. lines.append(f' case 0x{vk:x}: return GLFW_FKEY_{name.upper()};')
  385. patch_file('glfw/cocoa_window.m', 'vk to functional', '\n'.join(lines))
  386. lines = []
  387. for name, mac in name_to_macu.items():
  388. lines.append(f' case {mac}: return GLFW_FKEY_{name.upper()};')
  389. patch_file('glfw/cocoa_window.m', 'macu to functional', '\n'.join(lines))
  390. lines = []
  391. for name, mac in name_to_macu.items():
  392. lines.append(f' case GLFW_FKEY_{name.upper()}: return {mac};')
  393. patch_file('glfw/cocoa_window.m', 'functional to macu', '\n'.join(lines))
  394. def main(args: List[str]=sys.argv) -> None:
  395. generate_glfw_header()
  396. generate_xkb_mapping()
  397. generate_functional_table()
  398. generate_legacy_text_key_maps()
  399. generate_ctrl_mapping()
  400. generate_macos_mapping()
  401. if __name__ == '__main__':
  402. import runpy
  403. m = runpy.run_path(os.path.dirname(os.path.abspath(__file__)))
  404. m['main']([sys.executable, 'key-constants'])