Skip to content

stasmarkin/sm_td

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

74 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SM_TD (QMK user library)

CI Discord QMK

SM Tap Dance Logo

What is SM_TD?

SM_TD is a QMK user library that makes Home Row Modifiers (HRMs) and Tap Dance reliable during fast typing. It improves tap vs. hold decisions by analyzing key releases (not just presses).

Why SM_TD?

Typing often involves overlapping keypresses. For example:

↓h ↓i ↑h ↑i

This happens when you type "hi" quickly. But QMK’s default behavior may misinterpret ↓h as a hold, not a tap, because ↓i occurred before ↑h.

This leads to bugs when using keys like LT(1, KC_H) for home row mods — triggering layer_move(1) instead of typing h.

SM_TD solves this by:

  • Interpreting keys based on release timing
  • Respecting natural typing habits
  • Avoiding false holds during fast sequences

Background

This library follows the natural overlap that happens when we type quickly. In the hi example, most people press i before releasing h — i.e., ↓h, ↓i, ↑h, ↑i. Stock QMK often interprets these in strict press order, which can misclassify a tap-hold key (e.g., LT(1, KC_H)) as a hold, leading to layer_move(1) instead of a tap.

SM_TD respects your habits rather than forcing you to change them. It pays attention to the time between key releases and interprets them accordingly:

  • ↓h, ↓i, ↑h (tiny pause), ↑i → treat as a combo-like overlap: hold/action on h + tap i
  • ↓h, ↓i, ↑h (long pause), ↑i → treat as sequential taps: tap h + tap i

Features

  • Human-friendly tap+tap vs. hold+tap interpretation for MT and LT
  • Per-key behavior tuning (e.g., hold after N taps in a row)
  • Immediate Tap Dance-style responses (no extra timeout needed)
  • Configurable timeouts per key or globally
  • Feature flags per key or globally
  • Debugging tools
  • Caps Word: full integration with QMK Caps Word (shifts letters, ends on word-breaking keys, respects caps_word_press_user)
  • Standard QMK MT() / LT() keycodes support (via SMTD_ENABLE_QMK_TAPHOLD)
  • Chordal hold ("opposite-hands rule", opt-in via SMTD_CHORDAL_HOLD): a tap-hold settles as hold only with an opposite-hand key, so same-hand rolls stay taps
  • Plays well with other QMK features: sm_td taps go through the regular process_record() pipeline
  • Combos: partial support for QMK Combos

Installation

There are two ways to install SM_TD:

Option 1: Manual

  1. In rules.mk, add DEFERRED_EXEC_ENABLE = yes and SRC += sm_td.c.
  2. In config.h, add #define MAX_DEFERRED_EXECUTORS 10 (or increase if already defined).
  3. Copy sm_td/sm_td.h and sm_td/sm_td.c into your keymaps/<your_keymap>/ folder (next to keymap.c).
  4. Add #include "sm_td.h" in your keymap.c.
  5. Check process_smtd(...) first in process_record_user(...) like this:
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    if (!process_smtd(keycode, record)) {
        return false;
    }

    // your code here

    return true;
}

Option 2: QMK Community Module

  1. In keymap.json, add:

    { "modules": ["stasmarkin/sm_td"] }

That’s it — proceed to Configuration.

Configuration

  1. Create an on_smtd_action() function in your keymap.c that handles extra actions for keycodes. For example, to use KC_A, KC_S, KC_D, and KC_F for Home Row Mods:

    smtd_resolution on_smtd_action(uint16_t keycode, smtd_action action, uint8_t tap_count) {
        switch (keycode) {
            SMTD_MT(KC_A, KC_LEFT_GUI)
            SMTD_MT(KC_S, KC_LEFT_ALT)
            SMTD_MT(KC_D, KC_LEFT_CTRL)
            SMTD_MT(KC_F, KC_LSFT)
        }
    
        return SMTD_RESOLUTION_UNHANDLED;
    }

    Optional: if you want to keep standard QMK MT() / LT() in your keymap (no SMTD_MT / SMTD_LT), add #define SMTD_ENABLE_QMK_TAPHOLD 1 to your config.h. This routes QMK mod-tap and layer-tap keycodes through sm_td timing. (Advanced features like tap-count thresholds still require SMTD_MT / SMTD_LT.) See the Customization Guide and practical Examples for more patterns.

    Optional: enable the chordal-hold "opposite-hands rule" with #define SMTD_CHORDAL_HOLD 1 in your config.h. A tap-hold then settles as hold only when an opposite-hand key is involved; same-hand rolls stay taps. Provide a chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] marking each key 'L' / 'R' / '*' (left / right / neutral thumb), or override char smtd_chordal_handedness(keypos_t key) to compute handedness yourself.

  2. (optional) Add global configuration parameters to your config.h file (see timeouts and feature flags).

  3. (optional) Add per-key configuration (see timeouts and feature flags).

Macros for on_smtd_action()

