This commit is contained in:
Christoph Cullmann 2025-03-18 22:27:19 +01:00
parent 4dc5811f00
commit ee0a3c3be0
No known key found for this signature in database
61 changed files with 5743 additions and 0 deletions

View file

@ -0,0 +1,57 @@
# Tap Flow
<table>
<tr><td><b>Module</b></td><td><tt>getreuer/tap_flow</tt></td></tr>
<tr><td><b>Version</b></td><td>2025-03-15</td></tr>
<tr><td><b>Maintainer</b></td><td>Pascal Getreuer (@getreuer)</td></tr>
<tr><td><b>License</b></td><td><a href="../LICENSE.txt">Apache 2.0</a></td></tr>
<tr><td><b>Documentation</b></td><td>
<a href="https://getreuer.info/posts/keyboards/tap-flow">https://getreuer.info/posts/keyboards/tap-flow</a>
</td></tr>
</table>
This module is an implementation of "global quick tap" (GQT), aka "require
priori idle," for tap-hold keys. It is particularly useful for home row mods to
avoid accidental mod triggers in fast typing.
To use this module, add the following to your `keymap.json`:
```json
{
"modules": ["getreuer/tap_flow"]
}
```
Tap Flow's term can be tuned on the fly with the following keycodes:
| Keycode | Alias | Description |
|-------------------|-----------|-----------------------------------|
| `TAP_FLOW_PRINT` | `TFLOW_P` | Type the current value. |
| `TAP_FLOW_UP` | `TFLOW_U` | Increase by 5&nbsp;ms. |
| `TAP_FLOW_DOWN` | `TFLOW_D` | Decrease by 5&nbsp;ms. |
Tap Flow's default behavior is:
* Filtering is done only when a tap-hold press is within `TAP_FLOW_TERM` of the
previous key event, which defaults to 150&nbsp;ms. Use `TFLOW_U` / `TFLOW_D`
to tune, then define `TAP_FLOW_TERM` in your `config.h` to set the value
printed by `TFLOW_P`.
* Filtering is done only when both the tap-hold key and the previous key are
among <kbd>Space</kbd>, letters <kbd>A</kbd>&ndash;<kbd>Z</kbd>, and
punctuations <kbd>,</kbd> <kbd>.</kbd> <kbd>;</kbd> <kbd>/</kbd>. Define the
`get_tap_flow()` callback to customize this logic.
Tap Flow modifies the tap-hold decision such that when a tap-hold key is pressed
within a short timeout of the preceding key, the tapping function is used. The
assumption is that during fast typing, only the tap function of tap-hold keys is
desired (though perhaps with an exception for Shift or AltGr, noted below),
whereas the hold functions (mods and layers) are used in isolation, or at least
with a brief pause preceding the tap-hold key press.
Optionally, the feature can be customized with the `get_tap_flow()` callback. In
this way, exceptions may be made for Shift and AltGr (or whatever you wish) to
use a shorter time or to disable filtering for those keys entirely.
For full documentation, see
<https://getreuer.info/posts/keyboards/tap-flow>

View file

@ -0,0 +1,17 @@
// 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.
#pragma once
#include "tap_flow.h"

View file

@ -0,0 +1,24 @@
{
"module_name": "Tap Flow",
"maintainer": "getreuer",
"keycodes": [
{
"key": "TAP_FLOW_PRINT",
"aliases": [
"TFLOW_P"
]
},
{
"key": "TAP_FLOW_UP",
"aliases": [
"TFLOW_U"
]
},
{
"key": "TAP_FLOW_DOWN",
"aliases": [
"TFLOW_D"
]
}
]
}

View file

