Merge 47e7343ae4
into c1ebadcd2a
This commit is contained in:
commit
3e87666744
9 changed files with 232 additions and 39 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -19,3 +19,4 @@
|
|||
#include <behaviors/key_repeat.dtsi>
|
||||
#include <behaviors/backlight.dtsi>
|
||||
#include <behaviors/macros.dtsi>
|
||||
#include <behaviors/combos.dtsi>
|
15
app/dts/behaviors/combos.dtsi
Normal file
15
app/dts/behaviors/combos.dtsi
Normal 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>;
|
||||
};
|
||||
};
|
||||
};
|
8
app/dts/bindings/zmk,combo-partial-hold-position.yaml
Normal file
8
app/dts/bindings/zmk,combo-partial-hold-position.yaml
Normal 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
|
|
@ -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]
|
||||
|
|
137
app/src/combo.c
137
app/src/combo.c
|
@ -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);
|
||||
|
|
|
@ -12,10 +12,11 @@ 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_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.
|
||||
|
||||
|
@ -32,11 +33,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 | |
|
||||
| ------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
|
||||
| `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.
|
||||
|
|
|
@ -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>
|
||||
```
|
||||
|
|
|
@ -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`.
|
||||
|
|
Loading…
Add table
Reference in a new issue