Macro Description
SMTD_MT(KC_A, KC_LEFT_GUI) Basic mod-tap: Tap KC_A → single tap, Hold KC_AKC_LEFT_GUI hold
SMTD_MT(KC_A, KC_LEFT_GUI, 2) Tap count mod-tap: Same as above, but hold after 2 sequential taps results in KC_A hold
↓KC_A, ↑KC_A, ↓KC_A...KC_A tap + KC_LEFT_GUI hold
↓KC_A, ↑KC_A, ↓KC_A, ↑KC_A, ↓KC_A... → 2× KC_A tap + KC_A hold
SMTD_MT(KC_A, KC_LEFT_GUI, 1, false) Caps Word disabled: Basic mod-tap with QMK’s Caps Word feature disabled
SMTD_MTE(KC_A, KC_LEFT_GUI) Eager mod-tap: Holds KC_LEFT_GUI immediately on press
• Quick release → KC_LEFT_GUI released + KC_A tapped
• Continue holding → KC_LEFT_GUI held, no KC_A tap
• Useful for fast mod+mouse clicks
SMTD_MTE(KC_A, KC_LEFT_GUI, 2) Eager with tap count: Eager version of tap count mod-tap
SMTD_MTE(KC_A, KC_LEFT_GUI, 1, false) Eager caps disabled: Eager version with Caps Word disabled
SMTD_LT(KC_A, 2) Layer tap: Momentary layer switching (layer 2), works like SMTD_MT but switches layers instead of modifiers
SMTD_LT(KC_A, 2, 3) Layer tap with count: Hold after 3 sequential taps results in KC_A hold
↓KC_A, ↑KC_A, ↓KC_A...KC_A tap + layer 2 activation
↓KC_A, ↑KC_A, ↓KC_A, ↑KC_A, ↓KC_A, ↑KC_A, ↓KC_A... → 3× KC_A tap + KC_A hold
SMTD_LT(KC_A, 2, 1, false) Layer tap caps disabled: Same as above with Caps Word disabled
SMTD_MT_ON_MKEY(CKC_A, KC_A, KC_LEFT_GUI) Mod-tap with custom keycode: Uses custom keycode CKC_A (do not forget to declare it) in keymap while treating it as KC_A tap and KC_LEFT_GUI hold
• Might be used if you need different behavior of KC_A on different layers
• Useful for migration from older SM_TD versions or when you need custom keycodes
SMTD_LT_ON_MKEY(CKC_A, KC_A, 2) Layer tap with custom keycode: Uses custom keycode CKC_A (do not forget to declare it) in keymap while treating it as KC_A tap and layer 2 activation
• Might be used if you need different behavior of KC_A on different layers
• Useful for migration from older SM_TD versions or when you need custom keycodes

Documentation

There is a /docs folder with extensive documentation.

Also, you may check my layout for a real-world example of using this library.

Community

Start with GitHub issues or pull requests for questions and ideas.

You can also join the SM_TD Discord channel, or reach me on Reddit (u/stasmarkin) or Discord (stasmarkin).

Also, you may email me or tag/text me on Reddit (u/stasmarkin) or Discord (stasmarkin).

Support This Project

If you find this library helpful, consider supporting the project:

GitHub Sponsors Buy Me A Coffee

Crypto support:

  • USDT on TRON: TE4QifvjnPSQoT4oJXYnYAnZxBKAvwUFCN
  • ByBit ID: 230327759

Your support helps me continue developing and maintaining this project. Thank you for using SM_TD!

Roadmap

v0.5.x

  • Full-pipeline taps + Caps Word (#23) and SMTD_ENABLE_QMK_TAPHOLD for native MT() / LT() (0.5.6), QMK community module integration (0.5.1 / 0.5.5), split into .h + .c (0.5.4), 3+ finger roll interpretation and a collection of macros (0.5.0 / 0.5.3), AVR fix (#48, 0.5.2), and assorted bug fixes

v0.6.0

  • Feature: dynamic release timeout derived from your typing rhythm (fixes #45); a new global flag tunes the window (set to 0 for the old fixed-timeout behavior)

v0.6.1

  • Fix: guard against state double-removal crash in smtd_apply_stage
  • Fix: SMTD_TK / SMTD_TTO macro expansion
  • New: smtd_reset() for test harnesses

v0.6.2

  • Fix: SMTD_LT uses native layer_on / layer_off, so it no longer wipes foreign layer bits on release (fixes #57, unblocks tri-layer #44)
  • Fix: a held key released under a stacked key now finalizes instead of hanging its modifier (fixes #58)

v0.6.3

  • Feature: chordal hold ("opposite-hands rule") via SMTD_CHORDAL_HOLD — same-hand rolls stay taps, cross-hand chords hold; handedness from a chordal_hold_layout array or an overridable smtd_chordal_handedness() (#60)
  • Fix: custom / derived keycode taps now feed the QMK leader buffer, so Leader sequences see them (fixes #29)

v0.6.5 (we are here)

  • Feature: SMTD_GLOBAL_RELEASE_PERCENT controls the dynamic release window (min(p1, p2) * percent / 100) with fine, single-percent granularity. Behavior change: the default is now SMTD_GLOBAL_RELEASE_PERCENT 30 (a slightly wider window — fewer hold→tap-tap misfires); set SMTD_GLOBAL_RELEASE_PERCENT 20 to restore the previous behavior

v0.6.4

  • Fix: chordal hold holds (not taps) when a neutral ('*') key follows a mod-tap, matching the hold-timeout path (#62)

v0.7.0+ and further v0.x

  • better combo support
  • other feature requests (see issues)

v1.0.0

  • stable API
  • memory optimizations (on storing active states)
  • memory optimizations (on state machine stack size)

Special Thanks

Code contributions

Beta testing

(please, let me know, if I have forgotten someone)

Star History

Star History Chart