2 # -*- coding: utf-8 -*-
3 """Compiler for keymap.c files
5 This scrip will generate a keymap.c file from a simple
6 markdown file with a specific layout.
9 python compile_keymap.py INPUT_PATH [OUTPUT_PATH]
11 from __future__ import division
12 from __future__ import print_function
13 from __future__ import absolute_import
14 from __future__ import unicode_literals
24 PY2 = sys.version_info.major == 2
30 BASEPATH = os.path.abspath(os.path.join(
31 os.path.dirname(__file__), "..", ".."
36 # These map positions in the parsed layout to
37 # positions in the KEYMAP MATRIX
39 [ 0, 1, 2, 3, 4, 5, 6], [38, 39, 40, 41, 42, 43, 44],
40 [ 7, 8, 9, 10, 11, 12, 13], [45, 46, 47, 48, 49, 50, 51],
41 [14, 15, 16, 17, 18, 19 ], [ 52, 53, 54, 55, 56, 57],
42 [20, 21, 22, 23, 24, 25, 26], [58, 59, 60, 61, 62, 63, 64],
43 [27, 28, 29, 30, 31 ], [ 65, 66, 67, 68, 69],
46 [ 35, 36, 37], [73, 74, 75 ],
54 .------------------------------------.------------------------------------.
55 | | | | | | | | | | | | | | |
56 !-----+----+----+----+----+----------!-----+----+----+----+----+----+-----!
57 | | | | | | | | | | | | | | |
58 !-----+----+----+----x----x----! ! !----x----x----+----+----+-----!
59 | | | | | | |-----!-----! | | | | | |
60 !-----+----+----+----x----x----! ! !----x----x----+----+----+-----!
61 | | | | | | | | | | | | | | |
62 '-----+----+----+----+----+----------'----------+----+----+----+----+-----'
63 | | | | | | ! | | | | |
64 '------------------------' '------------------------'
65 .-----------. .-----------.
67 .-----+-----+-----! !-----+-----+-----.
69 ! ! !-----! !-----! ! !
71 '-----------------' '-----------------'
76 .--------------------------------------------. .--------------------------------------------.
77 | | | | | | | | ! | | | | | | |
78 !------+-----+-----+-----+-----+-------------! !-------+-----+-----+-----+-----+-----+------!
79 | | | | | | | | ! | | | | | | |
80 !------+-----+-----+-----x-----x-----! ! ! !-----x-----x-----+-----+-----+------!
81 | | | | | | |-------! !-------! | | | | | |
82 !------+-----+-----+-----x-----x-----! ! ! !-----x-----x-----+-----+-----+------!
83 | | | | | | | | ! | | | | | | |
84 '------+-----+-----+-----+-----+-------------' '-------------+-----+-----+-----+-----+------'
85 | | | | | | ! | | | | |
86 '-----------------------------' '-----------------------------'
87 .---------------. .---------------.
89 .-------+-------+-------! !-------+-------+-------.
91 ! ! !-------! !-------! ! !
93 '-----------------------' '-----------------------'
99 "includes_basedir": "quantum/",
100 "keymaps_includes": [
105 'default_key_prefix': ["KC_"],
118 ONELINE_COMMENT_RE = re.compile(r"""
119 ^ # comment must be at the start of the line
120 \s* # arbitrary whitespace
121 // # start of the comment
123 $ # until the end of line
124 """, re.MULTILINE | re.VERBOSE
127 INLINE_COMMENT_RE = re.compile(r"""
128 (?:[\,\"\[\]\{\}\d]) # anythig that might end a expression
129 \s+ # comment must be preceded by whitespace
130 // # start of the comment
131 \s # and succeded by whitespace
132 ([^\"\]\}\{\[]*) # the comment (except things which might be json)
133 $ # until the end of line
134 """, re.MULTILINE | re.VERBOSE
137 TRAILING_COMMA_RE = re.compile(r"""
139 \s* # arbitrary whitespace (including newlines)
140 ([\]\}]) # end of an array or object
141 """, re.MULTILINE | re.VERBOSE
144 if isinstance(raw_data, bytes):
145 raw_data = raw_data.decode('utf-8')
147 raw_data = ONELINE_COMMENT_RE.sub(r"", raw_data)
148 raw_data = INLINE_COMMENT_RE.sub(r"\1", raw_data)
149 raw_data = TRAILING_COMMA_RE.sub(r"\1", raw_data)
150 return json.loads(raw_data)
153 def parse_config(path):
156 'name': section.get('name', ""),
163 def start_section(line_index, line):
165 if line.startswith("# "):
167 elif line.startswith("## "):
170 name = name.strip().replace(" ", "_").lower()
172 section['name'] = name
174 section['sub_name'] = name
175 section['start_line'] = line_index
178 if section['start_line'] >= 0:
179 if section['name'] == 'layout_config':
180 config.update(loads("\n".join(
181 section['code_lines']
183 elif section['sub_name'].startswith('layer'):
184 layer_name = section['sub_name']
185 config['layer_lines'][layer_name] = section['code_lines']
189 def amend_section(line_index, line):
190 section['end_line'] = line_index
191 section['code_lines'].append(line)
193 config = DEFAULT_CONFIG.copy()
195 'layer_lines': collections.OrderedDict(),
197 'unicode_macros': {},
203 with io.open(path, encoding="utf-8") as fh:
204 for i, line in enumerate(fh):
205 if line.startswith("#"):
206 start_section(i, line)
207 elif line.startswith(" "):
208 amend_section(i, line[4:])
210 # TODO: maybe parse description
216 # header file parsing
218 IF0_RE = re.compile(r"""
223 """, re.MULTILINE | re.DOTALL | re.VERBOSE
227 COMMENT_RE = re.compile(r"""
231 """, re.MULTILINE | re.DOTALL | re.VERBOSE
234 def read_header_file(path):
235 with io.open(path, encoding="utf-8") as fh:
237 data, _ = COMMENT_RE.subn("", data)
238 data, _ = IF0_RE.subn("", data)
242 def regex_partial(re_str_fmt, flags=re.MULTILINE | re.DOTALL | re.VERBOSE):
243 def partial(*args, **kwargs):
244 re_str = re_str_fmt.format(*args, **kwargs)
245 return re.compile(re_str, flags)
249 KEYDEF_REP = regex_partial(r"""
253 (?:{}) # the prefixes
254 (?:\w+) # the key name
255 ) # capture group end
260 ENUM_RE = re.compile(r"""
265 .*? # the enum content
268 ) # capture group end
269 """, re.MULTILINE | re.DOTALL | re.VERBOSE
273 ENUM_KEY_REP = regex_partial(r"""
277 ) # capture group end
281 def parse_keydefs(config, data):
282 prefix_options = "|".join(config['key_prefixes'])
283 keydef_re = KEYDEF_REP(prefix_options)
284 enum_key_re = ENUM_KEY_REP(prefix_options)
285 for match in keydef_re.finditer(data):
286 yield match.groups()[0]
288 for enum_match in ENUM_RE.finditer(data):
289 enum = enum_match.groups()[0]
290 for key_match in enum_key_re.finditer(enum):
291 yield key_match.groups()[0]
294 def parse_valid_keys(config):
295 valid_keycodes = set()
297 os.path.join(BASEPATH, "tmk_core", "common", "keycode.h")
300 BASEPATH, config['includes_dir'], include_path
301 ) for include_path in config['keymaps_includes']
305 path = path.replace("/", os.sep)
306 # the config always uses forward slashe
307 if os.path.exists(path):
308 header_data = read_header_file(path)
309 valid_keycodes.update(
310 parse_keydefs(config, header_data)
312 return valid_keycodes
316 def iter_raw_codes(layer_lines, filler, separator):
317 filler_re = re.compile("[" + filler + " ]")
318 for line in layer_lines:
319 line, _ = filler_re.subn("", line.strip())
322 codes = line.split(separator)
323 for code in codes[1:-1]:
327 def iter_indexed_codes(raw_codes, key_indexes):
329 key_indexes_flat = []
330 for row_index, key_indexes in enumerate(key_indexes):
331 for key_index in key_indexes:
332 key_rows[key_index] = row_index
333 key_indexes_flat.extend(key_indexes)
334 assert len(raw_codes) == len(key_indexes_flat)
335 for raw_code, key_index in zip(raw_codes, key_indexes_flat):
336 # we keep track of the row mostly for layout purposes
337 yield raw_code, key_index, key_rows[key_index]
340 LAYER_CHANGE_RE = re.compile(r"""
345 MACRO_RE = re.compile(r"""
350 UNICODE_RE = re.compile(r"""
355 NON_CODE = re.compile(r"""
360 def parse_uni_code(raw_code):
362 unicodedata.name(raw_code)
366 code = "M({})".format(macro_id)
367 uc_hex = "{:04X}".format(ord(raw_code))
368 return code, macro_id, uc_hex
371 def parse_key_code(raw_code, key_prefixes, valid_keycodes):
372 if raw_code in valid_keycodes:
375 for prefix in key_prefixes:
376 code = prefix + raw_code
377 if code in valid_keycodes:
381 def parse_code(raw_code, key_prefixes, valid_keycodes):
383 return 'KC_TRNS', None, None
385 if LAYER_CHANGE_RE.match(raw_code):
386 return raw_code, None, None
388 if MACRO_RE.match(raw_code):
389 code = macro_id = raw_code[2:-1]
390 return code, macro_id, None
392 if UNICODE_RE.match(raw_code):
393 hex_code = raw_code[1:]
394 return parse_uni_code(chr(int(hex_code, 16)))
396 if NON_CODE.match(raw_code):
397 return parse_uni_code(raw_code)
399 code = parse_key_code(raw_code, key_prefixes, valid_keycodes)
400 return code, None, None
403 def parse_keymap(config, key_indexes, layer_lines, valid_keycodes):
405 raw_codes = list(iter_raw_codes(
406 layer_lines, config['filler'], config['separator']
408 indexed_codes = iter_indexed_codes(raw_codes, key_indexes)
409 for raw_code, key_index, row_index in indexed_codes:
410 code, macro_id, uc_hex = parse_code(
411 raw_code, config['key_prefixes'], valid_keycodes
414 config['macro_ids'].add(macro_id)
416 config['unicode_macros'][macro_id] = uc_hex
417 keymap[key_index] = (code, row_index)
421 def parse_keymaps(config, valid_keycodes):
422 keymaps = collections.OrderedDict()
423 key_indexes = config.get(
424 'key_indexes', KEYBOARD_LAYOUTS[config['layout']]
426 # TODO: maybe validate key_indexes
428 for layer_name, layer_lines, in config['layer_lines'].items():
429 keymaps[layer_name] = parse_keymap(
430 config, key_indexes, layer_lines, valid_keycodes
437 // Runs just one time when the keyboard initializes.
438 void matrix_init_user(void) {
442 // Runs constantly in the background, in a loop.
443 void matrix_scan_user(void) {
444 uint8_t layer = biton32(layer_state);
446 ergodox_board_led_off();
447 ergodox_right_led_1_off();
448 ergodox_right_led_2_off();
449 ergodox_right_led_3_off();
452 ergodox_right_led_1_on();
455 ergodox_right_led_2_on();
458 ergodox_right_led_3_on();
461 ergodox_right_led_1_on();
462 ergodox_right_led_2_on();
465 ergodox_right_led_1_on();
466 ergodox_right_led_3_on();
469 // ergodox_right_led_2_on();
470 // ergodox_right_led_3_on();
473 // ergodox_right_led_1_on();
474 // ergodox_right_led_2_on();
475 // ergodox_right_led_3_on();
478 ergodox_board_led_off();
485 #define UC_MODE_WIN 0
486 #define UC_MODE_LINUX 1
488 static uint16_t unicode_mode = UC_MODE_WIN;
490 const macro_t *action_get_macro(keyrecord_t *record, uint8_t id, uint8_t opt) {{
491 if (!record->event.pressed) {{
494 // MACRODOWN only works in this function
497 unicode_mode = (unicode_mode + 1) % 2;
503 if (unicode_mode == UC_MODE_WIN) {{
509 }} else if (unicode_mode == UC_MODE_LINUX) {{
520 WIN_UNICODE_MACRO_TEMPLATE = """
523 D(LALT), T(KP_PLUS), {1}, U(LALT), END
527 LINUX_UNICODE_MACRO_TEMPLATE = """
530 D(LCTRL), D(LSHIFT), T(U), U(LCTRL), U(LSHIFT), {1}, T(KP_ENTER), END
534 def macro_cases(config, mode):
536 template = WIN_UNICODE_MACRO_TEMPLATE
537 elif mode == 'linux':
538 template = LINUX_UNICODE_MACRO_TEMPLATE
540 raise ValueError("Invalid mode: ", mode)
541 template = template.strip()
543 for macro_id, uc_hex in config['unicode_macros'].items():
544 unimacro_keys = ", ".join(
546 "KP_" + digit if digit.isdigit() else digit
547 ) for digit in uc_hex
549 yield template.format(macro_id, unimacro_keys)
552 def iter_keymap_lines(keymap):
553 prev_row_index = None
554 for key_index in sorted(keymap):
555 code, row_index = keymap[key_index]
556 if row_index != prev_row_index:
558 yield " {}".format(code)
559 if key_index < len(keymap) - 1:
561 prev_row_index = row_index
564 def iter_keymap_parts(config, keymaps):
566 for include_path in config['keymaps_includes']:
567 yield '#include "{}"\n'.format(include_path)
572 for i, macro_id in enumerate(sorted(config['macro_ids'])):
573 yield "#define {} {}\n".format(macro_id, i)
577 for i, layer_name in enumerate(config['layer_lines']):
578 yield '#define L{0:<3} {0:<5} // {1}\n'.format(i, layer_name)
583 yield "const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\n"
585 for i, layer_name in enumerate(config['layer_lines']):
587 layer_lines = config['layer_lines'][layer_name]
588 prefixed_lines = " * " + " * ".join(layer_lines)
589 yield "/*\n{}*/\n".format(prefixed_lines)
592 keymap = keymaps[layer_name]
593 keymap_lines = "".join(iter_keymap_lines(keymap))
594 yield "[L{0}] = KEYMAP({1}\n),\n".format(i, keymap_lines)
598 # no idea what this is for
599 yield "const uint16_t PROGMEM fn_actions[] = {};\n"
602 yield MACROCODE.format(
604 win_macro_cases="\n".join(macro_cases(config, mode='win')),
605 linux_macro_cases="\n".join(macro_cases(config, mode='linux')),
608 # TODO: dynamically create blinking lights
612 def main(argv=sys.argv[1:]):
613 if not argv or '-h' in argv or '--help' in argv:
617 in_path = os.path.abspath(argv[0])
618 if not os.path.exists(in_path):
619 print("No such file '{}'".format(in_path))
623 out_path = os.path.abspath(argv[1])
625 dirname = os.path.dirname(in_path)
626 out_path = os.path.join(dirname, "keymap.c")
628 config = parse_config(in_path)
629 valid_keys = parse_valid_keys(config)
630 keymaps = parse_keymaps(config, valid_keys)
632 with io.open(out_path, mode="w", encoding="utf-8") as fh:
633 for part in iter_keymap_parts(config, keymaps):
637 if __name__ == '__main__':