at master 25 kB view raw
1"""Functions that help you work with QMK keymaps. 2""" 3import json 4import sys 5from pathlib import Path 6from subprocess import DEVNULL 7 8import argcomplete 9from milc import cli 10from pygments.lexers.c_cpp import CLexer 11from pygments.token import Token 12from pygments import lex 13 14import qmk.path 15from qmk.constants import QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE 16from qmk.keyboard import find_keyboard_from_dir, keyboard_folder, keyboard_aliases 17from qmk.errors import CppError 18from qmk.info import info_json 19 20# The `keymap.c` template to use when a keyboard doesn't have its own 21DEFAULT_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 44def _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 56def _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 70def _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 83def _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 144def _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 153def 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 190def 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 209def 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 243def 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 272def 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 323def 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 332def 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 357def 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 409def 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 419def 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 483def _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 502def _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 622def 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 651def 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