qmk_firmware

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

keymap.py (24888B)


      1 """Functions that help you work with QMK keymaps.
      2 """
      3 import json
      4 import sys
      5 from pathlib import Path
      6 from subprocess import DEVNULL
      7 
      8 import argcomplete
      9 from milc import cli
     10 from pygments.lexers.c_cpp import CLexer
     11 from pygments.token import Token
     12 from pygments import lex
     13 
     14 import qmk.path
     15 from qmk.constants import QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE
     16 from qmk.keyboard import find_keyboard_from_dir, keyboard_folder, keyboard_aliases
     17 from qmk.errors import CppError
     18 from qmk.info import info_json
     19 
     20 # The `keymap.c` template to use when a keyboard doesn't have its own
     21 DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
     22 #if __has_include("keymap.h")
     23 #    include "keymap.h"
     24 #endif
     25 __INCLUDES__
     26 
     27 /* THIS FILE WAS GENERATED!
     28  *
     29  * This file was generated by qmk json2c. You may or may not want to
     30  * edit it directly.
     31  */
     32 
     33 __KEYMAP_GOES_HERE__
     34 __ENCODER_MAP_GOES_HERE__
     35 __DIP_SWITCH_MAP_GOES_HERE__
     36 __MACRO_OUTPUT_GOES_HERE__
     37 
     38 #ifdef OTHER_KEYMAP_C
     39 #    include OTHER_KEYMAP_C
     40 #endif // OTHER_KEYMAP_C
     41 """
     42 
     43 
     44 def _generate_keymap_table(keymap_json):
     45     lines = ['const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {']
     46     for layer_num, layer in enumerate(keymap_json['layers']):
     47         if layer_num != 0:
     48             lines[-1] = lines[-1] + ','
     49         layer = map(_strip_any, layer)
     50         layer_keys = ', '.join(layer)
     51         lines.append('    [%s] = %s(%s)' % (layer_num, keymap_json['layout'], layer_keys))
     52     lines.append('};')
     53     return lines
     54 
     55 
     56 def _generate_encodermap_table(keymap_json):
     57     lines = [
     58         '#if defined(ENCODER_ENABLE) && defined(ENCODER_MAP_ENABLE)',
     59         'const uint16_t PROGMEM encoder_map[][NUM_ENCODERS][NUM_DIRECTIONS] = {',
     60     ]
     61     for layer_num, layer in enumerate(keymap_json['encoders']):
     62         if layer_num != 0:
     63             lines[-1] = lines[-1] + ','
     64         encoder_keycode_txt = ', '.join([f'ENCODER_CCW_CW({_strip_any(e["ccw"])}, {_strip_any(e["cw"])})' for e in layer])
     65         lines.append('    [%s] = {%s}' % (layer_num, encoder_keycode_txt))
     66     lines.extend(['};', '#endif // defined(ENCODER_ENABLE) && defined(ENCODER_MAP_ENABLE)'])
     67     return lines
     68 
     69 
     70 def _generate_dipswitchmap_table(keymap_json):
     71     lines = [
     72         '#if defined(DIP_SWITCH_ENABLE) && defined(DIP_SWITCH_MAP_ENABLE)',
     73         'const uint16_t PROGMEM dip_switch_map[NUM_DIP_SWITCHES][NUM_DIP_STATES] = {',
     74     ]
     75     for index, switch in enumerate(keymap_json['dip_switches']):
     76         if index != 0:
     77             lines[-1] = lines[-1] + ','
     78         lines.append(f'    DIP_SWITCH_OFF_ON({_strip_any(switch["off"])}, {_strip_any(switch["on"])})')
     79     lines.extend(['};', '#endif // defined(DIP_SWITCH_ENABLE) && defined(DIP_SWITCH_MAP_ENABLE)'])
     80     return lines
     81 
     82 
     83 def _generate_macros_function(keymap_json):
     84     macro_txt = [
     85         'bool process_record_user(uint16_t keycode, keyrecord_t *record) {',
     86         '    if (record->event.pressed) {',
     87         '        switch (keycode) {',
     88     ]
     89 
     90     for i, macro_array in enumerate(keymap_json['macros']):
     91         macro = []
     92 
     93         for macro_fragment in macro_array:
     94             if isinstance(macro_fragment, str):
     95                 macro_fragment = macro_fragment.replace('\\', '\\\\')
     96                 macro_fragment = macro_fragment.replace('\r\n', r'\n')
     97                 macro_fragment = macro_fragment.replace('\n', r'\n')
     98                 macro_fragment = macro_fragment.replace('\r', r'\n')
     99                 macro_fragment = macro_fragment.replace('\t', r'\t')
    100                 macro_fragment = macro_fragment.replace('"', r'\"')
    101 
    102                 macro.append(f'"{macro_fragment}"')
    103 
    104             elif isinstance(macro_fragment, dict):
    105                 newstring = []
    106 
    107                 if macro_fragment['action'] == 'delay':
    108                     newstring.append(f"SS_DELAY({macro_fragment['duration']})")
    109 
    110                 elif macro_fragment['action'] == 'beep':
    111                     newstring.append(r'"\a"')
    112 
    113                 elif macro_fragment['action'] == 'tap' and len(macro_fragment['keycodes']) > 1:
    114                     last_keycode = macro_fragment['keycodes'].pop()
    115 
    116                     for keycode in macro_fragment['keycodes']:
    117                         newstring.append(f'SS_DOWN(X_{keycode})')
    118 
    119                     newstring.append(f'SS_TAP(X_{last_keycode})')
    120 
    121                     for keycode in reversed(macro_fragment['keycodes']):
    122                         newstring.append(f'SS_UP(X_{keycode})')
    123 
    124                 else:
    125                     for keycode in macro_fragment['keycodes']:
    126                         newstring.append(f"SS_{macro_fragment['action'].upper()}(X_{keycode})")
    127 
    128                 macro.append(''.join(newstring))
    129 
    130         new_macro = "".join(macro)
    131         new_macro = new_macro.replace('""', '')
    132         macro_txt.append(f'            case QK_MACRO_{i}:')
    133         macro_txt.append(f'                SEND_STRING({new_macro});')
    134         macro_txt.append('                return false;')
    135 
    136     macro_txt.append('        }')
    137     macro_txt.append('    }')
    138     macro_txt.append('\n    return true;')
    139     macro_txt.append('};')
    140     macro_txt.append('')
    141     return macro_txt
    142 
    143 
    144 def _strip_any(keycode):
    145     """Remove ANY() from a keycode.
    146     """
    147     if keycode.startswith('ANY(') and keycode.endswith(')'):
    148         keycode = keycode[4:-1]
    149 
    150     return keycode
    151 
    152 
    153 def find_keymap_from_dir(*args):
    154     """Returns `(keymap_name, source)` for the directory provided (or cwd if not specified).
    155     """
    156     def _impl_find_keymap_from_dir(relative_path):
    157         if relative_path and len(relative_path.parts) > 1:
    158             # If we're in `qmk_firmware/keyboards` and `keymaps` is in our path, try to find the keyboard name.
    159             if relative_path.parts[0] == 'keyboards' and 'keymaps' in relative_path.parts:
    160                 current_path = Path('/'.join(relative_path.parts[1:]))  # Strip 'keyboards' from the front
    161 
    162                 if 'keymaps' in current_path.parts and current_path.name != 'keymaps':
    163                     while current_path.parent.name != 'keymaps':
    164                         current_path = current_path.parent
    165 
    166                     return current_path.name, 'keymap_directory'
    167 
    168             # If we're in `qmk_firmware/layouts` guess the name from the community keymap they're in
    169             elif relative_path.parts[0] == 'layouts' and is_keymap_dir(relative_path):
    170                 return relative_path.name, 'layouts_directory'
    171 
    172             # If we're in `qmk_firmware/users` guess the name from the userspace they're in
    173             elif relative_path.parts[0] == 'users':
    174                 # Guess the keymap name based on which userspace they're in
    175                 return relative_path.parts[1], 'users_directory'
    176         return None, None
    177 
    178     if HAS_QMK_USERSPACE:
    179         name, source = _impl_find_keymap_from_dir(qmk.path.under_qmk_userspace(*args))
    180         if name and source:
    181             return name, source
    182 
    183     name, source = _impl_find_keymap_from_dir(qmk.path.under_qmk_firmware(*args))
    184     if name and source:
    185         return name, source
    186 
    187     return (None, None)
    188 
    189 
    190 def keymap_completer(prefix, action, parser, parsed_args):
    191     """Returns a list of keymaps for tab completion.
    192     """
    193     try:
    194         if parsed_args.keyboard:
    195             return list_keymaps(parsed_args.keyboard)
    196 
    197         keyboard = find_keyboard_from_dir()
    198 
    199         if keyboard:
    200             return list_keymaps(keyboard)
    201 
    202     except Exception as e:
    203         argcomplete.warn(f'Error: {e.__class__.__name__}: {str(e)}')
    204         return []
    205 
    206     return []
    207 
    208 
    209 def is_keymap_dir(keymap, c=True, json=True, additional_files=None):
    210     """Return True if Path object `keymap` has a keymap file inside.
    211 
    212     Args:
    213         keymap
    214             A Path() object for the keymap directory you want to check.
    215 
    216         c
    217             When true include `keymap.c` keymaps.
    218 
    219         json
    220             When true include `keymap.json` keymaps.
    221 
    222         additional_files
    223             A sequence of additional filenames to check against to determine if a directory is a keymap. All files must exist for a match to happen. For example, if you want to match a C keymap with both a `config.h` and `rules.mk` file: `is_keymap_dir(keymap_dir, json=False, additional_files=['config.h', 'rules.mk'])`
    224     """
    225     files = []
    226 
    227     if c:
    228         files.append('keymap.c')
    229 
    230     if json:
    231         files.append('keymap.json')
    232 
    233     for file in files:
    234         if (keymap / file).is_file():
    235             if additional_files:
    236                 for additional_file in additional_files:
    237                     if not (keymap / additional_file).is_file():
    238                         return False
    239 
    240             return True
    241 
    242 
    243 def generate_json(keymap, keyboard, layout, layers, macros=None):
    244     """Returns a `keymap.json` for the specified keyboard, layout, and layers.
    245 
    246     Args:
    247         keymap
    248             A name for this keymap.
    249 
    250         keyboard
    251             The name of the keyboard.
    252 
    253         layout
    254             The LAYOUT macro this keymap uses.
    255 
    256         layers
    257             An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
    258 
    259         macros
    260             A sequence of strings containing macros to implement for this keyboard.
    261     """
    262     new_keymap = {'keyboard': keyboard}
    263     new_keymap['keymap'] = keymap
    264     new_keymap['layout'] = layout
    265     new_keymap['layers'] = layers
    266     if macros:
    267         new_keymap['macros'] = macros
    268 
    269     return new_keymap
    270 
    271 
    272 def generate_c(keymap_json):
    273     """Returns a `keymap.c`.
    274 
    275     `keymap_json` is a dictionary with the following keys:
    276 
    277         keyboard
    278             The name of the keyboard
    279 
    280         layout
    281             The LAYOUT macro this keymap uses.
    282 
    283         layers
    284             An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
    285 
    286         macros
    287             A sequence of strings containing macros to implement for this keyboard.
    288     """
    289     new_keymap = DEFAULT_KEYMAP_C
    290 
    291     keymap = ''
    292     if 'layers' in keymap_json and keymap_json['layers'] is not None:
    293         layer_txt = _generate_keymap_table(keymap_json)
    294         keymap = '\n'.join(layer_txt)
    295     new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap)
    296 
    297     encodermap = ''
    298     if 'encoders' in keymap_json and keymap_json['encoders'] is not None:
    299         encoder_txt = _generate_encodermap_table(keymap_json)
    300         encodermap = '\n'.join(encoder_txt)
    301     new_keymap = new_keymap.replace('__ENCODER_MAP_GOES_HERE__', encodermap)
    302 
    303     dipswitchmap = ''
    304     if 'dip_switches' in keymap_json and keymap_json['dip_switches'] is not None:
    305         dip_txt = _generate_dipswitchmap_table(keymap_json)
    306         dipswitchmap = '\n'.join(dip_txt)
    307     new_keymap = new_keymap.replace('__DIP_SWITCH_MAP_GOES_HERE__', dipswitchmap)
    308 
    309     macros = ''
    310     if 'macros' in keymap_json and keymap_json['macros'] is not None:
    311         macro_txt = _generate_macros_function(keymap_json)
    312         macros = '\n'.join(macro_txt)
    313     new_keymap = new_keymap.replace('__MACRO_OUTPUT_GOES_HERE__', macros)
    314 
    315     hostlang = ''
    316     if 'host_language' in keymap_json and keymap_json['host_language'] is not None:
    317         hostlang = f'#include "keymap_{keymap_json["host_language"]}.h"\n#include "sendstring_{keymap_json["host_language"]}.h"\n'
    318     new_keymap = new_keymap.replace('__INCLUDES__', hostlang)
    319 
    320     return new_keymap
    321 
    322 
    323 def write_file(keymap_filename, keymap_content):
    324     keymap_filename.parent.mkdir(parents=True, exist_ok=True)
    325     keymap_filename.write_text(keymap_content)
    326 
    327     cli.log.info('Wrote keymap to {fg_cyan}%s', keymap_filename)
    328 
    329     return keymap_filename
    330 
    331 
    332 def write_json(keyboard, keymap, layout, layers, macros=None):
    333     """Generate the `keymap.json` and write it to disk.
    334 
    335     Returns the filename written to.
    336 
    337     Args:
    338         keyboard
    339             The name of the keyboard
    340 
    341         keymap
    342             The name of the keymap
    343 
    344         layout
    345             The LAYOUT macro this keymap uses.
    346 
    347         layers
    348             An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
    349     """
    350     keymap_json = generate_json(keyboard, keymap, layout, layers, macros=None)
    351     keymap_content = json.dumps(keymap_json)
    352     keymap_file = qmk.path.keymaps(keyboard)[0] / keymap / 'keymap.json'
    353 
    354     return write_file(keymap_file, keymap_content)
    355 
    356 
    357 def locate_keymap(keyboard, keymap, force_layout=None):
    358     """Returns the path to a keymap for a specific keyboard.
    359     """
    360     if not qmk.path.is_keyboard(keyboard):
    361         raise KeyError('Invalid keyboard: ' + repr(keyboard))
    362 
    363     # Check the keyboard folder first, last match wins
    364     keymap_path = ''
    365 
    366     search_conf = {QMK_FIRMWARE: [keyboard_folder(keyboard)]}
    367     if HAS_QMK_USERSPACE:
    368         # When we've got userspace, check there _last_ as we want them to override anything in the main repo.
    369         # We also want to search for any aliases as QMK's folder structure may have changed, with an alias, but the user
    370         # hasn't updated their keymap location yet.
    371         search_conf[QMK_USERSPACE] = list(set([keyboard_folder(keyboard), *keyboard_aliases(keyboard)]))
    372 
    373     for search_dir, keyboard_dirs in search_conf.items():
    374         for keyboard_dir in keyboard_dirs:
    375             checked_dirs = ''
    376             for folder_name in keyboard_dir.split('/'):
    377                 if checked_dirs:
    378                     checked_dirs = '/'.join((checked_dirs, folder_name))
    379                 else:
    380                     checked_dirs = folder_name
    381 
    382                 keymap_dir = Path(search_dir) / Path('keyboards') / checked_dirs / 'keymaps'
    383 
    384                 if (keymap_dir / keymap / 'keymap.c').exists():
    385                     keymap_path = keymap_dir / keymap / 'keymap.c'
    386                 if (keymap_dir / keymap / 'keymap.json').exists():
    387                     keymap_path = keymap_dir / keymap / 'keymap.json'
    388 
    389         if keymap_path:
    390             return keymap_path
    391 
    392     # Check community layouts as a fallback
    393     info = info_json(keyboard, force_layout=force_layout)
    394 
    395     community_parents = list(Path('layouts').glob('*/'))
    396     if HAS_QMK_USERSPACE and (Path(QMK_USERSPACE) / "layouts").exists():
    397         community_parents.append(Path(QMK_USERSPACE) / "layouts")
    398 
    399     for community_parent in community_parents:
    400         for layout in info.get("community_layouts", []):
    401             community_layout = community_parent / layout / keymap
    402             if community_layout.exists():
    403                 if (community_layout / 'keymap.json').exists():
    404                     return community_layout / 'keymap.json'
    405                 if (community_layout / 'keymap.c').exists():
    406                     return community_layout / 'keymap.c'
    407 
    408 
    409 def is_keymap_target(keyboard, keymap):
    410     if keymap == 'all':
    411         return True
    412 
    413     if locate_keymap(keyboard, keymap):
    414         return True
    415 
    416     return False
    417 
    418 
    419 def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=False, include_userspace=True):
    420     """List the available keymaps for a keyboard.
    421 
    422     Args:
    423         keyboard
    424             The keyboards full name with vendor and revision if necessary, example: clueboard/66/rev3
    425 
    426         c
    427             When true include `keymap.c` keymaps.
    428 
    429         json
    430             When true include `keymap.json` keymaps.
    431 
    432         additional_files
    433             A sequence of additional filenames to check against to determine if a directory is a keymap. All files must exist for a match to happen. For example, if you want to match a C keymap with both a `config.h` and `rules.mk` file: `is_keymap_dir(keymap_dir, json=False, additional_files=['config.h', 'rules.mk'])`
    434 
    435         fullpath
    436             When set to True the full path of the keymap relative to the `qmk_firmware` root will be provided.
    437 
    438         include_userspace
    439             When set to True, also search userspace for available keymaps
    440 
    441     Returns:
    442         a sorted list of valid keymap names.
    443     """
    444     names = set()
    445 
    446     has_userspace = HAS_QMK_USERSPACE and include_userspace
    447 
    448     # walk up the directory tree until keyboards_dir
    449     # and collect all directories' name with keymap.c file in it
    450     for search_dir in [QMK_FIRMWARE, QMK_USERSPACE] if has_userspace else [QMK_FIRMWARE]:
    451         keyboards_dir = search_dir / Path('keyboards')
    452         kb_path = keyboards_dir / keyboard
    453 
    454         while kb_path != keyboards_dir:
    455             keymaps_dir = kb_path / "keymaps"
    456             if keymaps_dir.is_dir():
    457                 for keymap in keymaps_dir.iterdir():
    458                     if is_keymap_dir(keymap, c, json, additional_files):
    459                         keymap = keymap if fullpath else keymap.name
    460                         names.add(keymap)
    461 
    462             kb_path = kb_path.parent
    463 
    464     # Check community layouts as a fallback
    465     info = info_json(keyboard)
    466 
    467     community_parents = list(Path('layouts').glob('*/'))
    468     if has_userspace and (Path(QMK_USERSPACE) / "layouts").exists():
    469         community_parents.append(Path(QMK_USERSPACE) / "layouts")
    470 
    471     for community_parent in community_parents:
    472         for layout in info.get("community_layouts", []):
    473             cl_path = community_parent / layout
    474             if cl_path.is_dir():
    475                 for keymap in cl_path.iterdir():
    476                     if is_keymap_dir(keymap, c, json, additional_files):
    477                         keymap = keymap if fullpath else keymap.name
    478                         names.add(keymap)
    479 
    480     return sorted(names)
    481 
    482 
    483 def _c_preprocess(path, stdin=DEVNULL):
    484     """ Run a file through the C pre-processor
    485 
    486     Args:
    487         path: path of the keymap.c file (set None to use stdin)
    488         stdin: stdin pipe (e.g. sys.stdin)
    489 
    490     Returns:
    491         the stdout of the pre-processor
    492     """
    493     cmd = ['cpp', str(path)] if path else ['cpp']
    494     pre_processed_keymap = cli.run(cmd, stdin=stdin)
    495     if 'fatal error' in pre_processed_keymap.stderr:
    496         for line in pre_processed_keymap.stderr.split('\n'):
    497             if 'fatal error' in line:
    498                 raise (CppError(line))
    499     return pre_processed_keymap.stdout
    500 
    501 
    502 def _get_layers(keymap):  # noqa C901 : until someone has a good idea how to simplify/split up this code
    503     """ Find the layers in a keymap.c file.
    504 
    505     Args:
    506         keymap: the content of the keymap.c file
    507 
    508     Returns:
    509         a dictionary containing the parsed keymap
    510     """
    511     layers = list()
    512     opening_braces = '({['
    513     closing_braces = ')}]'
    514     keymap_certainty = brace_depth = 0
    515     is_keymap = is_layer = is_adv_kc = False
    516     layer = dict(name=False, layout=False, keycodes=list())
    517     for line in lex(keymap, CLexer()):
    518         if line[0] is Token.Name:
    519             if is_keymap:
    520                 # If we are inside the keymap array
    521                 # we know the keymap's name and the layout macro will come,
    522                 # followed by the keycodes
    523                 if not layer['name']:
    524                     if line[1].startswith('LAYOUT') or line[1].startswith('KEYMAP'):
    525                         # This can happen if the keymap array only has one layer,
    526                         # for macropads and such
    527                         layer['name'] = '0'
    528                         layer['layout'] = line[1]
    529                     else:
    530                         layer['name'] = line[1]
    531                 elif not layer['layout']:
    532                     layer['layout'] = line[1]
    533                 elif is_layer:
    534                     # If we are inside a layout macro,
    535                     # collect all keycodes
    536                     if line[1] == '_______':
    537                         kc = 'KC_TRNS'
    538                     elif line[1] == 'XXXXXXX':
    539                         kc = 'KC_NO'
    540                     else:
    541                         kc = line[1]
    542                     if is_adv_kc:
    543                         # If we are inside an advanced keycode
    544                         # collect everything and hope the user
    545                         # knew what he/she was doing
    546                         layer['keycodes'][-1] += kc
    547                     else:
    548                         layer['keycodes'].append(kc)
    549 
    550         # The keymaps array's signature:
    551         # const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS]
    552         #
    553         # Only if we've found all 6 keywords in this specific order
    554         # can we know for sure that we are inside the keymaps array
    555             elif line[1] == 'PROGMEM' and keymap_certainty == 2:
    556                 keymap_certainty = 3
    557             elif line[1] == 'keymaps' and keymap_certainty == 3:
    558                 keymap_certainty = 4
    559             elif line[1] == 'MATRIX_ROWS' and keymap_certainty == 4:
    560                 keymap_certainty = 5
    561             elif line[1] == 'MATRIX_COLS' and keymap_certainty == 5:
    562                 keymap_certainty = 6
    563         elif line[0] is Token.Keyword:
    564             if line[1] == 'const' and keymap_certainty == 0:
    565                 keymap_certainty = 1
    566         elif line[0] is Token.Keyword.Type:
    567             if line[1] == 'uint16_t' and keymap_certainty == 1:
    568                 keymap_certainty = 2
    569         elif line[0] is Token.Punctuation:
    570             if line[1] in opening_braces:
    571                 brace_depth += 1
    572                 if is_keymap:
    573                     if is_layer:
    574                         # We found the beginning of a non-basic keycode
    575                         is_adv_kc = True
    576                         layer['keycodes'][-1] += line[1]
    577                     elif line[1] == '(' and brace_depth == 2:
    578                         # We found the beginning of a layer
    579                         is_layer = True
    580                 elif line[1] == '{' and keymap_certainty == 6:
    581                     # We found the beginning of the keymaps array
    582                     is_keymap = True
    583             elif line[1] in closing_braces:
    584                 brace_depth -= 1
    585                 if is_keymap:
    586                     if is_adv_kc:
    587                         layer['keycodes'][-1] += line[1]
    588                         if brace_depth == 2:
    589                             # We found the end of a non-basic keycode
    590                             is_adv_kc = False
    591                     elif line[1] == ')' and brace_depth == 1:
    592                         # We found the end of a layer
    593                         is_layer = False
    594                         layers.append(layer)
    595                         layer = dict(name=False, layout=False, keycodes=list())
    596                     elif line[1] == '}' and brace_depth == 0:
    597                         # We found the end of the keymaps array
    598                         is_keymap = False
    599                         keymap_certainty = 0
    600             elif is_adv_kc:
    601                 # Advanced keycodes can contain other punctuation
    602                 # e.g.: MT(MOD_LCTL | MOD_LSFT, KC_ESC)
    603                 layer['keycodes'][-1] += line[1]
    604 
    605         elif line[0] is Token.Literal.Number.Integer and is_keymap and not is_adv_kc:
    606             # If the pre-processor finds the 'meaning' of the layer names,
    607             # they will be numbers
    608             if not layer['name']:
    609                 layer['name'] = line[1]
    610 
    611         else:
    612             # We only care about
    613             # operators and such if we
    614             # are inside an advanced keycode
    615             # e.g.: MT(MOD_LCTL | MOD_LSFT, KC_ESC)
    616             if is_adv_kc:
    617                 layer['keycodes'][-1] += line[1]
    618 
    619     return layers
    620 
    621 
    622 def parse_keymap_c(keymap_file, use_cpp=True):
    623     """ Parse a keymap.c file.
    624 
    625     Currently only cares about the keymaps array.
    626 
    627     Args:
    628         keymap_file: path of the keymap.c file (or '-' to use stdin)
    629 
    630         use_cpp: if True, pre-process the file with the C pre-processor
    631 
    632     Returns:
    633         a dictionary containing the parsed keymap
    634     """
    635     if not isinstance(keymap_file, (Path, str)) or keymap_file == '-':
    636         if use_cpp:
    637             keymap_file = _c_preprocess(None, sys.stdin)
    638         else:
    639             keymap_file = sys.stdin.read()
    640     else:
    641         if use_cpp:
    642             keymap_file = _c_preprocess(keymap_file)
    643         else:
    644             keymap_file = keymap_file.read_text(encoding='utf-8')
    645 
    646     keymap = dict()
    647     keymap['layers'] = _get_layers(keymap_file)
    648     return keymap
    649 
    650 
    651 def c2json(keyboard, keymap, keymap_file, use_cpp=True):
    652     """ Convert keymap.c to keymap.json
    653 
    654     Args:
    655         keyboard: The name of the keyboard
    656 
    657         keymap: The name of the keymap
    658 
    659         layout: The LAYOUT macro this keymap uses.
    660 
    661         keymap_file: path of the keymap.c file
    662 
    663         use_cpp: if True, pre-process the file with the C pre-processor
    664 
    665     Returns:
    666         a dictionary in keymap.json format
    667     """
    668     keymap_json = parse_keymap_c(keymap_file, use_cpp)
    669 
    670     dirty_layers = keymap_json.pop('layers', None)
    671     keymap_json['layers'] = list()
    672     for layer in dirty_layers:
    673         layer.pop('name')
    674         layout = layer.pop('layout')
    675         if not keymap_json.get('layout', False):
    676             keymap_json['layout'] = layout
    677         keymap_json['layers'].append(layer.pop('keycodes'))
    678 
    679     keymap_json['keyboard'] = keyboard
    680     keymap_json['keymap'] = keymap
    681     return keymap_json