qmk_firmware

QMK firmware for my keyboards (Corne, Sweep Ferris) and trackball (Ploopy Adept)
Log | Files | Refs | Submodules | LICENSE

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())