First stab at keycodes.
This commit is contained in:
parent
54673745a6
commit
1354424fd7
9 changed files with 159 additions and 31 deletions
7
data/constants/keycodes/keycodes_0.0.7.hjson
Normal file
7
data/constants/keycodes/keycodes_0.0.7.hjson
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"ranges": {
|
||||
"0x77C0/0x003F": {
|
||||
"define": "QK_COMMUNITY_MODULE"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
data/schemas/community_module.jsonschema
Normal file
16
data/schemas/community_module.jsonschema
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema#",
|
||||
"$id": "qmk.community_module.v1",
|
||||
"title": "Community Module Information",
|
||||
"type": "object",
|
||||
"required": ["module_name", "maintainer"]
|
||||
"properties": {
|
||||
"module_name": {"$ref": "qmk.definitions.v1#/text_identifier"},
|
||||
"maintainer": {"$ref": "qmk.definitions.v1#/text_identifier"},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"keycodes": {"$ref": "qmk.definitions.v1#/keycode_decl_array"},
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ from milc import cli
|
|||
|
||||
from qmk.info import info_json
|
||||
from qmk.json_schema import json_load, validate
|
||||
from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder
|
||||
from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder, CommunityModuleJSONEncoder
|
||||
from qmk.path import normpath
|
||||
|
||||
|
||||
|
|
@ -30,6 +30,13 @@ def _detect_json_format(file, json_data):
|
|||
except ValidationError:
|
||||
pass
|
||||
|
||||
if json_encoder is None:
|
||||
try:
|
||||
validate(json_data, 'qmk.community_module.v1')
|
||||
json_encoder = CommunityModuleJSONEncoder
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
if json_encoder is None:
|
||||
try:
|
||||
validate(json_data, 'qmk.keyboard.v1')
|
||||
|
|
@ -54,6 +61,8 @@ def _get_json_encoder(file, json_data):
|
|||
json_encoder = KeymapJSONEncoder
|
||||
elif cli.args.format == 'userspace':
|
||||
json_encoder = UserspaceJSONEncoder
|
||||
elif cli.args.format == 'community_module':
|
||||
json_encoder = CommunityModuleJSONEncoder
|
||||
else:
|
||||
# This should be impossible
|
||||
cli.log.error('Unknown format: %s', cli.args.format)
|
||||
|
|
@ -61,7 +70,7 @@ def _get_json_encoder(file, json_data):
|
|||
|
||||
|
||||
@cli.argument('json_file', arg_only=True, type=normpath, help='JSON file to format')
|
||||
@cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)')
|
||||
@cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace', 'community_module'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)')
|
||||
@cli.argument('-i', '--inplace', action='store_true', arg_only=True, help='If set, will operate in-place on the input file')
|
||||
@cli.argument('-p', '--print', action='store_true', arg_only=True, help='If set, will print the formatted json to stdout ')
|
||||
@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
|
||||
|
|
|
|||
|
|
@ -2,40 +2,17 @@ import contextlib
|
|||
from argcomplete.completers import FilesCompleter
|
||||
|
||||
from milc import cli
|
||||
from milc.attrdict import AttrDict
|
||||
|
||||
import qmk.path
|
||||
from qmk.info import get_modules
|
||||
from qmk.keyboard import keyboard_completer, keyboard_folder
|
||||
from qmk.commands import dump_lines
|
||||
from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
|
||||
|
||||
|
||||
class ModuleAPI(AttrDict):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__()
|
||||
for key, value in kwargs.items():
|
||||
self[key] = value
|
||||
|
||||
|
||||
MODULE_API_LIST = [
|
||||
ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', guard=None, header=None),
|
||||
ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', guard=None, header=None),
|
||||
ModuleAPI(ret_type='bool', name='pre_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None),
|
||||
ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None),
|
||||
ModuleAPI(ret_type='void', name='post_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None),
|
||||
ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', guard=None, header=None),
|
||||
ModuleAPI(ret_type='void', name='suspend_power_down', args='void', call_params='', guard=None, header=None),
|
||||
ModuleAPI(ret_type='void', name='suspend_wakeup_init', args='void', call_params='', guard=None, header=None),
|
||||
ModuleAPI(ret_type='bool', name='shutdown', args='bool jump_to_bootloader', call_params='jump_to_bootloader', guard=None, header=None),
|
||||
ModuleAPI(ret_type='bool', name='process_detected_host_os', args='os_variant_t os', call_params='os', guard="defined(OS_DETECTION_ENABLE)", header="os_detection.h"),
|
||||
]
|
||||
|
||||
MODULE_API_VERSION = '20250122'
|
||||
from qmk.community_modules import MODULE_API_LIST, MODULE_API_VERSION, load_module_jsons
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _api_guard(lines, api):
|
||||
def _render_api_guard(lines, api):
|
||||
if api.guard:
|
||||
lines.append(f'#if {api.guard}')
|
||||
yield
|
||||
|
|
@ -47,15 +24,35 @@ def _render_api_header(api):
|
|||
lines = []
|
||||
if api.header:
|
||||
lines.append('')
|
||||
with _api_guard(lines, api):
|
||||
with _render_api_guard(lines, api):
|
||||
lines.append(f'#include <{api.header}>')
|
||||
return lines
|
||||
|
||||
|
||||
def _render_keycodes(module_jsons):
|
||||
lines = []
|
||||
lines.append('')
|
||||
lines.append('enum {')
|
||||
first = True
|
||||
for module_json in module_jsons:
|
||||
keycodes = module_json.get('keycodes', [])
|
||||
for keycode in keycodes:
|
||||
key = keycode.get('key', None)
|
||||
if first:
|
||||
lines.append(f' {key} = QK_COMMUNITY_MODULE,')
|
||||
first = False
|
||||
else:
|
||||
lines.append(f' {key},')
|
||||
lines.append(' LAST_COMMUNITY_MODULE_KEY')
|
||||
lines.append('};')
|
||||
lines.append('_Static_assert(LAST_COMMUNITY_MODULE_KEY <= QK_COMMUNITY_MODULE_MAX, "Too many community module keycodes");')
|
||||
return lines
|
||||
|
||||
|
||||
def _render_api_declarations(api, module, user_kb=True):
|
||||
lines = []
|
||||
lines.append('')
|
||||
with _api_guard(lines, api):
|
||||
with _render_api_guard(lines, api):
|
||||
if user_kb:
|
||||
lines.append(f'{api.ret_type} {api.name}_{module}_user({api.args});')
|
||||
lines.append(f'{api.ret_type} {api.name}_{module}_kb({api.args});')
|
||||
|
|
@ -66,7 +63,7 @@ def _render_api_declarations(api, module, user_kb=True):
|
|||
def _render_api_implementations(api, module):
|
||||
lines = []
|
||||
lines.append('')
|
||||
with _api_guard(lines, api):
|
||||
with _render_api_guard(lines, api):
|
||||
# _user
|
||||
lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}_user({api.args}) {{')
|
||||
if api.ret_type == 'bool':
|
||||
|
|
@ -100,7 +97,7 @@ def _render_api_implementations(api, module):
|
|||
def _render_core_implementation(api, modules):
|
||||
lines = []
|
||||
lines.append('')
|
||||
with _api_guard(lines, api):
|
||||
with _render_api_guard(lines, api):
|
||||
lines.append(f'{api.ret_type} {api.name}_modules({api.args}) {{')
|
||||
if api.ret_type == 'bool':
|
||||
lines.append(' return true')
|
||||
|
|
@ -132,6 +129,7 @@ def generate_community_modules_h(cli):
|
|||
'#pragma once',
|
||||
'#include <stdint.h>',
|
||||
'#include <stdbool.h>',
|
||||
'#include <keycodes.h>',
|
||||
'',
|
||||
f'#define COMMUNITY_MODULES_API_VERSION {MODULE_API_VERSION}',
|
||||
f'#define ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(x) _Static_assert((x) <= COMMUNITY_MODULES_API_VERSION, "Community module requires higher version of QMK modules API -- needs: " #x ", current: {MODULE_API_VERSION}.")',
|
||||
|
|
@ -141,7 +139,10 @@ def generate_community_modules_h(cli):
|
|||
]
|
||||
|
||||
modules = get_modules(cli.args.keyboard, cli.args.filename)
|
||||
module_jsons = load_module_jsons(modules)
|
||||
if len(modules) > 0:
|
||||
lines.extend(_render_keycodes(module_jsons))
|
||||
|
||||
for api in MODULE_API_LIST:
|
||||
lines.extend(_render_api_header(api))
|
||||
|
||||
|
|
|
|||
48
lib/python/qmk/community_modules.py
Normal file
48
lib/python/qmk/community_modules.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from milc.attrdict import AttrDict
|
||||
|
||||
from qmk.json_schema import json_load, validate
|
||||
from qmk.util import truthy
|
||||
|
||||
class ModuleAPI(AttrDict):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__()
|
||||
for key, value in kwargs.items():
|
||||
self[key] = value
|
||||
|
||||
|
||||
MODULE_API_LIST = [
|
||||
ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', guard=None, header=None),
|
||||
ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', guard=None, header=None),
|
||||
ModuleAPI(ret_type='bool', name='pre_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None),
|
||||
ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None),
|
||||
ModuleAPI(ret_type='void', name='post_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None),
|
||||
ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', guard=None, header=None),
|
||||
ModuleAPI(ret_type='void', name='suspend_power_down', args='void', call_params='', guard=None, header=None),
|
||||
ModuleAPI(ret_type='void', name='suspend_wakeup_init', args='void', call_params='', guard=None, header=None),
|
||||
ModuleAPI(ret_type='bool', name='shutdown', args='bool jump_to_bootloader', call_params='jump_to_bootloader', guard=None, header=None),
|
||||
ModuleAPI(ret_type='bool', name='process_detected_host_os', args='os_variant_t os', call_params='os', guard="defined(OS_DETECTION_ENABLE)", header="os_detection.h"),
|
||||
]
|
||||
|
||||
MODULE_API_VERSION = '20250122'
|
||||
|
||||
def load_module_jsons(modules):
|
||||
"""Load the module JSON files, matching the specified order.
|
||||
"""
|
||||
module_jsons = []
|
||||
for module in modules:
|
||||
|
||||
module_json_path = Path('modules') / module / 'qmk_module.json'
|
||||
if not module_json_path.exists():
|
||||
raise FileNotFoundError(f'Module JSON file not found: {module_json_path}')
|
||||
|
||||
module_json = json_load(module_json_path)
|
||||
|
||||
if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False):
|
||||
validate(module_json, 'qmk.community_module.v1')
|
||||
|
||||
module_jsons.append(module_json)
|
||||
|
||||
return module_jsons
|
||||
|
|
@ -235,3 +235,24 @@ class UserspaceJSONEncoder(QMKJSONEncoder):
|
|||
return '01build_targets'
|
||||
|
||||
return key
|
||||
|
||||
|
||||
class CommunityModuleJSONEncoder(QMKJSONEncoder):
|
||||
"""Custom encoder to make qmk_module.json's a little nicer to work with.
|
||||
"""
|
||||
def sort_dict(self, item):
|
||||
"""Sorts the hashes in a nice way.
|
||||
"""
|
||||
key = item[0]
|
||||
|
||||
if self.indentation_level == 1:
|
||||
if key == 'module_name':
|
||||
return '00module_name'
|
||||
if key == 'maintainer':
|
||||
return '01maintainer'
|
||||
if key == 'url':
|
||||
return '02url'
|
||||
if key == 'keycodes':
|
||||
return '03keycodes'
|
||||
|
||||
return key
|
||||
|
|
|
|||
|
|
@ -21,3 +21,19 @@ void housekeeping_task_hello_world(void) {
|
|||
print("Hello, world! I'm a QMK based keyboard!\n");
|
||||
}
|
||||
}
|
||||
|
||||
bool process_record_hello_world(uint16_t keycode, keyrecord_t *record) {
|
||||
if (!process_record_hello_world_kb(keycode, record)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (keycode) {
|
||||
case CM_HELLO:
|
||||
if (record->event.pressed) {
|
||||
SEND_STRING("Hello there.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
7
modules/hello_world/qmk_module.json
Normal file
7
modules/hello_world/qmk_module.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"module_name": "Hello World",
|
||||
"maintainer": "QMK Maintainers",
|
||||
"keycodes": [
|
||||
{ "key": "CM_HELLO" }
|
||||
]
|
||||
}
|
||||
|
|
@ -76,6 +76,8 @@ enum qk_keycode_ranges {
|
|||
QK_MACRO_MAX = 0x777F,
|
||||
QK_CONNECTION = 0x7780,
|
||||
QK_CONNECTION_MAX = 0x77BF,
|
||||
QK_COMMUNITY_MODULE = 0x77C0,
|
||||
QK_COMMUNITY_MODULE_MAX = 0x77FF,
|
||||
QK_LIGHTING = 0x7800,
|
||||
QK_LIGHTING_MAX = 0x78FF,
|
||||
QK_QUANTUM = 0x7C00,
|
||||
|
|
@ -1476,6 +1478,7 @@ enum qk_keycode_defines {
|
|||
#define IS_QK_STENO(code) ((code) >= QK_STENO && (code) <= QK_STENO_MAX)
|
||||
#define IS_QK_MACRO(code) ((code) >= QK_MACRO && (code) <= QK_MACRO_MAX)
|
||||
#define IS_QK_CONNECTION(code) ((code) >= QK_CONNECTION && (code) <= QK_CONNECTION_MAX)
|
||||
#define IS_QK_COMMUNITY_MODULE(code) ((code) >= QK_COMMUNITY_MODULE && (code) <= QK_COMMUNITY_MODULE_MAX)
|
||||
#define IS_QK_LIGHTING(code) ((code) >= QK_LIGHTING && (code) <= QK_LIGHTING_MAX)
|
||||
#define IS_QK_QUANTUM(code) ((code) >= QK_QUANTUM && (code) <= QK_QUANTUM_MAX)
|
||||
#define IS_QK_KB(code) ((code) >= QK_KB && (code) <= QK_KB_MAX)
|
||||
|
|
|
|||
Loading…
Reference in a new issue