You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

702 lines
20 KiB

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