diff options
| author | Zach White <[email protected]> | 2021-11-22 11:11:35 -0800 | 
|---|---|---|
| committer | GitHub <[email protected]> | 2021-11-22 11:11:35 -0800 | 
| commit | 08ce0142bad40f22d05d33fdef8a7c8907154e96 (patch) | |
| tree | 5b5da4650a76ec902a550e2719b79ffc2a73d74d /lib/python | |
| parent | 8181b155dbfd07561200b30b52a4046f2da92248 (diff) | |
| download | qmk_firmware-08ce0142bad40f22d05d33fdef8a7c8907154e96.tar.gz qmk_firmware-08ce0142bad40f22d05d33fdef8a7c8907154e96.zip | |
Macros in JSON keymaps (#14374)
* macros in json keymaps
* add advanced macro support to json
* add a note about escaping macro strings
* add simple examples
* format json
* add support for language specific keymap extras
* switch to dictionaries instead of inline text for macros
* use SS_TAP on the innermost tap keycode
* add the new macro format to the schema
* document the macro limit
* add the json keyword for syntax highlighting
* fix format that vscode screwed up
* Update feature_macros.md
* add tests for macros
* change ding to beep
* add json support for SENDSTRING_BELL
* update doc based on feedback from sigprof
* document host_layout
* remove unused var
* improve carriage return handling
* support tab characters as well
* Update docs/feature_macros.md
Co-authored-by: Nick Brassel <[email protected]>
* escape backslash characters
* format
* flake8
* Update quantum/quantum_keycodes.h
Co-authored-by: Nick Brassel <[email protected]>
Diffstat (limited to 'lib/python')
| -rwxr-xr-x | lib/python/qmk/cli/json2c.py | 2 | ||||
| -rw-r--r-- | lib/python/qmk/commands.py | 2 | ||||
| -rw-r--r-- | lib/python/qmk/keymap.py | 100 | ||||
| -rw-r--r-- | lib/python/qmk/tests/test_cli_commands.py | 8 | ||||
| -rw-r--r-- | lib/python/qmk/tests/test_qmk_keymap.py | 8 | 
5 files changed, 105 insertions, 15 deletions
| diff --git a/lib/python/qmk/cli/json2c.py b/lib/python/qmk/cli/json2c.py index a90578c021..ae8248e6b7 100755 --- a/lib/python/qmk/cli/json2c.py +++ b/lib/python/qmk/cli/json2c.py @@ -33,7 +33,7 @@ def json2c(cli):          cli.args.output = None      # Generate the keymap -    keymap_c = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers']) +    keymap_c = qmk.keymap.generate_c(user_keymap)      if cli.args.output:          cli.args.output.parent.mkdir(parents=True, exist_ok=True) diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index 2995a5fdab..5a01943773 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -190,7 +190,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va      target = f'{keyboard_filesafe}_{user_keymap["keymap"]}'      keyboard_output = Path(f'{KEYBOARD_OUTPUT_PREFIX}{keyboard_filesafe}')      keymap_output = Path(f'{keyboard_output}_{user_keymap["keymap"]}') -    c_text = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers']) +    c_text = qmk.keymap.generate_c(user_keymap)      keymap_dir = keymap_output / 'src'      keymap_c = keymap_dir / 'keymap.c' diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py index 6eec49cfd1..00b5a78a5a 100644 --- a/lib/python/qmk/keymap.py +++ b/lib/python/qmk/keymap.py @@ -17,6 +17,7 @@ from qmk.errors import CppError  # The `keymap.c` template to use when a keyboard doesn't have its own  DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H +__INCLUDES__  /* THIS FILE WAS GENERATED!   * @@ -27,6 +28,7 @@ DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H  const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {  __KEYMAP_GOES_HERE__  }; +  """ @@ -180,10 +182,11 @@ def generate_json(keymap, keyboard, layout, layers):      return new_keymap -def generate_c(keyboard, layout, layers): -    """Returns a `keymap.c` or `keymap.json` for the specified keyboard, layout, and layers. +def generate_c(keymap_json): +    """Returns a `keymap.c`. + +    `keymap_json` is a dictionary with the following keys: -    Args:          keyboard              The name of the keyboard @@ -192,19 +195,89 @@ def generate_c(keyboard, layout, layers):          layers              An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. + +        macros +            A sequence of strings containing macros to implement for this keyboard.      """ -    new_keymap = template_c(keyboard) +    new_keymap = template_c(keymap_json['keyboard'])      layer_txt = [] -    for layer_num, layer in enumerate(layers): + +    for layer_num, layer in enumerate(keymap_json['layers']):          if layer_num != 0:              layer_txt[-1] = layer_txt[-1] + ','          layer = map(_strip_any, layer)          layer_keys = ', '.join(layer) -        layer_txt.append('\t[%s] = %s(%s)' % (layer_num, layout, layer_keys)) +        layer_txt.append('\t[%s] = %s(%s)' % (layer_num, keymap_json['layout'], layer_keys))      keymap = '\n'.join(layer_txt)      new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap) +    if keymap_json.get('macros'): +        macro_txt = [ +            'bool process_record_user(uint16_t keycode, keyrecord_t *record) {', +            '    if (record->event.pressed) {', +            '        switch (keycode) {', +        ] + +        for i, macro_array in enumerate(keymap_json['macros']): +            macro = [] + +            for macro_fragment in macro_array: +                if isinstance(macro_fragment, str): +                    macro_fragment = macro_fragment.replace('\\', '\\\\') +                    macro_fragment = macro_fragment.replace('\r\n', r'\n') +                    macro_fragment = macro_fragment.replace('\n', r'\n') +                    macro_fragment = macro_fragment.replace('\r', r'\n') +                    macro_fragment = macro_fragment.replace('\t', r'\t') +                    macro_fragment = macro_fragment.replace('"', r'\"') + +                    macro.append(f'"{macro_fragment}"') + +                elif isinstance(macro_fragment, dict): +                    newstring = [] + +                    if macro_fragment['action'] == 'delay': +                        newstring.append(f"SS_DELAY({macro_fragment['duration']})") + +                    elif macro_fragment['action'] == 'beep': +                        newstring.append(r'"\a"') + +                    elif macro_fragment['action'] == 'tap' and len(macro_fragment['keycodes']) > 1: +                        last_keycode = macro_fragment['keycodes'].pop() + +                        for keycode in macro_fragment['keycodes']: +                            newstring.append(f'SS_DOWN(X_{keycode})') + +                        newstring.append(f'SS_TAP(X_{last_keycode})') + +                        for keycode in reversed(macro_fragment['keycodes']): +                            newstring.append(f'SS_UP(X_{keycode})') + +                    else: +                        for keycode in macro_fragment['keycodes']: +                            newstring.append(f"SS_{macro_fragment['action'].upper()}(X_{keycode})") + +                    macro.append(''.join(newstring)) + +            new_macro = "".join(macro) +            new_macro = new_macro.replace('""', '') +            macro_txt.append(f'            case MACRO_{i}:') +            macro_txt.append(f'                SEND_STRING({new_macro});') +            macro_txt.append('                return false;') + +        macro_txt.append('        }') +        macro_txt.append('    }') +        macro_txt.append('\n    return true;') +        macro_txt.append('};') +        macro_txt.append('') + +        new_keymap = '\n'.join((new_keymap, *macro_txt)) + +    if keymap_json.get('host_language'): +        new_keymap = new_keymap.replace('__INCLUDES__', f'#include "keymap_{keymap_json["host_language"]}.h"\n#include "sendstring_{keymap_json["host_language"]}.h"\n') +    else: +        new_keymap = new_keymap.replace('__INCLUDES__', '') +      return new_keymap @@ -217,7 +290,7 @@ def write_file(keymap_filename, keymap_content):      return keymap_filename -def write_json(keyboard, keymap, layout, layers): +def write_json(keyboard, keymap, layout, layers, macros=None):      """Generate the `keymap.json` and write it to disk.      Returns the filename written to. @@ -235,19 +308,19 @@ def write_json(keyboard, keymap, layout, layers):          layers              An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.      """ -    keymap_json = generate_json(keyboard, keymap, layout, layers) +    keymap_json = generate_json(keyboard, keymap, layout, layers, macros=None)      keymap_content = json.dumps(keymap_json)      keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.json'      return write_file(keymap_file, keymap_content) -def write(keyboard, keymap, layout, layers): +def write(keymap_json):      """Generate the `keymap.c` and write it to disk.      Returns the filename written to. -    Args: +    `keymap_json` should be a dict with the following keys:          keyboard              The name of the keyboard @@ -259,9 +332,12 @@ def write(keyboard, keymap, layout, layers):          layers              An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. + +        macros +            A list of macros for this keymap.      """ -    keymap_content = generate_c(keyboard, layout, layers) -    keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.c' +    keymap_content = generate_c(keymap_json) +    keymap_file = qmk.path.keymap(keymap_json['keyboard']) / keymap_json['keymap'] / 'keymap.c'      return write_file(keymap_file, keymap_content) diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py index 1e3c64e735..2973f81702 100644 --- a/lib/python/qmk/tests/test_cli_commands.py +++ b/lib/python/qmk/tests/test_cli_commands.py @@ -142,6 +142,14 @@ def test_json2c():      assert result.stdout == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT_ortho_1x1(KC_A)};\n\n' +def test_json2c_macros(): +    result = check_subcommand("json2c", 'keyboards/handwired/pytest/macro/keymaps/default/keymap.json') +    check_returncode(result) +    assert 'LAYOUT_ortho_1x1(MACRO_0)' in result.stdout +    assert 'case MACRO_0:' in result.stdout +    assert 'SEND_STRING("Hello, World!"SS_TAP(X_ENTER));' in result.stdout + +  def test_json2c_stdin():      result = check_subcommand_stdin('keyboards/handwired/pytest/has_template/keymaps/default_json/keymap.json', 'json2c', '-')      check_returncode(result) diff --git a/lib/python/qmk/tests/test_qmk_keymap.py b/lib/python/qmk/tests/test_qmk_keymap.py index b9e80df672..5e2efc1232 100644 --- a/lib/python/qmk/tests/test_qmk_keymap.py +++ b/lib/python/qmk/tests/test_qmk_keymap.py @@ -22,7 +22,13 @@ def test_template_json_pytest_has_template():  def test_generate_c_pytest_has_template(): -    templ = qmk.keymap.generate_c('handwired/pytest/has_template', 'LAYOUT', [['KC_A']]) +    keymap_json = { +        'keyboard': 'handwired/pytest/has_template', +        'layout': 'LAYOUT', +        'layers': [['KC_A']], +        'macros': None, +    } +    templ = qmk.keymap.generate_c(keymap_json)      assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT(KC_A)};\n' | 
