Skip to content

Conversation

@getreuer
Copy link

Adds the new Speculative Hold tap-hold option to zsa/qmk_firmware.

Description

This is a cherry pick of qmk#25125 + qmk#25797. Speculative Hold makes mod-tap keys more responsive by applying the modifier instantly on keydown, before the tap-hold decision is made. This is especially useful for actions like Shift+click and Ctrl+scroll wheel with an external mouse, which can feel laggy with standard mod-taps.

The firmware holds the modifier speculatively. Once the key's behavior is settled:

  • If held, the modifier remains active as expected until the key is released.
  • If tapped, the speculative modifier is canceled just before the tapping keycode is sent.

Speculative Hold applies the modifier early but does not change the underlying tap-hold decision logic. Speculative Hold is compatible to use in combination with any other tap-hold options.

Using Speculative Hold

To enable Speculative Hold, add #define SPECULATIVE_HOLD in config.h, or in keyboard.json set config.tapping.speculative_hold to true.

By default, Speculative Hold is enabled only for Shift, Ctrl, and Ctrl+Shift mod-taps. It is possible to customize which keys it is enabled on with get_speculative_hold(), for instance:

bool get_speculative_hold(uint16_t keycode, keyrecord_t* record) {
    switch (keycode) { // Enable speculative holding for these keys.
        case LCTL_T(KC_ESC):
        case LSFT_T(KC_Z):
        case RSFT_T(KC_SLSH):
            return true;
    }
    return false; // Disable otherwise.
}

A technicality is that some mods, such as GUI, may be bound to application actions when pressed by themselves, and Speculative Hold may falsely trigger such hotkeys, and this is of course OS and application dependent. Define DUMMY_MOD_NEUTRALIZER_KEYCODE (and optionally MODS_TO_NEUTRALIZE) in your config.h to prevent this as described here.

Types of Changes

  • Core
  • Bugfix
  • New feature
  • Enhancement/optimization
  • Keyboard (addition or update)
  • Keymap/layout (addition or update)
  • Documentation

Checklist

  • My code follows the code style of this project: C, Python
  • I have read the PR Checklist document and have made the appropriate changes.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • I have tested the changes and verified that they work and don't break anything (as well as I can manage).

@fdidron
Copy link
Collaborator

fdidron commented Nov 27, 2025

Hi @getreuer

Thanks for this, we are currently evaluating this feature internally and will possibly add it to Oryx's advanced config.

@getreuer
Copy link
Author

@fdidron thank you!

The selling story is that I've been using and tinkering with HRMs for a while, as you know. Something that's always bugged me is that Shift+clicking or similar actions when using HRMs with an external mouse normally requires waiting out the tapping term before the click. It's not a huge thing, but it disrupts my flow and feels clumsy. Speculative Hold fixes that, the mod applies instantaneously, so I can Shift+click as fast as I want. Yet otherwise, the mod-tap still works in regular typing, and Speculative Hold is compatible with all tap-hold options.

The important complication to consider is that the "speculatively" held mod is sent to the computer even when tapping function is settled. This is usually harmless: in most applications, pressing and releasing Shift or Ctrl without other keys has no effect. However, in same cases a mod key may be interpreted as a hotkey of some sort, left GUI opening the Windows start menu being a big example. I've heard also that Speculative Hold confuses the Zed editor to break certain normal mode inputs.

Many games interpret Shift and Ctrl as having special functions. I'd strongly recommend against using Speculative Hold on the gaming layer, but to be clear, I'd also recommend against using mod-taps on a game layer in any case, because of the significant added input lag.

Ultimately, whether Speculative Hold works properly depends on how the application (and OS) interpret these modifier events.

For this reason, it is useful to consider two means of configurations to mitigate such issues:

  • The get_speculative_hold() callback may be defined to specify for which mod-taps Speculative Hold is active.

    • This makes it possible to enable Speculative Hold for specific mods. E.g. perhaps a good scheme at Oryx's level would be four separate toggles to enable Speculative Hold for Ctrl, Shift, Alt, and GUI.
    • This also makes it possible to enable/disable Speculative Hold dynamically by pressing a toggle key. QMK doesn't have such a key implemented in core, but with this callback it would be possible to implement one at the keymap level.
  • The "DUMMY_MOD_NEUTRALIZER_KEYCODE," if defined, is sent before releasing a speculatively held mod, when the mod-tap turns out to be settled as tapped. This key is meant to be mapped to a do-nothing key on the computer, so that the chord is "neutralized" as described here. QMK's documentation suggests the Right Ctrl key as a potential candidate for DUMMY_MOD_NEUTRALIZER_KEYCODE, and that works great on my (Linux) system. I don't in general what works on other platforms, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants