~ruther/qmk_firmware

79b0f9168eafef1642f3223500197bf630a2433f — Joel Challis 2 years ago e35bb8e
Custom keycodes in JSON (#19925)

M data/schemas/definitions.jsonschema => data/schemas/definitions.jsonschema +32 -0
@@ 71,6 71,38 @@
        "type": "string",
        "pattern": "^[0-9a-z][0-9a-z_/]*$"
    },
    "keycode": {
        "type": "string",
        "minLength": 2,
        "maxLength": 50,
        "pattern": "^[A-Z][A-Zs_0-9]*$"
    },
    "keycode_short": {
        "type": "string",
        "minLength": 2,
        "maxLength": 7,
        "pattern": "^[A-Z][A-Zs_0-9]*$"
    },
    "keycode_decl": {
        "type": "object",
        "required": [
            "key"
        ],
        "properties": {
            "key": {"$ref": "#/keycode"},
            "label": {"$ref": "#/text_identifier"},
            "aliases": {
                "type": "array",
                "minItems": 1,
                "items": {"$ref": "#/keycode_short"}
            }
        }
    },
    "keycode_decl_array": {
        "type": "array",
        "minItems": 1
        "items": {"$ref": "#/keycode_decl"}
    },
    "mcu_pin_array": {
        "type": "array",
        "items": {"$ref": "#/mcu_pin"}

M data/schemas/keyboard.jsonschema => data/schemas/keyboard.jsonschema +1 -0
@@ 259,6 259,7 @@
                "on_state": {"$ref": "qmk.definitions.v1#/bit"}
            }
        },
        "keycodes": {"$ref": "qmk.definitions.v1#/keycode_decl_array"},
        "layout_aliases": {
            "type": "object",
            "additionalProperties": {"$ref": "qmk.definitions.v1#/layout_macro"}

M data/schemas/keycodes.jsonschema => data/schemas/keycodes.jsonschema +3 -7
@@ 8,11 8,7 @@
            "type": "string",
            "minLength": 2,
            "maxLength": 50,
            "pattern": "^[A-Zs_0-9]*$"
        },
        "hex_number_4d": {
            "type": "string",
            "pattern": "^0x[0-9A-F]{4}$"
            "pattern": "^[A-Z][A-Zs_0-9]*$"
        }
    },
    "properties": {


@@ 34,10 30,10 @@
        "keycodes": {
            "type": "object",
            "propertyNames": {
                "$ref": "#/definitions/hex_number_4d"
                "$ref": "qmk.definitions.v1#/hex_number_4d"
            },
            "additionalProperties": {
                "type": "object",
                "type": "object", // use 'qmk.definitions.v1#/keycode_decl' when problem keycodes are removed
                "required": [
                    "key"
                ],

M data/schemas/keymap.jsonschema => data/schemas/keymap.jsonschema +1 -0
@@ 67,6 67,7 @@
                }
            }
        },
        "keycodes": {"$ref": "qmk.definitions.v1#/keycode_decl_array"},
        "config": {"$ref": "qmk.keyboard.v1"},
        "notes": {
            "type": "string"

A keyboards/handwired/pytest/basic/keymaps/dd_keycodes/keymap.json => keyboards/handwired/pytest/basic/keymaps/dd_keycodes/keymap.json +12 -0
@@ 0,0 1,12 @@
{
    "keyboard": "handwired/pytest/basic",
    "keymap": "default_json",
    "layout": "LAYOUT_ortho_1x1",
    "layers": [["EXAMPLE_1"]],
    "keycodes": [
        { "key": "EXAMPLE_1" }
    ],
    "author": "qmk",
    "notes": "This file is a keymap.json file for handwired/pytest/basic",
    "version": 1
}

M lib/python/qmk/cli/generate/api.py => lib/python/qmk/cli/generate/api.py +6 -0
@@ 67,6 67,12 @@ def _filtered_copy(src, dst):
        dst.write_text(json.dumps(data), encoding='utf-8')
        return dst

    if dst.suffix == '.jsonschema':
        data = json_load(src)

        dst.write_text(json.dumps(data), encoding='utf-8')
        return dst

    return shutil.copy2(src, dst)



M lib/python/qmk/cli/generate/keyboard_h.py => lib/python/qmk/cli/generate/keyboard_h.py +38 -6
@@ 11,12 11,9 @@ from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.constants import COL_LETTERS, ROW_LETTERS, GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE


def _generate_layouts(keyboard):
    """Generates the layouts.h file.
def _generate_layouts(keyboard, kb_info_json):
    """Generates the layouts macros.
    """
    # Build the info.json file
    kb_info_json = info_json(keyboard)

    if 'matrix_size' not in kb_info_json:
        cli.log.error(f'{keyboard}: Invalid matrix config.')
        return []


@@ 65,6 62,32 @@ def _generate_layouts(keyboard):
    return lines


def _generate_keycodes(kb_info_json):
    """Generates keyboard level keycodes.
    """
    if 'keycodes' not in kb_info_json:
        return []

    lines = []
    lines.append('enum keyboard_keycodes {')

    for index, item in enumerate(kb_info_json.get('keycodes')):
        key = item["key"]
        if index == 0:
            lines.append(f'  {key} = QK_KB_0,')
        else:
            lines.append(f'  {key},')

    lines.append('};')

    for item in kb_info_json.get('keycodes', []):
        key = item["key"]
        for alias in item.get("aliases", []):
            lines.append(f'#define {alias} {key}')

    return lines


@cli.argument('-i', '--include', nargs='?', arg_only=True, help='Optional file to include')
@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")


@@ 73,8 96,12 @@ def _generate_layouts(keyboard):
def generate_keyboard_h(cli):
    """Generates the keyboard.h file.
    """
    # Build the info.json file
    kb_info_json = info_json(cli.args.keyboard)

    keyboard_h = cli.args.include
    dd_layouts = _generate_layouts(cli.args.keyboard)
    dd_layouts = _generate_layouts(cli.args.keyboard, kb_info_json)
    dd_keycodes = _generate_keycodes(kb_info_json)
    valid_config = dd_layouts or keyboard_h

    # Build the layouts.h file.


@@ 87,6 114,11 @@ def generate_keyboard_h(cli):
    if keyboard_h:
        keyboard_h_lines.append(f'#include "{Path(keyboard_h).name}"')

    keyboard_h_lines.append('')
    keyboard_h_lines.append('// Keycode content')
    if dd_keycodes:
        keyboard_h_lines.extend(dd_keycodes)

    # Protect against poorly configured keyboards
    if not valid_config:
        keyboard_h_lines.append('#error("<keyboard>.h is required unless your keyboard uses data-driven configuration. Please rename your keyboard\'s header file to <keyboard>.h")')

M lib/python/qmk/info.py => lib/python/qmk/info.py +6 -0
@@ 103,6 103,12 @@ def _validate(keyboard, info_data):
        if layout_name not in layouts and layout_name not in layout_aliases:
            _log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name))

    # keycodes with length > 7 must have short forms for visualisation purposes
    for decl in info_data.get('keycodes', []):
        if len(decl["key"]) > 7:
            if not decl.get("aliases", []):
                _log_error(info_data, f'Keycode {decl["key"]} has no short form alias')


def info_json(keyboard):
    """Generate the info.json data for a specific keyboard.

M lib/python/qmk/keymap.py => lib/python/qmk/keymap.py +30 -0
@@ 25,6 25,7 @@ __INCLUDES__
 * This file was generated by qmk json2c. You may or may not want to
 * edit it directly.
 */
__KEYCODE_OUTPUT_GOES_HERE__

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
__KEYMAP_GOES_HERE__


@@ 123,6 124,29 @@ def _generate_macros_function(keymap_json):
    return macro_txt


def _generate_keycodes_function(keymap_json):
    """Generates keymap level keycodes.
    """
    lines = []
    lines.append('enum keymap_keycodes {')

    for index, item in enumerate(keymap_json.get('keycodes', [])):
        key = item["key"]
        if index == 0:
            lines.append(f'  {key} = QK_USER_0,')
        else:
            lines.append(f'  {key},')

    lines.append('};')

    for item in keymap_json.get('keycodes', []):
        key = item["key"]
        for alias in item.get("aliases", []):
            lines.append(f'#define {alias} {key}')

    return lines


def template_json(keyboard):
    """Returns a `keymap.json` template for a keyboard.



@@ 317,6 341,12 @@ def generate_c(keymap_json):
        hostlang = f'#include "keymap_{keymap_json["host_language"]}.h"\n#include "sendstring_{keymap_json["host_language"]}.h"\n'
    new_keymap = new_keymap.replace('__INCLUDES__', hostlang)

    keycodes = ''
    if 'keycodes' in keymap_json and keymap_json['keycodes'] is not None:
        keycodes_txt = _generate_keycodes_function(keymap_json)
        keycodes = '\n'.join(keycodes_txt)
    new_keymap = new_keymap.replace('__KEYCODE_OUTPUT_GOES_HERE__', keycodes)

    return new_keymap