Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Allow C and JSON keymaps to be used together #16636

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions data/schemas/keymap.jsonschema
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"properties": {
"author": {"type": "string"},
"host_language": {"$ref": "qmk.definitions.v1#/text_identifier"},
"includes": {
"$ref": "qmk.definitions.v1#/string_array",
"minItems": 1
}
"keyboard": {"$ref": "qmk.definitions.v1#/text_identifier"},
"keymap": {"$ref": "qmk.definitions.v1#/text_identifier"},
"layout": {"$ref": "qmk.definitions.v1#/layout_macro"},
Expand Down
68 changes: 68 additions & 0 deletions docs/feature_macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,74 @@ Only basic keycodes (prefixed by `KC_`) are supported. Do not include the `KC_`
* Example, single key: `{"action":"up", "keycodes": ["LSFT"]}`
* Example, multiple keys: `{"action":"up", "keycodes": ["CTRL", "LSFT"]}`

### Combining C macros with JSON keymap macros

It's possible to use both [C macros](#using-macros-in-c-keymaps) and JSON keymap macros at the same time.

Necessary steps:

* Have the C header and source files (e.g.: `macros.h` and `macros.c`) in your keymap directory next to the `keymap.json`. Examples:

* `macros.h`

```c
#pragma once
#include QMK_KEYBOARD_H

enum custom_keycodes {
QMKBEST = SAFE_RANGE,
};
```

* `macros.c`

```c
#include "macros.h"

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case QMKBEST:
if (record->event.pressed) {
// when keycode QMKBEST is pressed
SEND_STRING("QMK is the best thing ever!");
} else {
// when keycode QMKBEST is released
}
break;
}
return true;
};
```
* Add the source file to `SRC` in the keymap's `rules.mk` file.
```make
SRC += macros.c
```

* Include the header in `keymap.json` with `"includes": ["header1.h", "headers2.h", ..., "headerN.h"],`:
```json
{
"keyboard": "handwired/pytest/basic",
"keymap": "default_json",
"layout": "LAYOUT_ortho_1x1",
"includes": ["macros.h"],
"layers": [
["MACRO_0"],
["QMKBEST"]
],
"macros": [
[
"Hello, World!",
{"action":"tap", "keycodes":["ENTER"]}
]
],
"author": "qmk",
"notes": "This file is a keymap.json file for handwired/pytest/basic",
"version": 1
}
```

**Note**: Check the `keyboards/handwired/pytest/macro/keymaps/default` keymap for this example.

## Using Macros in C Keymaps

### `SEND_STRING()` & `process_record_user`
Expand Down
8 changes: 6 additions & 2 deletions keyboards/handwired/pytest/macro/keymaps/default/keymap.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
"keyboard": "handwired/pytest/basic",
"keymap": "default_json",
"layout": "LAYOUT_ortho_1x1",
"layers": [["MACRO_0"]],
"includes": ["macros.h"],
"layers": [
["MACRO_0"],
["QMKBEST"]
],
"macros": [
[
"Hello, World!",
{"action":"tap", "keycodes":["ENTER"]}
]
]
],
"author": "qmk",
"notes": "This file is a keymap.json file for handwired/pytest/basic",
Expand Down
15 changes: 15 additions & 0 deletions keyboards/handwired/pytest/macro/keymaps/default/macros.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#include "macros.h"

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case QMKBEST:
if (record->event.pressed) {
// when keycode QMKBEST is pressed
SEND_STRING("QMK is the best thing ever!");
} else {
// when keycode QMKBEST is released
}
break;
}
return true;
};
6 changes: 6 additions & 0 deletions keyboards/handwired/pytest/macro/keymaps/default/macros.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#pragma once
#include QMK_KEYBOARD_H

enum custom_keycodes {
QMKBEST = SAFE_RANGE,
};
1 change: 1 addition & 0 deletions keyboards/handwired/pytest/macro/keymaps/default/rules.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SRC += macros.c
137 changes: 76 additions & 61 deletions lib/python/qmk/keymap.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

