feat(behaviors): Support parameterized macros.

* Add two new compatibles for macros that
  take one or two parameters when bound in
  a keymap.
* Use `&macro_param_1to1`, `&macro_param_1to2`, `&macro_param_2to1`,
  and `&macro_param_2to2` control entries in the bindings for the macro
  to have the next binding entry have it's values substituted.

Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>
This commit is contained in:
Peter Johanson 2022-04-08 15:38:46 +00:00 committed by Pete Johanson
parent e686fce4d9
commit 805dd4a53b
16 changed files with 310 additions and 48 deletions

View file

@ -43,7 +43,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
target_sources(app PRIVATE src/behaviors/behavior_sticky_key.c)
target_sources(app PRIVATE src/behaviors/behavior_caps_word.c)
target_sources(app PRIVATE src/behaviors/behavior_key_repeat.c)
target_sources(app PRIVATE src/behaviors/behavior_macro.c)
target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_MACRO app PRIVATE src/behaviors/behavior_macro.c)
target_sources(app PRIVATE src/behaviors/behavior_momentary_layer.c)
target_sources(app PRIVATE src/behaviors/behavior_mod_morph.c)
target_sources(app PRIVATE src/behaviors/behavior_outputs.c)

View file

@ -21,4 +21,9 @@ config ZMK_BEHAVIOR_SENSOR_ROTATE_VAR
bool
default y
depends on DT_HAS_ZMK_BEHAVIOR_SENSOR_ROTATE_VAR_ENABLED
select ZMK_BEHAVIOR_SENSOR_ROTATE_COMMON
select ZMK_BEHAVIOR_SENSOR_ROTATE_COMMON
config ZMK_BEHAVIOR_MACRO
bool
default y
depends on DT_HAS_ZMK_BEHAVIOR_MACRO_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_ONE_PARAM_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_TWO_PARAM_ENABLED

View file

