This commit is contained in:
Theo Lemay 2023-10-01 20:23:27 -07:00 committed by GitHub
commit 3e87666744
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 232 additions and 39 deletions

View file

@ -357,6 +357,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

View file

@ -19,3 +19,4 @@
#include <behaviors/key_repeat.dtsi>
#include <behaviors/backlight.dtsi>
#include <behaviors/macros.dtsi>
#include <behaviors/combos.dtsi>

View file

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

View file

@ -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

View file

@ -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]

View file

@ -28,16 +28,19 @@ 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.
// 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;
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 {
@ -46,6 +49,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 {
@ -266,22 +271,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) {
@ -304,6 +319,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];
}
@ -322,8 +338,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 +353,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 +379,72 @@ 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);
// 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++) {
// 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 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)) {
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 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) {
// 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 +515,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 +554,24 @@ 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), \
.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__), \
.behaviors = TRANSFORMED_BEHAVIORS(n), \
.behaviors_len = DT_PROP_LEN(n, bindings), \
};
#define INITIALIZE_COMBO(n) initialize_combo(&combo_config_##n);

View file

@ -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,13 @@ 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 |
| `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.

View file

@ -404,6 +404,7 @@ After creating the `.dtsi` from above, update `app/dts/behaviors.dtsi` to includ
#include <behaviors/key_repeat.dtsi>
#include <behaviors/backlight.dtsi>
#include <behaviors/macros.dtsi>
#include <behaviors/combos.dtsi>
// highlight-next-line
#include <behaviors/new_behavior_instance.dtsi>
```

View file

@ -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,69 @@ 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`.
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 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:
```
#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`.