qmk / qmk_firmware

Open-source keyboard firmware for Atmel AVR and Arm USB families
https://qmk.fm
GNU General Public License v2.0
17.99k stars 38.69k forks source link

[Feature Request] Implement ZMK's `require-prior-idle-ms` in QMK #24262

Open amarakon opened 1 month ago

amarakon commented 1 month ago

Feature Request Type

Description

ZMK has the configuration option require-prior-idle-ms for tap-hold keys. It effectively makes all tap-hold keys resolve as tapped when typing quickly. Aside from decreasing the possibility of accidentally triggering modifiers, it has the added benefit of eliminating the delay when typing quickly.

I did find a pull request on Manna Harbour's fork of the QMK firmware, but it's from two years ago and I can't merge it into my clone of this repository without a lot of merge conflicts. There's also the achordion userspace library, but it can't get rid of the delay; it can only prevent modifiers from being triggered.

filterpaper commented 3 weeks ago

It is a non-trivial request to implement this into QMK's core action tapping code. But you can easily do so in your user keymap code using the pre_process_record_user function with something like:

// Decision macro for mod-tap keys to override
#define IS_HOMEROW_MOD_TAP(kc) (              \
    IS_QK_MOD_TAP(kc)                      && \
    QK_MOD_TAP_GET_TAP_KEYCODE(kc) >= KC_A && \
    QK_MOD_TAP_GET_TAP_KEYCODE(kc) <= KC_Z    )

// Decision macro for preceding trigger key and typing interval
#define IS_TYPING(k) ( \
    ((uint8_t)(k) <= KC_Z || (uint8_t)(k) == KC_SPC) && \
    (last_input_activity_elapsed() < QUICK_TAP_TERM)    )

bool pre_process_record_user(uint16_t keycode, keyrecord_t *record) {
    static bool     is_pressed[UINT8_MAX];
    static uint16_t prev_keycode;
    const  uint8_t  tap_keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);

    if (record->event.pressed) {
        // Press the tap keycode if the tap-hold key follows the previous key swiftly
        if (IS_HOMEROW_MOD_TAP(keycode) && IS_TYPING(prev_keycode)) {
            is_pressed[tap_keycode] = true;
            record->keycode = tap_keycode;
        }
        // Cache the keycode for subsequent tap decision
        prev_keycode = keycode;
    }

    // Release the tap keycode if pressed
    else if (is_pressed[tap_keycode]) {
        is_pressed[tap_keycode] = false;
        record->keycode = tap_keycode;
    }

    return true;
}

The preprocessor macros can be adjusted to your preference. Unlike ZMK, you can choose the preceding keycode that will trigger the feature. It uses the keycode container in the keyrecord_t. If you don't have COMBO_ENABLE, enable REPEAT_KEY_ENABLE.

amarakon commented 3 weeks ago

First, thanks a lot for the code. I tested it and it works for home row mods. However, I also like to use my spacebar as a mod-tap key. This is how I set up my mod-taps:

I changed the IS_HOMEROW_MOD_TAP macro to the following:

#define IS_HOMEROW_MOD_TAP(kc) ( \
    IS_QK_MOD_TAP(kc) && \
    ((QK_MOD_TAP_GET_TAP_KEYCODE(kc) >= KC_A && \
      QK_MOD_TAP_GET_TAP_KEYCODE(kc) <= KC_Z) || \
      QK_MOD_TAP_GET_TAP_KEYCODE(kc) == KC_SPC))

This makes it so that KC_SPC can also be considered a mod-tap key. It works for the most part, but sometimes it won't work and there'll be an unexpected delay when pressing space during a typing streak, as if I was using standard mod-taps.

I attached a video. I used keyd to monitor my keystrokes. Notice how there's a delay when typing the space right before the word The. Also notice how the difference in time between space down and f down is one millisecond. This probably means that the space is delayed until the F key is pressed. By the way, I'm using the Colemak layout so I press the F key to type the letter T.

https://github.com/user-attachments/assets/96b35bea-0999-4a32-a3da-edf997f1311e

Edit: I forgot to mention that my QUICK_TAP_TERM is defined as 200.

amarakon commented 3 weeks ago

I just realized that the reason I was experiencing that issue was because I also had a key-remapping daemon running which does the same thing. I was using it as a temporary solution for this problem and forgot to turn it off when I re-flashed my firmware. I'll still keep this issue open because I'd like to see this get implemented as a core QMK feature.