@ -4,16 +4,33 @@
* SPDX-License-Identifier: MIT
*/
#define MACRO_PLACEHOLDER 0
#define ZMK_MACRO_STRINGIFY(x) #x
#define ZMK_MACRO(name,...) \
name: name { \
label = ZMK_MACRO_STRINGIFY(ZM_ ## name); \
compatible = "zmk,behavior-macro"; \
#binding-cells = <0>; \
__VA_ARGS__ \
};
name: name { \
label = ZMK_MACRO_STRINGIFY(ZM_ ## name); \
compatible = "zmk,behavior-macro"; \
#binding-cells = <0>; \
__VA_ARGS__ \
};
/ {
#define ZMK_MACRO1(name,...) \
name: name { \
label = ZMK_MACRO_STRINGIFY(ZM_ ## name); \
compatible = "zmk,behavior-macro-one-param"; \
#binding-cells = <1>; \
__VA_ARGS__ \
};
#define ZMK_MACRO2(name,...) \
name: name { \
label = ZMK_MACRO_STRINGIFY(ZM_ ## name); \
compatible = "zmk,behavior-macro-two-param"; \
#binding-cells = <2>; \
__VA_ARGS__ \
};
/ {
behaviors {
macro_tap: macro_control_mode_tap {
compatible = "zmk,macro-control-mode-tap";
@ -50,5 +67,29 @@
label = "MAC_WAIT_REL";
#binding-cells = <0>;
};
macro_param_1to1: macro_param_1to1 {
compatible = "zmk,macro-param-1to1";
label = "MAC_PARAM_1TO1";
#binding-cells = <0>;
};
macro_param_1to2: macro_param_1to2 {
compatible = "zmk,macro-param-1to2";
label = "MAC_PARAM_1TO2";
#binding-cells = <0>;
};
macro_param_2to1: macro_param_2to1 {
compatible = "zmk,macro-param-2to1";
label = "MAC_PARAM_2TO1";
#binding-cells = <0>;
};
macro_param_2to2: macro_param_2to2 {
compatible = "zmk,macro-param-2to2";
label = "MAC_PARAM_2TO2";
#binding-cells = <0>;
};
};
};

View file

@ -0,0 +1,13 @@
# Copyright (c) 2022 The ZMK Contributors
# SPDX-License-Identifier: MIT
properties:
bindings:
type: phandle-array
required: true
wait-ms:
type: int
description: The default time to wait (in milliseconds) before triggering the next behavior in the macro bindings list.
tap-ms:
type: int
description: The default time to wait (in milliseconds) between the press and release events on a tapped macro behavior binding

View file

@ -0,0 +1,8 @@
# Copyright (c) 2022 The ZMK Contributors
# SPDX-License-Identifier: MIT
description: Macro Behavior
compatible: "zmk,behavior-macro-one-param"
include: [one_param.yaml, macro_base.yaml]

View file

@ -0,0 +1,8 @@
# Copyright (c) 2022 The ZMK Contributors
# SPDX-License-Identifier: MIT
description: Macro Behavior
compatible: "zmk,behavior-macro-two-param"
include: [two_param.yaml, macro_base.yaml]

View file

@ -5,15 +5,4 @@ description: Macro Behavior
compatible: "zmk,behavior-macro"
include: zero_param.yaml
properties:
bindings:
type: phandle-array
required: true
wait-ms:
type: int
description: The default time to wait (in milliseconds) before triggering the next behavior in the macro bindings list.
tap-ms:
type: int
description: The default time to wait (in milliseconds) between the press and release events on a tapped macro behavior binding
include: [zero_param.yaml, macro_base.yaml]

View file

@ -0,0 +1,8 @@
# Copyright (c) 2023 The ZMK Contributors
# SPDX-License-Identifier: MIT
description: Macro Parameter One Substituted Into Next Binding's First Parameter
compatible: "zmk,macro-param-1to1"
include: zero_param.yaml

View file

@ -0,0 +1,8 @@
# Copyright (c) 2023 The ZMK Contributors
# SPDX-License-Identifier: MIT
description: Macro Parameter One Substituted Into Next Binding's Second Parameter
compatible: "zmk,macro-param-1to2"
include: zero_param.yaml

View file

@ -0,0 +1,8 @@
# Copyright (c) 2023 The ZMK Contributors
# SPDX-License-Identifier: MIT
description: Macro Parameter Two Substituted Into Next Binding's First Parameter
compatible: "zmk,macro-param-2to1"
include: zero_param.yaml

View file

@ -0,0 +1,8 @@
# Copyright (c) 2023 The ZMK Contributors
# SPDX-License-Identifier: MIT
description: Macro Parameter Two Substituted Into Next Binding's Second Parameter
compatible: "zmk,macro-param-2to2"
include: zero_param.yaml

View file

@ -4,8 +4,6 @@
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_behavior_macro
#include <zephyr/device.h>
#include <drivers/behavior.h>
#include <zephyr/logging/log.h>
@ -15,20 +13,22 @@
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
enum behavior_macro_mode {
MACRO_MODE_TAP,
MACRO_MODE_PRESS,
MACRO_MODE_RELEASE,
};
enum param_source { PARAM_SOURCE_BINDING, PARAM_SOURCE_MACRO_1ST, PARAM_SOURCE_MACRO_2ND };
struct behavior_macro_trigger_state {
uint32_t wait_ms;
uint32_t tap_ms;
enum behavior_macro_mode mode;
uint16_t start_index;
uint16_t count;
enum param_source param1_source;
enum param_source param2_source;
};
struct behavior_macro_state {
@ -52,6 +52,11 @@ struct behavior_macro_config {
#define WAIT_TIME DT_PROP(DT_INST(0, zmk_macro_control_wait_time), label)
#define WAIT_REL DT_PROP(DT_INST(0, zmk_macro_pause_for_release), label)
#define P1TO1 DT_PROP(DT_INST(0, zmk_macro_param_1to1), label)
#define P1TO2 DT_PROP(DT_INST(0, zmk_macro_param_1to2), label)
#define P2TO1 DT_PROP(DT_INST(0, zmk_macro_param_2to1), label)
#define P2TO2 DT_PROP(DT_INST(0, zmk_macro_param_2to2), label)
#define ZM_IS_NODE_MATCH(a, b) (strcmp(a, b) == 0)
#define IS_TAP_MODE(dev) ZM_IS_NODE_MATCH(dev, TAP_MODE)
#define IS_PRESS_MODE(dev) ZM_IS_NODE_MATCH(dev, PRESS_MODE)
@ -61,6 +66,11 @@ struct behavior_macro_config {
#define IS_WAIT_TIME(dev) ZM_IS_NODE_MATCH(dev, WAIT_TIME)
#define IS_PAUSE(dev) ZM_IS_NODE_MATCH(dev, WAIT_REL)
#define IS_P1TO1(dev) ZM_IS_NODE_MATCH(dev, P1TO1)
#define IS_P1TO2(dev) ZM_IS_NODE_MATCH(dev, P1TO2)
#define IS_P2TO1(dev) ZM_IS_NODE_MATCH(dev, P2TO1)
#define IS_P2TO2(dev) ZM_IS_NODE_MATCH(dev, P2TO2)
static bool handle_control_binding(struct behavior_macro_trigger_state *state,
const struct zmk_behavior_binding *binding) {
if (IS_TAP_MODE(binding->behavior_dev)) {
@ -78,6 +88,18 @@ static bool handle_control_binding(struct behavior_macro_trigger_state *state,
} else if (IS_WAIT_TIME(binding->behavior_dev)) {
state->wait_ms = binding->param1;
LOG_DBG("macro wait time set: %d", state->wait_ms);
} else if (IS_P1TO1(binding->behavior_dev)) {
state->param1_source = PARAM_SOURCE_MACRO_1ST;
LOG_DBG("macro param: 1to1");
} else if (IS_P1TO2(binding->behavior_dev)) {
state->param2_source = PARAM_SOURCE_MACRO_1ST;
LOG_DBG("macro param: 1to2");
} else if (IS_P2TO1(binding->behavior_dev)) {
state->param1_source = PARAM_SOURCE_MACRO_2ND;
LOG_DBG("macro param: 2to1");
} else if (IS_P2TO2(binding->behavior_dev)) {
state->param2_source = PARAM_SOURCE_MACRO_2ND;
LOG_DBG("macro param: 2to2");
} else {
return false;
}
@ -110,21 +132,47 @@ static int behavior_macro_init(const struct device *dev) {
return 0;
};
static uint32_t select_param(enum param_source param_source, uint32_t source_binding,
const struct zmk_behavior_binding *macro_binding) {
switch (param_source) {
case PARAM_SOURCE_MACRO_1ST:
return macro_binding->param1;
case PARAM_SOURCE_MACRO_2ND:
return macro_binding->param2;
default:
return source_binding;
}
};
static void replace_params(struct behavior_macro_trigger_state *state,
struct zmk_behavior_binding *binding,
const struct zmk_behavior_binding *macro_binding) {
binding->param1 = select_param(state->param1_source, binding->param1, macro_binding);
binding->param2 = select_param(state->param2_source, binding->param2, macro_binding);
state->param1_source = PARAM_SOURCE_BINDING;
state->param2_source = PARAM_SOURCE_BINDING;
}
static void queue_macro(uint32_t position, const struct zmk_behavior_binding bindings[],
struct behavior_macro_trigger_state state) {
struct behavior_macro_trigger_state state,
const struct zmk_behavior_binding *macro_binding) {
LOG_DBG("Iterating macro bindings - starting: %d, count: %d", state.start_index, state.count);
for (int i = state.start_index; i < state.start_index + state.count; i++) {
if (!handle_control_binding(&state, &bindings[i])) {
struct zmk_behavior_binding binding = bindings[i];
replace_params(&state, &binding, macro_binding);
switch (state.mode) {
case MACRO_MODE_TAP:
zmk_behavior_queue_add(position, bindings[i], true, state.tap_ms);
zmk_behavior_queue_add(position, bindings[i], false, state.wait_ms);
zmk_behavior_queue_add(position, binding, true, state.tap_ms);
zmk_behavior_queue_add(position, binding, false, state.wait_ms);
break;
case MACRO_MODE_PRESS:
zmk_behavior_queue_add(position, bindings[i], true, state.wait_ms);
zmk_behavior_queue_add(position, binding, true, state.wait_ms);
break;
case MACRO_MODE_RELEASE:
zmk_behavior_queue_add(position, bindings[i], false, state.wait_ms);
zmk_behavior_queue_add(position, binding, false, state.wait_ms);
break;
default:
LOG_ERR("Unknown macro mode: %d", state.mode);
@ -145,7 +193,7 @@ static int on_macro_binding_pressed(struct zmk_behavior_binding *binding,
.start_index = 0,
.count = state->press_bindings_count};
queue_macro(event.position, cfg->bindings, trigger_state);
queue_macro(event.position, cfg->bindings, trigger_state, binding);
return ZMK_BEHAVIOR_OPAQUE;
}
@ -156,7 +204,7 @@ static int on_macro_binding_released(struct zmk_behavior_binding *binding,
const struct behavior_macro_config *cfg = dev->config;
struct behavior_macro_state *state = dev->data;
queue_macro(event.position, cfg->bindings, state->release_state);
queue_macro(event.position, cfg->bindings, state->release_state, binding);
return ZMK_BEHAVIOR_OPAQUE;
}
@ -166,22 +214,20 @@ static const struct behavior_driver_api behavior_macro_driver_api = {
.binding_released = on_macro_binding_released,
};
#define BINDING_WITH_COMMA(idx, drv_inst) ZMK_KEYMAP_EXTRACT_BINDING(idx, DT_DRV_INST(drv_inst))
#define TRANSFORMED_BEHAVIORS(n) \
{LISTIFY(DT_PROP_LEN(DT_DRV_INST(n), bindings), BINDING_WITH_COMMA, (, ), n)},
{LISTIFY(DT_PROP_LEN(n, bindings), ZMK_KEYMAP_EXTRACT_BINDING, (, ), n)},
#define MACRO_INST(n) \
static struct behavior_macro_state behavior_macro_state_##n = {}; \
static struct behavior_macro_config behavior_macro_config_##n = { \
.default_wait_ms = DT_INST_PROP_OR(n, wait_ms, CONFIG_ZMK_MACRO_DEFAULT_WAIT_MS), \
.default_tap_ms = DT_INST_PROP_OR(n, tap_ms, CONFIG_ZMK_MACRO_DEFAULT_TAP_MS), \
.count = DT_INST_PROP_LEN(n, bindings), \
.bindings = TRANSFORMED_BEHAVIORS(n)}; \
DEVICE_DT_INST_DEFINE(n, behavior_macro_init, NULL, &behavior_macro_state_##n, \
&behavior_macro_config_##n, APPLICATION, \
CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_macro_driver_api);
#define MACRO_INST(inst) \
static struct behavior_macro_state behavior_macro_state_##inst = {}; \
static struct behavior_macro_config behavior_macro_config_##inst = { \
.default_wait_ms = DT_PROP_OR(inst, wait_ms, CONFIG_ZMK_MACRO_DEFAULT_WAIT_MS), \
.default_tap_ms = DT_PROP_OR(inst, tap_ms, CONFIG_ZMK_MACRO_DEFAULT_TAP_MS), \
.count = DT_PROP_LEN(inst, bindings), \
.bindings = TRANSFORMED_BEHAVIORS(inst)}; \
DEVICE_DT_DEFINE(inst, behavior_macro_init, NULL, &behavior_macro_state_##inst, \
&behavior_macro_config_##inst, APPLICATION, \
CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_macro_driver_api);
DT_INST_FOREACH_STATUS_OKAY(MACRO_INST)
#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */
DT_FOREACH_STATUS_OKAY(zmk_behavior_macro, MACRO_INST)
DT_FOREACH_STATUS_OKAY(zmk_behavior_macro_one_param, MACRO_INST)
DT_FOREACH_STATUS_OKAY(zmk_behavior_macro_two_param, MACRO_INST)

View file

@ -0,0 +1 @@
s/.*hid_listener_keycode/kp/p

View file

@ -0,0 +1,16 @@
kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
kp_pressed: usage_page 0x07 keycode 0x38 implicit_mods 0x00 explicit_mods 0x00
kp_released: usage_page 0x07 keycode 0x38 implicit_mods 0x00 explicit_mods 0x00
kp_pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00
kp_released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00
kp_pressed: usage_page 0x07 keycode 0x34 implicit_mods 0x00 explicit_mods 0x00
kp_released: usage_page 0x07 keycode 0x34 implicit_mods 0x00 explicit_mods 0x00
kp_pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00
kp_released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00
kp_pressed: usage_page 0x07 keycode 0x34 implicit_mods 0x00 explicit_mods 0x00
kp_released: usage_page 0x07 keycode 0x34 implicit_mods 0x00 explicit_mods 0x00
kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00
kp_released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00
kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2022 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan_mock.h>
/ {
macros {
slash_macro: slash_macro {
#binding-cells = <2>;
label = "ZM_SLASH";
compatible = "zmk,behavior-macro-two-param";
wait-ms = <40>;
tap-ms = <40>;
bindings = <
&macro_param_1to1 &kp MACRO_PLACEHOLDER
&kp SLASH
&macro_param_2to1 &kp MACRO_PLACEHOLDER>;
};
to_second_macro: to_second_macro {
#binding-cells = <2>;
label = "ZMK_TO_SECOND";
compatible = "zmk,behavior-macro-two-param";
wait-ms = <40>;
tap-ms = <40>;
bindings = <
&macro_param_1to2 &mt LSHIFT MACRO_PLACEHOLDER
&macro_param_2to2 &mt RSHIFT MACRO_PLACEHOLDER>;
};
quote_letter_macro: quote_letter_macro {
#binding-cells = <1>;
label = "ZMK_QLET";
compatible = "zmk,behavior-macro-one-param";
wait-ms = <40>;
tap-ms = <40>;
bindings = <
&kp QUOT
&macro_param_1to1 &kp MACRO_PLACEHOLDER
&kp QUOT>;
};
};
keymap {
compatible = "zmk,keymap";
label = "Default keymap";
default_layer {
bindings = <
&slash_macro A B &quote_letter_macro B
&to_second_macro E F &kp C>;
};
};
};
&kscan {
events = <ZMK_MOCK_PRESS(0,0,20) ZMK_MOCK_PRESS(0,1,10) ZMK_MOCK_RELEASE(0,1,10) ZMK_MOCK_RELEASE(0,0,10) ZMK_MOCK_PRESS(1,0,10) ZMK_MOCK_RELEASE(1,0,1000)>;
};

View file

@ -49,6 +49,22 @@ For use cases involving sending a single keycode with modifiers, for instance ct
with [modifier functions](../codes/modifiers.mdx#modifier-functions) can be used instead of a macro.
:::
### Parameterized Macros
Macros can also be "parameterized", allowing them to be bound in your keymap with unique values passed into them, e.g.:
```
raise_layer {
bindings = <&my_cool_macro A>
};
```
When defining a parameterized macro, a different `compatible` value will be used depending on how many parameters are passed into it:
- `zmk,behavior-macro` - a parameter that takes no parameters.
- `zmk,behavior-macro-one-param` - a parameter that takes one parameter when used.
- `zmk,behavior-macro-two-param` - a parameter that takes two parameters when used.
### Bindings
Like [hold-taps](/docs/behaviors/hold-tap), macros are created by composing other behaviors, and any of those behaviors can
@ -67,6 +83,30 @@ bindings
There are a set of special macro controls that can be included in the `bindings` list to modify the
way the macro is processed.
### Parameters
When creating a macro that takes parameter(s), there are macro controls that change when the parameters passed to the macro are used
within the macro itself. All of the controls are "one shot" and will change how the passed in parameters are used for the very next non-macro control behavior in the `bindings` list of the macro.
For example, to pass the first parameter from the macro into a `&kp` used in the macro, you would use:
```
bindings
= <&macro_param_1to1>
, <&kp MACRO_PLACEHOLDER>
;
```
Because `kp` takes one parameter, you can't simply make the second entry `<&kp>` in the `bindings` list. Whatever value you do pass in will be replaced when the macro is triggered, so you can put _any_ value there, e.g. `0`, `A` keycode, etc. To make it very obvious that the parameter there is not actually going to be used, you can use `MACRO_PLACEHOLDER` which is simply an alias for `0`.
The available parameter controls are:
- `&macro_param_1to1` - pass the first parameter of the macro into the first parameter of the next behavior in the `bindings` list.
- `&macro_param_1to2` - pass the first parameter of the macro into the second parameter of the next behavior in the `bindings` list.
* `&macro_param_2to1` - pass the second parameter of the macro into the first parameter of the next behavior in the `bindings` list.
* `&macro_param_2to2` - pass the second parameter of the macro into the second parameter of the next behavior in the `bindings` list.
### Binding Activation Mode
Bindings in a macro are activated differently, depending on the current "activation mode" of the macro.