summary refs log tree commit diff
path: root/lib/python
diff options
context:
space:
mode:
authorZach White <skullydazed@gmail.com>2021-11-22 11:11:35 -0800
committerGitHub <noreply@github.com>2021-11-22 11:11:35 -0800
commit08ce0142bad40f22d05d33fdef8a7c8907154e96 (patch)
tree5b5da4650a76ec902a550e2719b79ffc2a73d74d /lib/python
parent8181b155dbfd07561200b30b52a4046f2da92248 (diff)
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 <nick@tzarc.org>

* escape backslash characters

* format

* flake8

* Update quantum/quantum_keycodes.h

Co-authored-by: Nick Brassel <nick@tzarc.org>
Diffstat (limited to 'lib/python')
-rwxr-xr-xlib/python/qmk/cli/json2c.py2
-rw-r--r--lib/python/qmk/commands.py2
-rw-r--r--lib/python/qmk/keymap.py100
-rw-r--r--lib/python/qmk/tests/test_cli_commands.py8
-rw-r--r--lib/python/qmk/tests/test_qmk_keymap.py8
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'