# The `keymap.c` template to use when a keyboard doesn't have its own
DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
__INCLUDES__
__LANG_INCLUDES__
__MISC_INCLUDES__


/* THIS FILE WAS GENERATED!
*
Expand Down Expand Up @@ -182,6 +184,68 @@ def generate_json(keymap, keyboard, layout, layers):
return new_keymap


def _template_macros(keymap_json):
macro_txt = [
'bool process_record_json(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 process_record_user(keycode, record);')
macro_txt.append('};')
macro_txt.append('')

return macro_txt


def generate_c(keymap_json):
"""Returns a `keymap.c`.

Expand Down Expand Up @@ -213,70 +277,21 @@ def generate_c(keymap_json):
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('')

macro_txt = _template_macros(keymap_json)
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')
new_keymap = new_keymap.replace('__LANG_INCLUDES__\n', f'#include "keymap_{keymap_json["host_language"]}.h"\n#include "sendstring_{keymap_json["host_language"]}.h"\n')
else:
new_keymap = new_keymap.replace('__LANG_INCLUDES__\n', '')

if keymap_json.get('includes'):
includes = ''
for include in keymap_json.get('includes'):
includes += f'#include "{include}"\n' if include.endswith('.h') else f'#include "{include}.h"\n'
new_keymap = new_keymap.replace('__MISC_INCLUDES__\n', includes)
else:
new_keymap = new_keymap.replace('__INCLUDES__', '')
new_keymap = new_keymap.replace('__MISC_INCLUDES__\n', '')

return new_keymap

Expand Down
8 changes: 8 additions & 0 deletions quantum/quantum.c
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ __attribute__((weak)) bool process_action_kb(keyrecord_t *record) {
}

__attribute__((weak)) bool process_record_kb(uint16_t keycode, keyrecord_t *record) {
return process_record_json(keycode, record);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really not a fan of introducing this convention, its already bad enough that keyboards forget to call process_record_user. Now there will be a new convention, and a bunch of "broken" keyboards as they do process_record_kb -> process_record_user.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not married to this implementation, but other approaches seemed more "hacky".
Although keyboard's process_record_kb can certainly brake this, keyboards breaking convention might be an other problem we have to tackle in some other way.

I marked this PR to be Draft until we decide how to proceed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If anything, I would say that it should be moved into to the rest of the process_record_* handling is done, just like a normal feature.

}

__attribute__((weak)) bool process_record_json(uint16_t keycode, keyrecord_t *record) {
return process_record_user(keycode, record);
}

Expand All @@ -119,6 +123,10 @@ __attribute__((weak)) void post_process_record_kb(uint16_t keycode, keyrecord_t
post_process_record_user(keycode, record);
}

__attribute__((weak)) void post_process_record_json(uint16_t keycode, keyrecord_t *record) {
post_process_record_user(keycode, record);
}

__attribute__((weak)) void post_process_record_user(uint16_t keycode, keyrecord_t *record) {}

void reset_keyboard(void) {
Expand Down
2 changes: 2 additions & 0 deletions quantum/quantum.h
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,10 @@ uint16_t get_record_keycode(keyrecord_t *record, bool update_layer_cache);
uint16_t get_event_keycode(keyevent_t event, bool update_layer_cache);
bool process_action_kb(keyrecord_t *record);
bool process_record_kb(uint16_t keycode, keyrecord_t *record);
bool process_record_json(uint16_t keycode, keyrecord_t *record);
bool process_record_user(uint16_t keycode, keyrecord_t *record);
void post_process_record_kb(uint16_t keycode, keyrecord_t *record);
void post_process_record_json(uint16_t keycode, keyrecord_t *record);
void post_process_record_user(uint16_t keycode, keyrecord_t *record);

void reset_keyboard(void);
Expand Down