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)
126 INLINE_COMMENT_RE = re.compile(r"""
127 ([\,\"\[\]\{\}\d]) # anythig that might end a expression
128 \s+ # comment must be preceded by whitespace
129 // # start of the comment
130 \s # and succeded by whitespace
131 (?:[^\"\]\}\{\[]*) # the comment (except things which might be json)
132 $ # until the end of line
133 """, re.MULTILINE | re.VERBOSE)
135 TRAILING_COMMA_RE = re.compile(r"""
137 (?:\s*) # arbitrary whitespace
138 $ # only works if the trailing comma is followed by newline
139 (\s*) # arbitrary whitespace
140 ([\]\}]) # end of an array or object
141 """, re.MULTILINE | re.VERBOSE)
142 if isinstance(raw_data, bytes):
143 raw_data = raw_data.decode('utf-8')
145 raw_data = ONELINE_COMMENT_RE.sub(r"", raw_data)
146 raw_data = INLINE_COMMENT_RE.sub(r"\1", raw_data)
147 raw_data = TRAILING_COMMA_RE.sub(r"\1\2", raw_data)
148 return json.loads(raw_data)
151 def parse_config(path):
154 'name': section.get('name', ""),
161 def start_section(line_index, line):
163 if line.startswith("# "):
165 elif line.startswith("## "):
168 name = name.strip().replace(" ", "_").lower()
170 section['name'] = name
172 section['sub_name'] = name
173 section['start_line'] = line_index
176 if section['start_line'] >= 0:
177 if section['name'] == 'layout_config':
178 config.update(loads("\n".join(
179 section['code_lines']
181 elif section['sub_name'].startswith('layer'):
182 layer_name = section['sub_name']
183 config['layer_lines'][layer_name] = section['code_lines']
187 def amend_section(line_index, line):
188 section['end_line'] = line_index
189 section['code_lines'].append(line)
191 config = DEFAULT_CONFIG.copy()
193 'layer_lines': collections.OrderedDict(),
195 'unicode_macros': {},
201 with io.open(path, encoding="utf-8") as fh:
202 for i, line in enumerate(fh):
203 if line.startswith("#"):
204 start_section(i, line)
205 elif line.startswith(" "):
206 amend_section(i, line[4:])
208 # TODO: maybe parse description
214 # header file parsing
216 IF0_RE = re.compile(r"""
221 """, re.MULTILINE | re.DOTALL | re.VERBOSE
225 COMMENT_RE = re.compile(r"""
229 """, re.MULTILINE | re.DOTALL | re.VERBOSE
232 def read_header_file(path):
233 with io.open(path, encoding="utf-8") as fh:
235 data, _ = COMMENT_RE.subn("", data)
236 data, _ = IF0_RE.subn("", data)
240 def regex_partial(re_str_fmt, flags=re.MULTILINE | re.DOTALL | re.VERBOSE):
241 def partial(*args, **kwargs):
242 re_str = re_str_fmt.format(*args, **kwargs)
243 return re.compile(re_str, flags)
247 KEYDEF_REP = regex_partial(r"""
251 (?:{}) # the prefixes
252 (?:\w+) # the key name
253 ) # capture group end
258 ENUM_RE = re.compile(r"""
263 .*? # the enum content
266 ) # capture group end
267 """, re.MULTILINE | re.DOTALL | re.VERBOSE
271 ENUM_KEY_REP = regex_partial(r"""
275 ) # capture group end
279 def parse_keydefs(config, data):
280 prefix_options = "|".join(config['key_prefixes'])
281 keydef_re = KEYDEF_REP(prefix_options)
282 enum_key_re = ENUM_KEY_REP(prefix_options)
283 for match in keydef_re.finditer(data):
284 yield match.groups()[0]
286 for enum_match in ENUM_RE.finditer(data):
287 enum = enum_match.groups()[0]
288 for key_match in enum_key_re.finditer(enum):
289 yield key_match.groups()[0]
292 def parse_valid_keys(config):
293 valid_keycodes = set()
295 os.path.join(BASEPATH, "tmk_core", "common", "keycode.h")
298 BASEPATH, config['includes_dir'], include_path
299 ) for include_path in config['keymaps_includes']
303 path = path.replace("/", os.sep)
304 # the config always uses forward slashe
305 if os.path.exists(path):
306 header_data = read_header_file(path)
307 valid_keycodes.update(
308 parse_keydefs(config, header_data)
310 return valid_keycodes
314 def iter_raw_codes(layer_lines, filler, separator):
315 filler_re = re.compile("[" + filler + " ]")
316 for line in layer_lines:
317 line, _ = filler_re.subn("", line.strip())
320 codes = line.split(separator)
321 for code in codes[1:-1]:
325 def iter_indexed_codes(raw_codes, key_indexes):
327 key_indexes_flat = []
328 for row_index, key_indexes in enumerate(key_indexes):
329 for key_index in key_indexes:
330 key_rows[key_index] = row_index
331 key_indexes_flat.extend(key_indexes)
332 assert len(raw_codes) == len(key_indexes_flat)
333 for raw_code, key_index in zip(raw_codes, key_indexes_flat):
334 # we keep track of the row mostly for layout purposes
335 yield raw_code, key_index, key_rows[key_index]
338 LAYER_CHANGE_RE = re.compile(r"""
343 MACRO_RE = re.compile(r"""
348 UNICODE_RE = re.compile(r"""
353 NON_CODE = re.compile(r"""
358 def parse_uni_code(raw_code):
360 unicodedata.name(raw_code)
364 code = "M({})".format(macro_id)
365 uc_hex = "{:04X}".format(ord(raw_code))
366 return code, macro_id, uc_hex
369 def parse_key_code(raw_code, key_prefixes, valid_keycodes):
370 if raw_code in valid_keycodes:
373 for prefix in key_prefixes:
374 code = prefix + raw_code
375 if code in valid_keycodes:
379 def parse_code(raw_code, key_prefixes, valid_keycodes):
381 return 'KC_TRNS', None, None
383 if LAYER_CHANGE_RE.match(raw_code):
384 return raw_code, None, None
386 if MACRO_RE.match(raw_code):
387 code = macro_id = raw_code[2:-1]
388 return code, macro_id, None
390 if UNICODE_RE.match(raw_code):
391 hex_code = raw_code[1:]
392 return parse_uni_code(chr(int(hex_code, 16)))
394 if NON_CODE.match(raw_code):
395 return parse_uni_code(raw_code)
397 code = parse_key_code(raw_code, key_prefixes, valid_keycodes)
398 return code, None, None
401 def parse_keymap(config, key_indexes, layer_lines, valid_keycodes):
403 raw_codes = list(iter_raw_codes(
404 layer_lines, config['filler'], config['separator']
406 indexed_codes = iter_indexed_codes(raw_codes, key_indexes)
407 for raw_code, key_index, row_index in indexed_codes:
408 code, macro_id, uc_hex = parse_code(
409 raw_code, config['key_prefixes'], valid_keycodes
412 config['macro_ids'].add(macro_id)
414 config['unicode_macros'][macro_id] = uc_hex
415 keymap[key_index] = (code, row_index)
419 def parse_keymaps(config, valid_keycodes):
420 keymaps = collections.OrderedDict()
421 key_indexes = config.get(
422 'key_indexes', KEYBOARD_LAYOUTS[config['layout']]
424 # TODO: maybe validate key_indexes
426 for layer_name, layer_lines, in config['layer_lines'].items():
427 keymaps[layer_name] = parse_keymap(
428 config, key_indexes, layer_lines, valid_keycodes
435 // Runs just one time when the keyboard initializes.
436 void matrix_init_user(void) {
440 // Runs constantly in the background, in a loop.
441 void matrix_scan_user(void) {
442 uint8_t layer = biton32(layer_state);
444 ergodox_board_led_off();
445 ergodox_right_led_1_off();
446 ergodox_right_led_2_off();
447 ergodox_right_led_3_off();
450 ergodox_right_led_1_on();
453 ergodox_right_led_2_on();
456 ergodox_right_led_3_on();
459 ergodox_right_led_1_on();
460 ergodox_right_led_2_on();
463 ergodox_right_led_1_on();
464 ergodox_right_led_3_on();
467 // ergodox_right_led_2_on();
468 // ergodox_right_led_3_on();
471 // ergodox_right_led_1_on();
472 // ergodox_right_led_2_on();
473 // ergodox_right_led_3_on();
476 ergodox_board_led_off();
483 #define UC_MODE_WIN 0
484 #define UC_MODE_LINUX 1
486 static uint16_t unicode_mode = UC_MODE_WIN;
488 const macro_t *action_get_macro(keyrecord_t *record, uint8_t id, uint8_t opt) {{
489 if (!record->event.pressed) {{
492 // MACRODOWN only works in this function
495 unicode_mode = (unicode_mode + 1) % 2;
501 if (unicode_mode == UC_MODE_WIN) {{
507 }} else if (unicode_mode == UC_MODE_LINUX) {{
518 WIN_UNICODE_MACRO_TEMPLATE = """
521 D(LALT), T(KP_PLUS), {1}, U(LALT), END
525 LINUX_UNICODE_MACRO_TEMPLATE = """
528 D(LCTRL), D(LSHIFT), T(U), U(LCTRL), U(LSHIFT), {1}, T(KP_ENTER), END
532 def macro_cases(config, mode):
534 template = WIN_UNICODE_MACRO_TEMPLATE
535 elif mode == 'linux':
536 template = LINUX_UNICODE_MACRO_TEMPLATE
538 raise ValueError("Invalid mode: ", mode)
539 template = template.strip()
541 for macro_id, uc_hex in config['unicode_macros'].items():
542 unimacro_keys = ", ".join(
544 "KP_" + digit if digit.isdigit() else digit
545 ) for digit in uc_hex
547 yield template.format(macro_id, unimacro_keys)
550 def iter_keymap_lines(keymap):
551 prev_row_index = None
552 for key_index in sorted(keymap):
553 code, row_index = keymap[key_index]
554 if row_index != prev_row_index:
556 yield " {}".format(code)
557 if key_index < len(keymap) - 1:
559 prev_row_index = row_index
562 def iter_keymap_parts(config, keymaps):
564 for include_path in config['keymaps_includes']:
565 yield '#include "{}"\n'.format(include_path)
570 for i, macro_id in enumerate(sorted(config['macro_ids'])):
571 yield "#define {} {}\n".format(macro_id, i)
575 for i, layer_name in enumerate(config['layer_lines']):
576 yield '#define L{0:<3} {0:<5} // {1}\n'.format(i, layer_name)
581 yield "const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\n"
583 for i, layer_name in enumerate(config['layer_lines']):
585 layer_lines = config['layer_lines'][layer_name]
586 prefixed_lines = " * " + " * ".join(layer_lines)
587 yield "/*\n{}*/\n".format(prefixed_lines)
590 keymap = keymaps[layer_name]
591 keymap_lines = "".join(iter_keymap_lines(keymap))
592 yield "[L{0}] = KEYMAP({1}\n),\n".format(i, keymap_lines)
596 # no idea what this is for
597 yield "const uint16_t PROGMEM fn_actions[] = {};\n"
600 yield MACROCODE.format(
602 win_macro_cases="\n".join(macro_cases(config, mode='win')),
603 linux_macro_cases="\n".join(macro_cases(config, mode='linux')),
606 # TODO: dynamically create blinking lights
610 def main(argv=sys.argv[1:]):
611 if not argv or '-h' in argv or '--help' in argv:
615 in_path = os.path.abspath(argv[0])
616 if not os.path.exists(in_path):
617 print("No such file '{}'".format(in_path))
621 out_path = os.path.abspath(argv[1])
623 dirname = os.path.dirname(in_path)
624 out_path = os.path.join(dirname, "keymap.c")
626 config = parse_config(in_path)
627 valid_keys = parse_valid_keys(config)
628 keymaps = parse_keymaps(config, valid_keys)
630 with io.open(out_path, mode="w", encoding="utf-8") as fh:
631 for part in iter_keymap_parts(config, keymaps):
635 if __name__ == '__main__':