diff options
Diffstat (limited to 'lib/python')
-rw-r--r-- | lib/python/qmk/c_parse.py | 23 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/api.py | 6 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/keyboard_h.py | 44 | ||||
-rw-r--r-- | lib/python/qmk/cli/generate/keycodes.py | 2 | ||||
-rw-r--r-- | lib/python/qmk/cli/git/submodule.py | 21 | ||||
-rwxr-xr-x | lib/python/qmk/cli/info.py | 27 | ||||
-rw-r--r-- | lib/python/qmk/commands.py | 9 | ||||
-rw-r--r-- | lib/python/qmk/info.py | 39 | ||||
-rwxr-xr-x | lib/python/qmk/json_encoders.py | 70 | ||||
-rw-r--r-- | lib/python/qmk/keymap.py | 30 | ||||
-rw-r--r-- | lib/python/qmk/tests/test_cli_commands.py | 4 |
11 files changed, 221 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..dbd26153d8 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -50,18 +50,14 @@ 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 - """ - # First validate against the jsonschema - try: - validate(info_data, 'qmk.api.keyboard.v1') +def _get_key_left_position(key): + # Special case for ISO enter + return key['x'] - 0.25 if key.get('h', 1) == 2 and key.get('w', 1) == 1.25 else key['x'] - 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 _additional_validation(keyboard, info_data): + """Non schema checks + """ layouts = info_data.get('layouts', {}) layout_aliases = info_data.get('layout_aliases', {}) community_layouts = info_data.get('community_layouts', []) @@ -73,7 +69,7 @@ def _validate(keyboard, info_data): # Warn if physical positions are offset (at least one key should be at x=0, and at least one key at y=0) for layout_name, layout_data in layouts.items(): - offset_x = min([k['x'] for k in layout_data['layout']]) + offset_x = min([_get_key_left_position(k) for k in layout_data['layout']]) if offset_x > 0: _log_warning(info_data, f'Layout "{layout_name}" is offset on X axis by {offset_x}') @@ -103,6 +99,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(): |