summary refs log tree commit diff
path: root/quantum/process_keycode/process_combo.c
diff options
context:
space:
mode:
authorNick Brassel <nick@tzarc.org>2021-08-29 08:20:25 +1000
committerNick Brassel <nick@tzarc.org>2021-08-29 08:20:25 +1000
commitf061ca497464fe85284906fb163a33eaee7a91ef (patch)
tree33ef1bfb529aed382e8526c607c4e18717f92571 /quantum/process_keycode/process_combo.c
parentff65185dec6f97be1eb49f17cea526a0d0bbf3d6 (diff)
parent4bad375d7c09d949a9dcdd4feba147c9c7a67ec6 (diff)
Breaking changes develop merge to master, 2021Q3 edition. (#14196)
Diffstat (limited to 'quantum/process_keycode/process_combo.c')
-rw-r--r--quantum/process_keycode/process_combo.c558
1 files changed, 455 insertions, 103 deletions
diff --git a/quantum/process_keycode/process_combo.c b/quantum/process_keycode/process_combo.c
index f38d7d47a0..e8661839c7 100644
--- a/quantum/process_keycode/process_combo.c
+++ b/quantum/process_keycode/process_combo.c
@@ -16,114 +16,449 @@
 
 #include "print.h"
 #include "process_combo.h"
+#include "action_tapping.h"
 
-#ifndef COMBO_VARIABLE_LEN
-__attribute__((weak)) combo_t key_combos[COMBO_COUNT] = {};
+
+#ifdef COMBO_COUNT
+__attribute__((weak)) combo_t  key_combos[COMBO_COUNT];
+uint16_t COMBO_LEN = COMBO_COUNT;
 #else
 extern combo_t  key_combos[];
-extern int      COMBO_LEN;
+extern uint16_t COMBO_LEN;
 #endif
 
 __attribute__((weak)) void process_combo_event(uint16_t combo_index, bool pressed) {}
 
-static uint16_t timer               = 0;
-static uint16_t current_combo_index = 0;
-static bool     drop_buffer         = false;
-static bool     is_active           = false;
-static bool     b_combo_enable      = true;  // defaults to enabled
+#ifdef COMBO_MUST_HOLD_PER_COMBO
+__attribute__((weak)) bool get_combo_must_hold(uint16_t index, combo_t *combo) { return false; }
+#endif
+
+#ifdef COMBO_MUST_TAP_PER_COMBO
+__attribute__((weak)) bool get_combo_must_tap(uint16_t index, combo_t *combo) { return false; }
+#endif
+
+#ifdef COMBO_TERM_PER_COMBO
+__attribute__((weak)) uint16_t get_combo_term(uint16_t index, combo_t *combo) { return COMBO_TERM; }
+#endif
+
+#ifdef COMBO_PROCESS_KEY_RELEASE
+__attribute__((weak)) bool process_combo_key_release(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode) { return false; }
+#endif
 
-static uint8_t buffer_size = 0;
-#ifdef COMBO_ALLOW_ACTION_KEYS
-static keyrecord_t key_buffer[MAX_COMBO_LENGTH];
+#ifndef COMBO_NO_TIMER
+static uint16_t timer                 = 0;
+#endif
+static bool     b_combo_enable        = true;  // defaults to enabled
+static uint16_t longest_term          = 0;
+
+typedef struct {
+    keyrecord_t record;
+    uint16_t combo_index;
+    uint16_t keycode;
+} queued_record_t;
+static uint8_t key_buffer_size = 0;
+static queued_record_t key_buffer[COMBO_KEY_BUFFER_LENGTH];
+
+typedef struct {
+    uint16_t combo_index;
+} queued_combo_t;
+static uint8_t combo_buffer_write= 0;
+static uint8_t combo_buffer_read = 0;
+static queued_combo_t combo_buffer[COMBO_BUFFER_LENGTH];
+
+#define INCREMENT_MOD(i) i = (i + 1) % COMBO_BUFFER_LENGTH
+
+#define COMBO_KEY_POS ((keypos_t){.col=254, .row=254})
+
+
+#ifndef EXTRA_SHORT_COMBOS
+/* flags are their own elements in combo_t struct. */
+#    define COMBO_ACTIVE(combo)   (combo->active)
+#    define COMBO_DISABLED(combo) (combo->disabled)
+#    define COMBO_STATE(combo)    (combo->state)
+
+#    define ACTIVATE_COMBO(combo)    do {combo->active = true;}while(0)
+#    define DEACTIVATE_COMBO(combo)  do {combo->active = false;}while(0)
+#    define DISABLE_COMBO(combo)     do {combo->disabled = true;}while(0)
+#    define RESET_COMBO_STATE(combo) do { \
+    combo->disabled = false; \
+    combo->state = 0; \
+}while(0)
 #else
