This commit is contained in:
John Wilmes
2025-09-10 17:32:11 +01:00
committed by GitHub
31 changed files with 1905 additions and 565 deletions

View File

@@ -17,6 +17,24 @@ combo_t key_combos[] = {
This will send "Escape" if you hit the A and B keys, and Ctrl+Z when you hit the C and D keys.
## Combo timing and triggering
A combo will trigger if and only if
- all of its keys are pressed within its combo term (by default defined as `COMBO_TERM` milliseconds), and
- none of the following occurs
- One of the combo's keys was released before the final key was pressed
- Two events (press and/or release) occur for any one keycode (whether or not it belongs to the combo) between the first and last triggering key presses
- The key buffer overflows between the first and last key presses of the combo
- A hold, tap, ordering, contiguity, or generic `combo_should_trigger` condition prevents it from firing (see below). By default, this constraints all combos to be *contiguous*. That means that if a key that doesn't belong to the combo is pressed between the first and last triggers, then the combo will not fire. (However, irrelevant key *releases* will not interrupt combos.)
- An overlapping combo with higher priority triggers instead
Combos, and key presses/releases not consumed by combos, will always be released in their event order. The event time of a combo is that of its FIRST trigger key press.
Each key press will be consumed by at most one combo. That means that when overlapping combos have all trigger keys pressed, we need to decide which one will fire. By default, we prioritize combos as follows:
- first, we prioritize combos whose first trigger key was pressed earliest
- next, we prioritize longer combos
- finally, we prioritize combos with a larger index (i.e., appearing later in the list of combos)
## Advanced Keycodes Support
Advanced keycodes, such as [Mod-Tap](../mod_tap) and [Tap Dance](tap_dance) are also supported together with combos. If you use these advanced keycodes in your keymap, you will need to place the full keycode in the combo definition, e.g.:
@@ -114,37 +132,42 @@ You can enable, disable and toggle the Combo feature on the fly. This is useful
These configuration settings can be set in your `config.h` file.
### Combo Term
By default, the timeout for the Combos to be recognized is set to 50ms. This can be changed if accidental combo misfires are happening or if you're having difficulties pressing keys at the same time. For instance, `#define COMBO_TERM 40` would set the timeout period for combos to 40ms.
By default, the timeout for the Combos to be recognized is set to 150ms. This can be changed if accidental combo misfires are happening or if you're having difficulties pressing keys at the same time. For instance, `#define COMBO_TERM 40` would set the timeout period for combos to 40ms. See below for how to define this differently for each combo.
### Buffer and state sizes
If you're using long combos, or you have a lot of overlapping combos, you may run into issues with this, as the buffers may not be large enough to accommodate what you're doing. In this case, you can configure the sizes of the buffers used. Be aware, larger combo sizes and larger buffers will increase memory usage!
To configure the amount of keys a combo can be composed of, change the following:
To configure the maximum amount of keys a combo can be composed of, change the following:
| Keys | Define to be set |
| Keys (`MAX_COMBO_LENGTH`) | Define to be set |
|------|-----------------------------------|
| 6 | `#define EXTRA_SHORT_COMBOS` |
| 4 | `#define EXTRA_SMALL_COMBOS` |
| 8 | QMK Default |
| 16 | `#define EXTRA_LONG_COMBOS` |
| 32 | `#define EXTRA_EXTRA_LONG_COMBOS` |
Defining `EXTRA_SHORT_COMBOS` combines a combo's internal state into just one byte. This can, in some cases, save some memory. If it doesn't, no point using it. If you do, you also have to make sure you don't define combos with more than 6 keys.
Processing combos has two buffers, one for the key presses, another for the combos being activated. Use the following options to configure the sizes of these buffers:
A larger maximum combo length will cause a (pretty negligible) increase in memory usage. Another cost of longer combos is limiting the maximum number of combos that can be defined. The maximum combo count is `(65536 / MAX_COMBO_LENGTH) - 1`.
If you have a modest number of combos that aren't too large, you can save additional memory by defining `COMBO_COMPRESSED`. This compresses the internal state of each combo to a single byte. However, this should ONLY be set if you have fewer than `(256/MAX_COMBO_LENGTH) -1` combos, where `MAX_COMBO_LENGTH` is inferred from the flags above.
Processing combos requires two buffers, one for the key presses, another for currently active combos. Use the following options to configure the sizes of these buffers:
| Define | Default |
|-------------------------------------|------------------------------------------------------|
| `#define COMBO_KEY_BUFFER_LENGTH 8` | 8 (the key amount `(EXTRA_)EXTRA_LONG_COMBOS` gives) |
| `#define COMBO_KEY_BUFFER_LENGTH 8` | `MAX_COMBO_LENGTH` + 4 |
| `#define COMBO_BUFFER_LENGTH 4` | 4 |
If the key buffer overflows, then completed combos might get activated before their hold term expires, and incomplete combos might get inactivated. If the combo buffer overflows, then active combos might be deactivated before all their keys are released. Longer buffers increase memory usage.
### Modifier Combos
If a combo resolves to a Modifier, the window for processing the combo can be extended independently from normal combos. By default, this is disabled but can be enabled with `#define COMBO_MUST_HOLD_MODS`, and the time window can be configured with `#define COMBO_HOLD_TERM 150` (default: `TAPPING_TERM`). With `COMBO_MUST_HOLD_MODS`, you cannot tap the combo any more which makes the combo less prone to misfires.
### Strict key press order
By defining `COMBO_MUST_PRESS_IN_ORDER` combos only activate when the keys are pressed in the same order as they are defined in the key array.
### Per Combo Timing, Holding, Tapping and Key Press Order
For each combo, it is possible to configure the time window it has to pressed in, if it needs to be held down, if it needs to be tapped, or if its keys need to be pressed in order.
### Per Combo Timing, Holding, Tapping, Key Press Order, and Contiguity
For each combo, it is possible to configure the time window it has to pressed in, if it needs to be held down, if it needs to be tapped, if its keys need to be pressed in order, or if its key presses need to be contiguous.
For example, tap-only combos are useful if any (or all) of the underlying keys are mod-tap or layer-tap keys. When you tap the combo, you get the combo result. When you press the combo and hold it down, the combo doesn't activate. Instead the keys are processed separately as if the combo wasn't even there.
@@ -156,6 +179,8 @@ In order to use these features, the following configuration options and function
| `COMBO_MUST_HOLD_PER_COMBO` | `bool get_combo_must_hold(uint16_t combo_index, combo_t *combo)` | Controls if a given combo should fire immediately on tap or if it needs to be held. (default: `false`) |
| `COMBO_MUST_TAP_PER_COMBO` | `bool get_combo_must_tap(uint16_t combo_index, combo_t *combo)` | Controls if a given combo should fire only if tapped within `COMBO_HOLD_TERM`. (default: `false`) |
| `COMBO_MUST_PRESS_IN_ORDER_PER_COMBO` | `bool get_combo_must_press_in_order(uint16_t combo_index, combo_t *combo)` | Controls if a given combo should fire only if its keys are pressed in order. (default: `true`) |
| `COMBO_CONTIGUOUS_PER_COMBO` | `bool is_combo_contiguous(uint16_t index, combo_t *combo, uint16_t keycode, keyrecord_t *record, uint8_t n_unpressed_keys)` | Controls if a partially activated combo should be de-activated by a keypress not included in the combo |
| `COMBO_SHOULD_TRIGGER` | `combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record)` |
Examples:
```c
@@ -177,7 +202,7 @@ uint16_t get_combo_term(uint16_t combo_index, combo_t *combo) {
// i.e. the exact array of keys you defined for the combo.
// This can be useful if your combos have a common key and you want to apply the
// same combo term for all of them.
if (combo->keys[0] == KC_ENT) { // if first key in the array is Enter
if (pgm_read_word(&combo->keys[0]) == KC_ENT) { // if first key in the array is Enter
return 150;
}
@@ -239,6 +264,32 @@ bool get_combo_must_press_in_order(uint16_t combo_index, combo_t *combo) {
}
}
#endif
#ifdef COMBO_CONTIGUOUS_PER_COMBO
bool is_combo_contiguous(uint16_t index, combo_t *combo, uint16_t keycode, keyrecord_t *record, uint8_t n_unpressed_keys) {
/* Decide if a key *press* for a key not involved in a combo should interrupt that combo.
* A "contiguous" combo requires that all the keys of the combo are pressed together, without any other key presses
* occurring in between. A "non-contiguous" combo will still fire even if irrelevant keys are pressed between its triggers.
* This function lets us define that behavior on a per-combo basis, and even based on which non-combo key has been pressed
*
* `index` and `combo` as above
* `keycode` and `record` describe a key that has been pressed that DOES NOT belong to this combo
* `n_unpressed_keys` is the number of keys of combo we are still waiting to be pressed for the combo to complete
*/
if (keycode == KC_LCTL) {
return false; // left control doesn't interrupt any combo
}
switch (combo_index) {
case MY_INDEPENDENT_COMBO_1:
case MY_INDEPENDENT_COMBO_2:
// I like to mash these together, so they shouldn't be contiguous
return false;
default:
// Default to requiring that no unrelated key presses interrupt the combo
return true;
}
}
#endif
```
### Generic hook to (dis)allow a combo activation
@@ -246,6 +297,9 @@ bool get_combo_must_press_in_order(uint16_t combo_index, combo_t *combo) {
By defining `COMBO_SHOULD_TRIGGER` and its companying function `bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record)` you can block or allow combos to activate on the conditions of your choice.
For example, you could disallow some combos on the base layer and allow them on another. Or disable combos on the home row when a timer is running.
This function is called for every keypress for keys included in the combo. It must return true for each of these keypresses in
order for the combo to trigger.
Examples:
```c
bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record) {
@@ -261,24 +315,24 @@ bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode
}
```
### Combo timer
### Customizable combo prioritization
Normally, the timer is started on the first key press and then reset on every subsequent key press within the `COMBO_TERM`.
Inputting combos is relaxed like this, but also slightly more prone to accidental misfires.
If the default prioritization of combos described above doesn't work for you, you can override it by defining the following function:
The next two options alter the behaviour of the timer.
```c
/* Return true if combo1 is preferred to combo2 if they could both activate.
* Default behavior: prefer longer combos, and break ties by preferring combos with higher indices */
bool is_combo_preferred(uint16_t combo_index1, uint16_t combo_index2, uint8_t combo_length1) {
uint8_t combo_length2 = _get_combo_length(combo_get(combo_index2));
if (combo_length1 > combo_length2) {
return true;
}
return combo_index1 > combo_index2;
}
```
#### `#define COMBO_STRICT_TIMER`
However, we always prefer combos whose first triggering key is earlier, even if they are shorter. E.g., if I press `A,B,C,D` in order, then combo `A+B` will be preferred to `B+C+D` regardless of how `is_combo_preferred` is implemented.
With `COMBO_STRICT_TIMER`, the timer is started only on the first key press.
Inputting combos is now less relaxed; you need to make sure the full chord is pressed within the `COMBO_TERM`.
Misfires are less common but if you type multiple combos fast, there is a
chance that the latter ones might not activate properly.
#### `#define COMBO_NO_TIMER`
By defining `COMBO_NO_TIMER`, the timer is disabled completely and combos are activated on the first key release.
This also disables the "must hold" functionalities as they just wouldn't work at all.
### Customizable key releases
@@ -316,6 +370,7 @@ bool process_combo_key_release(uint16_t combo_index, combo_t *combo, uint8_t key
}
```
### Customizable key repress
By defining `COMBO_PROCESS_KEY_REPRESS` and implementing `bool process_combo_key_repress(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode)` you can run your custom code when you repress just released key of a combo. By combining it with custom `process_combo_event` we can for example make special handling for Alt+Tab to switch windows, which, on combo F+G activation, registers Alt and presses Tab - then we can switch windows forward by releasing G and pressing it again, or backwards with F key. Here's the full example:

View File

@@ -541,6 +541,9 @@ void keyboard_init(void) {
#ifdef HAPTIC_ENABLE
haptic_init();
#endif
#ifdef COMBO_ENABLE
combo_enable();
#endif
#if defined(DEBUG_MATRIX_SCAN_RATE) && defined(CONSOLE_ENABLE)
debug_enable = true;

File diff suppressed because it is too large Load Diff

View File

@@ -22,39 +22,44 @@
#include "keycodes.h"
#include "quantum_keycodes.h"
#ifdef EXTRA_SHORT_COMBOS
# define MAX_COMBO_LENGTH 6
#elif defined(EXTRA_EXTRA_LONG_COMBOS)
/* COMBO_BUFFER_LENGTH defines the maximum number of simulatenously active combos. */
#ifndef COMBO_BUFFER_LENGTH
# define COMBO_BUFFER_LENGTH 4
#endif
#if defined(EXTRA_EXTRA_LONG_COMBOS)
# define MAX_COMBO_LENGTH 32
# define COMBO_STATE_BITS 5
typedef uint32_t combo_active_state_t;
#elif defined(EXTRA_LONG_COMBOS)
# define MAX_COMBO_LENGTH 16
# define COMBO_STATE_BITS 4
typedef uint16_t combo_active_state_t;
#elif defined(EXTRA_SMALL_COMBOS)
# define MAX_COMBO_LENGTH 4
# define COMBO_STATE_BITS 2
typedef uint8_t combo_active_state_t;
#else
# define MAX_COMBO_LENGTH 8
# define COMBO_STATE_BITS 3
typedef uint8_t combo_active_state_t;
#endif
#ifdef COMBO_COMPRESSED
/* If combo_count() < (256/MAX_COMBO_LENGTH) - 1, this can be defined to save some space */
typedef uint8_t combo_state_t;
#else
typedef uint16_t combo_state_t;
#endif
#ifndef COMBO_KEY_BUFFER_LENGTH
# define COMBO_KEY_BUFFER_LENGTH MAX_COMBO_LENGTH
#endif
#ifndef COMBO_BUFFER_LENGTH
# define COMBO_BUFFER_LENGTH 4
# define COMBO_KEY_BUFFER_LENGTH (MAX_COMBO_LENGTH + 4)
#endif
typedef struct combo_t {
const uint16_t *keys;
uint16_t keycode;
#ifdef EXTRA_SHORT_COMBOS
uint8_t state;
#else
bool disabled;
bool active;
# if defined(EXTRA_EXTRA_LONG_COMBOS)
uint32_t state;
# elif defined(EXTRA_LONG_COMBOS)
uint16_t state;
# else
uint8_t state;
# endif
#endif
combo_state_t state;
} combo_t;
#define COMBO(ck, ca) \
@@ -64,14 +69,14 @@ typedef struct combo_t {
#define COMBO_END 0
#ifndef COMBO_TERM
# define COMBO_TERM 50
# define COMBO_TERM 150
#endif
#ifndef COMBO_HOLD_TERM
# define COMBO_HOLD_TERM TAPPING_TERM
#endif
/* check if keycode is only modifiers */
#define KEYCODE_IS_MOD(code) (IS_MODIFIER_KEYCODE(code) || (IS_QK_MODS(code) && !QK_MODS_GET_BASIC_KEYCODE(code)))
#define KEYCODE_IS_MOD(code) (IS_MOD(code) || (code >= QK_MODS && code <= QK_MODS_MAX && !(code & QK_BASIC_MAX)))
bool process_combo(uint16_t keycode, keyrecord_t *record);
void combo_task(void);

View File

@@ -0,0 +1,8 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "test_common.h"
#define COMBO_BUFFER_LENGTH 2

View File

@@ -0,0 +1,6 @@
# Copyright 2025 @johnwilmes
# SPDX-License-Identifier: GPL-2.0-or-later
COMBO_ENABLE = yes
INTROSPECTION_KEYMAP_C = test_combo_buffer.c

View File

@@ -0,0 +1,37 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.h"
#include "test_common.hpp"
#include "test_driver.hpp"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
using testing::_;
using testing::InSequence;
extern bool combo_override;
class ComboBuffer : public TestFixture {};
TEST_F(ComboBuffer, combo_active_buffer_overflow) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 1, 0, KC_B);
KeymapKey key_c(0, 2, 0, KC_C);
KeymapKey key_d(0, 3, 0, KC_D);
KeymapKey key_e(0, 4, 0, KC_E);
KeymapKey key_f(0, 5, 0, KC_F);
set_keymap({key_a, key_b, key_c, key_d, key_e, key_f});
EXPECT_REPORT(driver, (KC_1));
EXPECT_REPORT(driver, (KC_1, KC_2));
EXPECT_REPORT(driver, (KC_2));
EXPECT_REPORT(driver, (KC_2, KC_3));
EXPECT_REPORT(driver, (KC_3));
EXPECT_EMPTY_REPORT(driver);
tap_combo({key_a, key_b, key_c, key_d, key_e, key_f});
VERIFY_AND_CLEAR(driver);
}

View File

@@ -0,0 +1,18 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#include "quantum.h"
#include "stdio.h"
enum combos { ab_1, cd_2, ef_3 };
uint16_t const ab_1_combo[] = {KC_A, KC_B, COMBO_END};
uint16_t const cd_2_combo[] = {KC_C, KC_D, COMBO_END};
uint16_t const ef_3_combo[] = {KC_E, KC_F, COMBO_END};
// clang-format off
combo_t key_combos[] = {
[ab_1] = COMBO(ab_1_combo, KC_1),
[cd_2] = COMBO(cd_2_combo, KC_2),
[ef_3] = COMBO(ef_3_combo, KC_3),
};
// clang-format on

View File

@@ -0,0 +1,8 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "test_common.h"
#define COMBO_CONTIGUOUS_PER_COMBO

View File

@@ -0,0 +1,6 @@
# Copyright 2025 @johnwilmes
# SPDX-License-Identifier: GPL-2.0-or-later
COMBO_ENABLE = yes
INTROSPECTION_KEYMAP_C = test_combo_conflicts.c

View File

@@ -0,0 +1,132 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.h"
#include "test_common.hpp"
#include "test_driver.hpp"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
using testing::_;
using testing::InSequence;
class ComboConflicts : public TestFixture {};
TEST_F(ComboConflicts, combo_irrelevant_press) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 1, 0, KC_B);
KeymapKey key_x(0, 2, 0, KC_X);
KeymapKey key_y(0, 3, 0, KC_Y);
KeymapKey key_z(0, 4, 0, KC_Z);
set_keymap({key_a, key_b, key_x, key_y, key_z});
EXPECT_REPORT(driver, (KC_1)).Times(2);
EXPECT_REPORT(driver, (KC_1, KC_Z));
EXPECT_EMPTY_REPORT(driver);
// Press A, Z, B in that order
// Combo for A+B should be triggered since it does not require contiguity
tap_combo({key_a, key_z, key_b});
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_A)).Times(3);
EXPECT_REPORT(driver, (KC_A, KC_Z));
EXPECT_REPORT(driver, (KC_A, KC_B));
EXPECT_EMPTY_REPORT(driver);
// Press A, press and release Z, press B in that order; release B then A
// Combo for A+B should not be triggered since there was a press+release of Z in between
run_one_scan_loop();
key_a.press();
run_one_scan_loop();
key_z.press();
run_one_scan_loop();
key_z.release();
run_one_scan_loop();
key_b.press();
run_one_scan_loop();
key_b.release();
run_one_scan_loop();
key_a.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_X));
EXPECT_REPORT(driver, (KC_X, KC_Z));
EXPECT_REPORT(driver, (KC_X, KC_Z, KC_Y));
EXPECT_REPORT(driver, (KC_Z, KC_Y));
EXPECT_REPORT(driver, (KC_Y));
EXPECT_EMPTY_REPORT(driver);
// Press X, Z, Y in that order; release X, Z, Y
// Combo for X+Y should not be triggered since it requires contiguity
tap_combo({key_x, key_z, key_y});
VERIFY_AND_CLEAR(driver);
}
TEST_F(ComboConflicts, combo_priority) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 1, 0, KC_B);
KeymapKey key_x(0, 2, 0, KC_X);
KeymapKey key_y(0, 3, 0, KC_Y);
set_keymap({key_a, key_b, key_x, key_y});
EXPECT_REPORT(driver, (KC_4)).Times(2);
EXPECT_REPORT(driver, (KC_4, KC_A));
EXPECT_EMPTY_REPORT(driver);
// Press X, A, B, Y in that order
// Combo for X+B+Y should be triggered since it has higher priority (index)
tap_combo({key_x, key_a, key_b, key_y});
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_4)).Times(2);
EXPECT_REPORT(driver, (KC_4, KC_A));
EXPECT_EMPTY_REPORT(driver);
// Press X, Y, A, B in that order
// Combo for X+B+Y should be triggered since it has higher priority (index)
tap_combo({key_x, key_y, key_a, key_b});
VERIFY_AND_CLEAR(driver);
}
TEST_F(ComboConflicts, combo_priority_trigger) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 1, 0, KC_B);
KeymapKey key_x(0, 2, 0, KC_X);
KeymapKey key_y(0, 3, 0, KC_Y);
set_keymap({key_a, key_b, key_x, key_y});
EXPECT_REPORT(driver, (KC_3)).Times(2);
EXPECT_REPORT(driver, (KC_3, KC_B));
EXPECT_EMPTY_REPORT(driver);
// Press A, X, B, Y in that order
// Combo for X+A+Y should be triggered since it has earlier trigger
tap_combo({key_a, key_x, key_b, key_y});
VERIFY_AND_CLEAR(driver);
}
TEST_F(ComboConflicts, combo_wait_for_preferred) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 1, 0, KC_B);
KeymapKey key_x(0, 2, 0, KC_X);
KeymapKey key_y(0, 3, 0, KC_Y);
KeymapKey key_c(0, 4, 0, KC_C);
set_keymap({key_a, key_b, key_x, key_y, key_c});
EXPECT_REPORT(driver, (KC_4)).Times(2);
EXPECT_REPORT(driver, (KC_4, KC_A));
EXPECT_EMPTY_REPORT(driver);
// Press X, A, Y, B in that order
// Combo for X+B+Y should be triggered since it has higher priority (index), even though X+A+Y completed before it
tap_combo({key_x, key_a, key_y, key_b});
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_6));
EXPECT_EMPTY_REPORT(driver);
// Press X, Y, A, B, C in that order
// Combo for X+A+B+C+Y should be triggered sice it has higher priority than the others
tap_combo({key_x, key_y, key_a, key_b, key_c});
VERIFY_AND_CLEAR(driver);
}

View File

@@ -0,0 +1,33 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#include "quantum.h"
enum combos { ab_1, xy_2, axy_3, bxy_4, cxy_5, abcxy_6 };
uint16_t const ab_1_combo[] = {KC_A, KC_B, COMBO_END};
uint16_t const xy_2_combo[] = {KC_X, KC_Y, COMBO_END};
uint16_t const axy_3_combo[] = {KC_A, KC_X, KC_Y, COMBO_END};
uint16_t const bxy_4_combo[] = {KC_B, KC_X, KC_Y, COMBO_END};
uint16_t const cxy_5_combo[] = {KC_C, KC_X, KC_Y, COMBO_END};
uint16_t const abcxy_6_combo[] = {KC_A, KC_B, KC_C, KC_X, KC_Y, COMBO_END};
// clang-format off
combo_t key_combos[] = {
[ab_1] = COMBO(ab_1_combo, KC_1),
[xy_2] = COMBO(xy_2_combo, KC_2),
[axy_3] = COMBO(axy_3_combo, KC_3),
[bxy_4] = COMBO(bxy_4_combo, KC_4),
[cxy_5] = COMBO(cxy_5_combo, KC_5),
[abcxy_6] = COMBO(abcxy_6_combo, KC_6)
};
// clang-format on
bool is_combo_contiguous(uint16_t index, combo_t *combo, keyrecord_t *record, uint8_t n_unpressed_keys) {
switch (index) {
case xy_2:
case cxy_5:
return true; // xy_2 and cxy_5 are contiguous combos
default:
return false; // no other combos are contiguous
}
}

View File

@@ -0,0 +1,9 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "test_common.h"
#define COMBO_PROCESS_KEY_RELEASE
#define COMBO_SHOULD_TRIGGER

View File

@@ -0,0 +1,6 @@
# Copyright 2025 @johnwilmes
# SPDX-License-Identifier: GPL-2.0-or-later
COMBO_ENABLE = yes
INTROSPECTION_KEYMAP_C = test_combo_events.c

View File

@@ -0,0 +1,67 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.h"
#include "test_common.hpp"
#include "test_driver.hpp"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
using testing::_;
using testing::InSequence;
extern bool combo_override;
class ComboEvents : public TestFixture {};
TEST_F(ComboEvents, combo_action) {
TestDriver driver;
KeymapKey key_a(0, 0, 1, KC_A);
KeymapKey key_b(0, 0, 2, KC_B);
set_keymap({key_a, key_b});
EXPECT_REPORT(driver, (KC_1));
EXPECT_REPORT(driver, (KC_2));
EXPECT_EMPTY_REPORT(driver).Times(2);
tap_combo({key_a, key_b});
VERIFY_AND_CLEAR(driver);
}
TEST_F(ComboEvents, combo_release) {
TestDriver driver;
KeymapKey key_x(0, 0, 1, KC_X);
KeymapKey key_y(0, 0, 2, KC_Y);
set_keymap({key_x, key_y});
EXPECT_REPORT(driver, (KC_1)).Times(3);
EXPECT_REPORT(driver, (KC_1, KC_2));
EXPECT_REPORT(driver, (KC_1, KC_3));
EXPECT_EMPTY_REPORT(driver);
tap_combo({key_x, key_y});
VERIFY_AND_CLEAR(driver);
// Combo deactivates early when y is released
EXPECT_REPORT(driver, (KC_1)).Times(2);
EXPECT_REPORT(driver, (KC_1, KC_3));
EXPECT_EMPTY_REPORT(driver);
tap_combo({key_y, key_x});
VERIFY_AND_CLEAR(driver);
}
TEST_F(ComboEvents, combo_should_trigger) {
TestDriver driver;
KeymapKey key_a(0, 0, 1, KC_A);
KeymapKey key_b(0, 0, 2, KC_B);
set_keymap({key_a, key_b});
combo_override = true;
EXPECT_REPORT(driver, (KC_A));
EXPECT_REPORT(driver, (KC_A, KC_B));
EXPECT_REPORT(driver, (KC_B));
EXPECT_EMPTY_REPORT(driver);
tap_combo({key_a, key_b});
VERIFY_AND_CLEAR(driver);
combo_override = false;
}

View File

@@ -0,0 +1,50 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#include "quantum.h"
#include "stdio.h"
enum combos { ab_action, xy_123 };
uint16_t const ab_action_combo[] = {KC_A, KC_B, COMBO_END};
uint16_t const xy_123_combo[] = {KC_X, KC_Y, COMBO_END};
// clang-format off
combo_t key_combos[] = {
[ab_action] = COMBO_ACTION(ab_action_combo),
[xy_123] = COMBO(xy_123_combo, KC_1),
};
// clang-format on
void process_combo_event(uint16_t combo_index, bool pressed) {
switch (combo_index) {
case ab_action:
if (pressed) {
tap_code(KC_1);
} else {
tap_code(KC_2);
}
break;
}
}
bool process_combo_key_release(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode) {
switch (combo_index) {
case xy_123:
switch (keycode) {
case KC_X:
tap_code(KC_2);
break;
case KC_Y:
tap_code(KC_3);
return true; // deactivate combo
break;
}
}
return false;
}
bool combo_override = false;
bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record) {
return !combo_override;
}

View File

@@ -0,0 +1,10 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "test_common.h"
#define COMBO_MUST_HOLD_PER_COMBO
#define COMBO_MUST_TAP_PER_COMBO
#define COMBO_HOLD_TERM 100

View File

@@ -0,0 +1,6 @@
# Copyright 2025 @johnwilmes
# SPDX-License-Identifier: GPL-2.0-or-later
COMBO_ENABLE = yes
INTROSPECTION_KEYMAP_C = test_combo_hold_tap.c

View File

@@ -0,0 +1,53 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.h"
#include "test_common.hpp"
#include "test_driver.hpp"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
using testing::_;
using testing::InSequence;
class ComboHoldTap : public TestFixture {};
TEST_F(ComboHoldTap, combo_hold) {
TestDriver driver;
KeymapKey key_a(0, 0, 1, KC_A);
KeymapKey key_b(0, 0, 2, KC_B);
set_keymap({key_a, key_b});
EXPECT_REPORT(driver, (KC_1));
EXPECT_EMPTY_REPORT(driver);
tap_combo({key_a, key_b}, COMBO_HOLD_TERM + 1);
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_A));
EXPECT_REPORT(driver, (KC_A, KC_B));
EXPECT_REPORT(driver, (KC_B));
EXPECT_EMPTY_REPORT(driver);
tap_combo({key_a, key_b}, COMBO_HOLD_TERM - 1);
VERIFY_AND_CLEAR(driver);
}
TEST_F(ComboHoldTap, combo_tap) {
TestDriver driver;
KeymapKey key_x(0, 0, 1, KC_X);
KeymapKey key_y(0, 0, 2, KC_Y);
set_keymap({key_x, key_y});
EXPECT_REPORT(driver, (KC_2));
EXPECT_EMPTY_REPORT(driver);
tap_combo({key_x, key_y}, COMBO_HOLD_TERM - 1);
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_X));
EXPECT_REPORT(driver, (KC_X, KC_Y));
EXPECT_REPORT(driver, (KC_Y));
EXPECT_EMPTY_REPORT(driver);
tap_combo({key_x, key_y}, COMBO_HOLD_TERM + 1);
VERIFY_AND_CLEAR(driver);
}

View File

@@ -0,0 +1,34 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#include "quantum.h"
#include "stdio.h"
enum combos { ab_1, xy_2 };
uint16_t const ab_1_combo[] = {KC_A, KC_B, COMBO_END};
uint16_t const xy_2_combo[] = {KC_X, KC_Y, COMBO_END};
// clang-format off
combo_t key_combos[] = {
[ab_1] = COMBO(ab_1_combo, KC_1),
[xy_2] = COMBO(xy_2_combo, KC_2),
};
// clang-format on
bool get_combo_must_hold(uint16_t combo_index, combo_t *combo) {
switch (combo_index) {
case ab_1:
return true;
default:
return false;
}
}
bool get_combo_must_tap(uint16_t combo_index, combo_t *combo) {
switch (combo_index) {
case xy_2:
return true;
default:
return false;
}
}

View File

@@ -0,0 +1,9 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "test_common.h"
#define COMBO_KEY_BUFFER_LENGTH 4
#define COMBO_CONTIGUOUS_PER_COMBO

View File

@@ -0,0 +1,6 @@
# Copyright 2025 @johnwilmes
# SPDX-License-Identifier: GPL-2.0-or-later
COMBO_ENABLE = yes
INTROSPECTION_KEYMAP_C = test_combo_key_buffer.c

View File

@@ -0,0 +1,103 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.h"
#include "test_common.hpp"
#include "test_driver.hpp"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
using testing::_;
using testing::InSequence;
extern bool combo_override;
class ComboKeyBuffer : public TestFixture {};
TEST_F(ComboKeyBuffer, combo_key_buffer) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 1, 0, KC_B);
KeymapKey key_i(0, 2, 0, KC_I);
KeymapKey key_j(0, 3, 0, KC_J);
KeymapKey key_k(0, 4, 0, KC_K);
KeymapKey key_a2(0, 5, 0, KC_A);
set_keymap({key_a, key_b, key_i, key_j, key_k, key_a2});
EXPECT_REPORT(driver, (KC_1)).Times(2);
EXPECT_REPORT(driver, (KC_1, KC_I));
EXPECT_REPORT(driver, (KC_1, KC_I, KC_J));
EXPECT_REPORT(driver, (KC_1, KC_J));
EXPECT_EMPTY_REPORT(driver);
tap_combo({key_a, key_i, key_j, key_b});
VERIFY_AND_CLEAR(driver);
// buffer overflow prevents combo from firing
EXPECT_REPORT(driver, (KC_A));
EXPECT_REPORT(driver, (KC_A, KC_I));
EXPECT_REPORT(driver, (KC_A, KC_I, KC_J));
EXPECT_REPORT(driver, (KC_A, KC_I, KC_J, KC_K));
EXPECT_REPORT(driver, (KC_A, KC_I, KC_J, KC_K, KC_B));
EXPECT_REPORT(driver, (KC_I, KC_J, KC_K, KC_B));
EXPECT_REPORT(driver, (KC_J, KC_K, KC_B));
EXPECT_REPORT(driver, (KC_K, KC_B));
EXPECT_REPORT(driver, (KC_B));
EXPECT_EMPTY_REPORT(driver);
tap_combo({key_a, key_i, key_j, key_k, key_b});
VERIFY_AND_CLEAR(driver);
// buffer overflow prevents combo from firing initially, fires with a second key press
EXPECT_REPORT(driver, (KC_A)).Times(2);
EXPECT_REPORT(driver, (KC_A, KC_I));
EXPECT_REPORT(driver, (KC_A, KC_I, KC_J));
EXPECT_REPORT(driver, (KC_A, KC_I, KC_J, KC_K));
EXPECT_REPORT(driver, (KC_A, KC_I, KC_J, KC_K, KC_1));
// the first KC_A release is consumed by the active combo even though it is a different position
EXPECT_REPORT(driver, (KC_A, KC_J, KC_K, KC_1));
EXPECT_REPORT(driver, (KC_A, KC_K, KC_1));
EXPECT_REPORT(driver, (KC_A, KC_1));
EXPECT_EMPTY_REPORT(driver);
for (KeymapKey key : {key_a, key_i, key_j, key_k, key_b, key_a2}) {
key.press();
run_one_scan_loop();
}
for (KeymapKey key : {key_a, key_i, key_j, key_k, key_b, key_a2}) {
key.release();
run_one_scan_loop();
}
VERIFY_AND_CLEAR(driver);
}
TEST_F(ComboKeyBuffer, combo_key_buffer_blocked) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 1, 0, KC_B);
KeymapKey key_i(0, 2, 0, KC_I);
KeymapKey key_j(0, 3, 0, KC_J);
KeymapKey key_k(0, 4, 0, KC_K);
KeymapKey key_x(0, 5, 0, KC_X);
set_keymap({key_a, key_b, key_i, key_j, key_k, key_x});
// If the key buffer doesn't overflow, we wait for ABX to fire
EXPECT_REPORT(driver, (KC_2)).Times(2);
EXPECT_REPORT(driver, (KC_I, KC_2));
EXPECT_EMPTY_REPORT(driver);
tap_combo({key_a, key_b, key_i, key_x});
VERIFY_AND_CLEAR(driver);
// If the key buffer overflows, we just use AB
EXPECT_REPORT(driver, (KC_1));
EXPECT_REPORT(driver, (KC_1, KC_I));
EXPECT_REPORT(driver, (KC_1, KC_I, KC_J));
EXPECT_REPORT(driver, (KC_1, KC_I, KC_J, KC_K));
EXPECT_REPORT(driver, (KC_1, KC_I, KC_J, KC_K, KC_X));
EXPECT_REPORT(driver, (KC_I, KC_J, KC_K, KC_X));
EXPECT_REPORT(driver, (KC_J, KC_K, KC_X));
EXPECT_REPORT(driver, (KC_K, KC_X));
EXPECT_REPORT(driver, (KC_X));
EXPECT_EMPTY_REPORT(driver);
tap_combo({key_a, key_b, key_i, key_j, key_k, key_x});
VERIFY_AND_CLEAR(driver);
}

View File

@@ -0,0 +1,20 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#include "quantum.h"
#include "stdio.h"
enum combos { ab_1, abx_2 };
uint16_t const ab_1_combo[] = {KC_A, KC_B, COMBO_END};
uint16_t const abx_2_combo[] = {KC_A, KC_B, KC_X, COMBO_END};
// clang-format off
combo_t key_combos[] = {
[ab_1] = COMBO(ab_1_combo, KC_1),
[abx_2] = COMBO(abx_2_combo, KC_2),
};
// clang-format on
bool is_combo_contiguous(uint16_t index, combo_t *combo, keyrecord_t *record, uint8_t n_unpressed_keys) {
return false; // No combos are contiguous in this test
}

View File

@@ -0,0 +1,8 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "test_common.h"
#define COMBO_MUST_PRESS_IN_ORDER_PER_COMBO

View File

@@ -0,0 +1,6 @@
# Copyright 2025 @johnwilmes
# SPDX-License-Identifier: GPL-2.0-or-later
COMBO_ENABLE = yes
INTROSPECTION_KEYMAP_C = test_combo_order.c

View File

@@ -0,0 +1,70 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.h"
#include "test_common.hpp"
#include "test_driver.hpp"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
using testing::_;
using testing::InSequence;
class ComboOrder : public TestFixture {};
TEST_F(ComboOrder, combo_requires_order) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 1, 0, KC_B);
KeymapKey key_c(0, 2, 0, KC_C);
set_keymap({key_a, key_b, key_c});
EXPECT_REPORT(driver, (KC_1));
EXPECT_EMPTY_REPORT(driver);
// Press A, B, C in that order
// triggers ABC combo
tap_combo({key_a, key_b, key_c});
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_2));
EXPECT_EMPTY_REPORT(driver);
// Press C, A, B in that order; release B, A, C
// triggers CAB combo in order
tap_combo({key_c, key_a, key_b});
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_A));
EXPECT_REPORT(driver, (KC_A, KC_C));
EXPECT_REPORT(driver, (KC_A, KC_C, KC_B));
EXPECT_REPORT(driver, (KC_C, KC_B));
EXPECT_REPORT(driver, (KC_B));
EXPECT_EMPTY_REPORT(driver);
// Press A, C, B
// does not trigger any combo
tap_combo({key_a, key_c, key_b});
VERIFY_AND_CLEAR(driver);
}
TEST_F(ComboOrder, combo_doesnt_require_order) {
TestDriver driver;
KeymapKey key_x(0, 0, 0, KC_X);
KeymapKey key_y(0, 1, 0, KC_Y);
KeymapKey key_z(0, 2, 0, KC_Z);
set_keymap({key_x, key_y, key_z});
EXPECT_REPORT(driver, (KC_3));
EXPECT_EMPTY_REPORT(driver);
// Press X, Y, Z in that order
// triggers XYZ combo
tap_combo({key_x, key_y, key_z});
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_3));
EXPECT_EMPTY_REPORT(driver);
// Press Y, X, Z in that order
// triggers XYZ combo
tap_combo({key_y, key_x, key_z});
VERIFY_AND_CLEAR(driver);
}

View File

@@ -0,0 +1,30 @@
// Copyright 2025 @johnwilmes
// SPDX-License-Identifier: GPL-2.0-or-later
#include "quantum.h"
#include "stdio.h"
enum combos { abc_1, cab_2, xyz_3 };
uint16_t const abc_1_combo[] = {KC_A, KC_B, KC_C, COMBO_END};
uint16_t const cab_2_combo[] = {KC_C, KC_A, KC_B, COMBO_END};
uint16_t const xyz_3_combo[] = {KC_X, KC_Y, KC_Z, COMBO_END};
// clang-format off
combo_t key_combos[] = {
[abc_1] = COMBO(abc_1_combo, KC_1),
[cab_2] = COMBO(cab_2_combo, KC_2),
[xyz_3] = COMBO(xyz_3_combo, KC_3)
};
// clang-format on
bool get_combo_must_press_in_order(uint16_t combo_index, combo_t *combo) {
switch (combo_index) {
// these two must be pressed in order
case abc_1:
case cab_2:
return true;
default:
// xyz does not require pressing in order
return false;
}
}

View File

@@ -6,3 +6,5 @@
#include "test_common.h"
#define TAPPING_TERM 200
#define COMBO_TERM 40
#define COMBO_COMPRESSED

View File

@@ -4,8 +4,7 @@
#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.h"
#include "test_driver.hpp"
#include "test_common.hpp"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
@@ -14,6 +13,117 @@ using testing::InSequence;
class Combo : public TestFixture {};
TEST_F(Combo, combo_basic) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 1, 0, KC_B);
set_keymap({key_a, key_b});
EXPECT_REPORT(driver, (KC_1));
EXPECT_EMPTY_REPORT(driver);
// Press key A, wait for less than COMBO_TERM, then press key B
run_one_scan_loop(); // Ensure that combo timer is > 0
key_a.press();
idle_for(COMBO_TERM - 1);
key_b.press();
run_one_scan_loop();
key_a.release();
run_one_scan_loop();
key_b.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(Combo, combo_too_slow) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 1, 0, KC_B);
set_keymap({key_a, key_b});
EXPECT_REPORT(driver, (KC_A));
EXPECT_REPORT(driver, (KC_A, KC_B));
EXPECT_REPORT(driver, (KC_B));
EXPECT_EMPTY_REPORT(driver);
// Press key A, wait for more than COMBO_TERM, then press key B
run_one_scan_loop(); // Ensure that combo timer is > 0
key_a.press();
idle_for(COMBO_TERM + 1);
key_b.press();
run_one_scan_loop();
key_a.release();
run_one_scan_loop();
key_b.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(Combo, combo_release_interrupt) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_c(0, 2, 0, KC_C);
KeymapKey key_d(0, 3, 0, KC_D);
set_keymap({key_a, key_c, key_d});
EXPECT_REPORT(driver, (KC_A));
EXPECT_REPORT(driver, (KC_A, KC_2));
EXPECT_REPORT(driver, (KC_2));
EXPECT_EMPTY_REPORT(driver);
// Press A, C, D in that order; release A after C
// Should still trigger combo for C+D
run_one_scan_loop();
key_a.press();
run_one_scan_loop();
key_c.press();
run_one_scan_loop();
key_a.release();
run_one_scan_loop();
key_d.press();
run_one_scan_loop();
key_c.release();
run_one_scan_loop();
key_d.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(Combo, combo_disjoint) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 1, 0, KC_B);
KeymapKey key_c(0, 2, 0, KC_C);
KeymapKey key_d(0, 3, 0, KC_D);
set_keymap({key_a, key_b, key_c, key_d});
EXPECT_REPORT(driver, (KC_1));
EXPECT_REPORT(driver, (KC_1, KC_2));
EXPECT_REPORT(driver, (KC_2));
EXPECT_EMPTY_REPORT(driver);
// Press A, B, C, D in that order; trigger combos for A+B and C+D
tap_combo({key_a, key_b, key_c, key_d});
VERIFY_AND_CLEAR(driver);
}
TEST_F(Combo, combo_noncontiguous) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 1, 0, KC_B);
KeymapKey key_c(0, 2, 0, KC_C);
KeymapKey key_d(0, 3, 0, KC_D);
set_keymap({key_a, key_b, key_c, key_d});
EXPECT_REPORT(driver, (KC_A));
EXPECT_REPORT(driver, (KC_A, KC_C));
EXPECT_REPORT(driver, (KC_A, KC_C, KC_B));
EXPECT_REPORT(driver, (KC_A, KC_C, KC_B, KC_D));
EXPECT_REPORT(driver, (KC_C, KC_B, KC_D));
EXPECT_REPORT(driver, (KC_B, KC_D));
EXPECT_REPORT(driver, (KC_D));
EXPECT_EMPTY_REPORT(driver);
// Press A, C, B, D in that order; don't trigger any combos
tap_combo({key_a, key_c, key_b, key_d});
VERIFY_AND_CLEAR(driver);
}
TEST_F(Combo, combo_modtest_tapped) {
TestDriver driver;
KeymapKey key_y(0, 0, 1, KC_Y);
@@ -54,3 +164,17 @@ TEST_F(Combo, combo_osmshift_tapped) {
tap_key(key_i);
VERIFY_AND_CLEAR(driver);
}
TEST_F(Combo, combo_single_key) {
// https://github.com/qmk/qmk_firmware/issues/25197
TestDriver driver;
KeymapKey key_t(0, 0, 0, KC_T);
set_keymap({key_t});
EXPECT_REPORT(driver, (KC_3)).Times(3);
EXPECT_EMPTY_REPORT(driver).Times(3);
tap_combo({key_t}, 1);
tap_combo({key_t}, 1);
tap_combo({key_t}, 1);
VERIFY_AND_CLEAR(driver);
}

View File

@@ -2,16 +2,22 @@
// Copyright 2023 @filterpaper
// Copyright 2023 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "quantum.h"
#include <quantum.h>
enum combos { modtest, osmshift };
enum combos { modtest, osmshift, ab_1, cd_2, t_3 };
uint16_t const modtest_combo[] = {KC_Y, KC_U, COMBO_END};
uint16_t const osmshift_combo[] = {KC_Z, KC_X, COMBO_END};
uint16_t const ab_1_combo[] = {KC_A, KC_B, COMBO_END};
uint16_t const cd_2_combo[] = {KC_C, KC_D, COMBO_END};
uint16_t const t_3_combo[] = {KC_T, COMBO_END};
// clang-format off
combo_t key_combos[] = {
[modtest] = COMBO(modtest_combo, RSFT_T(KC_SPACE)),
[osmshift] = COMBO(osmshift_combo, OSM(MOD_LSFT))
[osmshift] = COMBO(osmshift_combo, OSM(MOD_LSFT)),
[ab_1] = COMBO(ab_1_combo, KC_1),
[cd_2] = COMBO(cd_2_combo, KC_2),
[t_3] = COMBO(t_3_combo, KC_3),
};
// clang-format on