From 1354424fd70984ccc9a6242dde893c96b7340b56 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 24 Jan 2025 09:22:49 +1100 Subject: [PATCH] First stab at keycodes. --- data/constants/keycodes/keycodes_0.0.7.hjson | 7 +++ data/schemas/community_module.jsonschema | 16 +++++ lib/python/qmk/cli/format/json.py | 13 +++- .../qmk/cli/generate/community_modules.py | 59 ++++++++++--------- lib/python/qmk/community_modules.py | 48 +++++++++++++++ lib/python/qmk/json_encoders.py | 21 +++++++ modules/hello_world/hello_world.c | 16 +++++ modules/hello_world/qmk_module.json | 7 +++ quantum/keycodes.h | 3 + 9 files changed, 159 insertions(+), 31 deletions(-) create mode 100644 data/constants/keycodes/keycodes_0.0.7.hjson create mode 100644 data/schemas/community_module.jsonschema create mode 100644 lib/python/qmk/community_modules.py create mode 100644 modules/hello_world/qmk_module.json diff --git a/data/constants/keycodes/keycodes_0.0.7.hjson b/data/constants/keycodes/keycodes_0.0.7.hjson new file mode 100644 index 0000000000..52e1a50443 --- /dev/null +++ b/data/constants/keycodes/keycodes_0.0.7.hjson @@ -0,0 +1,7 @@ +{ + "ranges": { + "0x77C0/0x003F": { + "define": "QK_COMMUNITY_MODULE" + } + } +} diff --git a/data/schemas/community_module.jsonschema b/data/schemas/community_module.jsonschema new file mode 100644 index 0000000000..34362a918b --- /dev/null +++ b/data/schemas/community_module.jsonschema @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema#", + "$id": "qmk.community_module.v1", + "title": "Community Module Information", + "type": "object", + "required": ["module_name", "maintainer"] + "properties": { + "module_name": {"$ref": "qmk.definitions.v1#/text_identifier"}, + "maintainer": {"$ref": "qmk.definitions.v1#/text_identifier"}, + "url": { + "type": "string", + "format": "uri" + }, + "keycodes": {"$ref": "qmk.definitions.v1#/keycode_decl_array"}, + } +} 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 @@ -30,6 +30,13 @@ def _detect_json_format(file, json_data): except ValidationError: pass + 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') @@ -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') -@cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') +@cli.argument('-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 index cc192e20e4..e634521f77 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -2,40 +2,17 @@ import contextlib from argcomplete.completers import FilesCompleter from milc import cli -from milc.attrdict import AttrDict 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 - - -class ModuleAPI(AttrDict): - def __init__(self, **kwargs): - super().__init__() - for key, value in kwargs.items(): - self[key] = value - - -MODULE_API_LIST = [ - ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', guard=None, header=None), - ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', guard=None, header=None), - ModuleAPI(ret_type='bool', name='pre_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), - ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), - ModuleAPI(ret_type='void', name='post_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), - ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', guard=None, header=None), - ModuleAPI(ret_type='void', name='suspend_power_down', args='void', call_params='', guard=None, header=None), - ModuleAPI(ret_type='void', name='suspend_wakeup_init', args='void', call_params='', guard=None, header=None), - ModuleAPI(ret_type='bool', name='shutdown', args='bool jump_to_bootloader', call_params='jump_to_bootloader', guard=None, header=None), - ModuleAPI(ret_type='bool', name='process_detected_host_os', args='os_variant_t os', call_params='os', guard="defined(OS_DETECTION_ENABLE)", header="os_detection.h"), -] - -MODULE_API_VERSION = '20250122' +from qmk.community_modules import MODULE_API_LIST, MODULE_API_VERSION, load_module_jsons @contextlib.contextmanager -def _api_guard(lines, api): +def _render_api_guard(lines, api): if api.guard: lines.append(f'#if {api.guard}') yield @@ -47,15 +24,35 @@ def _render_api_header(api): lines = [] if api.header: lines.append('') - with _api_guard(lines, api): + 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: + keycodes = module_json.get('keycodes', []) + 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},') + lines.append(' LAST_COMMUNITY_MODULE_KEY') + lines.append('};') + lines.append('_Static_assert(LAST_COMMUNITY_MODULE_KEY <= QK_COMMUNITY_MODULE_MAX, "Too many community module keycodes");') + return lines + + def _render_api_declarations(api, module, user_kb=True): lines = [] lines.append('') - with _api_guard(lines, api): + 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});') @@ -66,7 +63,7 @@ def _render_api_declarations(api, module, user_kb=True): def _render_api_implementations(api, module): lines = [] lines.append('') - with _api_guard(lines, api): + with _render_api_guard(lines, api): # _user lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}_user({api.args}) {{') if api.ret_type == 'bool': @@ -100,7 +97,7 @@ def _render_api_implementations(api, module): def _render_core_implementation(api, modules): lines = [] lines.append('') - with _api_guard(lines, api): + 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') @@ -132,6 +129,7 @@ def generate_community_modules_h(cli): '#pragma once', '#include ', '#include ', + '#include ', '', f'#define COMMUNITY_MODULES_API_VERSION {MODULE_API_VERSION}', f'#define ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(x) _Static_assert((x) <= COMMUNITY_MODULES_API_VERSION, "Community module requires higher version of QMK modules API -- needs: " #x ", current: {MODULE_API_VERSION}.")', @@ -141,7 +139,10 @@ def generate_community_modules_h(cli): ] 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 MODULE_API_LIST: lines.extend(_render_api_header(api)) diff --git a/lib/python/qmk/community_modules.py b/lib/python/qmk/community_modules.py new file mode 100644 index 0000000000..fa9b1ac84b --- /dev/null +++ b/lib/python/qmk/community_modules.py @@ -0,0 +1,48 @@ +import os +from pathlib import Path + +from milc.attrdict import AttrDict + +from qmk.json_schema import json_load, validate +from qmk.util import truthy + +class ModuleAPI(AttrDict): + def __init__(self, **kwargs): + super().__init__() + for key, value in kwargs.items(): + self[key] = value + + +MODULE_API_LIST = [ + ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='bool', name='pre_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), + ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), + ModuleAPI(ret_type='void', name='post_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), + ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='void', name='suspend_power_down', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='void', name='suspend_wakeup_init', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='bool', name='shutdown', args='bool jump_to_bootloader', call_params='jump_to_bootloader', guard=None, header=None), + ModuleAPI(ret_type='bool', name='process_detected_host_os', args='os_variant_t os', call_params='os', guard="defined(OS_DETECTION_ENABLE)", header="os_detection.h"), +] + +MODULE_API_VERSION = '20250122' + +def load_module_jsons(modules): + """Load the module JSON files, matching the specified order. + """ + module_jsons = [] + for module in modules: + + module_json_path = Path('modules') / module / 'qmk_module.json' + if not module_json_path.exists(): + raise FileNotFoundError(f'Module JSON file not found: {module_json_path}') + + module_json = json_load(module_json_path) + + if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False): + validate(module_json, 'qmk.community_module.v1') + + module_jsons.append(module_json) + + return module_jsons diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py index 0e4ad1d220..25a01e9e21 100755 --- a/lib/python/qmk/json_encoders.py +++ b/lib/python/qmk/json_encoders.py @@ -235,3 +235,24 @@ 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 == 'keycodes': + return '03keycodes' + + return key diff --git a/modules/hello_world/hello_world.c b/modules/hello_world/hello_world.c index 0f02640cc4..c3bbbc64b8 100644 --- a/modules/hello_world/hello_world.c +++ b/modules/hello_world/hello_world.c @@ -21,3 +21,19 @@ void housekeeping_task_hello_world(void) { print("Hello, world! I'm a QMK based keyboard!\n"); } } + +bool process_record_hello_world(uint16_t keycode, keyrecord_t *record) { + if (!process_record_hello_world_kb(keycode, record)) { + return false; + } + + switch (keycode) { + case CM_HELLO: + if (record->event.pressed) { + SEND_STRING("Hello there."); + break; + } + } + + return true; +} diff --git a/modules/hello_world/qmk_module.json b/modules/hello_world/qmk_module.json new file mode 100644 index 0000000000..87410e08e6 --- /dev/null +++ b/modules/hello_world/qmk_module.json @@ -0,0 +1,7 @@ +{ + "module_name": "Hello World", + "maintainer": "QMK Maintainers", + "keycodes": [ + { "key": "CM_HELLO" } + ] +} diff --git a/quantum/keycodes.h b/quantum/keycodes.h index 5929e35687..b4fc38f5ff 100644 --- a/quantum/keycodes.h +++ b/quantum/keycodes.h @@ -76,6 +76,8 @@ enum qk_keycode_ranges { QK_MACRO_MAX = 0x777F, QK_CONNECTION = 0x7780, QK_CONNECTION_MAX = 0x77BF, + QK_COMMUNITY_MODULE = 0x77C0, + QK_COMMUNITY_MODULE_MAX = 0x77FF, QK_LIGHTING = 0x7800, QK_LIGHTING_MAX = 0x78FF, QK_QUANTUM = 0x7C00, @@ -1476,6 +1478,7 @@ enum qk_keycode_defines { #define IS_QK_STENO(code) ((code) >= QK_STENO && (code) <= QK_STENO_MAX) #define IS_QK_MACRO(code) ((code) >= QK_MACRO && (code) <= QK_MACRO_MAX) #define IS_QK_CONNECTION(code) ((code) >= QK_CONNECTION && (code) <= QK_CONNECTION_MAX) +#define IS_QK_COMMUNITY_MODULE(code) ((code) >= QK_COMMUNITY_MODULE && (code) <= QK_COMMUNITY_MODULE_MAX) #define IS_QK_LIGHTING(code) ((code) >= QK_LIGHTING && (code) <= QK_LIGHTING_MAX) #define IS_QK_QUANTUM(code) ((code) >= QK_QUANTUM && (code) <= QK_QUANTUM_MAX) #define IS_QK_KB(code) ((code) >= QK_KB && (code) <= QK_KB_MAX)