diff --git a/data/schemas/keymap.jsonschema b/data/schemas/keymap.jsonschema index a4bdab966b..0a515bfa84 100644 --- a/data/schemas/keymap.jsonschema +++ b/data/schemas/keymap.jsonschema @@ -6,6 +6,7 @@ "properties": { "author": {"type": "string"}, "keyboard": {"$ref": "qmk.definitions.v1#/text_identifier"}, + "keyboard_overrides": {"$ref": "qmk.keyboard.v1"}, "keymap": {"$ref": "qmk.definitions.v1#/text_identifier"}, "layout": {"$ref": "qmk.definitions.v1#/layout_macro"}, "layers": { @@ -15,10 +16,6 @@ "items": {"type": "string"} } }, - "config": {"$ref": "qmk.keyboard.v1"}, - "notes": { - "type": "string", - "description": "asdf" - } + "notes": { "type": "string" } } -} \ No newline at end of file +} diff --git a/keyboards/clueboard/2x1800/2019/keymaps/default/keymap.json b/keyboards/clueboard/2x1800/2019/keymaps/default/keymap.json index 3a60ce9c82..821320c804 100644 --- a/keyboards/clueboard/2x1800/2019/keymaps/default/keymap.json +++ b/keyboards/clueboard/2x1800/2019/keymaps/default/keymap.json @@ -1,5 +1,6 @@ { "keyboard":"clueboard/2x1800/2019", + "keyboard_overrides": {"keyboard_name": "keymap override"}, "keymap":"default", "layout":"LAYOUT_all", "layers":[ diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py index 19d504491f..08ef701606 100755 --- a/lib/python/qmk/cli/format/json.py +++ b/lib/python/qmk/cli/format/json.py @@ -42,7 +42,7 @@ def format_json(cli): if json_encoder == KeymapJSONEncoder and 'layout' in json_file: # Attempt to format the keycodes. layout = json_file['layout'] - info_data = info_json(json_file['keyboard']) + info_data = info_json(json_file['keyboard'], overrides=json_file.get('keyboard_overrides')) if layout in info_data.get('layout_aliases', {}): layout = json_file['layout'] = info_data['layout_aliases'][layout] diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index ca7e14fe6b..68348107fc 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -5,10 +5,9 @@ from pathlib import Path from dotty_dict import dotty from milc import cli -from qmk.info import info_json -from qmk.json_schema import json_load, validate +from qmk.info import info_json, get_keyboard_overrides +from qmk.json_schema import json_load from qmk.keyboard import keyboard_completer, keyboard_folder -from qmk.keymap import locate_keymap from qmk.path import normpath @@ -158,19 +157,11 @@ def generate_split_config(kb_info_json, config_h_lines): @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") @cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate config.h for.') -@cli.argument('-km', '--keymap', arg_only=True, help='Keymap to generate config.h for.') @cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True) def generate_config_h(cli): """Generates the info_config.h file. """ - # Determine our keyboard/keymap - if cli.args.keymap: - km = locate_keymap(cli.args.keyboard, cli.args.keymap) - km_json = json_load(km) - validate(km_json, 'qmk.keymap.v1') - kb_info_json = dotty(km_json.get('config', {})) - else: - kb_info_json = dotty(info_json(cli.args.keyboard)) + kb_info_json = dotty(info_json(cli.args.keyboard, overrides=get_keyboard_overrides(cli.args.keyboard))) # Build the info_config.h file. config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once'] diff --git a/lib/python/qmk/cli/generate/dfu_header.py b/lib/python/qmk/cli/generate/dfu_header.py index 211ed9991a..6517ae001b 100644 --- a/lib/python/qmk/cli/generate/dfu_header.py +++ b/lib/python/qmk/cli/generate/dfu_header.py @@ -4,7 +4,7 @@ from dotty_dict import dotty from milc import cli from qmk.decorators import automagic_keyboard -from qmk.info import info_json +from qmk.info import info_json, get_keyboard_overrides from qmk.path import is_keyboard, normpath from qmk.keyboard import keyboard_completer @@ -28,7 +28,7 @@ def generate_dfu_header(cli): return False # Build the Keyboard.h file. - kb_info_json = dotty(info_json(cli.config.generate_dfu_header.keyboard)) + kb_info_json = dotty(info_json(cli.config.generate_dfu_header.keyboard, overrides=get_keyboard_overrides(cli.args.keyboard))) keyboard_h_lines = ['/* This file was generated by `qmk generate-dfu-header`. Do not edit or copy.' ' */', '', '#pragma once'] keyboard_h_lines.append(f'#define MANUFACTURER {kb_info_json["manufacturer"]}') diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py index 284d1a8510..0ad01e8641 100755 --- a/lib/python/qmk/cli/generate/info_json.py +++ b/lib/python/qmk/cli/generate/info_json.py @@ -9,7 +9,7 @@ from jsonschema import Draft7Validator, RefResolver, validators from milc import cli from pathlib import Path -from qmk.decorators import automagic_keyboard, automagic_keymap +from qmk.decorators import automagic_keyboard from qmk.info import info_json from qmk.json_encoders import InfoJSONEncoder from qmk.json_schema import compile_schema_store @@ -46,12 +46,10 @@ def strip_info_json(kb_info_json): @cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to show info for.') -@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.') @cli.argument('-o', '--output', arg_only=True, completer=FilesCompleter, help='Write the output the specified file, overwriting if necessary.') @cli.argument('-ow', '--overwrite', arg_only=True, action='store_true', help='Overwrite the existing info.json. (Overrides the location of --output)') @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) @automagic_keyboard -@automagic_keymap def generate_info_json(cli): """Generate an info.json file for a keyboard """ diff --git a/lib/python/qmk/cli/generate/keyboard_h.py b/lib/python/qmk/cli/generate/keyboard_h.py index c9d7f549b3..9c99359dd7 100755 --- a/lib/python/qmk/cli/generate/keyboard_h.py +++ b/lib/python/qmk/cli/generate/keyboard_h.py @@ -2,7 +2,7 @@ """ from milc import cli -from qmk.info import info_json +from qmk.info import info_json, get_keyboard_overrides from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.path import normpath @@ -11,7 +11,7 @@ def would_populate_layout_h(keyboard): """Detect if a given keyboard is doing data driven layouts """ # Build the info.json file - kb_info_json = info_json(keyboard) + kb_info_json = info_json(keyboard, overrides=get_keyboard_overrides(keyboard)) for layout_name in kb_info_json['layouts']: if kb_info_json['layouts'][layout_name]['c_macro']: diff --git a/lib/python/qmk/cli/generate/layouts.py b/lib/python/qmk/cli/generate/layouts.py index 4de982f822..5123ebd481 100755 --- a/lib/python/qmk/cli/generate/layouts.py +++ b/lib/python/qmk/cli/generate/layouts.py @@ -4,7 +4,7 @@ from milc import cli from qmk.constants import COL_LETTERS, ROW_LETTERS from qmk.decorators import automagic_keyboard, automagic_keymap -from qmk.info import info_json +from qmk.info import info_json, get_keyboard_overrides from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.path import is_keyboard, normpath @@ -35,7 +35,7 @@ def generate_layouts(cli): return False # Build the info.json file - kb_info_json = info_json(cli.config.generate_layouts.keyboard) + kb_info_json = info_json(cli.config.generate_layouts.keyboard, overrides=get_keyboard_overrides(cli.config.generate_layouts.keyboard)) # Build the layouts.h file. layouts_h_lines = ['/* This file was generated by `qmk generate-layouts`. Do not edit or copy.' ' */', '', '#pragma once'] diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index cdf17dfbcb..e89b41ede1 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -5,10 +5,9 @@ from pathlib import Path from dotty_dict import dotty from milc import cli -from qmk.info import info_json -from qmk.json_schema import json_load, validate +from qmk.info import info_json, get_keyboard_overrides +from qmk.json_schema import json_load from qmk.keyboard import keyboard_completer, keyboard_folder -from qmk.keymap import locate_keymap from qmk.path import normpath @@ -40,21 +39,12 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict): @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @cli.argument('-e', '--escape', arg_only=True, action='store_true', help="Escape spaces in quiet mode") @cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate rules.mk for.') -@cli.argument('-km', '--keymap', arg_only=True, help='Keymap to generate rules.mk for.') @cli.subcommand('Used by the make system to generate rules.mk from info.json', hidden=True) def generate_rules_mk(cli): """Generates a rules.mk file from info.json. """ - # Determine our keyboard/keymap - if cli.args.keymap: - km = locate_keymap(cli.args.keyboard, cli.args.keymap) - km_json = json_load(km) - validate(km_json, 'qmk.keymap.v1') - kb_info_json = dotty(km_json.get('config', {})) - else: - kb_info_json = dotty(info_json(cli.args.keyboard)) - info_rules_map = json_load(Path('data/mappings/info_rules.json')) + kb_info_json = dotty(info_json(cli.args.keyboard, overrides=get_keyboard_overrides(cli.args.keyboard))) rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', ''] # Iterate through the info_rules map to generate basic rules diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py index 3131d4b53f..6ca64b62ad 100755 --- a/lib/python/qmk/cli/info.py +++ b/lib/python/qmk/cli/info.py @@ -18,23 +18,19 @@ from qmk.path import is_keyboard UNICODE_SUPPORT = sys.stdout.encoding.lower().startswith('utf') -def show_keymap(kb_info_json, title_caps=True): +def show_keymap(kb_info_json, keymap_data, title_caps=True): """Render the keymap in ascii art. """ - keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap) + layout_name = keymap_data['layout'] + layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names - if keymap_path and keymap_path.suffix == '.json': - keymap_data = json.load(keymap_path.open(encoding='utf-8')) - layout_name = keymap_data['layout'] - layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names + for layer_num, layer in enumerate(keymap_data['layers']): + if title_caps: + cli.echo('{fg_cyan}Keymap %s Layer %s{fg_reset}:', cli.config.info.keymap, layer_num) + else: + cli.echo('{fg_cyan}keymap.%s.layer.%s{fg_reset}:', cli.config.info.keymap, layer_num) - for layer_num, layer in enumerate(keymap_data['layers']): - if title_caps: - cli.echo('{fg_cyan}Keymap %s Layer %s{fg_reset}:', cli.config.info.keymap, layer_num) - else: - cli.echo('{fg_cyan}keymap.%s.layer.%s{fg_reset}:', cli.config.info.keymap, layer_num) - - print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, layer)) + print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, layer)) def show_layouts(kb_info_json, title_caps=True): @@ -161,19 +157,27 @@ def info(cli): print_parsed_rules_mk(cli.config.info.keyboard) return False + # Pull in keymap overrides if necessary + keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap) if cli.config.info.keymap else None + keymap_data = json.load(keymap_path.open(encoding='utf-8')) if keymap_path and keymap_path.suffix == '.json' else None + # Build the info.json file - kb_info_json = info_json(cli.config.info.keyboard) + overrides = keymap_data.get('keyboard_overrides') if keymap_data else None + kb_info_json = info_json(cli.config.info.keyboard, overrides=overrides) # Output in the requested format if cli.args.format == 'json': print(json.dumps(kb_info_json, cls=InfoJSONEncoder)) return True + elif cli.args.format == 'text': print_dotted_output(kb_info_json) title_caps = False + elif cli.args.format == 'friendly': print_friendly_output(kb_info_json) title_caps = True + else: cli.log.error('Unknown format: %s', cli.args.format) return False @@ -184,5 +188,5 @@ def info(cli): if cli.config.info.matrix: show_matrix(kb_info_json, title_caps) - if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file': - show_keymap(kb_info_json, title_caps) + if keymap_data: + show_keymap(kb_info_json, keymap_data, title_caps) diff --git a/lib/python/qmk/cli/lint.py b/lib/python/qmk/cli/lint.py index 96593ed69b..fbd2ad6da4 100644 --- a/lib/python/qmk/cli/lint.py +++ b/lib/python/qmk/cli/lint.py @@ -76,10 +76,12 @@ def lint(cli): cli.log.warning('Both --all-kb and --keyboard passed, --all-kb takes presidence.') keyboard_list = list_keyboards() + elif not cli.config.lint.keyboard: cli.log.error('Missing required arguments: --keyboard or --all-kb') cli.print_help() return False + else: keyboard_list = cli.config.lint.keyboard.split(',') diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index 01c23b2612..fc830f0468 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -12,7 +12,7 @@ from milc import cli import qmk.keymap from qmk.constants import QMK_FIRMWARE, KEYBOARD_OUTPUT_PREFIX -from qmk.json_schema import json_load +from qmk.json_schema import json_load, validate time_fmt = '%Y-%m-%d-%H:%M:%S' @@ -194,6 +194,15 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va version_h = Path('quantum/version.h') version_h.write_text(create_version_h()) + # Write the overrides file, if needed + override_file = keyboard_output / 'keyboard_overrides.json' + + if 'keyboard_overrides' in user_keymap: + keyboard_output.mkdir(exist_ok=True, parents=True) + json.dump(user_keymap['keyboard_overrides'], override_file.open('w', encoding='utf-8')) + elif override_file.exists(): + override_file.unlink() + # Return a command that can be run to make the keymap and flash if given verbose = 'true' if cli.config.general.verbose else 'false' color = 'true' if cli.config.general.color else 'false' @@ -242,8 +251,10 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va def parse_configurator_json(configurator_file): """Open and parse a configurator json export """ - # FIXME(skullydazed/anyone): Add validation here user_keymap = json.load(configurator_file) + + validate(user_keymap, 'qmk.keymap.v1') + orig_keyboard = user_keymap['keyboard'] aliases = json_load(Path('data/mappings/keyboard_aliases.json')) diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 7f3aabdc3b..e0520b8d74 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -1,5 +1,6 @@ """Functions that help us generate and use info.json files. """ +import json from glob import glob from pathlib import Path @@ -7,7 +8,7 @@ import jsonschema from dotty_dict import dotty from milc import cli -from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS +from qmk.constants import CHIBIOS_PROCESSORS, KEYBOARD_OUTPUT_PREFIX, LUFA_PROCESSORS, VUSB_PROCESSORS from qmk.c_parse import find_layouts from qmk.json_schema import deep_update, json_load, validate from qmk.keyboard import config_h, rules_mk @@ -25,7 +26,7 @@ def _valid_community_layout(layout): return (Path('layouts/default') / layout).exists() -def info_json(keyboard): +def info_json(keyboard, *, overrides=None): """Generate the info.json data for a specific keyboard. """ cur_dir = Path('keyboards') @@ -59,11 +60,14 @@ def info_json(keyboard): layout_json['c_macro'] = True info_data['layouts'][layout_name] = layout_json - # Merge in the data from info.json, config.h, and rules.mk + # Merge in the data from info.json, config.h, rules.mk, and overrides info_data = merge_info_jsons(keyboard, info_data) info_data = _extract_rules_mk(info_data) info_data = _extract_config_h(info_data) + if overrides: + info_data = merge_info_data(info_data, overrides) + # Ensure that we have matrix row and column counts info_data = _matrix_size(info_data) @@ -102,6 +106,17 @@ def info_json(keyboard): return info_data +def get_keyboard_overrides(keyboard): + """Checks for keyboard_overrides.json in the keyboard build directory and returns them if it exists. + """ + keyboard_filesafe = keyboard.replace('/', '_') + keyboard_output = Path(f'{KEYBOARD_OUTPUT_PREFIX}{keyboard_filesafe}') + keyboard_overrides_file = keyboard_output / 'keyboard_overrides.json' + + if keyboard_overrides_file.exists(): + return json.load(keyboard_overrides_file.open('r', encoding='utf-8')) + + def _extract_features(info_data, rules): """Find all the features enabled in rules.mk. """ @@ -660,6 +675,38 @@ def unknown_processor_rules(info_data, rules): return info_data +def merge_info_data(info_data, new_info_data): + """Return a merged copy of info_data and new_info_data. + """ + if 'layout_aliases' in new_info_data: + info_data['layout_aliases'] = {**info_data.get('layout_aliases', {}), **new_info_data['layout_aliases']} + del new_info_data['layout_aliases'] + + for layout_name, layout in new_info_data.get('layouts', {}).items(): + if layout_name in info_data.get('layout_aliases', {}): + _log_warning(info_data, f"info.json uses alias name {layout_name} instead of {info_data['layout_aliases'][layout_name]}") + layout_name = info_data['layout_aliases'][layout_name] + + if layout_name in info_data['layouts']: + if len(info_data['layouts'][layout_name]['layout']) != len(layout['layout']): + msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s' + _log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout']))) + else: + for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']): + existing_key.update(new_key) + else: + layout['c_macro'] = False + info_data['layouts'][layout_name] = layout + + # Update info_data with the new data + if 'layouts' in new_info_data: + del new_info_data['layouts'] + + deep_update(info_data, new_info_data) + + return info_data + + def merge_info_jsons(keyboard, info_data): """Return a merged copy of all the info.json files for a keyboard. """ @@ -680,31 +727,7 @@ def merge_info_jsons(keyboard, info_data): continue # Merge layout data in - if 'layout_aliases' in new_info_data: - info_data['layout_aliases'] = {**info_data.get('layout_aliases', {}), **new_info_data['layout_aliases']} - del new_info_data['layout_aliases'] - - for layout_name, layout in new_info_data.get('layouts', {}).items(): - if layout_name in info_data.get('layout_aliases', {}): - _log_warning(info_data, f"info.json uses alias name {layout_name} instead of {info_data['layout_aliases'][layout_name]}") - layout_name = info_data['layout_aliases'][layout_name] - - if layout_name in info_data['layouts']: - if len(info_data['layouts'][layout_name]['layout']) != len(layout['layout']): - msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s' - _log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout']))) - else: - for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']): - existing_key.update(new_key) - else: - layout['c_macro'] = False - info_data['layouts'][layout_name] = layout - - # Update info_data with the new data - if 'layouts' in new_info_data: - del new_info_data['layouts'] - - deep_update(info_data, new_info_data) + merge_info_data(info_data, new_info_data) return info_data