From df26967cc52d301f790e26a966414fa2f1fd974e Mon Sep 17 00:00:00 2001 From: Theo Lemay Date: Mon, 22 May 2023 15:26:55 -0700 Subject: [PATCH 1/5] feat: add partial combo holds --- app/Kconfig | 4 + app/dts/behaviors.dtsi | 3 +- app/dts/behaviors/combos.dtsi | 15 +++ .../zmk,combo-partial-hold-position.yaml | 8 ++ app/src/combo.c | 105 ++++++++++++++---- docs/docs/config/combos.md | 25 +++-- docs/docs/development/new-behavior.md | 1 + docs/docs/features/combos.md | 67 ++++++++++- 8 files changed, 191 insertions(+), 37 deletions(-) create mode 100644 app/dts/behaviors/combos.dtsi create mode 100644 app/dts/bindings/zmk,combo-partial-hold-position.yaml diff --git a/app/Kconfig b/app/Kconfig index d1b6682f..b2a936d0 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -346,6 +346,10 @@ config ZMK_COMBO_MAX_KEYS_PER_COMBO int "Maximum number of keys per combo" default 4 +config ZMK_COMBO_MAX_LAYERS_PER_COMBO + int "Maximum number of layers specified per combo" + default 4 + #Combo options endmenu diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index b3502cbb..a3497816 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -18,4 +18,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include \ No newline at end of file diff --git a/app/dts/behaviors/combos.dtsi b/app/dts/behaviors/combos.dtsi new file mode 100644 index 00000000..dc25b447 --- /dev/null +++ b/app/dts/behaviors/combos.dtsi @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + + / { + behaviors { + partial_hold_position: combo_partial_hold_position { + compatible = "zmk,combo-partial-hold-position"; + label = "PARTIAL_HOLD_POSITION"; + #binding-cells = <1>; + }; + }; +}; diff --git a/app/dts/bindings/zmk,combo-partial-hold-position.yaml b/app/dts/bindings/zmk,combo-partial-hold-position.yaml new file mode 100644 index 00000000..ae64c2f9 --- /dev/null +++ b/app/dts/bindings/zmk,combo-partial-hold-position.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2022 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Add a partial hold position to a combo + +compatible: "zmk,combo-partial-hold-position" + +include: one_param.yaml \ No newline at end of file diff --git a/app/src/combo.c b/app/src/combo.c index 90c89c15..31ceec40 100644 --- a/app/src/combo.c +++ b/app/src/combo.c @@ -28,16 +28,17 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); struct combo_cfg { int32_t key_positions[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO]; int32_t key_position_len; - struct zmk_behavior_binding behavior; int32_t timeout_ms; // if slow release is set, the combo releases when the last key is released. // otherwise, the combo releases when the first key is released. bool slow_release; + int8_t layers[CONFIG_ZMK_COMBO_MAX_LAYERS_PER_COMBO]; + int32_t layers_len; // the virtual key position is a key position outside the range used by the keyboard. // it is necessary so hold-taps can uniquely identify a behavior. int32_t virtual_key_position; - int32_t layers_len; - int8_t layers[]; + int32_t behaviors_len; + struct zmk_behavior_binding behaviors[]; }; struct active_combo { @@ -266,22 +267,32 @@ static int release_pressed_keys() { return CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO; } -static inline int press_combo_behavior(struct combo_cfg *combo, int32_t timestamp) { +static inline int process_behavior(int32_t position, int32_t timestamp, + struct zmk_behavior_binding *behavior, bool pressed) { struct zmk_behavior_binding_event event = { - .position = combo->virtual_key_position, + .position = position, .timestamp = timestamp, }; - return behavior_keymap_binding_pressed(&combo->behavior, event); + if (pressed) { + return behavior_keymap_binding_pressed(behavior, event); + } else { + return behavior_keymap_binding_released(behavior, event); + } } -static inline int release_combo_behavior(struct combo_cfg *combo, int32_t timestamp) { - struct zmk_behavior_binding_event event = { - .position = combo->virtual_key_position, - .timestamp = timestamp, - }; +static inline int process_combo_behavior(struct combo_cfg *combo, int32_t timestamp, bool pressed) { + return process_behavior(combo->virtual_key_position, timestamp, &combo->behaviors[0], pressed); +} - return behavior_keymap_binding_released(&combo->behavior, event); +// press or release the behavior at the given position, used for partial holds +static inline int process_position(int32_t position, int32_t timestamp, const zmk_event_t *ev, + bool pressed) { + const struct zmk_position_state_changed *pos_ev; + if ((pos_ev = as_zmk_position_state_changed(ev)) != NULL) { + return zmk_keymap_position_state_changed(pos_ev->source, position, pressed, timestamp); + } + return -ENOTSUP; } static void move_pressed_keys_to_active_combo(struct active_combo *active_combo) { @@ -322,8 +333,9 @@ static void activate_combo(struct combo_cfg *combo) { return; } move_pressed_keys_to_active_combo(active_combo); - press_combo_behavior( - combo, as_zmk_position_state_changed(active_combo->key_positions_pressed[0])->timestamp); + process_combo_behavior( + combo, as_zmk_position_state_changed(active_combo->key_positions_pressed[0])->timestamp, + true); } static void deactivate_combo(int active_combo_index) { @@ -336,8 +348,12 @@ static void deactivate_combo(int active_combo_index) { active_combos[active_combo_count] = (struct active_combo){0}; } +#define ZM_IS_NODE_MATCH(a, b) (strcmp(a, b) == 0) +#define PARTIAL_HOLD_POSITION DT_PROP(DT_INST(0, zmk_combo_partial_hold_position), label) +#define IS_PARTIAL_HOLD_POSITION(dev) ZM_IS_NODE_MATCH(dev, PARTIAL_HOLD_POSITION) + /* returns true if a key was released. */ -static bool release_combo_key(int32_t position, int64_t timestamp) { +static bool release_combo_key(int32_t position, int64_t timestamp, const zmk_event_t *ev) { for (int combo_idx = 0; combo_idx < active_combo_count; combo_idx++) { struct active_combo *active_combo = &active_combos[combo_idx]; @@ -358,10 +374,51 @@ static bool release_combo_key(int32_t position, int64_t timestamp) { } if (key_released) { - if ((active_combo->combo->slow_release && all_keys_released) || - (!active_combo->combo->slow_release && all_keys_pressed)) { - release_combo_behavior(active_combo->combo, timestamp); + + // partial holds + for (int i = 1; i < active_combo->combo->behaviors_len; i++) { + // loop through every behavior except the first. If the behavior is a partial hold + // position, process it. If the next behavior is also a behavior, press that + // behavior instead of the partial hold position. + struct zmk_behavior_binding *binding = &active_combo->combo->behaviors[i]; + // if the behavior is not a partial hold position, skip it + if (!IS_PARTIAL_HOLD_POSITION(binding->behavior_dev)) { + continue; + } + int32_t partial_hold_position = binding->param1; + // if the next behavior is a behavior (not a partial hold position) + bool has_explicit_behavior = + i == active_combo->combo->behaviors_len - 1 + ? false + : !IS_PARTIAL_HOLD_POSITION( + active_combo->combo->behaviors[i + 1].behavior_dev); + + // if all keys are pressed, press all other partial hold keys that are still held + if (all_keys_pressed) { + // except the one that was released + if (partial_hold_position != position) { + // either press the position, or press the behavior + if (has_explicit_behavior) { + process_behavior(position, timestamp, + &active_combo->combo->behaviors[i + 1], true); + } else { + process_position(partial_hold_position, timestamp, ev, true); + } + } + } else { + // now release any partial hold keys that are released + if (partial_hold_position == position) { + // either release the position, or release the behavior + if (has_explicit_behavior) { + process_behavior(position, timestamp, + &active_combo->combo->behaviors[i + 1], false); + } else { + process_position(partial_hold_position, timestamp, ev, false); + } + } + } } + if (all_keys_released) { deactivate_combo(combo_idx); } @@ -432,7 +489,7 @@ static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_ static int position_state_up(const zmk_event_t *ev, struct zmk_position_state_changed *data) { int released_keys = cleanup(); - if (release_combo_key(data->position, data->timestamp)) { + if (release_combo_key(data->position, data->timestamp, ev)) { return ZMK_EV_EVENT_HANDLED; } if (released_keys > 1) { @@ -471,16 +528,22 @@ static int position_state_changed_listener(const zmk_event_t *ev) { ZMK_LISTENER(combo, position_state_changed_listener); ZMK_SUBSCRIPTION(combo, zmk_position_state_changed); +#define BINDING_WITH_COMMA(idx, drv_inst) ZMK_KEYMAP_EXTRACT_BINDING(idx, drv_inst) + +#define TRANSFORMED_BEHAVIORS(n) \ + { LISTIFY(DT_PROP_LEN(n, bindings), BINDING_WITH_COMMA, (, ), n) } + #define COMBO_INST(n) \ static struct combo_cfg combo_config_##n = { \ .timeout_ms = DT_PROP(n, timeout_ms), \ .key_positions = DT_PROP(n, key_positions), \ .key_position_len = DT_PROP_LEN(n, key_positions), \ - .behavior = ZMK_KEYMAP_EXTRACT_BINDING(0, n), \ - .virtual_key_position = ZMK_VIRTUAL_KEY_POSITION_COMBO(__COUNTER__), \ .slow_release = DT_PROP(n, slow_release), \ .layers = DT_PROP(n, layers), \ .layers_len = DT_PROP_LEN(n, layers), \ + .virtual_key_position = ZMK_VIRTUAL_KEY_POSITION_COMBO(__COUNTER__), \ + .behaviors = TRANSFORMED_BEHAVIORS(n), \ + .behaviors_len = DT_PROP_LEN(n, bindings), \ }; #define INITIALIZE_COMBO(n) initialize_combo(&combo_config_##n); diff --git a/docs/docs/config/combos.md b/docs/docs/config/combos.md index cd351125..b0b93b22 100644 --- a/docs/docs/config/combos.md +++ b/docs/docs/config/combos.md @@ -11,11 +11,12 @@ See [Configuration Overview](index.md) for instructions on how to change these s Definition file: [zmk/app/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/Kconfig) -| Config | Type | Description | Default | -| ------------------------------------- | ---- | -------------------------------------------------------------- | ------- | -| `CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS` | int | Maximum number of combos that can be active at the same time | 4 | -| `CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY` | int | Maximum number of active combos that use the same key position | 5 | -| `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` | int | Maximum number of keys to press to activate a combo | 4 | +| Config | Type | Description | Default | +| --------------------------------------- | ---- | -------------------------------------------------------------- | ------- | +| `CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS` | int | Maximum number of combos that can be active at the same time | 4 | +| `CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY` | int | Maximum number of active combos that use the same key position | 5 | +| `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` | int | Maximum number of keys to press to activate a combo | 4 | +| `CONFIG_ZMK_COMBO_MAX_LAYERS_PER_COMBO` | int | Maximum number of layers specified per combo | 4 | If `CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY` is 5, you can have 5 separate combos that use position `0`, 5 combos that use position `1`, and so on. @@ -31,12 +32,12 @@ The `zmk,combos` node itself has no properties. It should have one child node pe Each child node can have the following properties: -| Property | Type | Description | Default | -| --------------- | ------------- | ----------------------------------------------------------------------------------------------------- | ------- | -| `bindings` | phandle-array | A [behavior](../features/keymaps.md#behaviors) to run when the combo is triggered | | -| `key-positions` | array | A list of key position indices for the keys which should trigger the combo | | -| `timeout-ms` | int | All the keys in `key-positions` must be pressed within this time in milliseconds to trigger the combo | 50 | -| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | -| `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` | +| Property | Type | Description | Default | +| ------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | +| `bindings` | phandle-array | A [behavior](../features/keymaps.md#behaviors) to run when the combo is triggered, along with optional `partial-hold-position` specifiers with associated bindings | | +| `key-positions` | array | A list of key position indices for the keys which should trigger the combo | | +| `timeout-ms` | int | All the keys in `key-positions` must be pressed within this time in milliseconds to trigger the combo | 50 | +| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | +| `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` | The `key-positions` array must not be longer than the `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` setting, which defaults to 4. If you want a combo that triggers when pressing 5 keys, then you must change the setting to 5. diff --git a/docs/docs/development/new-behavior.md b/docs/docs/development/new-behavior.md index 0d70aa3b..16fa50aa 100644 --- a/docs/docs/development/new-behavior.md +++ b/docs/docs/development/new-behavior.md @@ -404,6 +404,7 @@ After creating the `.dtsi` from above, update `app/dts/behaviors.dtsi` to includ #include #include #include +#include // highlight-next-line #include ``` diff --git a/docs/docs/features/combos.md b/docs/docs/features/combos.md index 44313cc1..f266be0f 100644 --- a/docs/docs/features/combos.md +++ b/docs/docs/features/combos.md @@ -6,7 +6,7 @@ title: Combos Combo keys are a way to combine multiple keypresses to output a different key. For example, you can hit the Q and W keys on your keyboard to output escape. -### Configuration +## Configuration Combos configured in your `.keymap` file, but are separate from the `keymap` node found there, since they are processed before the normal keymap. They are specified like this: @@ -29,7 +29,8 @@ Combos configured in your `.keymap` file, but are separate from the `keymap` nod - `key-positions` is an array of key positions. See the info section below about how to figure out the positions on your board. - `layers = <0 1...>` will allow limiting a combo to specific layers. This is an _optional_ parameter, when omitted it defaults to global scope. - `bindings` is the behavior that is activated when the behavior is pressed. -- (advanced) you can specify `slow-release` if you want the combo binding to be released when all key-positions are released. The default is to release the combo as soon as any of the keys in the combo is released. +- (advanced) you can specify `slow-release` if you want the combo binding to be released when all key-positions are released. The default is to release the combo as soon as any of the keys in the combo is released. You can also specify `slow-release-positions` to select which key-positions _must_ be held to maintain the combo. +- (advanced) you can add `partial-hold-position` to the `bindings` array, optionally along with additional behaviors, to control what happens when a combo is partially released. :::info @@ -37,7 +38,7 @@ Key positions are numbered like the keys in your keymap, starting at 0. So, if t ::: -### Advanced usage +## Advanced usage - Partially overlapping combos like `0 1` and `0 2` are supported. - Fully overlapping combos like `0 1` and `0 1 2` are supported. @@ -48,3 +49,63 @@ Invoking a source-specific behavior such as one of the [reset behaviors](behavio ::: See [combo configuration](/docs/config/combos) for advanced configuration options. + +### Partial Holds + +After pressing a combo, you may want to specify the behavior that is activated when the combo is partially released. For example, if the keys corresponding to the combo positions 0, 1, and 2 are `&tog NAV`, `&kp A`, and `&kp LSFT` and the combo behavior is `&kp LEFT`, you may want to activate `&mo NAV` when you release `A` or `LSFT` but continue to hold `NAV`, or activate `LSFT` when you release `NAV` or `A` but continue to hold `LSFT`. + +To do this, you can add `partial-hold-position` to the `bindings` array, optionally along with associated behaviors. When no explicit behavior is specified, it will default to the behavior belonging to the key position. + +In this example, you would specify: + +``` +combo_nav { + timeout-ms = <50>; + key-positions = <0 1 2>; + bindings + = <&kp LEFT> + , <&partial-hold-position 0 &mo NAV> + , <&partial-hold-position 2> // defaults to &kp LSFT + ; +}; +``` + +### Partial Hold with Slow Release Positions + +Partial holds compliment `slow-release-positions`, by letting you control what happens when a combo is partially released. The best motivating example is a combo that is used to "accelerate" an existing thumb momentary layer with a key on that layer, allowing you to mash the keys together at the same time. The following layout is an example of this: + +``` +#define NAV 1 +/ { + combos { + compatible = "zmk,combos"; + combo_nav { + timeout-ms = <50>; + key-positions = <0 1>; + bindings = <&kp LEFT>, <&partial-hold-position 0>; + slow-release; + slow-release-positions = <1>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + + default_layer { + bindings = < + &mo NAV &kp A &kp B + >; + }; + + nav_layer { + bindings = < + &mo NAV &kp LEFT &kp RIGHT + >; + }; + }; +}; +``` + +In this example, you can press `LEFT` by pressing `NAV`, pressing `LEFT`, then releasing `NAV`. However, this requires a slight pause to ensure `NAV` was pressed before `LEFT`, so that `A` isn't pressed instead. What if you wanted to be able to mash the keys together at the same time, and achieve a consistent result? This is what the combo does — it allows you to press `LEFT` up to 50ms before `NAV` and still activate `LEFT`. + +However, the introduction of the combo means that you can no longer release `LEFT` while holding `NAV` then press `RIGHT`. This is because the combo will be released when `LEFT` is released, and `B` will be pressed instead. To solve this, you can use `partial-hold-position` to specify that `NAV` should be pressed when `LEFT` is released. Finally, you can use `slow-release-positions` to specify that the combo should be held as long as `LEFT` is held, allowing you to use key-repeat while holding `LEFT` but releasing `NAV`. From 016a77af71c913ca7c44dc63e235c04209243871 Mon Sep 17 00:00:00 2001 From: Theo Lemay Date: Mon, 22 May 2023 15:27:25 -0700 Subject: [PATCH 2/5] feat: add slow release positions --- app/dts/bindings/zmk,combos.yaml | 4 ++++ app/src/combo.c | 33 ++++++++++++++++++++++++++++++-- docs/docs/config/combos.md | 1 + docs/docs/features/combos.md | 6 ++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/app/dts/bindings/zmk,combos.yaml b/app/dts/bindings/zmk,combos.yaml index d094b5c4..fa18070b 100644 --- a/app/dts/bindings/zmk,combos.yaml +++ b/app/dts/bindings/zmk,combos.yaml @@ -20,6 +20,10 @@ child-binding: default: 50 slow-release: type: boolean + slow-release-positions: + type: array + required: false + default: [] layers: type: array default: [-1] diff --git a/app/src/combo.c b/app/src/combo.c index 31ceec40..ee2a8cfd 100644 --- a/app/src/combo.c +++ b/app/src/combo.c @@ -29,9 +29,12 @@ struct combo_cfg { int32_t key_positions[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO]; int32_t key_position_len; int32_t timeout_ms; - // if slow release is set, the combo releases when the last key is released. - // otherwise, the combo releases when the first key is released. + // if slow release is set, the combo releases when the last key in slow_release_positions is + // released or all keys are released. otherwise, the combo releases when the first key is + // released. bool slow_release; + int32_t slow_release_positions[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO]; + int32_t slow_release_positions_len; int8_t layers[CONFIG_ZMK_COMBO_MAX_LAYERS_PER_COMBO]; int32_t layers_len; // the virtual key position is a key position outside the range used by the keyboard. @@ -47,6 +50,8 @@ struct active_combo { // The keys are removed from this array when they are released. // Once this array is empty, the behavior is released. const zmk_event_t *key_positions_pressed[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO]; + // keep track if the behavior has already been released (used for slow release) + bool behavior_released; }; struct combo_candidate { @@ -315,6 +320,7 @@ static struct active_combo *store_active_combo(struct combo_cfg *combo) { for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS; i++) { if (active_combos[i].combo == NULL) { active_combos[i].combo = combo; + active_combos[i].behavior_released = false; active_combo_count++; return &active_combos[i]; } @@ -374,6 +380,27 @@ static bool release_combo_key(int32_t position, int64_t timestamp, const zmk_eve } if (key_released) { + // slow release + if (!active_combo->combo->slow_release && all_keys_pressed) { + // if slow release is not enabled, release the behavior + process_combo_behavior(active_combo->combo, timestamp, false); + } else if (active_combo->combo->slow_release && !active_combo->behavior_released) { + // if slow release is enabled and the behavior has not yet been released + if (all_keys_released) { + // if all keys are released, release the behavior + process_combo_behavior(active_combo->combo, timestamp, false); + active_combo->behavior_released = true; + } else { + // if the key being released is a slow release key, release the behavior, + // otherwise ignore + for (int i = 0; i < active_combo->combo->slow_release_positions_len; i++) { + if (active_combo->combo->slow_release_positions[i] == position) { + process_combo_behavior(active_combo->combo, timestamp, false); + active_combo->behavior_released = true; + } + } + } + } // partial holds for (int i = 1; i < active_combo->combo->behaviors_len; i++) { @@ -539,6 +566,8 @@ ZMK_SUBSCRIPTION(combo, zmk_position_state_changed); .key_positions = DT_PROP(n, key_positions), \ .key_position_len = DT_PROP_LEN(n, key_positions), \ .slow_release = DT_PROP(n, slow_release), \ + .slow_release_positions = DT_PROP(n, slow_release_positions), \ + .slow_release_positions_len = DT_PROP_LEN(n, slow_release_positions), \ .layers = DT_PROP(n, layers), \ .layers_len = DT_PROP_LEN(n, layers), \ .virtual_key_position = ZMK_VIRTUAL_KEY_POSITION_COMBO(__COUNTER__), \ diff --git a/docs/docs/config/combos.md b/docs/docs/config/combos.md index b0b93b22..86809ab1 100644 --- a/docs/docs/config/combos.md +++ b/docs/docs/config/combos.md @@ -38,6 +38,7 @@ Each child node can have the following properties: | `key-positions` | array | A list of key position indices for the keys which should trigger the combo | | | `timeout-ms` | int | All the keys in `key-positions` must be pressed within this time in milliseconds to trigger the combo | 50 | | `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | +| `slow-release-positions` | array | A list of key position indices for the keys that must be held during `slow-release`. If any key in `slow-release-positions` is released, the combo is released. | | | `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` | The `key-positions` array must not be longer than the `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` setting, which defaults to 4. If you want a combo that triggers when pressing 5 keys, then you must change the setting to 5. diff --git a/docs/docs/features/combos.md b/docs/docs/features/combos.md index f266be0f..57e460b5 100644 --- a/docs/docs/features/combos.md +++ b/docs/docs/features/combos.md @@ -50,6 +50,12 @@ Invoking a source-specific behavior such as one of the [reset behaviors](behavio See [combo configuration](/docs/config/combos) for advanced configuration options. +### Slow Release + +If you want the combo binding to be released when all positions are released, instead of when any position is released, enable `slow-release`. This is useful for combos that are used to toggle a layer, for example. + +However, you may want to continue to hold the combo when one position is held but not the other. For example, if the keys corresponding to the combo positions 0 and 1 are `&mo NAV` and `&kp A`, and the combo behavior is `&kp LEFT`, you may want to continue holding `LEFT` while you hold `A` and release `NAV`, but not if you hold `NAV` and release `A`. To solve this, you can specify `slow-release-positions` to select which keys must be held to maintain `slow-release`. In this example, you would specify `slow-release-positions = <1>`. In other words, the combo will be held as long _all_ keys in `slow-release-positions` are held, and released when _any_ key in `slow-release-positions` is released. + ### Partial Holds After pressing a combo, you may want to specify the behavior that is activated when the combo is partially released. For example, if the keys corresponding to the combo positions 0, 1, and 2 are `&tog NAV`, `&kp A`, and `&kp LSFT` and the combo behavior is `&kp LEFT`, you may want to activate `&mo NAV` when you release `A` or `LSFT` but continue to hold `NAV`, or activate `LSFT` when you release `NAV` or `A` but continue to hold `LSFT`. From 3f4aedd24b41fb27533efd02402fd3f509ab46f4 Mon Sep 17 00:00:00 2001 From: Theo Lemay Date: Mon, 22 May 2023 15:43:54 -0700 Subject: [PATCH 3/5] fix: whitespace --- app/Kconfig | 4 ++-- app/dts/behaviors/combos.dtsi | 14 +++++++------- .../bindings/zmk,combo-partial-hold-position.yaml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/Kconfig b/app/Kconfig index b2a936d0..3f2c1f8a 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -347,8 +347,8 @@ config ZMK_COMBO_MAX_KEYS_PER_COMBO default 4 config ZMK_COMBO_MAX_LAYERS_PER_COMBO - int "Maximum number of layers specified per combo" - default 4 + int "Maximum number of layers specified per combo" + default 4 #Combo options endmenu diff --git a/app/dts/behaviors/combos.dtsi b/app/dts/behaviors/combos.dtsi index dc25b447..04f41cd9 100644 --- a/app/dts/behaviors/combos.dtsi +++ b/app/dts/behaviors/combos.dtsi @@ -5,11 +5,11 @@ */ / { - behaviors { - partial_hold_position: combo_partial_hold_position { - compatible = "zmk,combo-partial-hold-position"; - label = "PARTIAL_HOLD_POSITION"; - #binding-cells = <1>; - }; - }; + behaviors { + partial_hold_position: combo_partial_hold_position { + compatible = "zmk,combo-partial-hold-position"; + label = "PARTIAL_HOLD_POSITION"; + #binding-cells = <1>; + }; + }; }; diff --git a/app/dts/bindings/zmk,combo-partial-hold-position.yaml b/app/dts/bindings/zmk,combo-partial-hold-position.yaml index ae64c2f9..9282719e 100644 --- a/app/dts/bindings/zmk,combo-partial-hold-position.yaml +++ b/app/dts/bindings/zmk,combo-partial-hold-position.yaml @@ -5,4 +5,4 @@ description: Add a partial hold position to a combo compatible: "zmk,combo-partial-hold-position" -include: one_param.yaml \ No newline at end of file +include: one_param.yaml From 76fabfca4b2d759e4cb011719012571fa3a84545 Mon Sep 17 00:00:00 2001 From: Theo Lemay Date: Mon, 22 May 2023 15:48:57 -0700 Subject: [PATCH 4/5] fix: tweak docs --- docs/docs/features/combos.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/features/combos.md b/docs/docs/features/combos.md index 57e460b5..0d7df930 100644 --- a/docs/docs/features/combos.md +++ b/docs/docs/features/combos.md @@ -76,7 +76,7 @@ combo_nav { }; ``` -### Partial Hold with Slow Release Positions +### Partial Holds with Slow Release Positions Partial holds compliment `slow-release-positions`, by letting you control what happens when a combo is partially released. The best motivating example is a combo that is used to "accelerate" an existing thumb momentary layer with a key on that layer, allowing you to mash the keys together at the same time. The following layout is an example of this: From 47e7343ae467f7d1eb08cfb2b116adbe9c576991 Mon Sep 17 00:00:00 2001 From: Theo Lemay Date: Mon, 22 May 2023 18:37:10 -0700 Subject: [PATCH 5/5] fix: tweak comments --- app/src/combo.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/combo.c b/app/src/combo.c index ee2a8cfd..1a09f8e8 100644 --- a/app/src/combo.c +++ b/app/src/combo.c @@ -29,9 +29,8 @@ struct combo_cfg { int32_t key_positions[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO]; int32_t key_position_len; int32_t timeout_ms; - // if slow release is set, the combo releases when the last key in slow_release_positions is - // released or all keys are released. otherwise, the combo releases when the first key is - // released. + // if slow release is set, the combo releases when any key in slow_release_positions is released + // or all keys are released. otherwise, the combo releases when the first key is released. bool slow_release; int32_t slow_release_positions[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO]; int32_t slow_release_positions_len; @@ -406,7 +405,7 @@ static bool release_combo_key(int32_t position, int64_t timestamp, const zmk_eve for (int i = 1; i < active_combo->combo->behaviors_len; i++) { // loop through every behavior except the first. If the behavior is a partial hold // position, process it. If the next behavior is also a behavior, press that - // behavior instead of the partial hold position. + // behavior instead of the key at the position. struct zmk_behavior_binding *binding = &active_combo->combo->behaviors[i]; // if the behavior is not a partial hold position, skip it if (!IS_PARTIAL_HOLD_POSITION(binding->behavior_dev)) { @@ -420,7 +419,7 @@ static bool release_combo_key(int32_t position, int64_t timestamp, const zmk_eve : !IS_PARTIAL_HOLD_POSITION( active_combo->combo->behaviors[i + 1].behavior_dev); - // if all keys are pressed, press all other partial hold keys that are still held + // if all keys were pressed, press all other partial hold keys that are still held if (all_keys_pressed) { // except the one that was released if (partial_hold_position != position) {