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

feat(behaviors): Support parameterized macros. #1232

Merged

Conversation

petejohanson
Copy link
Contributor

@petejohanson petejohanson commented Apr 8, 2022

* 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.

Ok, draft for now until I can create updated docs, but hoping for some feedback on this idea. We use more marker bindings to trigger different behavior for the next binding entry, e.g. &macro_param_1to1.

So a new macro that surround a given letter with quotes:

		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>;
		};

Could then be used in your keymap like &quote_letter A.

Thoughts?

@petejohanson petejohanson added enhancement New feature or request behaviors labels Apr 8, 2022
@petejohanson petejohanson self-assigned this Apr 8, 2022
@joelspadin
Copy link
Collaborator

The most likely scenario I can think where this could cause an issue is if a behavior packs multiple values into one parameter such that 0xFFFF is a valid and common value, for example RGBW(255, 255, 255, 255) for a 4-channel LED color.

@joelspadin
Copy link
Collaborator

Copying over my suggestion from Discord so it doesn't get lost:

What if instead of having a special parameter value that gets replaced, we add a macro-specific behavior that modifies the following binding? For example, we could have something like

  • &macro_param1_1st replace 1st param with macro param 1
  • &macro_param1_2nd replace 2nd param with macro param 1
  • &macro_param2_1st replace 1st param with macro param 2
  • &macro_param2_2nd replace 2nd param with macro param 2
  • &macro_param1 alias for &macro_param1_1st (for bindings that only take one param)
  • &macro_param2 alias for &macro_param2_1st

@petejohanson
Copy link
Contributor Author

I dig this idea. Implementation seemed simple enough. I opted for &macro_param_1to1, &macro_param_2to1 but I'm really open to some feedback/ideas on better naming on how to handle this.

I also left a define MACRO_PLACEHOLDER in, that could be used just for clarity in the bindings for a macro, but you could just as easily just hardcode 0 there if you wanted.

From the test:

		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>;
		};

Thoughts?

@petejohanson petejohanson marked this pull request as ready for review March 23, 2023 04:16
@petejohanson
Copy link
Contributor Author

@joelspadin Rebased onto the new Zephyr 3.2 main, still ready for review. TIA.

@kvietcong
Copy link

Used to make a dynamic CTRL+KEY macro for easy use with a tap_hold key.

No weird behaviors as everything seems to be working just fine, allowing me to hold keys like V (&tct V V) to get a CTRL+V tap (not hold like with a regular hold_tap). With some documentation, I think the feature would be really easy to use for anyone. I also needed to make sure to #include <dt-bindings/zmk/macro.h>, which wasn't mentioned in the PR yet.

Actual Configuration
behaviors: {
    tct: tap_ctrl_tap {
        compatible = "zmk,behavior-hold-tap";
        label = "TAP_CTRL_TAP";
        #binding-cells = <2>;
        tapping_term_ms = <300>;
        flavor = "tap-preferred";
        bindings = <&ctrl_kp>, <&kp>;
    };
};

macros {
    ctrl_kp: macro_ctrl_kp {
        #binding-cells = <1>;
        label = "MACRO_CTRL_KP";
        compatible = "zmk,behavior-macro-one-param";
        bindings
            = <&macro_press &kp LCTL>
            , <&macro_param_1to1 &macro_tap &kp MACRO_PLACEHOLDER>
            , <&macro_release &kp LCTL>;
    };
};

Copy link
Collaborator

@joelspadin joelspadin left a comment

Choose a reason for hiding this comment

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

Needs docs updates, but the implementation looks good to me.


LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);

#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
#if DT_HAS_COMPAT_STATUS_OKAY(zmk_behavior_macro) || \
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could this be simplified using Kconfig + CMake, or would you rather do that in a separate commit?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it can be! Rebased onto main where we have the Kconfig.behaviors and tweaked this to use the DT_HAS defines and simplify the code by only compiling if we have any of the macro DT compatibles enabled.

Comment on lines 143 to 163
switch (state->param1_source) {
case PARAM_SOURCE_MACRO_1ST:
binding->param1 = macro_binding->param1;
break;
case PARAM_SOURCE_MACRO_2ND:
binding->param1 = macro_binding->param2;
break;
default:
break;
}

switch (state->param2_source) {
case PARAM_SOURCE_MACRO_1ST:
binding->param2 = macro_binding->param1;
break;
case PARAM_SOURCE_MACRO_2ND:
binding->param2 = macro_binding->param2;
break;
default:
break;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This could be deduplicated with something like

    update_param(&binding->param1, state->param1_source, &macro_binding);
    update_param(&binding->param2, state->param2_source, &macro_binding);

Or more functional programming style:

    binding->param1 = update_param(state->param1_source, binding->param1, &macro_binding);
    binding->param2 = update_param(state->param2_source, binding->param2, &macro_binding);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Used the more functional style.

@petejohanson petejohanson force-pushed the behaviors/macro-parameters branch 4 times, most recently from a48ffa9 to 8a0c9b5 Compare May 14, 2023 21:29
Comment on lines 217 to 229
#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), \
.default_wait_ms = DT_PROP_OR(n, wait_ms, CONFIG_ZMK_MACRO_DEFAULT_WAIT_MS), \
.default_tap_ms = DT_PROP_OR(n, tap_ms, CONFIG_ZMK_MACRO_DEFAULT_TAP_MS), \
.count = DT_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);

DT_INST_FOREACH_STATUS_OKAY(MACRO_INST)
DEVICE_DT_DEFINE(n, behavior_macro_init, NULL, &behavior_macro_state_##n, \
&behavior_macro_config_##n, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \
&behavior_macro_driver_api);
Copy link
Collaborator

Choose a reason for hiding this comment

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

n is now a node ID instead of an integer. Should it be renamed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed.

```

:::note
The `MACRO_PLACEHOLDER` is defined as a convenient way to avoid just putting `0` in that location. It does _not_ control what parameters are substituted; only the macro controls do that.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might be useful to first explain that you still need to put something in for every binding, even if it will just get overwritten due to &macro_param_XtoY. Otherwise, it might be confusing why you would need to put 0 there, and therefore why you might use this instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, good call. I expanded on this and pulled it out of the note aside it had been hiding in.

### Parameters

When creating a macro that takes parameter(s), there are control 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 behavior in the `bindings` list of the macro.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might be useful to clarify that it applies to the next non-macro control behavior, so &macro_param1to1 &macro_param2to2 @macro_press &foo 0 0 is a valid thing that works.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tweaked the wording here.

@@ -62,6 +78,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 control when the parameters passed to the macro are used
Copy link
Contributor

Choose a reason for hiding this comment

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

This sentence (specifically "there are control when the parameters [...]") seems to be broken, I couldn't tell the intention.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was... incomprehensible.... Remind me not to write docs late at night when I'm tired. Reworded, to hopefully actually make sense now. Thanks!

docs/docs/behaviors/macros.md Outdated Show resolved Hide resolved
docs/docs/behaviors/macros.md Outdated Show resolved Hide resolved
Copy link
Contributor

@caksoylar caksoylar left a comment

Choose a reason for hiding this comment

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

Docs LGTM, noted a couple things in the logging and about the tests.

app/src/behaviors/behavior_macro.c Outdated Show resolved Hide resolved
app/src/behaviors/behavior_macro.c Outdated Show resolved Hide resolved
app/src/behaviors/behavior_macro.c Outdated Show resolved Hide resolved
@petejohanson petejohanson requested a review from a team as a code owner June 20, 2023 16:35
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
behaviors enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants