compile_keymap.py 20 KB

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