From 71d99323e057cab0a5824e8899e9a34912c84e5c Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Sat, 21 Mar 2026 21:30:50 +0100 Subject: [PATCH 01/13] Chords: Can now store and fetch chords --- right/src/CMakeLists.txt | 1 + right/src/chords.c | 160 +++++++++++++++++++++++++++++++++ right/src/chords.h | 30 +++++++ right/src/keymap.c | 2 + right/src/macros/commands.c | 6 +- right/src/macros/set_command.c | 79 ++++++++++++++-- 6 files changed, 270 insertions(+), 8 deletions(-) create mode 100644 right/src/chords.c create mode 100644 right/src/chords.h diff --git a/right/src/CMakeLists.txt b/right/src/CMakeLists.txt index 8a42ceebd..15947ad40 100644 --- a/right/src/CMakeLists.txt +++ b/right/src/CMakeLists.txt @@ -21,6 +21,7 @@ endif() target_sources(${PROJECT_NAME} PRIVATE caret_config.c + chords.c config_manager.c debug.c eeprom.c diff --git a/right/src/chords.c b/right/src/chords.c new file mode 100644 index 000000000..34a2af82a --- /dev/null +++ b/right/src/chords.c @@ -0,0 +1,160 @@ +#include "chords.h" +#include "macros/status_buffer.h" + +#define MAX_CHORDS_COUNT 64 + +chord_def_t Chords[MAX_CHORDS_COUNT]; +uint8_t ChordCount = 0; + +static inline void sortKeys(chord_keys_t keys, uint8_t keyCount) +{ + // Some kind of bubblesort, I dunno + bool recheck = true; + while ( recheck ) { + recheck = false; + for (int i = 0; i < keyCount - 1; ++i) { + if (keys[i] > keys[i + 1] ) { + uint8_t temp = keys[i]; + keys[i] = keys[i + 1]; + keys[i + 1] = temp; + if (i < keyCount - 1) { + // If the keys we just switched were not the last keys, we need to go again. + recheck = true; + } + } + } + } +} + +/* +Chords are stored sorted as follows: +First by layer, direction doesn't matter, to group by layer +Then ascending by key count - ascending, this matters +Then by key id list, direction doesn't matter, only that they're sorted consistently +*/ +static inline int8_t compareToChord(const chord_def_t *left, uint8_t layer, uint8_t keyCount, const chord_keys_t keys) { + // These could be collapsed and optimized more if we're willing to do union with a bitfield, but that's hacky + if (left->layer != layer) { + return left->layer - layer; + } + if (left->keyCount != keyCount) { + return left->keyCount - keyCount; + } + return memcmp(left->keys, keys, keyCount); +} + +bool Chords_TryAddChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount, key_action_t *action) +{ + sortKeys(keys, keyCount); + + // Find a spot + uint8_t i = 0; + int8_t chordCmp = 1; // Initialize to not zero to not overwrite first on empty list + while (i < ChordCount && (chordCmp = compareToChord(&Chords[i], layer, keyCount, keys)) < 0) {++i;} + + if (chordCmp == 0) { + // We already have the chord, erase or update, depending on provided action + if (action->type == KeyActionType_None) { + // Erase the chord and return + memmove(&Chords[i], &Chords[i + 1], sizeof(chord_def_t) * (--ChordCount - i)); + memset(&Chords[ChordCount], 0, sizeof(chord_def_t)); + return true; + } + // Just overwrite action, the rest of the chord is already in place + Chords[i].action = *action; + return true; + } + + if (action->type == KeyActionType_None) { + // We were told to remove it, but we don't have it, great success! + return true; + } + + // Check if there is room if we're not removing a chord + if (action->type != KeyActionType_None && ChordCount >= MAX_CHORDS_COUNT) { + // No room + return false; + } + + // We do not have the chord, but we know where to put it now + // Insert the chord. + memmove(&Chords[i + 1], &Chords[i], sizeof(chord_def_t) * (ChordCount++ - i)); + chord_def_t *chord = &Chords[i]; + memcpy(chord->keys, keys, MAX_CHORD_KEYS); + chord->keyCount = keyCount; + chord->layer = layer; + chord->action = *action; + return true; +} + +// This function assumes that both lists of keys are sorted smallest to largest. +static bool isPartialOfChord(const chord_def_t *large, chord_keys_t keys, uint8_t keyCount) +{ + uint8_t i = 0, j = 0; + for (; i < keyCount; ++i) { + // We can skip keys in the chord to see if all the provided keys exist + while (keys[i] > large->keys[j]) { + ++j; + } + if (keys[i] != large->keys[j]) { + // A provided key was not in the chord + // The provided key cannot grow into the chord + return false; + } + ++j; + } + return true; +} + +chord_search_result_t Chords_TryGetChordAction(key_action_t *outAction, uint8_t layer, chord_keys_t keys, uint8_t keyCount) +{ + sortKeys(keys, keyCount); + + uint8_t i = 0; + bool matched = false; + bool partial = false; + + // First maybe find the chord + for (; i < ChordCount; ++i) { + int8_t chordCmp = compareToChord(&Chords[i], layer, keyCount, keys); + if (chordCmp < 0) continue; + if (chordCmp == 0) { + // We found the longest completed chord so far + *outAction = Chords[i].action; + matched = true; + } + break; + } + + // We now may have a match, but we can't return it yet. + // We need to check if we have a potential for a longer chord + // Because of the sort order of the chord list, all chords after this slot + // with the same layer are same length or longer than the desired chord. + + // Now iterate until we get to the longer chords in the layer because + // we want to see if there are potential matches in them + while (i < ChordCount && Chords[i].layer == layer && Chords[i].keyCount == keyCount) {++i;} + + // i is now at the first longer chord in the layer, or the first key in another layer, on at list's end + // Now we look for potential matches with more keys + while (i < ChordCount && Chords[i].layer == layer) { + if (isPartialOfChord(&Chords[i++], keys, keyCount)) { + // If we have a partial chord + partial = true; + break; + } + } + + if (matched) { + // We have found an exact match + return partial ? ChordSearch_MatchAndPartial : ChordSearch_FinalMatch; + } + if (partial) { + return ChordSearch_PartialOnly; + } + return ChordSearch_Nothing; +} + +void Chords_ResetChords() { + ChordCount = 0; +} \ No newline at end of file diff --git a/right/src/chords.h b/right/src/chords.h new file mode 100644 index 000000000..0be611135 --- /dev/null +++ b/right/src/chords.h @@ -0,0 +1,30 @@ +#ifndef __CHORDS_H__ +#define __CHORDS_H__ + +#include "key_action.h" + +#define MAX_CHORD_KEYS 5 +typedef uint8_t chord_keys_t[MAX_CHORD_KEYS]; + +typedef enum { + ChordSearch_Nothing, // There is no chord which could match provided data + ChordSearch_PartialOnly, // There are no finished chords, but some partially finished + ChordSearch_MatchAndPartial, // There was an exact match, but also longer chords + ChordSearch_FinalMatch, // There was an exact match and no other potential matches +} chord_search_result_t; + +typedef struct { + chord_keys_t keys; + + key_action_t action; + + uint8_t layer : 4; + + uint8_t keyCount : 3; +} ATTR_PACKED chord_def_t; + +bool Chords_TryAddChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount, key_action_t *action); +chord_search_result_t Chords_TryGetChordAction(key_action_t *outAction, uint8_t layer, chord_keys_t keys, uint8_t keyCount); +void Chords_ResetChords(); + +#endif diff --git a/right/src/keymap.c b/right/src/keymap.c index 33e042f14..300854618 100644 --- a/right/src/keymap.c +++ b/right/src/keymap.c @@ -1,6 +1,7 @@ #include #include "arduino_hid/ConsumerAPI.h" #include "arduino_hid/SystemAPI.h" +#include "chords.h" #include "keymap.h" #include "layer.h" #include "layer_switcher.h" @@ -53,6 +54,7 @@ void SwitchKeymapById(uint8_t index, bool resetLayerStack) #endif SegmentDisplay_UpdateKeymapText(); if (DEVICE_IS_MASTER) { + Chords_ResetChords(); MacroEvent_RegisterLayerMacros(); MacroEvent_OnKeymapChange(index); MacroEvent_OnLayerChange(ActiveLayer); diff --git a/right/src/macros/commands.c b/right/src/macros/commands.c index 71cede640..1a17cb14f 100644 --- a/right/src/macros/commands.c +++ b/right/src/macros/commands.c @@ -1302,8 +1302,12 @@ uint8_t Macros_TryConsumeKeyId(parser_context_t* ctx) uint8_t keyId = MacroKeyIdParser_TryConsumeKeyId(ctx); if (keyId == 255 && isNUM(ctx)) { + bool ErrorBefore = Macros_ParserError; + Macros_ParserError = false; uint8_t num = Macros_ConsumeInt(ctx); - if (Macros_ParserError) { + bool ErrorAfter = Macros_ParserError; + Macros_ParserError |= ErrorBefore; + if (ErrorAfter) { return 255; } else { return num; diff --git a/right/src/macros/set_command.c b/right/src/macros/set_command.c index c787c6715..109c689d0 100644 --- a/right/src/macros/set_command.c +++ b/right/src/macros/set_command.c @@ -1,5 +1,6 @@ #include "macros/set_command.h" #include "attributes.h" +#include "chords.h" #include "config_parser/parse_config.h" #include "config_parser/parse_keymap.h" #include "key_states.h" @@ -799,6 +800,17 @@ static macro_variable_t navigationModeAction(parser_context_t* ctx, set_command_ return noneVar(); } +static inline key_coordinates_t validateKeyId(uint8_t keyId, const char *at) { + uint8_t slotIdx = keyId/64; + uint8_t inSlotIdx = keyId%64; + + if(slotIdx > SLOT_COUNT || inSlotIdx > MAX_KEY_COUNT_PER_MODULE) { + Macros_ReportErrorNum("Invalid key id:", keyId, at); + } + + return (key_coordinates_t){.slotId = slotIdx, .inSlotId = inSlotIdx}; +} + static macro_variable_t keymapAction(parser_context_t* ctx, set_command_action_t action) { uint8_t layerId = Macros_ConsumeLayerId(ctx); @@ -820,12 +832,7 @@ static macro_variable_t keymapAction(parser_context_t* ctx, set_command_action_t key_action_t keyAction = parseKeyAction(ctx); - uint8_t slotIdx = keyId/64; - uint8_t inSlotIdx = keyId%64; - - if(slotIdx > SLOT_COUNT || inSlotIdx > MAX_KEY_COUNT_PER_MODULE) { - Macros_ReportErrorNum("Invalid key id:", keyId, keyIdPos.at); - } + key_coordinates_t cords = validateKeyId(keyId, keyIdPos.at); if (Macros_ParserError) { return noneVar(); @@ -834,7 +841,7 @@ static macro_variable_t keymapAction(parser_context_t* ctx, set_command_action_t return noneVar(); } - key_action_t* actionSlot = &CurrentKeymap[layerId][slotIdx][inSlotIdx].action; + key_action_t* actionSlot = &CurrentKeymap[layerId][cords.slotId][cords.inSlotId].action; #ifdef __ZEPHYR__ StateSync_UpdateLayer(layerId, true); @@ -844,6 +851,60 @@ static macro_variable_t keymapAction(parser_context_t* ctx, set_command_action_t return noneVar(); } +static macro_variable_t chordAction(parser_context_t* ctx, set_command_action_t action) +{ + uint8_t layerId = Macros_ConsumeLayerId(ctx); + + uint8_t keyCount = 0; + uint8_t keys[MAX_CHORD_KEYS]; + const char *duplicatePos = NULL; + do { + const char *keyPos = ctx->at; + keys[keyCount] = Macros_TryConsumeKeyId(ctx); + + if (keys[keyCount] == 255) break; + + validateKeyId(keys[keyCount], keyPos); + if(duplicatePos == NULL) { + for (int i = 0; i < keyCount; ++i) { + if (keys[i] == keys[keyCount]) { + duplicatePos = keyPos; + break; + } + } + } + } while (++keyCount < MAX_CHORD_KEYS); + + if(duplicatePos != NULL) { + Macros_ReportError("Cannot have duplicate keys in a chord!", duplicatePos, NULL); + } + + if (keyCount < 2 || keyCount == MAX_CHORD_KEYS) { + CTX_COPY(errorPos, *ctx); + if (keyCount < 2 || Macros_TryConsumeKeyId(ctx) != 255) { + while (Macros_TryConsumeKeyId(ctx) != 255) {} + Macros_ReportErrorPos(&errorPos, "Invalid number of keys in chord. Must be between 2 and 5."); + } + } + + if (action == SetCommandAction_Read) { + Macros_ReportErrorPos(ctx, "Reading actions is not supported!"); + return noneVar(); + } + + key_action_t chordAction = parseKeyAction(ctx); + + if (Macros_ParserError || Macros_DryRun) { + return noneVar(); + } + + if (!Chords_TryAddChord(layerId, keys, keyCount, &chordAction)) { + Macros_ReportErrorPos(ctx, "Could not add chords, no more free chord slots."); + } + + return noneVar(); +} + static macro_variable_t modLayerTriggers(parser_context_t* ctx, set_command_action_t action) { uint8_t layerId = Macros_ConsumeLayerId(ctx); @@ -999,6 +1060,10 @@ static macro_variable_t root(parser_context_t* ctx, set_command_action_t action) ConsumeUntilDot(ctx); return keymapAction(ctx, action); } + else if (ConsumeToken(ctx, "chord")) { + ConsumeUntilDot(ctx); + return chordAction(ctx, action); + } else if (ConsumeToken(ctx, "navigationModeAction")) { ConsumeUntilDot(ctx); return navigationModeAction(ctx, action); From 687fdb9ac377a417c35269985c3241ecd345a5f4 Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Mon, 30 Mar 2026 21:28:04 +0200 Subject: [PATCH 02/13] Chords: It works, but it's not pretty yet --- right/src/chords.c | 111 ++++++++++++++++++++++++++++++++- right/src/chords.h | 8 +++ right/src/config_manager.c | 1 + right/src/config_manager.h | 3 +- right/src/key_action.h | 4 +- right/src/macros/set_command.c | 4 ++ right/src/mouse_controller.c | 4 +- right/src/postponer.c | 11 ++++ right/src/postponer.h | 1 + right/src/usb_report_updater.c | 38 ++++++----- right/src/usb_report_updater.h | 2 +- right/src/utils.c | 2 +- right/src/utils.h | 2 +- 13 files changed, 169 insertions(+), 22 deletions(-) diff --git a/right/src/chords.c b/right/src/chords.c index 34a2af82a..c363ac5d9 100644 --- a/right/src/chords.c +++ b/right/src/chords.c @@ -1,5 +1,11 @@ #include "chords.h" +#include "config_manager.h" +#include "event_scheduler.h" +#include "keymap.h" +#include "layer.h" #include "macros/status_buffer.h" +#include "postponer.h" +#include "utils.h" #define MAX_CHORDS_COUNT 64 @@ -26,6 +32,33 @@ static inline void sortKeys(chord_keys_t keys, uint8_t keyCount) } } +uint8_t findUsedKeys(chord_keys_t unusedKeys, uint8_t keyCount, const chord_def_t *chord) { + uint8_t i = 0; + uint8_t j = 0; + while (i < keyCount && j < chord->keyCount) { + while (i < keyCount && unusedKeys[i] < chord->keys[j]) {++i;} + if (unusedKeys[i] == chord->keys[j]) { + memmove(unusedKeys + i, unusedKeys + i + 1, keyCount-- - i); + } + while (j < chord->keyCount && chord->keys[j] < unusedKeys[i]) {++j;} + } + return keyCount; +} + +void handleUpdateKeysOfDeletedChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount) { + // Now figure out what keys should be marked as now unused + for (int i = 0; i < ChordCount; ++i) { + if (Chords[i].layer < layer) continue; + if (Chords[i].layer > layer) break; + if ((keyCount = findUsedKeys(keys, keyCount, &Chords[i])) == 0) break; + } + + for (int i = 0; i < keyCount; ++i) { + CurrentKeymap[layer][keys[i]/64][keys[i]%64].isPartOfChord = false; + } +} + + /* Chords are stored sorted as follows: First by layer, direction doesn't matter, to group by layer @@ -55,9 +88,10 @@ bool Chords_TryAddChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount, key_ if (chordCmp == 0) { // We already have the chord, erase or update, depending on provided action if (action->type == KeyActionType_None) { - // Erase the chord and return + // Erase the chord memmove(&Chords[i], &Chords[i + 1], sizeof(chord_def_t) * (--ChordCount - i)); memset(&Chords[ChordCount], 0, sizeof(chord_def_t)); + handleUpdateKeysOfDeletedChord(layer, keys, keyCount); return true; } // Just overwrite action, the rest of the chord is already in place @@ -84,6 +118,9 @@ bool Chords_TryAddChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount, key_ chord->keyCount = keyCount; chord->layer = layer; chord->action = *action; + for (i = 0; i < keyCount; ++i) { + CurrentKeymap[layer][keys[i]/64][keys[i]%64].isPartOfChord = true; + } return true; } @@ -157,4 +194,76 @@ chord_search_result_t Chords_TryGetChordAction(key_action_t *outAction, uint8_t void Chords_ResetChords() { ChordCount = 0; +} + +static struct { + key_state_t *initialKey; + uint32_t initialTime; +} ResolutionState; + +static inline void finishResolution() { + ResolutionState.initialKey->current = true; + ResolutionState.initialKey->previous = false; + memset(&ResolutionState, 0, sizeof(ResolutionState)); +} + +#define DOWAIT(start) EventScheduler_Schedule(start + Cfg.ChordTimeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord");\ + return ChordResolution_Wait; +#define DOFAIL finishResolution();\ + return ChordResolution_Failed; +#define RESOLVED PostponerExtended_ConsumePendingKeypresses(keyCount, true);\ + finishResolution();\ + return ChordResolution_Resolved; +#define MUSTCHOOSE ((ResolutionState.initialTime + Cfg.ChordTimeout <= Timer_GetCurrentTime()) || PostponerQuery_IsKeyReleased(keyState)) + +chord_resolution_t Chords_Driver(key_state_t *keyState, uint8_t layer, key_action_t *resolvedAction) +{ + if (KeyState_ActivatedNow(keyState) && ResolutionState.initialKey == NULL) { + ResolutionState.initialKey = keyState; + ResolutionState.initialTime = CurrentPostponedTime; + DOWAIT(CurrentPostponedTime) + } + else if (ResolutionState.initialKey != keyState) { + return ChordResolution_Failed; + } + + chord_keys_t pressedKeys; + uint8_t keyCount = PostponerQuery_GetPendingKeypresses(pressedKeys, MAX_CHORD_KEYS); + if(keyCount == MAX_CHORD_KEYS) { + DOFAIL + } + + pressedKeys[keyCount] = Utils_KeyStateToKeyId(keyState); + + //Macros_SetStatusString("KeyCount: ", NULL); + //Macros_SetStatusNum(keyCount + 1); + //Macros_SetStatusChar('\n'); + + chord_search_result_t searchRes = Chords_TryGetChordAction(resolvedAction, layer, pressedKeys, keyCount + 1); + + //Macros_SetStatusString("SearchRes: ", NULL); + //Macros_SetStatusNum(searchRes); + //Macros_SetStatusNum(MUSTCHOOSE); + //Macros_SetStatusNum(ResolutionState.initialTime); + //Macros_SetStatusNum(Timer_GetCurrentTime()); + //Macros_SetStatusNum(KeyState_DeactivatedNow(keyState)); + //Macros_SetStatusChar('\n'); + + switch (searchRes) { + case ChordSearch_FinalMatch: + RESOLVED + case ChordSearch_Nothing: + DOFAIL + case ChordSearch_PartialOnly: + if (MUSTCHOOSE) { + DOFAIL + } + DOWAIT(ResolutionState.initialTime) + case ChordSearch_MatchAndPartial: + if (MUSTCHOOSE) { + RESOLVED + } + DOWAIT(ResolutionState.initialTime) + } + return ChordResolution_Failed; } \ No newline at end of file diff --git a/right/src/chords.h b/right/src/chords.h index 0be611135..c380c77e1 100644 --- a/right/src/chords.h +++ b/right/src/chords.h @@ -26,5 +26,13 @@ typedef struct { bool Chords_TryAddChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount, key_action_t *action); chord_search_result_t Chords_TryGetChordAction(key_action_t *outAction, uint8_t layer, chord_keys_t keys, uint8_t keyCount); void Chords_ResetChords(); + +typedef enum { + ChordResolution_Wait, + ChordResolution_Failed, + ChordResolution_Resolved, +} chord_resolution_t; + +chord_resolution_t Chords_Driver(key_state_t *keyState, uint8_t layer, key_action_t *resolvedAction); #endif diff --git a/right/src/config_manager.c b/right/src/config_manager.c index a9515186d..be135a9a0 100644 --- a/right/src/config_manager.c +++ b/right/src/config_manager.c @@ -271,6 +271,7 @@ const config_t DefaultCfg = (config_t){ .Macros_OneShotTimeout = 500, .AutoShiftDelay = 0, .ChordingDelay = 0, + .ChordTimeout = 75, .BatteryStationaryMode = false, #ifdef __ZEPHYR__ .I2cBaudRate = 0, diff --git a/right/src/config_manager.h b/right/src/config_manager.h index 09568be88..d5008037c 100644 --- a/right/src/config_manager.h +++ b/right/src/config_manager.h @@ -91,7 +91,8 @@ uint16_t AutoRepeatDelayRate; uint16_t Macros_OneShotTimeout; uint16_t AutoShiftDelay; - uint8_t ChordingDelay; + uint8_t ChordingDelay; + uint8_t ChordTimeout; key_state_t* EmergencyKey; // bluetooth diff --git a/right/src/key_action.h b/right/src/key_action.h index 8e603c7dc..9d4e4cbf7 100644 --- a/right/src/key_action.h +++ b/right/src/key_action.h @@ -116,12 +116,14 @@ typedef struct { key_action_t action; rgb_t color; - bool colorOverridden; + bool colorOverridden : 1; + bool isPartOfChord : 1; } ATTR_PACKED key_definition_t; typedef struct { key_action_t action; uint8_t modifierLayerMask; + uint8_t originKeymap; } ATTR_PACKED key_action_cached_t; // Variables: diff --git a/right/src/macros/set_command.c b/right/src/macros/set_command.c index 109c689d0..56d499128 100644 --- a/right/src/macros/set_command.c +++ b/right/src/macros/set_command.c @@ -1148,6 +1148,10 @@ static macro_variable_t root(parser_context_t* ctx, set_command_action_t action) DEFINE_INT_LIMITS(0, 255); ASSIGN_INT(Cfg.ChordingDelay); } + else if (ConsumeToken(ctx, "chordTimeout")) { + DEFINE_INT_LIMITS(0, 255); + ASSIGN_INT(Cfg.ChordTimeout); + } else if (ConsumeToken(ctx, "autoShiftDelay")) { DEFINE_INT_LIMITS(0, 65535); ASSIGN_INT(Cfg.AutoShiftDelay); diff --git a/right/src/mouse_controller.c b/right/src/mouse_controller.c index 940b8c001..cc2d21e6d 100644 --- a/right/src/mouse_controller.c +++ b/right/src/mouse_controller.c @@ -336,7 +336,7 @@ static void handleNewCaretModeAction(caret_axis_t axis, uint8_t resultSign, int1 caret_dir_action_t* dirActions = ¤tCaretConfig->axisActions[ks->caretAxis]; ks->caretAction.action = resultSign > 0 ? dirActions->positiveAction : dirActions->negativeAction; ks->caretFakeKeystate.current = true; - ApplyKeyAction(&ks->caretFakeKeystate, &ks->caretAction, &ks->caretAction.action, &MouseControllerKeyboardReports); + ApplyKeyAction(&ks->caretFakeKeystate, &ks->caretAction, &MouseControllerKeyboardReports); Macros_WakeBecauseOfKeystateChange(); EventVector_Set(EventVector_MouseController); break; @@ -359,7 +359,7 @@ static void handleSimpleRunningAction(module_kinetic_state_t* ks) { bool tmp = ks->caretFakeKeystate.current; ks->caretFakeKeystate.current = !ks->caretFakeKeystate.previous; ks->caretFakeKeystate.previous = tmp; - ApplyKeyAction(&ks->caretFakeKeystate, &ks->caretAction, &ks->caretAction.action, &MouseControllerKeyboardReports); + ApplyKeyAction(&ks->caretFakeKeystate, &ks->caretAction, &MouseControllerKeyboardReports); Macros_WakeBecauseOfKeystateChange(); EventVector_Set(EventVector_MouseController); } diff --git a/right/src/postponer.c b/right/src/postponer.c index 1aebb4fb1..91440f96f 100644 --- a/right/src/postponer.c +++ b/right/src/postponer.c @@ -442,6 +442,17 @@ void PostponerQuery_FindFirstReleased(const postponer_buffer_record_type_t** rel return; } +uint8_t PostponerQuery_GetPendingKeypresses(uint8_t keyIds[], uint8_t maxCount) { + uint8_t pos = 0; + uint8_t i = 0; + while (pos < maxCount) { + while (i < bufferSize && buffer[POS(i)].event.type != PostponerEventType_PressKey) { ++i; } + if (i == bufferSize) break; + keyIds[pos++] = Utils_KeyStateToKeyId(buffer[POS(i++)].event.key.keyState); + } + return pos; +} + //########################## //### Extended Functions ### //########################## diff --git a/right/src/postponer.h b/right/src/postponer.h index b2816533c..6db233d11 100644 --- a/right/src/postponer.h +++ b/right/src/postponer.h @@ -90,6 +90,7 @@ bool PostponerQuery_ContainsKeyId(uint8_t keyid); void PostponerQuery_FindFirstPressed(const postponer_buffer_record_type_t** press, const key_state_t* opposingKey); void PostponerQuery_FindFirstReleased(const postponer_buffer_record_type_t** release, const key_state_t* opposingKey); + uint8_t PostponerQuery_GetPendingKeypresses(uint8_t pendingKeyIds[], uint8_t maxCount); // Functions (Query APIs extended): uint16_t PostponerExtended_PendingId(uint16_t idx); diff --git a/right/src/usb_report_updater.c b/right/src/usb_report_updater.c index 408195e7a..320112d8f 100644 --- a/right/src/usb_report_updater.c +++ b/right/src/usb_report_updater.c @@ -2,6 +2,7 @@ #include #include "atomicity.h" #include "bt_defs.h" +#include "chords.h" #include "event_scheduler.h" #include "host_connection.h" #include "key_action.h" @@ -365,13 +366,13 @@ static void applyKeystrokePrimary(key_state_t *keyState, key_action_cached_t *ca } } -static void applyKeystrokeSecondary(key_state_t *keyState, key_action_t *action, key_action_t *actionBase, usb_keyboard_reports_t* reports) +static void applyKeystrokeSecondary(key_state_t *keyState, key_action_cached_t *cachedAction, usb_keyboard_reports_t* reports) { - secondary_role_t secondaryRole = action->keystroke.secondaryRole; + secondary_role_t secondaryRole = cachedAction->action.keystroke.secondaryRole; if ( IS_SECONDARY_ROLE_LAYER_SWITCHER(secondaryRole) ) { // If the cached action is the current base role, then hold, otherwise keymap was changed. In that case do nothing just // as a well behaved hold action should. - if(KeyState_Active(keyState) && action->type == actionBase->type && action->keystroke.secondaryRole == actionBase->keystroke.secondaryRole) { + if(KeyState_Active(keyState) && cachedAction->originKeymap == CurrentKeymapIndex) { LayerSwitcher_HoldLayer(SECONDARY_ROLE_LAYER_TO_LAYER_ID(secondaryRole), false); } if (KeyState_ActivatedNow(keyState) || KeyState_DeactivatedNow(keyState)) { @@ -385,7 +386,7 @@ static void applyKeystrokeSecondary(key_state_t *keyState, key_action_t *action, } } -static void applyKeystroke(key_state_t *keyState, key_action_cached_t *cachedAction, key_action_t *actionBase, usb_keyboard_reports_t* reports) +static void applyKeystroke(key_state_t *keyState, key_action_cached_t *cachedAction, usb_keyboard_reports_t* reports) { key_action_t* action = &cachedAction->action; if (action->keystroke.secondaryRole) { @@ -395,7 +396,7 @@ static void applyKeystroke(key_state_t *keyState, key_action_cached_t *cachedAct applyKeystrokePrimary(keyState, cachedAction, reports); return; case SecondaryRoleState_Secondary: - applyKeystrokeSecondary(keyState, action, actionBase, reports); + applyKeystrokeSecondary(keyState, cachedAction, reports); return; case SecondaryRoleState_DontKnowYet: // Repeatedly trigger to keep Postponer in postponing mode until the driver decides. @@ -498,7 +499,7 @@ static void applyOtherAction(other_action_t actionSubtype) } } -void ApplyKeyAction(key_state_t *keyState, key_action_cached_t *cachedAction, key_action_t *actionBase, usb_keyboard_reports_t* reports) +void ApplyKeyAction(key_state_t *keyState, key_action_cached_t *cachedAction, usb_keyboard_reports_t* reports) { key_action_t* action = &cachedAction->action; @@ -506,7 +507,7 @@ void ApplyKeyAction(key_state_t *keyState, key_action_cached_t *cachedAction, ke case KeyActionType_Keystroke: if (KeyState_NonZero(keyState)) { EventVector_Set(EventVector_NativeActionReportsUsed); - applyKeystroke(keyState, cachedAction, actionBase, reports); + applyKeystroke(keyState, cachedAction, reports); } break; case KeyActionType_Mouse: @@ -740,7 +741,6 @@ static void updateActionStates() { for (uint8_t keyId=0; keyIdactivationId; KeyHistory_RecordPress(keyState); @@ -785,15 +799,11 @@ static void updateActionStates() { } cachedAction = &actionCache[slotId][keyId]; - actionBase = &CurrentKeymap[LayerId_Base][slotId][keyId].action; Trace_Printc("w3"); - //apply base-layer holds - applyLayerHolds(keyState, actionBase); - Trace_Printc("w4"); //apply active-layer action - ApplyKeyAction(keyState, cachedAction, actionBase, &NativeKeyboardReports); + ApplyKeyAction(keyState, cachedAction, &NativeKeyboardReports); if (KeyState_DeactivatedNow(keyState)) { KeyHistory_RecordRelease(keyState); diff --git a/right/src/usb_report_updater.h b/right/src/usb_report_updater.h index 4cd2193f6..498e330f3 100644 --- a/right/src/usb_report_updater.h +++ b/right/src/usb_report_updater.h @@ -59,7 +59,7 @@ void ToggleMouseState(serialized_mouse_action_t action, bool activate); void ActivateKey(key_state_t *keyState, bool debounce); void ActivateStickyMods(key_state_t *keyState, uint8_t mods); - void ApplyKeyAction(key_state_t *keyState, key_action_cached_t *cachedAction, key_action_t *actionBase, usb_keyboard_reports_t* reports); + void ApplyKeyAction(key_state_t *keyState, key_action_cached_t *cachedAction, usb_keyboard_reports_t* reports); void StickyMods_ResetLater(key_action_cached_t *cachedAction); void UsbReportUpdater_ResetKeyboardReports(usb_keyboard_reports_t* reports); diff --git a/right/src/utils.c b/right/src/utils.c index 827646397..020142c81 100644 --- a/right/src/utils.c +++ b/right/src/utils.c @@ -31,7 +31,7 @@ static uint16_t recodeId(uint16_t newFormat, uint16_t fromBase, uint16_t toBase) } #endif -uint16_t Utils_KeyStateToKeyId(key_state_t* key) +uint16_t Utils_KeyStateToKeyId(const key_state_t* key) { #if DEVICE_IS_UHK_DONGLE return 0; diff --git a/right/src/utils.h b/right/src/utils.h index 80ec4363c..d7d149554 100644 --- a/right/src/utils.h +++ b/right/src/utils.h @@ -34,7 +34,7 @@ if (reentrancyGuard_active) { \ uint8_t Utils_SafeStrCopy(char* target, const char* src, uint8_t max); key_state_t* Utils_KeyIdToKeyState(uint16_t keyid); - uint16_t Utils_KeyStateToKeyId(key_state_t* key); + uint16_t Utils_KeyStateToKeyId(const key_state_t* key); const char* Utils_KeyStateToKeyAbbreviation(key_state_t* key); void Utils_DecodeId(uint16_t keyid, uint8_t* outSlotId, uint8_t* outSlotIdx); const char* Utils_GetUsbReportString(const hid_keyboard_report_t* report); From 1c301645f7cb49ddc6f48e25fc5f3dead34c903f Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Tue, 26 May 2026 21:25:15 +0200 Subject: [PATCH 03/13] Key Chords: Support for the Any layer --- right/src/chords.c | 166 +++++++++++++++++++-------------- right/src/chords.h | 4 +- right/src/macros/set_command.c | 5 +- right/src/postponer.c | 4 +- right/src/postponer.h | 2 +- right/src/str_utils.c | 8 ++ right/src/str_utils.h | 1 + right/src/usb_report_updater.c | 22 +++-- 8 files changed, 128 insertions(+), 84 deletions(-) diff --git a/right/src/chords.c b/right/src/chords.c index c363ac5d9..79702c37b 100644 --- a/right/src/chords.c +++ b/right/src/chords.c @@ -45,11 +45,10 @@ uint8_t findUsedKeys(chord_keys_t unusedKeys, uint8_t keyCount, const chord_def_ return keyCount; } -void handleUpdateKeysOfDeletedChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount) { - // Now figure out what keys should be marked as now unused +void handleUpdateKeysOfDeletedChordOnLayer(uint8_t layer, chord_keys_t keys, uint8_t keyCount) { for (int i = 0; i < ChordCount; ++i) { if (Chords[i].layer < layer) continue; - if (Chords[i].layer > layer) break; + if (Chords[i].layer > layer && Chords[i].layer != LayerId_None) continue; if ((keyCount = findUsedKeys(keys, keyCount, &Chords[i])) == 0) break; } @@ -58,20 +57,44 @@ void handleUpdateKeysOfDeletedChord(uint8_t layer, chord_keys_t keys, uint8_t ke } } +void handleUpdateKeysOfDeletedChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount) { + if (layer == LayerId_None) { + for (layer_id_t laier = LayerId_Base; laier < LayerId_Count; ++laier) { + handleUpdateKeysOfDeletedChordOnLayer(laier, keys, keyCount); + } + } + else { + handleUpdateKeysOfDeletedChordOnLayer(layer, keys, keyCount); + } +} + +void markKeysAsPartsOfChord(chord_def_t *chord) { + for (uint8_t i = 0; i < chord->keyCount; ++i) { + if (chord->layer != LayerId_None) { + CurrentKeymap[chord->layer][chord->keys[i]/64][chord->keys[i]%64].isPartOfChord = true; + } + else { + for (layer_id_t j = 0; j < LayerId_Count; ++j) { + CurrentKeymap[j][chord->keys[i]/64][chord->keys[i]%64].isPartOfChord = true; + } + } + } +} + /* Chords are stored sorted as follows: -First by layer, direction doesn't matter, to group by layer +First by layer, ascending to put the any layer (255) last Then ascending by key count - ascending, this matters Then by key id list, direction doesn't matter, only that they're sorted consistently */ -static inline int8_t compareToChord(const chord_def_t *left, uint8_t layer, uint8_t keyCount, const chord_keys_t keys) { +static inline int16_t compareToChord(const chord_def_t *left, layer_id_t layer, uint8_t keyCount, const chord_keys_t keys) { // These could be collapsed and optimized more if we're willing to do union with a bitfield, but that's hacky if (left->layer != layer) { - return left->layer - layer; + return (int16_t)left->layer - layer; } if (left->keyCount != keyCount) { - return left->keyCount - keyCount; + return (int16_t)left->keyCount - keyCount; } return memcmp(left->keys, keys, keyCount); } @@ -82,7 +105,7 @@ bool Chords_TryAddChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount, key_ // Find a spot uint8_t i = 0; - int8_t chordCmp = 1; // Initialize to not zero to not overwrite first on empty list + int16_t chordCmp = 1; // Initialize to not zero to not overwrite first on empty list while (i < ChordCount && (chordCmp = compareToChord(&Chords[i], layer, keyCount, keys)) < 0) {++i;} if (chordCmp == 0) { @@ -90,7 +113,6 @@ bool Chords_TryAddChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount, key_ if (action->type == KeyActionType_None) { // Erase the chord memmove(&Chords[i], &Chords[i + 1], sizeof(chord_def_t) * (--ChordCount - i)); - memset(&Chords[ChordCount], 0, sizeof(chord_def_t)); handleUpdateKeysOfDeletedChord(layer, keys, keyCount); return true; } @@ -118,9 +140,7 @@ bool Chords_TryAddChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount, key_ chord->keyCount = keyCount; chord->layer = layer; chord->action = *action; - for (i = 0; i < keyCount; ++i) { - CurrentKeymap[layer][keys[i]/64][keys[i]%64].isPartOfChord = true; - } + markKeysAsPartsOfChord(chord); return true; } @@ -143,51 +163,58 @@ static bool isPartialOfChord(const chord_def_t *large, chord_keys_t keys, uint8_ return true; } -chord_search_result_t Chords_TryGetChordAction(key_action_t *outAction, uint8_t layer, chord_keys_t keys, uint8_t keyCount) +chord_search_result_t searchForChordAction(key_action_t *outAction, uint8_t layer, chord_keys_t keys, uint8_t keyCount, bool noPartial) { sortKeys(keys, keyCount); uint8_t i = 0; bool matched = false; - bool partial = false; - - // First maybe find the chord - for (; i < ChordCount; ++i) { - int8_t chordCmp = compareToChord(&Chords[i], layer, keyCount, keys); - if (chordCmp < 0) continue; - if (chordCmp == 0) { - // We found the longest completed chord so far - *outAction = Chords[i].action; - matched = true; - } - break; - } - - // We now may have a match, but we can't return it yet. - // We need to check if we have a potential for a longer chord - // Because of the sort order of the chord list, all chords after this slot - // with the same layer are same length or longer than the desired chord. - // Now iterate until we get to the longer chords in the layer because - // we want to see if there are potential matches in them - while (i < ChordCount && Chords[i].layer == layer && Chords[i].keyCount == keyCount) {++i;} - - // i is now at the first longer chord in the layer, or the first key in another layer, on at list's end - // Now we look for potential matches with more keys - while (i < ChordCount && Chords[i].layer == layer) { - if (isPartialOfChord(&Chords[i++], keys, keyCount)) { - // If we have a partial chord - partial = true; + while (true) { + // First maybe find the chord + if ( !matched ) { + for (; i < ChordCount; ++i) { + int16_t chordCmp = compareToChord(&Chords[i], layer, keyCount, keys); + if (chordCmp < 0) continue; + if (chordCmp == 0) { + // We found the longest completed chord so far + *outAction = Chords[i].action; + matched = true; + ++i; + } + break; + } + } + + // We now may have a match, but we can't return it yet. + // We need to check if we have a potential for a longer chord + // Because of the sort order of the chord list, all chords after this slot + // with the same layer are same length or longer than the desired chord. + + // Now iterate until we get to the longer chords in the layer because + // we want to see if there are potential matches in them + if ( !noPartial ) { + while (i < ChordCount && Chords[i].layer == layer && Chords[i].keyCount == keyCount) {++i;} + + // i is now at the first longer chord in the layer, or the first key in another layer, on at list's end + // Now we look for potential matches with more keys + while (i < ChordCount && Chords[i].layer == layer) { + if (isPartialOfChord(&Chords[i++], keys, keyCount)) { + // If we have a partial chord + return ChordSearch_NonConclusive; + } + } + } + + if (layer == LayerId_None) { break; } + layer = LayerId_None; } if (matched) { // We have found an exact match - return partial ? ChordSearch_MatchAndPartial : ChordSearch_FinalMatch; - } - if (partial) { - return ChordSearch_PartialOnly; + return ChordSearch_FinalMatch; } return ChordSearch_Nothing; } @@ -198,71 +225,72 @@ void Chords_ResetChords() { static struct { key_state_t *initialKey; - uint32_t initialTime; -} ResolutionState; + volatile uint32_t initialTime; +} volatile ResolutionState; static inline void finishResolution() { + // Fake activation of the key now. ResolutionState.initialKey->current = true; ResolutionState.initialKey->previous = false; + // Then clear the resolution state. memset(&ResolutionState, 0, sizeof(ResolutionState)); } #define DOWAIT(start) EventScheduler_Schedule(start + Cfg.ChordTimeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord");\ return ChordResolution_Wait; + #define DOFAIL finishResolution();\ return ChordResolution_Failed; + #define RESOLVED PostponerExtended_ConsumePendingKeypresses(keyCount, true);\ finishResolution();\ return ChordResolution_Resolved; -#define MUSTCHOOSE ((ResolutionState.initialTime + Cfg.ChordTimeout <= Timer_GetCurrentTime()) || PostponerQuery_IsKeyReleased(keyState)) chord_resolution_t Chords_Driver(key_state_t *keyState, uint8_t layer, key_action_t *resolvedAction) { + uint32_t start = Timer_GetCurrentTimeMicros(); if (KeyState_ActivatedNow(keyState) && ResolutionState.initialKey == NULL) { ResolutionState.initialKey = keyState; ResolutionState.initialTime = CurrentPostponedTime; - DOWAIT(CurrentPostponedTime) } else if (ResolutionState.initialKey != keyState) { return ChordResolution_Failed; } chord_keys_t pressedKeys; - uint8_t keyCount = PostponerQuery_GetPendingKeypresses(pressedKeys, MAX_CHORD_KEYS); + uint8_t keyCount = PostponerQuery_GetPendingKeypresses(pressedKeys, MAX_CHORD_KEYS, ResolutionState.initialTime + Cfg.ChordTimeout); if(keyCount == MAX_CHORD_KEYS) { + // We have mashed more keys than we support in chords. + // Note that next key in line might trigger chord with the rest, this one just doesn't. DOFAIL } pressedKeys[keyCount] = Utils_KeyStateToKeyId(keyState); - //Macros_SetStatusString("KeyCount: ", NULL); - //Macros_SetStatusNum(keyCount + 1); - //Macros_SetStatusChar('\n'); + const bool isFinalChoice = + (ResolutionState.initialTime + Cfg.ChordTimeout <= Timer_GetCurrentTime()) + || PostponerQuery_IsKeyReleased(keyState); + - chord_search_result_t searchRes = Chords_TryGetChordAction(resolvedAction, layer, pressedKeys, keyCount + 1); + chord_search_result_t searchRes = searchForChordAction(resolvedAction, layer, pressedKeys, keyCount + 1, isFinalChoice); - //Macros_SetStatusString("SearchRes: ", NULL); - //Macros_SetStatusNum(searchRes); - //Macros_SetStatusNum(MUSTCHOOSE); - //Macros_SetStatusNum(ResolutionState.initialTime); - //Macros_SetStatusNum(Timer_GetCurrentTime()); - //Macros_SetStatusNum(KeyState_DeactivatedNow(keyState)); + uint32_t elapsed = Timer_GetCurrentTimeMicros() - start; + //Macros_SetStatusString("Processing micros: ", NULL); + //Macros_SetStatusNum(elapsed); //Macros_SetStatusChar('\n'); + switch (searchRes) { case ChordSearch_FinalMatch: + //Macros_SetStatusNum(Timer_GetCurrentTime() - ResolutionState.initialTime); + //Macros_SetStatusChar('\n'); + //Macros_SetStatusString("Matched\n", NULL); RESOLVED case ChordSearch_Nothing: + //Macros_SetStatusString("Failed\n", NULL); DOFAIL - case ChordSearch_PartialOnly: - if (MUSTCHOOSE) { - DOFAIL - } - DOWAIT(ResolutionState.initialTime) - case ChordSearch_MatchAndPartial: - if (MUSTCHOOSE) { - RESOLVED - } + case ChordSearch_NonConclusive: + //Macros_SetStatusString("Partial\n", NULL); DOWAIT(ResolutionState.initialTime) } return ChordResolution_Failed; diff --git a/right/src/chords.h b/right/src/chords.h index c380c77e1..3d6577ad8 100644 --- a/right/src/chords.h +++ b/right/src/chords.h @@ -8,8 +8,7 @@ typedef uint8_t chord_keys_t[MAX_CHORD_KEYS]; typedef enum { ChordSearch_Nothing, // There is no chord which could match provided data - ChordSearch_PartialOnly, // There are no finished chords, but some partially finished - ChordSearch_MatchAndPartial, // There was an exact match, but also longer chords + ChordSearch_NonConclusive, // There are potentially longer chords ChordSearch_FinalMatch, // There was an exact match and no other potential matches } chord_search_result_t; @@ -24,7 +23,6 @@ typedef struct { } ATTR_PACKED chord_def_t; bool Chords_TryAddChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount, key_action_t *action); -chord_search_result_t Chords_TryGetChordAction(key_action_t *outAction, uint8_t layer, chord_keys_t keys, uint8_t keyCount); void Chords_ResetChords(); typedef enum { diff --git a/right/src/macros/set_command.c b/right/src/macros/set_command.c index 56d499128..84ecc3802 100644 --- a/right/src/macros/set_command.c +++ b/right/src/macros/set_command.c @@ -853,7 +853,9 @@ static macro_variable_t keymapAction(parser_context_t* ctx, set_command_action_t static macro_variable_t chordAction(parser_context_t* ctx, set_command_action_t action) { - uint8_t layerId = Macros_ConsumeLayerId(ctx); + bool hasLayer = TryConsumeDot(ctx); + + uint8_t layerId = hasLayer ? Macros_ConsumeLayerId(ctx) : LayerId_None; uint8_t keyCount = 0; uint8_t keys[MAX_CHORD_KEYS]; @@ -1061,7 +1063,6 @@ static macro_variable_t root(parser_context_t* ctx, set_command_action_t action) return keymapAction(ctx, action); } else if (ConsumeToken(ctx, "chord")) { - ConsumeUntilDot(ctx); return chordAction(ctx, action); } else if (ConsumeToken(ctx, "navigationModeAction")) { diff --git a/right/src/postponer.c b/right/src/postponer.c index 91440f96f..1997601ca 100644 --- a/right/src/postponer.c +++ b/right/src/postponer.c @@ -442,12 +442,12 @@ void PostponerQuery_FindFirstReleased(const postponer_buffer_record_type_t** rel return; } -uint8_t PostponerQuery_GetPendingKeypresses(uint8_t keyIds[], uint8_t maxCount) { +uint8_t PostponerQuery_GetPendingKeypresses(uint8_t keyIds[], uint8_t maxCount, uint32_t cutoffTime) { uint8_t pos = 0; uint8_t i = 0; while (pos < maxCount) { while (i < bufferSize && buffer[POS(i)].event.type != PostponerEventType_PressKey) { ++i; } - if (i == bufferSize) break; + if (i == bufferSize || buffer[POS(i)].time > cutoffTime) break; keyIds[pos++] = Utils_KeyStateToKeyId(buffer[POS(i++)].event.key.keyState); } return pos; diff --git a/right/src/postponer.h b/right/src/postponer.h index 6db233d11..240514604 100644 --- a/right/src/postponer.h +++ b/right/src/postponer.h @@ -90,7 +90,7 @@ bool PostponerQuery_ContainsKeyId(uint8_t keyid); void PostponerQuery_FindFirstPressed(const postponer_buffer_record_type_t** press, const key_state_t* opposingKey); void PostponerQuery_FindFirstReleased(const postponer_buffer_record_type_t** release, const key_state_t* opposingKey); - uint8_t PostponerQuery_GetPendingKeypresses(uint8_t pendingKeyIds[], uint8_t maxCount); + uint8_t PostponerQuery_GetPendingKeypresses(uint8_t pendingKeyIds[], uint8_t maxCount, uint32_t cutoffTime); // Functions (Query APIs extended): uint16_t PostponerExtended_PendingId(uint16_t idx); diff --git a/right/src/str_utils.c b/right/src/str_utils.c index fa3368c9f..1a1574d06 100644 --- a/right/src/str_utils.c +++ b/right/src/str_utils.c @@ -156,6 +156,14 @@ static void consumeWhite(parser_context_t* ctx) } } +bool TryConsumeDot(parser_context_t* ctx) +{ + if (*ctx->at == '.') { + ++ctx->at; + return true; + } + return false; +} void ConsumeCommentsAsWhite(bool consume) { diff --git a/right/src/str_utils.h b/right/src/str_utils.h index 0341d18da..9f12fcefa 100644 --- a/right/src/str_utils.h +++ b/right/src/str_utils.h @@ -84,6 +84,7 @@ const char* CmdEnd(const char* cmd, const char *cmdEnd); void ConsumeUntilDot(parser_context_t* ctx); void ConsumeWhiteAt(parser_context_t* ctx, const char* at); + bool TryConsumeDot(parser_context_t* ctx); const char* SkipWhite(const char* cmd, const char *cmdEnd); uint8_t CountCommands(const char* text, uint16_t textLen); const char* TokEnd(const char* cmd, const char *cmdEnd); diff --git a/right/src/usb_report_updater.c b/right/src/usb_report_updater.c index 320112d8f..bcc77727b 100644 --- a/right/src/usb_report_updater.c +++ b/right/src/usb_report_updater.c @@ -372,7 +372,7 @@ static void applyKeystrokeSecondary(key_state_t *keyState, key_action_cached_t * if ( IS_SECONDARY_ROLE_LAYER_SWITCHER(secondaryRole) ) { // If the cached action is the current base role, then hold, otherwise keymap was changed. In that case do nothing just // as a well behaved hold action should. - if(KeyState_Active(keyState) && cachedAction->originKeymap == CurrentKeymapIndex) { + if(KeyState_Active(keyState)) { LayerSwitcher_HoldLayer(SECONDARY_ROLE_LAYER_TO_LAYER_ID(secondaryRole), false); } if (KeyState_ActivatedNow(keyState) || KeyState_DeactivatedNow(keyState)) { @@ -642,6 +642,7 @@ static void commitKeyState(key_state_t *keyState, bool active, uint8_t pressTime } else { KEY_TIMING(KeyTiming_RecordKeystroke(keyState, active, Timer_GetCurrentTime(), Timer_GetCurrentTime())); keyState->current = active; + EventVector_Set(EventVector_NativeActionsPostponing); } Macros_WakeBecauseOfKeystateChange(); } @@ -740,13 +741,24 @@ static void updateActionStates() { for (uint8_t slotId=0; slotIdactivationId; KeyHistory_RecordPress(keyState); From e0aebc26eff7bad7c0f6de47c852fd0bc2188037 Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Sun, 21 Jun 2026 21:10:44 +0200 Subject: [PATCH 04/13] Key Chords: Support for chord trigger on hold and on release --- right/src/chords.c | 268 +++++++++++++++++++++++++++------ right/src/chords.h | 17 +-- right/src/config_manager.c | 4 +- right/src/config_manager.h | 6 +- right/src/key_action.h | 1 - right/src/macros/set_command.c | 8 +- right/src/postponer.c | 38 +++++ right/src/postponer.h | 3 + right/src/usb_report_updater.c | 5 + 9 files changed, 287 insertions(+), 63 deletions(-) diff --git a/right/src/chords.c b/right/src/chords.c index 79702c37b..9a546e0ae 100644 --- a/right/src/chords.c +++ b/right/src/chords.c @@ -18,7 +18,7 @@ static inline void sortKeys(chord_keys_t keys, uint8_t keyCount) bool recheck = true; while ( recheck ) { recheck = false; - for (int i = 0; i < keyCount - 1; ++i) { + for (uint8_t i = 0; i < keyCount - 1; ++i) { if (keys[i] > keys[i + 1] ) { uint8_t temp = keys[i]; keys[i] = keys[i + 1]; @@ -45,19 +45,19 @@ uint8_t findUsedKeys(chord_keys_t unusedKeys, uint8_t keyCount, const chord_def_ return keyCount; } -void handleUpdateKeysOfDeletedChordOnLayer(uint8_t layer, chord_keys_t keys, uint8_t keyCount) { - for (int i = 0; i < ChordCount; ++i) { +void handleUpdateKeysOfDeletedChordOnLayer(layer_id_t layer, chord_keys_t keys, uint8_t keyCount) { + for (uint8_t i = 0; i < ChordCount; ++i) { if (Chords[i].layer < layer) continue; if (Chords[i].layer > layer && Chords[i].layer != LayerId_None) continue; if ((keyCount = findUsedKeys(keys, keyCount, &Chords[i])) == 0) break; } - for (int i = 0; i < keyCount; ++i) { + for (uint8_t i = 0; i < keyCount; ++i) { CurrentKeymap[layer][keys[i]/64][keys[i]%64].isPartOfChord = false; } } -void handleUpdateKeysOfDeletedChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount) { +void handleUpdateKeysOfDeletedChord(layer_id_t layer, chord_keys_t keys, uint8_t keyCount) { if (layer == LayerId_None) { for (layer_id_t laier = LayerId_Base; laier < LayerId_Count; ++laier) { handleUpdateKeysOfDeletedChordOnLayer(laier, keys, keyCount); @@ -99,7 +99,7 @@ static inline int16_t compareToChord(const chord_def_t *left, layer_id_t layer, return memcmp(left->keys, keys, keyCount); } -bool Chords_TryAddChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount, key_action_t *action) +bool Chords_TryAddChord(layer_id_t layer, chord_keys_t keys, uint8_t keyCount, key_action_t *action) { sortKeys(keys, keyCount); @@ -163,12 +163,13 @@ static bool isPartialOfChord(const chord_def_t *large, chord_keys_t keys, uint8_ return true; } -chord_search_result_t searchForChordAction(key_action_t *outAction, uint8_t layer, chord_keys_t keys, uint8_t keyCount, bool noPartial) +chord_search_result_t searchForChordAction(key_action_t *outAction, layer_id_t layer, chord_keys_t keys, uint8_t keyCount, bool noPartial) { sortKeys(keys, keyCount); uint8_t i = 0; bool matched = false; + bool partialMatched = false; while (true) { // First maybe find the chord @@ -179,6 +180,8 @@ chord_search_result_t searchForChordAction(key_action_t *outAction, uint8_t laye if (chordCmp == 0) { // We found the longest completed chord so far *outAction = Chords[i].action; + //Macros_SetStatusNum(i); + //Macros_SetStatusString("Matched!!!\n", NULL); matched = true; ++i; } @@ -193,7 +196,7 @@ chord_search_result_t searchForChordAction(key_action_t *outAction, uint8_t laye // Now iterate until we get to the longer chords in the layer because // we want to see if there are potential matches in them - if ( !noPartial ) { + if ( !noPartial && !partialMatched ) { while (i < ChordCount && Chords[i].layer == layer && Chords[i].keyCount == keyCount) {++i;} // i is now at the first longer chord in the layer, or the first key in another layer, on at list's end @@ -201,7 +204,7 @@ chord_search_result_t searchForChordAction(key_action_t *outAction, uint8_t laye while (i < ChordCount && Chords[i].layer == layer) { if (isPartialOfChord(&Chords[i++], keys, keyCount)) { // If we have a partial chord - return ChordSearch_NonConclusive; + partialMatched = true; } } } @@ -212,9 +215,15 @@ chord_search_result_t searchForChordAction(key_action_t *outAction, uint8_t laye layer = LayerId_None; } + if (matched && partialMatched) { + return ChordSearch_ExactAndPartial; + } if (matched) { // We have found an exact match - return ChordSearch_FinalMatch; + return ChordSearch_Exact; + } + if (partialMatched) { + return ChordSearch_Partial; } return ChordSearch_Nothing; } @@ -223,75 +232,236 @@ void Chords_ResetChords() { ChordCount = 0; } +typedef enum { + ResolutionStage_Press, + ResolutionStage_Wait, + ResolutionStage_Release, +} ResolutionStage; + static struct { key_state_t *initialKey; - volatile uint32_t initialTime; -} volatile ResolutionState; + uint32_t pressTime; + uint32_t releaseTime; + ResolutionStage stage; +} ResolutionState; static inline void finishResolution() { // Fake activation of the key now. ResolutionState.initialKey->current = true; ResolutionState.initialKey->previous = false; // Then clear the resolution state. - memset(&ResolutionState, 0, sizeof(ResolutionState)); + memset((void *)&ResolutionState, 0, sizeof(ResolutionState)); +} + + +// returns length of longest matched chord +static uint8_t matchLongestAvailableChord(key_action_t *resolvedAction, chord_keys_t keys, uint8_t count, layer_id_t layer) { + // Cut off any trailing keys which are not part of any chords. + for (uint8_t i = 0; i < count; ++i) { + if (!CurrentKeymap[layer][keys[i] / 64][keys[i] % 64].isPartOfChord) { + count = i; + break; + } + // Any double represented keys means that we have spam from macros. Stop at the dual key. + if (i > 0) { + for (uint8_t j = 0; j < i; ++j) { + if (keys[j] == keys[i]) { + count = i; + break; + } + } + } + } + if (count < 2) { + //Macros_SetStatusString("NoLong\n", NULL); + return 0; + } + // Have to test from the bottom up to keep order, to find the longest viable chord *from the beginning* + // Because of the sorting of the keys happening in the searcher, we can't start from the full key amount, as we need to sort of maintain order + uint8_t lastMatch = 0; + for (uint8_t i = 2; i <= count; ++i) { + chord_search_result_t searchRes = searchForChordAction(resolvedAction, layer, keys, i, i == count); + if (searchRes & ChordSearch_Exact) { + //Macros_SetStatusNum(searchRes); + //Macros_SetStatusString("GotExact\n", NULL); + lastMatch = i; + } + if (!(searchRes & ChordSearch_Partial)) { + break; // There are no potentially longer chords we could match with more keys + } + } + //Macros_SetStatusNum(lastMatch); + //Macros_SetStatusString("Longest\n", NULL); + return lastMatch; } -#define DOWAIT(start) EventScheduler_Schedule(start + Cfg.ChordTimeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord");\ - return ChordResolution_Wait; -#define DOFAIL finishResolution();\ +static chord_resolution_t runPressStage(key_state_t *keyState, layer_id_t layer, key_action_t *resolvedAction) +{ + chord_keys_t pressedKeys; + uint8_t keyCount = PostponerQuery_GetPendingKeypresses(pressedKeys + 1, MAX_CHORD_KEYS - 1, ResolutionState.pressTime + Cfg.Chords_Timeout) + 1; + + pressedKeys[0] = Utils_KeyStateToKeyId(keyState); + bool hasDuplicate = PostponerQuery_ContainsKeyId(pressedKeys[0]); + for (uint8_t i = 1; i < keyCount; ++i) { + for (uint8_t j = 0; j < i; ++j) { + if (pressedKeys[i] == pressedKeys[j]) { + keyCount = i; + break; + } + } + } + + const bool hasReasonToWait = Cfg.Chords_TriggerOnHold || Cfg.Chords_TriggerOnRelease; + const bool pressIntervalIsOver = ResolutionState.pressTime + Cfg.Chords_Timeout <= Timer_GetCurrentTime(); + const bool isFinalChoice = !hasReasonToWait && pressIntervalIsOver; + + chord_search_result_t searchRes = searchForChordAction(resolvedAction, layer, pressedKeys, keyCount, isFinalChoice); + + switch (searchRes) { + case ChordSearch_Exact: + //Macros_SetStatusNum(Timer_GetCurrentTime() - ResolutionState.pressTime); + //Macros_SetStatusChar('\n'); + //Macros_SetStatusString("Matched\n", NULL); + PostponerExtended_ConsumePendingKeypresses(keyCount - 1, true); + finishResolution(); + return ChordResolution_Resolved; + case ChordSearch_Nothing: + //Macros_SetStatusString("Failed\n", NULL); + finishResolution(); return ChordResolution_Failed; + case ChordSearch_Partial: + if (hasDuplicate) { + // If we had a duplicate, it will still be there in any future checks and block matches + finishResolution(); + return ChordResolution_Failed; + } + // Intentional fallthrough + case ChordSearch_ExactAndPartial: + //Macros_SetStatusString("Partial\n", NULL); + if (!pressIntervalIsOver) { + EventScheduler_Schedule(ResolutionState.pressTime + Cfg.Chords_Timeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Press Interval"); + } + else { + ResolutionState.stage = ResolutionStage_Wait; + EventScheduler_Schedule(ResolutionState.pressTime + Cfg.HoldTimeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Wait For Release"); + } + return ChordResolution_Wait; + } + return ChordResolution_Failed; +} -#define RESOLVED PostponerExtended_ConsumePendingKeypresses(keyCount, true);\ - finishResolution();\ + +static chord_resolution_t runWaitStage(key_state_t *keyState, layer_id_t layer, key_action_t *resolvedAction) +{ + //Macros_SetStatusString("InWait\n", NULL); + if (PostponerQuery_IsKeyReleased(keyState)) { + //Macros_SetStatusString("Released\n", NULL); + if (!Cfg.Chords_TriggerOnRelease) { + finishResolution(); + return ChordResolution_Failed; + } + postponer_buffer_record_type_t *press, *release; + PostponerQuery_InfoByKeystate(keyState, &press, &release); + ResolutionState.stage = ResolutionStage_Release; + ResolutionState.releaseTime = release->time; + EventScheduler_Schedule(release->time + Cfg.Chords_Timeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Wait release timeout"); + return ChordResolution_Wait; + } + if ((ResolutionState.pressTime + Cfg.HoldTimeout <= Timer_GetCurrentTime())) { + //Macros_SetStatusString("Held!\n", NULL); + // Reached timeout without releasing any keys. + if (Cfg.Chords_TriggerOnHold){ + // Resolve chord from currently held keys + chord_keys_t keys; + keys[0] = Utils_KeyStateToKeyId(keyState); + // Get the start of the queue which is still being held + uint8_t count = PostponerQuery_GetPendingHeldKeys(keys + 1, MAX_CHORD_KEYS - 1) + 1; + for (uint8_t i = 1; i < count; ++i) { + for (uint8_t j = 0; j < i; ++j) { + if (keys[i] == keys[j]) { + count = i; + break; + } + } + } + + count = matchLongestAvailableChord(resolvedAction, keys, count, layer); + + finishResolution(); + if (count == 0) { + //Macros_SetStatusString("HeldFailed\n", NULL); + return ChordResolution_Failed; + } + else { + //Macros_SetStatusNum(count); + //Macros_SetStatusString("HeldMatched\n", NULL); + PostponerExtended_ConsumePendingKeypresses(count - 1, true); + return ChordResolution_Resolved; + } + } + + finishResolution(); + return ChordResolution_Failed; + } + else { + EventScheduler_Schedule(ResolutionState.pressTime + Cfg.HoldTimeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Wait For Release"); + return ChordResolution_Wait; + } +} + +static chord_resolution_t runReleaseStage(key_state_t *keyState, layer_id_t layer, key_action_t *resolvedAction) { + if (ResolutionState.releaseTime + Cfg.Chords_Timeout > Timer_GetCurrentTime()) { + //Macros_SetStatusString("ReleasedWaiting\n", NULL); + EventScheduler_Schedule(ResolutionState.releaseTime + Cfg.Chords_Timeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Wait release timeout 2"); + return ChordResolution_Wait; + } + + + //Macros_SetStatusString("ReleasedReleased\n", NULL); + chord_keys_t keys; + uint8_t count = PostponerQuery_GetPendingReleaseCluster(keys, MAX_CHORD_KEYS, ResolutionState.releaseTime - Cfg.Chords_Timeout, Cfg.Chords_Timeout); + //Macros_SetStatusNum(count); + //Macros_SetStatusString("ReleaseCount\n", NULL); + + count = matchLongestAvailableChord(resolvedAction, keys, count, layer); + + finishResolution(); + if (count == 0) { + return ChordResolution_Failed; + } + else { + PostponerExtended_ConsumePendingKeypresses(count - 1, true); return ChordResolution_Resolved; + } +} -chord_resolution_t Chords_Driver(key_state_t *keyState, uint8_t layer, key_action_t *resolvedAction) +chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, key_action_t *resolvedAction) { uint32_t start = Timer_GetCurrentTimeMicros(); if (KeyState_ActivatedNow(keyState) && ResolutionState.initialKey == NULL) { ResolutionState.initialKey = keyState; - ResolutionState.initialTime = CurrentPostponedTime; + ResolutionState.pressTime = CurrentPostponedTime; + ResolutionState.stage = ResolutionStage_Press; } else if (ResolutionState.initialKey != keyState) { return ChordResolution_Failed; } - chord_keys_t pressedKeys; - uint8_t keyCount = PostponerQuery_GetPendingKeypresses(pressedKeys, MAX_CHORD_KEYS, ResolutionState.initialTime + Cfg.ChordTimeout); - if(keyCount == MAX_CHORD_KEYS) { - // We have mashed more keys than we support in chords. - // Note that next key in line might trigger chord with the rest, this one just doesn't. - DOFAIL + if (ResolutionState.stage == ResolutionStage_Press) { + return runPressStage(keyState, layer, resolvedAction); + } + if (ResolutionState.stage == ResolutionStage_Wait) { + return runWaitStage(keyState, layer, resolvedAction); + } + if (ResolutionState.stage == ResolutionStage_Release) { + return runReleaseStage(keyState, layer, resolvedAction); } - - pressedKeys[keyCount] = Utils_KeyStateToKeyId(keyState); - - const bool isFinalChoice = - (ResolutionState.initialTime + Cfg.ChordTimeout <= Timer_GetCurrentTime()) - || PostponerQuery_IsKeyReleased(keyState); - - - chord_search_result_t searchRes = searchForChordAction(resolvedAction, layer, pressedKeys, keyCount + 1, isFinalChoice); uint32_t elapsed = Timer_GetCurrentTimeMicros() - start; //Macros_SetStatusString("Processing micros: ", NULL); //Macros_SetStatusNum(elapsed); //Macros_SetStatusChar('\n'); - - switch (searchRes) { - case ChordSearch_FinalMatch: - //Macros_SetStatusNum(Timer_GetCurrentTime() - ResolutionState.initialTime); - //Macros_SetStatusChar('\n'); - //Macros_SetStatusString("Matched\n", NULL); - RESOLVED - case ChordSearch_Nothing: - //Macros_SetStatusString("Failed\n", NULL); - DOFAIL - case ChordSearch_NonConclusive: - //Macros_SetStatusString("Partial\n", NULL); - DOWAIT(ResolutionState.initialTime) - } return ChordResolution_Failed; } \ No newline at end of file diff --git a/right/src/chords.h b/right/src/chords.h index 3d6577ad8..523874608 100644 --- a/right/src/chords.h +++ b/right/src/chords.h @@ -7,22 +7,21 @@ typedef uint8_t chord_keys_t[MAX_CHORD_KEYS]; typedef enum { - ChordSearch_Nothing, // There is no chord which could match provided data - ChordSearch_NonConclusive, // There are potentially longer chords - ChordSearch_FinalMatch, // There was an exact match and no other potential matches + ChordSearch_Nothing = 0b00, // There is no chord which could match provided data + ChordSearch_Partial = 0b01, // There was no exact match, but there were chords eligible with more keys + ChordSearch_Exact = 0b10, // There was an exact match and no other potential matches + ChordSearch_ExactAndPartial = 0b11, // There was a match, but there are potentially longer chords } chord_search_result_t; typedef struct { + layer_id_t layer; + uint8_t keyCount; chord_keys_t keys; key_action_t action; - - uint8_t layer : 4; - - uint8_t keyCount : 3; } ATTR_PACKED chord_def_t; -bool Chords_TryAddChord(uint8_t layer, chord_keys_t keys, uint8_t keyCount, key_action_t *action); +bool Chords_TryAddChord(layer_id_t layer, chord_keys_t keys, uint8_t keyCount, key_action_t *action); void Chords_ResetChords(); typedef enum { @@ -31,6 +30,6 @@ typedef enum { ChordResolution_Resolved, } chord_resolution_t; -chord_resolution_t Chords_Driver(key_state_t *keyState, uint8_t layer, key_action_t *resolvedAction); +chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, key_action_t *resolvedAction); #endif diff --git a/right/src/config_manager.c b/right/src/config_manager.c index be135a9a0..3a6244e2e 100644 --- a/right/src/config_manager.c +++ b/right/src/config_manager.c @@ -271,7 +271,9 @@ const config_t DefaultCfg = (config_t){ .Macros_OneShotTimeout = 500, .AutoShiftDelay = 0, .ChordingDelay = 0, - .ChordTimeout = 75, + .Chords_Timeout = 75, + .Chords_TriggerOnHold = false, + .Chords_TriggerOnRelease = false, .BatteryStationaryMode = false, #ifdef __ZEPHYR__ .I2cBaudRate = 0, diff --git a/right/src/config_manager.h b/right/src/config_manager.h index d5008037c..b99d27838 100644 --- a/right/src/config_manager.h +++ b/right/src/config_manager.h @@ -91,8 +91,10 @@ uint16_t AutoRepeatDelayRate; uint16_t Macros_OneShotTimeout; uint16_t AutoShiftDelay; - uint8_t ChordingDelay; - uint8_t ChordTimeout; + uint8_t ChordingDelay; + uint8_t Chords_Timeout; + bool Chords_TriggerOnRelease; + bool Chords_TriggerOnHold; key_state_t* EmergencyKey; // bluetooth diff --git a/right/src/key_action.h b/right/src/key_action.h index 9d4e4cbf7..267cab68d 100644 --- a/right/src/key_action.h +++ b/right/src/key_action.h @@ -123,7 +123,6 @@ typedef struct { key_action_t action; uint8_t modifierLayerMask; - uint8_t originKeymap; } ATTR_PACKED key_action_cached_t; // Variables: diff --git a/right/src/macros/set_command.c b/right/src/macros/set_command.c index 84ecc3802..c57bdcacb 100644 --- a/right/src/macros/set_command.c +++ b/right/src/macros/set_command.c @@ -1151,7 +1151,13 @@ static macro_variable_t root(parser_context_t* ctx, set_command_action_t action) } else if (ConsumeToken(ctx, "chordTimeout")) { DEFINE_INT_LIMITS(0, 255); - ASSIGN_INT(Cfg.ChordTimeout); + ASSIGN_INT(Cfg.Chords_Timeout); + } + else if (ConsumeToken(ctx, "chordOnHold")) { + ASSIGN_BOOL(Cfg.Chords_TriggerOnHold); + } + else if (ConsumeToken(ctx, "chordOnRelease")) { + ASSIGN_BOOL(Cfg.Chords_TriggerOnRelease); } else if (ConsumeToken(ctx, "autoShiftDelay")) { DEFINE_INT_LIMITS(0, 65535); diff --git a/right/src/postponer.c b/right/src/postponer.c index 1997601ca..010b5e1cb 100644 --- a/right/src/postponer.c +++ b/right/src/postponer.c @@ -453,6 +453,44 @@ uint8_t PostponerQuery_GetPendingKeypresses(uint8_t keyIds[], uint8_t maxCount, return pos; } +// The point of this function is to get the unbroken string of pending key presses from the first one which have not yet been released +// This is used for chords activate on hold. For example, a chord could be for a mod like shift, and then another key was tapped while the chord was held +uint8_t PostponerQuery_GetPendingHeldKeys(uint8_t keyIds[], uint8_t maxCount) { + uint8_t pos = 0; + uint8_t i = 0; + while (pos < maxCount) { + while (i < bufferSize && buffer[POS(i)].event.type != PostponerEventType_PressKey) { ++i; } + if (i == bufferSize) break; + uint8_t j; + for (j = i + 1; j < bufferSize; ++j) { + if (buffer[POS(j)].event.type == PostponerEventType_ReleaseKey + && buffer[POS(j)].event.key.keyState == buffer[POS(i)].event.key.keyState) + { + break; + } + } + if (j == bufferSize) keyIds[pos++] = Utils_KeyStateToKeyId(buffer[POS(i)].event.key.keyState); + ++i; + } + return pos; +} + +uint8_t PostponerQuery_GetPendingReleaseCluster(uint8_t keyIds[], uint8_t maxCount, uint32_t startTime, uint8_t clusterTimespan) { + uint8_t pos = 0; + uint8_t i = 0; + uint32_t firstEventTime = startTime; + while (i < bufferSize && buffer[POS(i)].time < startTime) ++i; + while (pos < maxCount) { + while (i < bufferSize && buffer[POS(i)].event.type != PostponerEventType_ReleaseKey) ++i; + if (i == bufferSize + || buffer[POS(i)].time > firstEventTime + clusterTimespan) + break; + if (pos == 0) firstEventTime = buffer[POS(i)].time; + keyIds[pos++] = Utils_KeyStateToKeyId(buffer[POS(i++)].event.key.keyState); + } + return pos; +} + //########################## //### Extended Functions ### //########################## diff --git a/right/src/postponer.h b/right/src/postponer.h index 240514604..5c48c7a1c 100644 --- a/right/src/postponer.h +++ b/right/src/postponer.h @@ -91,6 +91,9 @@ void PostponerQuery_FindFirstPressed(const postponer_buffer_record_type_t** press, const key_state_t* opposingKey); void PostponerQuery_FindFirstReleased(const postponer_buffer_record_type_t** release, const key_state_t* opposingKey); uint8_t PostponerQuery_GetPendingKeypresses(uint8_t pendingKeyIds[], uint8_t maxCount, uint32_t cutoffTime); + uint8_t PostponerQuery_GetPendingHeldKeys(uint8_t keyIds[], uint8_t maxCount); + uint8_t PostponerQuery_GetPendingReleaseCluster(uint8_t keyIds[], uint8_t maxCount, uint32_t startTime, uint8_t clusterTimespan); + // Functions (Query APIs extended): uint16_t PostponerExtended_PendingId(uint16_t idx); diff --git a/right/src/usb_report_updater.c b/right/src/usb_report_updater.c index bcc77727b..1efad8fde 100644 --- a/right/src/usb_report_updater.c +++ b/right/src/usb_report_updater.c @@ -809,6 +809,11 @@ static void updateActionStates() { cachedAction = &actionCache[slotId][keyId]; Trace_Printc("w3"); + //apply base-layer holds + if (chordRes != ChordResolution_Wait && actionCache[slotId][keyId].action.type != KeyActionType_None) { + applyLayerHolds(keyState, &CurrentKeymap[LayerId_Base][slotId][keyId].action); + } + Trace_Printc("w4"); //apply active-layer action ApplyKeyAction(keyState, cachedAction, &NativeKeyboardReports); From 73abc0409372a9157e09c8a4214d6c0223084f89 Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Tue, 30 Jun 2026 21:00:36 +0200 Subject: [PATCH 05/13] Key Chords: Remove the logic to trigger on hold and release --- right/src/chords.c | 218 +++------------------------------ right/src/config_manager.c | 2 - right/src/config_manager.h | 2 - right/src/macros/set_command.c | 6 - right/src/usb_report_updater.c | 2 +- 5 files changed, 17 insertions(+), 213 deletions(-) diff --git a/right/src/chords.c b/right/src/chords.c index 9a546e0ae..0d88a2fb7 100644 --- a/right/src/chords.c +++ b/right/src/chords.c @@ -215,34 +215,17 @@ chord_search_result_t searchForChordAction(key_action_t *outAction, layer_id_t l layer = LayerId_None; } - if (matched && partialMatched) { - return ChordSearch_ExactAndPartial; - } - if (matched) { - // We have found an exact match - return ChordSearch_Exact; - } - if (partialMatched) { - return ChordSearch_Partial; - } - return ChordSearch_Nothing; + return (matched ? ChordSearch_Exact : 0) | (partialMatched ? ChordSearch_Partial : 0); } void Chords_ResetChords() { ChordCount = 0; } -typedef enum { - ResolutionStage_Press, - ResolutionStage_Wait, - ResolutionStage_Release, -} ResolutionStage; - static struct { key_state_t *initialKey; uint32_t pressTime; uint32_t releaseTime; - ResolutionStage stage; } ResolutionState; static inline void finishResolution() { @@ -253,51 +236,17 @@ static inline void finishResolution() { memset((void *)&ResolutionState, 0, sizeof(ResolutionState)); } - -// returns length of longest matched chord -static uint8_t matchLongestAvailableChord(key_action_t *resolvedAction, chord_keys_t keys, uint8_t count, layer_id_t layer) { - // Cut off any trailing keys which are not part of any chords. - for (uint8_t i = 0; i < count; ++i) { - if (!CurrentKeymap[layer][keys[i] / 64][keys[i] % 64].isPartOfChord) { - count = i; - break; - } - // Any double represented keys means that we have spam from macros. Stop at the dual key. - if (i > 0) { - for (uint8_t j = 0; j < i; ++j) { - if (keys[j] == keys[i]) { - count = i; - break; - } - } - } - } - if (count < 2) { - //Macros_SetStatusString("NoLong\n", NULL); - return 0; +chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, key_action_t *resolvedAction) +{ + uint32_t start = Timer_GetCurrentTimeMicros(); + if (KeyState_ActivatedNow(keyState) && ResolutionState.initialKey == NULL) { + ResolutionState.initialKey = keyState; + ResolutionState.pressTime = CurrentPostponedTime; } - // Have to test from the bottom up to keep order, to find the longest viable chord *from the beginning* - // Because of the sorting of the keys happening in the searcher, we can't start from the full key amount, as we need to sort of maintain order - uint8_t lastMatch = 0; - for (uint8_t i = 2; i <= count; ++i) { - chord_search_result_t searchRes = searchForChordAction(resolvedAction, layer, keys, i, i == count); - if (searchRes & ChordSearch_Exact) { - //Macros_SetStatusNum(searchRes); - //Macros_SetStatusString("GotExact\n", NULL); - lastMatch = i; - } - if (!(searchRes & ChordSearch_Partial)) { - break; // There are no potentially longer chords we could match with more keys - } + else if (ResolutionState.initialKey != keyState) { + return ChordResolution_Failed; } - //Macros_SetStatusNum(lastMatch); - //Macros_SetStatusString("Longest\n", NULL); - return lastMatch; -} - -static chord_resolution_t runPressStage(key_state_t *keyState, layer_id_t layer, key_action_t *resolvedAction) -{ chord_keys_t pressedKeys; uint8_t keyCount = PostponerQuery_GetPendingKeypresses(pressedKeys + 1, MAX_CHORD_KEYS - 1, ResolutionState.pressTime + Cfg.Chords_Timeout) + 1; @@ -312,156 +261,21 @@ static chord_resolution_t runPressStage(key_state_t *keyState, layer_id_t layer, } } - const bool hasReasonToWait = Cfg.Chords_TriggerOnHold || Cfg.Chords_TriggerOnRelease; const bool pressIntervalIsOver = ResolutionState.pressTime + Cfg.Chords_Timeout <= Timer_GetCurrentTime(); - const bool isFinalChoice = !hasReasonToWait && pressIntervalIsOver; - - chord_search_result_t searchRes = searchForChordAction(resolvedAction, layer, pressedKeys, keyCount, isFinalChoice); - - switch (searchRes) { - case ChordSearch_Exact: - //Macros_SetStatusNum(Timer_GetCurrentTime() - ResolutionState.pressTime); - //Macros_SetStatusChar('\n'); - //Macros_SetStatusString("Matched\n", NULL); - PostponerExtended_ConsumePendingKeypresses(keyCount - 1, true); - finishResolution(); - return ChordResolution_Resolved; - case ChordSearch_Nothing: - //Macros_SetStatusString("Failed\n", NULL); - finishResolution(); - return ChordResolution_Failed; - case ChordSearch_Partial: - if (hasDuplicate) { - // If we had a duplicate, it will still be there in any future checks and block matches - finishResolution(); - return ChordResolution_Failed; - } - // Intentional fallthrough - case ChordSearch_ExactAndPartial: - //Macros_SetStatusString("Partial\n", NULL); - if (!pressIntervalIsOver) { - EventScheduler_Schedule(ResolutionState.pressTime + Cfg.Chords_Timeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Press Interval"); - } - else { - ResolutionState.stage = ResolutionStage_Wait; - EventScheduler_Schedule(ResolutionState.pressTime + Cfg.HoldTimeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Wait For Release"); - } - return ChordResolution_Wait; - } - return ChordResolution_Failed; -} - - -static chord_resolution_t runWaitStage(key_state_t *keyState, layer_id_t layer, key_action_t *resolvedAction) -{ - //Macros_SetStatusString("InWait\n", NULL); - if (PostponerQuery_IsKeyReleased(keyState)) { - //Macros_SetStatusString("Released\n", NULL); - if (!Cfg.Chords_TriggerOnRelease) { - finishResolution(); - return ChordResolution_Failed; - } - postponer_buffer_record_type_t *press, *release; - PostponerQuery_InfoByKeystate(keyState, &press, &release); - ResolutionState.stage = ResolutionStage_Release; - ResolutionState.releaseTime = release->time; - EventScheduler_Schedule(release->time + Cfg.Chords_Timeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Wait release timeout"); - return ChordResolution_Wait; - } - if ((ResolutionState.pressTime + Cfg.HoldTimeout <= Timer_GetCurrentTime())) { - //Macros_SetStatusString("Held!\n", NULL); - // Reached timeout without releasing any keys. - if (Cfg.Chords_TriggerOnHold){ - // Resolve chord from currently held keys - chord_keys_t keys; - keys[0] = Utils_KeyStateToKeyId(keyState); - // Get the start of the queue which is still being held - uint8_t count = PostponerQuery_GetPendingHeldKeys(keys + 1, MAX_CHORD_KEYS - 1) + 1; - for (uint8_t i = 1; i < count; ++i) { - for (uint8_t j = 0; j < i; ++j) { - if (keys[i] == keys[j]) { - count = i; - break; - } - } - } - count = matchLongestAvailableChord(resolvedAction, keys, count, layer); + chord_search_result_t searchRes = searchForChordAction(resolvedAction, layer, pressedKeys, keyCount, pressIntervalIsOver || hasDuplicate); - finishResolution(); - if (count == 0) { - //Macros_SetStatusString("HeldFailed\n", NULL); - return ChordResolution_Failed; - } - else { - //Macros_SetStatusNum(count); - //Macros_SetStatusString("HeldMatched\n", NULL); - PostponerExtended_ConsumePendingKeypresses(count - 1, true); - return ChordResolution_Resolved; - } - } - - finishResolution(); - return ChordResolution_Failed; - } - else { - EventScheduler_Schedule(ResolutionState.pressTime + Cfg.HoldTimeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Wait For Release"); + if (searchRes & ChordSearch_Partial) { + EventScheduler_Schedule(ResolutionState.pressTime + Cfg.Chords_Timeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Press Interval"); return ChordResolution_Wait; } -} - -static chord_resolution_t runReleaseStage(key_state_t *keyState, layer_id_t layer, key_action_t *resolvedAction) { - if (ResolutionState.releaseTime + Cfg.Chords_Timeout > Timer_GetCurrentTime()) { - //Macros_SetStatusString("ReleasedWaiting\n", NULL); - EventScheduler_Schedule(ResolutionState.releaseTime + Cfg.Chords_Timeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Wait release timeout 2"); - return ChordResolution_Wait; - } - - - //Macros_SetStatusString("ReleasedReleased\n", NULL); - chord_keys_t keys; - uint8_t count = PostponerQuery_GetPendingReleaseCluster(keys, MAX_CHORD_KEYS, ResolutionState.releaseTime - Cfg.Chords_Timeout, Cfg.Chords_Timeout); - //Macros_SetStatusNum(count); - //Macros_SetStatusString("ReleaseCount\n", NULL); - - count = matchLongestAvailableChord(resolvedAction, keys, count, layer); - + finishResolution(); - if (count == 0) { - return ChordResolution_Failed; - } - else { - PostponerExtended_ConsumePendingKeypresses(count - 1, true); - return ChordResolution_Resolved; - } -} - -chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, key_action_t *resolvedAction) -{ - uint32_t start = Timer_GetCurrentTimeMicros(); - if (KeyState_ActivatedNow(keyState) && ResolutionState.initialKey == NULL) { - ResolutionState.initialKey = keyState; - ResolutionState.pressTime = CurrentPostponedTime; - ResolutionState.stage = ResolutionStage_Press; - } - else if (ResolutionState.initialKey != keyState) { - return ChordResolution_Failed; - } - if (ResolutionState.stage == ResolutionStage_Press) { - return runPressStage(keyState, layer, resolvedAction); - } - if (ResolutionState.stage == ResolutionStage_Wait) { - return runWaitStage(keyState, layer, resolvedAction); - } - if (ResolutionState.stage == ResolutionStage_Release) { - return runReleaseStage(keyState, layer, resolvedAction); + if (searchRes & ChordSearch_Exact) { + PostponerExtended_ConsumePendingKeypresses(keyCount - 1, true); + return ChordResolution_Resolved; } - uint32_t elapsed = Timer_GetCurrentTimeMicros() - start; - //Macros_SetStatusString("Processing micros: ", NULL); - //Macros_SetStatusNum(elapsed); - //Macros_SetStatusChar('\n'); - return ChordResolution_Failed; } \ No newline at end of file diff --git a/right/src/config_manager.c b/right/src/config_manager.c index 3a6244e2e..4d0146cfa 100644 --- a/right/src/config_manager.c +++ b/right/src/config_manager.c @@ -272,8 +272,6 @@ const config_t DefaultCfg = (config_t){ .AutoShiftDelay = 0, .ChordingDelay = 0, .Chords_Timeout = 75, - .Chords_TriggerOnHold = false, - .Chords_TriggerOnRelease = false, .BatteryStationaryMode = false, #ifdef __ZEPHYR__ .I2cBaudRate = 0, diff --git a/right/src/config_manager.h b/right/src/config_manager.h index b99d27838..2a299fe6e 100644 --- a/right/src/config_manager.h +++ b/right/src/config_manager.h @@ -93,8 +93,6 @@ uint16_t AutoShiftDelay; uint8_t ChordingDelay; uint8_t Chords_Timeout; - bool Chords_TriggerOnRelease; - bool Chords_TriggerOnHold; key_state_t* EmergencyKey; // bluetooth diff --git a/right/src/macros/set_command.c b/right/src/macros/set_command.c index c57bdcacb..e35c0a048 100644 --- a/right/src/macros/set_command.c +++ b/right/src/macros/set_command.c @@ -1153,12 +1153,6 @@ static macro_variable_t root(parser_context_t* ctx, set_command_action_t action) DEFINE_INT_LIMITS(0, 255); ASSIGN_INT(Cfg.Chords_Timeout); } - else if (ConsumeToken(ctx, "chordOnHold")) { - ASSIGN_BOOL(Cfg.Chords_TriggerOnHold); - } - else if (ConsumeToken(ctx, "chordOnRelease")) { - ASSIGN_BOOL(Cfg.Chords_TriggerOnRelease); - } else if (ConsumeToken(ctx, "autoShiftDelay")) { DEFINE_INT_LIMITS(0, 65535); ASSIGN_INT(Cfg.AutoShiftDelay); diff --git a/right/src/usb_report_updater.c b/right/src/usb_report_updater.c index 1efad8fde..e4c877e7b 100644 --- a/right/src/usb_report_updater.c +++ b/right/src/usb_report_updater.c @@ -810,7 +810,7 @@ static void updateActionStates() { Trace_Printc("w3"); //apply base-layer holds - if (chordRes != ChordResolution_Wait && actionCache[slotId][keyId].action.type != KeyActionType_None) { + if (chordRes != ChordResolution_Wait) { applyLayerHolds(keyState, &CurrentKeymap[LayerId_Base][slotId][keyId].action); } Trace_Printc("w4"); From 251c7de3555052e515edb6f98187df42ade40289 Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Fri, 3 Jul 2026 21:39:20 +0200 Subject: [PATCH 06/13] Key Chords: Key history now supports chords --- right/src/chords.c | 20 +++--- right/src/chords.h | 2 +- right/src/key_history.c | 113 ++++++++++++++++++++++++++------- right/src/key_history.h | 2 + right/src/usb_report_updater.c | 12 +++- 5 files changed, 111 insertions(+), 38 deletions(-) diff --git a/right/src/chords.c b/right/src/chords.c index 0d88a2fb7..679b4131d 100644 --- a/right/src/chords.c +++ b/right/src/chords.c @@ -12,6 +12,12 @@ chord_def_t Chords[MAX_CHORDS_COUNT]; uint8_t ChordCount = 0; +static struct { + key_state_t *initialKey; + uint32_t pressTime; + uint32_t releaseTime; +} ResolutionState; + static inline void sortKeys(chord_keys_t keys, uint8_t keyCount) { // Some kind of bubblesort, I dunno @@ -163,7 +169,7 @@ static bool isPartialOfChord(const chord_def_t *large, chord_keys_t keys, uint8_ return true; } -chord_search_result_t searchForChordAction(key_action_t *outAction, layer_id_t layer, chord_keys_t keys, uint8_t keyCount, bool noPartial) +chord_search_result_t searchForChordAction(chord_def_t **out_matchedChord, layer_id_t layer, chord_keys_t keys, uint8_t keyCount, bool noPartial) { sortKeys(keys, keyCount); @@ -179,7 +185,7 @@ chord_search_result_t searchForChordAction(key_action_t *outAction, layer_id_t l if (chordCmp < 0) continue; if (chordCmp == 0) { // We found the longest completed chord so far - *outAction = Chords[i].action; + *out_matchedChord = &Chords[i]; //Macros_SetStatusNum(i); //Macros_SetStatusString("Matched!!!\n", NULL); matched = true; @@ -222,12 +228,6 @@ void Chords_ResetChords() { ChordCount = 0; } -static struct { - key_state_t *initialKey; - uint32_t pressTime; - uint32_t releaseTime; -} ResolutionState; - static inline void finishResolution() { // Fake activation of the key now. ResolutionState.initialKey->current = true; @@ -236,7 +236,7 @@ static inline void finishResolution() { memset((void *)&ResolutionState, 0, sizeof(ResolutionState)); } -chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, key_action_t *resolvedAction) +chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, chord_def_t **out_matchedChord) { uint32_t start = Timer_GetCurrentTimeMicros(); if (KeyState_ActivatedNow(keyState) && ResolutionState.initialKey == NULL) { @@ -263,7 +263,7 @@ chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, key_ac const bool pressIntervalIsOver = ResolutionState.pressTime + Cfg.Chords_Timeout <= Timer_GetCurrentTime(); - chord_search_result_t searchRes = searchForChordAction(resolvedAction, layer, pressedKeys, keyCount, pressIntervalIsOver || hasDuplicate); + chord_search_result_t searchRes = searchForChordAction(out_matchedChord, layer, pressedKeys, keyCount, pressIntervalIsOver || hasDuplicate); if (searchRes & ChordSearch_Partial) { EventScheduler_Schedule(ResolutionState.pressTime + Cfg.Chords_Timeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Press Interval"); diff --git a/right/src/chords.h b/right/src/chords.h index 523874608..7d53862ba 100644 --- a/right/src/chords.h +++ b/right/src/chords.h @@ -30,6 +30,6 @@ typedef enum { ChordResolution_Resolved, } chord_resolution_t; -chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, key_action_t *resolvedAction); +chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, chord_def_t **out_matchedChord); #endif diff --git a/right/src/key_history.c b/right/src/key_history.c index 4734dc30a..f0c85c071 100644 --- a/right/src/key_history.c +++ b/right/src/key_history.c @@ -1,55 +1,120 @@ +#include "chords.h" #include "config_manager.h" #include "key_history.h" #include "postponer.h" typedef enum { - DoubletapState_Blocked, - DoubletapState_First, - DoubletapState_Multitap, - DoubletapState_Doubletap, -} doubletap_state_t; + HistoryEventType_Key, + HistoryEventType_Chord, +} history_event_type_t; typedef struct { - const key_state_t *keyState; - uint8_t keyActivationId; + const chord_def_t *chord; + const key_state_t *chord_keys[MAX_CHORD_KEYS]; +} ATTR_PACKED previous_chord_event_type_t; + +typedef struct { + history_event_type_t eventType; + union { + const key_state_t *key; + previous_chord_event_type_t chord; + } event; uint32_t timestamp; - doubletap_state_t doubletapState; -} previous_key_event_type_t; + bool multiTapBroken : 1; + uint8_t multiTapCount : 7; +} ATTR_PACKED previous_event_type_t; -static previous_key_event_type_t lastPress; + +previous_event_type_t lastPress; void KeyHistory_RecordPress(const key_state_t *keyState) { const bool isMultitap = - keyState == lastPress.keyState - && lastPress.doubletapState != DoubletapState_Blocked + lastPress.eventType == HistoryEventType_Key + && keyState == lastPress.event.key + && !lastPress.multiTapBroken && CurrentPostponedTime < lastPress.timestamp + Cfg.DoubletapTimeout; - const bool isDoubletap = isMultitap && - (lastPress.doubletapState == DoubletapState_First || lastPress.doubletapState == DoubletapState_Multitap); - lastPress = (previous_key_event_type_t){ - .keyState = keyState, - .keyActivationId = keyState->activationId, + lastPress = (previous_event_type_t){ + .eventType = HistoryEventType_Key, + .event = { .key = keyState }, + .timestamp = CurrentPostponedTime, + .multiTapBroken = false, + .multiTapCount = 1 + (isMultitap ? lastPress.multiTapCount : 0), + }; +} + +void KeyHistory_RecordChordPress(const key_state_t *keyState, const chord_def_t *chord) +{ + const bool isSameChord = + lastPress.eventType == HistoryEventType_Chord + && lastPress.event.chord.chord == chord; + + // There is a chance that the key press is one from the same chord activation + // This will be the case if we can see it's the same chord as last, but we haven't seen that key + // for that chord for that event. + // If so, register the key as pressed as part of this chord + bool isSameActivation = isSameChord; + for (uint8_t i = 0; isSameActivation && i < MAX_CHORD_KEYS; ++i) { + if (lastPress.event.chord.chord_keys[i] == keyState) { + isSameActivation = false; + break; + } + if (lastPress.event.chord.chord_keys[i] == NULL) { + break; + } + } + + if (isSameActivation) { + uint8_t i = 0; + while (lastPress.event.chord.chord_keys[i] != NULL) ++i; + lastPress.event.chord.chord_keys[i] = keyState; + return; + } + + const bool isMultitap = + isSameChord + && !lastPress.multiTapBroken + && CurrentPostponedTime < lastPress.timestamp + Cfg.DoubletapTimeout; + + lastPress = (previous_event_type_t) { + .eventType = HistoryEventType_Chord, + .event.chord = { + .chord_keys = { keyState, NULL, NULL, NULL, NULL }, + .chord = chord, + }, .timestamp = CurrentPostponedTime, - .doubletapState = isDoubletap ? DoubletapState_Doubletap : - isMultitap ? DoubletapState_Multitap : - DoubletapState_First, + .multiTapBroken = false, + .multiTapCount = 1 + (isMultitap ? lastPress.multiTapCount : 0), }; } void KeyHistory_RecordRelease(const key_state_t *keyState) { - if (keyState != lastPress.keyState) { - lastPress.doubletapState = DoubletapState_Blocked; + if (lastPress.eventType == HistoryEventType_Key) { + if (keyState != lastPress.event.key) { + lastPress.multiTapBroken = true; + } + } + else if (lastPress.eventType == HistoryEventType_Chord) { + bool breaksMulti = true; + for (uint8_t i = 0; i < MAX_CHORD_KEYS; ++i) { + if (lastPress.event.chord.chord_keys[i] == NULL) break; + if (lastPress.event.chord.chord_keys[i] == keyState ) { + breaksMulti = false; + break; + } + } + lastPress.multiTapBroken |= breaksMulti; } } bool KeyHistory_WasLastDoubletap() { - return lastPress.doubletapState == DoubletapState_Doubletap; + return lastPress.multiTapCount % 2 == 0; } bool KeyHistory_WasLastMultitap() { - return lastPress.doubletapState >= DoubletapState_Multitap; + return lastPress.multiTapCount > 1; } \ No newline at end of file diff --git a/right/src/key_history.h b/right/src/key_history.h index cba1547f3..fb16fc4d9 100644 --- a/right/src/key_history.h +++ b/right/src/key_history.h @@ -3,6 +3,7 @@ // Includes: +#include "chords.h" #include "key_states.h" // Macros: @@ -14,6 +15,7 @@ // Functions: void KeyHistory_RecordPress(const key_state_t *keyState); +void KeyHistory_RecordChordPress(const key_state_t *keyState, const chord_def_t *chord); void KeyHistory_RecordRelease(const key_state_t *keyState); bool KeyHistory_WasLastDoubletap(); bool KeyHistory_WasLastMultitap(); diff --git a/right/src/usb_report_updater.c b/right/src/usb_report_updater.c index e4c877e7b..80cd71617 100644 --- a/right/src/usb_report_updater.c +++ b/right/src/usb_report_updater.c @@ -755,6 +755,8 @@ static void updateActionStates() { for (uint8_t keyId=0; keyIdaction; + KeyHistory_RecordChordPress(keyState, activatedChord); + } + else if (chordRes == ChordResolution_Wait) { actionCache[slotId][keyId].action = (key_action_t){.type = KeyActionType_None}; EventVector_Set(EventVector_NativeActionsPostponing); } - if (chordRes == ChordResolution_Failed && KeyState_ActivatedNow(keyState)) { + else if (chordRes == ChordResolution_Failed && KeyState_ActivatedNow(keyState)) { // cache action so that key's meaning remains the same as long // as it is pressed actionCache[slotId][keyId].modifierLayerMask = 0; From a79733f77d3a147873d123d59371054690cdf094 Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Sat, 4 Jul 2026 21:46:39 +0200 Subject: [PATCH 07/13] Key Chords: Added support for the allKey application type Allows effect to last until the last key Lacks support for macro checks for key release Also removed an unused parameter --- right/src/chords.c | 65 ++++++++++++++++++++++++++-------- right/src/chords.h | 13 ++++--- right/src/config_manager.c | 1 + right/src/config_manager.h | 3 ++ right/src/key_history.c | 38 ++++++++++---------- right/src/macros/commands.c | 10 +++--- right/src/macros/set_command.c | 13 +++++++ right/src/postponer.c | 4 +-- right/src/postponer.h | 2 +- right/src/usb_report_updater.c | 2 +- 10 files changed, 102 insertions(+), 49 deletions(-) diff --git a/right/src/chords.c b/right/src/chords.c index 679b4131d..168840faf 100644 --- a/right/src/chords.c +++ b/right/src/chords.c @@ -9,13 +9,22 @@ #define MAX_CHORDS_COUNT 64 +typedef enum { + ChordSearch_Nothing = 0b00, // There is no chord which could match provided data + ChordSearch_Partial = 0b01, // There was no exact match, but there were chords eligible with more keys + ChordSearch_Exact = 0b10, // There was an exact match and no other potential matches + ChordSearch_ExactAndPartial = 0b11, // There was a match, but there are potentially longer chords +} chord_search_result_t; + chord_def_t Chords[MAX_CHORDS_COUNT]; uint8_t ChordCount = 0; static struct { - key_state_t *initialKey; + const key_state_t *initialKey; uint32_t pressTime; uint32_t releaseTime; + uint8_t unrollingKeysCount; + const chord_def_t *unrollingChord; } ResolutionState; static inline void sortKeys(chord_keys_t keys, uint8_t keyCount) @@ -169,7 +178,7 @@ static bool isPartialOfChord(const chord_def_t *large, chord_keys_t keys, uint8_ return true; } -chord_search_result_t searchForChordAction(chord_def_t **out_matchedChord, layer_id_t layer, chord_keys_t keys, uint8_t keyCount, bool noPartial) +chord_search_result_t searchForChordAction(const chord_def_t **out_matchedChord, layer_id_t layer, chord_keys_t keys, uint8_t keyCount, bool noPartial) { sortKeys(keys, keyCount); @@ -228,17 +237,20 @@ void Chords_ResetChords() { ChordCount = 0; } -static inline void finishResolution() { - // Fake activation of the key now. - ResolutionState.initialKey->current = true; - ResolutionState.initialKey->previous = false; - // Then clear the resolution state. - memset((void *)&ResolutionState, 0, sizeof(ResolutionState)); -} - -chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, chord_def_t **out_matchedChord) +chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, const chord_def_t **out_matchedChord) { - uint32_t start = Timer_GetCurrentTimeMicros(); + const uint8_t thisKeyId = Utils_KeyStateToKeyId(keyState); + if (KeyState_ActivatedNow(keyState) && ResolutionState.unrollingKeysCount > 0) { + --ResolutionState.unrollingKeysCount; + *out_matchedChord = ResolutionState.unrollingChord; + // Since the chord is unrolling, initial action on the keystroke should already have taken effect on the first key. + // We set the chord action, but block the initial effect. This allows the effect of keys to linger until release. + // Except with macros, where we specifically want the initial effect on the last key. + if ( !(ResolutionState.unrollingKeysCount == 0 && ResolutionState.unrollingChord->action.type == KeyActionType_PlayMacro) ) { + keyState->previous = true; + } + return ChordResolution_Resolved; + } if (KeyState_ActivatedNow(keyState) && ResolutionState.initialKey == NULL) { ResolutionState.initialKey = keyState; ResolutionState.pressTime = CurrentPostponedTime; @@ -250,7 +262,7 @@ chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, chord_ chord_keys_t pressedKeys; uint8_t keyCount = PostponerQuery_GetPendingKeypresses(pressedKeys + 1, MAX_CHORD_KEYS - 1, ResolutionState.pressTime + Cfg.Chords_Timeout) + 1; - pressedKeys[0] = Utils_KeyStateToKeyId(keyState); + pressedKeys[0] = thisKeyId; bool hasDuplicate = PostponerQuery_ContainsKeyId(pressedKeys[0]); for (uint8_t i = 1; i < keyCount; ++i) { for (uint8_t j = 0; j < i; ++j) { @@ -270,10 +282,33 @@ chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, chord_ return ChordResolution_Wait; } - finishResolution(); + memset((void *)&ResolutionState, 0, sizeof(ResolutionState)); + // Fake activation of the key now. + keyState->current = true; + keyState->previous = false; if (searchRes & ChordSearch_Exact) { - PostponerExtended_ConsumePendingKeypresses(keyCount - 1, true); + if (Cfg.Chords_ApplicationType == ChordApplicationType_LeadingKey) { + PostponerExtended_ConsumePendingKeypresses(keyCount - 1); + } + else if (Cfg.Chords_ApplicationType == ChordApplicationType_AllKeys) { + if ((*out_matchedChord)->action.type == KeyActionType_PlayMacro) { + // Do not activate on the first key, but rather on the last. + // This is to prevent the following issues: + // - activateKeyPostponed with prepend, or consumePending modifying the roll-out + // - the rest of the chord waiting on the queue causing problems with ifSecondary in the macro we run + // We are still hypothetically vulnerable to other macros using those commands, but that's highly hypothetical as they would + // likely be used while postponeKeys is active, meaning we would not be resolving anyway. + keyState->previous = true; + } + // TODO: + // - Somehow ensure that macro related release detections work with the idea of chord release rather than just the leading key. + // In the macro engine, could grab chord key list from key history on launch, and then perform the existing released check on all + // Costs some RAM to make room for 4 additional keyStates and activationIds in macro engine + // Paves the way for ifChord and chordKey.n commands + ResolutionState.unrollingKeysCount = keyCount - 1; + ResolutionState.unrollingChord = *out_matchedChord; + } return ChordResolution_Resolved; } diff --git a/right/src/chords.h b/right/src/chords.h index 7d53862ba..94b907207 100644 --- a/right/src/chords.h +++ b/right/src/chords.h @@ -4,14 +4,13 @@ #include "key_action.h" #define MAX_CHORD_KEYS 5 -typedef uint8_t chord_keys_t[MAX_CHORD_KEYS]; typedef enum { - ChordSearch_Nothing = 0b00, // There is no chord which could match provided data - ChordSearch_Partial = 0b01, // There was no exact match, but there were chords eligible with more keys - ChordSearch_Exact = 0b10, // There was an exact match and no other potential matches - ChordSearch_ExactAndPartial = 0b11, // There was a match, but there are potentially longer chords -} chord_search_result_t; + ChordApplicationType_LeadingKey, + ChordApplicationType_AllKeys, +} chord_application_type_t; + +typedef uint8_t chord_keys_t[MAX_CHORD_KEYS]; typedef struct { layer_id_t layer; @@ -30,6 +29,6 @@ typedef enum { ChordResolution_Resolved, } chord_resolution_t; -chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, chord_def_t **out_matchedChord); +chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, const chord_def_t **out_matchedChord); #endif diff --git a/right/src/config_manager.c b/right/src/config_manager.c index 4d0146cfa..5a9844d3c 100644 --- a/right/src/config_manager.c +++ b/right/src/config_manager.c @@ -272,6 +272,7 @@ const config_t DefaultCfg = (config_t){ .AutoShiftDelay = 0, .ChordingDelay = 0, .Chords_Timeout = 75, + .Chords_ApplicationType = ChordApplicationType_LeadingKey, .BatteryStationaryMode = false, #ifdef __ZEPHYR__ .I2cBaudRate = 0, diff --git a/right/src/config_manager.h b/right/src/config_manager.h index 2a299fe6e..68e91e762 100644 --- a/right/src/config_manager.h +++ b/right/src/config_manager.h @@ -3,6 +3,7 @@ // Includes: + #include "chords.h" #include "key_action.h" #include "module.h" #include "secondary_role_driver.h" @@ -93,6 +94,8 @@ uint16_t AutoShiftDelay; uint8_t ChordingDelay; uint8_t Chords_Timeout; + uint8_t Chords_ApplicationType; + key_state_t* EmergencyKey; // bluetooth diff --git a/right/src/key_history.c b/right/src/key_history.c index f0c85c071..90fc5c1d3 100644 --- a/right/src/key_history.c +++ b/right/src/key_history.c @@ -50,26 +50,28 @@ void KeyHistory_RecordChordPress(const key_state_t *keyState, const chord_def_t lastPress.eventType == HistoryEventType_Chord && lastPress.event.chord.chord == chord; - // There is a chance that the key press is one from the same chord activation - // This will be the case if we can see it's the same chord as last, but we haven't seen that key - // for that chord for that event. - // If so, register the key as pressed as part of this chord - bool isSameActivation = isSameChord; - for (uint8_t i = 0; isSameActivation && i < MAX_CHORD_KEYS; ++i) { - if (lastPress.event.chord.chord_keys[i] == keyState) { - isSameActivation = false; - break; - } - if (lastPress.event.chord.chord_keys[i] == NULL) { - break; + if (Cfg.Chords_ApplicationType == ChordApplicationType_AllKeys) { + // There is a chance that the key press is one from the same chord activation + // This will be the case if we can see it's the same chord as last, but we haven't seen that key + // for that chord for that event. + // If so, register the key as pressed as part of this chord + bool isSameActivation = isSameChord; + for (uint8_t i = 0; isSameActivation && i < MAX_CHORD_KEYS; ++i) { + if (lastPress.event.chord.chord_keys[i] == keyState) { + isSameActivation = false; + break; + } + if (lastPress.event.chord.chord_keys[i] == NULL) { + break; + } } - } - if (isSameActivation) { - uint8_t i = 0; - while (lastPress.event.chord.chord_keys[i] != NULL) ++i; - lastPress.event.chord.chord_keys[i] = keyState; - return; + if (isSameActivation) { + uint8_t i = 0; + while (lastPress.event.chord.chord_keys[i] != NULL) ++i; + lastPress.event.chord.chord_keys[i] = keyState; + return; + } } const bool isMultitap = diff --git a/right/src/macros/commands.c b/right/src/macros/commands.c index 1a17cb14f..3ce46efd1 100644 --- a/right/src/macros/commands.c +++ b/right/src/macros/commands.c @@ -165,7 +165,7 @@ static macro_result_t writeNum(uint32_t a) macro_result_t res = Macros_DispatchText(&num[10-len], len, NULL); if (res == MacroResult_Finished) { - PostponerExtended_ConsumePendingKeypresses(1, true); + PostponerExtended_ConsumePendingKeypresses(1); return MacroResult_Finished; } return res; @@ -1099,7 +1099,7 @@ static macro_result_t processResolveNextKeyIdCommand() } macro_result_t res = writeNum(PostponerExtended_PendingId(0)); if (res == MacroResult_Finished) { - PostponerExtended_ConsumePendingKeypresses(1, true); + PostponerExtended_ConsumePendingKeypresses(1); return MacroResult_Finished; } return res; @@ -1238,7 +1238,7 @@ static macro_result_t processIfShortcutCommand(parser_context_t* ctx, bool negat return MacroResult_Sleeping; } else if (cancelInTimedOut) { - PostponerExtended_ConsumePendingKeypresses(numArgs, true); + PostponerExtended_ConsumePendingKeypresses(numArgs); S->ms.macroBroken = true; goto conditionFailed; } @@ -1273,7 +1273,7 @@ static macro_result_t processIfShortcutCommand(parser_context_t* ctx, bool negat matched: //all keys match if (consume) { - PostponerExtended_ConsumePendingKeypresses(numArgs, true); + PostponerExtended_ConsumePendingKeypresses(numArgs); } if (negate) { goto conditionFailed; @@ -1659,7 +1659,7 @@ static macro_result_t processConsumePendingCommand(parser_context_t* ctx) if (Macros_DryRun) { return MacroResult_Finished; } - PostponerExtended_ConsumePendingKeypresses(cnt, true); + PostponerExtended_ConsumePendingKeypresses(cnt); return MacroResult_Finished; } diff --git a/right/src/macros/set_command.c b/right/src/macros/set_command.c index e35c0a048..1984fc3ec 100644 --- a/right/src/macros/set_command.c +++ b/right/src/macros/set_command.c @@ -1153,6 +1153,19 @@ static macro_variable_t root(parser_context_t* ctx, set_command_action_t action) DEFINE_INT_LIMITS(0, 255); ASSIGN_INT(Cfg.Chords_Timeout); } + else if (ConsumeToken(ctx, "chordApplication")) { + if (ConsumeToken(ctx, "leadingKey")) { + DEFINE_NONE_LIMITS(); + ASSIGN_CUSTOM(int32_t, intVar, Cfg.Chords_ApplicationType, ChordApplicationType_LeadingKey); + } + else if (ConsumeToken(ctx, "allKeys")) { + DEFINE_NONE_LIMITS(); + ASSIGN_CUSTOM(int32_t, intVar, Cfg.Chords_ApplicationType, ChordApplicationType_AllKeys); + } + else { + Macros_ReportError("Parameter not recognized:", ctx->at, ctx->end); + } + } else if (ConsumeToken(ctx, "autoShiftDelay")) { DEFINE_INT_LIMITS(0, 65535); ASSIGN_INT(Cfg.AutoShiftDelay); diff --git a/right/src/postponer.c b/right/src/postponer.c index 010b5e1cb..efcbb4f67 100644 --- a/right/src/postponer.c +++ b/right/src/postponer.c @@ -532,10 +532,10 @@ uint32_t PostponerExtended_LastPressTime() return lastPressTime; } -void PostponerExtended_ConsumePendingKeypresses(int count, bool suppress) +void PostponerExtended_ConsumePendingKeypresses(int count) { for (int i = 0; i < count; i++) { - consumeOneKeypress(suppress); + consumeOneKeypress(); } } diff --git a/right/src/postponer.h b/right/src/postponer.h index 5c48c7a1c..fb1d9ee8b 100644 --- a/right/src/postponer.h +++ b/right/src/postponer.h @@ -99,7 +99,7 @@ uint16_t PostponerExtended_PendingId(uint16_t idx); uint32_t PostponerExtended_LastPressTime(void); bool PostponerExtended_IsPendingKeyReleased(uint8_t idx); - void PostponerExtended_ConsumePendingKeypresses(int count, bool suppress); + void PostponerExtended_ConsumePendingKeypresses(int count); void PostponerExtended_ResetPostponer(void); void PostponerExtended_PrintContent(); diff --git a/right/src/usb_report_updater.c b/right/src/usb_report_updater.c index 80cd71617..fcc97d614 100644 --- a/right/src/usb_report_updater.c +++ b/right/src/usb_report_updater.c @@ -755,7 +755,7 @@ static void updateActionStates() { for (uint8_t keyId=0; keyId Date: Sat, 4 Jul 2026 22:03:19 +0200 Subject: [PATCH 08/13] Key Chords: Cleanup --- right/src/postponer.c | 38 ---------------------------------- right/src/postponer.h | 3 --- right/src/usb_report_updater.c | 5 ++--- 3 files changed, 2 insertions(+), 44 deletions(-) diff --git a/right/src/postponer.c b/right/src/postponer.c index efcbb4f67..4cfce3334 100644 --- a/right/src/postponer.c +++ b/right/src/postponer.c @@ -453,44 +453,6 @@ uint8_t PostponerQuery_GetPendingKeypresses(uint8_t keyIds[], uint8_t maxCount, return pos; } -// The point of this function is to get the unbroken string of pending key presses from the first one which have not yet been released -// This is used for chords activate on hold. For example, a chord could be for a mod like shift, and then another key was tapped while the chord was held -uint8_t PostponerQuery_GetPendingHeldKeys(uint8_t keyIds[], uint8_t maxCount) { - uint8_t pos = 0; - uint8_t i = 0; - while (pos < maxCount) { - while (i < bufferSize && buffer[POS(i)].event.type != PostponerEventType_PressKey) { ++i; } - if (i == bufferSize) break; - uint8_t j; - for (j = i + 1; j < bufferSize; ++j) { - if (buffer[POS(j)].event.type == PostponerEventType_ReleaseKey - && buffer[POS(j)].event.key.keyState == buffer[POS(i)].event.key.keyState) - { - break; - } - } - if (j == bufferSize) keyIds[pos++] = Utils_KeyStateToKeyId(buffer[POS(i)].event.key.keyState); - ++i; - } - return pos; -} - -uint8_t PostponerQuery_GetPendingReleaseCluster(uint8_t keyIds[], uint8_t maxCount, uint32_t startTime, uint8_t clusterTimespan) { - uint8_t pos = 0; - uint8_t i = 0; - uint32_t firstEventTime = startTime; - while (i < bufferSize && buffer[POS(i)].time < startTime) ++i; - while (pos < maxCount) { - while (i < bufferSize && buffer[POS(i)].event.type != PostponerEventType_ReleaseKey) ++i; - if (i == bufferSize - || buffer[POS(i)].time > firstEventTime + clusterTimespan) - break; - if (pos == 0) firstEventTime = buffer[POS(i)].time; - keyIds[pos++] = Utils_KeyStateToKeyId(buffer[POS(i++)].event.key.keyState); - } - return pos; -} - //########################## //### Extended Functions ### //########################## diff --git a/right/src/postponer.h b/right/src/postponer.h index fb1d9ee8b..ecf4d94e4 100644 --- a/right/src/postponer.h +++ b/right/src/postponer.h @@ -91,9 +91,6 @@ void PostponerQuery_FindFirstPressed(const postponer_buffer_record_type_t** press, const key_state_t* opposingKey); void PostponerQuery_FindFirstReleased(const postponer_buffer_record_type_t** release, const key_state_t* opposingKey); uint8_t PostponerQuery_GetPendingKeypresses(uint8_t pendingKeyIds[], uint8_t maxCount, uint32_t cutoffTime); - uint8_t PostponerQuery_GetPendingHeldKeys(uint8_t keyIds[], uint8_t maxCount); - uint8_t PostponerQuery_GetPendingReleaseCluster(uint8_t keyIds[], uint8_t maxCount, uint32_t startTime, uint8_t clusterTimespan); - // Functions (Query APIs extended): uint16_t PostponerExtended_PendingId(uint16_t idx); diff --git a/right/src/usb_report_updater.c b/right/src/usb_report_updater.c index fcc97d614..c629adda3 100644 --- a/right/src/usb_report_updater.c +++ b/right/src/usb_report_updater.c @@ -756,7 +756,6 @@ static void updateActionStates() { key_state_t *keyState = &KeyStates[slotId][keyId]; key_action_cached_t *cachedAction; const chord_def_t *activatedChord = NULL; - uint8_t chordActivationId; if(KEYSTATE_KEYINACTIVE(keyState)) { continue; @@ -815,8 +814,8 @@ static void updateActionStates() { cachedAction = &actionCache[slotId][keyId]; Trace_Printc("w3"); - //apply base-layer holds - if (chordRes != ChordResolution_Wait) { + //apply base-layer holds, but only if the key is not currenty applying another action + if (chordRes != ChordResolution_Wait && cachedAction->action.type == KeyActionType_None) { applyLayerHolds(keyState, &CurrentKeymap[LayerId_Base][slotId][keyId].action); } Trace_Printc("w4"); From 7c3c6c0ca0eb2ec8d81ed3690ab128b16141feea Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Fri, 24 Jul 2026 21:34:45 +0200 Subject: [PATCH 09/13] Key Chords: Support release detection in macros when using allKeys application --- right/src/chords.c | 104 ++++++++++++++++-- right/src/chords.h | 14 ++- right/src/key_history.c | 8 ++ right/src/key_history.h | 4 + right/src/macro_events.c | 4 +- right/src/macros/commands.c | 3 + right/src/macros/core.c | 60 +++++++--- right/src/macros/core.h | 4 +- right/src/postponer.c | 2 +- right/src/postponer.h | 2 +- .../usb_command_exec_macro_command.c | 2 +- right/src/usb_report_updater.c | 7 +- right/src/utils.c | 14 +-- 13 files changed, 184 insertions(+), 44 deletions(-) diff --git a/right/src/chords.c b/right/src/chords.c index 168840faf..abd191389 100644 --- a/right/src/chords.c +++ b/right/src/chords.c @@ -18,18 +18,26 @@ typedef enum { chord_def_t Chords[MAX_CHORDS_COUNT]; uint8_t ChordCount = 0; +uint8_t NextActivationId = CHORDS_INVALID_ACTIVATION_ID + 1; + +// Not sure how nice or not this is. I like named parameters rather than just true/false in calls. +// I also don't like the all-caps of macros, this C doesn't have consexpr, and I don't know if global consts will get optimized. +typedef enum { + KeyReleased = false, + KeyPressed = true, +} key_pressed_state_t; static struct { const key_state_t *initialKey; uint32_t pressTime; uint32_t releaseTime; uint8_t unrollingKeysCount; - const chord_def_t *unrollingChord; + chord_def_t *unrollingChord; } ResolutionState; static inline void sortKeys(chord_keys_t keys, uint8_t keyCount) { - // Some kind of bubblesort, I dunno + // Some kind of bubblesort, I dunno, 5 keys max, don't care about optimal algorithm bool recheck = true; while ( recheck ) { recheck = false; @@ -114,6 +122,11 @@ static inline int16_t compareToChord(const chord_def_t *left, layer_id_t layer, return memcmp(left->keys, keys, keyCount); } +/* +Search here is implemented as just iteration over the list. +One could do a binary search instead, for a minor speed increase. +I might do that later. +*/ bool Chords_TryAddChord(layer_id_t layer, chord_keys_t keys, uint8_t keyCount, key_action_t *action) { sortKeys(keys, keyCount); @@ -178,7 +191,8 @@ static bool isPartialOfChord(const chord_def_t *large, chord_keys_t keys, uint8_ return true; } -chord_search_result_t searchForChordAction(const chord_def_t **out_matchedChord, layer_id_t layer, chord_keys_t keys, uint8_t keyCount, bool noPartial) +// Again linear search, not binary +chord_search_result_t searchForChordAction(chord_def_t **out_matchedChord, layer_id_t layer, chord_keys_t keys, uint8_t keyCount, bool noPartial) { sortKeys(keys, keyCount); @@ -237,12 +251,25 @@ void Chords_ResetChords() { ChordCount = 0; } +bool setKeyHoldingChord(chord_def_t *chord, uint8_t keyId, key_pressed_state_t pressed) { + for (uint8_t i = 0; i < chord->keyCount; ++i) { + if (chord->keys[i] == keyId) { + if (pressed) chord->pressedStates |= 1 << i; + else chord->pressedStates &= ~(1 << i); + + return true; + } + } + return false; +} + chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, const chord_def_t **out_matchedChord) { const uint8_t thisKeyId = Utils_KeyStateToKeyId(keyState); if (KeyState_ActivatedNow(keyState) && ResolutionState.unrollingKeysCount > 0) { --ResolutionState.unrollingKeysCount; *out_matchedChord = ResolutionState.unrollingChord; + setKeyHoldingChord(ResolutionState.unrollingChord, thisKeyId, KeyPressed); // Since the chord is unrolling, initial action on the keystroke should already have taken effect on the first key. // We set the chord action, but block the initial effect. This allows the effect of keys to linger until release. // Except with macros, where we specifically want the initial effect on the last key. @@ -275,7 +302,8 @@ chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, const const bool pressIntervalIsOver = ResolutionState.pressTime + Cfg.Chords_Timeout <= Timer_GetCurrentTime(); - chord_search_result_t searchRes = searchForChordAction(out_matchedChord, layer, pressedKeys, keyCount, pressIntervalIsOver || hasDuplicate); + chord_def_t * matchedChord = NULL; + chord_search_result_t searchRes = searchForChordAction(&matchedChord, layer, pressedKeys, keyCount, pressIntervalIsOver || hasDuplicate); if (searchRes & ChordSearch_Partial) { EventScheduler_Schedule(ResolutionState.pressTime + Cfg.Chords_Timeout, EventSchedulerEvent_NativeActions, "NativeActions - Chord Press Interval"); @@ -288,11 +316,35 @@ chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, const keyState->previous = false; if (searchRes & ChordSearch_Exact) { + *out_matchedChord = matchedChord; + // Check that next acitvation id does not collide with an existing one since we use the activationId to identify chords. + // Collision can cause the following + // - Chord activated macro checks for release may use the wrong chord and be incorrect + // - If colliding chords share keyIds, one will not be registered as released until pressed and released again + //PR Maybe not worth the cost? It's highly unlikely and currently only affects macro checks for release if a macro is kept active for more than 60 chord presses. + bool collision; + do { + collision = false; + if (NextActivationId == CHORDS_INVALID_ACTIVATION_ID) ++NextActivationId; + for (uint8_t i = 0; i < ChordCount; ++i) { + if (Chords[i].activationId == NextActivationId) { + if (++NextActivationId == CHORDS_INVALID_ACTIVATION_ID) { + ++NextActivationId; + } + // Recheck from begining + collision = true; + } + } + } while (collision); + + matchedChord->activationId = NextActivationId++; + matchedChord->pressedStates = 0; + setKeyHoldingChord(matchedChord, thisKeyId, KeyPressed); if (Cfg.Chords_ApplicationType == ChordApplicationType_LeadingKey) { PostponerExtended_ConsumePendingKeypresses(keyCount - 1); } else if (Cfg.Chords_ApplicationType == ChordApplicationType_AllKeys) { - if ((*out_matchedChord)->action.type == KeyActionType_PlayMacro) { + if (matchedChord->action.type == KeyActionType_PlayMacro) { // Do not activate on the first key, but rather on the last. // This is to prevent the following issues: // - activateKeyPostponed with prepend, or consumePending modifying the roll-out @@ -301,16 +353,46 @@ chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, const // likely be used while postponeKeys is active, meaning we would not be resolving anyway. keyState->previous = true; } - // TODO: - // - Somehow ensure that macro related release detections work with the idea of chord release rather than just the leading key. - // In the macro engine, could grab chord key list from key history on launch, and then perform the existing released check on all - // Costs some RAM to make room for 4 additional keyStates and activationIds in macro engine - // Paves the way for ifChord and chordKey.n commands ResolutionState.unrollingKeysCount = keyCount - 1; - ResolutionState.unrollingChord = *out_matchedChord; + ResolutionState.unrollingChord = matchedChord; } return ChordResolution_Resolved; } return ChordResolution_Failed; +} + +void Chords_KeyReleased(const key_state_t *keyState) +{ + for (uint8_t i = 0; i < ChordCount; ++i) { + if (Chords[i].pressedStates == 0) continue; + if (setKeyHoldingChord(&Chords[i], Utils_KeyStateToKeyId(keyState), KeyReleased) ) { + if (Chords[i].pressedStates == 0) { + Chords[i].activationId = 0; + } + return; + } + } +} + +bool Chords_IsChordActivationActive(uint8_t activationId, bool checkPostponer) +{ + if (activationId == CHORDS_INVALID_ACTIVATION_ID) { + return false; + } + for (uint8_t i = 0; i < ChordCount; ++i) { + if (Chords[i].activationId == activationId) { + for (uint8_t j = 0; j < Chords[i].keyCount; ++j) { + if (Chords[i].pressedStates & (1 << j)) { + const key_state_t * const keyState = Utils_KeyIdToKeyState(Chords[i].keys[j]); + // Since we catch releases, if it's active, it's still the same activation. + // Check keystate because releases are registered after macro has it's go, so otherwise, this release would not get registered + if (KeyState_Active(keyState) && (!checkPostponer || !PostponerQuery_IsKeyReleased(keyState))) { + return true; + } + } + } + } + } + return false; } \ No newline at end of file diff --git a/right/src/chords.h b/right/src/chords.h index 94b907207..b5b49e161 100644 --- a/right/src/chords.h +++ b/right/src/chords.h @@ -4,6 +4,8 @@ #include "key_action.h" #define MAX_CHORD_KEYS 5 +#define CHORDS_ACTIVATIONID_SIZE 6 +#define CHORDS_INVALID_ACTIVATION_ID 0 typedef enum { ChordApplicationType_LeadingKey, @@ -13,11 +15,15 @@ typedef enum { typedef uint8_t chord_keys_t[MAX_CHORD_KEYS]; typedef struct { + // Chord definition layer_id_t layer; - uint8_t keyCount; chord_keys_t keys; - key_action_t action; + uint8_t keyCount : 3; + + // Chord state + uint8_t pressedStates : MAX_CHORD_KEYS; + uint8_t activationId : CHORDS_ACTIVATIONID_SIZE; } ATTR_PACKED chord_def_t; bool Chords_TryAddChord(layer_id_t layer, chord_keys_t keys, uint8_t keyCount, key_action_t *action); @@ -30,5 +36,7 @@ typedef enum { } chord_resolution_t; chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, const chord_def_t **out_matchedChord); - +void Chords_KeyReleased(const key_state_t *keyState); +bool Chords_IsChordActivationActive(uint8_t activationId, bool checkPostponer); + #endif diff --git a/right/src/key_history.c b/right/src/key_history.c index 90fc5c1d3..00c19b2ce 100644 --- a/right/src/key_history.c +++ b/right/src/key_history.c @@ -119,4 +119,12 @@ bool KeyHistory_WasLastDoubletap() bool KeyHistory_WasLastMultitap() { return lastPress.multiTapCount > 1; +} + +uint8_t KeyHistory_GetChordActivationIdOfLastAction() +{ + if (lastPress.eventType != HistoryEventType_Chord) { + return CHORDS_INVALID_ACTIVATION_ID; + } + return lastPress.event.chord.chord->activationId; } \ No newline at end of file diff --git a/right/src/key_history.h b/right/src/key_history.h index fb16fc4d9..939e8e7b0 100644 --- a/right/src/key_history.h +++ b/right/src/key_history.h @@ -14,11 +14,15 @@ // Functions: +// Registering events void KeyHistory_RecordPress(const key_state_t *keyState); void KeyHistory_RecordChordPress(const key_state_t *keyState, const chord_def_t *chord); void KeyHistory_RecordRelease(const key_state_t *keyState); + +// Querying data bool KeyHistory_WasLastDoubletap(); bool KeyHistory_WasLastMultitap(); +uint8_t KeyHistory_GetChordActivationIdOfLastAction(); #endif \ No newline at end of file diff --git a/right/src/macro_events.c b/right/src/macro_events.c index 38c7d3718..3bbf304b7 100644 --- a/right/src/macro_events.c +++ b/right/src/macro_events.c @@ -55,7 +55,7 @@ void MacroEvent_OnInit() const char* name = "$onInit"; uint8_t idx = FindMacroIndexByName(name, name + strlen(name), false); if (idx != 255) { - previousEventMacroSlot = Macros_StartMacro(idx, NULL, 0, 255, 255, false, NULL); + previousEventMacroSlot = Macros_StartMacro(idx, NULL, 0, 255, false, NULL); } registerJoinSplitEvents(); @@ -68,7 +68,7 @@ static void startMacroInSlot(macro_index_t macroIndex, uint8_t* slotId) { if (*slotId != 255 && MacroState[*slotId].ms.macroPlaying) { *slotId = Macros_QueueMacro(macroIndex, NULL, 255, *slotId); } else { - *slotId = Macros_StartMacro(macroIndex, NULL, 0, 255, 255, false, NULL); + *slotId = Macros_StartMacro(macroIndex, NULL, 0, 255, false, NULL); } } } diff --git a/right/src/macros/commands.c b/right/src/macros/commands.c index 3ce46efd1..0adc02f8e 100644 --- a/right/src/macros/commands.c +++ b/right/src/macros/commands.c @@ -138,6 +138,9 @@ bool Macros_CurrentMacroKeyIsActive() if (S->ms.currentMacroKey == NULL) { return S->ms.oneShot == 1; } + if (S->ms.chordActivationId != CHORDS_INVALID_ACTIVATION_ID && Cfg.Chords_ApplicationType == ChordApplicationType_AllKeys) { + return (S->ms.oneShot == 1) || Chords_IsChordActivationActive(S->ms.chordActivationId, isCurrentMacroPostponing()); + } if (isCurrentMacroPostponing()) { bool isSameActivation = (S->ms.currentMacroKey->activationId == S->ms.keyActivationId); bool keyIsActive = (KeyState_Active(S->ms.currentMacroKey) && !PostponerQuery_IsKeyReleased(S->ms.currentMacroKey)); diff --git a/right/src/macros/core.c b/right/src/macros/core.c index ed832360c..61314793a 100644 --- a/right/src/macros/core.c +++ b/right/src/macros/core.c @@ -420,10 +420,22 @@ macro_result_t Macros_ExecMacro(uint8_t macroIndex) return MacroResult_JumpedForward; } +//partentMacroSlot == 255 means no parent +uint8_t startMacro( + uint8_t index, + key_state_t *keyState, + uint16_t argumentOffset, + uint8_t keyActivationId, + uint8_t parentMacroSlot, + bool runFirstAction, + const char *inlineText, + const macro_state_t *parentMacroState +); + macro_result_t Macros_CallMacro(uint8_t macroIndex) { uint32_t parentSlotIndex = S - MacroState; - uint8_t childSlotIndex = Macros_StartMacro(macroIndex, S->ms.currentMacroKey, 0, S->ms.keyActivationId, parentSlotIndex, true, NULL); + uint8_t childSlotIndex = startMacro(macroIndex, S->ms.currentMacroKey, 0, S->ms.keyActivationId, parentSlotIndex, true, NULL, S); if (childSlotIndex != 255) { unscheduleCurrentSlot(); @@ -437,7 +449,7 @@ macro_result_t Macros_CallMacro(uint8_t macroIndex) macro_result_t Macros_ForkMacro(uint8_t macroIndex) { - Macros_StartMacro(macroIndex, S->ms.currentMacroKey, 0, S->ms.keyActivationId, 255, true, NULL); + startMacro(macroIndex, S->ms.currentMacroKey, 0, S->ms.keyActivationId, 255, true, NULL, S); return MacroResult_Finished; } @@ -447,7 +459,8 @@ uint8_t initMacro( uint16_t argumentOffset, uint8_t keyActivationId, uint8_t parentMacroSlot, - const char *inlineText + const char *inlineText, + const macro_state_t *parentState ) { if (!macroIsValid(index) || !findFreeStateSlot() || !findFreeScopeStateSlot()) { return 255; @@ -461,11 +474,22 @@ uint8_t initMacro( S->ms.currentMacroIndex = index; S->ms.currentMacroKey = keyState; S->ms.keyActivationId = keyActivationId; - S->ms.currentMacroStartTime = CurrentPostponedTime; S->ms.currentMacroArgumentOffset = argumentOffset; S->ms.parentMacroSlot = parentMacroSlot; - S->ms.isDoubletap = keyState != NULL && KeyHistory_WasLastDoubletap(); S->ms.isFirstCommand = true; + if (parentState != NULL) { + S->ms.currentMacroStartTime = S->ms.currentMacroStartTime; + S->ms.secondaryRoleState = S->ms.secondaryRoleState; + S->ms.isDoubletap = S->ms.isDoubletap; + S->ms.chordActivationId = S->ms.chordActivationId; + } + else { + S->ms.currentMacroStartTime = CurrentPostponedTime; + if (keyState != NULL) { + S->ms.isDoubletap = KeyHistory_WasLastDoubletap(); + S->ms.chordActivationId = KeyHistory_GetChordActivationIdOfLastAction(); // returns invalid id if it was not a chord. + } + } // If inline text is provided, set up the action before resetToAddressZero if (inlineText != NULL) { @@ -488,20 +512,19 @@ uint8_t initMacro( return S - MacroState; } - -//partentMacroSlot == 255 means no parent -uint8_t Macros_StartMacro( +uint8_t startMacro( uint8_t index, key_state_t *keyState, uint16_t argumentOffset, uint8_t keyActivationId, uint8_t parentMacroSlot, bool runFirstAction, - const char *inlineText + const char *inlineText, + const macro_state_t *parentMacroState ) { macro_state_t* oldState = S; - uint8_t slotIndex = initMacro(index, keyState, argumentOffset, keyActivationId, parentMacroSlot, inlineText); + uint8_t slotIndex = initMacro(index, keyState, argumentOffset, keyActivationId, parentMacroSlot, inlineText, parentMacroState); if (slotIndex == 255) { S = oldState; @@ -525,14 +548,25 @@ uint8_t Macros_StartMacro( return slotIndex; } +uint8_t Macros_StartMacro( + uint8_t index, + key_state_t *keyState, + uint16_t argumentOffset, + uint8_t keyActivationId, + bool runFirstAction, + const char *inlineText +) { + return startMacro(index, keyState, argumentOffset, keyActivationId, MacroIndex_None, runFirstAction, inlineText, NULL); +} + uint8_t Macros_StartInlineMacro(const char *text, key_state_t *keyState, uint8_t keyActivationId) { - return Macros_StartMacro(MacroIndex_InlineMacro, keyState, 0, keyActivationId, 255, true, text); + return startMacro(MacroIndex_InlineMacro, keyState, 0, keyActivationId, 255, true, text, NULL); } void Macros_ValidateMacro(uint8_t macroIndex, uint16_t argumentOffset, uint8_t moduleId, uint8_t keyIdx, uint8_t keymapIdx, uint8_t layerIdx) { bool wasValid = true; - uint8_t slotIndex = initMacro(macroIndex, NULL, argumentOffset, 255, 255, NULL); + uint8_t slotIndex = initMacro(macroIndex, NULL, argumentOffset, 255, 255, NULL, NULL); if (slotIndex == 255) { S = NULL; @@ -615,7 +649,7 @@ uint8_t Macros_QueueMacro(uint8_t index, key_state_t *keyState, uint8_t keyActiv { macro_state_t* oldState = S; - uint8_t slotIndex = initMacro(index, keyState, 0, keyActivationId, 255, NULL); + uint8_t slotIndex = initMacro(index, keyState, 0, keyActivationId, 255, NULL, NULL); if (slotIndex == 255) { return slotIndex; diff --git a/right/src/macros/core.h b/right/src/macros/core.h index 4f7779dca..82c3e8841 100644 --- a/right/src/macros/core.h +++ b/right/src/macros/core.h @@ -6,6 +6,7 @@ #include #include #include "attributes.h" + #include "chords.h" #include "key_action.h" #include "key_states.h" #include "str_utils.h" @@ -155,6 +156,7 @@ uint8_t oneShot : 2; bool macroInterrupted : 1; uint8_t keyActivationId: 4; + uint8_t chordActivationId: CHORDS_ACTIVATIONID_SIZE; // TODO: refactor macroSleeping, macroBroken and macroPlaying into a single state? bool macroSleeping : 1; bool macroBroken : 1; @@ -277,7 +279,7 @@ macro_result_t Macros_SleepTillTime(uint32_t time, const char* reason); uint8_t Macros_ConsumeLayerId(parser_context_t* ctx); uint8_t Macros_QueueMacro(uint8_t index, key_state_t *keyState, uint8_t keyActivationId, uint8_t queueAfterSlot); - uint8_t Macros_StartMacro(uint8_t index, key_state_t *keyState, uint16_t argumentOffset, uint8_t keyActivationId, uint8_t parentMacroSlot, bool runFirstAction, const char *inlineText); + uint8_t Macros_StartMacro(uint8_t index, key_state_t *keyState, uint16_t argumentOffset, uint8_t keyActivationId, bool runFirstAction, const char *inlineText); uint8_t Macros_StartInlineMacro(const char *text, key_state_t *keyState, uint8_t keyActivationId); uint8_t Macros_TryConsumeKeyId(parser_context_t* ctx); void Macros_ContinueMacro(void); diff --git a/right/src/postponer.c b/right/src/postponer.c index 4cfce3334..182d38d2b 100644 --- a/right/src/postponer.c +++ b/right/src/postponer.c @@ -361,7 +361,7 @@ uint8_t PostponerQuery_PendingKeypressCount() } -bool PostponerQuery_IsKeyReleased(key_state_t* key) +bool PostponerQuery_IsKeyReleased(const key_state_t* key) { if (key == NULL) { return false; diff --git a/right/src/postponer.h b/right/src/postponer.h index ecf4d94e4..3e6a77223 100644 --- a/right/src/postponer.h +++ b/right/src/postponer.h @@ -84,7 +84,7 @@ // Functions (Basic Query APIs): uint8_t PostponerQuery_PendingKeypressCount(); - bool PostponerQuery_IsKeyReleased(key_state_t* key); + bool PostponerQuery_IsKeyReleased(const key_state_t* key); bool PostponerQuery_IsActiveEventually(key_state_t* key); void PostponerQuery_InfoByKeystate(key_state_t* key, postponer_buffer_record_type_t** press, postponer_buffer_record_type_t** release); bool PostponerQuery_ContainsKeyId(uint8_t keyid); diff --git a/right/src/usb_commands/usb_command_exec_macro_command.c b/right/src/usb_commands/usb_command_exec_macro_command.c index 62d964db9..7d94058e0 100644 --- a/right/src/usb_commands/usb_command_exec_macro_command.c +++ b/right/src/usb_commands/usb_command_exec_macro_command.c @@ -42,7 +42,7 @@ static bool canExecute() void UsbMacroCommand_ExecuteSynchronously() { - Macros_StartMacro(MacroIndex_InlineMacro, &dummyState, 0, 255, MacroIndex_None, false, UsbMacroCommand); + Macros_StartMacro(MacroIndex_InlineMacro, &dummyState, 0, 255, false, UsbMacroCommand); EventVector_Unset(EventVector_UsbMacroCommandWaitingForExecution); } diff --git a/right/src/usb_report_updater.c b/right/src/usb_report_updater.c index c629adda3..cd3572d21 100644 --- a/right/src/usb_report_updater.c +++ b/right/src/usb_report_updater.c @@ -538,7 +538,7 @@ void ApplyKeyAction(key_state_t *keyState, key_action_cached_t *cachedAction, us case KeyActionType_PlayMacro: if (KeyState_ActivatedNow(keyState)) { StickyMods_ResetLater(cachedAction); - Macros_StartMacro(action->playMacro.macroId, keyState, action->playMacro.offset, keyState->activationId, 255, true, NULL); + Macros_StartMacro(action->playMacro.macroId, keyState, action->playMacro.offset, keyState->activationId, true, NULL); } break; case KeyActionType_InlineMacro: @@ -824,6 +824,11 @@ static void updateActionStates() { ApplyKeyAction(keyState, cachedAction, &NativeKeyboardReports); if (KeyState_DeactivatedNow(keyState)) { + // I kind of want to only trigger chord releases for chord-holding keys. + // It's safe as it is, but it is not exactly free if there are chords. + // Maybe grab a bit in the keyState for isHoldingChord? + // I could also get rid of the need to do this by storing activationIds for the holding keys, costing memory per chord. + Chords_KeyReleased(keyState); KeyHistory_RecordRelease(keyState); keyState->secondaryState = SecondaryRoleState_DontKnowYet; } diff --git a/right/src/utils.c b/right/src/utils.c index 020142c81..f7b4bc681 100644 --- a/right/src/utils.c +++ b/right/src/utils.c @@ -25,10 +25,7 @@ #if !DEVICE_IS_UHK_DONGLE //this is noop at the moment, prepared for time when MAX_KEY_COUNT_PER_MODULE changes //the purpose is to preserve current keyids -static uint16_t recodeId(uint16_t newFormat, uint16_t fromBase, uint16_t toBase) -{ - return toBase * (newFormat / fromBase) + (newFormat % fromBase); -} +#define RECODE_ID(id, oldBase, newBase) (newBase * (id / oldBase) + id % oldBase) #endif uint16_t Utils_KeyStateToKeyId(const key_state_t* key) @@ -37,12 +34,9 @@ uint16_t Utils_KeyStateToKeyId(const key_state_t* key) return 0; #else if (key == NULL) { - return 0; + return 255; } - uint32_t ptr1 = (uint32_t)(key_state_t*)key; - uint32_t ptr2 = (uint32_t)(key_state_t*)&(KeyStates[0][0]); - uint32_t res = (ptr1 - ptr2) / sizeof(key_state_t); - return recodeId(res, MAX_KEY_COUNT_PER_MODULE, 64); + return RECODE_ID((uint16_t)(key - (key_state_t*)KeyStates), MAX_KEY_COUNT_PER_MODULE, 64); #endif } @@ -51,7 +45,7 @@ key_state_t* Utils_KeyIdToKeyState(uint16_t keyid) #if DEVICE_IS_UHK_DONGLE return NULL; #else - return &(((key_state_t*)KeyStates)[recodeId(keyid, 64, MAX_KEY_COUNT_PER_MODULE)]); + return &(((key_state_t*)KeyStates)[RECODE_ID(keyid, 64, MAX_KEY_COUNT_PER_MODULE)]); #endif } From d2d0647cd81335c7ce2a0a44a9917218a42ca850 Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Sat, 25 Jul 2026 21:05:37 +0200 Subject: [PATCH 10/13] Key Chords: Minimum idle time since last pressed implemented --- right/src/chords.c | 4 ++++ right/src/config_manager.c | 1 + right/src/config_manager.h | 1 + right/src/key_history.c | 4 ++++ right/src/key_history.h | 1 + right/src/macros/set_command.c | 4 ++++ 6 files changed, 15 insertions(+) diff --git a/right/src/chords.c b/right/src/chords.c index abd191389..2afc43ce1 100644 --- a/right/src/chords.c +++ b/right/src/chords.c @@ -1,6 +1,7 @@ #include "chords.h" #include "config_manager.h" #include "event_scheduler.h" +#include "key_history.h" #include "keymap.h" #include "layer.h" #include "macros/status_buffer.h" @@ -278,6 +279,9 @@ chord_resolution_t Chords_Driver(key_state_t *keyState, layer_id_t layer, const } return ChordResolution_Resolved; } + if (Cfg.Chords_MinimumIdleTime + KeyHistory_GetLastActivationTime() > CurrentPostponedTime) { + return ChordResolution_Failed; + } if (KeyState_ActivatedNow(keyState) && ResolutionState.initialKey == NULL) { ResolutionState.initialKey = keyState; ResolutionState.pressTime = CurrentPostponedTime; diff --git a/right/src/config_manager.c b/right/src/config_manager.c index 5a9844d3c..f6c1d210b 100644 --- a/right/src/config_manager.c +++ b/right/src/config_manager.c @@ -272,6 +272,7 @@ const config_t DefaultCfg = (config_t){ .AutoShiftDelay = 0, .ChordingDelay = 0, .Chords_Timeout = 75, + .Chords_MinimumIdleTime = 70, .Chords_ApplicationType = ChordApplicationType_LeadingKey, .BatteryStationaryMode = false, #ifdef __ZEPHYR__ diff --git a/right/src/config_manager.h b/right/src/config_manager.h index 68e91e762..3cac9b588 100644 --- a/right/src/config_manager.h +++ b/right/src/config_manager.h @@ -95,6 +95,7 @@ uint8_t ChordingDelay; uint8_t Chords_Timeout; uint8_t Chords_ApplicationType; + uint16_t Chords_MinimumIdleTime; key_state_t* EmergencyKey; diff --git a/right/src/key_history.c b/right/src/key_history.c index 00c19b2ce..88a753a3f 100644 --- a/right/src/key_history.c +++ b/right/src/key_history.c @@ -127,4 +127,8 @@ uint8_t KeyHistory_GetChordActivationIdOfLastAction() return CHORDS_INVALID_ACTIVATION_ID; } return lastPress.event.chord.chord->activationId; +} + +uint32_t KeyHistory_GetLastActivationTime() { + return lastPress.timestamp; } \ No newline at end of file diff --git a/right/src/key_history.h b/right/src/key_history.h index 939e8e7b0..8670f7e1d 100644 --- a/right/src/key_history.h +++ b/right/src/key_history.h @@ -23,6 +23,7 @@ void KeyHistory_RecordRelease(const key_state_t *keyState); bool KeyHistory_WasLastDoubletap(); bool KeyHistory_WasLastMultitap(); uint8_t KeyHistory_GetChordActivationIdOfLastAction(); +uint32_t KeyHistory_GetLastActivationTime(); #endif \ No newline at end of file diff --git a/right/src/macros/set_command.c b/right/src/macros/set_command.c index 1984fc3ec..8a4606e89 100644 --- a/right/src/macros/set_command.c +++ b/right/src/macros/set_command.c @@ -1153,6 +1153,10 @@ static macro_variable_t root(parser_context_t* ctx, set_command_action_t action) DEFINE_INT_LIMITS(0, 255); ASSIGN_INT(Cfg.Chords_Timeout); } + else if (ConsumeToken(ctx, "chordPriorIdleTime")) { + DEFINE_INT_LIMITS(0, 65535); + ASSIGN_INT(Cfg.Chords_MinimumIdleTime); + } else if (ConsumeToken(ctx, "chordApplication")) { if (ConsumeToken(ctx, "leadingKey")) { DEFINE_NONE_LIMITS(); From e51201e03e40212735fa3f4901e50b52e2da1b2e Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Sun, 26 Jul 2026 16:43:44 +0200 Subject: [PATCH 11/13] Key Chords: Fix after rebase to master --- right/src/usb_report_sender.c | 10 ---------- right/src/usb_report_updater.c | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/right/src/usb_report_sender.c b/right/src/usb_report_sender.c index 1f710f4f2..579288014 100644 --- a/right/src/usb_report_sender.c +++ b/right/src/usb_report_sender.c @@ -136,12 +136,7 @@ static void clearMouseMovement(void) { } } -static bool mouseButtonsChanged(void) { - return mouseReports[0].buttons != mouseReports[1].buttons; -} - static void sendActiveReports(bool resending) { - bool usbReportsChangedByAction = false; bool usbReportsChangedByAnything = false; errno_t ret; @@ -178,7 +173,6 @@ static void sendActiveReports(bool resending) { UsbReportSender_ResendOrGiveUp(&UsbSemaphore.keyboard, ret, true); } } - usbReportsChangedByAction = true; usbReportsChangedByAnything = true; lastBasicReportTime = Timer_GetCurrentTime(); UsbReportUpdater_LastActivityTime = resending ? UsbReportUpdater_LastActivityTime : Timer_GetCurrentTime(); @@ -193,13 +187,10 @@ static void sendActiveReports(bool resending) { UsbReportSender_ResendOrGiveUp(&UsbSemaphore.controls, ret, true); } UsbReportUpdater_LastActivityTime = resending ? UsbReportUpdater_LastActivityTime : Timer_GetCurrentTime(); - usbReportsChangedByAction = true; usbReportsChangedByAnything = true; } if (MouseReport_HasChanges(mouseReports, ActiveMouseReport) && (!resending || UsbSemaphore.mouse.needsResending)) { - bool usbMouseButtonsChanged = mouseButtonsChanged(); - UsbSemaphore.mouse.inFlight = true; ret = Hid_SendMouseReport(ActiveMouseReport); if (ret != 0) { @@ -213,7 +204,6 @@ static void sendActiveReports(bool resending) { UsbReportUpdater_LastActivityTime = resending ? UsbReportUpdater_LastActivityTime : Timer_GetCurrentTime(); UsbReportUpdater_LastMouseActivityTime = resending ? UsbReportUpdater_LastMouseActivityTime : Timer_GetCurrentTime(); - usbReportsChangedByAction |= usbMouseButtonsChanged; usbReportsChangedByAnything = true; } diff --git a/right/src/usb_report_updater.c b/right/src/usb_report_updater.c index cd3572d21..aac761839 100644 --- a/right/src/usb_report_updater.c +++ b/right/src/usb_report_updater.c @@ -747,7 +747,7 @@ static void updateActionStates() { continue; } - preprocessKeyState(keyState); + preprocessKeyState(keyState, false); } } From 65bc11de5205bc1c8264e2a21d2182d8a039e4a3 Mon Sep 17 00:00:00 2001 From: Christian Dirksen Date: Sun, 26 Jul 2026 21:41:14 +0200 Subject: [PATCH 12/13] Key Chords: Reference manual --- doc-dev/reference-manual.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc-dev/reference-manual.md b/doc-dev/reference-manual.md index c9afd069a..cd76cb4eb 100644 --- a/doc-dev/reference-manual.md +++ b/doc-dev/reference-manual.md @@ -184,6 +184,10 @@ COMMAND = set oneShotTimeout