Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/keycodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ See also: [Dynamic Tapping Term](tap_hold#dynamic-tapping-term)
|-------------------------------|---------|-------------------------------------------------------------------------------------------|
|`QK_DYNAMIC_TAPPING_TERM_PRINT`|`DT_PRNT`| Types the current tapping term, in milliseconds |
|`QK_DYNAMIC_TAPPING_TERM_UP` |`DT_UP` | Increases the current tapping term by `DYNAMIC_TAPPING_TERM_INCREMENT`ms (5ms by default) |
|`QK_DYNAMIC_TAPPING_TERM_DOWN` |`DT_DOWN`| Decreases the current tapping term by `DYNAMIC_TAPPING_TERM_INCREMENT`ms (5ms by default) |
|`QK_DYNAMIC_TAPPING_TERM_DOWN` |`DT_DOWN`| Decreases the current tapping term by `DYNAMIC_TAPPING_TERM_INCREMENT`ms (5ms by default), never below the increment |

## RGB Lighting {#rgb-lighting}

Expand Down
2 changes: 1 addition & 1 deletion docs/tap_hold.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ uint16_t get_tapping_term(uint16_t keycode, keyrecord_t *record) {
|-------------------------------|---------|-------------------------------------------------------------------------------------------|
|`QK_DYNAMIC_TAPPING_TERM_PRINT`|`DT_PRNT`| Types the current tapping term, in milliseconds |
|`QK_DYNAMIC_TAPPING_TERM_UP` |`DT_UP` | Increases the current tapping term by `DYNAMIC_TAPPING_TERM_INCREMENT`ms (5ms by default) |
|`QK_DYNAMIC_TAPPING_TERM_DOWN` |`DT_DOWN`| Decreases the current tapping term by `DYNAMIC_TAPPING_TERM_INCREMENT`ms (5ms by default) |
|`QK_DYNAMIC_TAPPING_TERM_DOWN` |`DT_DOWN`| Decreases the current tapping term by `DYNAMIC_TAPPING_TERM_INCREMENT`ms (5ms by default), never below the increment |

Set the tapping term as usual with `#define TAPPING_TERM <value>` in `config.h` and add `DYNAMIC_TAPPING_TERM_ENABLE = yes` in `rules.mk`. Then, place the above three keys somewhere in your keymap and flash the new firmware onto your board.

Expand Down
4 changes: 3 additions & 1 deletion quantum/process_keycode/process_dynamic_tapping_term.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ bool process_dynamic_tapping_term(uint16_t keycode, keyrecord_t *record) {
return false;

case QK_DYNAMIC_TAPPING_TERM_DOWN:
g_tapping_term -= DYNAMIC_TAPPING_TERM_INCREMENT;
if (g_tapping_term > DYNAMIC_TAPPING_TERM_INCREMENT) {
g_tapping_term -= DYNAMIC_TAPPING_TERM_INCREMENT;
}
return false;
}
}
Expand Down
6 changes: 6 additions & 0 deletions tests/dynamic_tapping_term/config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright 2026 Arca <arca.artem@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include "test_common.h"
8 changes: 8 additions & 0 deletions tests/dynamic_tapping_term/test.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright 2026 Arca <arca.artem@gmail.com>
# SPDX-License-Identifier: GPL-2.0-or-later

# --------------------------------------------------------------------------------
# Keep this file, even if it is empty, as a marker that this folder contains tests
# --------------------------------------------------------------------------------

DYNAMIC_TAPPING_TERM_ENABLE = yes
121 changes: 121 additions & 0 deletions tests/dynamic_tapping_term/test_dynamic_tapping_term.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2026 Arca <arca.artem@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later

#include "test_common.hpp"

using testing::_;
using testing::InSequence;

class DynamicTappingTerm : public TestFixture {
public:
~DynamicTappingTerm() override {
g_tapping_term = TAPPING_TERM;
}
};

TEST_F(DynamicTappingTerm, TappingUpRaisesTermByIncrementPerPress) {
TestDriver driver;
KeymapKey up_key = KeymapKey{0, 0, 0, DT_UP};

set_keymap({up_key});
g_tapping_term = TAPPING_TERM;

EXPECT_NO_REPORT(driver);
tap_key(up_key);
EXPECT_EQ(g_tapping_term, TAPPING_TERM + DYNAMIC_TAPPING_TERM_INCREMENT);

tap_key(up_key);
EXPECT_EQ(g_tapping_term, TAPPING_TERM + 2 * DYNAMIC_TAPPING_TERM_INCREMENT);
VERIFY_AND_CLEAR(driver);
}

TEST_F(DynamicTappingTerm, TappingDownLowersTermByIncrementPerPress) {
TestDriver driver;
KeymapKey down_key = KeymapKey{0, 0, 0, DT_DOWN};

set_keymap({down_key});
g_tapping_term = 200;

EXPECT_NO_REPORT(driver);
tap_key(down_key);
EXPECT_EQ(g_tapping_term, 200 - DYNAMIC_TAPPING_TERM_INCREMENT);

tap_key(down_key);
EXPECT_EQ(g_tapping_term, 200 - 2 * DYNAMIC_TAPPING_TERM_INCREMENT);
VERIFY_AND_CLEAR(driver);
}

TEST_F(DynamicTappingTerm, TappingDownPastFloorDoesNotUnderflow) {
TestDriver driver;
KeymapKey down_key = KeymapKey{0, 0, 0, DT_DOWN};

// An exact multiple of the increment, so the floor is reached precisely
// regardless of how DYNAMIC_TAPPING_TERM_INCREMENT is configured.
const uint16_t start = 40 * DYNAMIC_TAPPING_TERM_INCREMENT;

set_keymap({down_key});
g_tapping_term = start;

EXPECT_NO_REPORT(driver);
for (int i = 0; i < 100; i++) {
tap_key(down_key);
EXPECT_LE(g_tapping_term, start);
EXPECT_GE(g_tapping_term, DYNAMIC_TAPPING_TERM_INCREMENT);
}
EXPECT_EQ(g_tapping_term, DYNAMIC_TAPPING_TERM_INCREMENT);
VERIFY_AND_CLEAR(driver);
}

TEST_F(DynamicTappingTerm, LoweredTappingTermChangesModTapResolution) {
TestDriver driver;
InSequence s;
KeymapKey mod_tap_key = KeymapKey{0, 0, 0, SFT_T(KC_P)};
KeymapKey down_key = KeymapKey{0, 1, 0, DT_DOWN};

set_keymap({mod_tap_key, down_key});
g_tapping_term = TAPPING_TERM;

// Strictly between the lowered term (TAPPING_TERM - 10 * INCREMENT) and the
// default TAPPING_TERM, so the same hold duration taps under the default
// term but holds once DT_DOWN has lowered the term below it.
const uint16_t hold_ms = TAPPING_TERM - 5 * DYNAMIC_TAPPING_TERM_INCREMENT;

/* Part A: baseline, hold_ms held under the default tapping term resolves as a tap. */
EXPECT_NO_REPORT(driver);
mod_tap_key.press();
idle_for(hold_ms);
VERIFY_AND_CLEAR(driver);

EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);

// Flush the quick-tap window (QUICK_TAP_TERM defaults to TAPPING_TERM) so the
// next press of the same mod-tap key hold-resolves instead of auto-repeating
// the tap above.
EXPECT_NO_REPORT(driver);
idle_for(TAPPING_TERM + 1);
VERIFY_AND_CLEAR(driver);

/* Part B: lower the tapping term below hold_ms. */
EXPECT_NO_REPORT(driver);
for (int i = 0; i < 10; i++) {
tap_key(down_key);
}
EXPECT_EQ(g_tapping_term, TAPPING_TERM - 10 * DYNAMIC_TAPPING_TERM_INCREMENT);
VERIFY_AND_CLEAR(driver);

/* Part C: the same hold duration now expires the lowered term mid-idle,
* so the mod-tap key resolves as a hold instead of a tap. */
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
idle_for(hold_ms);
VERIFY_AND_CLEAR(driver);

EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}