keyboard stuff
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