summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Brassel <nick@tzarc.org>2025-02-26 22:25:41 +1100
committerGitHub <noreply@github.com>2025-02-26 22:25:41 +1100
commit1efc82403bebe759272d1ba7a79d9dfa0d5df506 (patch)
treec356ae7afeb0849926534beb5dd4515f693abf99
parent63b095212b157c4522bdeda3de144fb87213085d (diff)
Community modules (#24848)
-rw-r--r--.github/labeler.yml4
-rw-r--r--.github/workflows/format.yml1
-rw-r--r--builddefs/build_keyboard.mk122
-rw-r--r--data/constants/keycodes/keycodes_0.0.7.hjson7
-rw-r--r--data/constants/module_hooks/0.1.0.hjson25
-rw-r--r--data/constants/module_hooks/1.0.0.hjson26
-rw-r--r--data/schemas/community_module.jsonschema17
-rw-r--r--data/schemas/keyboard.jsonschema17
-rw-r--r--data/schemas/keymap.jsonschema6
-rw-r--r--docs/_sidebar.json1
-rw-r--r--docs/custom_quantum_functions.md11
-rw-r--r--docs/features/community_modules.md142
-rw-r--r--keyboards/handwired/onekey/keymaps/community_module/keymap.c7
-rw-r--r--keyboards/handwired/onekey/keymaps/community_module/keymap.json3
-rw-r--r--lib/python/qmk/cli/__init__.py1
-rw-r--r--lib/python/qmk/cli/format/c.py2
-rwxr-xr-xlib/python/qmk/cli/format/json.py13
-rw-r--r--lib/python/qmk/cli/generate/community_modules.py263
-rwxr-xr-xlib/python/qmk/cli/generate/rules_mk.py46
-rwxr-xr-xlib/python/qmk/cli/info.py5
-rw-r--r--lib/python/qmk/commands.py5
-rw-r--r--lib/python/qmk/community_modules.py100
-rw-r--r--lib/python/qmk/info.py27
-rwxr-xr-xlib/python/qmk/json_encoders.py28
-rw-r--r--lib/python/qmk/keymap.py27
-rw-r--r--modules/qmk/hello_world/hello_world.c33
-rw-r--r--modules/qmk/hello_world/introspection.c10
-rw-r--r--modules/qmk/hello_world/introspection.h10
-rw-r--r--modules/qmk/hello_world/qmk_module.json13
-rw-r--r--modules/qmk/hello_world/rules.mk2
-rw-r--r--quantum/action.h2
-rw-r--r--quantum/keyboard.c44
-rw-r--r--quantum/keycodes.h3
-rw-r--r--quantum/keymap_introspection.c11
-rw-r--r--quantum/os_detection.c7
-rw-r--r--quantum/quantum.c26
-rw-r--r--quantum/quantum.h4
37 files changed, 987 insertions, 84 deletions
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 270cd1a813..82f9672bb7 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -54,3 +54,7 @@ dd:
54 - data/constants/** 54 - data/constants/**
55 - data/mappings/** 55 - data/mappings/**
56 - data/schemas/** 56 - data/schemas/**
57community_module:
58 - changed-files:
59 - any-glob-to-any-file:
60 - modules/**
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
index 74c518fe05..b4e32f981b 100644
--- a/.github/workflows/format.yml
+++ b/.github/workflows/format.yml
@@ -10,6 +10,7 @@ on:
10 - 'lib/arm_atsam/**' 10 - 'lib/arm_atsam/**'
11 - 'lib/lib8tion/**' 11 - 'lib/lib8tion/**'
12 - 'lib/python/**' 12 - 'lib/python/**'
13 - 'modules/**'
13 - 'platforms/**' 14 - 'platforms/**'
14 - 'quantum/**' 15 - 'quantum/**'
15 - 'tests/**' 16 - 'tests/**'
diff --git a/builddefs/build_keyboard.mk b/builddefs/build_keyboard.mk
index 7d58c29462..c5fc9cd25d 100644
--- a/builddefs/build_keyboard.mk
+++ b/builddefs/build_keyboard.mk
@@ -112,6 +112,39 @@ endif
112ifneq ("$(wildcard $(KEYBOARD_PATH_1)/rules.mk)","") 112ifneq ("$(wildcard $(KEYBOARD_PATH_1)/rules.mk)","")
113 include $(KEYBOARD_PATH_1)/rules.mk 113 include $(KEYBOARD_PATH_1)/rules.mk
114endif 114endif
115# Create dependencies on DD keyboard config - structure validated elsewhere
116DD_CONFIG_FILES :=
117ifneq ("$(wildcard $(KEYBOARD_PATH_1)/info.json)","")
118 DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/info.json
119endif
120ifneq ("$(wildcard $(KEYBOARD_PATH_2)/info.json)","")
121 DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/info.json
122endif
123ifneq ("$(wildcard $(KEYBOARD_PATH_3)/info.json)","")
124 DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/info.json
125endif
126ifneq ("$(wildcard $(KEYBOARD_PATH_4)/info.json)","")
127 DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/info.json
128endif
129ifneq ("$(wildcard $(KEYBOARD_PATH_5)/info.json)","")
130 DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/info.json
131endif
132
133ifneq ("$(wildcard $(KEYBOARD_PATH_1)/keyboard.json)","")
134 DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/keyboard.json
135endif
136ifneq ("$(wildcard $(KEYBOARD_PATH_2)/keyboard.json)","")
137 DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/keyboard.json
138endif
139ifneq ("$(wildcard $(KEYBOARD_PATH_3)/keyboard.json)","")
140 DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/keyboard.json
141endif
142ifneq ("$(wildcard $(KEYBOARD_PATH_4)/keyboard.json)","")
143 DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/keyboard.json
144endif
145ifneq ("$(wildcard $(KEYBOARD_PATH_5)/keyboard.json)","")
146 DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/keyboard.json
147endif
115 148
116MAIN_KEYMAP_PATH_1 := $(KEYBOARD_PATH_1)/keymaps/$(KEYMAP) 149MAIN_KEYMAP_PATH_1 := $(KEYBOARD_PATH_1)/keymaps/$(KEYMAP)
117MAIN_KEYMAP_PATH_2 := $(KEYBOARD_PATH_2)/keymaps/$(KEYMAP) 150MAIN_KEYMAP_PATH_2 := $(KEYBOARD_PATH_2)/keymaps/$(KEYMAP)
@@ -207,17 +240,17 @@ ifneq ("$(wildcard $(KEYMAP_JSON))", "")
207 include $(INFO_RULES_MK) 240 include $(INFO_RULES_MK)
208 241
209# Add rules to generate the keymap files - indentation here is important 242# Add rules to generate the keymap files - indentation here is important
210$(INTERMEDIATE_OUTPUT)/src/keymap.c: $(KEYMAP_JSON) 243$(INTERMEDIATE_OUTPUT)/src/keymap.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
211 @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) 244 @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
212 $(eval CMD=$(QMK_BIN) json2c --quiet --output $(KEYMAP_C) $(KEYMAP_JSON)) 245 $(eval CMD=$(QMK_BIN) json2c --quiet --output $(KEYMAP_C) $(KEYMAP_JSON))
213 @$(BUILD_CMD) 246 @$(BUILD_CMD)
214 247
215$(INTERMEDIATE_OUTPUT)/src/config.h: $(KEYMAP_JSON) 248$(INTERMEDIATE_OUTPUT)/src/config.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
216 @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) 249 @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
217 $(eval CMD=$(QMK_BIN) generate-config-h --quiet --output $(KEYMAP_H) $(KEYMAP_JSON)) 250 $(eval CMD=$(QMK_BIN) generate-config-h --quiet --output $(KEYMAP_H) $(KEYMAP_JSON))
218 @$(BUILD_CMD) 251 @$(BUILD_CMD)
219 252
220$(INTERMEDIATE_OUTPUT)/src/keymap.h: $(KEYMAP_JSON) 253$(INTERMEDIATE_OUTPUT)/src/keymap.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
221 @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) 254 @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
222 $(eval CMD=$(QMK_BIN) generate-keymap-h --quiet --output $(INTERMEDIATE_OUTPUT)/src/keymap.h $(KEYMAP_JSON)) 255 $(eval CMD=$(QMK_BIN) generate-keymap-h --quiet --output $(INTERMEDIATE_OUTPUT)/src/keymap.h $(KEYMAP_JSON))
223 @$(BUILD_CMD) 256 @$(BUILD_CMD)
@@ -226,6 +259,32 @@ generated-files: $(INTERMEDIATE_OUTPUT)/src/config.h $(INTERMEDIATE_OUTPUT)/src/
226 259
227endif 260endif
228 261
262# Community modules
263$(INTERMEDIATE_OUTPUT)/src/community_modules.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
264 @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
265 $(eval CMD=$(QMK_BIN) generate-community-modules-h -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(KEYMAP_JSON))
266 @$(BUILD_CMD)
267
268$(INTERMEDIATE_OUTPUT)/src/community_modules.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
269 @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
270 $(eval CMD=$(QMK_BIN) generate-community-modules-c -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(KEYMAP_JSON))
271 @$(BUILD_CMD)
272
273$(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
274 @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
275 $(eval CMD=$(QMK_BIN) generate-community-modules-introspection-c -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c $(KEYMAP_JSON))
276 @$(BUILD_CMD)
277
278$(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES)
279 @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
280 $(eval CMD=$(QMK_BIN) generate-community-modules-introspection-h -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h $(KEYMAP_JSON))
281 @$(BUILD_CMD)
282
283SRC += $(INTERMEDIATE_OUTPUT)/src/community_modules.c
284
285generated-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
286
287
229include $(BUILDDEFS_PATH)/converters.mk 288include $(BUILDDEFS_PATH)/converters.mk
230 289
231# Generate the board's version.h file. 290# Generate the board's version.h file.
@@ -315,6 +374,14 @@ endif
315 374
316# Find all of the config.h files and add them to our CONFIG_H define. 375# Find all of the config.h files and add them to our CONFIG_H define.
317CONFIG_H := 376CONFIG_H :=
377
378define config_h_community_module_appender
379 ifneq ("$(wildcard $(1)/config.h)","")
380 CONFIG_H += $(1)/config.h
381 endif
382endef
383$(foreach module,$(COMMUNITY_MODULE_PATHS),$(eval $(call config_h_community_module_appender,$(module))))
384
318ifneq ("$(wildcard $(KEYBOARD_PATH_5)/config.h)","") 385ifneq ("$(wildcard $(KEYBOARD_PATH_5)/config.h)","")
319 CONFIG_H += $(KEYBOARD_PATH_5)/config.h 386 CONFIG_H += $(KEYBOARD_PATH_5)/config.h
320endif 387endif
@@ -332,6 +399,14 @@ ifneq ("$(wildcard $(KEYBOARD_PATH_1)/config.h)","")
332endif 399endif
333 400
334POST_CONFIG_H := 401POST_CONFIG_H :=
402
403define post_config_h_community_module_appender
404 ifneq ("$(wildcard $(1)/post_config.h)","")
405 POST_CONFIG_H += $(1)/post_config.h
406 endif
407endef
408$(foreach module,$(COMMUNITY_MODULE_PATHS),$(eval $(call post_config_h_community_module_appender,$(module))))
409
335ifneq ("$(wildcard $(KEYBOARD_PATH_1)/post_config.h)","") 410ifneq ("$(wildcard $(KEYBOARD_PATH_1)/post_config.h)","")
336 POST_CONFIG_H += $(KEYBOARD_PATH_1)/post_config.h 411 POST_CONFIG_H += $(KEYBOARD_PATH_1)/post_config.h
337endif 412endif
@@ -348,40 +423,6 @@ ifneq ("$(wildcard $(KEYBOARD_PATH_5)/post_config.h)","")
348 POST_CONFIG_H += $(KEYBOARD_PATH_5)/post_config.h 423 POST_CONFIG_H += $(KEYBOARD_PATH_5)/post_config.h
349endif 424endif
350 425
351# Create dependencies on DD keyboard config - structure validated elsewhere
352DD_CONFIG_FILES :=
353ifneq ("$(wildcard $(KEYBOARD_PATH_1)/info.json)","")
354 DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/info.json
355endif
356ifneq ("$(wildcard $(KEYBOARD_PATH_2)/info.json)","")
357 DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/info.json
358endif
359ifneq ("$(wildcard $(KEYBOARD_PATH_3)/info.json)","")
360 DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/info.json
361endif
362ifneq ("$(wildcard $(KEYBOARD_PATH_4)/info.json)","")
363 DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/info.json
364endif
365ifneq ("$(wildcard $(KEYBOARD_PATH_5)/info.json)","")
366 DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/info.json
367endif
368
369ifneq ("$(wildcard $(KEYBOARD_PATH_1)/keyboard.json)","")
370 DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/keyboard.json
371endif
372ifneq ("$(wildcard $(KEYBOARD_PATH_2)/keyboard.json)","")
373 DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/keyboard.json
374endif
375ifneq ("$(wildcard $(KEYBOARD_PATH_3)/keyboard.json)","")
376 DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/keyboard.json
377endif
378ifneq ("$(wildcard $(KEYBOARD_PATH_4)/keyboard.json)","")
379 DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/keyboard.json
380endif
381ifneq ("$(wildcard $(KEYBOARD_PATH_5)/keyboard.json)","")
382 DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/keyboard.json
383endif
384
385CONFIG_H += $(INTERMEDIATE_OUTPUT)/src/info_config.h 426CONFIG_H += $(INTERMEDIATE_OUTPUT)/src/info_config.h
386KEYBOARD_SRC += $(INTERMEDIATE_OUTPUT)/src/default_keyboard.c 427KEYBOARD_SRC += $(INTERMEDIATE_OUTPUT)/src/default_keyboard.c
387 428
@@ -462,6 +503,13 @@ ifneq ("$(wildcard $(KEYBOARD_PATH_5)/post_rules.mk)","")
462 include $(KEYBOARD_PATH_5)/post_rules.mk 503 include $(KEYBOARD_PATH_5)/post_rules.mk
463endif 504endif
464 505
506define post_rules_mk_community_module_includer
507 ifneq ("$(wildcard $(1)/post_rules.mk)","")
508 include $(1)/post_rules.mk
509 endif
510endef
511$(foreach module,$(COMMUNITY_MODULE_PATHS),$(eval $(call post_rules_mk_community_module_includer,$(module))))
512
465ifneq ("$(wildcard $(KEYMAP_PATH)/config.h)","") 513ifneq ("$(wildcard $(KEYMAP_PATH)/config.h)","")
466 CONFIG_H += $(KEYMAP_PATH)/config.h 514 CONFIG_H += $(KEYMAP_PATH)/config.h
467endif 515endif
diff --git a/data/constants/keycodes/keycodes_0.0.7.hjson b/data/constants/keycodes/keycodes_0.0.7.hjson
new file mode 100644
index 0000000000..52e1a50443
--- /dev/null
+++ b/data/constants/keycodes/keycodes_0.0.7.hjson
@@ -0,0 +1,7 @@
1{
2 "ranges": {
3 "0x77C0/0x003F": {
4 "define": "QK_COMMUNITY_MODULE"
5 }
6 }
7}
diff --git a/data/constants/module_hooks/0.1.0.hjson b/data/constants/module_hooks/0.1.0.hjson
new file mode 100644
index 0000000000..c77f4c297e
--- /dev/null
+++ b/data/constants/module_hooks/0.1.0.hjson
@@ -0,0 +1,25 @@
1{
2 keyboard_pre_init: {
3 ret_type: void
4 args: void
5 }
6 keyboard_post_init: {
7 ret_type: void
8 args: void
9 }
10 pre_process_record: {
11 ret_type: bool
12 args: uint16_t keycode, keyrecord_t *record
13 call_params: keycode, record
14 }
15 process_record: {
16 ret_type: bool
17 args: uint16_t keycode, keyrecord_t *record
18 call_params: keycode, record
19 }
20 post_process_record: {
21 ret_type: void
22 args: uint16_t keycode, keyrecord_t *record
23 call_params: keycode, record
24 }
25}
diff --git a/data/constants/module_hooks/1.0.0.hjson b/data/constants/module_hooks/1.0.0.hjson
new file mode 100644
index 0000000000..4e7bf30412
--- /dev/null
+++ b/data/constants/module_hooks/1.0.0.hjson
@@ -0,0 +1,26 @@
1{
2 housekeeping_task: {
3 ret_type: void
4 args: void
5 }
6 suspend_power_down: {
7 ret_type: void
8 args: void
9 }
10 suspend_wakeup_init: {
11 ret_type: void
12 args: void
13 }
14 shutdown: {
15 ret_type: bool
16 args: bool jump_to_bootloader
17 call_params: jump_to_bootloader
18 }
19 process_detected_host_os: {
20 ret_type: bool
21 args: os_variant_t os
22 call_params: os
23 guard: defined(OS_DETECTION_ENABLE)
24 header: os_detection.h
25 }
26}
diff --git a/data/schemas/community_module.jsonschema b/data/schemas/community_module.jsonschema
new file mode 100644
index 0000000000..a3474476df
--- /dev/null
+++ b/data/schemas/community_module.jsonschema
@@ -0,0 +1,17 @@
1{
2 "$schema": "https://json-schema.org/draft/2020-12/schema#",
3 "$id": "qmk.community_module.v1",
4 "title": "Community Module Information",
5 "type": "object",
6 "required": ["module_name", "maintainer"]
7 "properties": {
8 "module_name": {"$ref": "qmk.definitions.v1#/text_identifier"},
9 "maintainer": {"$ref": "qmk.definitions.v1#/text_identifier"},
10 "url": {
11 "type": "string",
12 "format": "uri"
13 },
14 "keycodes": {"$ref": "qmk.definitions.v1#/keycode_decl_array"},
15 "features": {"$ref": "qmk.keyboard.v1#/definitions/features_config"},
16 }
17}
diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema
index 8b6cc7032b..9b63f62d45 100644
--- a/data/schemas/keyboard.jsonschema
+++ b/data/schemas/keyboard.jsonschema
@@ -31,6 +31,11 @@
31 "pins": {"$ref": "qmk.definitions.v1#/mcu_pin_array"} 31 "pins": {"$ref": "qmk.definitions.v1#/mcu_pin_array"}
32 } 32 }
33 } 33 }
34 "features_config": {
35 "$ref": "qmk.definitions.v1#/boolean_array",
36 "propertyNames": {"$ref": "qmk.definitions.v1#/snake_case"},
37 "not": {"required": ["lto"]}
38 },
34 }, 39 },
35 "type": "object", 40 "type": "object",
36 "not": {"required": ["vendorId", "productId"]}, // reject via keys... 41 "not": {"required": ["vendorId", "productId"]}, // reject via keys...
@@ -328,11 +333,7 @@
328 "enabled": {"type": "boolean"} 333 "enabled": {"type": "boolean"}
329 } 334 }
330 }, 335 },
331 "features": { 336 "features": { "$ref": "#/definitions/features_config" },
332 "$ref": "qmk.definitions.v1#/boolean_array",
333 "propertyNames": {"$ref": "qmk.definitions.v1#/snake_case"},
334 "not": {"required": ["lto"]}
335 },
336 "indicators": { 337 "indicators": {
337 "type": "object", 338 "type": "object",
338 "properties": { 339 "properties": {
@@ -467,6 +468,12 @@
467 "rows": {"$ref": "qmk.definitions.v1#/mcu_pin_array"} 468 "rows": {"$ref": "qmk.definitions.v1#/mcu_pin_array"}
468 } 469 }
469 }, 470 },
471 "modules": {
472 "type": "array",
473 "items": {
474 "type": "string"
475 }
476 },
470 "mouse_key": { 477 "mouse_key": {
471 "type": "object", 478 "type": "object",
472 "properties": { 479 "properties": {
diff --git a/data/schemas/keymap.jsonschema b/data/schemas/keymap.jsonschema
index e967e45c53..b92a536c2c 100644
--- a/data/schemas/keymap.jsonschema
+++ b/data/schemas/keymap.jsonschema
@@ -71,6 +71,12 @@
71 "config": {"$ref": "qmk.keyboard.v1"}, 71 "config": {"$ref": "qmk.keyboard.v1"},
72 "notes": { 72 "notes": {
73 "type": "string" 73 "type": "string"
74 },
75 "modules": {
76 "type": "array",
77 "items": {
78 "type": "string"
79 }
74 } 80 }
75 } 81 }
76} 82}
diff --git a/docs/_sidebar.json b/docs/_sidebar.json
index 95601be7de..0b5f297018 100644
--- a/docs/_sidebar.json
+++ b/docs/_sidebar.json
@@ -60,6 +60,7 @@
60 "items": [ 60 "items": [
61 { "text": "Customizing Functionality", "link": "/custom_quantum_functions" }, 61 { "text": "Customizing Functionality", "link": "/custom_quantum_functions" },
62 { "text": "Driver Installation with Zadig", "link": "/driver_installation_zadig" }, 62 { "text": "Driver Installation with Zadig", "link": "/driver_installation_zadig" },
63 { "text": "Community Modules", "link": "/features/community_modules" },
63 { "text": "Keymap Overview", "link": "/keymap" }, 64 { "text": "Keymap Overview", "link": "/keymap" },
64 { 65 {
65 "text": "Development Environments", 66 "text": "Development Environments",
diff --git a/docs/custom_quantum_functions.md b/docs/custom_quantum_functions.md
index 1479eb53f6..c69beb055e 100644
--- a/docs/custom_quantum_functions.md
+++ b/docs/custom_quantum_functions.md
@@ -9,12 +9,19 @@ This page does not assume any special knowledge about QMK, but reading [Understa
9We have structured QMK as a hierarchy: 9We have structured QMK as a hierarchy:
10 10
11* Core (`_quantum`) 11* Core (`_quantum`)
12 * Community Module (`_<module>`)
13 * Community Module -> Keyboard/Revision (`_<module>_kb`)
14 * Community Module -> Keymap (`_<module>_user`)
12 * Keyboard/Revision (`_kb`) 15 * Keyboard/Revision (`_kb`)
13 * Keymap (`_user`) 16 * Keymap (`_user`)
14 17
15Each 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. 18Each 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.
16 19
17When 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. 20When 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.
21
22Functions 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).
23
24When defining module overrides such as `process_record_<module>()`, the same pattern should be used; the module must invoke `process_record_<module>_kb()` as appropriate.
18 25
19# Custom Keycodes 26# Custom Keycodes
20 27
@@ -99,7 +106,7 @@ These are the three main initialization functions, listed in the order that they
99* `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. 106* `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.
100 107
101::: warning 108::: warning
102For 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. 109For 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.
103::: 110:::
104 111
105## Keyboard Pre Initialization code 112## Keyboard Pre Initialization code
diff --git a/docs/features/community_modules.md b/docs/features/community_modules.md
new file mode 100644
index 0000000000..3a1a82e7bc
--- /dev/null
+++ b/docs/features/community_modules.md
@@ -0,0 +1,142 @@
1# Community Modules
2
3Community 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.
4
5These 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.
6
7## Adding a Community Module to your build
8
9Community 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.
10
11Modules must live in either of two locations:
12
13* `<QMK_USERSPACE>/modules/`
14* `<QMK_FIRMWARE>/modules/`
15
16A 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.
17
18To add this module to your build, in your keymap's directory create a `keymap.json` with the following content:
19
20```json
21{
22 "modules": [
23 "qmk/hello_world"
24 ]
25}
26```
27
28If you already have a `keymap.json`, you'll need to manually merge the `modules` section into your keymap.
29
30::: warning
31Community Modules are not supported by QMK Configurator. If you wish to use Community Modules, you must build your own firmware.
32:::
33
34## Adding a Community Module to your External Userspace
35
36Module 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:
37
38```sh
39cd /path/to/your/external/userspace
40mkdir -p modules
41# Replace the following {user} and {repo} with the author's community module repository
42git submodule add https://github.com/{user}/{repo}.git modules/{user}
43git submdule update --init --recursive
44```
45
46This will ensure the copy of the module is made in your userspace.
47
48Add a new entry into your `keymap.json` with the desired modules, replacing `{user}` and `{module_name}` as appropriate:
49
50```json
51{
52 "modules": [
53 "qmk/hello_world",
54 "{user}/{module_name}"
55 ]
56}
57```
58
59::: info
60The 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.
61:::
62
63## Writing a QMK Community Module
64
65As stated earlier, Community Module authors are strongly encouraged to provide their modules through git, allowing users to leverage submodules to import functionality.
66
67### `qmk_module.json`
68
69A Community Module is denoted by a `qmk_module.json` file such as the following:
70
71```json
72{
73 "module_name": "Hello World",
74 "maintainer": "QMK Maintainers",
75 "features": {
76 "deferred_exec": true
77 },
78 "keycodes": [
79 {
80 "key": "COMMUNITY_MODULE_HELLO",
81 "aliases": ["CM_HELO"]
82 }
83 ]
84}
85```
86
87At minimum, the module must provide the `module_name` and `maintainer` fields.
88
89The 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.
90
91The `keycodes` array allows a module to provide new keycodes (as well as corresponding aliases) to a keymap.
92
93### `rules.mk` / `post_rules.mk`
94
95These two files follows standard QMK build system logic, allowing for `Makefile`-style customisation as if it were present in the keyboard or keymap.
96
97### `<module>.c`
98
99This 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.
100
101::: info
102Other files intended to be included must use the normal method of `SRC += my_file.c` inside `rules.mk`.
103:::
104
105::: tip
106This 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.
107:::
108
109### `introspection.c` / `introspection.h`
110
111These 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.
112
113The header may provide definitions which are useful to the user's `keymap.c`.
114
115The source file may provide functions which allow access to information specified in the user's `keymap.c`.
116
117::: warning
118Introspection 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).
119:::
120
121### Compatible APIs
122
123Community Modules may provide specializations for the following APIs:
124
125| Base API | API Format | Example (`hello_world` module) | API Version |
126|----------------------------|-------------------------------------|----------------------------------------|-------------|
127| `keyboard_pre_init` | `keyboard_pre_init_<module>` | `keyboard_pre_init_hello_world` | `0.1.0` |
128| `keyboard_post_init` | `keyboard_post_init_<module>` | `keyboard_post_init_hello_world` | `0.1.0` |
129| `pre_process_record` | `pre_process_record_<module>` | `pre_process_record_hello_world` | `0.1.0` |
130| `process_record` | `process_record_<module>` | `process_record_hello_world` | `0.1.0` |
131| `post_process_record` | `post_process_record_<module>` | `post_process_record_hello_world` | `0.1.0` |
132| `housekeeping_task` | `housekeeping_task_<module>` | `housekeeping_task_hello_world` | `1.0.0` |
133| `suspend_power_down` | `suspend_power_down_<module>` | `suspend_power_down_hello_world` | `1.0.0` |
134| `suspend_wakeup_init` | `suspend_wakeup_init_<module>` | `suspend_wakeup_init_hello_world` | `1.0.0` |
135| `shutdown` | `shutdown_<module>` | `shutdown_hello_world` | `1.0.0` |
136| `process_detected_host_os` | `process_detected_host_os_<module>` | `process_detected_host_os_hello_world` | `1.0.0` |
137
138::: info
139An unspecified API is disregarded if a Community Module does not provide a specialization for it.
140:::
141
142Each 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).
diff --git a/keyboards/handwired/onekey/keymaps/community_module/keymap.c b/keyboards/handwired/onekey/keymaps/community_module/keymap.c
new file mode 100644
index 0000000000..5115a9fea0
--- /dev/null
+++ b/keyboards/handwired/onekey/keymaps/community_module/keymap.c
@@ -0,0 +1,7 @@
1// Copyright 2025 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3#include QMK_KEYBOARD_H
4
5const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
6 LAYOUT_ortho_1x1(CM_HELO)
7};
diff --git a/keyboards/handwired/onekey/keymaps/community_module/keymap.json b/keyboards/handwired/onekey/keymaps/community_module/keymap.json
new file mode 100644
index 0000000000..e3b4051b9e
--- /dev/null
+++ b/keyboards/handwired/onekey/keymaps/community_module/keymap.json
@@ -0,0 +1,3 @@
1{
2 "modules": ["qmk/hello_world"]
3}
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index bb47ea26ed..3f2ba9ce3c 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -49,6 +49,7 @@ subcommands = [
49 'qmk.cli.generate.api', 49 'qmk.cli.generate.api',
50 'qmk.cli.generate.autocorrect_data', 50 'qmk.cli.generate.autocorrect_data',
51 'qmk.cli.generate.compilation_database', 51 'qmk.cli.generate.compilation_database',
52 'qmk.cli.generate.community_modules',
52 'qmk.cli.generate.config_h', 53 'qmk.cli.generate.config_h',
53 'qmk.cli.generate.develop_pr_list', 54 'qmk.cli.generate.develop_pr_list',
54 'qmk.cli.generate.dfu_header', 55 'qmk.cli.generate.dfu_header',
diff --git a/lib/python/qmk/cli/format/c.py b/lib/python/qmk/cli/format/c.py
index a58aef3fbc..65818155b0 100644
--- a/lib/python/qmk/cli/format/c.py
+++ b/lib/python/qmk/cli/format/c.py
@@ -10,7 +10,7 @@ from qmk.path import normpath
10from qmk.c_parse import c_source_files 10from qmk.c_parse import c_source_files
11 11
12c_file_suffixes = ('c', 'h', 'cpp', 'hpp') 12c_file_suffixes = ('c', 'h', 'cpp', 'hpp')
13core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms') 13core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms', 'modules')
14ignored = ('tmk_core/protocol/usb_hid', 'platforms/chibios/boards') 14ignored = ('tmk_core/protocol/usb_hid', 'platforms/chibios/boards')
15 15
16 16
diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py
index 3670294434..61f5254184 100755
--- a/lib/python/qmk/cli/format/json.py
+++ b/lib/python/qmk/cli/format/json.py
@@ -9,7 +9,7 @@ from milc import cli
9 9
10from qmk.info import info_json 10from qmk.info import info_json
11from qmk.json_schema import json_load, validate 11from qmk.json_schema import json_load, validate
12from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder 12from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder, CommunityModuleJSONEncoder
13from qmk.path import normpath 13from qmk.path import normpath
14 14
15 15
@@ -32,6 +32,13 @@ def _detect_json_format(file, json_data):
32 32
33 if json_encoder is None: 33 if json_encoder is None:
34 try: 34 try:
35 validate(json_data, 'qmk.community_module.v1')
36 json_encoder = CommunityModuleJSONEncoder
37 except ValidationError:
38 pass
39
40 if json_encoder is None:
41 try:
35 validate(json_data, 'qmk.keyboard.v1') 42 validate(json_data, 'qmk.keyboard.v1')
36 json_encoder = InfoJSONEncoder 43 json_encoder = InfoJSONEncoder
37 except ValidationError as e: 44 except ValidationError as e:
@@ -54,6 +61,8 @@ def _get_json_encoder(file, json_data):
54 json_encoder = KeymapJSONEncoder 61 json_encoder = KeymapJSONEncoder
55 elif cli.args.format == 'userspace': 62 elif cli.args.format == 'userspace':
56 json_encoder = UserspaceJSONEncoder 63 json_encoder = UserspaceJSONEncoder
64 elif cli.args.format == 'community_module':
65 json_encoder = CommunityModuleJSONEncoder
57 else: 66 else:
58 # This should be impossible 67 # This should be impossible
59 cli.log.error('Unknown format: %s', cli.args.format) 68 cli.log.error('Unknown format: %s', cli.args.format)
@@ -61,7 +70,7 @@ def _get_json_encoder(file, json_data):
61 70
62 71
63@cli.argument('json_file', arg_only=True, type=normpath, help='JSON file to format') 72@cli.argument('json_file', arg_only=True, type=normpath, help='JSON file to format')
64@cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') 73@cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace', 'community_module'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)')
65@cli.argument('-i', '--inplace', action='store_true', arg_only=True, help='If set, will operate in-place on the input file') 74@cli.argument('-i', '--inplace', action='store_true', arg_only=True, help='If set, will operate in-place on the input file')
66@cli.argument('-p', '--print', action='store_true', arg_only=True, help='If set, will print the formatted json to stdout ') 75@cli.argument('-p', '--print', action='store_true', arg_only=True, help='If set, will print the formatted json to stdout ')
67@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) 76@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py
new file mode 100644
index 0000000000..23678a2fb5
--- /dev/null
+++ b/lib/python/qmk/cli/generate/community_modules.py
@@ -0,0 +1,263 @@
1import contextlib
2from argcomplete.completers import FilesCompleter
3from pathlib import Path
4
5from milc import cli
6
7import qmk.path
8from qmk.info import get_modules
9from qmk.keyboard import keyboard_completer, keyboard_folder
10from qmk.commands import dump_lines
11from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
12from qmk.community_modules import module_api_list, load_module_jsons, find_module_path
13
14
15@contextlib.contextmanager
16def _render_api_guard(lines, api):
17 if api.guard:
18 lines.append(f'#if {api.guard}')
19 yield
20 if api.guard:
21 lines.append(f'#endif // {api.guard}')
22
23
24def _render_api_header(api):
25 lines = []
26 if api.header:
27 lines.append('')
28 with _render_api_guard(lines, api):
29 lines.append(f'#include <{api.header}>')
30 return lines
31
32
33def _render_keycodes(module_jsons):
34 lines = []
35 lines.append('')
36 lines.append('enum {')
37 first = True
38 for module_json in module_jsons:
39 module_name = Path(module_json['module']).name
40 keycodes = module_json.get('keycodes', [])
41 if len(keycodes) > 0:
42 lines.append(f' // From module: {module_name}')
43 for keycode in keycodes:
44 key = keycode.get('key', None)
45 if first:
46 lines.append(f' {key} = QK_COMMUNITY_MODULE,')
47 first = False
48 else:
49 lines.append(f' {key},')
50 for alias in keycode.get('aliases', []):
51 lines.append(f' {alias} = {key},')
52 lines.append('')
53 lines.append(' LAST_COMMUNITY_MODULE_KEY')
54 lines.append('};')
55 lines.append('_Static_assert((int)LAST_COMMUNITY_MODULE_KEY <= (int)(QK_COMMUNITY_MODULE_MAX+1), "Too many community module keycodes");')
56 return lines
57
58
59def _render_api_declarations(api, module, user_kb=True):
60 lines = []
61 lines.append('')
62 with _render_api_guard(lines, api):
63 if user_kb:
64 lines.append(f'{api.ret_type} {api.name}_{module}_user({api.args});')
65 lines.append(f'{api.ret_type} {api.name}_{module}_kb({api.args});')
66 lines.append(f'{api.ret_type} {api.name}_{module}({api.args});')
67 return lines
68
69
70def _render_api_implementations(api, module):
71 module_name = Path(module).name
72 lines = []
73 lines.append('')
74 with _render_api_guard(lines, api):
75 # _user
76 lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}_user({api.args}) {{')
77 if api.ret_type == 'bool':
78 lines.append(' return true;')
79 else:
80 pass
81 lines.append('}')
82 lines.append('')
83
84 # _kb
85 lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}_kb({api.args}) {{')
86 if api.ret_type == 'bool':
87 lines.append(f' if(!{api.name}_{module_name}_user({api.call_params})) {{ return false; }}')
88 lines.append(' return true;')
89 else:
90 lines.append(f' {api.name}_{module_name}_user({api.call_params});')
91 lines.append('}')
92 lines.append('')
93
94 # module (non-suffixed)
95 lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}({api.args}) {{')
96 if api.ret_type == 'bool':
97 lines.append(f' if(!{api.name}_{module_name}_kb({api.call_params})) {{ return false; }}')
98 lines.append(' return true;')
99 else:
100 lines.append(f' {api.name}_{module_name}_kb({api.call_params});')
101 lines.append('}')
102 return lines
103
104
105def _render_core_implementation(api, modules):
106 lines = []
107 lines.append('')
108 with _render_api_guard(lines, api):
109 lines.append(f'{api.ret_type} {api.name}_modules({api.args}) {{')
110 if api.ret_type == 'bool':
111 lines.append(' return true')
112 for module in modules:
113 module_name = Path(module).name
114 if api.ret_type == 'bool':
115 lines.append(f' && {api.name}_{module_name}({api.call_params})')
116 else:
117 lines.append(f' {api.name}_{module_name}({api.call_params});')
118 if api.ret_type == 'bool':
119 lines.append(' ;')
120 lines.append('}')
121 return lines
122
123
124@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
125@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
126@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.h for.')
127@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file')
128@cli.subcommand('Creates a community_modules.h from a keymap.json file.')
129def generate_community_modules_h(cli):
130 """Creates a community_modules.h from a keymap.json file
131 """
132 if cli.args.output and cli.args.output.name == '-':
133 cli.args.output = None
134
135 api_list, api_version, ver_major, ver_minor, ver_patch = module_api_list()
136
137 lines = [
138 GPL2_HEADER_C_LIKE,
139 GENERATED_HEADER_C_LIKE,
140 '#pragma once',
141 '#include <stdint.h>',
142 '#include <stdbool.h>',
143 '#include <keycodes.h>',
144 '',
145 '#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))',
146 f'#define COMMUNITY_MODULES_API_VERSION COMMUNITY_MODULES_API_VERSION_BUILDER({ver_major},{ver_minor},{ver_patch})',
147 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}.")',
148 '',
149 'typedef struct keyrecord_t keyrecord_t; // forward declaration so we don\'t need to include quantum.h',
150 '',
151 ]
152
153 modules = get_modules(cli.args.keyboard, cli.args.filename)
154 module_jsons = load_module_jsons(modules)
155 if len(modules) > 0:
156 lines.extend(_render_keycodes(module_jsons))
157
158 for api in api_list:
159 lines.extend(_render_api_header(api))
160
161 for module in modules:
162 lines.append('')
163 lines.append(f'// From module: {module}')
164 for api in api_list:
165 lines.extend(_render_api_declarations(api, Path(module).name))
166 lines.append('')
167
168 lines.append('// Core wrapper')
169 for api in api_list:
170 lines.extend(_render_api_declarations(api, 'modules', user_kb=False))
171
172 dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True)
173
174
175@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
176@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
177@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.')
178@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file')
179@cli.subcommand('Creates a community_modules.c from a keymap.json file.')
180def generate_community_modules_c(cli):
181 """Creates a community_modules.c from a keymap.json file
182 """
183 if cli.args.output and cli.args.output.name == '-':
184 cli.args.output = None
185
186 api_list, _, _, _, _ = module_api_list()
187
188 lines = [
189 GPL2_HEADER_C_LIKE,
190 GENERATED_HEADER_C_LIKE,
191 '',
192 '#include "community_modules.h"',
193 ]
194
195 modules = get_modules(cli.args.keyboard, cli.args.filename)
196 if len(modules) > 0:
197
198 for module in modules:
199 for api in api_list:
200 lines.extend(_render_api_implementations(api, Path(module).name))
201
202 for api in api_list:
203 lines.extend(_render_core_implementation(api, modules))
204
205 dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True)
206
207
208@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
209@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
210@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.')
211@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file')
212@cli.subcommand('Creates a community_modules_introspection.h from a keymap.json file.')
213def generate_community_modules_introspection_h(cli):
214 """Creates a community_modules_introspection.h from a keymap.json file
215 """
216 if cli.args.output and cli.args.output.name == '-':
217 cli.args.output = None
218
219 lines = [
220 GPL2_HEADER_C_LIKE,
221 GENERATED_HEADER_C_LIKE,
222 '',
223 ]
224
225 modules = get_modules(cli.args.keyboard, cli.args.filename)
226 if len(modules) > 0:
227 for module in modules:
228 module_path = find_module_path(module)
229 lines.append(f'#if __has_include("{module_path}/introspection.h")')
230 lines.append(f'#include "{module_path}/introspection.h"')
231 lines.append(f'#endif // __has_include("{module_path}/introspection.h")')
232 lines.append('')
233
234 dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True)
235
236
237@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
238@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
239@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.')
240@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file')
241@cli.subcommand('Creates a community_modules_introspection.c from a keymap.json file.')
242def generate_community_modules_introspection_c(cli):
243 """Creates a community_modules_introspection.c from a keymap.json file
244 """
245 if cli.args.output and cli.args.output.name == '-':
246 cli.args.output = None
247
248 lines = [
249 GPL2_HEADER_C_LIKE,
250 GENERATED_HEADER_C_LIKE,
251 '',
252 ]
253
254 modules = get_modules(cli.args.keyboard, cli.args.filename)
255 if len(modules) > 0:
256 for module in modules:
257 module_path = find_module_path(module)
258 lines.append(f'#if __has_include("{module_path}/introspection.c")')
259 lines.append(f'#include "{module_path}/introspection.c"')
260 lines.append(f'#endif // __has_include("{module_path}/introspection.c")')
261 lines.append('')
262
263 dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True)
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index 5291556109..cae9b07c3e 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -6,12 +6,13 @@ from dotty_dict import dotty
6from argcomplete.completers import FilesCompleter 6from argcomplete.completers import FilesCompleter
7from milc import cli 7from milc import cli
8 8
9from qmk.info import info_json 9from qmk.info import info_json, get_modules
10from qmk.json_schema import json_load 10from qmk.json_schema import json_load
11from qmk.keyboard import keyboard_completer, keyboard_folder 11from qmk.keyboard import keyboard_completer, keyboard_folder
12from qmk.commands import dump_lines, parse_configurator_json 12from qmk.commands import dump_lines, parse_configurator_json
13from qmk.path import normpath, FileType 13from qmk.path import normpath, FileType
14from qmk.constants import GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE 14from qmk.constants import GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE
15from qmk.community_modules import find_module_path, load_module_jsons
15 16
16 17
17def generate_rule(rules_key, rules_value): 18def generate_rule(rules_key, rules_value):
@@ -46,6 +47,42 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict):
46 return generate_rule(rules_key, rules_value) 47 return generate_rule(rules_key, rules_value)
47 48
48 49
50def generate_features_rules(features_dict):
51 lines = []
52 for feature, enabled in features_dict.items():
53 feature = feature.upper()
54 enabled = 'yes' if enabled else 'no'
55 lines.append(generate_rule(f'{feature}_ENABLE', enabled))
56 return lines
57
58
59def generate_modules_rules(keyboard, filename):
60 lines = []
61 modules = get_modules(keyboard, filename)
62 if len(modules) > 0:
63 lines.append('')
64 lines.append('OPT_DEFS += -DCOMMUNITY_MODULES_ENABLE=TRUE')
65 for module in modules:
66 module_path = find_module_path(module)
67 if not module_path:
68 raise FileNotFoundError(f"Module '{module}' not found.")
69 lines.append('')
70 lines.append(f'COMMUNITY_MODULES += {module_path.name}') # use module_path here instead of module as it may be a subdirectory
71 lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module_path.name.upper()}_ENABLE=TRUE')
72 lines.append(f'COMMUNITY_MODULE_PATHS += {module_path}')
73 lines.append(f'VPATH += {module_path}')
74 lines.append(f'SRC += $(wildcard {module_path}/{module_path.name}.c)')
75 lines.append(f'-include {module_path}/rules.mk')
76
77 module_jsons = load_module_jsons(modules)
78 for module_json in module_jsons:
79 if 'features' in module_json:
80 lines.append('')
81 lines.append(f'# Module: {module_json["module_name"]}')
82 lines.extend(generate_features_rules(module_json['features']))
83 return lines
84
85
49@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.') 86@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.')
50@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') 87@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
51@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 88@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):
80 117
81 # Iterate through features to enable/disable them 118 # Iterate through features to enable/disable them
82 if 'features' in kb_info_json: 119 if 'features' in kb_info_json:
83 for feature, enabled in kb_info_json['features'].items(): 120 rules_mk_lines.extend(generate_features_rules(kb_info_json['features']))
84 feature = feature.upper()
85 enabled = 'yes' if enabled else 'no'
86 rules_mk_lines.append(generate_rule(f'{feature}_ENABLE', enabled))
87 121
88 # Set SPLIT_TRANSPORT, if needed 122 # Set SPLIT_TRANSPORT, if needed
89 if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom': 123 if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom':
@@ -99,6 +133,8 @@ def generate_rules_mk(cli):
99 if converter: 133 if converter:
100 rules_mk_lines.append(generate_rule('CONVERT_TO', converter)) 134 rules_mk_lines.append(generate_rule('CONVERT_TO', converter))
101 135
136 rules_mk_lines.extend(generate_modules_rules(cli.args.keyboard, cli.args.filename))
137
102 # Show the results 138 # Show the results
103 dump_lines(cli.args.output, rules_mk_lines) 139 dump_lines(cli.args.output, rules_mk_lines)
104 140
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
index e662407474..5925b57258 100755
--- a/lib/python/qmk/cli/info.py
+++ b/lib/python/qmk/cli/info.py
@@ -52,6 +52,11 @@ def show_keymap(kb_info_json, title_caps=True):
52 52
53 if keymap_path and keymap_path.suffix == '.json': 53 if keymap_path and keymap_path.suffix == '.json':
54 keymap_data = json.load(keymap_path.open(encoding='utf-8')) 54 keymap_data = json.load(keymap_path.open(encoding='utf-8'))
55
56 # cater for layout-less keymap.json
57 if 'layout' not in keymap_data:
58 return
59
55 layout_name = keymap_data['layout'] 60 layout_name = keymap_data['layout']
56 layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names 61 layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names
57 62
diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py
index a05b3641b5..0e1876ca7a 100644
--- a/lib/python/qmk/commands.py
+++ b/lib/python/qmk/commands.py
@@ -98,11 +98,14 @@ def in_virtualenv():
98 return active_prefix != sys.prefix 98 return active_prefix != sys.prefix
99 99
100 100
101def dump_lines(output_file, lines, quiet=True): 101def dump_lines(output_file, lines, quiet=True, remove_repeated_newlines=False):
102 """Handle dumping to stdout or file 102 """Handle dumping to stdout or file
103 Creates parent folders if required 103 Creates parent folders if required
104 """ 104 """
105 generated = '\n'.join(lines) + '\n' 105 generated = '\n'.join(lines) + '\n'
106 if remove_repeated_newlines:
107 while '\n\n\n' in generated:
108 generated = generated.replace('\n\n\n', '\n\n')
106 if output_file and output_file.name != '-': 109 if output_file and output_file.name != '-':
107 output_file.parent.mkdir(parents=True, exist_ok=True) 110 output_file.parent.mkdir(parents=True, exist_ok=True)
108 if output_file.exists(): 111 if output_file.exists():
diff --git a/lib/python/qmk/community_modules.py b/lib/python/qmk/community_modules.py
new file mode 100644
index 0000000000..f7e96a6b93
--- /dev/null
+++ b/lib/python/qmk/community_modules.py
@@ -0,0 +1,100 @@
1import os
2
3from pathlib import Path
4from functools import lru_cache
5
6from milc.attrdict import AttrDict
7
8from qmk.json_schema import json_load, validate, merge_ordered_dicts
9from qmk.util import truthy
10from qmk.constants import QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE
11from qmk.path import under_qmk_firmware, under_qmk_userspace
12
13COMMUNITY_MODULE_JSON_FILENAME = 'qmk_module.json'
14
15
16class ModuleAPI(AttrDict):
17 def __init__(self, **kwargs):
18 super().__init__()
19 for key, value in kwargs.items():
20 self[key] = value
21
22
23@lru_cache(maxsize=1)
24def module_api_list():
25 module_definition_files = sorted(set(QMK_FIRMWARE.glob('data/constants/module_hooks/*.hjson')))
26 module_definition_jsons = [json_load(f) for f in module_definition_files]
27 module_definitions = merge_ordered_dicts(module_definition_jsons)
28 latest_module_version = module_definition_files[-1].stem
29 latest_module_version_parts = latest_module_version.split('.')
30
31 api_list = []
32 for name, mod in module_definitions.items():
33 api_list.append(ModuleAPI(
34 ret_type=mod['ret_type'],
35 name=name,
36 args=mod['args'],
37 call_params=mod.get('call_params', ''),
38 guard=mod.get('guard', None),
39 header=mod.get('header', None),
40 ))
41
42 return api_list, latest_module_version, latest_module_version_parts[0], latest_module_version_parts[1], latest_module_version_parts[2]
43
44
45def find_available_module_paths():
46 """Find all available modules.
47 """
48 search_dirs = []
49 if HAS_QMK_USERSPACE:
50 search_dirs.append(QMK_USERSPACE / 'modules')
51 search_dirs.append(QMK_FIRMWARE / 'modules')
52
53 modules = []
54 for search_dir in search_dirs:
55 for module_json_path in search_dir.rglob(COMMUNITY_MODULE_JSON_FILENAME):
56 modules.append(module_json_path.parent)
57 return modules
58
59
60def find_module_path(module):
61 """Find a module by name.
62 """
63 for module_path in find_available_module_paths():
64 # Ensure the module directory is under QMK Firmware or QMK Userspace
65 relative_path = under_qmk_firmware(module_path)
66 if not relative_path:
67 relative_path = under_qmk_userspace(module_path)
68 if not relative_path:
69 continue
70
71 lhs = str(relative_path.as_posix())[len('modules/'):]
72 rhs = str(Path(module).as_posix())
73
74 if relative_path and lhs == rhs:
75 return module_path
76 return None
77
78
79def load_module_json(module):
80 """Load a module JSON file.
81 """
82 module_path = find_module_path(module)
83 if not module_path:
84 raise FileNotFoundError(f'Module not found: {module}')
85
86 module_json = json_load(module_path / COMMUNITY_MODULE_JSON_FILENAME)
87
88 if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False):
89 validate(module_json, 'qmk.community_module.v1')
90
91 module_json['module'] = module
92 module_json['module_path'] = module_path
93
94 return module_json
95
96
97def load_module_jsons(modules):
98 """Load the module JSON files, matching the specified order.
99 """
100 return list(map(load_module_json, modules))
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index d70e7ee1b3..93eba7376a 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -1059,3 +1059,30 @@ def keymap_json(keyboard, keymap, force_layout=None):
1059 _extract_config_h(kb_info_json, parse_config_h_file(keymap_config)) 1059 _extract_config_h(kb_info_json, parse_config_h_file(keymap_config))
1060 1060
1061 return kb_info_json 1061 return kb_info_json
1062
1063
1064def get_modules(keyboard, keymap_filename):
1065 """Get the modules for a keyboard/keymap.
1066 """
1067 modules = []
1068
1069 if keymap_filename:
1070 keymap_json = parse_configurator_json(keymap_filename)
1071
1072 if keymap_json:
1073 kb = keymap_json.get('keyboard', None)
1074 if not kb:
1075 kb = keyboard
1076
1077 if kb:
1078 kb_info_json = info_json(kb)
1079 if kb_info_json:
1080 modules.extend(kb_info_json.get('modules', []))
1081
1082 modules.extend(keymap_json.get('modules', []))
1083
1084 elif keyboard:
1085 kb_info_json = info_json(keyboard)
1086 modules.extend(kb_info_json.get('modules', []))
1087
1088 return list(dict.fromkeys(modules)) # remove dupes
diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py
index 0e4ad1d220..e83a381d52 100755
--- a/lib/python/qmk/json_encoders.py
+++ b/lib/python/qmk/json_encoders.py
@@ -235,3 +235,31 @@ class UserspaceJSONEncoder(QMKJSONEncoder):
235 return '01build_targets' 235 return '01build_targets'
236 236
237 return key 237 return key
238
239
240class CommunityModuleJSONEncoder(QMKJSONEncoder):
241 """Custom encoder to make qmk_module.json's a little nicer to work with.
242 """
243 def sort_dict(self, item):
244 """Sorts the hashes in a nice way.
245 """
246 key = item[0]
247
248 if self.indentation_level == 1:
249 if key == 'module_name':
250 return '00module_name'
251 if key == 'maintainer':
252 return '01maintainer'
253 if key == 'url':
254 return '02url'
255 if key == 'features':
256 return '03features'
257 if key == 'keycodes':
258 return '04keycodes'
259 elif self.indentation_level == 3: # keycodes
260 if key == 'key':
261 return '00key'
262 if key == 'aliases':
263 return '01aliases'
264
265 return key
diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py
index 9dd043c4a8..8e36461722 100644
--- a/lib/python/qmk/keymap.py
+++ b/lib/python/qmk/keymap.py
@@ -334,33 +334,6 @@ def write_json(keyboard, keymap, layout, layers, macros=None):
334 return write_file(keymap_file, keymap_content) 334 return write_file(keymap_file, keymap_content)
335 335
336 336
337def write(keymap_json):
338 """Generate the `keymap.c` and write it to disk.
339
340 Returns the filename written to.
341
342 `keymap_json` should be a dict with the following keys:
343 keyboard
344 The name of the keyboard
345
346 keymap
347 The name of the keymap
348
349 layout
350 The LAYOUT macro this keymap uses.
351
352 layers
353 An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
354
355 macros
356 A list of macros for this keymap.
357 """
358 keymap_content = generate_c(keymap_json)
359 keymap_file = qmk.path.keymaps(keymap_json['keyboard'])[0] / keymap_json['keymap'] / 'keymap.c'
360
361 return write_file(keymap_file, keymap_content)
362
363
364def locate_keymap(keyboard, keymap, force_layout=None): 337def locate_keymap(keyboard, keymap, force_layout=None):
365 """Returns the path to a keymap for a specific keyboard. 338 """Returns the path to a keymap for a specific keyboard.
366 """ 339 """
diff --git a/modules/qmk/hello_world/hello_world.c b/modules/qmk/hello_world/hello_world.c
new file mode 100644
index 0000000000..d9dd366100
--- /dev/null
+++ b/modules/qmk/hello_world/hello_world.c
@@ -0,0 +1,33 @@
1// Copyright 2025 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3#include QMK_KEYBOARD_H
4
5#include "introspection.h"
6
7ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
8
9uint32_t delayed_hello_world(uint32_t trigger_time, void *cb_arg) {
10 printf("Hello, world! I'm a QMK based keyboard! The keymap array size is %d bytes.\n", (int)hello_world_introspection().total_size);
11 return 0;
12}
13
14void keyboard_post_init_hello_world(void) {
15 keyboard_post_init_hello_world_kb();
16 defer_exec(10000, delayed_hello_world, NULL);
17}
18
19bool process_record_hello_world(uint16_t keycode, keyrecord_t *record) {
20 if (!process_record_hello_world_kb(keycode, record)) {
21 return false;
22 }
23
24 switch (keycode) {
25 case COMMUNITY_MODULE_HELLO:
26 if (record->event.pressed) {
27 SEND_STRING("Hello there.");
28 break;
29 }
30 }
31
32 return true;
33}
diff --git a/modules/qmk/hello_world/introspection.c b/modules/qmk/hello_world/introspection.c
new file mode 100644
index 0000000000..2c32a074f5
--- /dev/null
+++ b/modules/qmk/hello_world/introspection.c
@@ -0,0 +1,10 @@
1// Copyright 2025 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4hello_world_introspection_t hello_world_introspection(void) {
5 hello_world_introspection_t introspection = {
6 .total_size = sizeof(keymaps),
7 .layer_count = sizeof(keymaps) / sizeof(keymaps[0]),
8 };
9 return introspection;
10}
diff --git a/modules/qmk/hello_world/introspection.h b/modules/qmk/hello_world/introspection.h
new file mode 100644
index 0000000000..fd3d7f24a0
--- /dev/null
+++ b/modules/qmk/hello_world/introspection.h
@@ -0,0 +1,10 @@
1// Copyright 2025 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3#include QMK_KEYBOARD_H
4
5typedef struct hello_world_introspection_t {
6 int16_t total_size;
7 int16_t layer_count;
8} hello_world_introspection_t;
9
10hello_world_introspection_t hello_world_introspection(void);
diff --git a/modules/qmk/hello_world/qmk_module.json b/modules/qmk/hello_world/qmk_module.json
new file mode 100644
index 0000000000..4f269cb4e9
--- /dev/null
+++ b/modules/qmk/hello_world/qmk_module.json
@@ -0,0 +1,13 @@
1{
2 "module_name": "Hello World",
3 "maintainer": "QMK Maintainers",
4 "features": {
5 "deferred_exec": true
6 },
7 "keycodes": [
8 {
9 "key": "COMMUNITY_MODULE_HELLO",
10 "aliases": ["CM_HELO"]
11 }
12 ]
13}
diff --git a/modules/qmk/hello_world/rules.mk b/modules/qmk/hello_world/rules.mk
new file mode 100644
index 0000000000..91806fb1e3
--- /dev/null
+++ b/modules/qmk/hello_world/rules.mk
@@ -0,0 +1,2 @@
1# Just a simple rules.mk which tests that they work from a community module.
2$(shell $(QMK_BIN) hello -n "from QMK's hello world community module")
diff --git a/quantum/action.h b/quantum/action.h
index d5b15c6f17..7596688f31 100644
--- a/quantum/action.h
+++ b/quantum/action.h
@@ -45,7 +45,7 @@ typedef struct {
45} tap_t; 45} tap_t;
46 46
47/* Key event container for recording */ 47/* Key event container for recording */
48typedef struct { 48typedef struct keyrecord_t {
49 keyevent_t event; 49 keyevent_t event;
50#ifndef NO_ACTION_TAPPING 50#ifndef NO_ACTION_TAPPING
51 tap_t tap; 51 tap_t tap;
diff --git a/quantum/keyboard.c b/quantum/keyboard.c
index d7836cf36e..ad740de4b3 100644
--- a/quantum/keyboard.c
+++ b/quantum/keyboard.c
@@ -289,6 +289,21 @@ __attribute__((weak)) void keyboard_pre_init_kb(void) {
289 keyboard_pre_init_user(); 289 keyboard_pre_init_user();
290} 290}
291 291
292/** \brief keyboard_pre_init_modules
293 *
294 * FIXME: needs doc
295 */
296__attribute__((weak)) void keyboard_pre_init_modules(void) {}
297
298/** \brief keyboard_pre_init_quantum
299 *
300 * FIXME: needs doc
301 */
302void keyboard_pre_init_quantum(void) {
303 keyboard_pre_init_modules();
304 keyboard_pre_init_kb();
305}
306
292/** \brief keyboard_post_init_user 307/** \brief keyboard_post_init_user
293 * 308 *
294 * FIXME: needs doc 309 * FIXME: needs doc
@@ -305,6 +320,23 @@ __attribute__((weak)) void keyboard_post_init_kb(void) {
305 keyboard_post_init_user(); 320 keyboard_post_init_user();
306} 321}
307 322
323/** \brief keyboard_post_init_modules
324 *
325 * FIXME: needs doc
326 */
327
328__attribute__((weak)) void keyboard_post_init_modules(void) {}
329
330/** \brief keyboard_post_init_quantum
331 *
332 * FIXME: needs doc
333 */
334
335void keyboard_post_init_quantum(void) {
336 keyboard_post_init_modules();
337 keyboard_post_init_kb();
338}
339
308/** \brief matrix_can_read 340/** \brief matrix_can_read
309 * 341 *
310 * Allows overriding when matrix scanning operations should be executed. 342 * Allows overriding when matrix scanning operations should be executed.
@@ -323,7 +355,7 @@ void keyboard_setup(void) {
323 eeprom_driver_init(); 355 eeprom_driver_init();
324#endif 356#endif
325 matrix_setup(); 357 matrix_setup();
326 keyboard_pre_init_kb(); 358 keyboard_pre_init_quantum();
327} 359}
328 360
329#ifndef SPLIT_KEYBOARD 361#ifndef SPLIT_KEYBOARD
@@ -355,6 +387,13 @@ __attribute__((weak)) bool should_process_keypress(void) {
355 return is_keyboard_master(); 387 return is_keyboard_master();
356} 388}
357 389
390/** \brief housekeeping_task_modules
391 *
392 * Codegen will override this if community modules are enabled.
393 * This is specific to keyboard-level functionality.
394 */
395__attribute__((weak)) void housekeeping_task_modules(void) {}
396
358/** \brief housekeeping_task_kb 397/** \brief housekeeping_task_kb
359 * 398 *
360 * Override this function if you have a need to execute code for every keyboard main loop iteration. 399 * 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) {}
374 * Invokes hooks for executing code after QMK is done after each loop iteration. 413 * Invokes hooks for executing code after QMK is done after each loop iteration.
375 */ 414 */
376void housekeeping_task(void) { 415void housekeeping_task(void) {
416 housekeeping_task_modules();
377 housekeeping_task_kb(); 417 housekeeping_task_kb();
378 housekeeping_task_user(); 418 housekeeping_task_user();
379} 419}
@@ -493,7 +533,7 @@ void keyboard_init(void) {
493 debug_enable = true; 533 debug_enable = true;
494#endif 534#endif
495 535
496 keyboard_post_init_kb(); /* Always keep this last */ 536 keyboard_post_init_quantum(); /* Always keep this last */
497} 537}
498 538
499/** \brief key_event_task 539/** \brief key_event_task
diff --git a/quantum/keycodes.h b/quantum/keycodes.h
index 5929e35687..b4fc38f5ff 100644
--- a/quantum/keycodes.h
+++ b/quantum/keycodes.h
@@ -76,6 +76,8 @@ enum qk_keycode_ranges {
76 QK_MACRO_MAX = 0x777F, 76 QK_MACRO_MAX = 0x777F,
77 QK_CONNECTION = 0x7780, 77 QK_CONNECTION = 0x7780,
78 QK_CONNECTION_MAX = 0x77BF, 78 QK_CONNECTION_MAX = 0x77BF,
79 QK_COMMUNITY_MODULE = 0x77C0,
80 QK_COMMUNITY_MODULE_MAX = 0x77FF,
79 QK_LIGHTING = 0x7800, 81 QK_LIGHTING = 0x7800,
80 QK_LIGHTING_MAX = 0x78FF, 82 QK_LIGHTING_MAX = 0x78FF,
81 QK_QUANTUM = 0x7C00, 83 QK_QUANTUM = 0x7C00,
@@ -1476,6 +1478,7 @@ enum qk_keycode_defines {
1476#define IS_QK_STENO(code) ((code) >= QK_STENO && (code) <= QK_STENO_MAX) 1478#define IS_QK_STENO(code) ((code) >= QK_STENO && (code) <= QK_STENO_MAX)
1477#define IS_QK_MACRO(code) ((code) >= QK_MACRO && (code) <= QK_MACRO_MAX) 1479#define IS_QK_MACRO(code) ((code) >= QK_MACRO && (code) <= QK_MACRO_MAX)
1478#define IS_QK_CONNECTION(code) ((code) >= QK_CONNECTION && (code) <= QK_CONNECTION_MAX) 1480#define IS_QK_CONNECTION(code) ((code) >= QK_CONNECTION && (code) <= QK_CONNECTION_MAX)
1481#define IS_QK_COMMUNITY_MODULE(code) ((code) >= QK_COMMUNITY_MODULE && (code) <= QK_COMMUNITY_MODULE_MAX)
1479#define IS_QK_LIGHTING(code) ((code) >= QK_LIGHTING && (code) <= QK_LIGHTING_MAX) 1482#define IS_QK_LIGHTING(code) ((code) >= QK_LIGHTING && (code) <= QK_LIGHTING_MAX)
1480#define IS_QK_QUANTUM(code) ((code) >= QK_QUANTUM && (code) <= QK_QUANTUM_MAX) 1483#define IS_QK_QUANTUM(code) ((code) >= QK_QUANTUM && (code) <= QK_QUANTUM_MAX)
1481#define IS_QK_KB(code) ((code) >= QK_KB && (code) <= QK_KB_MAX) 1484#define IS_QK_KB(code) ((code) >= QK_KB && (code) <= QK_KB_MAX)
diff --git a/quantum/keymap_introspection.c b/quantum/keymap_introspection.c
index 236b54ce98..23e842353a 100644
--- a/quantum/keymap_introspection.c
+++ b/quantum/keymap_introspection.c
@@ -1,6 +1,10 @@
1// Copyright 2022 Nick Brassel (@tzarc) 1// Copyright 2022 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later 2// SPDX-License-Identifier: GPL-2.0-or-later
3 3
4#if defined(COMMUNITY_MODULES_ENABLE)
5# include "community_modules_introspection.h"
6#endif // defined(COMMUNITY_MODULES_ENABLE)
7
4// Pull the actual keymap code so that we can inspect stuff from it 8// Pull the actual keymap code so that we can inspect stuff from it
5#include KEYMAP_C 9#include KEYMAP_C
6 10
@@ -171,3 +175,10 @@ __attribute__((weak)) const key_override_t* key_override_get(uint16_t key_overri
171} 175}
172 176
173#endif // defined(KEY_OVERRIDE_ENABLE) 177#endif // defined(KEY_OVERRIDE_ENABLE)
178
179////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
180// Community modules (must be last in this file!)
181
182#if defined(COMMUNITY_MODULES_ENABLE)
183# include "community_modules_introspection.c"
184#endif // defined(COMMUNITY_MODULES_ENABLE)
diff --git a/quantum/os_detection.c b/quantum/os_detection.c
index 84bbeeed54..9a9f9052f2 100644
--- a/quantum/os_detection.c
+++ b/quantum/os_detection.c
@@ -72,6 +72,8 @@ static volatile struct usb_device_state maxprev_usb_device_state = {.configure_s
72static volatile bool debouncing = false; 72static volatile bool debouncing = false;
73static volatile fast_timer_t last_time = 0; 73static volatile fast_timer_t last_time = 0;
74 74
75bool process_detected_host_os_modules(os_variant_t os);
76
75void os_detection_task(void) { 77void os_detection_task(void) {
76#ifdef OS_DETECTION_KEYBOARD_RESET 78#ifdef OS_DETECTION_KEYBOARD_RESET
77 // resetting the keyboard on the USB device state change callback results in instability, so delegate that to this task 79 // 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) {
96 if (detected_os != reported_os || first_report) { 98 if (detected_os != reported_os || first_report) {
97 first_report = false; 99 first_report = false;
98 reported_os = detected_os; 100 reported_os = detected_os;
101 process_detected_host_os_modules(detected_os);
99 process_detected_host_os_kb(detected_os); 102 process_detected_host_os_kb(detected_os);
100 } 103 }
101 } 104 }
102 } 105 }
103} 106}
104 107
108__attribute__((weak)) bool process_detected_host_os_modules(os_variant_t os) {
109 return true;
110}
111
105__attribute__((weak)) bool process_detected_host_os_kb(os_variant_t detected_os) { 112__attribute__((weak)) bool process_detected_host_os_kb(os_variant_t detected_os) {
106 return process_detected_host_os_user(detected_os); 113 return process_detected_host_os_user(detected_os);
107} 114}
diff --git a/quantum/quantum.c b/quantum/quantum.c
index b63300add8..adb14d64b6 100644
--- a/quantum/quantum.c
+++ b/quantum/quantum.c
@@ -162,6 +162,10 @@ __attribute__((weak)) void tap_code16(uint16_t code) {
162 tap_code16_delay(code, code == KC_CAPS_LOCK ? TAP_HOLD_CAPS_DELAY : TAP_CODE_DELAY); 162 tap_code16_delay(code, code == KC_CAPS_LOCK ? TAP_HOLD_CAPS_DELAY : TAP_CODE_DELAY);
163} 163}
164 164
165__attribute__((weak)) bool pre_process_record_modules(uint16_t keycode, keyrecord_t *record) {
166 return true;
167}
168
165__attribute__((weak)) bool pre_process_record_kb(uint16_t keycode, keyrecord_t *record) { 169__attribute__((weak)) bool pre_process_record_kb(uint16_t keycode, keyrecord_t *record) {
166 return pre_process_record_user(keycode, record); 170 return pre_process_record_user(keycode, record);
167} 171}
@@ -174,6 +178,10 @@ __attribute__((weak)) bool process_action_kb(keyrecord_t *record) {
174 return true; 178 return true;
175} 179}
176 180
181__attribute__((weak)) bool process_record_modules(uint16_t keycode, keyrecord_t *record) {
182 return true;
183}
184
177__attribute__((weak)) bool process_record_kb(uint16_t keycode, keyrecord_t *record) { 185__attribute__((weak)) bool process_record_kb(uint16_t keycode, keyrecord_t *record) {
178 return process_record_user(keycode, record); 186 return process_record_user(keycode, record);
179} 187}
@@ -182,12 +190,22 @@ __attribute__((weak)) bool process_record_user(uint16_t keycode, keyrecord_t *re
182 return true; 190 return true;
183} 191}
184 192
193__attribute__((weak)) void post_process_record_modules(uint16_t keycode, keyrecord_t *record) {}
194
185__attribute__((weak)) void post_process_record_kb(uint16_t keycode, keyrecord_t *record) { 195__attribute__((weak)) void post_process_record_kb(uint16_t keycode, keyrecord_t *record) {
186 post_process_record_user(keycode, record); 196 post_process_record_user(keycode, record);
187} 197}
188 198
189__attribute__((weak)) void post_process_record_user(uint16_t keycode, keyrecord_t *record) {} 199__attribute__((weak)) void post_process_record_user(uint16_t keycode, keyrecord_t *record) {}
190 200
201__attribute__((weak)) bool shutdown_modules(bool jump_to_bootloader) {
202 return true;
203}
204
205__attribute__((weak)) void suspend_power_down_modules(void) {}
206
207__attribute__((weak)) void suspend_wakeup_init_modules(void) {}
208
191void shutdown_quantum(bool jump_to_bootloader) { 209void shutdown_quantum(bool jump_to_bootloader) {
192 clear_keyboard(); 210 clear_keyboard();
193#if defined(MIDI_ENABLE) && defined(MIDI_BASIC) 211#if defined(MIDI_ENABLE) && defined(MIDI_BASIC)
@@ -199,11 +217,13 @@ void shutdown_quantum(bool jump_to_bootloader) {
199# endif 217# endif
200 uint16_t timer_start = timer_read(); 218 uint16_t timer_start = timer_read();
201 PLAY_SONG(goodbye_song); 219 PLAY_SONG(goodbye_song);
220 shutdown_modules(jump_to_bootloader);
202 shutdown_kb(jump_to_bootloader); 221 shutdown_kb(jump_to_bootloader);
203 while (timer_elapsed(timer_start) < 250) 222 while (timer_elapsed(timer_start) < 250)
204 wait_ms(1); 223 wait_ms(1);
205 stop_all_notes(); 224 stop_all_notes();
206#else 225#else
226 shutdown_modules(jump_to_bootloader);
207 shutdown_kb(jump_to_bootloader); 227 shutdown_kb(jump_to_bootloader);
208 wait_ms(250); 228 wait_ms(250);
209#endif 229#endif
@@ -258,7 +278,7 @@ uint16_t get_event_keycode(keyevent_t event, bool update_layer_cache) {
258 278
259/* Get keycode, and then process pre tapping functionality */ 279/* Get keycode, and then process pre tapping functionality */
260bool pre_process_record_quantum(keyrecord_t *record) { 280bool pre_process_record_quantum(keyrecord_t *record) {
261 return pre_process_record_kb(get_record_keycode(record, true), record) && 281 return pre_process_record_modules(get_record_keycode(record, true), record) && pre_process_record_kb(get_record_keycode(record, true), record) &&
262#ifdef COMBO_ENABLE 282#ifdef COMBO_ENABLE
263 process_combo(get_record_keycode(record, true), record) && 283 process_combo(get_record_keycode(record, true), record) &&
264#endif 284#endif
@@ -268,6 +288,7 @@ bool pre_process_record_quantum(keyrecord_t *record) {
268/* Get keycode, and then call keyboard function */ 288/* Get keycode, and then call keyboard function */
269void post_process_record_quantum(keyrecord_t *record) { 289void post_process_record_quantum(keyrecord_t *record) {
270 uint16_t keycode = get_record_keycode(record, false); 290 uint16_t keycode = get_record_keycode(record, false);
291 post_process_record_modules(keycode, record);
271 post_process_record_kb(keycode, record); 292 post_process_record_kb(keycode, record);
272} 293}
273 294
@@ -332,6 +353,7 @@ bool process_record_quantum(keyrecord_t *record) {
332#if defined(POINTING_DEVICE_ENABLE) && defined(POINTING_DEVICE_AUTO_MOUSE_ENABLE) 353#if defined(POINTING_DEVICE_ENABLE) && defined(POINTING_DEVICE_AUTO_MOUSE_ENABLE)
333 process_auto_mouse(keycode, record) && 354 process_auto_mouse(keycode, record) &&
334#endif 355#endif
356 process_record_modules(keycode, record) && // modules must run before kb
335 process_record_kb(keycode, record) && 357 process_record_kb(keycode, record) &&
336#if defined(VIA_ENABLE) 358#if defined(VIA_ENABLE)
337 process_record_via(keycode, record) && 359 process_record_via(keycode, record) &&
@@ -526,6 +548,7 @@ __attribute__((weak)) bool shutdown_kb(bool jump_to_bootloader) {
526} 548}
527 549
528void suspend_power_down_quantum(void) { 550void suspend_power_down_quantum(void) {
551 suspend_power_down_modules();
529 suspend_power_down_kb(); 552 suspend_power_down_kb();
530#ifndef NO_SUSPEND_POWER_DOWN 553#ifndef NO_SUSPEND_POWER_DOWN
531// Turn off backlight 554// Turn off backlight
@@ -593,6 +616,7 @@ __attribute__((weak)) void suspend_wakeup_init_quantum(void) {
593#if defined(RGB_MATRIX_ENABLE) 616#if defined(RGB_MATRIX_ENABLE)
594 rgb_matrix_set_suspend_state(false); 617 rgb_matrix_set_suspend_state(false);
595#endif 618#endif
619 suspend_wakeup_init_modules();
596 suspend_wakeup_init_kb(); 620 suspend_wakeup_init_kb();
597} 621}
598 622
diff --git a/quantum/quantum.h b/quantum/quantum.h
index 9db88a54d4..59a415ead4 100644
--- a/quantum/quantum.h
+++ b/quantum/quantum.h
@@ -244,6 +244,10 @@ extern layer_state_t layer_state;
244# include "layer_lock.h" 244# include "layer_lock.h"
245#endif 245#endif
246 246
247#ifdef COMMUNITY_MODULES_ENABLE
248# include "community_modules.h"
249#endif
250
247void set_single_default_layer(uint8_t default_layer); 251void set_single_default_layer(uint8_t default_layer);
248void set_single_persistent_default_layer(uint8_t default_layer); 252void set_single_persistent_default_layer(uint8_t default_layer);
249 253