-static uint16_t key_buffer[MAX_COMBO_LENGTH];
+/* flags are at the two high bits of state. */
+#    define COMBO_ACTIVE(combo)   (combo->state & 0x80)
+#    define COMBO_DISABLED(combo) (combo->state & 0x40)
+#    define COMBO_STATE(combo)    (combo->state & 0x3F)
+
+#    define ACTIVATE_COMBO(combo)    do {combo->state |= 0x80;}while(0)
+#    define DEACTIVATE_COMBO(combo)  do {combo->state &= ~0x80;}while(0)
+#    define DISABLE_COMBO(combo)     do {combo->state |= 0x40;}while(0)
+#    define RESET_COMBO_STATE(combo) do {combo->state &= ~0x7F;}while(0)
 #endif
 
-static inline void send_combo(uint16_t action, bool pressed) {
-    if (action) {
-        if (pressed) {
-            register_code16(action);
-        } else {
-            unregister_code16(action);
-        }
+static inline void release_combo(uint16_t combo_index, combo_t *combo) {
+    if (combo->keycode) {
+        keyrecord_t record = {
+            .event = {
+                .key = COMBO_KEY_POS,
+                .time = timer_read()|1,
+                .pressed = false,
+            },
+            .keycode = combo->keycode,
+        };
+#ifndef NO_ACTION_TAPPING
+        action_tapping_process(record);
+#else
+        process_record(&record);
+#endif
     } else {
-        process_combo_event(current_combo_index, pressed);
+        process_combo_event(combo_index, false);
     }
+    DEACTIVATE_COMBO(combo);
+}
+
+static inline bool _get_combo_must_hold(uint16_t combo_index, combo_t *combo) {
+#ifdef COMBO_NO_TIMER
+    return false;
+#elif defined(COMBO_MUST_HOLD_PER_COMBO)
+    return get_combo_must_hold(combo_index, combo);
+#elif defined(COMBO_MUST_HOLD_MODS)
+    return (KEYCODE_IS_MOD(combo->keycode) ||
+            (combo->keycode >= QK_MOMENTARY && combo->keycode <= QK_MOMENTARY_MAX));
+#endif
+    return false;
+}
+
+static inline uint16_t _get_wait_time(uint16_t combo_index, combo_t *combo ) {
+    if (_get_combo_must_hold(combo_index, combo)
+#ifdef COMBO_MUST_TAP_PER_COMBO
+            || get_combo_must_tap(combo_index, combo)
+#endif
+       ) {
+        if (longest_term < COMBO_HOLD_TERM) {
+            return COMBO_HOLD_TERM;
+        }
+    }
+
+    return longest_term;
+}
+
+static inline uint16_t _get_combo_term(uint16_t combo_index, combo_t *combo) {
+
+#if defined(COMBO_TERM_PER_COMBO)
+        return get_combo_term(combo_index, combo);
+#endif
+
+    return COMBO_TERM;
 }
 
-static inline void dump_key_buffer(bool emit) {
-    if (buffer_size == 0) {
+void clear_combos(void) {
+    uint16_t index = 0;
+    longest_term = 0;
+    for (index = 0; index < COMBO_LEN; ++index) {
+        combo_t *combo = &key_combos[index];
+        if (!COMBO_ACTIVE(combo)) {
+            RESET_COMBO_STATE(combo);
+        }
+    }
+}
+
+static inline void dump_key_buffer(void) {
+    /* First call start from 0 index; recursive calls need to start from i+1 index */
+    static uint8_t key_buffer_next = 0;
+
+    if (key_buffer_size == 0) {
         return;
     }
 
-    if (emit) {
-        for (uint8_t i = 0; i < buffer_size; i++) {
-#ifdef COMBO_ALLOW_ACTION_KEYS
-            const action_t action = store_or_get_action(key_buffer[i].event.pressed, key_buffer[i].event.key);
-            process_action(&(key_buffer[i]), action);
+    for (uint8_t key_buffer_i = key_buffer_next; key_buffer_i < key_buffer_size; key_buffer_i++) {
+        key_buffer_next = key_buffer_i + 1;
+
+        queued_record_t *qrecord = &key_buffer[key_buffer_i];
+        keyrecord_t *record = &qrecord->record;
+
+        if (IS_NOEVENT(record->event)) {
+            continue;
+        }
+
+        if (!record->keycode && qrecord->combo_index != (uint16_t)-1) {
+            process_combo_event(qrecord->combo_index, true);
+        } else {
+#ifndef NO_ACTION_TAPPING
+        action_tapping_process(*record);
 #else
-            register_code16(key_buffer[i]);
-            send_keyboard_report();
+        process_record(record);
 #endif
         }
+        record->event.time = 0;
     }
 
-    buffer_size = 0;
+    key_buffer_next = key_buffer_size = 0;
 }
 
-#define ALL_COMBO_KEYS_ARE_DOWN (((1 << count) - 1) == combo->state)
-#define KEY_STATE_DOWN(key)         \
-    do {                            \
-        combo->state |= (1 << key); \
+#define NO_COMBO_KEYS_ARE_DOWN (0 == COMBO_STATE(combo))
+#define ALL_COMBO_KEYS_ARE_DOWN(state, key_count) (((1 << key_count) - 1) == state)
+#define ONLY_ONE_KEY_IS_DOWN(state) !(state & (state - 1))
+#define KEY_NOT_YET_RELEASED(state, key_index) ((1 << key_index) & state)
+#define KEY_STATE_DOWN(state, key_index) \
+    do {                                 \
+        state |= (1 << key_index);       \
     } while (0)
-#define KEY_STATE_UP(key)            \
-    do {                             \
-        combo->state &= ~(1 << key); \
+#define KEY_STATE_UP(state, key_index) \
+    do {                               \
+        state &= ~(1 << key_index);    \
     } while (0)
 
-static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *record) {
-    uint8_t  count = 0;
-    uint16_t index = -1;
-    /* Find index of keycode and number of combo keys */
-    for (const uint16_t *keys = combo->keys;; ++count) {
-        uint16_t key = pgm_read_word(&keys[count]);
-        if (keycode == key) index = count;
+static inline void _find_key_index_and_count(const uint16_t *keys, uint16_t keycode, uint16_t *key_index, uint8_t *key_count) {
+    while (true) {
+        uint16_t key = pgm_read_word(&keys[*key_count]);
+        if (keycode == key) *key_index = *key_count;
         if (COMBO_END == key) break;
+        (*key_count)++;
+    }
+}
+
+void drop_combo_from_buffer(uint16_t combo_index) {
+    /* Mark a combo as processed from the buffer. If the buffer is in the
+     * beginning of the buffer, drop it.  */
+    uint8_t i = combo_buffer_read;
+    while (i != combo_buffer_write) {
+        queued_combo_t *qcombo = &combo_buffer[i];
+
+        if (qcombo->combo_index == combo_index) {
+            combo_t *combo = &key_combos[combo_index];
+            DISABLE_COMBO(combo);
+
+            if (i == combo_buffer_read) {
+                INCREMENT_MOD(combo_buffer_read);
+            }
+            break;
+        }
+        INCREMENT_MOD(i);
     }
+}
+
+void apply_combo(uint16_t combo_index, combo_t *combo) {
+    /* Apply combo's result keycode to the last chord key of the combo and
+     * disable the other keys. */
 
-    /* Continue processing if not a combo key */
-    if (-1 == (int8_t)index) return false;
+    if (COMBO_DISABLED(combo)) { return; }
 
-    bool is_combo_active = is_active;
+    // state to check against so we find the last key of the combo from the buffer
+#if defined(EXTRA_EXTRA_LONG_COMBOS)
+    uint32_t state = 0;
+#elif defined(EXTRA_LONG_COMBOS)
+    uint16_t state = 0;
+#else
+    uint8_t state = 0;
+#endif
+
+    for (uint8_t key_buffer_i = 0; key_buffer_i < key_buffer_size; key_buffer_i++) {
+
+        queued_record_t *qrecord = &key_buffer[key_buffer_i];
+        keyrecord_t *record = &qrecord->record;
+        uint16_t keycode = qrecord->keycode;
+
+        uint8_t key_count = 0;
+        uint16_t key_index = -1;
+        _find_key_index_and_count(combo->keys, keycode, &key_index, &key_count);
+
+        if (-1 == (int16_t)key_index) {
+            // key not part of this combo
+            continue;
+        }
 
-    if (record->event.pressed) {
-        KEY_STATE_DOWN(index);
+        KEY_STATE_DOWN(state, key_index);
+        if (ALL_COMBO_KEYS_ARE_DOWN(state, key_count)) {
+            // this in the end executes the combo when the key_buffer is dumped.
+            record->keycode = combo->keycode;
+            record->event.key = COMBO_KEY_POS;
 
-        if (is_combo_active) {
-            if (ALL_COMBO_KEYS_ARE_DOWN) { /* Combo was pressed */
-                send_combo(combo->keycode, true);
-                drop_buffer = true;
+            qrecord->combo_index = combo_index;
+            ACTIVATE_COMBO(combo);
+
+            break;
+        } else {
+            // key was part of the combo but not the last one, "disable" it
+            // by making it a TICK event.
+            record->event.time = 0;
+        }
+
+    }
+    drop_combo_from_buffer(combo_index);
+}
+
+static inline void apply_combos(void) {
+    // Apply all buffered normal combos.
+    for (uint8_t i = combo_buffer_read;
+            i != combo_buffer_write;
+            INCREMENT_MOD(i)) {
+
+        queued_combo_t *buffered_combo = &combo_buffer[i];
+        combo_t *combo = &key_combos[buffered_combo->combo_index];
+
+#ifdef COMBO_MUST_TAP_PER_COMBO
+        if (get_combo_must_tap(buffered_combo->combo_index, combo)) {
+            // Tap-only combos are applied on key release only, so let's drop 'em here.
+            drop_combo_from_buffer(buffered_combo->combo_index);
+            continue;
+        }
+#endif
+        apply_combo(buffered_combo->combo_index, combo);
+    }
+    dump_key_buffer();
+    clear_combos();
+}
+
+combo_t* overlaps(combo_t *combo1, combo_t *combo2) {
+    /* Checks if the combos overlap and returns the combo that should be
+     * dropped from the combo buffer.
+     * The combo that has less keys will be dropped. If they have the same
+     * amount of keys, drop combo1. */
+
+    uint8_t idx1 = 0, idx2 = 0;
+    uint16_t key1, key2;
+    bool overlaps = false;
+
+    while ((key1 = pgm_read_word(&combo1->keys[idx1])) != COMBO_END) {
+        idx2 = 0;
+        while ((key2 = pgm_read_word(&combo2->keys[idx2])) != COMBO_END) {
+            if (key1 == key2) overlaps = true;
+            idx2 += 1;
+        }
+        idx1 += 1;
+    }
+
+    if (!overlaps) return NULL;
+    if (idx2 < idx1) return combo2;
+    return combo1;
+}
+
+static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *record, uint16_t combo_index) {
+    uint8_t key_count = 0;
+    uint16_t key_index = -1;
+    _find_key_index_and_count(combo->keys, keycode, &key_index, &key_count);
+
+    /* Continue processing if key isn't part of current combo. */
+    if (-1 == (int16_t)key_index) {
+        return false;
+    }
+
+    bool key_is_part_of_combo = !COMBO_DISABLED(combo) && is_combo_enabled();
+
+    if (record->event.pressed && key_is_part_of_combo) {
+        uint16_t time = _get_combo_term(combo_index, combo);
+        if (!COMBO_ACTIVE(combo)) {
+            KEY_STATE_DOWN(combo->state, key_index);
+            if (longest_term < time) {
+                longest_term = time;
             }
         }
+        if (ALL_COMBO_KEYS_ARE_DOWN(COMBO_STATE(combo), key_count)) {
+            /* Combo was fully pressed */
+            /* Buffer the combo so we can fire it after COMBO_TERM */
+
+#ifndef COMBO_NO_TIMER
+            /* Don't buffer this combo if its combo term has passed. */
+            if (timer && timer_elapsed(timer) > time) {
+                DISABLE_COMBO(combo);
+                return true;
+            } else
+#endif
+            {
+
+                // disable readied combos that overlap with this combo
+                combo_t *drop = NULL;
+                for (uint8_t combo_buffer_i = combo_buffer_read;
+                        combo_buffer_i != combo_buffer_write;
+                        INCREMENT_MOD(combo_buffer_i)) {
+
+                    queued_combo_t *qcombo = &combo_buffer[combo_buffer_i];
+                    combo_t *buffered_combo = &key_combos[qcombo->combo_index];
+
+                    if ((drop = overlaps(buffered_combo, combo))) {
+                        DISABLE_COMBO(drop);
+                        if (drop == combo) {
+                            // stop checking for overlaps if dropped combo was current combo.
+                            break;
+                        } else if (combo_buffer_i == combo_buffer_read && drop == buffered_combo) {
+                            /* Drop the disabled buffered combo from the buffer if
+                             * it is in the beginning of the buffer. */
+                            INCREMENT_MOD(combo_buffer_read);
+                        }
+                    }
+
+                }
+
+                if (drop != combo) {
+                    // save this combo to buffer
+                    combo_buffer[combo_buffer_write] = (queued_combo_t){
+                        .combo_index=combo_index,
+                    };
+                    INCREMENT_MOD(combo_buffer_write);
+
+                    // get possible longer waiting time for tap-/hold-only combos.
+                    longest_term = _get_wait_time(combo_index, combo);
+                }
+            } // if timer elapsed end
+
+        }
     } else {
-        if (ALL_COMBO_KEYS_ARE_DOWN) { /* Combo was released */
-            send_combo(combo->keycode, false);
+        // chord releases
+        if (!COMBO_ACTIVE(combo) && ALL_COMBO_KEYS_ARE_DOWN(COMBO_STATE(combo), key_count)) {
+            /* First key quickly released */
+            if (COMBO_DISABLED(combo) || _get_combo_must_hold(combo_index, combo)) {
+                // combo wasn't tappable, disable it and drop it from buffer.
+                drop_combo_from_buffer(combo_index);
+                key_is_part_of_combo = false;
+            }
+#ifdef COMBO_MUST_TAP_PER_COMBO
+            else if (get_combo_must_tap(combo_index, combo)) {
+                // immediately apply tap-only combo
+                apply_combo(combo_index, combo);
+                apply_combos(); // also apply other prepared combos and dump key buffer
+#    ifdef COMBO_PROCESS_KEY_RELEASE
+                if (process_combo_key_release(combo_index, combo, key_index, keycode)) {
+                    release_combo(combo_index, combo);
+                }
+#    endif
+            }
+#endif
+        } else if (COMBO_ACTIVE(combo)
+                && ONLY_ONE_KEY_IS_DOWN(COMBO_STATE(combo))
+                && KEY_NOT_YET_RELEASED(COMBO_STATE(combo), key_index)
+                ) {
+            /* last key released */
+            release_combo(combo_index, combo);
+            key_is_part_of_combo = true;
+
+#ifdef COMBO_PROCESS_KEY_RELEASE
+            process_combo_key_release(combo_index, combo, key_index, keycode);
+#endif
+        } else if (COMBO_ACTIVE(combo)
+                && KEY_NOT_YET_RELEASED(COMBO_STATE(combo), key_index)
+                ) {
+            /* first or middle key released */
+            key_is_part_of_combo = true;
+
+#ifdef COMBO_PROCESS_KEY_RELEASE
+            if (process_combo_key_release(combo_index, combo, key_index, keycode)) {
+                release_combo(combo_index, combo);
+            }
+#endif
         } else {
-            /* continue processing without immediately returning */
-            is_combo_active = false;
+            /* The released key was part of an incomplete combo */
+            key_is_part_of_combo = false;
         }
 
-        KEY_STATE_UP(index);
+        KEY_STATE_UP(combo->state, key_index);
     }
 
-    return is_combo_active;
+    return key_is_part_of_combo;
 }
 
-#define NO_COMBO_KEYS_ARE_DOWN (0 == combo->state)
-
 bool process_combo(uint16_t keycode, keyrecord_t *record) {
     bool is_combo_key          = false;
-    drop_buffer                = false;
     bool no_combo_keys_pressed = true;
 
     if (keycode == CMB_ON && record->event.pressed) {
@@ -141,65 +476,82 @@ bool process_combo(uint16_t keycode, keyrecord_t *record) {
         return true;
     }
 
-    if (!is_combo_enabled()) {
-        return true;
-    }
-#ifndef COMBO_VARIABLE_LEN
-    for (current_combo_index = 0; current_combo_index < COMBO_COUNT; ++current_combo_index) {
-#else
-    for (current_combo_index = 0; current_combo_index < COMBO_LEN; ++current_combo_index) {
+#ifdef COMBO_ONLY_FROM_LAYER
+    /* Only check keycodes from one layer. */
+    keycode = keymap_key_to_keycode(COMBO_ONLY_FROM_LAYER, record->event.key);
 #endif
-        combo_t *combo = &key_combos[current_combo_index];
-        is_combo_key |= process_single_combo(combo, keycode, record);
-        no_combo_keys_pressed = no_combo_keys_pressed && NO_COMBO_KEYS_ARE_DOWN;
+
+    for (uint16_t idx = 0; idx < COMBO_LEN; ++idx) {
+        combo_t *combo = &key_combos[idx];
+        is_combo_key |= process_single_combo(combo, keycode, record, idx);
+        no_combo_keys_pressed = no_combo_keys_pressed && (NO_COMBO_KEYS_ARE_DOWN || COMBO_ACTIVE(combo) || COMBO_DISABLED(combo));
     }
 
-    if (drop_buffer) {
-        /* buffer is only dropped when we complete a combo, so we refresh the timer
-         * here */
-        timer = timer_read();
-        dump_key_buffer(false);
-    } else if (!is_combo_key) {
-        /* if no combos claim the key we need to emit the keybuffer */
-        dump_key_buffer(true);
-
-        // reset state if there are no combo keys pressed at all
-        if (no_combo_keys_pressed) {
-            timer     = 0;
-            is_active = true;
-        }
-    } else if (record->event.pressed && is_active) {
-        /* otherwise the key is consumed and placed in the buffer */
+    if (record->event.pressed && is_combo_key) {
+#ifndef COMBO_NO_TIMER
+#   ifdef COMBO_STRICT_TIMER
+        if (!timer) {
+            // timer is set only on the first key
+            timer = timer_read();
+        }
+#   else
         timer = timer_read();
+#   endif
+#endif
 
-        if (buffer_size < MAX_COMBO_LENGTH) {
-#ifdef COMBO_ALLOW_ACTION_KEYS
-            key_buffer[buffer_size++] = *record;
-#else
-            key_buffer[buffer_size++] = keycode;
+        if (key_buffer_size < COMBO_KEY_BUFFER_LENGTH) {
+            key_buffer[key_buffer_size++] = (queued_record_t){
+                .record = *record,
+                .keycode = keycode,
+                .combo_index = -1, // this will be set when applying combos
+            };
+        }
+    } else {
+        if (combo_buffer_read != combo_buffer_write) {
+            // some combo is prepared
+            apply_combos();
+        } else {
+            // reset state if there are no combo keys pressed at all
+            dump_key_buffer();
+#ifndef COMBO_NO_TIMER
+            timer = 0;
 #endif
+            clear_combos();
         }
     }
-
     return !is_combo_key;
 }
 
-void matrix_scan_combo(void) {
-    if (b_combo_enable && is_active && timer && timer_elapsed(timer) > COMBO_TERM) {
-        /* This disables the combo, meaning key events for this
-         * combo will be handled by the next processors in the chain
-         */
-        is_active = false;
-        dump_key_buffer(true);
+void combo_task(void) {
+    if (!b_combo_enable) {
+        return;
     }
+
+#ifndef COMBO_NO_TIMER
+    if (timer && timer_elapsed(timer) > longest_term) {
+        if (combo_buffer_read != combo_buffer_write) {
+            apply_combos();
+            longest_term = 0;
+            timer = 0;
+        } else {
+            dump_key_buffer();
+            timer = 0;
+            clear_combos();
+        }
+    }
+#endif
 }
 
 void combo_enable(void) { b_combo_enable = true; }
 
 void combo_disable(void) {
-    b_combo_enable = is_active = false;
+#ifndef COMBO_NO_TIMER
     timer                      = 0;
-    dump_key_buffer(true);
+#endif
+    b_combo_enable = false;
+    combo_buffer_read = combo_buffer_write;
+    clear_combos();
+    dump_key_buffer();
 }
 
 void combo_toggle(void) {