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>
This commit is contained in:
		
							parent
							
								
									8181b155db
								
							
						
					
					
						commit
						08ce0142ba
					
				
					 16 changed files with 319 additions and 33 deletions
				
			
		| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue