diff options
Diffstat (limited to 'lib/python/qmk')
-rw-r--r-- | lib/python/qmk/c_parse.py | 6 | ||||
-rw-r--r-- | lib/python/qmk/cli/__init__.py | 2 | ||||
-rw-r--r-- | lib/python/qmk/cli/docs.py | 9 | ||||
-rw-r--r-- | lib/python/qmk/cli/format/c.py | 2 | ||||
-rwxr-xr-x | lib/python/qmk/cli/format/json.py | 13 | ||||
-rw-r--r-- | lib/python/qmk/cli/generate/community_modules.py | 263 | ||||
-rw-r--r-- | lib/python/qmk/cli/generate/docs.py | 6 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/keyboard_c.py | 138 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/keyboard_h.py | 19 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/rules_mk.py | 46 | ||||
-rwxr-xr-x | lib/python/qmk/cli/info.py | 5 | ||||
-rw-r--r-- | lib/python/qmk/cli/lint.py | 160 | ||||
-rwxr-xr-x | lib/python/qmk/cli/new/keymap.py | 6 | ||||
-rw-r--r-- | lib/python/qmk/cli/resolve_alias.py | 16 | ||||
-rwxr-xr-x | lib/python/qmk/cli/via2json.py | 32 | ||||
-rw-r--r-- | lib/python/qmk/commands.py | 5 | ||||
-rw-r--r-- | lib/python/qmk/community_modules.py | 100 | ||||
-rw-r--r-- | lib/python/qmk/docs.py | 8 | ||||
-rw-r--r-- | lib/python/qmk/flashers.py | 7 | ||||
-rw-r--r-- | lib/python/qmk/info.py | 27 | ||||
-rwxr-xr-x | lib/python/qmk/json_encoders.py | 28 | ||||
-rw-r--r-- | lib/python/qmk/keymap.py | 27 |
22 files changed, 806 insertions, 119 deletions
diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py index 08d23cf5ba..785b940456 100644 --- a/lib/python/qmk/c_parse.py +++ b/lib/python/qmk/c_parse.py @@ -24,7 +24,7 @@ def _get_chunks(it, size): return iter(lambda: tuple(islice(it, size)), ()) -def _preprocess_c_file(file): +def preprocess_c_file(file): """Load file and strip comments """ file_contents = file.read_text(encoding='utf-8') @@ -66,7 +66,7 @@ def find_layouts(file): parsed_layouts = {} # Search the file for LAYOUT macros and aliases - file_contents = _preprocess_c_file(file) + file_contents = preprocess_c_file(file) for line in file_contents.split('\n'): if layout_macro_define_regex.match(line.lstrip()) and '(' in line and 'LAYOUT' in line: @@ -248,7 +248,7 @@ def _parse_led_config(file, matrix_cols, matrix_rows): current_row_index = 0 current_row = [] - for _type, value in lex(_preprocess_c_file(file), CLexer()): + for _type, value in lex(preprocess_c_file(file), CLexer()): if not found_g_led_config: # Check for type if value == 'led_config_t': diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 2d63dfb447..3f2ba9ce3c 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -49,6 +49,7 @@ subcommands = [ 'qmk.cli.generate.api', 'qmk.cli.generate.autocorrect_data', 'qmk.cli.generate.compilation_database', + 'qmk.cli.generate.community_modules', 'qmk.cli.generate.config_h', 'qmk.cli.generate.develop_pr_list', 'qmk.cli.generate.dfu_header', @@ -82,6 +83,7 @@ subcommands = [ 'qmk.cli.new.keymap', 'qmk.cli.painter', 'qmk.cli.pytest', + 'qmk.cli.resolve_alias', 'qmk.cli.test.c', 'qmk.cli.userspace.add', 'qmk.cli.userspace.compile', diff --git a/lib/python/qmk/cli/docs.py b/lib/python/qmk/cli/docs.py index d28dddf194..da02ebf95e 100644 --- a/lib/python/qmk/cli/docs.py +++ b/lib/python/qmk/cli/docs.py @@ -6,6 +6,8 @@ from qmk.docs import prepare_docs_build_area, run_docs_command from milc import cli [email protected]('-p', '--port', default=8936, type=int, help='Port number to use.') [email protected]('-b', '--browser', action='store_true', help='Open the docs in the default browser.') @cli.subcommand('Run a local webserver for QMK documentation.', hidden=False if cli.config.user.developer else True) def docs(cli): """Spin up a local HTTP server for the QMK docs. @@ -22,6 +24,7 @@ def docs(cli): if not prepare_docs_build_area(is_production=False): return False - if not cli.config.general.verbose: - cli.log.info('Serving docs at http://localhost:5173/ (Ctrl+C to stop)') - run_docs_command('run', 'docs:dev') + cmd = ['docs:dev', '--port', f'{cli.args.port}'] + if cli.args.browser: + cmd.append('--open') + run_docs_command('run', cmd) diff --git a/lib/python/qmk/cli/format/c.py b/lib/python/qmk/cli/format/c.py index a58aef3fbc..65818155b0 100644 --- a/lib/python/qmk/cli/format/c.py +++ b/lib/python/qmk/cli/format/c.py @@ -10,7 +10,7 @@ from qmk.path import normpath from qmk.c_parse import c_source_files c_file_suffixes = ('c', 'h', 'cpp', 'hpp') -core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms') +core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms', 'modules') ignored = ('tmk_core/protocol/usb_hid', 'platforms/chibios/boards') diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py index 3670294434..61f5254184 100755 --- a/lib/python/qmk/cli/format/json.py +++ b/lib/python/qmk/cli/format/json.py @@ -9,7 +9,7 @@ from milc import cli from qmk.info import info_json from qmk.json_schema import json_load, validate -from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder +from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder, CommunityModuleJSONEncoder from qmk.path import normpath @@ -32,6 +32,13 @@ def _detect_json_format(file, json_data): if json_encoder is None: try: + validate(json_data, 'qmk.community_module.v1') + json_encoder = CommunityModuleJSONEncoder + except ValidationError: + pass + + if json_encoder is None: + try: validate(json_data, 'qmk.keyboard.v1') json_encoder = InfoJSONEncoder except ValidationError as e: @@ -54,6 +61,8 @@ def _get_json_encoder(file, json_data): json_encoder = KeymapJSONEncoder elif cli.args.format == 'userspace': json_encoder = UserspaceJSONEncoder + elif cli.args.format == 'community_module': + json_encoder = CommunityModuleJSONEncoder else: # This should be impossible cli.log.error('Unknown format: %s', cli.args.format) @@ -61,7 +70,7 @@ def _get_json_encoder(file, json_data): @cli.argument('json_file', arg_only=True, type=normpath, help='JSON file to format') [email protected]('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') [email protected]('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace', 'community_module'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') @cli.argument('-i', '--inplace', action='store_true', arg_only=True, help='If set, will operate in-place on the input file') @cli.argument('-p', '--print', action='store_true', arg_only=True, help='If set, will print the formatted json to stdout ') @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py new file mode 100644 index 0000000000..23678a2fb5 --- /dev/null +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -0,0 +1,263 @@ +import contextlib +from argcomplete.completers import FilesCompleter +from pathlib import Path + +from milc import cli + +import qmk.path +from qmk.info import get_modules +from qmk.keyboard import keyboard_completer, keyboard_folder +from qmk.commands import dump_lines +from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE +from qmk.community_modules import module_api_list, load_module_jsons, find_module_path + + +def _render_api_guard(lines, api): + if api.guard: + lines.append(f'#if {api.guard}') + yield + if api.guard: + lines.append(f'#endif // {api.guard}') + + +def _render_api_header(api): + lines = [] + if api.header: + lines.append('') + with _render_api_guard(lines, api): + lines.append(f'#include <{api.header}>') + return lines + + +def _render_keycodes(module_jsons): + lines = [] + lines.append('') + lines.append('enum {') + first = True + for module_json in module_jsons: + module_name = Path(module_json['module']).name + keycodes = module_json.get('keycodes', []) + if len(keycodes) > 0: + lines.append(f' // From module: {module_name}') + for keycode in keycodes: + key = keycode.get('key', None) + if first: + lines.append(f' {key} = QK_COMMUNITY_MODULE,') + first = False + else: + lines.append(f' {key},') + for alias in keycode.get('aliases', []): + lines.append(f' {alias} = {key},') + lines.append('') + lines.append(' LAST_COMMUNITY_MODULE_KEY') + lines.append('};') + lines.append('_Static_assert((int)LAST_COMMUNITY_MODULE_KEY <= (int)(QK_COMMUNITY_MODULE_MAX+1), "Too many community module keycodes");') + return lines + + +def _render_api_declarations(api, module, user_kb=True): + lines = [] + lines.append('') + with _render_api_guard(lines, api): + if user_kb: + lines.append(f'{api.ret_type} {api.name}_{module}_user({api.args});') + lines.append(f'{api.ret_type} {api.name}_{module}_kb({api.args});') + lines.append(f'{api.ret_type} {api.name}_{module}({api.args});') + return lines + + +def _render_api_implementations(api, module): + module_name = Path(module).name + lines = [] + lines.append('') + with _render_api_guard(lines, api): + # _user + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}_user({api.args}) {{') + if api.ret_type == 'bool': + lines.append(' return true;') + else: + pass + lines.append('}') + lines.append('') + + # _kb + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}_kb({api.args}) {{') + if api.ret_type == 'bool': + lines.append(f' if(!{api.name}_{module_name}_user({api.call_params})) {{ return false; }}') + lines.append(' return true;') + else: + lines.append(f' {api.name}_{module_name}_user({api.call_params});') + lines.append('}') + lines.append('') + + # module (non-suffixed) + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}({api.args}) {{') + if api.ret_type == 'bool': + lines.append(f' if(!{api.name}_{module_name}_kb({api.call_params})) {{ return false; }}') + lines.append(' return true;') + else: + lines.append(f' {api.name}_{module_name}_kb({api.call_params});') + lines.append('}') + return lines + + +def _render_core_implementation(api, modules): + lines = [] + lines.append('') + with _render_api_guard(lines, api): + lines.append(f'{api.ret_type} {api.name}_modules({api.args}) {{') + if api.ret_type == 'bool': + lines.append(' return true') + for module in modules: + module_name = Path(module).name + if api.ret_type == 'bool': + lines.append(f' && {api.name}_{module_name}({api.call_params})') + else: + lines.append(f' {api.name}_{module_name}({api.call_params});') + if api.ret_type == 'bool': + lines.append(' ;') + lines.append('}') + return lines + + [email protected]('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') [email protected]('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") [email protected]('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.h for.') [email protected]('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') [email protected]('Creates a community_modules.h from a keymap.json file.') +def generate_community_modules_h(cli): + """Creates a community_modules.h from a keymap.json file + """ + if cli.args.output and cli.args.output.name == '-': + cli.args.output = None + + api_list, api_version, ver_major, ver_minor, ver_patch = module_api_list() + + lines = [ + GPL2_HEADER_C_LIKE, + GENERATED_HEADER_C_LIKE, + '#pragma once', + '#include <stdint.h>', + '#include <stdbool.h>', + '#include <keycodes.h>', + '', + '#define COMMUNITY_MODULES_API_VERSION_BUILDER(ver_major,ver_minor,ver_patch) (((((uint32_t)(ver_major))&0xFF) << 24) | ((((uint32_t)(ver_minor))&0xFF) << 16) | (((uint32_t)(ver_patch))&0xFF))', + f'#define COMMUNITY_MODULES_API_VERSION COMMUNITY_MODULES_API_VERSION_BUILDER({ver_major},{ver_minor},{ver_patch})', + f'#define ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(ver_major,ver_minor,ver_patch) _Static_assert(COMMUNITY_MODULES_API_VERSION_BUILDER(ver_major,ver_minor,ver_patch) <= COMMUNITY_MODULES_API_VERSION, "Community module requires a newer version of QMK modules API -- needs: " #ver_major "." #ver_minor "." #ver_patch ", current: {api_version}.")', + '', + 'typedef struct keyrecord_t keyrecord_t; // forward declaration so we don\'t need to include quantum.h', + '', + ] + + modules = get_modules(cli.args.keyboard, cli.args.filename) + module_jsons = load_module_jsons(modules) + if len(modules) > 0: + lines.extend(_render_keycodes(module_jsons)) + + for api in api_list: + lines.extend(_render_api_header(api)) + + for module in modules: + lines.append('') + lines.append(f'// From module: {module}') + for api in api_list: + lines.extend(_render_api_declarations(api, Path(module).name)) + lines.append('') + + lines.append('// Core wrapper') + for api in api_list: + lines.extend(_render_api_declarations(api, 'modules', user_kb=False)) + + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) + + [email protected]('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') [email protected]('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") [email protected]('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.') [email protected]('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') [email protected]('Creates a community_modules.c from a keymap.json file.') +def generate_community_modules_c(cli): + """Creates a community_modules.c from a keymap.json file + """ + if cli.args.output and cli.args.output.name == '-': + cli.args.output = None + + api_list, _, _, _, _ = module_api_list() + + lines = [ + GPL2_HEADER_C_LIKE, + GENERATED_HEADER_C_LIKE, + '', + '#include "community_modules.h"', + ] + + modules = get_modules(cli.args.keyboard, cli.args.filename) + if len(modules) > 0: + + for module in modules: + for api in api_list: + lines.extend(_render_api_implementations(api, Path(module).name)) + + for api in api_list: + lines.extend(_render_core_implementation(api, modules)) + + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) + + [email protected]('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') [email protected]('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") [email protected]('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.') [email protected]('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') [email protected]('Creates a community_modules_introspection.h from a keymap.json file.') +def generate_community_modules_introspection_h(cli): + """Creates a community_modules_introspection.h from a keymap.json file + """ + if cli.args.output and cli.args.output.name == '-': + cli.args.output = None + + lines = [ + GPL2_HEADER_C_LIKE, + GENERATED_HEADER_C_LIKE, + '', + ] + + modules = get_modules(cli.args.keyboard, cli.args.filename) + if len(modules) > 0: + for module in modules: + module_path = find_module_path(module) + lines.append(f'#if __has_include("{module_path}/introspection.h")') + lines.append(f'#include "{module_path}/introspection.h"') + lines.append(f'#endif // __has_include("{module_path}/introspection.h")') + lines.append('') + + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) + + [email protected]('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') [email protected]('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") [email protected]('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.') [email protected]('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') [email protected]('Creates a community_modules_introspection.c from a keymap.json file.') +def generate_community_modules_introspection_c(cli): + """Creates a community_modules_introspection.c from a keymap.json file + """ + if cli.args.output and cli.args.output.name == '-': + cli.args.output = None + + lines = [ + GPL2_HEADER_C_LIKE, + GENERATED_HEADER_C_LIKE, + '', + ] + + modules = get_modules(cli.args.keyboard, cli.args.filename) + if len(modules) > 0: + for module in modules: + module_path = find_module_path(module) + lines.append(f'#if __has_include("{module_path}/introspection.c")') + lines.append(f'#include "{module_path}/introspection.c"') + lines.append(f'#endif // __has_include("{module_path}/introspection.c")') + lines.append('') + + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) diff --git a/lib/python/qmk/cli/generate/docs.py b/lib/python/qmk/cli/generate/docs.py index 5821d43b86..7abeca9d2a 100644 --- a/lib/python/qmk/cli/generate/docs.py +++ b/lib/python/qmk/cli/generate/docs.py @@ -27,10 +27,8 @@ def generate_docs(cli): return False cli.log.info('Building vitepress docs') - run_docs_command('run', 'docs:build') + run_docs_command('run', ['docs:build']) cli.log.info('Successfully generated docs to %s.', BUILD_DOCS_PATH) if cli.args.serve: - if not cli.config.general.verbose: - cli.log.info('Serving docs at http://localhost:4173/ (Ctrl+C to stop)') - run_docs_command('run', 'docs:preview') + run_docs_command('run', ['docs:preview']) diff --git a/lib/python/qmk/cli/generate/keyboard_c.py b/lib/python/qmk/cli/generate/keyboard_c.py index 228b320942..1978de4a22 100755 --- a/lib/python/qmk/cli/generate/keyboard_c.py +++ b/lib/python/qmk/cli/generate/keyboard_c.py @@ -1,5 +1,9 @@ """Used by the make system to generate keyboard.c from info.json. """ +import bisect +import dataclasses +from typing import Optional + from milc import cli from qmk.info import info_json @@ -87,6 +91,7 @@ def _gen_matrix_mask(info_data): lines.append(f' 0b{"".join(reversed(mask[i]))},') lines.append('};') lines.append('#endif') + lines.append('') return lines @@ -122,6 +127,128 @@ def _gen_joystick_axes(info_data): lines.append('};') lines.append('#endif') + lines.append('') + + return lines + + +class LayoutKey: + """Geometric info for one key in a layout.""" + row: int + col: int + x: float + y: float + w: float = 1.0 + h: float = 1.0 + hand: Optional[str] = None + + @staticmethod + def from_json(key_json): + row, col = key_json['matrix'] + return LayoutKey( + row=row, + col=col, + x=key_json['x'], + y=key_json['y'], + w=key_json.get('w', 1.0), + h=key_json.get('h', 1.0), + hand=key_json.get('hand', None), + ) + + @property + def cx(self): + """Center x coordinate of the key.""" + return self.x + self.w / 2.0 + + @property + def cy(self): + """Center y coordinate of the key.""" + return self.y + self.h / 2.0 + + +class Layout: + """Geometric info of a layout.""" + def __init__(self, layout_json): + self.keys = [LayoutKey.from_json(key_json) for key_json in layout_json['layout']] + self.x_min = min(key.cx for key in self.keys) + self.x_max = max(key.cx for key in self.keys) + self.x_mid = (self.x_min + self.x_max) / 2 + # If there is one key with width >= 6u, it is probably the spacebar. + i = [i for i, key in enumerate(self.keys) if key.w >= 6.0] + self.spacebar = self.keys[i[0]] if len(i) == 1 else None + + def is_symmetric(self, tol: float = 0.02): + """Whether the key positions are symmetric about x_mid.""" + x = sorted([key.cx for key in self.keys]) + for i in range(len(x)): + x_i_mirrored = 2.0 * self.x_mid - x[i] + # Find leftmost x element greater than or equal to (x_i_mirrored - tol). + j = bisect.bisect_left(x, x_i_mirrored - tol) + if j == len(x) or abs(x[j] - x_i_mirrored) > tol: + return False + + return True + + def widest_horizontal_gap(self): + """Finds the x midpoint of the widest horizontal gap between keys.""" + x = sorted([key.cx for key in self.keys]) + x_mid = self.x_mid + max_sep = 0 + for i in range(len(x) - 1): + sep = x[i + 1] - x[i] + if sep > max_sep: + max_sep = sep + x_mid = (x[i + 1] + x[i]) / 2 + + return x_mid + + +def _gen_chordal_hold_layout(info_data): + """Convert info.json content to chordal_hold_layout + """ + # NOTE: If there are multiple layouts, only the first is read. + for layout_name, layout_json in info_data['layouts'].items(): + layout = Layout(layout_json) + break + + if layout.is_symmetric(): + # If the layout is symmetric (e.g. most split keyboards), guess the + # handedness based on the sign of (x - layout.x_mid). + hand_signs = [key.x - layout.x_mid for key in layout.keys] + elif layout.spacebar is not None: + # If the layout has a spacebar, form a dividing line through the spacebar, + # nearly vertical but with a slight angle to follow typical row stagger. + x0 = layout.spacebar.cx - 0.05 + y0 = layout.spacebar.cy - 1.0 + hand_signs = [(key.x - x0) - (key.y - y0) / 3.0 for key in layout.keys] + else: + # Fallback: assume handedness based on the widest horizontal separation. + x_mid = layout.widest_horizontal_gap() + hand_signs = [key.x - x_mid for key in layout.keys] + + for key, hand_sign in zip(layout.keys, hand_signs): + if key.hand is None: + if key == layout.spacebar or abs(hand_sign) <= 0.02: + key.hand = '*' + else: + key.hand = 'L' if hand_sign < 0.0 else 'R' + + lines = [] + lines.append('#ifdef CHORDAL_HOLD') + line = ('__attribute__((weak)) const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = ' + layout_name + '(') + + x_prev = None + for key in layout.keys: + if x_prev is None or key.x < x_prev: + lines.append(line) + line = ' ' + line += f"'{key.hand}', " + x_prev = key.x + + lines.append(line[:-2]) + lines.append(');') + lines.append('#endif') return lines @@ -136,11 +263,12 @@ def generate_keyboard_c(cli): kb_info_json = info_json(cli.args.keyboard) # Build the layouts.h file. - keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', ''] + keyboard_c_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', ''] - keyboard_h_lines.extend(_gen_led_configs(kb_info_json)) - keyboard_h_lines.extend(_gen_matrix_mask(kb_info_json)) - keyboard_h_lines.extend(_gen_joystick_axes(kb_info_json)) + keyboard_c_lines.extend(_gen_led_configs(kb_info_json)) + keyboard_c_lines.extend(_gen_matrix_mask(kb_info_json)) + keyboard_c_lines.extend(_gen_joystick_axes(kb_info_json)) + keyboard_c_lines.extend(_gen_chordal_hold_layout(kb_info_json)) # Show the results - dump_lines(cli.args.output, keyboard_h_lines, cli.args.quiet) + dump_lines(cli.args.output, keyboard_c_lines, cli.args.quiet) diff --git a/lib/python/qmk/cli/generate/keyboard_h.py b/lib/python/qmk/cli/generate/keyboard_h.py index 5863a0983a..cb9528d96b 100755 --- a/lib/python/qmk/cli/generate/keyboard_h.py +++ b/lib/python/qmk/cli/generate/keyboard_h.py @@ -22,6 +22,11 @@ def _generate_layouts(keyboard, kb_info_json): row_num = kb_info_json['matrix_size']['rows'] lines = [] + lines.append('') + lines.append('// Layout content') + lines.append('') + lines.append('#define XXX KC_NO') + for layout_name, layout_data in kb_info_json['layouts'].items(): if layout_data['c_macro']: continue @@ -31,7 +36,7 @@ def _generate_layouts(keyboard, kb_info_json): continue layout_keys = [] - layout_matrix = [['KC_NO'] * col_num for _ in range(row_num)] + layout_matrix = [['XXX'] * col_num for _ in range(row_num)] for key_data in layout_data['layout']: row, col = key_data['matrix'] @@ -46,7 +51,7 @@ def _generate_layouts(keyboard, kb_info_json): lines.append('') lines.append(f'#define {layout_name}({", ".join(layout_keys)}) {{ \\') - rows = ', \\\n'.join(['\t {' + ', '.join(row) + '}' for row in layout_matrix]) + rows = ', \\\n'.join([' { ' + ', '.join(row) + ' }' for row in layout_matrix]) rows += ' \\' lines.append(rows) lines.append('}') @@ -67,6 +72,9 @@ def _generate_keycodes(kb_info_json): return [] lines = [] + lines.append('') + lines.append('// Keycode content') + lines.append('') lines.append('enum keyboard_keycodes {') for index, item in enumerate(kb_info_json.get('keycodes')): @@ -103,17 +111,14 @@ def generate_keyboard_h(cli): valid_config = dd_layouts or keyboard_h # Build the layouts.h file. - keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', '#include "quantum.h"'] + keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', '', '#include "quantum.h"'] - keyboard_h_lines.append('') - keyboard_h_lines.append('// Layout content') if dd_layouts: keyboard_h_lines.extend(dd_layouts) + 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) diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 5291556109..cae9b07c3e 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -6,12 +6,13 @@ from dotty_dict import dotty from argcomplete.completers import FilesCompleter from milc import cli -from qmk.info import info_json +from qmk.info import info_json, get_modules from qmk.json_schema import json_load from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.commands import dump_lines, parse_configurator_json from qmk.path import normpath, FileType from qmk.constants import GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE +from qmk.community_modules import find_module_path, load_module_jsons def generate_rule(rules_key, rules_value): @@ -46,6 +47,42 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict): return generate_rule(rules_key, rules_value) +def generate_features_rules(features_dict): + lines = [] + for feature, enabled in features_dict.items(): + feature = feature.upper() + enabled = 'yes' if enabled else 'no' + lines.append(generate_rule(f'{feature}_ENABLE', enabled)) + return lines + + +def generate_modules_rules(keyboard, filename): + lines = [] + modules = get_modules(keyboard, filename) + if len(modules) > 0: + lines.append('') + lines.append('OPT_DEFS += -DCOMMUNITY_MODULES_ENABLE=TRUE') + for module in modules: + module_path = find_module_path(module) + if not module_path: + raise FileNotFoundError(f"Module '{module}' not found.") + lines.append('') + lines.append(f'COMMUNITY_MODULES += {module_path.name}') # use module_path here instead of module as it may be a subdirectory + lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module_path.name.upper()}_ENABLE=TRUE') + lines.append(f'COMMUNITY_MODULE_PATHS += {module_path}') + lines.append(f'VPATH += {module_path}') + lines.append(f'SRC += $(wildcard {module_path}/{module_path.name}.c)') + lines.append(f'-include {module_path}/rules.mk') + + module_jsons = load_module_jsons(modules) + for module_json in module_jsons: + if 'features' in module_json: + lines.append('') + lines.append(f'# Module: {module_json["module_name"]}') + lines.extend(generate_features_rules(module_json['features'])) + return lines + + @cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), completer=FilesCompleter('.json'), help='A configurator export JSON to be compiled and flashed or a pre-compiled binary firmware file (bin/hex) to be flashed.') @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") @@ -80,10 +117,7 @@ def generate_rules_mk(cli): # Iterate through features to enable/disable them if 'features' in kb_info_json: - for feature, enabled in kb_info_json['features'].items(): - feature = feature.upper() - enabled = 'yes' if enabled else 'no' - rules_mk_lines.append(generate_rule(f'{feature}_ENABLE', enabled)) + rules_mk_lines.extend(generate_features_rules(kb_info_json['features'])) # Set SPLIT_TRANSPORT, if needed if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom': @@ -99,6 +133,8 @@ def generate_rules_mk(cli): if converter: rules_mk_lines.append(generate_rule('CONVERT_TO', converter)) + rules_mk_lines.extend(generate_modules_rules(cli.args.keyboard, cli.args.filename)) + # Show the results dump_lines(cli.args.output, rules_mk_lines) diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py index e662407474..5925b57258 100755 --- a/lib/python/qmk/cli/info.py +++ b/lib/python/qmk/cli/info.py @@ -52,6 +52,11 @@ def show_keymap(kb_info_json, title_caps=True): if keymap_path and keymap_path.suffix == '.json': keymap_data = json.load(keymap_path.open(encoding='utf-8')) + + # cater for layout-less keymap.json + if 'layout' not in keymap_data: + return + layout_name = keymap_data['layout'] layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names diff --git a/lib/python/qmk/cli/lint.py b/lib/python/qmk/cli/lint.py index efb29704ae..c09e377ad6 100644 --- a/lib/python/qmk/cli/lint.py +++ b/lib/python/qmk/cli/lint.py @@ -10,7 +10,7 @@ from qmk.keyboard import keyboard_completer, keyboard_folder_or_all, is_all_keyb from qmk.keymap import locate_keymap, list_keymaps from qmk.path import keyboard from qmk.git import git_get_ignored_files -from qmk.c_parse import c_source_files +from qmk.c_parse import c_source_files, preprocess_c_file CHIBIOS_CONF_CHECKS = ['chconf.h', 'halconf.h', 'mcuconf.h', 'board.h'] INVALID_KB_FEATURES = set(['encoder_map', 'dip_switch_map', 'combo', 'tap_dance', 'via']) @@ -33,12 +33,64 @@ def _list_defaultish_keymaps(kb): return keymaps +def _get_readme_files(kb, km=None): + """Return potential keyboard/keymap readme files + """ + search_path = locate_keymap(kb, km).parent if km else keyboard(kb) + + readme_files = [] + + if not km: + current_path = Path(search_path.parts[0]) + for path_part in search_path.parts[1:]: + current_path = current_path / path_part + readme_files.extend(current_path.glob('*readme.md')) + + for file in search_path.glob("**/*readme.md"): + # Ignore keymaps when only globing keyboard files + if not km and 'keymaps' in file.parts: + continue + readme_files.append(file) + + return set(readme_files) + + +def _get_build_files(kb, km=None): + """Return potential keyboard/keymap build files + """ + search_path = locate_keymap(kb, km).parent if km else keyboard(kb) + + build_files = [] + + if not km: + current_path = Path() + for path_part in search_path.parts: + current_path = current_path / path_part + build_files.extend(current_path.glob('*rules.mk')) + + for file in search_path.glob("**/*rules.mk"): + # Ignore keymaps when only globing keyboard files + if not km and 'keymaps' in file.parts: + continue + build_files.append(file) + + return set(build_files) + + def _get_code_files(kb, km=None): """Return potential keyboard/keymap code files """ search_path = locate_keymap(kb, km).parent if km else keyboard(kb) code_files = [] + + if not km: + current_path = Path() + for path_part in search_path.parts: + current_path = current_path / path_part + code_files.extend(current_path.glob('*.h')) + code_files.extend(current_path.glob('*.c')) + for file in c_source_files([search_path]): # Ignore keymaps when only globing keyboard files if not km and 'keymaps' in file.parts: @@ -48,6 +100,43 @@ def _get_code_files(kb, km=None): return code_files +def _is_invalid_readme(file): + """Check if file contains any unfilled content + """ + tokens = [ + '%KEYBOARD%', + '%REAL_NAME%', + '%USER_NAME%', + 'image replace me!', + 'A short description of the keyboard/project', + 'The PCBs, controllers supported', + 'Links to where you can find this hardware', + ] + + for line in file.read_text(encoding='utf-8').split("\n"): + if any(token in line for token in tokens): + return True + return False + + +def _is_empty_rules(file): + """Check if file contains any useful content + """ + for line in file.read_text(encoding='utf-8').split("\n"): + if len(line) > 0 and not line.isspace() and not line.startswith('#'): + return False + return True + + +def _is_empty_include(file): + """Check if file contains any useful content + """ + for line in preprocess_c_file(file).split("\n"): + if len(line) > 0 and not line.isspace() and not line.startswith('#pragma once'): + return False + return True + + def _has_license(file): """Check file has a license header """ @@ -91,37 +180,28 @@ def _chibios_conf_includenext_check(target): return None -def _rules_mk_assignment_only(kb): +def _rules_mk_assignment_only(rules_mk): """Check the keyboard-level rules.mk to ensure it only has assignments. """ - keyboard_path = keyboard(kb) - current_path = Path() errors = [] + continuation = None + for i, line in enumerate(rules_mk.open()): + line = line.strip() - for path_part in keyboard_path.parts: - current_path = current_path / path_part - rules_mk = current_path / 'rules.mk' + if '#' in line: + line = line[:line.index('#')] - if rules_mk.exists(): + if continuation: + line = continuation + line continuation = None - for i, line in enumerate(rules_mk.open()): - line = line.strip() - - if '#' in line: - line = line[:line.index('#')] - - if continuation: - line = continuation + line - continuation = None + if line: + if line[-1] == '\\': + continuation = line[:-1] + continue - if line: - if line[-1] == '\\': - continuation = line[:-1] - continue - - if line and '=' not in line: - errors.append(f'Non-assignment code on line +{i} {rules_mk}: {line}') + if line and '=' not in line: + errors.append(f'Non-assignment code on line +{i} {rules_mk}: {line}') return errors @@ -162,7 +242,7 @@ def keymap_check(kb, km): return ok -def keyboard_check(kb): +def keyboard_check(kb): # noqa C901 """Perform the keyboard level checks. """ ok = True @@ -175,13 +255,6 @@ def keyboard_check(kb): if not _handle_invalid_features(kb, kb_info): ok = False - rules_mk_assignment_errors = _rules_mk_assignment_only(kb) - if rules_mk_assignment_errors: - ok = False - cli.log.error('%s: Non-assignment code found in rules.mk. Move it to post_rules.mk instead.', kb) - for assignment_error in rules_mk_assignment_errors: - cli.log.error(assignment_error) - invalid_files = git_get_ignored_files(f'keyboards/{kb}/') for file in invalid_files: if 'keymap' in file: @@ -189,11 +262,34 @@ def keyboard_check(kb): cli.log.error(f'{kb}: The file "{file}" should not exist!') ok = False + for file in _get_readme_files(kb): + if _is_invalid_readme(file): + cli.log.error(f'{kb}: The file "{file}" still contains template tokens!') + ok = False + + for file in _get_build_files(kb): + if _is_empty_rules(file): + cli.log.error(f'{kb}: The file "{file}" is effectively empty and should be removed!') + ok = False + + if file.suffix in ['rules.mk']: + rules_mk_assignment_errors = _rules_mk_assignment_only(file) + if rules_mk_assignment_errors: + ok = False + cli.log.error('%s: Non-assignment code found in rules.mk. Move it to post_rules.mk instead.', kb) + for assignment_error in rules_mk_assignment_errors: + cli.log.error(assignment_error) + for file in _get_code_files(kb): if not _has_license(file): cli.log.error(f'{kb}: The file "{file}" does not have a license header!') ok = False + if file.name in ['config.h']: + if _is_empty_include(file): + cli.log.error(f'{kb}: The file "{file}" is effectively empty and should be removed!') + ok = False + if file.name in CHIBIOS_CONF_CHECKS: check_error = _chibios_conf_includenext_check(file) if check_error is not None: diff --git a/lib/python/qmk/cli/new/keymap.py b/lib/python/qmk/cli/new/keymap.py index f1df05636c..8b7160f5f2 100755 --- a/lib/python/qmk/cli/new/keymap.py +++ b/lib/python/qmk/cli/new/keymap.py @@ -23,19 +23,17 @@ def validate_keymap_name(name): def prompt_keyboard(): prompt = """{fg_yellow}Select Keyboard{style_reset_all} -If you`re unsure you can view a full list of supported keyboards with {fg_yellow}qmk list-keyboards{style_reset_all}. +If you're unsure you can view a full list of supported keyboards with {fg_yellow}qmk list-keyboards{style_reset_all}. Keyboard Name? """ - return question(prompt) def prompt_user(): prompt = """ {fg_yellow}Name Your Keymap{style_reset_all} -Used for maintainer, copyright, etc -Your GitHub Username? """ +Keymap name? """ return question(prompt, default=git_get_username()) diff --git a/lib/python/qmk/cli/resolve_alias.py b/lib/python/qmk/cli/resolve_alias.py new file mode 100644 index 0000000000..b9ffb46618 --- /dev/null +++ b/lib/python/qmk/cli/resolve_alias.py @@ -0,0 +1,16 @@ +from qmk.keyboard import keyboard_folder + +from milc import cli + + [email protected]('--allow-unknown', arg_only=True, action='store_true', help="Return original if rule is not a valid keyboard.") [email protected]('keyboard', arg_only=True, help='The keyboard\'s name') [email protected]('Resolve DEFAULT_FOLDER and any keyboard_aliases for provided rule') +def resolve_alias(cli): + try: + print(keyboard_folder(cli.args.keyboard)) + except ValueError: + if cli.args.allow_unknown: + print(cli.args.keyboard) + else: + raise diff --git a/lib/python/qmk/cli/via2json.py b/lib/python/qmk/cli/via2json.py index 537e102640..0997e9ca9f 100755 --- a/lib/python/qmk/cli/via2json.py +++ b/lib/python/qmk/cli/via2json.py @@ -9,19 +9,15 @@ import qmk.keyboard import qmk.path from qmk.info import info_json from qmk.json_encoders import KeymapJSONEncoder -from qmk.commands import parse_configurator_json, dump_lines -from qmk.keymap import generate_json, list_keymaps, locate_keymap, parse_keymap_c +from qmk.commands import dump_lines +from qmk.keymap import generate_json -def _find_via_layout_macro(keyboard): - keymap_layout = None - if 'via' in list_keymaps(keyboard): - keymap_path = locate_keymap(keyboard, 'via') - if keymap_path.suffix == '.json': - keymap_layout = parse_configurator_json(keymap_path)['layout'] - else: - keymap_layout = parse_keymap_c(keymap_path)['layers'][0]['layout'] - return keymap_layout +def _find_via_layout_macro(keyboard_data): + """Assume layout macro when only 1 is available + """ + layouts = list(keyboard_data['layouts'].keys()) + return layouts[0] if len(layouts) == 1 else None def _convert_macros(via_macros): @@ -130,20 +126,16 @@ def via2json(cli): This command uses the `qmk.keymap` module to generate a keymap.json from a VIA backup json. The generated keymap is written to stdout, or to a file if -o is provided. """ - # Find appropriate layout macro - keymap_layout = cli.args.layout if cli.args.layout else _find_via_layout_macro(cli.args.keyboard) - if not keymap_layout: - cli.log.error(f"Couldn't find LAYOUT macro for keyboard {cli.args.keyboard}. Please specify it with the '-l' argument.") - return False - # Load the VIA backup json with cli.args.filename.open('r') as fd: via_backup = json.load(fd) - # Generate keyboard metadata keyboard_data = info_json(cli.args.keyboard) - if not keyboard_data: - cli.log.error(f'LAYOUT macro {keymap_layout} is not a valid one for keyboard {cli.args.keyboard}!') + + # Find appropriate layout macro + keymap_layout = cli.args.layout if cli.args.layout else _find_via_layout_macro(keyboard_data) + if not keymap_layout: + cli.log.error(f"Couldn't find LAYOUT macro for keyboard {cli.args.keyboard}. Please specify it with the '-l' argument.") return False # Get keycode array diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index a05b3641b5..0e1876ca7a 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -98,11 +98,14 @@ def in_virtualenv(): return active_prefix != sys.prefix -def dump_lines(output_file, lines, quiet=True): +def dump_lines(output_file, lines, quiet=True, remove_repeated_newlines=False): """Handle dumping to stdout or file Creates parent folders if required """ generated = '\n'.join(lines) + '\n' + if remove_repeated_newlines: + while '\n\n\n' in generated: + generated = generated.replace('\n\n\n', '\n\n') if output_file and output_file.name != '-': output_file.parent.mkdir(parents=True, exist_ok=True) if output_file.exists(): diff --git a/lib/python/qmk/community_modules.py b/lib/python/qmk/community_modules.py new file mode 100644 index 0000000000..f7e96a6b93 --- /dev/null +++ b/lib/python/qmk/community_modules.py @@ -0,0 +1,100 @@ +import os + +from pathlib import Path +from functools import lru_cache + +from milc.attrdict import AttrDict + +from qmk.json_schema import json_load, validate, merge_ordered_dicts +from qmk.util import truthy +from qmk.constants import QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE +from qmk.path import under_qmk_firmware, under_qmk_userspace + +COMMUNITY_MODULE_JSON_FILENAME = 'qmk_module.json' + + +class ModuleAPI(AttrDict): + def __init__(self, **kwargs): + super().__init__() + for key, value in kwargs.items(): + self[key] = value + + +@lru_cache(maxsize=1) +def module_api_list(): + module_definition_files = sorted(set(QMK_FIRMWARE.glob('data/constants/module_hooks/*.hjson'))) + module_definition_jsons = [json_load(f) for f in module_definition_files] + module_definitions = merge_ordered_dicts(module_definition_jsons) + latest_module_version = module_definition_files[-1].stem + latest_module_version_parts = latest_module_version.split('.') + + api_list = [] + for name, mod in module_definitions.items(): + api_list.append(ModuleAPI( + ret_type=mod['ret_type'], + name=name, + args=mod['args'], + call_params=mod.get('call_params', ''), + guard=mod.get('guard', None), + header=mod.get('header', None), + )) + + return api_list, latest_module_version, latest_module_version_parts[0], latest_module_version_parts[1], latest_module_version_parts[2] + + +def find_available_module_paths(): + """Find all available modules. + """ + search_dirs = [] + if HAS_QMK_USERSPACE: + search_dirs.append(QMK_USERSPACE / 'modules') + search_dirs.append(QMK_FIRMWARE / 'modules') + + modules = [] + for search_dir in search_dirs: + for module_json_path in search_dir.rglob(COMMUNITY_MODULE_JSON_FILENAME): + modules.append(module_json_path.parent) + return modules + + +def find_module_path(module): + """Find a module by name. + """ + for module_path in find_available_module_paths(): + # Ensure the module directory is under QMK Firmware or QMK Userspace + relative_path = under_qmk_firmware(module_path) + if not relative_path: + relative_path = under_qmk_userspace(module_path) + if not relative_path: + continue + + lhs = str(relative_path.as_posix())[len('modules/'):] + rhs = str(Path(module).as_posix()) + + if relative_path and lhs == rhs: + return module_path + return None + + +def load_module_json(module): + """Load a module JSON file. + """ + module_path = find_module_path(module) + if not module_path: + raise FileNotFoundError(f'Module not found: {module}') + + module_json = json_load(module_path / COMMUNITY_MODULE_JSON_FILENAME) + + if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False): + validate(module_json, 'qmk.community_module.v1') + + module_json['module'] = module + module_json['module_path'] = module_path + + return module_json + + +def load_module_jsons(modules): + """Load the module JSON files, matching the specified order. + """ + return list(map(load_module_json, modules)) diff --git a/lib/python/qmk/docs.py b/lib/python/qmk/docs.py index 56694cf6ae..75d2d60bda 100644 --- a/lib/python/qmk/docs.py +++ b/lib/python/qmk/docs.py @@ -17,18 +17,18 @@ BUILD_DOCS_PATH = BUILD_PATH / 'docs' DOXYGEN_PATH = BUILD_DOCS_PATH / 'static' / 'doxygen' -def run_docs_command(verb, cmd=None): +def run_docs_command(verb, cmd_args=None): environ['PATH'] += pathsep + str(NODE_MODULES_PATH / '.bin') - args = {'capture_output': False if cli.config.general.verbose else True, 'check': True, 'stdin': DEVNULL} + args = {'capture_output': False, 'check': True} docs_env = environ.copy() if cli.config.general.verbose: docs_env['DEBUG'] = 'vitepress:*,vite:*' args['env'] = docs_env arg_list = ['yarn', verb] - if cmd: - arg_list.append(cmd) + if cmd_args: + arg_list.extend(cmd_args) chdir(BUILDDEFS_PATH) cli.run(arg_list, **args) diff --git a/lib/python/qmk/flashers.py b/lib/python/qmk/flashers.py index 7ee1bc8de7..2cca4941d3 100644 --- a/lib/python/qmk/flashers.py +++ b/lib/python/qmk/flashers.py @@ -202,6 +202,10 @@ def _flash_mdloader(file): def _flash_uf2(file): + output = cli.run(['util/uf2conv.py', '--info', file]).stdout + if 'UF2 File' not in output: + return True + cli.run(['util/uf2conv.py', '--deploy', file], capture_output=False) @@ -235,7 +239,8 @@ def flasher(mcu, file): elif bl == 'md-boot': _flash_mdloader(file) elif bl == '_uf2_compatible_': - _flash_uf2(file) + if _flash_uf2(file): + return (True, "Flashing only supports uf2 format files.") else: return (True, "Known bootloader found but flashing not currently supported!") diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index d70e7ee1b3..93eba7376a 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -1059,3 +1059,30 @@ def keymap_json(keyboard, keymap, force_layout=None): _extract_config_h(kb_info_json, parse_config_h_file(keymap_config)) return kb_info_json + + +def get_modules(keyboard, keymap_filename): + """Get the modules for a keyboard/keymap. + """ + modules = [] + + if keymap_filename: + keymap_json = parse_configurator_json(keymap_filename) + + if keymap_json: + kb = keymap_json.get('keyboard', None) + if not kb: + kb = keyboard + + if kb: + kb_info_json = info_json(kb) + if kb_info_json: + modules.extend(kb_info_json.get('modules', [])) + + modules.extend(keymap_json.get('modules', [])) + + elif keyboard: + kb_info_json = info_json(keyboard) + modules.extend(kb_info_json.get('modules', [])) + + return list(dict.fromkeys(modules)) # remove dupes diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py index 0e4ad1d220..e83a381d52 100755 --- a/lib/python/qmk/json_encoders.py +++ b/lib/python/qmk/json_encoders.py @@ -235,3 +235,31 @@ class UserspaceJSONEncoder(QMKJSONEncoder): return '01build_targets' return key + + +class CommunityModuleJSONEncoder(QMKJSONEncoder): + """Custom encoder to make qmk_module.json's a little nicer to work with. + """ + def sort_dict(self, item): + """Sorts the hashes in a nice way. + """ + key = item[0] + + if self.indentation_level == 1: + if key == 'module_name': + return '00module_name' + if key == 'maintainer': + return '01maintainer' + if key == 'url': + return '02url' + if key == 'features': + return '03features' + if key == 'keycodes': + return '04keycodes' + elif self.indentation_level == 3: # keycodes + if key == 'key': + return '00key' + if key == 'aliases': + return '01aliases' + + return key diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py index 9dd043c4a8..8e36461722 100644 --- a/lib/python/qmk/keymap.py +++ b/lib/python/qmk/keymap.py @@ -334,33 +334,6 @@ def write_json(keyboard, keymap, layout, layers, macros=None): return write_file(keymap_file, keymap_content) -def write(keymap_json): - """Generate the `keymap.c` and write it to disk. - - Returns the filename written to. - - `keymap_json` should be a dict with the following keys: - keyboard - The name of the keyboard - - keymap - The name of the keymap - - layout - The LAYOUT macro this keymap uses. - - layers - An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. - - macros - A list of macros for this keymap. - """ - keymap_content = generate_c(keymap_json) - keymap_file = qmk.path.keymaps(keymap_json['keyboard'])[0] / keymap_json['keymap'] / 'keymap.c' - - return write_file(keymap_file, keymap_content) - - def locate_keymap(keyboard, keymap, force_layout=None): """Returns the path to a keymap for a specific keyboard. """ |