@ -0,0 +1,205 @@
// 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.
/**
* @file tap_flow.c
* @brief Tap Flow module implementation
*
* For full documentation, see
* <https://getreuer.info/posts/keyboards/tap-flow>
*/
#include "tap_flow.h"
ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
// Either the Combos feature or Repeat Key (or both) need to be enabled. Either
// of these features enables the .keycode field in keyrecord_t, which the
// tap_flow implementation relies on.
#if !defined(COMBO_ENABLE) && !defined(REPEAT_KEY_ENABLE)
#error "tap_flow: Please enable Combos (COMBO_ENABLE = true) or Repeat Key (REPEAT_KEY_ENABLE = yes), or both, in rules.mk."
#else
uint16_t g_tap_flow_term = TAP_FLOW_TERM;
static uint16_t prev_keycode = KC_NO;
// Events bypass tap_flow when there are unsettled LT keys in action_tapping's
// waiting_queue. Particularly, supposing an LT settles as held, the layer state
// will change and buffered events following the LT will be reconsidered as keys
// on that layer. That may change whether tap_flow is enabled or the timeout
// to use on those keys. We don't know in advance how the LT will settle.
static uint16_t settle_timer = 0;
static uint8_t is_tapped[(MATRIX_ROWS * MATRIX_COLS + 7) / 8] = {0};
static 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);
#ifndef NO_ACTION_LAYER
case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
return QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
#endif // NO_ACTION_LAYER
}
return keycode;
}
#ifdef COMBO_ENABLE
#include "keymap_introspection.h"
static bool is_combo_key(uint16_t keycode) {
for (uint16_t i = 0; i < combo_count(); ++i) {
const uint16_t* keys = combo_get(i)->keys;
uint16_t key;
do {
key = pgm_read_word(keys++);
if (key == keycode) { return true; }
} while (key != COMBO_END);
}
return false;
}
#else
#define is_combo_key(keycode) false
#endif // COMBO_ENABLE
void housekeeping_task_tap_flow(void) {
if (settle_timer && timer_expired(timer_read(), settle_timer)) {
#ifdef TAP_FLOW_DEBUG
dprintf("tap_flow: settled.\n");
#endif // TAP_FLOW_DEBUG
settle_timer = 0;
}
}
bool pre_process_record_tap_flow(uint16_t keycode, keyrecord_t* record) {
const keypos_t pos = record->event.key;
if (IS_KEYEVENT(record->event) && pos.row < MATRIX_ROWS
&& pos.col < MATRIX_COLS &&
(IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode))) {
// The event is on an MT or LT with a valid matrix position.
const uint16_t tap_keycode = get_tap_keycode(keycode);
// Determine the key's index in the bit arrays.
const uint16_t index = pos.row * MATRIX_COLS + pos.col;
const uint16_t array_index = index / 8;
const uint8_t bit_mask = UINT8_C(1) << (index % 8);
if (record->event.pressed) { // On press.
const uint32_t idle_time = last_input_activity_elapsed();
uint16_t tap_flow_term = get_tap_flow(keycode, record, prev_keycode);
if (tap_flow_term > 500) {
tap_flow_term = 500;
}
if (!settle_timer && !is_combo_key(keycode) &&
idle_time < 500 && idle_time < tap_flow_term) {
#ifdef TAP_FLOW_DEBUG
dprintf("tap_flow: %02x%02xd within term (%u < %u) converted to tap.\n",
pos.row, pos.col, (uint16_t)idle_time, tap_flow_term);
#endif // TAP_FLOW_DEBUG
// Rewrite the event as a press of the tap keycode. This way, it
// bypasses the usual action_tapping logic.
record->keycode = tap_keycode;
// Record this key as tapped.
is_tapped[array_index] |= bit_mask;
} else {
// Otherwise if this is an LT key, track when it will settle according
// to its tapping term.
// NOTE: To be precise, the key could settle before the tapping term.
// This is an approximation.
#ifdef TAP_FLOW_DEBUG
if (settle_timer) {
dprintf("tap_flow: %02x%02xd unchanged (unsettled state).\n",
pos.row, pos.col);
} else if (is_combo_key(keycode)) {
dprintf("tap_flow: %02x%02xd unchanged (combo key).\n",
pos.row, pos.col);
} else {
dprintf("tap_flow: %02x%02xd unchanged (outside time).\n",
pos.row, pos.col);
}
#endif // TAP_FLOW_DEBUG
if (IS_QK_LAYER_TAP(keycode)) {
const uint16_t term = GET_TAPPING_TERM(keycode, record);
const uint16_t now = timer_read();
if (!settle_timer || term > TIMER_DIFF_16(settle_timer, now)) {
settle_timer = (now + term) | 1;
}
}
}
} else if ((is_tapped[array_index] & bit_mask) != 0) { // On tap release.
#ifdef TAP_FLOW_DEBUG
dprintf("tap_flow: %02x%02xu tap release.\n", pos.row, pos.col);
#endif // TAP_FLOW_DEBUG
// Rewrite the event as a release of the tap keycode.
record->keycode = tap_keycode;
// Record the key as released.
is_tapped[array_index] &= ~bit_mask;
}
}
if (record->event.pressed) { // Track the previous key press.
prev_keycode = keycode;
}
return true;
}
bool process_record_tap_flow(uint16_t keycode, keyrecord_t* record) {
if (record->event.pressed) {
switch (keycode) {
case TAP_FLOW_PRINT:
send_string(get_u16_str(g_tap_flow_term, ' '));
return false;
case TAP_FLOW_UP:
g_tap_flow_term += 5;
return false;
case TAP_FLOW_DOWN:
g_tap_flow_term -= 5;
return false;
}
}
return true;
}
// Keycode is a "typing" key: Space, A-Z, or main alphas area punctuation.
static bool is_typing(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;
}
// By default, enable filtering when both the tap-hold key and previous key are
// typing keys, and use the quick tap term.
__attribute__((weak)) uint16_t get_tap_flow(
uint16_t keycode, keyrecord_t* record, uint16_t prev_keycode) {
if (!is_typing(keycode) || !is_typing(prev_keycode)) {
return 0;
}
return g_tap_flow_term;
}
#endif // !defined(COMBO_ENABLE) && !defined(REPEAT_KEY_ENABLE)

