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