From 6f0bc5ed3b3ba5f43e1914dbe928cfb8252296f8 Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Fri, 6 Mar 2026 21:28:09 +0100 Subject: [PATCH 01/11] Key History: Implement actual key history --- right/src/key_history.c | 45 ++++++++++++++++++++++--------- right/src/key_history.h | 4 +-- right/src/key_states.h | 2 +- right/src/layer_switcher.c | 2 +- right/src/macros/core.c | 2 +- right/src/postponer.c | 2 +- right/src/secondary_role_driver.c | 2 +- 7 files changed, 39 insertions(+), 20 deletions(-) diff --git a/right/src/key_history.c b/right/src/key_history.c index 4734dc30a..50c2bf313 100644 --- a/right/src/key_history.c +++ b/right/src/key_history.c @@ -2,6 +2,10 @@ #include "key_history.h" #include "postponer.h" +#define HISTORY_SIZE 2 +#define LAST (position % HISTORY_SIZE) +#define POS(p) ((position - p) % HISTORY_SIZE) + typedef enum { DoubletapState_Blocked, DoubletapState_First, @@ -14,20 +18,23 @@ typedef struct { uint8_t keyActivationId; uint32_t timestamp; doubletap_state_t doubletapState; -} previous_key_event_type_t; +} key_press_event_t; -static previous_key_event_type_t lastPress; +static key_press_event_t history[HISTORY_SIZE]; +static uint8_t position = 0; void KeyHistory_RecordPress(const key_state_t *keyState) { + const key_press_event_t * const lastPress = &history[LAST]; const bool isMultitap = - keyState == lastPress.keyState - && lastPress.doubletapState != DoubletapState_Blocked - && CurrentPostponedTime < lastPress.timestamp + Cfg.DoubletapTimeout; + keyState == lastPress->keyState + && lastPress->doubletapState != DoubletapState_Blocked + && CurrentPostponedTime < lastPress->timestamp + Cfg.DoubletapTimeout; const bool isDoubletap = isMultitap && - (lastPress.doubletapState == DoubletapState_First || lastPress.doubletapState == DoubletapState_Multitap); + (lastPress->doubletapState == DoubletapState_First || lastPress->doubletapState == DoubletapState_Multitap); - lastPress = (previous_key_event_type_t){ + ++position; + history[LAST] = (key_press_event_t) { .keyState = keyState, .keyActivationId = keyState->activationId, .timestamp = CurrentPostponedTime, @@ -39,17 +46,29 @@ void KeyHistory_RecordPress(const key_state_t *keyState) void KeyHistory_RecordRelease(const key_state_t *keyState) { - if (keyState != lastPress.keyState) { - lastPress.doubletapState = DoubletapState_Blocked; + if (keyState != history[LAST].keyState) { + history[LAST].doubletapState = DoubletapState_Blocked; } } -bool KeyHistory_WasLastDoubletap() +bool KeyHistory_WasDoubletap(const key_state_t *keyState, uint8_t activationId) { - return lastPress.doubletapState == DoubletapState_Doubletap; + for (uint8_t i = 0; i < HISTORY_SIZE; ++i) { + const key_press_event_t * const event = &history[POS(i)]; + if (event->keyState == keyState && event->keyActivationId == activationId) { + return event->doubletapState == DoubletapState_Doubletap; + } + } + return false; } -bool KeyHistory_WasLastMultitap() +bool KeyHistory_WasMultitap(const key_state_t *keyState, uint8_t activationId) { - return lastPress.doubletapState >= DoubletapState_Multitap; + for (uint8_t i = 0; i < HISTORY_SIZE; ++i) { + const key_press_event_t * const event = &history[POS(i)]; + if (event->keyState == keyState && event->keyActivationId == activationId) { + return event->doubletapState >= DoubletapState_Multitap; + } + } + return false; } \ No newline at end of file diff --git a/right/src/key_history.h b/right/src/key_history.h index cba1547f3..3a0f1636d 100644 --- a/right/src/key_history.h +++ b/right/src/key_history.h @@ -15,8 +15,8 @@ void KeyHistory_RecordPress(const key_state_t *keyState); void KeyHistory_RecordRelease(const key_state_t *keyState); -bool KeyHistory_WasLastDoubletap(); -bool KeyHistory_WasLastMultitap(); +bool KeyHistory_WasDoubletap(const key_state_t *keyState, uint8_t activationId); +bool KeyHistory_WasMultitap(const key_state_t *keyState, uint8_t activationId); #endif \ No newline at end of file diff --git a/right/src/key_states.h b/right/src/key_states.h index 97ac8dbdb..ff9b1d0dc 100644 --- a/right/src/key_states.h +++ b/right/src/key_states.h @@ -43,7 +43,7 @@ bool debouncing : 1; secondary_role_state_t secondaryState : 2; bool padding : 1; // This allows the KEY_INACTIVE() macro to not trigger false because of sequence - uint8_t activationId: 4; + uint8_t activationId : 4; } key_state_t; // Variables: diff --git a/right/src/layer_switcher.c b/right/src/layer_switcher.c index aca3f70c3..49399a31c 100644 --- a/right/src/layer_switcher.c +++ b/right/src/layer_switcher.c @@ -117,7 +117,7 @@ void LayerSwitcher_DoubleTapToggle(layer_id_t layer, key_state_t *keyState) { if(KeyState_ActivatedNow(keyState)) { LayerStack_LegacyPop(layer); - if (KeyHistory_WasLastDoubletap()) { + if (KeyHistory_WasDoubletap(keyState, keyState->activationId)) { LayerStack_LegacyPush(layer); doubleTapSwitchLayerKey = keyState; doubleTapSwitchLayerTriggerTime = Timer_GetCurrentTime(); diff --git a/right/src/macros/core.c b/right/src/macros/core.c index d06c088e6..fe3125ee1 100644 --- a/right/src/macros/core.c +++ b/right/src/macros/core.c @@ -463,7 +463,7 @@ uint8_t initMacro( S->ms.currentMacroStartTime = CurrentPostponedTime; S->ms.currentMacroArgumentOffset = argumentOffset; S->ms.parentMacroSlot = parentMacroSlot; - S->ms.isDoubletap = keyState != NULL && KeyHistory_WasLastDoubletap(); + S->ms.isDoubletap = keyState != NULL && KeyHistory_WasDoubletap(keyState, keyActivationId); // If inline text is provided, set up the action before resetToAddressZero if (inlineText != NULL) { diff --git a/right/src/postponer.c b/right/src/postponer.c index aa5689b20..5a64f7ac2 100644 --- a/right/src/postponer.c +++ b/right/src/postponer.c @@ -33,7 +33,7 @@ uint8_t Postponer_LastKeyMods = 0; //static uint8_t cyclesUntilActivation = 0; static uint32_t lastPressTime; -#define POS(idx) ((bufferPosition + POSTPONER_BUFFER_SIZE + (idx)) % POSTPONER_BUFFER_SIZE) +#define POS(idx) ((bufferPosition + (idx)) % POSTPONER_BUFFER_SIZE) uint32_t CurrentPostponedTime = 0; diff --git a/right/src/secondary_role_driver.c b/right/src/secondary_role_driver.c index dec039d4c..16f3d906f 100644 --- a/right/src/secondary_role_driver.c +++ b/right/src/secondary_role_driver.c @@ -250,7 +250,7 @@ static void startResolution( { // store current state currentlyResolving = true; - isDoubletap = KeyHistory_WasLastMultitap(); + isDoubletap = KeyHistory_WasMultitap(keyState, keyState->activationId); resolutionKey = keyState; resolutionStartTime = CurrentPostponedTime; resolutionCallerIsMacroEngine = isMacroResolution; From 5beefea1cad58a16f742fba91eb9f6f9d013ffc8 Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Fri, 6 Mar 2026 21:52:08 +0100 Subject: [PATCH 02/11] Macros: If not more readable, it should be faster --- right/src/macros/commands.c | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/right/src/macros/commands.c b/right/src/macros/commands.c index d3416c6ec..9fb6d8d95 100644 --- a/right/src/macros/commands.c +++ b/right/src/macros/commands.c @@ -136,18 +136,22 @@ static bool isCurrentMacroPostponing() */ bool Macros_CurrentMacroKeyIsActive() { + if (S->ms.oneShot == 1) { + return true; + } if (S->ms.currentMacroKey == NULL) { - return S->ms.oneShot == 1; + return false; + } + if (S->ms.currentMacroKey->activationId != S->ms.keyActivationId) { + return false; + } + if (!KeyState_Active(S->ms.currentMacroKey)) { + return false; } if (isCurrentMacroPostponing()) { - bool isSameActivation = (S->ms.currentMacroKey->activationId == S->ms.keyActivationId); - bool keyIsActive = (KeyState_Active(S->ms.currentMacroKey) && !PostponerQuery_IsKeyReleased(S->ms.currentMacroKey)); - return (isSameActivation && keyIsActive) || S->ms.oneShot == 1; - } else { - bool isSameActivation = (S->ms.currentMacroKey->activationId == S->ms.keyActivationId); - bool keyIsActive = KeyState_Active(S->ms.currentMacroKey); - return (isSameActivation && keyIsActive) || S->ms.oneShot == 1; + return !PostponerQuery_IsKeyReleased(S->ms.currentMacroKey); } + return true; } static macro_result_t writeNum(uint32_t a) From 85d2610ed341eed2fca81253318f33a6ff749abd Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Sat, 7 Mar 2026 08:10:44 +0100 Subject: [PATCH 03/11] Doubletap: N-tap support, theoretically, with easier logic --- right/src/key_history.c | 46 ++++++++----------------------- right/src/key_history.h | 3 +- right/src/layer_switcher.c | 2 +- right/src/macros/core.c | 2 +- right/src/secondary_role_driver.c | 2 +- 5 files changed, 16 insertions(+), 39 deletions(-) diff --git a/right/src/key_history.c b/right/src/key_history.c index 50c2bf313..11ee9b82e 100644 --- a/right/src/key_history.c +++ b/right/src/key_history.c @@ -6,18 +6,12 @@ #define LAST (position % HISTORY_SIZE) #define POS(p) ((position - p) % HISTORY_SIZE) -typedef enum { - DoubletapState_Blocked, - DoubletapState_First, - DoubletapState_Multitap, - DoubletapState_Doubletap, -} doubletap_state_t; - typedef struct { const key_state_t *keyState; - uint8_t keyActivationId; uint32_t timestamp; - doubletap_state_t doubletapState; + uint8_t multiTapCount; + uint8_t keyActivationId : 4; + bool multiTapBreaker : 1; } key_press_event_t; static key_press_event_t history[HISTORY_SIZE]; @@ -28,47 +22,31 @@ void KeyHistory_RecordPress(const key_state_t *keyState) const key_press_event_t * const lastPress = &history[LAST]; const bool isMultitap = keyState == lastPress->keyState - && lastPress->doubletapState != DoubletapState_Blocked + && !lastPress->multiTapBreaker && CurrentPostponedTime < lastPress->timestamp + Cfg.DoubletapTimeout; - const bool isDoubletap = isMultitap && - (lastPress->doubletapState == DoubletapState_First || lastPress->doubletapState == DoubletapState_Multitap); ++position; history[LAST] = (key_press_event_t) { .keyState = keyState, .keyActivationId = keyState->activationId, .timestamp = CurrentPostponedTime, - .doubletapState = isDoubletap ? DoubletapState_Doubletap : - isMultitap ? DoubletapState_Multitap : - DoubletapState_First, + .multiTapCount = 1 + (isMultitap ? lastPress->multiTapCount : 0), + .multiTapBreaker = false, }; } void KeyHistory_RecordRelease(const key_state_t *keyState) { if (keyState != history[LAST].keyState) { - history[LAST].doubletapState = DoubletapState_Blocked; + history[LAST].multiTapBreaker = true; } } -bool KeyHistory_WasDoubletap(const key_state_t *keyState, uint8_t activationId) -{ - for (uint8_t i = 0; i < HISTORY_SIZE; ++i) { - const key_press_event_t * const event = &history[POS(i)]; - if (event->keyState == keyState && event->keyActivationId == activationId) { - return event->doubletapState == DoubletapState_Doubletap; - } - } - return false; -} - -bool KeyHistory_WasMultitap(const key_state_t *keyState, uint8_t activationId) -{ - for (uint8_t i = 0; i < HISTORY_SIZE; ++i) { - const key_press_event_t * const event = &history[POS(i)]; - if (event->keyState == keyState && event->keyActivationId == activationId) { - return event->doubletapState >= DoubletapState_Multitap; +uint8_t KeyHistory_GetMultitapCount(const key_state_t *keyState, uint8_t activationId) { + for (uint8_t i = 0; i < HISTORY_SIZE - 1; ++i) { + if(history[POS(i)].keyState == keyState && history[POS(i)].keyActivationId == activationId) { + return history[POS(i)].multiTapCount; } } - return false; + return 0; } \ No newline at end of file diff --git a/right/src/key_history.h b/right/src/key_history.h index 3a0f1636d..b94ccca05 100644 --- a/right/src/key_history.h +++ b/right/src/key_history.h @@ -15,8 +15,7 @@ void KeyHistory_RecordPress(const key_state_t *keyState); void KeyHistory_RecordRelease(const key_state_t *keyState); -bool KeyHistory_WasDoubletap(const key_state_t *keyState, uint8_t activationId); -bool KeyHistory_WasMultitap(const key_state_t *keyState, uint8_t activationId); +uint8_t KeyHistory_GetMultitapCount(const key_state_t *keyState, uint8_t activationId); #endif \ No newline at end of file diff --git a/right/src/layer_switcher.c b/right/src/layer_switcher.c index 49399a31c..b128f686a 100644 --- a/right/src/layer_switcher.c +++ b/right/src/layer_switcher.c @@ -117,7 +117,7 @@ void LayerSwitcher_DoubleTapToggle(layer_id_t layer, key_state_t *keyState) { if(KeyState_ActivatedNow(keyState)) { LayerStack_LegacyPop(layer); - if (KeyHistory_WasDoubletap(keyState, keyState->activationId)) { + if (KeyHistory_GetMultitapCount(keyState, keyState->activationId) % 2 == 0) { LayerStack_LegacyPush(layer); doubleTapSwitchLayerKey = keyState; doubleTapSwitchLayerTriggerTime = Timer_GetCurrentTime(); diff --git a/right/src/macros/core.c b/right/src/macros/core.c index fe3125ee1..6ae9abee3 100644 --- a/right/src/macros/core.c +++ b/right/src/macros/core.c @@ -463,7 +463,7 @@ uint8_t initMacro( S->ms.currentMacroStartTime = CurrentPostponedTime; S->ms.currentMacroArgumentOffset = argumentOffset; S->ms.parentMacroSlot = parentMacroSlot; - S->ms.isDoubletap = keyState != NULL && KeyHistory_WasDoubletap(keyState, keyActivationId); + S->ms.isDoubletap = keyState != NULL && KeyHistory_GetMultitapCount(keyState, keyActivationId) % 2 == 0; // If inline text is provided, set up the action before resetToAddressZero if (inlineText != NULL) { diff --git a/right/src/secondary_role_driver.c b/right/src/secondary_role_driver.c index 16f3d906f..371a90437 100644 --- a/right/src/secondary_role_driver.c +++ b/right/src/secondary_role_driver.c @@ -250,7 +250,7 @@ static void startResolution( { // store current state currentlyResolving = true; - isDoubletap = KeyHistory_WasMultitap(keyState, keyState->activationId); + isDoubletap = KeyHistory_GetMultitapCount(keyState, keyState->activationId) > 1; resolutionKey = keyState; resolutionStartTime = CurrentPostponedTime; resolutionCallerIsMacroEngine = isMacroResolution; From 09ed5b7039a5b511285833d9e1fd0253af3c021a Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Sat, 7 Mar 2026 21:18:32 +0100 Subject: [PATCH 04/11] Macros: ifDoubletap can now test for any number of taps --- doc-dev/reference-manual.md | 6 +++--- right/src/key_history.c | 2 +- right/src/macros/commands.c | 18 ++++++++++++++---- right/src/macros/core.c | 2 +- right/src/macros/core.h | 6 ++++-- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/doc-dev/reference-manual.md b/doc-dev/reference-manual.md index f6ae84b22..099285f07 100644 --- a/doc-dev/reference-manual.md +++ b/doc-dev/reference-manual.md @@ -202,7 +202,7 @@ CONDITION = {ifShortcut | ifNotShortcut} [IFSHORTCUT_OPTIONS]* [KEYID]+ CONDITION = {ifGesture | ifNotGesture} [IFSHORTCUT_OPTIONS]* [KEYID]+ CONDITION = {ifPrimary | ifSecondary} [ simpleStrategy | advancedStrategy | ignoreTriggersFromSameHalf | acceptTriggersFromSameHalf ] CONDITION = {ifHold | ifTap} -CONDITION = {ifDoubletap | ifNotDoubletap} +CONDITION = {ifDoubletap | ifNotDoubletap} [taps ] CONDITION = {ifInterrupted | ifNotInterrupted} CONDITION = {ifReleased | ifNotReleased} CONDITION = {ifKeyActive | ifNotKeyActive} KEYID @@ -541,7 +541,7 @@ Conditions are checked before processing the rest of the command. If the conditi - `if BOOL` allows switching based on a custom expression. E.g., `if ($keystrokeDelay > 10) ...` - `else` condition is true if the previous command ended due to a failed condition. -- `ifDoubletap/ifNotDoubletap` is true if the macro was started at most 300ms after the start of another instance of the same macro. +- `ifDoubletap/ifNotDoubletap` is true if the macro was started at most 300ms after the start of another instance of the same macro. If `taps` is provided, test for that number of rapid taps in a row, for example `ifDoubletap taps 3` will test for a tripletap. - `ifInterrupted/ifNotInterrupted` is true if a keystroke action or mouse action was triggered during macro runtime. Allows fake implementation of secondary roles. Also allows interruption of cycles. - `ifReleased/ifNotReleased` is true if the key which activated current macro has been released. If the key has been physically released but the release has been postponed by another key, the conditien yields false. If the key has been physically released and the postponing mode was initiated by this macro (e.g., `postponeKeys ifReleased goTo ($currentAddress+2)`), it returns non-postponed release state (i.e., true if there's a matching release event in the postponing queue). - `ifPending/ifNotPending ` is true if there is at least `n` postponed keys in the postponing queue. In context of postponing mechanism, this condition acts similar in place of ifInterrupted. @@ -620,7 +620,7 @@ Key actions can be parametrized with macro arguments. These arguments can be exp This allows the user to trigger chorded shortcuts in an arbitrary order (all at the "same" time). E.g., if `A+Ctrl` is pressed instead of `Ctrl+A`, the keyboard will still send `Ctrl+A` if the two key presses follow within the specified time. - `set autoShiftDelay 0 |