From 2b00b846dce1d1267dfdb2a3c2972a367cc0dd44 Mon Sep 17 00:00:00 2001
From: Nick Brassel
Date: Fri, 21 Mar 2025 23:38:34 +1100
Subject: Non-volatile memory data repository pattern (#24356)
* First batch of eeconfig conversions.
* Offset and length for datablocks.
* `via`, `dynamic_keymap`.
* Fix filename.
* Commentary.
* wilba leds
* satisfaction75
* satisfaction75
* more keyboard whack-a-mole
* satisfaction75
* omnikeyish
* more whack-a-mole
* `generic_features.mk` to automatically pick up nvm repositories
* thievery
* deferred variable resolve
* whitespace
* convert api to structs/unions
* convert api to structs/unions
* convert api to structs/unions
* fixups
* code-side docs
* code size fix
* rollback
* nvm_xxxxx_erase
* Updated location of eeconfig magic numbers so non-EEPROM nvm drivers can use them too.
* Fixup build.
* Fixup compilation error with encoders.
* Build fixes.
* Add `via_ci` keymap to onekey to exercise VIA bindings (and thus dynamic keymap et.al.), fixup compilation errors based on preprocessor+sizeof.
* Build failure rectification.---
quantum/process_keycode/process_autocorrect.c | 6 +++---
quantum/process_keycode/process_clicky.c | 6 +++---
quantum/process_keycode/process_magic.c | 4 ++--
quantum/process_keycode/process_steno.c | 7 ++-----
4 files changed, 10 insertions(+), 13 deletions(-)
(limited to 'quantum/process_keycode')
diff --git a/quantum/process_keycode/process_autocorrect.c b/quantum/process_keycode/process_autocorrect.c
index b7f9132acf..0cde520778 100644
--- a/quantum/process_keycode/process_autocorrect.c
+++ b/quantum/process_keycode/process_autocorrect.c
@@ -38,7 +38,7 @@ bool autocorrect_is_enabled(void) {
*/
void autocorrect_enable(void) {
keymap_config.autocorrect_enable = true;
- eeconfig_update_keymap(keymap_config.raw);
+ eeconfig_update_keymap(&keymap_config);
}
/**
@@ -48,7 +48,7 @@ void autocorrect_enable(void) {
void autocorrect_disable(void) {
keymap_config.autocorrect_enable = false;
typo_buffer_size = 0;
- eeconfig_update_keymap(keymap_config.raw);
+ eeconfig_update_keymap(&keymap_config);
}
/**
@@ -58,7 +58,7 @@ void autocorrect_disable(void) {
void autocorrect_toggle(void) {
keymap_config.autocorrect_enable = !keymap_config.autocorrect_enable;
typo_buffer_size = 0;
- eeconfig_update_keymap(keymap_config.raw);
+ eeconfig_update_keymap(&keymap_config);
}
/**
diff --git a/quantum/process_keycode/process_clicky.c b/quantum/process_keycode/process_clicky.c
index 82000db9b3..f50761268a 100644
--- a/quantum/process_keycode/process_clicky.c
+++ b/quantum/process_keycode/process_clicky.c
@@ -66,17 +66,17 @@ void clicky_freq_reset(void) {
void clicky_toggle(void) {
audio_config.clicky_enable ^= 1;
- eeconfig_update_audio(audio_config.raw);
+ eeconfig_update_audio(&audio_config);
}
void clicky_on(void) {
audio_config.clicky_enable = 1;
- eeconfig_update_audio(audio_config.raw);
+ eeconfig_update_audio(&audio_config);
}
void clicky_off(void) {
audio_config.clicky_enable = 0;
- eeconfig_update_audio(audio_config.raw);
+ eeconfig_update_audio(&audio_config);
}
bool is_clicky_on(void) {
diff --git a/quantum/process_keycode/process_magic.c b/quantum/process_keycode/process_magic.c
index 3b35884d68..d5280105de 100644
--- a/quantum/process_keycode/process_magic.c
+++ b/quantum/process_keycode/process_magic.c
@@ -47,7 +47,7 @@ bool process_magic(uint16_t keycode, keyrecord_t *record) {
if (record->event.pressed) {
if (IS_MAGIC_KEYCODE(keycode)) {
/* keymap config */
- keymap_config.raw = eeconfig_read_keymap();
+ eeconfig_read_keymap(&keymap_config);
switch (keycode) {
case QK_MAGIC_SWAP_CONTROL_CAPS_LOCK:
keymap_config.swap_control_capslock = true;
@@ -187,7 +187,7 @@ bool process_magic(uint16_t keycode, keyrecord_t *record) {
break;
}
- eeconfig_update_keymap(keymap_config.raw);
+ eeconfig_update_keymap(&keymap_config);
clear_keyboard(); // clear to prevent stuck keys
return false;
diff --git a/quantum/process_keycode/process_steno.c b/quantum/process_keycode/process_steno.c
index c491d6f1d8..4789461352 100644
--- a/quantum/process_keycode/process_steno.c
+++ b/quantum/process_keycode/process_steno.c
@@ -20,9 +20,6 @@
#ifdef VIRTSER_ENABLE
# include "virtser.h"
#endif
-#ifdef STENO_ENABLE_ALL
-# include "eeprom.h"
-#endif
// All steno keys that have been pressed to form this chord,
// stored in MAX_STROKE_SIZE groups of 8-bit arrays.
@@ -128,13 +125,13 @@ static const uint16_t combinedmap_second[] PROGMEM = {STN_S2, STN_KL, STN_WL, ST
#ifdef STENO_ENABLE_ALL
void steno_init(void) {
- mode = eeprom_read_byte(EECONFIG_STENOMODE);
+ mode = eeconfig_read_steno_mode();
}
void steno_set_mode(steno_mode_t new_mode) {
steno_clear_chord();
mode = new_mode;
- eeprom_update_byte(EECONFIG_STENOMODE, mode);
+ eeconfig_update_steno_mode(mode);
}
#endif // STENO_ENABLE_ALL
--
cgit v1.2.3
From 8d8dcb089ed36e7e1a61d77e5a4b6b08c8668869 Mon Sep 17 00:00:00 2001
From: Pascal Getreuer
Date: Mon, 14 Apr 2025 09:46:24 -0700
Subject: [Core] Flow Tap tap-hold option to disable HRMs during fast typing
(#25125)
aka Global Quick Tap, Require Prior Idle---
data/mappings/info_config.hjson | 1 +
docs/tap_hold.md | 83 ++++++
quantum/action.c | 17 ++
quantum/action.h | 6 +
quantum/action_tapping.c | 138 +++++++--
quantum/action_tapping.h | 57 ++++
quantum/process_keycode/process_leader.c | 6 +-
tests/tap_hold_configurations/flow_tap/config.h | 22 ++
tests/tap_hold_configurations/flow_tap/test.mk | 18 ++
.../tap_hold_configurations/flow_tap/test_keymap.c | 23 ++
.../flow_tap/test_tap_hold.cpp | 313 +++++++++++++++++++++
11 files changed, 648 insertions(+), 36 deletions(-)
create mode 100644 tests/tap_hold_configurations/flow_tap/config.h
create mode 100644 tests/tap_hold_configurations/flow_tap/test.mk
create mode 100644 tests/tap_hold_configurations/flow_tap/test_keymap.c
create mode 100644 tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp
(limited to 'quantum/process_keycode')
diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson
index b643553b52..bab881583a 100644
--- a/data/mappings/info_config.hjson
+++ b/data/mappings/info_config.hjson
@@ -201,6 +201,7 @@
// Tapping
"CHORDAL_HOLD": {"info_key": "tapping.chordal_hold", "value_type": "flag"},
+ "FLOW_TAP_TERM": {"info_key": "tapping.flow_tap_term", "value_type": "int"},
"HOLD_ON_OTHER_KEY_PRESS": {"info_key": "tapping.hold_on_other_key_press", "value_type": "flag"},
"HOLD_ON_OTHER_KEY_PRESS_PER_KEY": {"info_key": "tapping.hold_on_other_key_press_per_key", "value_type": "flag"},
"PERMISSIVE_HOLD": {"info_key": "tapping.permissive_hold", "value_type": "flag"},
diff --git a/docs/tap_hold.md b/docs/tap_hold.md
index 254d5de5ec..f1af753eba 100644
--- a/docs/tap_hold.md
+++ b/docs/tap_hold.md
@@ -425,6 +425,89 @@ uint16_t get_quick_tap_term(uint16_t keycode, keyrecord_t *record) {
If `QUICK_TAP_TERM` is set higher than `TAPPING_TERM`, it will default to `TAPPING_TERM`.
:::
+## Flow Tap
+
+Flow Tap modifies mod-tap `MT` and layer-tap `LT` keys such that when pressed within a short timeout of the preceding key, the tapping behavior is triggered. This is particularly useful for home row mods to avoid accidental mod triggers. It basically disables the hold behavior during fast typing, creating a "flow of taps." This also helps to reduce the input lag of tap-hold keys during fast typing, since the tapped behavior is sent immediately.
+
+Flow Tap is enabled by defining `FLOW_TAP_TERM` in your `config.h` with the desired timeout in milliseconds. A timeout of 150 ms is recommended as a starting point:
+
+```c
+#define FLOW_TAP_TERM 150
+```
+
+By default, Flow Tap is enabled when:
+
+* The tap-hold key is pressed within `FLOW_TAP_TERM` milliseconds of the previous key press.
+
+* The tapping keycodes of the previous key and tap-hold key are *both* among `KC_A`–`KC_Z`, `KC_COMM`, `KC_DOT`, `KC_SCLN`, `KC_SLSH` (the main alphas area of a conventional QWERTY layout) or `KC_SPC`.
+
+As an exception to the above, Flow Tap is temporarily disabled while a tap-hold key is undecided. This is to allow chording multiple mod-tap keys without having to wait out the Flow Tap term.
+
+
+### is_flow_tap_key()
+
+Optionally, define the `is_flow_tap_key()` callback to specify where Flow Tap is enabled. The callback is called for both the tap-hold key *and* the key press immediately preceding it, and if the callback returns true for both keycodes, Flow Tap is enabled.
+
+The default implementation of this callback is:
+
+```.c
+bool is_flow_tap_key(uint16_t keycode) {
+ if ((get_mods() & (MOD_MASK_CG | MOD_BIT_LALT)) != 0) {
+ return false; // Disable Flow Tap on hotkeys.
+ }
+ switch (get_tap_keycode(keycode)) {
+ case KC_SPC:
+ case KC_A ... KC_Z:
+ case KC_DOT:
+ case KC_COMM:
+ case KC_SCLN:
+ case KC_SLSH:
+ return true;
+ }
+ return false;
+}
+```
+
+Copy the above to your `keymap.c` and edit to customize. For instance, remove the `case KC_SPC` line to disable Flow Tap for the Space key.
+
+### get_flow_tap_term()
+
+Optionally, for further flexibility, define the `get_flow_tap_term()` callback. Flow Tap acts only when key events are closer together than the time returned by the callback. Return a time of 0 to disable filtering. In this way, Flow Tap may be disabled for certain tap-hold keys, or when following certain previous keys.
+
+The default implementation of this callback is
+
+```.c
+uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t* record,
+ uint16_t prev_keycode) {
+ if (is_flow_tap_key(keycode) && is_flow_tap_key(prev_keycode)) {
+ return FLOW_TAP_TERM;
+ }
+ return 0;
+}
+```
+
+In this callback, `keycode` and `record` correspond to the current tap-hold key, and `prev_keycode` is the keycode of the previous key. Return the timeout to use. Returning `0` disables Flow Tap. This callback enables setting per-key timeouts. It is also possible to enable or disable Flow Tap for certain tap-hold keys or when following certain previous keys. Example:
+
+```c
+uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t* record,
+ uint16_t prev_keycode) {
+ if (is_flow_tap_key(keycode) && is_flow_tap_key(prev_keycode)) {
+ switch (keycode) {
+ case LCTL_T(KC_F):
+ case RCTL_T(KC_H):
+ return FLOW_TAP_TERM - 25; // Short timeout on these keys.
+
+ default:
+ return FLOW_TAP_TERM; // Longer timeout otherwise.
+ }
+ }
+ return 0; // Disable Flow Tap.
+}
+```
+
+::: tip If you define both `is_flow_tap_key()` and `get_flow_tap_term()`, then the latter takes precedence.
+:::
+
## Chordal Hold
Chordal Hold is intended to be used together with either Permissive Hold or Hold
diff --git a/quantum/action.c b/quantum/action.c
index be85192d25..eb0dbc7022 100644
--- a/quantum/action.c
+++ b/quantum/action.c
@@ -1183,6 +1183,23 @@ bool is_tap_action(action_t action) {
return false;
}
+uint16_t get_tap_keycode(uint16_t keycode) {
+ switch (keycode) {
+ case QK_MOD_TAP ... QK_MOD_TAP_MAX:
+ return QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
+ case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
+ return QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
+ case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX:
+ // IS_SWAP_HANDS_KEYCODE() tests for the special action keycodes
+ // like SH_TOGG, SH_TT, ..., which overlap the SH_T(kc) range.
+ if (!IS_SWAP_HANDS_KEYCODE(keycode)) {
+ return QK_SWAP_HANDS_GET_TAP_KEYCODE(keycode);
+ }
+ break;
+ }
+ return keycode;
+}
+
/** \brief Debug print (FIXME: Needs better description)
*
* FIXME: Needs documentation.
diff --git a/quantum/action.h b/quantum/action.h
index 7596688f31..7616486c6d 100644
--- a/quantum/action.h
+++ b/quantum/action.h
@@ -128,6 +128,12 @@ void layer_switch(uint8_t new_layer);
bool is_tap_record(keyrecord_t *record);
bool is_tap_action(action_t action);
+/**
+ * Given an MT or LT keycode, returns the tap keycode. Otherwise returns the
+ * original keycode unchanged.
+ */
+uint16_t get_tap_keycode(uint16_t keycode);
+
#ifndef NO_ACTION_TAPPING
void process_record_tap_hint(keyrecord_t *record);
#endif
diff --git a/quantum/action_tapping.c b/quantum/action_tapping.c
index e42a98554d..312c639169 100644
--- a/quantum/action_tapping.c
+++ b/quantum/action_tapping.c
@@ -4,6 +4,7 @@
#include "action.h"
#include "action_layer.h"
#include "action_tapping.h"
+#include "action_util.h"
#include "keycode.h"
#include "timer.h"
@@ -49,9 +50,7 @@ __attribute__((weak)) bool get_permissive_hold(uint16_t keycode, keyrecord_t *re
}
# endif
-# if defined(CHORDAL_HOLD)
-extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM;
-
+# if defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
# define REGISTERED_TAPS_SIZE 8
// Array of tap-hold keys that have been settled as tapped but not yet released.
static keypos_t registered_taps[REGISTERED_TAPS_SIZE] = {};
@@ -66,6 +65,14 @@ static void registered_taps_del_index(uint8_t i);
/** Logs the registered_taps array for debugging. */
static void debug_registered_taps(void);
+static bool is_mt_or_lt(uint16_t keycode) {
+ return IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode);
+}
+# endif // defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
+
+# if defined(CHORDAL_HOLD)
+extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM;
+
/** \brief Finds which queued events should be held according to Chordal Hold.
*
* In a situation with multiple unsettled tap-hold key presses, scan the queue
@@ -82,10 +89,6 @@ static void waiting_buffer_chordal_hold_taps_until(keypos_t key);
/** \brief Processes and pops buffered events until the first tap-hold event. */
static void waiting_buffer_process_regular(void);
-
-static bool is_mt_or_lt(uint16_t keycode) {
- return IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode);
-}
# endif // CHORDAL_HOLD
# ifdef HOLD_ON_OTHER_KEY_PRESS_PER_KEY
@@ -98,6 +101,13 @@ __attribute__((weak)) bool get_hold_on_other_key_press(uint16_t keycode, keyreco
# include "process_auto_shift.h"
# endif
+# if defined(FLOW_TAP_TERM)
+static uint32_t last_input = 0;
+static uint16_t prev_keycode = KC_NO;
+
+uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode);
+# endif // defined(FLOW_TAP_TERM)
+
static keyrecord_t tapping_key = {};
static keyrecord_t waiting_buffer[WAITING_BUFFER_SIZE] = {};
static uint8_t waiting_buffer_head = 0;
@@ -147,6 +157,19 @@ void action_tapping_process(keyrecord_t record) {
}
}
if (IS_EVENT(record.event)) {
+# if defined(FLOW_TAP_TERM)
+ const uint16_t keycode = get_record_keycode(&record, false);
+ // Track the previous key press.
+ if (record.event.pressed) {
+ prev_keycode = keycode;
+ }
+ // If there is no unsettled tap-hold key, update last input time. Ignore
+ // mod keys in this update to allow for chording multiple mods for
+ // hotkeys like "Ctrl+Shift+arrow".
+ if (IS_NOEVENT(tapping_key.event) && !IS_MODIFIER_KEYCODE(keycode)) {
+ last_input = timer_read32();
+ }
+# endif // defined(FLOW_TAP_TERM)
ac_dprintf("\n");
}
}
@@ -205,7 +228,7 @@ void action_tapping_process(keyrecord_t record) {
bool process_tapping(keyrecord_t *keyp) {
const keyevent_t event = keyp->event;
-# if defined(CHORDAL_HOLD)
+# if defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
if (!event.pressed) {
const int8_t i = registered_tap_find(event.key);
if (i != -1) {
@@ -217,7 +240,7 @@ bool process_tapping(keyrecord_t *keyp) {
debug_registered_taps();
}
}
-# endif // CHORDAL_HOLD
+# endif // defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
// state machine is in the "reset" state, no tapping key is to be
// processed
@@ -227,6 +250,27 @@ bool process_tapping(keyrecord_t *keyp) {
} else if (event.pressed && is_tap_record(keyp)) {
// the currently pressed key is a tapping key, therefore transition
// into the "pressed" tapping key state
+
+# if defined(FLOW_TAP_TERM)
+ const uint16_t keycode = get_record_keycode(keyp, false);
+ if (is_mt_or_lt(keycode)) {
+ const uint32_t idle_time = timer_elapsed32(last_input);
+ uint16_t term = get_flow_tap_term(keycode, keyp, prev_keycode);
+ if (term > 500) {
+ term = 500;
+ }
+ if (idle_time < 500 && idle_time < term) {
+ debug_event(keyp->event);
+ ac_dprintf(" within flow tap term (%u < %u) considered a tap\n", (int16_t)idle_time, term);
+ keyp->tap.count = 1;
+ registered_taps_add(keyp->event.key);
+ debug_registered_taps();
+ process_record(keyp);
+ return true;
+ }
+ }
+# endif // defined(FLOW_TAP_TERM)
+
ac_dprintf("Tapping: Start(Press tap key).\n");
tapping_key = *keyp;
process_record_tap_hint(&tapping_key);
@@ -655,28 +699,7 @@ void waiting_buffer_scan_tap(void) {
}
}
-# ifdef CHORDAL_HOLD
-__attribute__((weak)) bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t *tap_hold_record, uint16_t other_keycode, keyrecord_t *other_record) {
- return get_chordal_hold_default(tap_hold_record, other_record);
-}
-
-bool get_chordal_hold_default(keyrecord_t *tap_hold_record, keyrecord_t *other_record) {
- if (tap_hold_record->event.type != KEY_EVENT || other_record->event.type != KEY_EVENT) {
- return true; // Return true on combos or other non-key events.
- }
-
- char tap_hold_hand = chordal_hold_handedness(tap_hold_record->event.key);
- if (tap_hold_hand == '*') {
- return true;
- }
- char other_hand = chordal_hold_handedness(other_record->event.key);
- return other_hand == '*' || tap_hold_hand != other_hand;
-}
-
-__attribute__((weak)) char chordal_hold_handedness(keypos_t key) {
- return (char)pgm_read_byte(&chordal_hold_layout[key.row][key.col]);
-}
-
+# if defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
static void registered_taps_add(keypos_t key) {
if (num_registered_taps >= REGISTERED_TAPS_SIZE) {
ac_dprintf("TAPS OVERFLOW: CLEAR ALL STATES\n");
@@ -714,6 +737,30 @@ static void debug_registered_taps(void) {
ac_dprintf("}\n");
}
+# endif // defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
+
+# ifdef CHORDAL_HOLD
+__attribute__((weak)) bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t *tap_hold_record, uint16_t other_keycode, keyrecord_t *other_record) {
+ return get_chordal_hold_default(tap_hold_record, other_record);
+}
+
+bool get_chordal_hold_default(keyrecord_t *tap_hold_record, keyrecord_t *other_record) {
+ if (tap_hold_record->event.type != KEY_EVENT || other_record->event.type != KEY_EVENT) {
+ return true; // Return true on combos or other non-key events.
+ }
+
+ char tap_hold_hand = chordal_hold_handedness(tap_hold_record->event.key);
+ if (tap_hold_hand == '*') {
+ return true;
+ }
+ char other_hand = chordal_hold_handedness(other_record->event.key);
+ return other_hand == '*' || tap_hold_hand != other_hand;
+}
+
+__attribute__((weak)) char chordal_hold_handedness(keypos_t key) {
+ return (char)pgm_read_byte(&chordal_hold_layout[key.row][key.col]);
+}
+
static uint8_t waiting_buffer_find_chordal_hold_tap(void) {
keyrecord_t *prev = &tapping_key;
uint16_t prev_keycode = get_record_keycode(&tapping_key, false);
@@ -761,6 +808,35 @@ static void waiting_buffer_process_regular(void) {
}
# endif // CHORDAL_HOLD
+# ifdef FLOW_TAP_TERM
+// By default, enable Flow Tap for the keys in the main alphas area and Space.
+// This should work reasonably even if the layout is remapped on the host to an
+// alt layout or international layout (e.g. Dvorak or AZERTY), where these same
+// key positions are mostly used for typing letters.
+__attribute__((weak)) bool is_flow_tap_key(uint16_t keycode) {
+ if ((get_mods() & (MOD_MASK_CG | MOD_BIT_LALT)) != 0) {
+ return false; // Disable Flow Tap on hotkeys.
+ }
+ switch (get_tap_keycode(keycode)) {
+ case KC_SPC:
+ case KC_A ... KC_Z:
+ case KC_DOT:
+ case KC_COMM:
+ case KC_SCLN:
+ case KC_SLSH:
+ return true;
+ }
+ return false;
+}
+
+__attribute__((weak)) uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode) {
+ if (is_flow_tap_key(keycode) && is_flow_tap_key(prev_keycode)) {
+ return FLOW_TAP_TERM;
+ }
+ return 0;
+}
+# endif // FLOW_TAP_TERM
+
/** \brief Logs tapping key if ACTION_DEBUG is enabled. */
static void debug_tapping_key(void) {
ac_dprintf("TAPPING_KEY=");
diff --git a/quantum/action_tapping.h b/quantum/action_tapping.h
index c3c7b999ec..2af000ad73 100644
--- a/quantum/action_tapping.h
+++ b/quantum/action_tapping.h
@@ -111,6 +111,63 @@ char chordal_hold_handedness(keypos_t key);
extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM;
#endif
+#ifdef FLOW_TAP_TERM
+/**
+ * Callback to specify the keys where Flow Tap is enabled.
+ *
+ * Flow Tap is constrained to certain keys by the following rule: this callback
+ * is called for both the tap-hold key *and* the key press immediately preceding
+ * it. If the callback returns true for both keycodes, Flow Tap is enabled.
+ *
+ * The default implementation of this callback corresponds to
+ *
+ * bool is_flow_tap_key(uint16_t keycode) {
+ * switch (get_tap_keycode(keycode)) {
+ * case KC_SPC:
+ * case KC_A ... KC_Z:
+ * case KC_DOT:
+ * case KC_COMM:
+ * case KC_SCLN:
+ * case KC_SLSH:
+ * return true;
+ * }
+ * return false;
+ * }
+ *
+ * @param keycode Keycode of the key.
+ * @return Whether to enable Flow Tap for this key.
+ */
+bool is_flow_tap_key(uint16_t keycode);
+
+/**
+ * Callback to customize Flow Tap filtering.
+ *
+ * Flow Tap acts only when key events are closer together than this time.
+ *
+ * Return a time of 0 to disable filtering. In this way, Flow Tap may be
+ * disabled for certain tap-hold keys, or when following certain previous keys.
+ *
+ * The default implementation of this callback is
+ *
+ * uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t* record,
+ * uint16_t prev_keycode) {
+ * if (is_flow_tap_key(keycode) && is_flow_tap_key(prev_keycode)) {
+ * return g_flow_tap_term;
+ * }
+ * return 0;
+ * }
+ *
+ * NOTE: If both `is_flow_tap_key()` and `get_flow_tap_term()` are defined, then
+ * `get_flow_tap_term()` takes precedence.
+ *
+ * @param keycode Keycode of the tap-hold key.
+ * @param record keyrecord_t of the tap-hold event.
+ * @param prev_keycode Keycode of the previously pressed key.
+ * @return Time in milliseconds.
+ */
+uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode);
+#endif // FLOW_TAP_TERM
+
#ifdef DYNAMIC_TAPPING_TERM_ENABLE
extern uint16_t g_tapping_term;
#endif
diff --git a/quantum/process_keycode/process_leader.c b/quantum/process_keycode/process_leader.c
index ca017a577d..a5466c513c 100644
--- a/quantum/process_keycode/process_leader.c
+++ b/quantum/process_keycode/process_leader.c
@@ -22,11 +22,7 @@ bool process_leader(uint16_t keycode, keyrecord_t *record) {
if (record->event.pressed) {
if (leader_sequence_active() && !leader_sequence_timed_out()) {
#ifndef LEADER_KEY_STRICT_KEY_PROCESSING
- if (IS_QK_MOD_TAP(keycode)) {
- keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
- } else if (IS_QK_LAYER_TAP(keycode)) {
- keycode = QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
- }
+ keycode = get_tap_keycode(keycode);
#endif
if (!leader_sequence_add(keycode)) {
diff --git a/tests/tap_hold_configurations/flow_tap/config.h b/tests/tap_hold_configurations/flow_tap/config.h
new file mode 100644
index 0000000000..a17d488214
--- /dev/null
+++ b/tests/tap_hold_configurations/flow_tap/config.h
@@ -0,0 +1,22 @@
+/* Copyright 2022 Vladislav Kucheriavykh
+ * Copyright 2025 Google LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include "test_common.h"
+
+#define FLOW_TAP_TERM 150
diff --git a/tests/tap_hold_configurations/flow_tap/test.mk b/tests/tap_hold_configurations/flow_tap/test.mk
new file mode 100644
index 0000000000..81ba8da66d
--- /dev/null
+++ b/tests/tap_hold_configurations/flow_tap/test.mk
@@ -0,0 +1,18 @@
+# Copyright 2022 Vladislav Kucheriavykh
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+COMBO_ENABLE = yes
+
+INTROSPECTION_KEYMAP_C = test_keymap.c
diff --git a/tests/tap_hold_configurations/flow_tap/test_keymap.c b/tests/tap_hold_configurations/flow_tap/test_keymap.c
new file mode 100644
index 0000000000..4dfe5e4cb6
--- /dev/null
+++ b/tests/tap_hold_configurations/flow_tap/test_keymap.c
@@ -0,0 +1,23 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "quantum.h"
+
+uint16_t const mt_lt_combo[] = {SFT_T(KC_X), LT(1, KC_Y), COMBO_END};
+
+// clang-format off
+combo_t key_combos[] = {
+ COMBO(mt_lt_combo, KC_Z),
+};
+// clang-format on
diff --git a/tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp b/tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp
new file mode 100644
index 0000000000..7816fcb6da
--- /dev/null
+++ b/tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp
@@ -0,0 +1,313 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "keyboard_report_util.hpp"
+#include "keycode.h"
+#include "test_common.hpp"
+#include "action_tapping.h"
+#include "test_fixture.hpp"
+#include "test_keymap_key.hpp"
+
+using testing::_;
+using testing::AnyNumber;
+using testing::InSequence;
+
+class FlowTapTest : public TestFixture {};
+
+TEST_F(FlowTapTest, short_flow_tap_settled_as_tapped) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_B));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_C));
+
+ set_keymap({regular_key, mod_tap_key1, mod_tap_key2});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap key 1 quickly after regular key. The mod-tap should settle
+ // immediately as tapped, sending `KC_B`.
+ EXPECT_REPORT(driver, (KC_B));
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap key 2 quickly.
+ EXPECT_REPORT(driver, (KC_B, KC_C));
+ mod_tap_key2.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Hold for longer than the tapping term.
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap keys.
+ EXPECT_REPORT(driver, (KC_C));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, long_flow_tap_settled_as_held) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_B));
+
+ set_keymap({regular_key, mod_tap_key});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap key.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ // Hold for the tapping term.
+ EXPECT_REPORT(driver, (KC_LSFT));
+ idle_for(TAPPING_TERM);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap key.
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, holding_multiple_mod_taps) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_B));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_C));
+
+ set_keymap({regular_key, mod_tap_key1, mod_tap_key2});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap keys.
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ mod_tap_key2.press();
+ idle_for(TAPPING_TERM - 5); // Hold almost until tapping term.
+ VERIFY_AND_CLEAR(driver);
+
+ // Press regular key.
+ EXPECT_REPORT(driver, (KC_LSFT));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL, KC_A));
+ regular_key.press();
+ idle_for(10);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release keys.
+ EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
+ EXPECT_REPORT(driver, (KC_LCTL));
+ EXPECT_EMPTY_REPORT(driver);
+ regular_key.release();
+ run_one_scan_loop();
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, layer_tap_key) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_B));
+ auto regular_key2 = KeymapKey(1, 0, 0, KC_C);
+
+ set_keymap({regular_key, layer_tap_key, regular_key2});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press layer-tap key, quickly after the regular key.
+ EXPECT_REPORT(driver, (KC_B));
+ layer_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release layer-tap key.
+ EXPECT_EMPTY_REPORT(driver);
+ layer_tap_key.release();
+ run_one_scan_loop();
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(FLOW_TAP_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press layer-tap key, slowly after the regular key.
+ EXPECT_NO_REPORT(driver);
+ layer_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ EXPECT_EQ(layer_state, 1 << 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Tap regular key2.
+ EXPECT_REPORT(driver, (KC_C));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release layer-tap key.
+ EXPECT_NO_REPORT(driver);
+ layer_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, combo_key) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_X));
+ auto layer_tap_key = KeymapKey(0, 2, 0, LT(1, KC_Y));
+
+ set_keymap({regular_key, mod_tap_key, layer_tap_key});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Press combo keys quickly after regular key.
+ EXPECT_REPORT(driver, (KC_Z));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_combo({mod_tap_key, layer_tap_key});
+ VERIFY_AND_CLEAR(driver);
+
+ // Press mod-tap key quickly.
+ EXPECT_REPORT(driver, (KC_X));
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap key.
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, oneshot_mod_key) {
+ TestDriver driver;
+ InSequence s;
+ auto regular_key = KeymapKey(0, 0, 0, KC_A);
+ auto osm_key = KeymapKey(0, 1, 0, OSM(MOD_LSFT));
+
+ set_keymap({regular_key, osm_key});
+
+ // Tap regular key.
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Tap OSM, tap regular key.
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
+ EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(osm_key);
+ tap_key(regular_key);
+ VERIFY_AND_CLEAR(driver);
+
+ // Nested press of OSM and regular keys.
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
+ EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ osm_key.press();
+ run_one_scan_loop();
+ tap_key(regular_key);
+ osm_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, quick_tap) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_A));
+
+ set_keymap({mod_tap_key});
+
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
+ tap_key(mod_tap_key);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_A));
+ mod_tap_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_NO_REPORT(driver);
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ // Release mod-tap key.
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
--
cgit v1.2.3
From ea85ace4a90baca401e49f35365a6a8f7d3802c4 Mon Sep 17 00:00:00 2001
From: Pascal Getreuer
Date: Sat, 19 Apr 2025 11:57:00 -0700
Subject: Ignore the Layer Lock key in Repeat Key and Caps Word. (#25171)
---
quantum/process_keycode/process_caps_word.c | 7 +++-
quantum/process_keycode/process_repeat_key.c | 5 ++-
tests/caps_word/test.mk | 2 ++
tests/caps_word/test_caps_word.cpp | 52 ++++++++++++++++++++++++----
tests/repeat_key/test.mk | 1 +
tests/repeat_key/test_repeat_key.cpp | 33 ++++++++++++++++++
6 files changed, 91 insertions(+), 9 deletions(-)
(limited to 'quantum/process_keycode')
diff --git a/quantum/process_keycode/process_caps_word.c b/quantum/process_keycode/process_caps_word.c
index b8fb868c6d..8ab66cc521 100644
--- a/quantum/process_keycode/process_caps_word.c
+++ b/quantum/process_keycode/process_caps_word.c
@@ -160,8 +160,13 @@ bool process_caps_word(uint16_t keycode, keyrecord_t* record) {
case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX:
case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX:
case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX:
+#ifdef TRI_LAYER_ENABLE // Ignore Tri Layer keys.
case QK_TRI_LAYER_LOWER ... QK_TRI_LAYER_UPPER:
- // Ignore AltGr.
+#endif // TRI_LAYER_ENABLE
+#ifdef LAYER_LOCK_ENABLE // Ignore Layer Lock key.
+ case QK_LAYER_LOCK:
+#endif // LAYER_LOCK_ENABLE
+ // Ignore AltGr.
case KC_RALT:
case OSM(MOD_RALT):
return true;
diff --git a/quantum/process_keycode/process_repeat_key.c b/quantum/process_keycode/process_repeat_key.c
index 73f4ddedcf..fdeed4f466 100644
--- a/quantum/process_keycode/process_repeat_key.c
+++ b/quantum/process_keycode/process_repeat_key.c
@@ -41,7 +41,10 @@ static bool remember_last_key(uint16_t keycode, keyrecord_t* record, uint8_t* re
#ifdef TRI_LAYER_ENABLE // Ignore Tri Layer keys.
case QK_TRI_LAYER_LOWER:
case QK_TRI_LAYER_UPPER:
-#endif // TRI_LAYER_ENABLE
+#endif // TRI_LAYER_ENABLE
+#ifdef LAYER_LOCK_ENABLE // Ignore Layer Lock key.
+ case QK_LAYER_LOCK:
+#endif // LAYER_LOCK_ENABLE
return false;
// Ignore hold events on tap-hold keys.
diff --git a/tests/caps_word/test.mk b/tests/caps_word/test.mk
index 2509b01858..6d5664aa05 100644
--- a/tests/caps_word/test.mk
+++ b/tests/caps_word/test.mk
@@ -15,5 +15,7 @@
CAPS_WORD_ENABLE = yes
COMMAND_ENABLE = no
+LAYER_LOCK_ENABLE = yes
SPACE_CADET_ENABLE = yes
+TRI_LAYER_ENABLE = yes
diff --git a/tests/caps_word/test_caps_word.cpp b/tests/caps_word/test_caps_word.cpp
index 28d86e9324..4b58790915 100644
--- a/tests/caps_word/test_caps_word.cpp
+++ b/tests/caps_word/test_caps_word.cpp
@@ -156,21 +156,22 @@ TEST_F(CapsWord, IdleTimeout) {
// Turn on Caps Word and tap "A".
caps_word_on();
tap_key(key_a);
-
VERIFY_AND_CLEAR(driver);
+ EXPECT_EMPTY_REPORT(driver);
idle_for(CAPS_WORD_IDLE_TIMEOUT);
run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
// Caps Word should be off and mods should be clear.
EXPECT_EQ(is_caps_word_on(), false);
EXPECT_EQ(get_mods() | get_weak_mods(), 0);
- EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
// Expect unshifted "A".
EXPECT_REPORT(driver, (KC_A));
+ EXPECT_EMPTY_REPORT(driver);
tap_key(key_a);
-
+ run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
@@ -244,6 +245,7 @@ TEST_F(CapsWord, ShiftsAltGrSymbols) {
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(),
+ KeyboardReport(KC_LSFT),
KeyboardReport(KC_RALT),
KeyboardReport(KC_LSFT, KC_RALT))))
.Times(AnyNumber());
@@ -259,6 +261,9 @@ TEST_F(CapsWord, ShiftsAltGrSymbols) {
tap_key(key_a);
run_one_scan_loop();
key_altgr.release();
+ run_one_scan_loop();
+
+ idle_for(CAPS_WORD_IDLE_TIMEOUT);
VERIFY_AND_CLEAR(driver);
}
@@ -274,6 +279,7 @@ TEST_F(CapsWord, ShiftsModTapAltGrSymbols) {
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(),
+ KeyboardReport(KC_LSFT),
KeyboardReport(KC_RALT),
KeyboardReport(KC_LSFT, KC_RALT))))
.Times(AnyNumber());
@@ -289,8 +295,11 @@ TEST_F(CapsWord, ShiftsModTapAltGrSymbols) {
tap_key(key_a);
run_one_scan_loop();
key_altgr_t.release();
-
+ run_one_scan_loop();
EXPECT_TRUE(is_caps_word_on());
+
+ idle_for(CAPS_WORD_IDLE_TIMEOUT);
+
VERIFY_AND_CLEAR(driver);
}
@@ -535,7 +544,11 @@ TEST_P(CapsWordDoubleTapShift, Activation) {
// machine at this point. This due to imperfect test isolation which can't
// reset the caps word double shift timer on test case setup.
idle_for(CAPS_WORD_IDLE_TIMEOUT);
+
+ EXPECT_REPORT(driver, (KC_ESC));
+ EXPECT_EMPTY_REPORT(driver);
tap_key(esc);
+ VERIFY_AND_CLEAR(driver);
}
// Double tap doesn't count if another key is pressed between the taps.
@@ -589,6 +602,7 @@ TEST_P(CapsWordDoubleTapShift, SlowTaps) {
EXPECT_EQ(is_caps_word_on(), false); // Caps Word is still off.
clear_oneshot_mods();
+ send_keyboard_report();
VERIFY_AND_CLEAR(driver);
}
@@ -626,7 +640,7 @@ TEST_F(CapsWord, IgnoresOSLHold) {
run_one_scan_loop();
tap_key(key_b);
key_osl.release();
- run_one_scan_loop();
+ idle_for(CAPS_WORD_IDLE_TIMEOUT + 1);
VERIFY_AND_CLEAR(driver);
}
@@ -645,15 +659,39 @@ TEST_F(CapsWord, IgnoresOSLTap) {
KeyboardReport(),
KeyboardReport(KC_LSFT))))
.Times(AnyNumber());
+ // clang-format on
EXPECT_REPORT(driver, (KC_LSFT, KC_B));
caps_word_on();
tap_key(key_osl);
tap_key(key_b);
- run_one_scan_loop();
+ idle_for(CAPS_WORD_IDLE_TIMEOUT);
+
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(CapsWord, IgnoresLayerLockKey) {
+ TestDriver driver;
+ KeymapKey key_llock(0, 1, 0, QK_LAYER_LOCK);
+ KeymapKey key_b(0, 0, 0, KC_B);
+ set_keymap({key_llock, key_b});
+
+ // Allow any number of reports with no keys or only modifiers.
+ // clang-format off
+ EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+ KeyboardReport(),
+ KeyboardReport(KC_LSFT))))
+ .Times(AnyNumber());
+ // clang-format on
+
+ EXPECT_REPORT(driver, (KC_LSFT, KC_B));
+ caps_word_on();
+
+ tap_key(key_llock);
+ tap_key(key_b);
+ idle_for(CAPS_WORD_IDLE_TIMEOUT);
VERIFY_AND_CLEAR(driver);
}
-// clang-format on
} // namespace
diff --git a/tests/repeat_key/test.mk b/tests/repeat_key/test.mk
index aec8ff3bfb..186207ffc2 100644
--- a/tests/repeat_key/test.mk
+++ b/tests/repeat_key/test.mk
@@ -16,3 +16,4 @@
REPEAT_KEY_ENABLE = yes
AUTO_SHIFT_ENABLE = yes
+LAYER_LOCK_ENABLE = yes
diff --git a/tests/repeat_key/test_repeat_key.cpp b/tests/repeat_key/test_repeat_key.cpp
index eee44fc104..ed5d618761 100644
--- a/tests/repeat_key/test_repeat_key.cpp
+++ b/tests/repeat_key/test_repeat_key.cpp
@@ -751,4 +751,37 @@ TEST_F(RepeatKey, RepeatKeyInvoke) {
testing::Mock::VerifyAndClearExpectations(&driver);
}
+// Check that mods and Layer Lock are not remembered.
+TEST_F(RepeatKey, IgnoredKeys) {
+ TestDriver driver;
+ KeymapKey regular_key(0, 0, 0, KC_A);
+ KeymapKey key_repeat(0, 1, 0, QK_REP);
+ KeymapKey key_lsft(0, 2, 0, KC_LSFT);
+ KeymapKey key_lctl(0, 3, 0, KC_LCTL);
+ KeymapKey key_llck(0, 4, 0, QK_LAYER_LOCK);
+ set_keymap({regular_key, key_repeat, key_lsft, key_lctl, key_llck});
+
+ // Allow any number of empty reports.
+ EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
+ {
+ InSequence seq;
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_LSFT));
+ EXPECT_REPORT(driver, (KC_LCTL));
+ EXPECT_REPORT(driver, (KC_A));
+ EXPECT_REPORT(driver, (KC_A));
+ }
+
+ tap_key(regular_key); // Taps the KC_A key.
+
+ // Tap Shift, Ctrl, and Layer Lock keys, which should not be remembered.
+ tap_keys(key_lsft, key_lctl, key_llck);
+ EXPECT_KEYCODE_EQ(get_last_keycode(), KC_A);
+
+ // Tapping the Repeat Key should still reproduce KC_A.
+ tap_keys(key_repeat, key_repeat);
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
} // namespace
--
cgit v1.2.3
From c7cb7ba9765b35930a26ec247e362615ffd10ed2 Mon Sep 17 00:00:00 2001
From: Joel Challis
Date: Mon, 21 Apr 2025 22:27:56 +0100
Subject: Implement connection keycode logic (#25176)
---
builddefs/common_features.mk | 2 +-
builddefs/generic_features.mk | 1 +
drivers/bluetooth/outputselect.c | 70 -----------
drivers/bluetooth/outputselect.h | 24 ++--
quantum/connection/connection.c | 147 ++++++++++++++++++++++
quantum/connection/connection.h | 110 ++++++++++++++++
quantum/eeconfig.c | 18 +++
quantum/eeconfig.h | 6 +
quantum/keyboard.c | 6 +
quantum/nvm/eeprom/nvm_eeconfig.c | 13 ++
quantum/nvm/eeprom/nvm_eeprom_eeconfig_internal.h | 2 +
quantum/nvm/nvm_eeconfig.h | 6 +
quantum/process_keycode/process_connection.c | 24 ++--
quantum/quantum.c | 4 +-
tmk_core/protocol/host.c | 11 +-
15 files changed, 346 insertions(+), 98 deletions(-)
delete mode 100644 drivers/bluetooth/outputselect.c
create mode 100644 quantum/connection/connection.c
create mode 100644 quantum/connection/connection.h
(limited to 'quantum/process_keycode')
diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk
index ec856715b0..0b9840a14e 100644
--- a/builddefs/common_features.mk
+++ b/builddefs/common_features.mk
@@ -892,8 +892,8 @@ ifeq ($(strip $(BLUETOOTH_ENABLE)), yes)
OPT_DEFS += -DBLUETOOTH_ENABLE
OPT_DEFS += -DBLUETOOTH_$(strip $(shell echo $(BLUETOOTH_DRIVER) | tr '[:lower:]' '[:upper:]'))
NO_USB_STARTUP_CHECK := yes
+ CONNECTION_ENABLE := yes
COMMON_VPATH += $(DRIVER_PATH)/bluetooth
- SRC += outputselect.c process_connection.c
ifeq ($(strip $(BLUETOOTH_DRIVER)), bluefruit_le)
SPI_DRIVER_REQUIRED = yes
diff --git a/builddefs/generic_features.mk b/builddefs/generic_features.mk
index d39727f23b..c826514431 100644
--- a/builddefs/generic_features.mk
+++ b/builddefs/generic_features.mk
@@ -25,6 +25,7 @@ GENERIC_FEATURES = \
CAPS_WORD \
COMBO \
COMMAND \
+ CONNECTION \
CRC \
DEFERRED_EXEC \
DIGITIZER \
diff --git a/drivers/bluetooth/outputselect.c b/drivers/bluetooth/outputselect.c
deleted file mode 100644
index b986ba274e..0000000000
--- a/drivers/bluetooth/outputselect.c
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
-Copyright 2017 Priyadi Iman Nurcahyo
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 2 of the License, or
-(at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-#include "outputselect.h"
-#include "usb_util.h"
-
-#ifdef BLUETOOTH_BLUEFRUIT_LE
-# include "bluefruit_le.h"
-#endif
-
-uint8_t desired_output = OUTPUT_DEFAULT;
-
-/** \brief Set Output
- *
- * FIXME: Needs doc
- */
-void set_output(uint8_t output) {
- set_output_user(output);
- desired_output = output;
-}
-
-/** \brief Set Output User
- *
- * FIXME: Needs doc
- */
-__attribute__((weak)) void set_output_user(uint8_t output) {}
-
-/** \brief Auto Detect Output
- *
- * FIXME: Needs doc
- */
-uint8_t auto_detect_output(void) {
- if (usb_connected_state()) {
- return OUTPUT_USB;
- }
-
-#ifdef BLUETOOTH_BLUEFRUIT_LE
- if (bluefruit_le_is_connected()) {
- return OUTPUT_BLUETOOTH;
- }
-#endif
-
-#ifdef BLUETOOTH_ENABLE
- return OUTPUT_BLUETOOTH; // should check if BT is connected here
-#endif
-
- return OUTPUT_NONE;
-}
-
-/** \brief Where To Send
- *
- * FIXME: Needs doc
- */
-uint8_t where_to_send(void) {
- if (desired_output == OUTPUT_AUTO) {
- return auto_detect_output();
- }
- return desired_output;
-}
diff --git a/drivers/bluetooth/outputselect.h b/drivers/bluetooth/outputselect.h
index c4548e1122..25f063bbff 100644
--- a/drivers/bluetooth/outputselect.h
+++ b/drivers/bluetooth/outputselect.h
@@ -14,21 +14,17 @@ along with this program. If not, see .
#pragma once
-#include
+#include "connection.h"
-enum outputs {
- OUTPUT_AUTO,
+// DEPRECATED - DO NOT USE
- OUTPUT_NONE,
- OUTPUT_USB,
- OUTPUT_BLUETOOTH
-};
+#define OUTPUT_AUTO CONNECTION_HOST_AUTO
+#define OUTPUT_NONE CONNECTION_HOST_NONE
+#define OUTPUT_USB CONNECTION_HOST_USB
+#define OUTPUT_BLUETOOTH CONNECTION_HOST_BLUETOOTH
-#ifndef OUTPUT_DEFAULT
-# define OUTPUT_DEFAULT OUTPUT_AUTO
-#endif
+#define set_output connection_set_host_noeeprom
+#define where_to_send connection_get_host
+#define auto_detect_output connection_auto_detect_host
-void set_output(uint8_t output);
-void set_output_user(uint8_t output);
-uint8_t auto_detect_output(void);
-uint8_t where_to_send(void);
+void set_output_user(uint8_t output);
diff --git a/quantum/connection/connection.c b/quantum/connection/connection.c
new file mode 100644
index 0000000000..c7f3c4b424
--- /dev/null
+++ b/quantum/connection/connection.c
@@ -0,0 +1,147 @@
+// Copyright 2025 QMK
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "connection.h"
+#include "eeconfig.h"
+#include "usb_util.h"
+#include "util.h"
+
+// ======== DEPRECATED DEFINES - DO NOT USE ========
+#ifdef OUTPUT_DEFAULT
+# undef CONNECTION_HOST_DEFAULT
+# define CONNECTION_HOST_DEFAULT OUTPUT_DEFAULT
+#endif
+
+__attribute__((weak)) void set_output_user(uint8_t output) {}
+// ========
+
+#ifdef BLUETOOTH_ENABLE
+# ifdef BLUETOOTH_BLUEFRUIT_LE
+# include "bluefruit_le.h"
+# define bluetooth_is_connected() bluefruit_le_is_connected()
+# else
+// TODO: drivers should check if BT is connected here
+# define bluetooth_is_connected() true
+# endif
+#endif
+
+#define CONNECTION_HOST_INVALID 0xFF
+
+#ifndef CONNECTION_HOST_DEFAULT
+# define CONNECTION_HOST_DEFAULT CONNECTION_HOST_AUTO
+#endif
+
+static const connection_host_t host_candidates[] = {
+ CONNECTION_HOST_AUTO,
+ CONNECTION_HOST_USB,
+#ifdef BLUETOOTH_ENABLE
+ CONNECTION_HOST_BLUETOOTH,
+#endif
+#if 0
+ CONNECTION_HOST_2P4GHZ,
+#endif
+};
+
+#define HOST_CANDIDATES_COUNT ARRAY_SIZE(host_candidates)
+
+static connection_config_t config = {.desired_host = CONNECTION_HOST_INVALID};
+
+void eeconfig_update_connection_default(void) {
+ config.desired_host = CONNECTION_HOST_DEFAULT;
+
+ eeconfig_update_connection(&config);
+}
+
+void connection_init(void) {
+ eeconfig_read_connection(&config);
+ if (config.desired_host == CONNECTION_HOST_INVALID) {
+ eeconfig_update_connection_default();
+ }
+}
+
+__attribute__((weak)) void connection_host_changed_user(connection_host_t host) {}
+__attribute__((weak)) void connection_host_changed_kb(connection_host_t host) {}
+
+static void handle_host_changed(void) {
+ connection_host_changed_user(config.desired_host);
+ connection_host_changed_kb(config.desired_host);
+
+ // TODO: Remove deprecated callback
+ set_output_user(config.desired_host);
+}
+
+void connection_set_host_noeeprom(connection_host_t host) {
+ if (config.desired_host == host) {
+ return;
+ }
+
+ config.desired_host = host;
+
+ handle_host_changed();
+}
+
+void connection_set_host(connection_host_t host) {
+ connection_set_host_noeeprom(host);
+
+ eeconfig_update_connection(&config);
+}
+
+void connection_next_host_noeeprom(void) {
+ uint8_t next = 0;
+ for (uint8_t i = 0; i < HOST_CANDIDATES_COUNT; i++) {
+ if (host_candidates[i] == config.desired_host) {
+ next = i == HOST_CANDIDATES_COUNT - 1 ? 0 : i + 1;
+ break;
+ }
+ }
+
+ connection_set_host_noeeprom(host_candidates[next]);
+}
+
+void connection_next_host(void) {
+ connection_next_host_noeeprom();
+
+ eeconfig_update_connection(&config);
+}
+
+void connection_prev_host_noeeprom(void) {
+ uint8_t next = 0;
+ for (uint8_t i = 0; i < HOST_CANDIDATES_COUNT; i++) {
+ if (host_candidates[i] == config.desired_host) {
+ next = i == 0 ? HOST_CANDIDATES_COUNT - 1 : i - 1;
+ break;
+ }
+ }
+
+ connection_set_host_noeeprom(host_candidates[next]);
+}
+
+void connection_prev_host(void) {
+ connection_prev_host_noeeprom();
+
+ eeconfig_update_connection(&config);
+}
+
+connection_host_t connection_get_host_raw(void) {
+ return config.desired_host;
+}
+
+connection_host_t connection_auto_detect_host(void) {
+ if (usb_connected_state()) {
+ return CONNECTION_HOST_USB;
+ }
+
+#ifdef BLUETOOTH_ENABLE
+ if (bluetooth_is_connected()) {
+ return CONNECTION_HOST_BLUETOOTH;
+ }
+#endif
+
+ return CONNECTION_HOST_NONE;
+}
+
+connection_host_t connection_get_host(void) {
+ if (config.desired_host == CONNECTION_HOST_AUTO) {
+ return connection_auto_detect_host();
+ }
+ return config.desired_host;
+}
diff --git a/quantum/connection/connection.h b/quantum/connection/connection.h
new file mode 100644
index 0000000000..e403141fae
--- /dev/null
+++ b/quantum/connection/connection.h
@@ -0,0 +1,110 @@
+// Copyright 2025 QMK
+// SPDX-License-Identifier: GPL-2.0-or-later
+#pragma once
+
+#include
+#include "util.h"
+
+/**
+ * \enum connection_host_t
+ *
+ * An enumeration of the possible hosts.
+ */
+typedef enum connection_host_t {
+ CONNECTION_HOST_AUTO,
+
+ CONNECTION_HOST_NONE,
+ CONNECTION_HOST_USB,
+ CONNECTION_HOST_BLUETOOTH,
+ CONNECTION_HOST_2P4GHZ
+} connection_host_t;
+
+/**
+ * \union connection_config_t
+ *
+ * Configuration structure for the connection subsystem.
+ */
+typedef union connection_config_t {
+ uint8_t raw;
+ connection_host_t desired_host : 8;
+} PACKED connection_config_t;
+
+_Static_assert(sizeof(connection_config_t) == sizeof(uint8_t), "Connection EECONFIG out of spec.");
+
+/**
+ * \brief Initialize the subsystem.
+ *
+ * This function must be called only once, before any of the below functions can be called.
+ */
+void connection_init(void);
+
+/**
+ * \brief Get currently configured host. Does not resolve 'CONNECTION_HOST_AUTO'.
+ *
+ * \return 'connection_host_t' of the configured host.
+ */
+connection_host_t connection_get_host_raw(void);
+
+/**
+ * \brief Get current active host.
+ *
+ * \return 'connection_host_t' of the configured host.
+ */
+connection_host_t connection_auto_detect_host(void);
+
+/**
+ * \brief Get currently configured host. Resolves 'CONNECTION_HOST_AUTO' using 'connection_auto_detect_host()'.
+ *
+ * \return 'connection_host_t' of the configured host.
+ */
+connection_host_t connection_get_host(void);
+
+/**
+ * \brief Get current host. New state is not written to EEPROM.
+ *
+ * \param host The host to configure.
+ */
+void connection_set_host_noeeprom(connection_host_t host);
+
+/**
+ * \brief Get current host.
+ *
+ * \param host The host to configure.
+ */
+void connection_set_host(connection_host_t host);
+
+/**
+ * \brief Move to the next potential host. New state is not written to EEPROM.
+ *
+ */
+void connection_next_host_noeeprom(void);
+
+/**
+ * \brief Move to the next potential host.
+ *
+ */
+void connection_next_host(void);
+
+/**
+ * \brief Move to the previous potential host. New state is not written to EEPROM.
+ *
+ */
+void connection_prev_host_noeeprom(void);
+
+/**
+ * \brief Move to the previous potential host.
+ *
+ */
+void connection_prev_host(void);
+
+/**
+ * \brief user hook called when changing configured host
+ *
+ */
+void connection_host_changed_user(connection_host_t host);
+
+/**
+ * \brief keyboard hook called when changing configured host
+ *
+ */
+void connection_host_changed_kb(connection_host_t host);
diff --git a/quantum/eeconfig.c b/quantum/eeconfig.c
index addc07ae53..1e8cfd758a 100644
--- a/quantum/eeconfig.c
+++ b/quantum/eeconfig.c
@@ -35,6 +35,10 @@
# include "haptic.h"
#endif // HAPTIC_ENABLE
+#ifdef CONNECTION_ENABLE
+# include "connection.h"
+#endif // CONNECTION_ENABLE
+
#ifdef VIA_ENABLE
bool via_eeprom_is_valid(void);
void via_eeprom_set_valid(bool valid);
@@ -127,6 +131,11 @@ void eeconfig_init_quantum(void) {
haptic_reset();
#endif // HAPTIC_ENABLE
+#ifdef CONNECTION_ENABLE
+ extern void eeconfig_update_connection_default(void);
+ eeconfig_update_connection_default();
+#endif // CONNECTION_ENABLE
+
#if (EECONFIG_KB_DATA_SIZE) > 0
eeconfig_init_kb_datablock();
#endif // (EECONFIG_KB_DATA_SIZE) > 0
@@ -299,6 +308,15 @@ void eeconfig_update_haptic(const haptic_config_t *haptic_config) {
}
#endif // HAPTIC_ENABLE
+#ifdef CONNECTION_ENABLE
+void eeconfig_read_connection(connection_config_t *config) {
+ nvm_eeconfig_read_connection(config);
+}
+void eeconfig_update_connection(const connection_config_t *config) {
+ nvm_eeconfig_update_connection(config);
+}
+#endif // CONNECTION_ENABLE
+
bool eeconfig_read_handedness(void) {
return nvm_eeconfig_read_handedness();
}
diff --git a/quantum/eeconfig.h b/quantum/eeconfig.h
index 4044f1c294..d4d8d957be 100644
--- a/quantum/eeconfig.h
+++ b/quantum/eeconfig.h
@@ -131,6 +131,12 @@ void eeconfig_read_haptic(haptic_config_t *haptic_confi
void eeconfig_update_haptic(const haptic_config_t *haptic_config) __attribute__((nonnull));
#endif
+#ifdef CONNECTION_ENABLE
+typedef union connection_config_t connection_config_t;
+void eeconfig_read_connection(connection_config_t *config);
+void eeconfig_update_connection(const connection_config_t *config);
+#endif
+
bool eeconfig_read_handedness(void);
void eeconfig_update_handedness(bool val);
diff --git a/quantum/keyboard.c b/quantum/keyboard.c
index 0671b0461f..be51190a87 100644
--- a/quantum/keyboard.c
+++ b/quantum/keyboard.c
@@ -146,6 +146,9 @@ along with this program. If not, see .
#ifdef LAYER_LOCK_ENABLE
# include "layer_lock.h"
#endif
+#ifdef CONNECTION_ENABLE
+# include "connection.h"
+#endif
static uint32_t last_input_modification_time = 0;
uint32_t last_input_activity_time(void) {
@@ -465,6 +468,9 @@ void keyboard_init(void) {
#endif
matrix_init();
quantum_init();
+#ifdef CONNECTION_ENABLE
+ connection_init();
+#endif
led_init_ports();
#ifdef BACKLIGHT_ENABLE
backlight_init_ports();
diff --git a/quantum/nvm/eeprom/nvm_eeconfig.c b/quantum/nvm/eeprom/nvm_eeconfig.c
index d6c388f3bc..d9495d2753 100644
--- a/quantum/nvm/eeprom/nvm_eeconfig.c
+++ b/quantum/nvm/eeprom/nvm_eeconfig.c
@@ -41,6 +41,10 @@
# include "haptic.h"
#endif
+#ifdef CONNECTION_ENABLE
+# include "connection.h"
+#endif
+
void nvm_eeconfig_erase(void) {
#ifdef EEPROM_DRIVER
eeprom_driver_format(false);
@@ -196,6 +200,15 @@ void nvm_eeconfig_update_haptic(const haptic_config_t *haptic_config) {
}
#endif // HAPTIC_ENABLE
+#ifdef CONNECTION_ENABLE
+void nvm_eeconfig_read_connection(connection_config_t *config) {
+ config->raw = eeprom_read_byte(EECONFIG_CONNECTION);
+}
+void nvm_eeconfig_update_connection(const connection_config_t *config) {
+ eeprom_update_byte(EECONFIG_CONNECTION, config->raw);
+}
+#endif // CONNECTION_ENABLE
+
bool nvm_eeconfig_read_handedness(void) {
return !!eeprom_read_byte(EECONFIG_HANDEDNESS);
}
diff --git a/quantum/nvm/eeprom/nvm_eeprom_eeconfig_internal.h b/quantum/nvm/eeprom/nvm_eeprom_eeconfig_internal.h
index 6efbf9480b..41b76f1f65 100644
--- a/quantum/nvm/eeprom/nvm_eeprom_eeconfig_internal.h
+++ b/quantum/nvm/eeprom/nvm_eeprom_eeconfig_internal.h
@@ -27,6 +27,7 @@ typedef struct PACKED {
};
uint32_t haptic;
uint8_t rgblight_ext;
+ uint8_t connection;
} eeprom_core_t;
/* EEPROM parameter address */
@@ -46,6 +47,7 @@ typedef struct PACKED {
#define EECONFIG_RGB_MATRIX (uint64_t *)(offsetof(eeprom_core_t, rgb_matrix))
#define EECONFIG_HAPTIC (uint32_t *)(offsetof(eeprom_core_t, haptic))
#define EECONFIG_RGBLIGHT_EXTENDED (uint8_t *)(offsetof(eeprom_core_t, rgblight_ext))
+#define EECONFIG_CONNECTION (uint8_t *)(offsetof(eeprom_core_t, connection))
// Size of EEPROM being used for core data storage
#define EECONFIG_BASE_SIZE ((uint8_t)sizeof(eeprom_core_t))
diff --git a/quantum/nvm/nvm_eeconfig.h b/quantum/nvm/nvm_eeconfig.h
index 131f61d534..40827361ca 100644
--- a/quantum/nvm/nvm_eeconfig.h
+++ b/quantum/nvm/nvm_eeconfig.h
@@ -87,6 +87,12 @@ void nvm_eeconfig_read_haptic(haptic_config_t *haptic_c
void nvm_eeconfig_update_haptic(const haptic_config_t *haptic_config);
#endif // HAPTIC_ENABLE
+#ifdef CONNECTION_ENABLE
+typedef union connection_config_t connection_config_t;
+void nvm_eeconfig_read_connection(connection_config_t *config);
+void nvm_eeconfig_update_connection(const connection_config_t *config);
+#endif // CONNECTION_ENABLE
+
bool nvm_eeconfig_read_handedness(void);
void nvm_eeconfig_update_handedness(bool val);
diff --git a/quantum/process_keycode/process_connection.c b/quantum/process_keycode/process_connection.c
index b0e230d680..501529ede7 100644
--- a/quantum/process_keycode/process_connection.c
+++ b/quantum/process_keycode/process_connection.c
@@ -1,24 +1,34 @@
// Copyright 2024 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
-#include "outputselect.h"
+#include "connection.h"
#include "process_connection.h"
bool process_connection(uint16_t keycode, keyrecord_t *record) {
if (record->event.pressed) {
switch (keycode) {
case QK_OUTPUT_NEXT:
- set_output(OUTPUT_AUTO); // This should cycle through the outputs going forward. Ensure `docs/keycodes.md`, `docs/features/bluetooth.md` are updated when it does.
+ connection_next_host();
+ return false;
+ case QK_OUTPUT_PREV:
+ connection_prev_host();
+ return false;
+
+ case QK_OUTPUT_AUTO:
+ connection_set_host(CONNECTION_HOST_AUTO);
+ return false;
+ case QK_OUTPUT_NONE:
+ connection_set_host(CONNECTION_HOST_NONE);
return false;
case QK_OUTPUT_USB:
- set_output(OUTPUT_USB);
+ connection_set_host(CONNECTION_HOST_USB);
return false;
case QK_OUTPUT_BLUETOOTH:
- set_output(OUTPUT_BLUETOOTH);
+ connection_set_host(CONNECTION_HOST_BLUETOOTH);
return false;
-
- case QK_OUTPUT_PREV:
- case QK_OUTPUT_NONE:
case QK_OUTPUT_2P4GHZ:
+ connection_set_host(CONNECTION_HOST_2P4GHZ);
+ return false;
+
case QK_BLUETOOTH_PROFILE_NEXT:
case QK_BLUETOOTH_PROFILE_PREV:
case QK_BLUETOOTH_UNPAIR:
diff --git a/quantum/quantum.c b/quantum/quantum.c
index adb14d64b6..0bb6ee0a91 100644
--- a/quantum/quantum.c
+++ b/quantum/quantum.c
@@ -20,7 +20,7 @@
# include "process_backlight.h"
#endif
-#ifdef BLUETOOTH_ENABLE
+#ifdef CONNECTION_ENABLE
# include "process_connection.h"
#endif
@@ -436,7 +436,7 @@ bool process_record_quantum(keyrecord_t *record) {
#ifdef LAYER_LOCK_ENABLE
process_layer_lock(keycode, record) &&
#endif
-#ifdef BLUETOOTH_ENABLE
+#ifdef CONNECTION_ENABLE
process_connection(keycode, record) &&
#endif
true)) {
diff --git a/tmk_core/protocol/host.c b/tmk_core/protocol/host.c
index df805c827c..453952049f 100644
--- a/tmk_core/protocol/host.c
+++ b/tmk_core/protocol/host.c
@@ -31,8 +31,11 @@ along with this program. If not, see .
#endif
#ifdef BLUETOOTH_ENABLE
+# ifndef CONNECTION_ENABLE
+# error CONNECTION_ENABLE required and not enabled
+# endif
+# include "connection.h"
# include "bluetooth.h"
-# include "outputselect.h"
#endif
#ifdef NKRO_ENABLE
@@ -74,7 +77,7 @@ led_t host_keyboard_led_state(void) {
/* send report */
void host_keyboard_send(report_keyboard_t *report) {
#ifdef BLUETOOTH_ENABLE
- if (where_to_send() == OUTPUT_BLUETOOTH) {
+ if (connection_get_host() == CONNECTION_HOST_BLUETOOTH) {
bluetooth_send_keyboard(report);
return;
}
@@ -111,7 +114,7 @@ void host_nkro_send(report_nkro_t *report) {
void host_mouse_send(report_mouse_t *report) {
#ifdef BLUETOOTH_ENABLE
- if (where_to_send() == OUTPUT_BLUETOOTH) {
+ if (connection_get_host() == CONNECTION_HOST_BLUETOOTH) {
bluetooth_send_mouse(report);
return;
}
@@ -147,7 +150,7 @@ void host_consumer_send(uint16_t usage) {
last_consumer_usage = usage;
#ifdef BLUETOOTH_ENABLE
- if (where_to_send() == OUTPUT_BLUETOOTH) {
+ if (connection_get_host() == CONNECTION_HOST_BLUETOOTH) {
bluetooth_send_consumer(usage);
return;
}
--
cgit v1.2.3