View file

@ -0,0 +1,72 @@
// 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.
/**
* @file tap_flow.h
* @brief Tap Flow module: disable HRMs during fast typing
*
* This module is an implementation of "global quick tap" (GQT), aka "require
* priori idle," for tap-hold keys. It is particularly useful for home row mods
* to avoid accidental mod triggers in fast typing.
*
* Tap Flow modifies the tap-hold decision such that when a tap-hold key is
* pressed within a short timeout of the preceding key, the tapping function is
* used. The assumption is that during fast typing, only the tap function of
* tap-hold keys is desired (though perhaps with an exception for Shift or
* AltGr, noted below), whereas the hold functions (mods and layers) are
* used in isolation, or at least with a brief pause preceding the tap-hold key
* press.
*
* Optionally, the feature can be customized with the `get_tap_flow()` callback.
* In this way, exceptions may be made for Shift and AltGr (or whatever you
* wish) to use a shorter time or to disable filtering for those keys entirely.
*
*
* For full documentation, see
* <https://getreuer.info/posts/keyboards/tap-flow>
*/
#pragma once
#include "quantum.h"
#ifdef __cplusplus
extern "C" {
#endif
#ifndef TAP_FLOW_TERM
# define TAP_FLOW_TERM 150
#endif // TAP_FLOW_TERM
/**
* Optional callback to customize filtering.
*
* Tap Flow acts only when key events are closer together than this time.
*
* Return a time of 0 to disable filtering. In this way, Tap Flow may be
* disabled for certain tap-hold keys, or when following certain previous keys.
*
* @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_tap_flow(uint16_t keycode, keyrecord_t* record,
uint16_t prev_keycode);
extern uint16_t g_tap_flow_term;
#ifdef __cplusplus
}
#endif