Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
Nick Brassel
519bd5ac45
Keycode alias support. 2025-02-07 23:51:53 +11:00
Nick Brassel
4758f6c695
Header ordering ftw. 2025-02-07 23:38:24 +11:00
Nick Brassel
b4981aefe4
Ordering so module introspection types can be used in keymap.c. 2025-02-07 23:33:55 +11:00
Nick Brassel
dd2ea4aae0
Add introspection.h support and example. 2025-02-07 23:30:48 +11:00
Nick Brassel
6f4db948e3
Add introspection.c example. 2025-02-07 23:20:02 +11:00
Nick Brassel
3a5c075cf7
Add support for module-level introspection.c to be automatically injected. 2025-02-07 23:07:57 +11:00
Nick Brassel
e86d8d248f
Preprocessor consistency. 2025-02-06 16:49:33 +11:00
Nick Brassel
368972afa6
Merge issue. 2025-02-06 10:06:27 +11:00
Nick Brassel
ad90132710
Subdirs and relative pathing. 2025-02-05 21:26:59 +11:00
Nick Brassel
520b5bf980
Merge remote-tracking branch 'upstream/develop' into community-modules 2025-02-05 20:40:33 +11:00
Nick Brassel
d7e374be12
Allow for modules in subdirectories. 2025-01-29 23:57:43 +11:00
Nick Brassel
37e3ee051c
Add onekey. 2025-01-24 23:06:35 +11:00
Nick Brassel
8b47562c5d
Userspace modules. 2025-01-24 22:57:54 +11:00
Nick Brassel
d3300daaa7
Automatic addition of community_module label. 2025-01-24 21:49:17 +11:00
Nick Brassel
fa53413a5d
Ensure modules are subject to formatting rules. 2025-01-24 21:47:45 +11:00
Nick Brassel
c6454b5585
Add support for features object. 2025-01-24 21:42:33 +11:00
Nick Brassel
1c4c6ae280
Appease the gods of formatting. 2025-01-24 09:26:14 +11:00
Nick Brassel
1354424fd7
First stab at keycodes. 2025-01-24 09:22:49 +11:00
Nick Brassel
54673745a6
nitpicks 2025-01-24 01:17:16 +11:00
Nick Brassel
c9ee24ba86
keyboard and keymap modules 2025-01-24 01:11:45 +11:00
Nick Brassel
d4733cc574
user/kb module hooks. 2025-01-23 23:21:07 +11:00
Nick Brassel
fe3eac28ba
Preprocessor guards, OS detection hooks. 2025-01-22 17:00:06 +11:00
Nick Brassel
5a70aecb6b
Suspend callbacks. 2025-01-22 14:19:56 +11:00
Nick Brassel
8f79ed1f23
More APIs added, versioning. 2025-01-22 14:13:20 +11:00
Nick Brassel
11b9ff0c4f
Appease the gods of formatting. 2025-01-22 00:59:50 +11:00
Nick Brassel
e3fe9fac60
First stab at codegen. 2025-01-22 00:50:55 +11:00
31 changed files with 746 additions and 82 deletions

4
.github/labeler.yml vendored
View file

