summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/python/qmk/c_parse.py23
-rwxr-xr-xlib/python/qmk/cli/generate/api.py6
-rwxr-xr-xlib/python/qmk/cli/generate/keyboard_h.py44
-rw-r--r--lib/python/qmk/cli/generate/keycodes.py2
-rw-r--r--lib/python/qmk/cli/git/submodule.py21
-rwxr-xr-xlib/python/qmk/cli/info.py27
-rw-r--r--lib/python/qmk/commands.py9
-rw-r--r--lib/python/qmk/info.py34
-rwxr-xr-xlib/python/qmk/json_encoders.py70
-rw-r--r--lib/python/qmk/keymap.py30
-rw-r--r--lib/python/qmk/tests/test_cli_commands.py4
11 files changed, 216 insertions, 54 deletions
diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py
index b8e5f6a3c9..7dd464bd34 100644
--- a/lib/python/qmk/c_parse.py
+++ b/lib/python/qmk/c_parse.py
@@ -11,7 +11,7 @@ from milc import cli
from qmk.comment_remover import comment_remover
-default_key_entry = {'x': -1, 'y': 0, 'w': 1}
+default_key_entry = {'x': -1, 'y': 0}
single_comment_regex = re.compile(r'\s+/[/*].*$')
multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE)
layout_macro_define_regex = re.compile(r'^#\s*define')
@@ -217,10 +217,13 @@ def _coerce_led_token(_type, value):
return value_map[value]
-def _validate_led_config(matrix, matrix_rows, matrix_indexes, position, position_raw, flags):
+def _validate_led_config(matrix, matrix_rows, matrix_cols, matrix_indexes, position, position_raw, flags):
# TODO: Improve crude parsing/validation
if len(matrix) != matrix_rows and len(matrix) != (matrix_rows / 2):
raise ValueError("Unable to parse g_led_config matrix data")
+ for index, row in enumerate(matrix):
+ if len(row) != matrix_cols:
+ raise ValueError(f"Number of columns in row {index} ({len(row)}) does not match matrix ({matrix_cols})")
if len(position) != len(flags):
raise ValueError(f"Number of g_led_config physical positions ({len(position)}) does not match number of flags ({len(flags)})")
if len(matrix_indexes) and (max(matrix_indexes) >= len(flags)):
@@ -234,13 +237,16 @@ def _validate_led_config(matrix, matrix_rows, matrix_indexes, position, position
def _parse_led_config(file, matrix_cols, matrix_rows):
"""Return any 'raw' led/rgb matrix config
"""
- matrix_raw = []
+ matrix = []
position_raw = []
flags = []
found_led_config = False
bracket_count = 0
section = 0
+ current_row_index = 0
+ current_row = []
+
for _type, value in lex(_preprocess_c_file(file), CLexer()):
# Assume g_led_config..stuff..;
if value == 'g_led_config':
@@ -254,12 +260,16 @@ def _parse_led_config(file, matrix_cols, matrix_rows):
if bracket_count == 2:
section += 1
elif value == '}':
+ if section == 1 and bracket_count == 3:
+ matrix.append(current_row)
+ current_row = []
+ current_row_index += 1
bracket_count -= 1
else:
# Assume any non whitespace value here is important enough to stash
if _type in [Token.Literal.Number.Integer, Token.Literal.Number.Float, Token.Literal.Number.Hex, Token.Name]:
if section == 1 and bracket_count == 3:
- matrix_raw.append(_coerce_led_token(_type, value))
+ current_row.append(_coerce_led_token(_type, value))
if section == 2 and bracket_count == 3:
position_raw.append(_coerce_led_token(_type, value))
if section == 3 and bracket_count == 2:
@@ -269,16 +279,15 @@ def _parse_led_config(file, matrix_cols, matrix_rows):
return None
# Slightly better intrim format
- matrix = list(_get_chunks(matrix_raw, matrix_cols))
position = list(_get_chunks(position_raw, 2))
- matrix_indexes = list(filter(lambda x: x is not None, matrix_raw))
+ matrix_indexes = list(filter(lambda x: x is not None, sum(matrix, [])))
# If we have not found anything - bail with no error
if not section:
return None
# Throw any validation errors
- _validate_led_config(matrix, matrix_rows, matrix_indexes, position, position_raw, flags)
+ _validate_led_config(matrix, matrix_rows, matrix_cols, matrix_indexes, position, position_raw, flags)
return (matrix, position, flags)
diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py
index 580e080eeb..61a2f9f732 100755
--- a/lib/python/qmk/cli/generate/api.py
+++ b/lib/python/qmk/cli/generate/api.py
@@ -66,6 +66,12 @@ def _filtered_copy(src, dst):
dst.write_text(json.dumps(data, separators=(',', ':')), encoding='utf-8')
return dst
+ if dst.suffix == '.jsonschema':
+ data = json_load(src)
+
+ dst.write_text(json.dumps(data), encoding='utf-8')
+ return dst
+
return shutil.copy2(src, dst)
diff --git a/lib/python/qmk/cli/generate/keyboard_h.py b/lib/python/qmk/cli/generate/keyboard_h.py
index 152921bdce..fa4036e39a 100755
--- a/lib/python/qmk/cli/generate/keyboard_h.py
+++ b/lib/python/qmk/cli/generate/keyboard_h.py
@@ -11,12 +11,9 @@ from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.constants import COL_LETTERS, ROW_LETTERS, GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
-def _generate_layouts(keyboard):
- """Generates the layouts.h file.
+def _generate_layouts(keyboard, kb_info_json):
+ """Generates the layouts macros.
"""
- # Build the info.json file
- kb_info_json = info_json(keyboard)
-
if 'matrix_size' not in kb_info_json:
cli.log.error(f'{keyboard}: Invalid matrix config.')
return []
@@ -65,6 +62,32 @@ def _generate_layouts(keyboard):
return lines
+def _generate_keycodes(kb_info_json):
+ """Generates keyboard level keycodes.
+ """
+ if 'keycodes' not in kb_info_json:
+ return []
+
+ lines = []
+ lines.append('enum keyboard_keycodes {')
+
+ for index, item in enumerate(kb_info_json.get('keycodes')):
+ key = item["key"]
+ if index == 0:
+ lines.append(f' {key} = QK_KB_0,')
+ else:
+ lines.append(f' {key},')
+
+ lines.append('};')
+
+ for item in kb_info_json.get('keycodes', []):
+ key = item["key"]
+ for alias in item.get("aliases", []):
+ lines.append(f'#define {alias} {key}')
+
+ return lines
+
+
@cli.argument('-i', '--include', nargs='?', arg_only=True, help='Optional file to include')
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@@ -73,8 +96,12 @@ def _generate_layouts(keyboard):
def generate_keyboard_h(cli):
"""Generates the keyboard.h file.
"""
+ # Build the info.json file
+ kb_info_json = info_json(cli.args.keyboard)
+
keyboard_h = cli.args.include
- dd_layouts = _generate_layouts(cli.args.keyboard)
+ dd_layouts = _generate_layouts(cli.args.keyboard, kb_info_json)
+ dd_keycodes = _generate_keycodes(kb_info_json)
valid_config = dd_layouts or keyboard_h
# Build the layouts.h file.
@@ -87,6 +114,11 @@ def generate_keyboard_h(cli):
if keyboard_h:
keyboard_h_lines.append(f'#include "{Path(keyboard_h).name}"')
+ keyboard_h_lines.append('')
+ keyboard_h_lines.append('// Keycode content')
+ if dd_keycodes:
+ keyboard_h_lines.extend(dd_keycodes)
+
# Protect against poorly configured keyboards
if not valid_config:
keyboard_h_lines.append('#error("<keyboard>.h is required unless your keyboard uses data-driven configuration. Please rename your keyboard\'s header file to <keyboard>.h")')
diff --git a/lib/python/qmk/cli/generate/keycodes.py b/lib/python/qmk/cli/generate/keycodes.py
index 17503bac63..3b69b17ed1 100644
--- a/lib/python/qmk/cli/generate/keycodes.py
+++ b/lib/python/qmk/cli/generate/keycodes.py
@@ -143,7 +143,7 @@ def generate_keycode_extras(cli):
"""
# Build the header file.
- keycodes_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', '#include "keymap.h"', '// clang-format off']
+ keycodes_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', '#include "keycodes.h"', '// clang-format off']
keycodes = load_spec(cli.args.version, cli.args.lang)
diff --git a/lib/python/qmk/cli/git/submodule.py b/lib/python/qmk/cli/git/submodule.py
index 9f354c021e..ef116ea124 100644
--- a/lib/python/qmk/cli/git/submodule.py
+++ b/lib/python/qmk/cli/git/submodule.py
@@ -7,14 +7,21 @@ from qmk import submodules
REMOVE_DIRS = [
'lib/ugfx',
- 'lib/pico-sdk',
'lib/chibios-contrib/ext/mcux-sdk',
- 'lib/lvgl',
+]
+
+IGNORE_DIRS = [
+ 'lib/arm_atsam',
+ 'lib/fnv',
+ 'lib/lib8tion',
+ 'lib/python',
+ 'lib/usbhost',
]
@cli.argument('--check', arg_only=True, action='store_true', help='Check if the submodules are dirty, and display a warning if they are.')
@cli.argument('--sync', arg_only=True, action='store_true', help='Shallow clone any missing submodules.')
+@cli.argument('-f', '--force', action='store_true', help='Flag to remove unexpected directories')
@cli.subcommand('Git Submodule actions.')
def git_submodule(cli):
"""Git Submodule actions
@@ -29,7 +36,15 @@ def git_submodule(cli):
cli.run(['git', 'submodule', 'update', '--depth=50', '--init', name], capture_output=False)
return True
- for folder in REMOVE_DIRS:
+ # can be the default behavior with: qmk config git_submodule.force=True
+ remove_dirs = REMOVE_DIRS
+ if cli.config.git_submodule.force:
+ # Also trash everything that isnt marked as "safe"
+ for path in normpath('lib').iterdir():
+ if not any(ignore in path.as_posix() for ignore in IGNORE_DIRS):
+ remove_dirs.append(path)
+
+ for folder in map(normpath, remove_dirs):
if normpath(folder).is_dir():
print(f"Removing '{folder}'")
shutil.rmtree(folder)
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
index fa5729bcc9..839139346c 100755
--- a/lib/python/qmk/cli/info.py
+++ b/lib/python/qmk/cli/info.py
@@ -18,6 +18,29 @@ from qmk.path import is_keyboard
UNICODE_SUPPORT = sys.stdout.encoding.lower().startswith('utf')
+def _strip_api_content(info_json):
+ # Ideally this would only be added in the API pathway.
+ info_json.pop('platform', None)
+ info_json.pop('platform_key', None)
+ info_json.pop('processor_type', None)
+ info_json.pop('protocol', None)
+ info_json.pop('config_h_features', None)
+ info_json.pop('keymaps', None)
+ info_json.pop('keyboard_folder', None)
+ info_json.pop('parse_errors', None)
+ info_json.pop('parse_warnings', None)
+
+ for layout in info_json.get('layouts', {}).values():
+ layout.pop('filename', None)
+ layout.pop('c_macro', None)
+ layout.pop('json_layout', None)
+
+ if 'matrix_pins' in info_json:
+ info_json.pop('matrix_size', None)
+
+ return info_json
+
+
def show_keymap(kb_info_json, title_caps=True):
"""Render the keymap in ascii art.
"""
@@ -141,6 +164,7 @@ def print_parsed_rules_mk(keyboard_name):
@cli.argument('-f', '--format', default='friendly', arg_only=True, help='Format to display the data in (friendly, text, json) (Default: friendly).')
@cli.argument('--ascii', action='store_true', default=not UNICODE_SUPPORT, help='Render layout box drawings in ASCII only.')
@cli.argument('-r', '--rules-mk', action='store_true', help='Render the parsed values of the keyboard\'s rules.mk file.')
+@cli.argument('-a', '--api', action='store_true', help='Show fully processed info intended for API consumption.')
@cli.subcommand('Keyboard information.')
@automagic_keyboard
@automagic_keymap
@@ -171,6 +195,9 @@ def info(cli):
else:
kb_info_json = info_json(cli.config.info.keyboard)
+ if not cli.args.api:
+ kb_info_json = _strip_api_content(kb_info_json)
+
# Output in the requested format
if cli.args.format == 'json':
print(json.dumps(kb_info_json, cls=InfoJSONEncoder))
diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py
index 5561a354c5..b1eac13957 100644
--- a/lib/python/qmk/commands.py
+++ b/lib/python/qmk/commands.py
@@ -51,6 +51,9 @@ def create_make_target(target, dry_run=False, parallel=1, **env_vars):
for key, value in env_vars.items():
env.append(f'{key}={value}')
+ if cli.config.general.verbose:
+ env.append('VERBOSE=true')
+
return [make_cmd, *(['-n'] if dry_run else []), *get_make_parallel_args(parallel), *env, target]
@@ -175,9 +178,6 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, clean=Fa
if bootloader:
make_command.append(bootloader)
- for key, value in env_vars.items():
- make_command.append(f'{key}={value}')
-
make_command.extend([
f'KEYBOARD={user_keymap["keyboard"]}',
f'KEYMAP={user_keymap["keymap"]}',
@@ -198,6 +198,9 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, clean=Fa
'QMK_BIN="qmk"',
])
+ for key, value in env_vars.items():
+ make_command.append(f'{key}={value}')
+
return make_command
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index b7ee055eef..924d31ac81 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -50,18 +50,9 @@ def _valid_community_layout(layout):
return (Path('layouts/default') / layout).exists()
-def _validate(keyboard, info_data):
- """Perform various validation on the provided info.json data
+def _additional_validation(keyboard, info_data):
+ """Non schema checks
"""
- # First validate against the jsonschema
- try:
- validate(info_data, 'qmk.api.keyboard.v1')
-
- except jsonschema.ValidationError as e:
- json_path = '.'.join([str(p) for p in e.absolute_path])
- cli.log.error('Invalid API data: %s: %s: %s', keyboard, json_path, e.message)
- exit(1)
-
layouts = info_data.get('layouts', {})
layout_aliases = info_data.get('layout_aliases', {})
community_layouts = info_data.get('community_layouts', [])
@@ -103,6 +94,27 @@ def _validate(keyboard, info_data):
if layout_name not in layouts and layout_name not in layout_aliases:
_log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name))
+ # keycodes with length > 7 must have short forms for visualisation purposes
+ for decl in info_data.get('keycodes', []):
+ if len(decl["key"]) > 7:
+ if not decl.get("aliases", []):
+ _log_error(info_data, f'Keycode {decl["key"]} has no short form alias')
+
+
+def _validate(keyboard, info_data):
+ """Perform various validation on the provided info.json data
+ """
+ # First validate against the jsonschema
+ try:
+ validate(info_data, 'qmk.api.keyboard.v1')
+
+ _additional_validation(keyboard, info_data)
+
+ except jsonschema.ValidationError as e:
+ json_path = '.'.join([str(p) for p in e.absolute_path])
+ cli.log.error('Invalid API data: %s: %s: %s', keyboard, json_path, e.message)
+ exit(1)
+
def info_json(keyboard):
"""Generate the info.json data for a specific keyboard.
diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py
index f968b3dbb2..e61c63aff3 100755
--- a/lib/python/qmk/json_encoders.py
+++ b/lib/python/qmk/json_encoders.py
@@ -27,7 +27,10 @@ class QMKJSONEncoder(json.JSONEncoder):
return float(obj)
- def encode_list(self, obj):
+ def encode_dict_single_line(self, obj):
+ return "{" + ", ".join(f"{self.encode(key)}: {self.encode(element)}" for key, element in sorted(obj.items(), key=self.sort_layout)) + "}"
+
+ def encode_list(self, obj, key=None):
"""Encode a list-like object.
"""
if self.primitives_only(obj):
@@ -35,22 +38,28 @@ class QMKJSONEncoder(json.JSONEncoder):
else:
self.indentation_level += 1
- output = [self.indent_str + self.encode(element) for element in obj]
+
+ if key in ('layout', 'rotary'):
+ # These are part of a layout or led/encoder config, put them on a single line.
+ output = [self.indent_str + self.encode_dict_single_line(element) for element in obj]
+ else:
+ output = [self.indent_str + self.encode(element) for element in obj]
+
self.indentation_level -= 1
return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]"
- def encode(self, obj):
+ def encode(self, obj, key=None):
"""Encode keymap.json objects for QMK.
"""
if isinstance(obj, Decimal):
return self.encode_decimal(obj)
elif isinstance(obj, (list, tuple)):
- return self.encode_list(obj)
+ return self.encode_list(obj, key)
elif isinstance(obj, dict):
- return self.encode_dict(obj)
+ return self.encode_dict(obj, key)
else:
return super().encode(obj)
@@ -71,22 +80,43 @@ class QMKJSONEncoder(json.JSONEncoder):
class InfoJSONEncoder(QMKJSONEncoder):
"""Custom encoder to make info.json's a little nicer to work with.
"""
- def encode_dict(self, obj):
+ def encode_dict(self, obj, key):
"""Encode info.json dictionaries.
"""
if obj:
- if set(("x", "y")).issubset(obj.keys()):
- # These are part of a layout/led_config, put them on a single line.
- return "{ " + ", ".join(f"{self.encode(key)}: {self.encode(element)}" for key, element in sorted(obj.items())) + " }"
-
- else:
- self.indentation_level += 1
- output = [self.indent_str + f"{json.dumps(key)}: {self.encode(value)}" for key, value in sorted(obj.items(), key=self.sort_dict)]
- self.indentation_level -= 1
- return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}"
+ self.indentation_level += 1
+ output = [self.indent_str + f"{json.dumps(k)}: {self.encode(v, k)}" for k, v in sorted(obj.items(), key=self.sort_dict)]
+ self.indentation_level -= 1
+ return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}"
else:
return "{}"
+ def sort_layout(self, key):
+ key = key[0]
+
+ if key == 'label':
+ return '00label'
+
+ elif key == 'matrix':
+ return '01matrix'
+
+ elif key == 'x':
+ return '02x'
+
+ elif key == 'y':
+ return '03y'
+
+ elif key == 'w':
+ return '04w'
+
+ elif key == 'h':
+ return '05h'
+
+ elif key == 'flags':
+ return '06flags'
+
+ return key
+
def sort_dict(self, key):
"""Forces layout to the back of the sort order.
"""
@@ -120,21 +150,19 @@ class InfoJSONEncoder(QMKJSONEncoder):
class KeymapJSONEncoder(QMKJSONEncoder):
"""Custom encoder to make keymap.json's a little nicer to work with.
"""
- def encode_dict(self, obj):
+ def encode_dict(self, obj, key):
"""Encode dictionary objects for keymap.json.
"""
if obj:
self.indentation_level += 1
- output_lines = [f"{self.indent_str}{json.dumps(key)}: {self.encode(value)}" for key, value in sorted(obj.items(), key=self.sort_dict)]
- output = ',\n'.join(output_lines)
+ output = [self.indent_str + f"{json.dumps(k)}: {self.encode(v, k)}" for k, v in sorted(obj.items(), key=self.sort_dict)]
self.indentation_level -= 1
-
- return f"{{\n{output}\n{self.indent_str}}}"
+ return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}"
else:
return "{}"
- def encode_list(self, obj):
+ def encode_list(self, obj, k=None):
"""Encode a list-like object.
"""
if self.indentation_level == 2:
diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py
index dddf6449a7..8ae8a5ed19 100644
--- a/lib/python/qmk/keymap.py
+++ b/lib/python/qmk/keymap.py
@@ -25,6 +25,7 @@ __INCLUDES__
* This file was generated by qmk json2c. You may or may not want to
* edit it directly.
*/
+__KEYCODE_OUTPUT_GOES_HERE__
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
__KEYMAP_GOES_HERE__
@@ -123,6 +124,29 @@ def _generate_macros_function(keymap_json):
return macro_txt
+def _generate_keycodes_function(keymap_json):
+ """Generates keymap level keycodes.
+ """
+ lines = []
+ lines.append('enum keymap_keycodes {')
+
+ for index, item in enumerate(keymap_json.get('keycodes', [])):
+ key = item["key"]
+ if index == 0:
+ lines.append(f' {key} = QK_USER_0,')
+ else:
+ lines.append(f' {key},')
+
+ lines.append('};')
+
+ for item in keymap_json.get('keycodes', []):
+ key = item["key"]
+ for alias in item.get("aliases", []):
+ lines.append(f'#define {alias} {key}')
+
+ return lines
+
+
def template_json(keyboard):
"""Returns a `keymap.json` template for a keyboard.
@@ -317,6 +341,12 @@ def generate_c(keymap_json):
hostlang = f'#include "keymap_{keymap_json["host_language"]}.h"\n#include "sendstring_{keymap_json["host_language"]}.h"\n'
new_keymap = new_keymap.replace('__INCLUDES__', hostlang)
+ keycodes = ''
+ if 'keycodes' in keymap_json and keymap_json['keycodes'] is not None:
+ keycodes_txt = _generate_keycodes_function(keymap_json)
+ keycodes = '\n'.join(keycodes_txt)
+ new_keymap = new_keymap.replace('__KEYCODE_OUTPUT_GOES_HERE__', keycodes)
+
return new_keymap
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
index e598b281a6..28838ec368 100644
--- a/lib/python/qmk/tests/test_cli_commands.py
+++ b/lib/python/qmk/tests/test_cli_commands.py
@@ -291,7 +291,7 @@ def test_generate_version_h():
def test_format_json_keyboard():
result = check_subcommand('format-json', '--format', 'keyboard', 'lib/python/qmk/tests/minimal_info.json')
check_returncode(result)
- assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n'
+ assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "layouts": {\n "LAYOUT": {\n "layout": [\n {"label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0}\n ]\n }\n }\n}\n'
def test_format_json_keymap():
@@ -303,7 +303,7 @@ def test_format_json_keymap():
def test_format_json_keyboard_auto():
result = check_subcommand('format-json', '--format', 'auto', 'lib/python/qmk/tests/minimal_info.json')
check_returncode(result)
- assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n'
+ assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "layouts": {\n "LAYOUT": {\n "layout": [\n {"label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0}\n ]\n }\n }\n}\n'
def test_format_json_keymap_auto():