logo

qmk_firmware

custom branch of QMK firmware git clone https://anongit.hacktivis.me/git/qmk_firmware.git

compile_keymap.py (20594B)


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