@ -54,3 +54,7 @@ dd:
- data/constants/** - data/constants/**
- data/mappings/** - data/mappings/**
- data/schemas/** - data/schemas/**
community_module:
- changed-files:
- any-glob-to-any-file:
- modules/**

View file

@ -10,6 +10,7 @@ on:
- 'lib/arm_atsam/**' - 'lib/arm_atsam/**'
- 'lib/lib8tion/**' - 'lib/lib8tion/**'
- 'lib/python/**' - 'lib/python/**'
- 'modules/**'
- 'platforms/**' - 'platforms/**'
- 'quantum/**' - 'quantum/**'
- 'tests/**' - 'tests/**'

View file

@ -112,6 +112,39 @@ endif
ifneq ("$(wildcard $(KEYBOARD_PATH_1)/rules.mk)","") ifneq ("$(wildcard $(KEYBOARD_PATH_1)/rules.mk)","")
include $(KEYBOARD_PATH_1)/rules.mk include $(KEYBOARD_PATH_1)/rules.mk
endif endif
# Create dependencies on DD keyboard config - structure validated elsewhere
DD_CONFIG_FILES :=
ifneq ("$(wildcard $(KEYBOARD_PATH_1)/info.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/info.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_2)/info.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/info.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_3)/info.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/info.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_4)/info.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/info.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_5)/info.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/info.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_1)/keyboard.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/keyboard.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_2)/keyboard.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/keyboard.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_3)/keyboard.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/keyboard.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_4)/keyboard.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/keyboard.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_5)/keyboard.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/keyboard.json
endif
MAIN_KEYMAP_PATH_1 := $(KEYBOARD_PATH_1)/keymaps/$(KEYMAP) MAIN_KEYMAP_PATH_1 := $(KEYBOARD_PATH_1)/keymaps/$(KEYMAP)
MAIN_KEYMAP_PATH_2 := $(KEYBOARD_PATH_2)/keymaps/$(KEYMAP) MAIN_KEYMAP_PATH_2 := $(KEYBOARD_PATH_2)/keymaps/$(KEYMAP)
@ -207,17 +240,17 @@ ifneq ("$(wildcard $(KEYMAP_JSON))", "")
include $(INFO_RULES_MK) include $(INFO_RULES_MK)
# Add rules to generate the keymap files - indentation here is important # Add rules to generate the keymap files - indentation here is important
$(INTERMEDIATE_OUTPUT)/src/keymap.c: $(KEYMAP_JSON) $(INTERMEDIATE_OUTPUT)/src/keymap.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
@$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
$(eval CMD=$(QMK_BIN) json2c --quiet --output $(KEYMAP_C) $(KEYMAP_JSON)) $(eval CMD=$(QMK_BIN) json2c --quiet --output $(KEYMAP_C) $(KEYMAP_JSON))
@$(BUILD_CMD) @$(BUILD_CMD)
$(INTERMEDIATE_OUTPUT)/src/config.h: $(KEYMAP_JSON) $(INTERMEDIATE_OUTPUT)/src/config.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
@$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
$(eval CMD=$(QMK_BIN) generate-config-h --quiet --output $(KEYMAP_H) $(KEYMAP_JSON)) $(eval CMD=$(QMK_BIN) generate-config-h --quiet --output $(KEYMAP_H) $(KEYMAP_JSON))
@$(BUILD_CMD) @$(BUILD_CMD)
$(INTERMEDIATE_OUTPUT)/src/keymap.h: $(KEYMAP_JSON) $(INTERMEDIATE_OUTPUT)/src/keymap.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
@$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
$(eval CMD=$(QMK_BIN) generate-keymap-h --quiet --output $(INTERMEDIATE_OUTPUT)/src/keymap.h $(KEYMAP_JSON)) $(eval CMD=$(QMK_BIN) generate-keymap-h --quiet --output $(INTERMEDIATE_OUTPUT)/src/keymap.h $(KEYMAP_JSON))
@$(BUILD_CMD) @$(BUILD_CMD)
@ -226,6 +259,32 @@ generated-files: $(INTERMEDIATE_OUTPUT)/src/config.h $(INTERMEDIATE_OUTPUT)/src/
endif endif
# Community modules
$(INTERMEDIATE_OUTPUT)/src/community_modules.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
@$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
$(eval CMD=$(QMK_BIN) generate-community-modules-h -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(KEYMAP_JSON))
@$(BUILD_CMD)
$(INTERMEDIATE_OUTPUT)/src/community_modules.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
@$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
$(eval CMD=$(QMK_BIN) generate-community-modules-c -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(KEYMAP_JSON))
@$(BUILD_CMD)
$(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
@$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
$(eval CMD=$(QMK_BIN) generate-community-modules-introspection-c -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c $(KEYMAP_JSON))
@$(BUILD_CMD)
$(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
@$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
$(eval CMD=$(QMK_BIN) generate-community-modules-introspection-h -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h $(KEYMAP_JSON))
@$(BUILD_CMD)
SRC += $(INTERMEDIATE_OUTPUT)/src/community_modules.c
generated-files: $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h
include $(BUILDDEFS_PATH)/converters.mk include $(BUILDDEFS_PATH)/converters.mk
# Generate the board's version.h file. # Generate the board's version.h file.
@ -348,40 +407,6 @@ ifneq ("$(wildcard $(KEYBOARD_PATH_5)/post_config.h)","")
POST_CONFIG_H += $(KEYBOARD_PATH_5)/post_config.h POST_CONFIG_H += $(KEYBOARD_PATH_5)/post_config.h
endif endif
# Create dependencies on DD keyboard config - structure validated elsewhere
DD_CONFIG_FILES :=
ifneq ("$(wildcard $(KEYBOARD_PATH_1)/info.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/info.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_2)/info.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/info.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_3)/info.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/info.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_4)/info.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/info.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_5)/info.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/info.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_1)/keyboard.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/keyboard.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_2)/keyboard.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/keyboard.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_3)/keyboard.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/keyboard.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_4)/keyboard.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/keyboard.json
endif
ifneq ("$(wildcard $(KEYBOARD_PATH_5)/keyboard.json)","")
DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/keyboard.json
endif
CONFIG_H += $(INTERMEDIATE_OUTPUT)/src/info_config.h CONFIG_H += $(INTERMEDIATE_OUTPUT)/src/info_config.h
KEYBOARD_SRC += $(INTERMEDIATE_OUTPUT)/src/default_keyboard.c KEYBOARD_SRC += $(INTERMEDIATE_OUTPUT)/src/default_keyboard.c

View file

@ -0,0 +1,7 @@
{
"ranges": {
"0x77C0/0x003F": {
"define": "QK_COMMUNITY_MODULE"
}
}
}

View file

@ -0,0 +1,17 @@
{
"$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"},
"features": {"$ref": "qmk.keyboard.v1#/definitions/features_config"},
}
}

View file

@ -31,6 +31,11 @@
"pins": {"$ref": "qmk.definitions.v1#/mcu_pin_array"} "pins": {"$ref": "qmk.definitions.v1#/mcu_pin_array"}
} }
} }
"features_config": {
"$ref": "qmk.definitions.v1#/boolean_array",
"propertyNames": {"$ref": "qmk.definitions.v1#/snake_case"},
"not": {"required": ["lto"]}
},
}, },
"type": "object", "type": "object",
"not": {"required": ["vendorId", "productId"]}, // reject via keys... "not": {"required": ["vendorId", "productId"]}, // reject via keys...
@ -328,11 +333,7 @@
"enabled": {"type": "boolean"} "enabled": {"type": "boolean"}
} }
}, },
"features": { "features": { "$ref": "#/definitions/features_config" },
"$ref": "qmk.definitions.v1#/boolean_array",
"propertyNames": {"$ref": "qmk.definitions.v1#/snake_case"},
"not": {"required": ["lto"]}
},
"indicators": { "indicators": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -467,6 +468,12 @@
"rows": {"$ref": "qmk.definitions.v1#/mcu_pin_array"} "rows": {"$ref": "qmk.definitions.v1#/mcu_pin_array"}
} }
}, },
"modules": {
"type": "array",
"items": {
"type": "string"
}
},
"mouse_key": { "mouse_key": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -71,6 +71,12 @@
"config": {"$ref": "qmk.keyboard.v1"}, "config": {"$ref": "qmk.keyboard.v1"},
"notes": { "notes": {
"type": "string" "type": "string"
},
"modules": {
"type": "array",
"items": {
"type": "string"
}
} }
} }
} }

View file

@ -0,0 +1,7 @@
// Copyright 2025 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include QMK_KEYBOARD_H
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
LAYOUT_ortho_1x1(CM_HELLO)
};

View file

@ -0,0 +1,3 @@
{
"modules": ["hello_world"]
}

View file

@ -49,6 +49,7 @@ subcommands = [
'qmk.cli.generate.api', 'qmk.cli.generate.api',
'qmk.cli.generate.autocorrect_data', 'qmk.cli.generate.autocorrect_data',
'qmk.cli.generate.compilation_database', 'qmk.cli.generate.compilation_database',
'qmk.cli.generate.community_modules',
'qmk.cli.generate.config_h', 'qmk.cli.generate.config_h',
'qmk.cli.generate.develop_pr_list', 'qmk.cli.generate.develop_pr_list',
'qmk.cli.generate.dfu_header', 'qmk.cli.generate.dfu_header',

View file

@ -10,7 +10,7 @@ from qmk.path import normpath
from qmk.c_parse import c_source_files from qmk.c_parse import c_source_files
c_file_suffixes = ('c', 'h', 'cpp', 'hpp') 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') ignored = ('tmk_core/protocol/usb_hid', 'platforms/chibios/boards')

View file

@ -9,7 +9,7 @@ from milc import cli
from qmk.info import info_json from qmk.info import info_json
from qmk.json_schema import json_load, validate 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 from qmk.path import normpath
@ -30,6 +30,13 @@ def _detect_json_format(file, json_data):
except ValidationError: except ValidationError:
pass 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: if json_encoder is None:
try: try:
validate(json_data, 'qmk.keyboard.v1') validate(json_data, 'qmk.keyboard.v1')
@ -54,6 +61,8 @@ def _get_json_encoder(file, json_data):
json_encoder = KeymapJSONEncoder json_encoder = KeymapJSONEncoder
elif cli.args.format == 'userspace': elif cli.args.format == 'userspace':
json_encoder = UserspaceJSONEncoder json_encoder = UserspaceJSONEncoder
elif cli.args.format == 'community_module':
json_encoder = CommunityModuleJSONEncoder
else: else:
# This should be impossible # This should be impossible
cli.log.error('Unknown format: %s', cli.args.format) 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('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('-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.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) @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)

View file

@ -0,0 +1,258 @@
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, MODULE_API_VERSION, load_module_jsons, find_module_path
@contextlib.contextmanager
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
lines.append(f' // From module: {module_name}')
keycodes = module_json.get('keycodes', [])
if len(keycodes) > 0:
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
@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.h for.')
@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file')
@cli.subcommand('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
lines = [
GPL2_HEADER_C_LIKE,
GENERATED_HEADER_C_LIKE,
'#pragma once',
'#include <stdint.h>',
'#include <stdbool.h>',
'#include <keycodes.h>',
'',
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}.")',
'',
'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 MODULE_API_LIST:
lines.extend(_render_api_header(api))
for module in modules:
lines.append('')
lines.append(f'// From module: {module}')
for api in MODULE_API_LIST:
lines.extend(_render_api_declarations(api, Path(module).name))
lines.append('')
lines.append('// Core wrapper')
for api in MODULE_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)
@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.')
@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file')
@cli.subcommand('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
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 MODULE_API_LIST:
lines.extend(_render_api_implementations(api, Path(module).name))
for api in MODULE_API_LIST:
lines.extend(_render_core_implementation(api, modules))
dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True)
@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.')
@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file')
@cli.subcommand('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)
@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.')
@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file')
@cli.subcommand('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)

View file

@ -6,12 +6,13 @@ from dotty_dict import dotty
from argcomplete.completers import FilesCompleter from argcomplete.completers import FilesCompleter
from milc import cli 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.json_schema import json_load
from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.commands import dump_lines, parse_configurator_json from qmk.commands import dump_lines, parse_configurator_json
from qmk.path import normpath, FileType from qmk.path import normpath, FileType
from qmk.constants import GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE 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): def generate_rule(rules_key, rules_value):
@ -46,6 +47,41 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict):
return generate_rule(rules_key, rules_value) 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'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('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('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@ -80,10 +116,7 @@ def generate_rules_mk(cli):
# Iterate through features to enable/disable them # Iterate through features to enable/disable them
if 'features' in kb_info_json: if 'features' in kb_info_json:
for feature, enabled in kb_info_json['features'].items(): rules_mk_lines.extend(generate_features_rules(kb_info_json['features']))
feature = feature.upper()
enabled = 'yes' if enabled else 'no'
rules_mk_lines.append(generate_rule(f'{feature}_ENABLE', enabled))
# Set SPLIT_TRANSPORT, if needed # Set SPLIT_TRANSPORT, if needed
if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom': if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom':
@ -99,6 +132,8 @@ def generate_rules_mk(cli):
if converter: if converter:
rules_mk_lines.append(generate_rule('CONVERT_TO', 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 # Show the results
dump_lines(cli.args.output, rules_mk_lines) dump_lines(cli.args.output, rules_mk_lines)

View file

@ -52,6 +52,11 @@ def show_keymap(kb_info_json, title_caps=True):
if keymap_path and keymap_path.suffix == '.json': if keymap_path and keymap_path.suffix == '.json':
keymap_data = json.load(keymap_path.open(encoding='utf-8')) 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 = keymap_data['layout']
layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names

View file

@ -98,11 +98,14 @@ def in_virtualenv():
return active_prefix != sys.prefix 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 """Handle dumping to stdout or file
Creates parent folders if required Creates parent folders if required
""" """
generated = '\n'.join(lines) + '\n' 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 != '-': if output_file and output_file.name != '-':
output_file.parent.mkdir(parents=True, exist_ok=True) output_file.parent.mkdir(parents=True, exist_ok=True)
if output_file.exists(): if output_file.exists():

View file

@ -0,0 +1,93 @@
import os
from pathlib import Path
from milc.attrdict import AttrDict
from qmk.json_schema import json_load, validate
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
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 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))

View file

@ -1059,3 +1059,30 @@ def keymap_json(keyboard, keymap, force_layout=None):
_extract_config_h(kb_info_json, parse_config_h_file(keymap_config)) _extract_config_h(kb_info_json, parse_config_h_file(keymap_config))
return kb_info_json 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

View file

@ -235,3 +235,31 @@ class UserspaceJSONEncoder(QMKJSONEncoder):
return '01build_targets' return '01build_targets'
return key 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

View file

@ -334,33 +334,6 @@ def write_json(keyboard, keymap, layout, layers, macros=None):
return write_file(keymap_file, keymap_content) 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): def locate_keymap(keyboard, keymap, force_layout=None):
"""Returns the path to a keymap for a specific keyboard. """Returns the path to a keymap for a specific keyboard.
""" """

Some files were not shown because too many files have changed in this diff Show more