Skip to content
Open
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
1 change: 1 addition & 0 deletions data/mappings/info_config.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
"PERMISSIVE_HOLD_PER_KEY": {"info_key": "tapping.permissive_hold_per_key", "value_type": "flag"},
"RETRO_TAPPING": {"info_key": "tapping.retro", "value_type": "flag"},
"RETRO_TAPPING_PER_KEY": {"info_key": "tapping.retro_per_key", "value_type": "flag"},
"SPECULATIVE_HOLD": {"info_key": "tapping.speculative_hold", "value_type": "flag"},
"TAP_CODE_DELAY": {"info_key": "qmk.tap_keycode_delay", "value_type": "int"},
"TAP_HOLD_CAPS_DELAY": {"info_key": "qmk.tap_capslock_delay", "value_type": "int"},
"TAPPING_TERM": {"info_key": "tapping.term", "value_type": "int"},
Expand Down
5 changes: 5 additions & 0 deletions quantum/action.c
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,11 @@ void process_record(keyrecord_t *record) {
if (IS_NOEVENT(record->event)) {
return;
}
#ifdef SPECULATIVE_HOLD
if (record->event.pressed) {
speculative_key_settled(record);
}
#endif // SPECULATIVE_HOLD
#ifdef FLOW_TAP_TERM
flow_tap_update_last_event(record);
#endif // FLOW_TAP_TERM
Expand Down
2 changes: 1 addition & 1 deletion quantum/action.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ extern "C" {
/* tapping count and state */
typedef struct {
bool interrupted : 1;
bool reserved2 : 1;
bool speculated : 1;
bool reserved1 : 1;
bool reserved0 : 1;
uint8_t count : 4;
Expand Down
171 changes: 171 additions & 0 deletions quantum/action_tapping.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
#include "action_tapping.h"
#include "action_util.h"
#include "keycode.h"
#include "keycode_config.h"
#include "quantum_keycodes.h"
#include "timer.h"
#include "wait.h"

#ifndef NO_ACTION_TAPPING

Expand Down Expand Up @@ -51,6 +53,21 @@ __attribute__((weak)) bool get_permissive_hold(uint16_t keycode, keyrecord_t *re
}
# endif

# ifdef SPECULATIVE_HOLD
typedef struct {
keypos_t key;
uint8_t mods;
} speculative_key_t;
# define SPECULATIVE_KEYS_SIZE 8
static speculative_key_t speculative_keys[SPECULATIVE_KEYS_SIZE] = {};
static uint8_t num_speculative_keys = 0;
static uint8_t prev_speculative_mods = 0;
static uint8_t speculative_mods = 0;

/** Handler to be called on incoming press events. */
static void speculative_key_press(keyrecord_t *record);
# endif // SPECULATIVE_HOLD

# if defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
# define REGISTERED_TAPS_SIZE 8
// Array of tap-hold keys that have been settled as tapped but not yet released.
Expand Down Expand Up @@ -129,6 +146,13 @@ static void debug_waiting_buffer(void);
* FIXME: Needs doc
*/
void action_tapping_process(keyrecord_t record) {
# ifdef SPECULATIVE_HOLD
prev_speculative_mods = speculative_mods;
if (record.event.pressed) {
speculative_key_press(&record);
}
# endif // SPECULATIVE_HOLD

if (process_tapping(&record)) {
if (IS_EVENT(record.event)) {
ac_dprintf("processed: ");
Expand All @@ -145,6 +169,12 @@ void action_tapping_process(keyrecord_t record) {
}
}

# ifdef SPECULATIVE_HOLD
if (speculative_mods != prev_speculative_mods) {
send_keyboard_report();
}
# endif // SPECULATIVE_HOLD

// process waiting_buffer
if (IS_EVENT(record.event) && waiting_buffer_head != waiting_buffer_tail) {
ac_dprintf("---- action_exec: process waiting_buffer -----\n");
Expand Down Expand Up @@ -708,6 +738,147 @@ void waiting_buffer_scan_tap(void) {
}
}

# ifdef SPECULATIVE_HOLD
static void debug_speculative_keys(void) {
ac_dprintf("mods = { ");
for (int8_t i = 0; i < num_speculative_keys; ++i) {
ac_dprintf("%02X ", speculative_keys[i].mods);
}
ac_dprintf("}, keys = { ");
for (int8_t i = 0; i < num_speculative_keys; ++i) {
ac_dprintf("%02X%02X ", speculative_keys[i].key.row, speculative_keys[i].key.col);
}
ac_dprintf("}\n");
}

// Find key in speculative_keys. Returns num_speculative_keys if not found.
static int8_t speculative_keys_find(keypos_t key) {
uint8_t i;
for (i = 0; i < num_speculative_keys; ++i) {
if (KEYEQ(speculative_keys[i].key, key)) {
break;
}
}
return i;
}

static void speculative_key_press(keyrecord_t *record) {
if (num_speculative_keys >= SPECULATIVE_KEYS_SIZE) { // Overflow!
ac_dprintf("SPECULATIVE KEYS OVERFLOW: IGNORING EVENT\n");
return; // Don't trigger: speculative_keys is full.
}
if (speculative_keys_find(record->event.key) < num_speculative_keys) {
return; // Don't trigger: key is already in speculative_keys.
}

const uint16_t keycode = get_record_keycode(record, false);
if (!IS_QK_MOD_TAP(keycode)) {
return; // Don't trigger: not a mod-tap key.
}

uint8_t mods = mod_config(QK_MOD_TAP_GET_MODS(keycode));
if ((mods & 0x10) != 0) { // Unpack 5-bit mods to 8-bit representation.
mods <<= 4;
}
if ((~(get_mods() | speculative_mods) & mods) == 0) {
return; // Don't trigger: mods are already active.
}

// Don't do Speculative Hold when there are non-speculated buffered events,
// since that could result in sending keys out of order.
for (uint8_t i = waiting_buffer_tail; i != waiting_buffer_head; i = (i + 1) % WAITING_BUFFER_SIZE) {
if (!waiting_buffer[i].tap.speculated) {
return;
}
}

if (get_speculative_hold(keycode, record)) {
record->tap.speculated = true;
speculative_mods |= mods;
// Remember the keypos and mods associated with this key.
speculative_keys[num_speculative_keys] = (speculative_key_t){
.key = record->event.key,
.mods = mods,
};
++num_speculative_keys;

ac_dprintf("Speculative Hold: ");
debug_speculative_keys();
}
}

uint8_t get_speculative_mods(void) {
return speculative_mods;
}

__attribute__((weak)) bool get_speculative_hold(uint16_t keycode, keyrecord_t *record) {
const uint8_t mods = mod_config(QK_MOD_TAP_GET_MODS(keycode));
return (mods & (MOD_LCTL | MOD_LSFT)) == (mods & (MOD_HYPR));
}

void speculative_key_settled(keyrecord_t *record) {
if (num_speculative_keys == 0) {
return; // Early return when there are no active speculative keys.
}

uint8_t i = speculative_keys_find(record->event.key);

const uint16_t keycode = get_record_keycode(record, false);
if (IS_QK_MOD_TAP(keycode) && record->tap.count == 0) { // MT hold press.
if (i < num_speculative_keys) {
--num_speculative_keys;
const uint8_t cleared_mods = speculative_keys[i].mods;

if (num_speculative_keys) {
speculative_mods &= ~cleared_mods;
// Don't call send_keyboard_report() here; allow default
// handling to reapply the mod before the next report.

// Remove the ith entry from speculative_keys.
for (uint8_t j = i; j < num_speculative_keys; ++j) {
speculative_keys[j] = speculative_keys[j + 1];
}
} else {
speculative_mods = 0;
}

ac_dprintf("Speculative Hold: settled %02x, ", cleared_mods);
debug_speculative_keys();
}
} else { // Tap press event; cancel speculatively-held mod.
if (i >= num_speculative_keys) {
i = 0;
}

// Clear mods for the ith key and all keys that follow.
uint8_t cleared_mods = 0;
for (uint8_t j = i; j < num_speculative_keys; ++j) {
cleared_mods |= speculative_keys[j].mods;
}

num_speculative_keys = i; // Remove ith and following entries.

if ((prev_speculative_mods & cleared_mods) != 0) {
# ifdef DUMMY_MOD_NEUTRALIZER_KEYCODE
neutralize_flashing_modifiers(get_mods() | prev_speculative_mods);
# endif // DUMMY_MOD_NEUTRALIZER_KEYCODE
}

if (num_speculative_keys) {
speculative_mods &= ~cleared_mods;
} else {
speculative_mods = 0;
}

send_keyboard_report();
wait_ms(TAP_CODE_DELAY);

ac_dprintf("Speculative Hold: canceled %02x, ", cleared_mods);
debug_speculative_keys();
}
}
# endif // SPECULATIVE_HOLD

# if defined(CHORDAL_HOLD) || defined(FLOW_TAP_TERM)
static void registered_taps_add(keypos_t key) {
if (num_registered_taps >= REGISTERED_TAPS_SIZE) {
Expand Down
30 changes: 30 additions & 0 deletions quantum/action_tapping.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,36 @@ bool get_permissive_hold(uint16_t keycode, keyrecord_t *record);
bool get_retro_tapping(uint16_t keycode, keyrecord_t *record);
bool get_hold_on_other_key_press(uint16_t keycode, keyrecord_t *record);

#ifdef SPECULATIVE_HOLD
/** Gets the currently active speculative mods. */
uint8_t get_speculative_mods(void);

/**
* Callback to say if a mod-tap key may be speculatively held.
*
* By default, speculative hold is enabled for mod-tap keys where the mod is
* Ctrl, Shift, and Ctrl+Shift for either hand.
*
* @param keycode Keycode of the mod-tap key.
* @param record Record associated with the mod-tap press event.
* @return True if the mod-tap key may be speculatively held.
*/
bool get_speculative_hold(uint16_t keycode, keyrecord_t *record);

/**
* Handler to be called on press events after tap-holds are settled.
*
* This function is to be called in process_record() in action.c, that is, just
* after tap-hold events are settled as either tapped or held. When `record`
* corresponds to a speculatively-held key, the speculative mod is cleared.
*
* @param record Record associated with the mod-tap press event.
*/
void speculative_key_settled(keyrecord_t *record);
#else
# define get_speculative_mods() 0
#endif // SPECULATIVE_HOLD

#ifdef CHORDAL_HOLD
/**
* Callback to say when a key chord before the tapping term may be held.
Expand Down
5 changes: 5 additions & 0 deletions quantum/action_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "debug.h"
#include "action_util.h"
#include "action_layer.h"
#include "action_tapping.h"
#include "timer.h"
#include "keycode_config.h"
#include <string.h>
Expand Down Expand Up @@ -273,6 +274,10 @@ static uint8_t get_mods_for_report(void) {
}
#endif

#ifdef SPECULATIVE_HOLD
mods |= get_speculative_mods();
#endif

#ifdef KEY_OVERRIDE_ENABLE
// These need to be last to be able to properly control key overrides
mods &= ~suppressed_mods;
Expand Down
23 changes: 23 additions & 0 deletions tests/tap_hold_configurations/speculative_hold/all_mods/config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* Copyright 2022 Vladislav Kucheriavykh
* Copyright 2025 Google LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include "test_common.h"

#define SPECULATIVE_HOLD
#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_F24
18 changes: 18 additions & 0 deletions tests/tap_hold_configurations/speculative_hold/all_mods/test.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2022 Vladislav Kucheriavykh
# Copyright 2025 Google LLC
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

MAGIC_ENABLE = yes

Loading
Loading