Community modules (#24848)

This commit is contained in:
Nick Brassel 2025-02-26 22:25:41 +11:00 committed by GitHub
parent 63b095212b
commit 1efc82403b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 987 additions and 84 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.
@ -315,6 +374,14 @@ endif
# Find all of the config.h files and add them to our CONFIG_H define. # Find all of the config.h files and add them to our CONFIG_H define.
CONFIG_H := CONFIG_H :=
define config_h_community_module_appender
ifneq ("$(wildcard $(1)/config.h)","")
CONFIG_H += $(1)/config.h
endif
endef
$(foreach module,$(COMMUNITY_MODULE_PATHS),$(eval $(call config_h_community_module_appender,$(module))))
ifneq ("$(wildcard $(KEYBOARD_PATH_5)/config.h)","") ifneq ("$(wildcard $(KEYBOARD_PATH_5)/config.h)","")
CONFIG_H += $(KEYBOARD_PATH_5)/config.h CONFIG_H += $(KEYBOARD_PATH_5)/config.h
endif endif
@ -332,6 +399,14 @@ ifneq ("$(wildcard $(KEYBOARD_PATH_1)/config.h)","")
endif endif
POST_CONFIG_H := POST_CONFIG_H :=
define post_config_h_community_module_appender
ifneq ("$(wildcard $(1)/post_config.h)","")
POST_CONFIG_H += $(1)/post_config.h
endif
endef
$(foreach module,$(COMMUNITY_MODULE_PATHS),$(eval $(call post_config_h_community_module_appender,$(module))))
ifneq ("$(wildcard $(KEYBOARD_PATH_1)/post_config.h)","") ifneq ("$(wildcard $(KEYBOARD_PATH_1)/post_config.h)","")
POST_CONFIG_H += $(KEYBOARD_PATH_1)/post_config.h POST_CONFIG_H += $(KEYBOARD_PATH_1)/post_config.h
endif endif
@ -348,40 +423,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
@ -462,6 +503,13 @@ ifneq ("$(wildcard $(KEYBOARD_PATH_5)/post_rules.mk)","")
include $(KEYBOARD_PATH_5)/post_rules.mk include $(KEYBOARD_PATH_5)/post_rules.mk
endif endif
define post_rules_mk_community_module_includer
ifneq ("$(wildcard $(1)/post_rules.mk)","")
include $(1)/post_rules.mk
endif
endef
$(foreach module,$(COMMUNITY_MODULE_PATHS),$(eval $(call post_rules_mk_community_module_includer,$(module))))
ifneq ("$(wildcard $(KEYMAP_PATH)/config.h)","") ifneq ("$(wildcard $(KEYMAP_PATH)/config.h)","")
CONFIG_H += $(KEYMAP_PATH)/config.h CONFIG_H += $(KEYMAP_PATH)/config.h
endif endif

View File

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

View File

@ -0,0 +1,25 @@
{
keyboard_pre_init: {
ret_type: void
args: void
}
keyboard_post_init: {
ret_type: void
args: void
}
pre_process_record: {
ret_type: bool
args: uint16_t keycode, keyrecord_t *record
call_params: keycode, record
}
process_record: {
ret_type: bool
args: uint16_t keycode, keyrecord_t *record
call_params: keycode, record
}
post_process_record: {
ret_type: void
args: uint16_t keycode, keyrecord_t *record
call_params: keycode, record
}
}

View File

@ -0,0 +1,26 @@
{
housekeeping_task: {
ret_type: void
args: void
}
suspend_power_down: {
ret_type: void
args: void
}
suspend_wakeup_init: {
ret_type: void
args: void
}
shutdown: {
ret_type: bool
args: bool jump_to_bootloader
call_params: jump_to_bootloader
}
process_detected_host_os: {
ret_type: bool
args: os_variant_t os
call_params: os
guard: defined(OS_DETECTION_ENABLE)
header: os_detection.h
}
}

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

@ -60,6 +60,7 @@
"items": [ "items": [
{ "text": "Customizing Functionality", "link": "/custom_quantum_functions" }, { "text": "Customizing Functionality", "link": "/custom_quantum_functions" },
{ "text": "Driver Installation with Zadig", "link": "/driver_installation_zadig" }, { "text": "Driver Installation with Zadig", "link": "/driver_installation_zadig" },
{ "text": "Community Modules", "link": "/features/community_modules" },
{ "text": "Keymap Overview", "link": "/keymap" }, { "text": "Keymap Overview", "link": "/keymap" },
{ {
"text": "Development Environments", "text": "Development Environments",

View File

@ -9,12 +9,19 @@ This page does not assume any special knowledge about QMK, but reading [Understa
We have structured QMK as a hierarchy: We have structured QMK as a hierarchy:
* Core (`_quantum`) * Core (`_quantum`)
* Community Module (`_<module>`)
* Community Module -> Keyboard/Revision (`_<module>_kb`)
* Community Module -> Keymap (`_<module>_user`)
* Keyboard/Revision (`_kb`) * Keyboard/Revision (`_kb`)
* Keymap (`_user`) * Keymap (`_user`)
Each of the functions described below can be defined with a `_kb()` suffix or a `_user()` suffix. We intend for you to use the `_kb()` suffix at the Keyboard/Revision level, while the `_user()` suffix should be used at the Keymap level. Each of the functions described below can be defined with a `_kb()` suffix or a `_user()` suffix. We intend for you to use the `_kb()` suffix at the Keyboard/Revision level, while the `_user()` suffix should be used at the Keymap level.
When defining functions at the Keyboard/Revision level it is important that your `_kb()` implementation call `_user()` before executing anything else- otherwise the keymap level function will never be called. When defining functions at the Keyboard/Revision level it is important that your `_kb()` implementation call `_user()` at an appropriate location, otherwise the keymap level function will never be called.
Functions at the `_<module>_xxx()` level are intended to allow keyboards or keymaps to override or enhance the processing associated with a [community module](/features/community_modules).
When defining module overrides such as `process_record_<module>()`, the same pattern should be used; the module must invoke `process_record_<module>_kb()` as appropriate.
# Custom Keycodes # Custom Keycodes
@ -99,7 +106,7 @@ These are the three main initialization functions, listed in the order that they
* `keyboard_post_init_*` - Happens at the end of the firmware's startup process. This is where you'd want to put "customization" code, for the most part. * `keyboard_post_init_*` - Happens at the end of the firmware's startup process. This is where you'd want to put "customization" code, for the most part.
::: warning ::: warning
For most people, the `keyboard_post_init_user` function is what you want to call. For instance, this is where you want to set up things for RGB Underglow. For most people, the `keyboard_post_init_user` function is what you want to implement. For instance, this is where you want to set up things for RGB Underglow.
::: :::
## Keyboard Pre Initialization code ## Keyboard Pre Initialization code

View File

@ -0,0 +1,142 @@
# Community Modules
Community Modules are a feature within QMK which allows code to be implemented by third parties, making it available for other people to import into their own builds.
These modules can provide implementations which override or enhance normal QMK processing; initialization, key processing, suspend, and shutdown are some of the provided hooks which modules may implement.
## Adding a Community Module to your build
Community Modules have first-class support for [External Userspace](/newbs_external_userspace), and QMK strongly recommends using External Userspace for hosting keymaps and Community Modules together.
Modules must live in either of two locations:
* `<QMK_USERSPACE>/modules/`
* `<QMK_FIRMWARE>/modules/`
A basic module is provided within QMK itself -- `qmk/hello_world` -- which prints out a notification over [HID console](/faq_debug) after 10 seconds, and adds a new keycode, `COMMUNITY_MODULE_HELLO` (aliased to `CM_HELO`) which types `Hello there.` to the active application when the corresponding key is pressed.
To add this module to your build, in your keymap's directory create a `keymap.json` with the following content:
```json
{
"modules": [
"qmk/hello_world"
]
}
```
If you already have a `keymap.json`, you'll need to manually merge the `modules` section into your keymap.
::: warning
Community Modules are not supported by QMK Configurator. If you wish to use Community Modules, you must build your own firmware.
:::
## Adding a Community Module to your External Userspace
Module authors are encouraged to provide a git repository on GitHub which may be imported into a user's external userspace. If a user wishes to import a module repository, they can do the following:
```sh
cd /path/to/your/external/userspace
mkdir -p modules
# Replace the following {user} and {repo} with the author's community module repository
git submodule add https://github.com/{user}/{repo}.git modules/{user}
git submdule update --init --recursive
```
This will ensure the copy of the module is made in your userspace.
Add a new entry into your `keymap.json` with the desired modules, replacing `{user}` and `{module_name}` as appropriate:
```json
{
"modules": [
"qmk/hello_world",
"{user}/{module_name}"
]
}
```
::: info
The module listed in `keymap.json` is the relative path within the `modules/` directory. So long as the module is present _somewhere_ under `modules/`, then the `keymap.json` can refer to that path.
:::
## Writing a QMK Community Module
As stated earlier, Community Module authors are strongly encouraged to provide their modules through git, allowing users to leverage submodules to import functionality.
### `qmk_module.json`
A Community Module is denoted by a `qmk_module.json` file such as the following:
```json
{
"module_name": "Hello World",
"maintainer": "QMK Maintainers",
"features": {
"deferred_exec": true
},
"keycodes": [
{
"key": "COMMUNITY_MODULE_HELLO",
"aliases": ["CM_HELO"]
}
]
}
```
At minimum, the module must provide the `module_name` and `maintainer` fields.
The use of `features` matches the definition normally provided within `keyboard.json` and `info.json`, allowing a module to signal to the build system that it has its own dependencies. In the example above, it enables the _deferred executor_ feature whenever the above module is used in a build.
The `keycodes` array allows a module to provide new keycodes (as well as corresponding aliases) to a keymap.
### `rules.mk` / `post_rules.mk`
These two files follows standard QMK build system logic, allowing for `Makefile`-style customisation as if it were present in the keyboard or keymap.
### `<module>.c`
This file will be automatically added to the build if the filename matches the directory name. For example, the `qmk/hello_world` module contains a `hello_world.c` file, which is automatically added to the build.
::: info
Other files intended to be included must use the normal method of `SRC += my_file.c` inside `rules.mk`.
:::
::: tip
This file should use `ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1,0,0);` to enforce a minimum version of the API that it requires, ensuring the Community Module is built with a compatible version of QMK. The list of APIs and corresponding version is given at the bottom of this document. Note the use of commas instead of periods.
:::
### `introspection.c` / `introspection.h`
These two files hook into the keymap introspection logic -- the header is prepended before the user keymap, and the C source file is appended after the user keymap.
The header may provide definitions which are useful to the user's `keymap.c`.
The source file may provide functions which allow access to information specified in the user's `keymap.c`.
::: warning
Introspection is a relatively advanced topic within QMK, and existing patterns should be followed. If you need help please [open an issue](https://github.com/qmk/qmk_firmware/issues/new) or [chat with us on Discord](https://discord.gg/qmk).
:::
### Compatible APIs
Community Modules may provide specializations for the following APIs:
| Base API | API Format | Example (`hello_world` module) | API Version |
|----------------------------|-------------------------------------|----------------------------------------|-------------|
| `keyboard_pre_init` | `keyboard_pre_init_<module>` | `keyboard_pre_init_hello_world` | `0.1.0` |
| `keyboard_post_init` | `keyboard_post_init_<module>` | `keyboard_post_init_hello_world` | `0.1.0` |
| `pre_process_record` | `pre_process_record_<module>` | `pre_process_record_hello_world` | `0.1.0` |
| `process_record` | `process_record_<module>` | `process_record_hello_world` | `0.1.0` |
| `post_process_record` | `post_process_record_<module>` | `post_process_record_hello_world` | `0.1.0` |
| `housekeeping_task` | `housekeeping_task_<module>` | `housekeeping_task_hello_world` | `1.0.0` |
| `suspend_power_down` | `suspend_power_down_<module>` | `suspend_power_down_hello_world` | `1.0.0` |
| `suspend_wakeup_init` | `suspend_wakeup_init_<module>` | `suspend_wakeup_init_hello_world` | `1.0.0` |
| `shutdown` | `shutdown_<module>` | `shutdown_hello_world` | `1.0.0` |
| `process_detected_host_os` | `process_detected_host_os_<module>` | `process_detected_host_os_hello_world` | `1.0.0` |
::: info
An unspecified API is disregarded if a Community Module does not provide a specialization for it.
:::
Each API has an equivalent `_<module>_kb()` and `_<module>_user()` hook, as per the normal QMK [`_quantum`, `_kb`, and `_user` functions](/custom_quantum_functions#a-word-on-core-vs-keyboards-vs-keymap).

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_HELO)
};

View File

@ -0,0 +1,3 @@
{
"modules": ["qmk/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,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
@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
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
@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
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)
@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
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)
@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,42 @@ 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'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('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 +117,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 +133,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,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))

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.
""" """

View File

@ -0,0 +1,33 @@
// Copyright 2025 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include QMK_KEYBOARD_H
#include "introspection.h"
ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
uint32_t delayed_hello_world(uint32_t trigger_time, void *cb_arg) {
printf("Hello, world! I'm a QMK based keyboard! The keymap array size is %d bytes.\n", (int)hello_world_introspection().total_size);
return 0;
}
void keyboard_post_init_hello_world(void) {
keyboard_post_init_hello_world_kb();
defer_exec(10000, delayed_hello_world, NULL);
}
bool process_record_hello_world(uint16_t keycode, keyrecord_t *record) {
if (!process_record_hello_world_kb(keycode, record)) {
return false;
}
switch (keycode) {
case COMMUNITY_MODULE_HELLO:
if (record->event.pressed) {
SEND_STRING("Hello there.");
break;
}
}
return true;
}

View File

@ -0,0 +1,10 @@
// Copyright 2025 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
hello_world_introspection_t hello_world_introspection(void) {
hello_world_introspection_t introspection = {
.total_size = sizeof(keymaps),
.layer_count = sizeof(keymaps) / sizeof(keymaps[0]),
};
return introspection;
}

View File

@ -0,0 +1,10 @@
// Copyright 2025 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include QMK_KEYBOARD_H
typedef struct hello_world_introspection_t {
int16_t total_size;
int16_t layer_count;
} hello_world_introspection_t;
hello_world_introspection_t hello_world_introspection(void);

View File

@ -0,0 +1,13 @@
{
"module_name": "Hello World",
"maintainer": "QMK Maintainers",
"features": {
"deferred_exec": true
},
"keycodes": [
{
"key": "COMMUNITY_MODULE_HELLO",
"aliases": ["CM_HELO"]
}
]
}

View File

@ -0,0 +1,2 @@
# Just a simple rules.mk which tests that they work from a community module.
$(shell $(QMK_BIN) hello -n "from QMK's hello world community module")

View File

@ -45,7 +45,7 @@ typedef struct {
} tap_t; } tap_t;
/* Key event container for recording */ /* Key event container for recording */
typedef struct { typedef struct keyrecord_t {
keyevent_t event; keyevent_t event;
#ifndef NO_ACTION_TAPPING #ifndef NO_ACTION_TAPPING
tap_t tap; tap_t tap;

View File

@ -289,6 +289,21 @@ __attribute__((weak)) void keyboard_pre_init_kb(void) {
keyboard_pre_init_user(); keyboard_pre_init_user();
} }
/** \brief keyboard_pre_init_modules
*
* FIXME: needs doc
*/
__attribute__((weak)) void keyboard_pre_init_modules(void) {}
/** \brief keyboard_pre_init_quantum
*
* FIXME: needs doc
*/
void keyboard_pre_init_quantum(void) {
keyboard_pre_init_modules();
keyboard_pre_init_kb();
}
/** \brief keyboard_post_init_user /** \brief keyboard_post_init_user
* *
* FIXME: needs doc * FIXME: needs doc
@ -305,6 +320,23 @@ __attribute__((weak)) void keyboard_post_init_kb(void) {
keyboard_post_init_user(); keyboard_post_init_user();
} }
/** \brief keyboard_post_init_modules
*
* FIXME: needs doc
*/
__attribute__((weak)) void keyboard_post_init_modules(void) {}
/** \brief keyboard_post_init_quantum
*
* FIXME: needs doc
*/
void keyboard_post_init_quantum(void) {
keyboard_post_init_modules();
keyboard_post_init_kb();
}
/** \brief matrix_can_read /** \brief matrix_can_read
* *
* Allows overriding when matrix scanning operations should be executed. * Allows overriding when matrix scanning operations should be executed.
@ -323,7 +355,7 @@ void keyboard_setup(void) {
eeprom_driver_init(); eeprom_driver_init();
#endif #endif
matrix_setup(); matrix_setup();
keyboard_pre_init_kb(); keyboard_pre_init_quantum();
} }
#ifndef SPLIT_KEYBOARD #ifndef SPLIT_KEYBOARD
@ -355,6 +387,13 @@ __attribute__((weak)) bool should_process_keypress(void) {
return is_keyboard_master(); return is_keyboard_master();
} }
/** \brief housekeeping_task_modules
*
* Codegen will override this if community modules are enabled.
* This is specific to keyboard-level functionality.
*/
__attribute__((weak)) void housekeeping_task_modules(void) {}
/** \brief housekeeping_task_kb /** \brief housekeeping_task_kb
* *
* Override this function if you have a need to execute code for every keyboard main loop iteration. * Override this function if you have a need to execute code for every keyboard main loop iteration.
@ -374,6 +413,7 @@ __attribute__((weak)) void housekeeping_task_user(void) {}
* Invokes hooks for executing code after QMK is done after each loop iteration. * Invokes hooks for executing code after QMK is done after each loop iteration.
*/ */
void housekeeping_task(void) { void housekeeping_task(void) {
housekeeping_task_modules();
housekeeping_task_kb(); housekeeping_task_kb();
housekeeping_task_user(); housekeeping_task_user();
} }
@ -493,7 +533,7 @@ void keyboard_init(void) {
debug_enable = true; debug_enable = true;
#endif #endif
keyboard_post_init_kb(); /* Always keep this last */ keyboard_post_init_quantum(); /* Always keep this last */
} }
/** \brief key_event_task /** \brief key_event_task

View File

@ -76,6 +76,8 @@ enum qk_keycode_ranges {
QK_MACRO_MAX = 0x777F, QK_MACRO_MAX = 0x777F,
QK_CONNECTION = 0x7780, QK_CONNECTION = 0x7780,
QK_CONNECTION_MAX = 0x77BF, QK_CONNECTION_MAX = 0x77BF,
QK_COMMUNITY_MODULE = 0x77C0,
QK_COMMUNITY_MODULE_MAX = 0x77FF,
QK_LIGHTING = 0x7800, QK_LIGHTING = 0x7800,
QK_LIGHTING_MAX = 0x78FF, QK_LIGHTING_MAX = 0x78FF,
QK_QUANTUM = 0x7C00, 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_STENO(code) ((code) >= QK_STENO && (code) <= QK_STENO_MAX)
#define IS_QK_MACRO(code) ((code) >= QK_MACRO && (code) <= QK_MACRO_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_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_LIGHTING(code) ((code) >= QK_LIGHTING && (code) <= QK_LIGHTING_MAX)
#define IS_QK_QUANTUM(code) ((code) >= QK_QUANTUM && (code) <= QK_QUANTUM_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) #define IS_QK_KB(code) ((code) >= QK_KB && (code) <= QK_KB_MAX)

View File

@ -1,6 +1,10 @@
// Copyright 2022 Nick Brassel (@tzarc) // Copyright 2022 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#if defined(COMMUNITY_MODULES_ENABLE)
# include "community_modules_introspection.h"
#endif // defined(COMMUNITY_MODULES_ENABLE)
// Pull the actual keymap code so that we can inspect stuff from it // Pull the actual keymap code so that we can inspect stuff from it
#include KEYMAP_C #include KEYMAP_C
@ -171,3 +175,10 @@ __attribute__((weak)) const key_override_t* key_override_get(uint16_t key_overri
} }
#endif // defined(KEY_OVERRIDE_ENABLE) #endif // defined(KEY_OVERRIDE_ENABLE)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Community modules (must be last in this file!)
#if defined(COMMUNITY_MODULES_ENABLE)
# include "community_modules_introspection.c"
#endif // defined(COMMUNITY_MODULES_ENABLE)

View File

@ -72,6 +72,8 @@ static volatile struct usb_device_state maxprev_usb_device_state = {.configure_s
static volatile bool debouncing = false; static volatile bool debouncing = false;
static volatile fast_timer_t last_time = 0; static volatile fast_timer_t last_time = 0;
bool process_detected_host_os_modules(os_variant_t os);
void os_detection_task(void) { void os_detection_task(void) {
#ifdef OS_DETECTION_KEYBOARD_RESET #ifdef OS_DETECTION_KEYBOARD_RESET
// resetting the keyboard on the USB device state change callback results in instability, so delegate that to this task // resetting the keyboard on the USB device state change callback results in instability, so delegate that to this task
@ -96,12 +98,17 @@ void os_detection_task(void) {
if (detected_os != reported_os || first_report) { if (detected_os != reported_os || first_report) {
first_report = false; first_report = false;
reported_os = detected_os; reported_os = detected_os;
process_detected_host_os_modules(detected_os);
process_detected_host_os_kb(detected_os); process_detected_host_os_kb(detected_os);
} }
} }
} }
} }
__attribute__((weak)) bool process_detected_host_os_modules(os_variant_t os) {
return true;
}
__attribute__((weak)) bool process_detected_host_os_kb(os_variant_t detected_os) { __attribute__((weak)) bool process_detected_host_os_kb(os_variant_t detected_os) {
return process_detected_host_os_user(detected_os); return process_detected_host_os_user(detected_os);
} }

View File

@ -162,6 +162,10 @@ __attribute__((weak)) void tap_code16(uint16_t code) {
tap_code16_delay(code, code == KC_CAPS_LOCK ? TAP_HOLD_CAPS_DELAY : TAP_CODE_DELAY); tap_code16_delay(code, code == KC_CAPS_LOCK ? TAP_HOLD_CAPS_DELAY : TAP_CODE_DELAY);
} }
__attribute__((weak)) bool pre_process_record_modules(uint16_t keycode, keyrecord_t *record) {
return true;
}
__attribute__((weak)) bool pre_process_record_kb(uint16_t keycode, keyrecord_t *record) { __attribute__((weak)) bool pre_process_record_kb(uint16_t keycode, keyrecord_t *record) {
return pre_process_record_user(keycode, record); return pre_process_record_user(keycode, record);
} }
@ -174,6 +178,10 @@ __attribute__((weak)) bool process_action_kb(keyrecord_t *record) {
return true; return true;
} }
__attribute__((weak)) bool process_record_modules(uint16_t keycode, keyrecord_t *record) {
return true;
}
__attribute__((weak)) bool process_record_kb(uint16_t keycode, keyrecord_t *record) { __attribute__((weak)) bool process_record_kb(uint16_t keycode, keyrecord_t *record) {
return process_record_user(keycode, record); return process_record_user(keycode, record);
} }
@ -182,12 +190,22 @@ __attribute__((weak)) bool process_record_user(uint16_t keycode, keyrecord_t *re
return true; return true;
} }
__attribute__((weak)) void post_process_record_modules(uint16_t keycode, keyrecord_t *record) {}
__attribute__((weak)) void post_process_record_kb(uint16_t keycode, keyrecord_t *record) { __attribute__((weak)) void post_process_record_kb(uint16_t keycode, keyrecord_t *record) {
post_process_record_user(keycode, record); post_process_record_user(keycode, record);
} }
__attribute__((weak)) void post_process_record_user(uint16_t keycode, keyrecord_t *record) {} __attribute__((weak)) void post_process_record_user(uint16_t keycode, keyrecord_t *record) {}
__attribute__((weak)) bool shutdown_modules(bool jump_to_bootloader) {
return true;
}
__attribute__((weak)) void suspend_power_down_modules(void) {}
__attribute__((weak)) void suspend_wakeup_init_modules(void) {}
void shutdown_quantum(bool jump_to_bootloader) { void shutdown_quantum(bool jump_to_bootloader) {
clear_keyboard(); clear_keyboard();
#if defined(MIDI_ENABLE) && defined(MIDI_BASIC) #if defined(MIDI_ENABLE) && defined(MIDI_BASIC)
@ -199,11 +217,13 @@ void shutdown_quantum(bool jump_to_bootloader) {
# endif # endif
uint16_t timer_start = timer_read(); uint16_t timer_start = timer_read();
PLAY_SONG(goodbye_song); PLAY_SONG(goodbye_song);
shutdown_modules(jump_to_bootloader);
shutdown_kb(jump_to_bootloader); shutdown_kb(jump_to_bootloader);
while (timer_elapsed(timer_start) < 250) while (timer_elapsed(timer_start) < 250)
wait_ms(1); wait_ms(1);
stop_all_notes(); stop_all_notes();
#else #else
shutdown_modules(jump_to_bootloader);
shutdown_kb(jump_to_bootloader); shutdown_kb(jump_to_bootloader);
wait_ms(250); wait_ms(250);
#endif #endif
@ -258,7 +278,7 @@ uint16_t get_event_keycode(keyevent_t event, bool update_layer_cache) {
/* Get keycode, and then process pre tapping functionality */ /* Get keycode, and then process pre tapping functionality */
bool pre_process_record_quantum(keyrecord_t *record) { bool pre_process_record_quantum(keyrecord_t *record) {
return pre_process_record_kb(get_record_keycode(record, true), record) && return pre_process_record_modules(get_record_keycode(record, true), record) && pre_process_record_kb(get_record_keycode(record, true), record) &&
#ifdef COMBO_ENABLE #ifdef COMBO_ENABLE
process_combo(get_record_keycode(record, true), record) && process_combo(get_record_keycode(record, true), record) &&
#endif #endif
@ -268,6 +288,7 @@ bool pre_process_record_quantum(keyrecord_t *record) {
/* Get keycode, and then call keyboard function */ /* Get keycode, and then call keyboard function */
void post_process_record_quantum(keyrecord_t *record) { void post_process_record_quantum(keyrecord_t *record) {
uint16_t keycode = get_record_keycode(record, false); uint16_t keycode = get_record_keycode(record, false);
post_process_record_modules(keycode, record);
post_process_record_kb(keycode, record); post_process_record_kb(keycode, record);
} }
@ -332,6 +353,7 @@ bool process_record_quantum(keyrecord_t *record) {
#if defined(POINTING_DEVICE_ENABLE) && defined(POINTING_DEVICE_AUTO_MOUSE_ENABLE) #if defined(POINTING_DEVICE_ENABLE) && defined(POINTING_DEVICE_AUTO_MOUSE_ENABLE)
process_auto_mouse(keycode, record) && process_auto_mouse(keycode, record) &&
#endif #endif
process_record_modules(keycode, record) && // modules must run before kb
process_record_kb(keycode, record) && process_record_kb(keycode, record) &&
#if defined(VIA_ENABLE) #if defined(VIA_ENABLE)
process_record_via(keycode, record) && process_record_via(keycode, record) &&
@ -526,6 +548,7 @@ __attribute__((weak)) bool shutdown_kb(bool jump_to_bootloader) {
} }
void suspend_power_down_quantum(void) { void suspend_power_down_quantum(void) {
suspend_power_down_modules();
suspend_power_down_kb(); suspend_power_down_kb();
#ifndef NO_SUSPEND_POWER_DOWN #ifndef NO_SUSPEND_POWER_DOWN
// Turn off backlight // Turn off backlight
@ -593,6 +616,7 @@ __attribute__((weak)) void suspend_wakeup_init_quantum(void) {
#if defined(RGB_MATRIX_ENABLE) #if defined(RGB_MATRIX_ENABLE)
rgb_matrix_set_suspend_state(false); rgb_matrix_set_suspend_state(false);
#endif #endif
suspend_wakeup_init_modules();
suspend_wakeup_init_kb(); suspend_wakeup_init_kb();
} }

View File

@ -244,6 +244,10 @@ extern layer_state_t layer_state;
# include "layer_lock.h" # include "layer_lock.h"
#endif #endif
#ifdef COMMUNITY_MODULES_ENABLE
# include "community_modules.h"
#endif
void set_single_default_layer(uint8_t default_layer); void set_single_default_layer(uint8_t default_layer);
void set_single_persistent_default_layer(uint8_t default_layer); void set_single_persistent_default_layer(uint8_t default_layer);