compile_keymap.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """Compiler for keymap.c files
  4. This scrip will generate a keymap.c file from a simple
  5. markdown file with a specific layout.
  6. Usage:
  7. python compile_keymap.py INPUT_PATH [OUTPUT_PATH]
  8. """
  9. from __future__ import division
  10. from __future__ import print_function
  11. from __future__ import absolute_import
  12. from __future__ import unicode_literals
  13. import os
  14. import io
  15. import re
  16. import sys
  17. import json
  18. import unicodedata
  19. import collections
  20. PY2 = sys.version_info.major == 2
  21. if PY2:
  22. chr = unichr
  23. BASEPATH = os.path.abspath(os.path.join(
  24. os.path.dirname(__file__), "..", ".."
  25. ))
  26. KEYBOARD_LAYOUTS = {
  27. # These map positions in the parsed layout to
  28. # positions in the KEYMAP MATRIX
  29. 'ergodox_ez': [
  30. [ 0, 1, 2, 3, 4, 5, 6], [38, 39, 40, 41, 42, 43, 44],
  31. [ 7, 8, 9, 10, 11, 12, 13], [45, 46, 47, 48, 49, 50, 51],
  32. [14, 15, 16, 17, 18, 19 ], [ 52, 53, 54, 55, 56, 57],
  33. [20, 21, 22, 23, 24, 25, 26], [58, 59, 60, 61, 62, 63, 64],
  34. [27, 28, 29, 30, 31 ], [ 65, 66, 67, 68, 69],
  35. [ 32, 33], [70, 71 ],
  36. [ 34], [72 ],
  37. [ 35, 36, 37], [73, 74, 75 ],
  38. ]
  39. }
  40. BLANK_LAYOUTS = [
  41. # Compact Layout
  42. """
  43. .------------------------------------.------------------------------------.
  44. | | | | | | | | | | | | | | |
  45. !-----+----+----+----+----+----------!-----+----+----+----+----+----+-----!
  46. | | | | | | | | | | | | | | |
  47. !-----+----+----+----x----x----! ! !----x----x----+----+----+-----!
  48. | | | | | | |-----!-----! | | | | | |
  49. !-----+----+----+----x----x----! ! !----x----x----+----+----+-----!
  50. | | | | | | | | | | | | | | |
  51. '-----+----+----+----+----+----------'----------+----+----+----+----+-----'
  52. | | | | | | ! | | | | |
  53. '------------------------' '------------------------'
  54. .-----------. .-----------.
  55. | | | ! | |
  56. .-----+-----+-----! !-----+-----+-----.
  57. ! ! | | ! | ! !
  58. ! ! !-----! !-----! ! !
  59. | | | | ! | | |
  60. '-----------------' '-----------------'
  61. """,
  62. # Wide Layout
  63. """
  64. .--------------------------------------------. .--------------------------------------------.
  65. | | | | | | | | ! | | | | | | |
  66. !------+-----+-----+-----+-----+-------------! !-------+-----+-----+-----+-----+-----+------!
  67. | | | | | | | | ! | | | | | | |
  68. !------+-----+-----+-----x-----x-----! ! ! !-----x-----x-----+-----+-----+------!
  69. | | | | | | |-------! !-------! | | | | | |
  70. !------+-----+-----+-----x-----x-----! ! ! !-----x-----x-----+-----+-----+------!
  71. | | | | | | | | ! | | | | | | |
  72. '------+-----+-----+-----+-----+-------------' '-------------+-----+-----+-----+-----+------'
  73. | | | | | | ! | | | | |
  74. '-----------------------------' '-----------------------------'
  75. .---------------. .---------------.
  76. | | | ! | |
  77. .-------+-------+-------! !-------+-------+-------.
  78. ! ! | | ! | ! !
  79. ! ! !-------! !-------! ! !
  80. | | | | ! | | |
  81. '-----------------------' '-----------------------'
  82. """,
  83. ]
  84. DEFAULT_CONFIG = {
  85. "includes_basedir": "quantum/",
  86. "keymaps_includes": [
  87. "keymap_common.h",
  88. ],
  89. 'filler': "-+.':x",
  90. 'separator': "|",
  91. 'default_key_prefix': ["KC_"],
  92. }
  93. SECTIONS = [
  94. 'layout_config',
  95. 'layers',
  96. ]
  97. # Markdown Parsing
  98. def loads(raw_data):
  99. ONELINE_COMMENT_RE = re.compile(r"""
  100. ^ # comment must be at the start of the line
  101. \s* # arbitrary whitespace
  102. // # start of the comment
  103. (.*) # the comment
  104. $ # until the end of line
  105. """, re.MULTILINE | re.VERBOSE
  106. )
  107. INLINE_COMMENT_RE = re.compile(r"""
  108. (?:[\,\"\[\]\{\}\d]) # anythig that might end a expression
  109. \s+ # comment must be preceded by whitespace
  110. // # start of the comment
  111. \s # and succeded by whitespace
  112. ([^\"\]\}\{\[]*) # the comment (except things which might be json)
  113. $ # until the end of line
  114. """, re.MULTILINE | re.VERBOSE
  115. )
  116. TRAILING_COMMA_RE = re.compile(r"""
  117. , # the comma
  118. \s* # arbitrary whitespace (including newlines)
  119. ([\]\}]) # end of an array or object
  120. """, re.MULTILINE | re.VERBOSE
  121. )
  122. if isinstance(raw_data, bytes):
  123. raw_data = raw_data.decode('utf-8')
  124. raw_data = ONELINE_COMMENT_RE.sub(r"", raw_data)
  125. raw_data = INLINE_COMMENT_RE.sub(r"\1", raw_data)
  126. raw_data = TRAILING_COMMA_RE.sub(r"\1", raw_data)
  127. return json.loads(raw_data)
  128. def parse_config(path):
  129. def reset_section():
  130. section.update({
  131. 'name': section.get('name', ""),
  132. 'sub_name': "",
  133. 'start_line': -1,
  134. 'end_line': -1,
  135. 'code_lines': [],
  136. })
  137. def start_section(line_index, line):
  138. end_section()
  139. if line.startswith("# "):
  140. name = line[2:]
  141. elif line.startswith("## "):
  142. name = line[3:]
  143. name = name.strip().replace(" ", "_").lower()
  144. if name in SECTIONS:
  145. section['name'] = name
  146. else:
  147. section['sub_name'] = name
  148. section['start_line'] = line_index
  149. def end_section():
  150. if section['start_line'] >= 0:
  151. if section['name'] == 'layout_config':
  152. config.update(loads("\n".join(
  153. section['code_lines']
  154. )))
  155. elif section['sub_name'].startswith('layer'):
  156. layer_name = section['sub_name']
  157. config['layer_lines'][layer_name] = section['code_lines']
  158. reset_section()
  159. def amend_section(line_index, line):
  160. section['end_line'] = line_index
  161. section['code_lines'].append(line)
  162. config = DEFAULT_CONFIG.copy()
  163. config.update({
  164. 'layer_lines': collections.OrderedDict(),
  165. 'macro_ids': {'UM'},
  166. 'unicode_macros': {},
  167. })
  168. section = {}
  169. reset_section()
  170. with io.open(path, encoding="utf-8") as fh:
  171. for i, line in enumerate(fh):
  172. if line.startswith("#"):
  173. start_section(i, line)
  174. elif line.startswith(" "):
  175. amend_section(i, line[4:])
  176. else:
  177. # TODO: maybe parse description
  178. pass
  179. end_section()
  180. return config
  181. # header file parsing
  182. IF0_RE = re.compile(r"""
  183. ^
  184. #if 0
  185. $.*?
  186. #endif
  187. """, re.MULTILINE | re.DOTALL | re.VERBOSE
  188. )
  189. COMMENT_RE = re.compile(r"""
  190. /\*
  191. .*?
  192. \*/"
  193. """, re.MULTILINE | re.DOTALL | re.VERBOSE
  194. )
  195. def read_header_file(path):
  196. with io.open(path, encoding="utf-8") as fh:
  197. data = fh.read()
  198. data, _ = COMMENT_RE.subn("", data)
  199. data, _ = IF0_RE.subn("", data)
  200. return data
  201. def regex_partial(re_str_fmt, flags=re.MULTILINE | re.DOTALL | re.VERBOSE):
  202. def partial(*args, **kwargs):
  203. re_str = re_str_fmt.format(*args, **kwargs)
  204. return re.compile(re_str, flags)
  205. return partial
  206. KEYDEF_REP = regex_partial(r"""
  207. #define
  208. \s
  209. (
  210. (?:{}) # the prefixes
  211. (?:\w+) # the key name
  212. ) # capture group end
  213. """
  214. )
  215. ENUM_RE = re.compile(r"""
  216. (
  217. enum
  218. \s\w+\s
  219. \{
  220. .*? # the enum content
  221. \}
  222. ;
  223. ) # capture group end
  224. """, re.MULTILINE | re.DOTALL | re.VERBOSE
  225. )
  226. ENUM_KEY_REP = regex_partial(r"""
  227. (
  228. {} # the prefixes
  229. \w+ # the key name
  230. ) # capture group end
  231. """
  232. )
  233. def parse_keydefs(config, data):
  234. prefix_options = "|".join(config['key_prefixes'])
  235. keydef_re = KEYDEF_REP(prefix_options)
  236. enum_key_re = ENUM_KEY_REP(prefix_options)
  237. for match in keydef_re.finditer(data):
  238. yield match.groups()[0]
  239. for enum_match in ENUM_RE.finditer(data):
  240. enum = enum_match.groups()[0]
  241. for key_match in enum_key_re.finditer(enum):
  242. yield key_match.groups()[0]
  243. def parse_valid_keys(config):
  244. valid_keycodes = set()
  245. paths = [
  246. os.path.join(BASEPATH, "tmk_core", "common", "keycode.h")
  247. ] + [
  248. os.path.join(
  249. BASEPATH, config['includes_dir'], include_path
  250. ) for include_path in config['keymaps_includes']
  251. ]
  252. for path in paths:
  253. path = path.replace("/", os.sep)
  254. # the config always uses forward slashe
  255. if os.path.exists(path):
  256. header_data = read_header_file(path)
  257. valid_keycodes.update(
  258. parse_keydefs(config, header_data)
  259. )
  260. return valid_keycodes
  261. # Keymap Parsing
  262. def iter_raw_codes(layer_lines, filler, separator):
  263. filler_re = re.compile("[" + filler + " ]")
  264. for line in layer_lines:
  265. line, _ = filler_re.subn("", line.strip())
  266. if not line:
  267. continue
  268. codes = line.split(separator)
  269. for code in codes[1:-1]:
  270. yield code
  271. def iter_indexed_codes(raw_codes, key_indexes):
  272. key_rows = {}
  273. key_indexes_flat = []
  274. for row_index, key_indexes in enumerate(key_indexes):
  275. for key_index in key_indexes:
  276. key_rows[key_index] = row_index
  277. key_indexes_flat.extend(key_indexes)
  278. assert len(raw_codes) == len(key_indexes_flat)
  279. for raw_code, key_index in zip(raw_codes, key_indexes_flat):
  280. # we keep track of the row mostly for layout purposes
  281. yield raw_code, key_index, key_rows[key_index]
  282. LAYER_CHANGE_RE = re.compile(r"""
  283. (DF|TG|MO)\(\d+\)
  284. """, re.VERBOSE)
  285. MACRO_RE = re.compile(r"""
  286. M\(\w+\)
  287. """, re.VERBOSE)
  288. UNICODE_RE = re.compile(r"""
  289. U[0-9A-F]{4}
  290. """, re.VERBOSE)
  291. NON_CODE = re.compile(r"""
  292. ^[^A-Z0-9_]$
  293. """, re.VERBOSE)
  294. def parse_uni_code(raw_code):
  295. macro_id = "UC_" + (
  296. unicodedata.name(raw_code)
  297. .replace(" ", "_")
  298. .replace("-", "_")
  299. )
  300. code = "M({})".format(macro_id)
  301. uc_hex = "{:04X}".format(ord(raw_code))
  302. return code, macro_id, uc_hex
  303. def parse_key_code(raw_code, key_prefixes, valid_keycodes):
  304. if raw_code in valid_keycodes:
  305. return raw_code
  306. for prefix in key_prefixes:
  307. code = prefix + raw_code
  308. if code in valid_keycodes:
  309. return code
  310. def parse_code(raw_code, key_prefixes, valid_keycodes):
  311. if not raw_code:
  312. return 'KC_TRNS', None, None
  313. if LAYER_CHANGE_RE.match(raw_code):
  314. return raw_code, None, None
  315. if MACRO_RE.match(raw_code):
  316. code = macro_id = raw_code[2:-1]
  317. return code, macro_id, None
  318. if UNICODE_RE.match(raw_code):
  319. hex_code = raw_code[1:]
  320. return parse_uni_code(chr(int(hex_code, 16)))
  321. if NON_CODE.match(raw_code):
  322. return parse_uni_code(raw_code)
  323. code = parse_key_code(raw_code, key_prefixes, valid_keycodes)
  324. return code, None, None
  325. def parse_keymap(config, key_indexes, layer_lines, valid_keycodes):
  326. keymap = {}
  327. raw_codes = list(iter_raw_codes(
  328. layer_lines, config['filler'], config['separator']
  329. ))
  330. indexed_codes = iter_indexed_codes(raw_codes, key_indexes)
  331. for raw_code, key_index, row_index in indexed_codes:
  332. code, macro_id, uc_hex = parse_code(
  333. raw_code, config['key_prefixes'], valid_keycodes
  334. )
  335. if macro_id:
  336. config['macro_ids'].add(macro_id)
  337. if uc_hex:
  338. config['unicode_macros'][macro_id] = uc_hex
  339. keymap[key_index] = (code, row_index)
  340. return keymap
  341. def parse_keymaps(config, valid_keycodes):
  342. keymaps = collections.OrderedDict()
  343. key_indexes = config.get(
  344. 'key_indexes', KEYBOARD_LAYOUTS[config['layout']]
  345. )
  346. # TODO: maybe validate key_indexes
  347. for layer_name, layer_lines, in config['layer_lines'].items():
  348. keymaps[layer_name] = parse_keymap(
  349. config, key_indexes, layer_lines, valid_keycodes
  350. )
  351. return keymaps
  352. # keymap.c output
  353. USERCODE = """
  354. // Runs just one time when the keyboard initializes.
  355. void matrix_init_user(void) {
  356. };
  357. // Runs constantly in the background, in a loop.
  358. void matrix_scan_user(void) {
  359. uint8_t layer = biton32(layer_state);
  360. ergodox_board_led_off();
  361. ergodox_right_led_1_off();
  362. ergodox_right_led_2_off();
  363. ergodox_right_led_3_off();
  364. switch (layer) {
  365. case L1:
  366. ergodox_right_led_1_on();
  367. break;
  368. case L2:
  369. ergodox_right_led_2_on();
  370. break;
  371. case L3:
  372. ergodox_right_led_3_on();
  373. break;
  374. case L4:
  375. ergodox_right_led_1_on();
  376. ergodox_right_led_2_on();
  377. break;
  378. case L5:
  379. ergodox_right_led_1_on();
  380. ergodox_right_led_3_on();
  381. break;
  382. // case L6:
  383. // ergodox_right_led_2_on();
  384. // ergodox_right_led_3_on();
  385. // break;
  386. // case L7:
  387. // ergodox_right_led_1_on();
  388. // ergodox_right_led_2_on();
  389. // ergodox_right_led_3_on();
  390. // break;
  391. default:
  392. ergodox_board_led_off();
  393. break;
  394. }
  395. };
  396. """
  397. MACROCODE = """
  398. #define UC_MODE_WIN 0
  399. #define UC_MODE_LINUX 1
  400. static uint16_t unicode_mode = UC_MODE_WIN;
  401. const macro_t *action_get_macro(keyrecord_t *record, uint8_t id, uint8_t opt) {{
  402. if (!record->event.pressed) {{
  403. return MACRO_NONE;
  404. }}
  405. // MACRODOWN only works in this function
  406. switch(id) {{
  407. case UM:
  408. unicode_mode = (unicode_mode + 1) % 2;
  409. break;
  410. {macro_cases}
  411. default:
  412. break;
  413. }}
  414. if (unicode_mode == UC_MODE_WIN) {{
  415. switch(id) {{
  416. {win_macro_cases}
  417. default:
  418. break;
  419. }}
  420. }} else if (unicode_mode == UC_MODE_LINUX) {{
  421. switch(id) {{
  422. {linux_macro_cases}
  423. default:
  424. break;
  425. }}
  426. }}
  427. return MACRO_NONE;
  428. }};
  429. """
  430. WIN_UNICODE_MACRO_TEMPLATE = """
  431. case {0}:
  432. return MACRODOWN(
  433. D(LALT), T(KP_PLUS), {1}, U(LALT), END
  434. );
  435. """
  436. LINUX_UNICODE_MACRO_TEMPLATE = """
  437. case {0}:
  438. return MACRODOWN(
  439. D(LCTRL), D(LSHIFT), T(U), U(LCTRL), U(LSHIFT), {1}, T(KP_ENTER), END
  440. );
  441. """
  442. def macro_cases(config, mode):
  443. if mode == 'win':
  444. template = WIN_UNICODE_MACRO_TEMPLATE
  445. elif mode == 'linux':
  446. template = LINUX_UNICODE_MACRO_TEMPLATE
  447. else:
  448. raise ValueError("Invalid mode: ", mode)
  449. template = template.strip()
  450. for macro_id, uc_hex in config['unicode_macros'].items():
  451. unimacro_keys = ", ".join(
  452. "T({})".format(
  453. "KP_" + digit if digit.isdigit() else digit
  454. ) for digit in uc_hex
  455. )
  456. yield template.format(macro_id, unimacro_keys)
  457. def iter_keymap_lines(keymap):
  458. prev_row_index = None
  459. for key_index in sorted(keymap):
  460. code, row_index = keymap[key_index]
  461. if row_index != prev_row_index:
  462. yield "\n"
  463. yield " {}".format(code)
  464. if key_index < len(keymap) - 1:
  465. yield ","
  466. prev_row_index = row_index
  467. def iter_keymap_parts(config, keymaps):
  468. # includes
  469. for include_path in config['keymaps_includes']:
  470. yield '#include "{}"\n'.format(include_path)
  471. yield "\n"
  472. # definitions
  473. for i, macro_id in enumerate(sorted(config['macro_ids'])):
  474. yield "#define {} {}\n".format(macro_id, i)
  475. yield "\n"
  476. for i, layer_name in enumerate(config['layer_lines']):
  477. yield '#define L{0:<3} {0:<5} // {1}\n'.format(i, layer_name)
  478. yield "\n"
  479. # keymaps
  480. yield "const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\n"
  481. for i, layer_name in enumerate(config['layer_lines']):
  482. # comment
  483. layer_lines = config['layer_lines'][layer_name]
  484. prefixed_lines = " * " + " * ".join(layer_lines)
  485. yield "/*\n{}*/\n".format(prefixed_lines)
  486. # keymap codes
  487. keymap = keymaps[layer_name]
  488. keymap_lines = "".join(iter_keymap_lines(keymap))
  489. yield "[L{0}] = KEYMAP({1}\n),\n".format(i, keymap_lines)
  490. yield "};\n\n"
  491. # no idea what this is for
  492. yield "const uint16_t PROGMEM fn_actions[] = {};\n"
  493. # macros
  494. yield MACROCODE.format(
  495. macro_cases="",
  496. win_macro_cases="\n".join(macro_cases(config, mode='win')),
  497. linux_macro_cases="\n".join(macro_cases(config, mode='linux')),
  498. )
  499. # TODO: dynamically create blinking lights
  500. yield USERCODE
  501. def main(argv=sys.argv[1:]):
  502. if not argv or '-h' in argv or '--help' in argv:
  503. print(__doc__)
  504. return 0
  505. in_path = os.path.abspath(argv[0])
  506. if not os.path.exists(in_path):
  507. print("No such file '{}'".format(in_path))
  508. return 1
  509. if len(argv) > 1:
  510. out_path = os.path.abspath(argv[1])
  511. else:
  512. dirname = os.path.dirname(in_path)
  513. out_path = os.path.join(dirname, "keymap.c")
  514. config = parse_config(in_path)
  515. valid_keys = parse_valid_keys(config)
  516. keymaps = parse_keymaps(config, valid_keys)
  517. with io.open(out_path, mode="w", encoding="utf-8") as fh:
  518. for part in iter_keymap_parts(config, keymaps):
  519. fh.write(part)
  520. if __name__ == '__main__':
  521. sys.exit(main())