aboutsummaryrefslogtreecommitdiff
path: root/lib/python/qmk/cli/generate
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python/qmk/cli/generate')
-rwxr-xr-xlib/python/qmk/cli/generate/api.py5
-rw-r--r--lib/python/qmk/cli/generate/community_modules.py341
-rw-r--r--[-rwxr-xr-x]lib/python/qmk/cli/generate/compilation_database.py174
-rwxr-xr-xlib/python/qmk/cli/generate/config_h.py15
-rw-r--r--lib/python/qmk/cli/generate/docs.py6
-rwxr-xr-xlib/python/qmk/cli/generate/keyboard_c.py138
-rw-r--r--lib/python/qmk/cli/generate/keycodes.py16
-rw-r--r--lib/python/qmk/cli/generate/keycodes_tests.py39
-rwxr-xr-xlib/python/qmk/cli/generate/make_dependencies.py4
-rwxr-xr-xlib/python/qmk/cli/generate/rules_mk.py14
10 files changed, 516 insertions, 236 deletions
diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py
index 8311818476..7f7c05f6e2 100755
--- a/lib/python/qmk/cli/generate/api.py
+++ b/lib/python/qmk/cli/generate/api.py
@@ -131,6 +131,11 @@ def generate_api(cli):
if keymap_rel is None:
cli.log.debug('Skipping keymap %s (not in qmk_firmware)', keymap)
continue
+
+ if (keymap_rel / 'keymap.c').exists():
+ cli.log.debug('Skipping keymap %s (not pure dd keymap)', keymap)
+ continue
+
kb_json['keymaps'][keymap.name] = {
# TODO: deprecate 'url' as consumer needs to know its potentially hjson
'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap_rel}/keymap.json',
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..a5ab61f9bd
--- /dev/null
+++ b/lib/python/qmk/cli/generate/community_modules.py
@@ -0,0 +1,341 @@
+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, GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_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;')
+ elif api.ret_type in ['layer_state_t', 'report_mouse_t']:
+ lines.append(f' return {api.call_params};')
+ 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;')
+ elif api.ret_type in ['layer_state_t', 'report_mouse_t']:
+ lines.append(f' return {api.name}_{module_name}_user({api.call_params});')
+ 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;')
+ elif api.ret_type in ['layer_state_t', 'report_mouse_t']:
+ lines.append(f' return {api.name}_{module_name}_kb({api.call_params});')
+ 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})')
+ elif api.ret_type in ['layer_state_t', 'report_mouse_t']:
+ lines.append(f' {api.call_params} = {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(' ;')
+ elif api.ret_type in ['layer_state_t', 'report_mouse_t']:
+ lines.append(f' return {api.call_params};')
+ lines.append('}')
+ return lines
+
+
+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(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 = qmk.path.unix_style_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'MODULE_NAME_{module_path.name.upper()} := {module_path.name}')
+ lines.append(f'MODULE_PATH_{module_path.name.upper()} := {module_path}')
+ 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
+
+
[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]('-e', '--escape', arg_only=True, action='store_true', help="Escape spaces in quiet mode")
[email protected]('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate rules.mk for.')
[email protected]('filename', nargs='?', arg_only=True, type=qmk.path.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.')
[email protected]('Creates a community_modules_rules_mk from a keymap.json file.')
+def generate_community_modules_rules_mk(cli):
+
+ rules_mk_lines = [GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE]
+
+ rules_mk_lines.extend(_generate_modules_rules(cli.args.keyboard, cli.args.filename))
+
+ # Show the results
+ dump_lines(cli.args.output, rules_mk_lines)
+
+ if cli.args.output:
+ if cli.args.quiet:
+ if cli.args.escape:
+ print(cli.args.output.as_posix().replace(' ', '\\ '))
+ else:
+ print(cli.args.output)
+ else:
+ cli.log.info('Wrote rules.mk to %s.', cli.args.output)
+
+
[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>',
+ '',
+ '#include "compiler_support.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)
+
+
+def _generate_include_per_module(cli, include_file_name):
+ """Generates C code to include "<module_path>/include_file_name" for each module."""
+ if cli.args.output and cli.args.output.name == '-':
+ cli.args.output = None
+
+ lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE]
+
+ for module in get_modules(cli.args.keyboard, cli.args.filename):
+ full_path = f'{find_module_path(module)}/{include_file_name}'
+ lines.append('')
+ lines.append(f'#if __has_include("{full_path}")')
+ lines.append(f'#include "{full_path}"')
+ lines.append(f'#endif // __has_include("{full_path}")')
+
+ 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_introspection.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_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
+ """
+ _generate_include_per_module(cli, 'introspection.h')
+
+
[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
+ """
+ _generate_include_per_module(cli, 'introspection.c')
+
+
[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 led_matrix_community_modules.inc for.')
[email protected]('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file')
[email protected]('Creates an led_matrix_community_modules.inc from a keymap.json file.')
+def generate_led_matrix_community_modules_inc(cli):
+ """Creates an led_matrix_community_modules.inc from a keymap.json file
+ """
+ _generate_include_per_module(cli, 'led_matrix_module.inc')
+
+
[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 rgb_matrix_community_modules.inc for.')
[email protected]('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file')
[email protected]('Creates an rgb_matrix_community_modules.inc from a keymap.json file.')
+def generate_rgb_matrix_community_modules_inc(cli):
+ """Creates an rgb_matrix_community_modules.inc from a keymap.json file
+ """
+ _generate_include_per_module(cli, 'rgb_matrix_module.inc')
diff --git a/lib/python/qmk/cli/generate/compilation_database.py b/lib/python/qmk/cli/generate/compilation_database.py
index b9c716bf0c..339b53c2c2 100755..100644
--- a/lib/python/qmk/cli/generate/compilation_database.py
+++ b/lib/python/qmk/cli/generate/compilation_database.py
@@ -1,169 +1,9 @@
-"""Creates a compilation database for the given keyboard build.
-"""
+from milc import cli
-import json
-import os
-import re
-import shlex
-import shutil
-from functools import lru_cache
-from pathlib import Path
-from typing import Dict, Iterator, List, Union
-from milc import cli, MILC
-
-from qmk.commands import find_make
-from qmk.constants import QMK_FIRMWARE
-from qmk.decorators import automagic_keyboard, automagic_keymap
-from qmk.keyboard import keyboard_completer, keyboard_folder
-from qmk.keymap import keymap_completer
-from qmk.build_targets import KeyboardKeymapBuildTarget
-
-
-@lru_cache(maxsize=10)
-def system_libs(binary: str) -> List[Path]:
- """Find the system include directory that the given build tool uses.
- """
- cli.log.debug("searching for system library directory for binary: %s", binary)
-
- # Actually query xxxxxx-gcc to find its include paths.
- if binary.endswith("gcc") or binary.endswith("g++"):
- # (TODO): Remove 'stdin' once 'input' no longer causes issues under MSYS
- result = cli.run([binary, '-E', '-Wp,-v', '-'], capture_output=True, check=True, stdin=None, input='\n')
- paths = []
- for line in result.stderr.splitlines():
- if line.startswith(" "):
- paths.append(Path(line.strip()).resolve())
- return paths
-
- return list(Path(binary).resolve().parent.parent.glob("*/include")) if binary else []
-
-
-@lru_cache(maxsize=10)
-def cpu_defines(binary: str, compiler_args: str) -> List[str]:
- cli.log.debug("gathering definitions for compilation: %s %s", binary, compiler_args)
- if binary.endswith("gcc") or binary.endswith("g++"):
- invocation = [binary, '-dM', '-E']
- if binary.endswith("gcc"):
- invocation.extend(['-x', 'c'])
- elif binary.endswith("g++"):
- invocation.extend(['-x', 'c++'])
- compiler_args = shlex.split(compiler_args)
- invocation.extend(compiler_args)
- invocation.append('-')
- result = cli.run(invocation, capture_output=True, check=True, stdin=None, input='\n')
- define_args = []
- for line in result.stdout.splitlines():
- line_args = line.split(' ', 2)
- if len(line_args) == 3 and line_args[0] == '#define':
- define_args.append(f'-D{line_args[1]}={line_args[2]}')
- elif len(line_args) == 2 and line_args[0] == '#define':
- define_args.append(f'-D{line_args[1]}')
- return list(sorted(set(define_args)))
- return []
-
-
-file_re = re.compile(r'printf "Compiling: ([^"]+)')
-cmd_re = re.compile(r'LOG=\$\((.+?)&&')
-
-
-def parse_make_n(f: Iterator[str]) -> List[Dict[str, str]]:
- """parse the output of `make -n <target>`
-
- This function makes many assumptions about the format of your build log.
- This happens to work right now for qmk.
- """
-
- state = 'start'
- this_file = None
- records = []
- for line in f:
- if state == 'start':
- m = file_re.search(line)
- if m:
- this_file = m.group(1)
- state = 'cmd'
-
- if state == 'cmd':
- assert this_file
- m = cmd_re.search(line)
- if m:
- # we have a hit!
- this_cmd = m.group(1)
- args = shlex.split(this_cmd)
- binary = shutil.which(args[0])
- compiler_args = set(filter(lambda x: x.startswith('-m') or x.startswith('-f'), args))
- for s in system_libs(binary):
- args += ['-isystem', '%s' % s]
- args.extend(cpu_defines(binary, ' '.join(shlex.quote(s) for s in compiler_args)))
- new_cmd = ' '.join(shlex.quote(s) for s in args)
- records.append({"directory": str(QMK_FIRMWARE.resolve()), "command": new_cmd, "file": this_file})
- state = 'start'
-
- return records
-
-
-def write_compilation_database(keyboard: str = None, keymap: str = None, output_path: Path = QMK_FIRMWARE / 'compile_commands.json', skip_clean: bool = False, command: List[str] = None, **env_vars) -> bool:
- # Generate the make command for a specific keyboard/keymap.
- if not command:
- from qmk.build_targets import KeyboardKeymapBuildTarget # Lazy load due to circular references
- target = KeyboardKeymapBuildTarget(keyboard, keymap)
- command = target.compile_command(dry_run=True, **env_vars)
-
- if not command:
- cli.log.error('You must supply both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
- cli.echo('usage: qmk generate-compilation-database [-kb KEYBOARD] [-km KEYMAP]')
- return False
-
- # remove any environment variable overrides which could trip us up
- env = os.environ.copy()
- env.pop("MAKEFLAGS", None)
-
- # re-use same executable as the main make invocation (might be gmake)
- if not skip_clean:
- clean_command = [find_make(), "clean"]
- cli.log.info('Making clean with {fg_cyan}%s', ' '.join(clean_command))
- cli.run(clean_command, capture_output=False, check=True, env=env)
-
- cli.log.info('Gathering build instructions from {fg_cyan}%s', ' '.join(command))
-
- result = cli.run(command, capture_output=True, check=True, env=env)
- db = parse_make_n(result.stdout.splitlines())
- if not db:
- cli.log.error("Failed to parse output from make output:\n%s", result.stdout)
- return False
-
- cli.log.info("Found %s compile commands", len(db))
-
- cli.log.info(f"Writing build database to {output_path}")
- output_path.write_text(json.dumps(db, indent=4))
-
- return True
-
-
[email protected]('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard\'s name')
[email protected]('-km', '--keymap', completer=keymap_completer, help='The keymap\'s name')
[email protected]('Create a compilation database.')
-@automagic_keyboard
-@automagic_keymap
-def generate_compilation_database(cli: MILC) -> Union[bool, int]:
- """Creates a compilation database for the given keyboard build.
-
- Does a make clean, then a make -n for this target and uses the dry-run output to create
- a compilation database (compile_commands.json). This file can help some IDEs and
- IDE-like editors work better. For more information about this:
-
- https://clang.llvm.org/docs/JSONCompilationDatabase.html
- """
- # check both config domains: the magic decorator fills in `generate_compilation_database` but the user is
- # more likely to have set `compile` in their config file.
- current_keyboard = cli.config.generate_compilation_database.keyboard or cli.config.user.keyboard
- current_keymap = cli.config.generate_compilation_database.keymap or cli.config.user.keymap
-
- if not current_keyboard:
- cli.log.error('Could not determine keyboard!')
- elif not current_keymap:
- cli.log.error('Could not determine keymap!')
-
- target = KeyboardKeymapBuildTarget(current_keyboard, current_keymap)
- return target.generate_compilation_database()
[email protected]('-kb', '--keyboard', help='[unused] The keyboard\'s name')
[email protected]('-km', '--keymap', help='[unused] The keymap\'s name')
[email protected]('[deprecated] Create a compilation database.')
+def generate_compilation_database(cli):
+ cli.log.error('This command is deprecated and has effectively been removed. Please use the `--compiledb` flag with `qmk compile` instead.')
+ return False
diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
index d613f7b92c..d6d0299291 100755
--- a/lib/python/qmk/cli/generate/config_h.py
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -72,19 +72,6 @@ def generate_matrix_size(kb_info_json, config_h_lines):
config_h_lines.append(generate_define('MATRIX_ROWS', kb_info_json['matrix_size']['rows']))
-def generate_matrix_masked(kb_info_json, config_h_lines):
- """"Enable matrix mask if required"""
- mask_required = False
-
- if 'matrix_grid' in kb_info_json.get('dip_switch', {}):
- mask_required = True
- if 'matrix_grid' in kb_info_json.get('split', {}).get('handedness', {}):
- mask_required = True
-
- if mask_required:
- config_h_lines.append(generate_define('MATRIX_MASKED'))
-
-
def generate_config_items(kb_info_json, config_h_lines):
"""Iterate through the info_config map to generate basic config values.
"""
@@ -202,8 +189,6 @@ def generate_config_h(cli):
generate_matrix_size(kb_info_json, config_h_lines)
- generate_matrix_masked(kb_info_json, config_h_lines)
-
if 'matrix_pins' in kb_info_json:
config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
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/keycodes.py b/lib/python/qmk/cli/generate/keycodes.py
index 719fced5d5..d686935fa8 100644
--- a/lib/python/qmk/cli/generate/keycodes.py
+++ b/lib/python/qmk/cli/generate/keycodes.py
@@ -125,6 +125,20 @@ def _generate_aliases(lines, keycodes):
lines.append(f'#define {alias} {value.get("key")}')
+def _generate_version(lines, keycodes, prefix=''):
+ version = keycodes['version']
+ major, minor, patch = map(int, version.split('.'))
+
+ bcd = f'0x{major:02d}{minor:02d}{patch:04d}'
+
+ lines.append('')
+ lines.append(f'#define QMK_{prefix}KEYCODES_VERSION "{version}"')
+ lines.append(f'#define QMK_{prefix}KEYCODES_VERSION_BCD {bcd}')
+ lines.append(f'#define QMK_{prefix}KEYCODES_VERSION_MAJOR {major}')
+ lines.append(f'#define QMK_{prefix}KEYCODES_VERSION_MINOR {minor}')
+ lines.append(f'#define QMK_{prefix}KEYCODES_VERSION_PATCH {patch}')
+
+
@cli.argument('-v', '--version', arg_only=True, required=True, help='Version of keycodes to generate.')
@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")
@@ -138,6 +152,7 @@ def generate_keycodes(cli):
keycodes = load_spec(cli.args.version)
+ _generate_version(keycodes_h_lines, keycodes)
_generate_ranges(keycodes_h_lines, keycodes)
_generate_defines(keycodes_h_lines, keycodes)
_generate_helpers(keycodes_h_lines, keycodes)
@@ -160,6 +175,7 @@ def generate_keycode_extras(cli):
keycodes = load_spec(cli.args.version, cli.args.lang)
+ _generate_version(keycodes_h_lines, keycodes, f'{cli.args.lang.upper()}_')
_generate_aliases(keycodes_h_lines, keycodes)
# Show the results
diff --git a/lib/python/qmk/cli/generate/keycodes_tests.py b/lib/python/qmk/cli/generate/keycodes_tests.py
deleted file mode 100644
index 453b4693a7..0000000000
--- a/lib/python/qmk/cli/generate/keycodes_tests.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""Used by the make system to generate a keycode lookup table from keycodes_{version}.json
-"""
-from milc import cli
-
-from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
-from qmk.commands import dump_lines
-from qmk.path import normpath
-from qmk.keycodes import load_spec
-
-
-def _generate_defines(lines, keycodes):
- lines.append('')
- lines.append('std::map<uint16_t, std::string> KEYCODE_ID_TABLE = {')
- for key, value in keycodes["keycodes"].items():
- lines.append(f' {{{value.get("key")}, "{value.get("key")}"}},')
- lines.append('};')
-
-
[email protected]('-v', '--version', arg_only=True, required=True, help='Version of keycodes to generate.')
[email protected]('-o', '--output', arg_only=True, type=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]('Used by the make system to generate a keycode lookup table from keycodes_{version}.json', hidden=True)
-def generate_keycodes_tests(cli):
- """Generates a keycode to identifier lookup table for unit test output.
- """
-
- # Build the keycodes.h file.
- keycodes_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '// clang-format off']
- keycodes_h_lines.append('extern "C" {\n#include <keycode.h>\n}')
- keycodes_h_lines.append('#include <map>')
- keycodes_h_lines.append('#include <string>')
- keycodes_h_lines.append('#include <cstdint>')
-
- keycodes = load_spec(cli.args.version)
-
- _generate_defines(keycodes_h_lines, keycodes)
-
- # Show the results
- dump_lines(cli.args.output, keycodes_h_lines, cli.args.quiet)
diff --git a/lib/python/qmk/cli/generate/make_dependencies.py b/lib/python/qmk/cli/generate/make_dependencies.py
index 331132a20f..9548187888 100755
--- a/lib/python/qmk/cli/generate/make_dependencies.py
+++ b/lib/python/qmk/cli/generate/make_dependencies.py
@@ -8,7 +8,7 @@ from argcomplete.completers import FilesCompleter
from qmk.commands import dump_lines
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.keymap import keymap_completer, locate_keymap
-from qmk.path import normpath, FileType
+from qmk.path import normpath, FileType, unix_style_path
@cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), completer=FilesCompleter('.json'), help='A configurator export JSON.')
@@ -53,4 +53,4 @@ def generate_make_dependencies(cli):
for file in interesting_files:
check_files.append(Path('users') / cli.args.keymap / file)
- dump_lines(cli.args.output, [f'generated-files: $(wildcard {found})\n' for found in check_files])
+ dump_lines(cli.args.output, [f'generated-files: $(wildcard {unix_style_path(found)})\n' for found in check_files])
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index 5291556109..358a22fd1d 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -46,6 +46,15 @@ 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
+
+
@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 +89,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':