Skip to content
8 changes: 5 additions & 3 deletions doc-dev/reference-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ CONDITION = {ifShortcut | ifNotShortcut} [IFSHORTCUT_OPTIONS]* [KEYID]+
CONDITION = {ifGesture | ifNotGesture} [IFSHORTCUT_OPTIONS]* [KEYID]+
CONDITION = {ifPrimary | ifSecondary} [ simpleStrategy | advancedStrategy | ignoreTriggersFromSameHalf | acceptTriggersFromSameHalf ]
CONDITION = {ifHold | ifTap}
CONDITION = {ifDoubletap | ifNotDoubletap}
CONDITION = {ifDoubletap | ifNotDoubletap} [taps <number of taps (INT)>]
CONDITION = {ifInterrupted | ifNotInterrupted}
CONDITION = {ifReleased | ifNotReleased}
CONDITION = {ifKeyActive | ifNotKeyActive} KEYID
Expand Down Expand Up @@ -541,7 +541,7 @@ Conditions are checked before processing the rest of the command. If the conditi

- `if BOOL` allows switching based on a custom expression. E.g., `if ($keystrokeDelay > 10) ...`
- `else` condition is true if the previous command ended due to a failed condition.
- `ifDoubletap/ifNotDoubletap` is true if the macro was started at most 300ms after the start of another instance of the same macro.
- `ifDoubletap/ifNotDoubletap` is true if the macro was started at most 300ms after the start of another instance of the same macro. If `taps` is provided, test for that number of rapid taps in a row, for example `ifDoubletap taps 3` will test for a tripletap.
- `ifInterrupted/ifNotInterrupted` is true if a keystroke action or mouse action was triggered during macro runtime. Allows fake implementation of secondary roles. Also allows interruption of cycles.
- `ifReleased/ifNotReleased` is true if the key which activated current macro has been released. If the key has been physically released but the release has been postponed by another key, the conditien yields false. If the key has been physically released and the postponing mode was initiated by this macro (e.g., `postponeKeys ifReleased goTo ($currentAddress+2)`), it returns non-postponed release state (i.e., true if there's a matching release event in the postponing queue).
- `ifPending/ifNotPending <n>` is true if there is at least `n` postponed keys in the postponing queue. In context of postponing mechanism, this condition acts similar in place of ifInterrupted.
Expand Down Expand Up @@ -620,7 +620,7 @@ Key actions can be parametrized with macro arguments. These arguments can be exp
This allows the user to trigger chorded shortcuts in an arbitrary order (all at the "same" time). E.g., if `A+Ctrl` is pressed instead of `Ctrl+A`, the keyboard will still send `Ctrl+A` if the two key presses follow within the specified time.
- `set autoShiftDelay 0 | <time in ms (INT)>` If nonzero, the autoshift feature is turned on. This adds shift to a scancode when the key is held for at least `autoShiftDelay` ms. (E.g., tapping 'a' results in 'a', pressing 'a' for a little bit longer results in 'A'.)
- `set debounceDelay <time in ms, at most 250>` prevents key state from changing for some time after every state change. This is needed because contacts of mechanical switches can bounce after contact and therefore change state multiple times in span of a few milliseconds. Official firmware debounce time is 50 ms for both press and release. Recommended value is 10-50, default is 50.
- `set doubletapTimeout <time in ms, at most 65535>` controls doubletap timeouts for both layer switchers and for the `ifDoubletap` condition.
- `set doubletapTimeout <time in ms, at most 65535>` controls doubletap interval limit for layer switchers, secondary role doubletap to primary, and for the `ifDoubletap` condition.
- `set keystrokeDelay <time in ms, at most 65535>` allows slowing down keyboard output. This is handy for lousily written RDP clients and other software which just scans keys once a while and processes them in wrong order if multiple keys have been pressed inbetween. In more detail, this setting adds a delay whenever a basic usb report is sent. During this delay, key matrix is still scanned and keys are debounced, but instead of activating, the keys are added into a queue to be replayed later. Recommended value is 10 if you have issues with RDP missing modifier keys, 0 otherwise.
- `set autoRepeatDelay <time in ms, at most 65535>` and `set autoRepeatRate <time in ms, at most 65535>` allows you to set the initial delay (default: 500 ms) and the repeat delay (default: 50 ms) when using `autoRepeat`. When you run the command `autoRepeat <command>`, the `<command>` is first run without delay. Then, it will waits `autoRepeatDelay` amount of time before running `<command>` again. Then and thereafter, it will waits `autoRepeatRate` amount of time before repeating `<command>` again. This is consistent with typical OS keyrepeat feature.
- `set oneShotTimeout <time in ms, at most 65535>` sets the timeout for `oneShot` modifier. Zero means infinite.
Expand Down Expand Up @@ -782,6 +782,8 @@ Key actions can be parametrized with macro arguments. These arguments can be exp
- `$currentTime` returns current time in milliseconds in 31 bit range.
- `$queuedKeyId.<index (NUMBER)>` which stands for a zero-indexed position in the postponer queue.
- `$uhk.name` returns a reference to the uhk name string.
- `$previousKeyId` returns the id of the last key that was pressed previously to the one which started the macro
- `$previousKeyPressTime` returns the time activation time of the last key pressed previously to the one which started the macro
- `KEYMAPID` - is assumed to be 3 characters long abbreviation of a keymap.
- `MACROID` - macro slot identifier is either a number or a single ascii character (interpreted as a one-byte value). `$thisKeyId` can be used so that the same macro refers to different slots when assigned to different keys.
- `custom text` is an arbitrary text starting on the next non-space character and ending at the end of the text action. (Yes, this should be refactored in the future.)
Expand Down
63 changes: 33 additions & 30 deletions right/src/key_history.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,57 @@
#include "key_history.h"
#include "postponer.h"

typedef enum {
DoubletapState_Blocked,
DoubletapState_First,
DoubletapState_Multitap,
DoubletapState_Doubletap,
} doubletap_state_t;
#define HISTORY_SIZE 2
#define LAST (position % HISTORY_SIZE)
#define POS(p) ((position + HISTORY_SIZE - p) % HISTORY_SIZE) // Proceeding backwards in time

typedef struct {
const key_state_t *keyState;
uint8_t keyActivationId;
uint32_t timestamp;
doubletap_state_t doubletapState;
} previous_key_event_type_t;

static previous_key_event_type_t lastPress;
static key_press_event_t history[HISTORY_SIZE];
static uint8_t position = 0;

void KeyHistory_RecordPress(const key_state_t *keyState)
{
const key_press_event_t * const lastPress = &history[LAST];
const bool isMultitap =
keyState == lastPress.keyState
&& lastPress.doubletapState != DoubletapState_Blocked
&& CurrentPostponedTime < lastPress.timestamp + Cfg.DoubletapTimeout;
const bool isDoubletap = isMultitap &&
(lastPress.doubletapState == DoubletapState_First || lastPress.doubletapState == DoubletapState_Multitap);
keyState == lastPress->keyState
&& !lastPress->multiTapBreaker
&& CurrentPostponedTime < lastPress->timestamp + Cfg.DoubletapTimeout;

lastPress = (previous_key_event_type_t){
position = (position + 1) % HISTORY_SIZE;

history[LAST] = (key_press_event_t) {
.keyState = keyState,
.keyActivationId = keyState->activationId,
.timestamp = CurrentPostponedTime,
.doubletapState = isDoubletap ? DoubletapState_Doubletap :
isMultitap ? DoubletapState_Multitap :
DoubletapState_First,
.multiTapCount = 1 + (isMultitap ? lastPress->multiTapCount : 0),
.multiTapBreaker = false,
};
}

void KeyHistory_RecordRelease(const key_state_t *keyState)
{
if (keyState != lastPress.keyState) {
lastPress.doubletapState = DoubletapState_Blocked;
if (keyState != history[LAST].keyState) {
history[LAST].multiTapBreaker = true;
}
}

bool KeyHistory_WasLastDoubletap()
const key_press_event_t * KeyHistory_GetPreceedingPress(const key_state_t *keyState, uint8_t activationId)
{
return lastPress.doubletapState == DoubletapState_Doubletap;
for (uint8_t i = 0; i < HISTORY_SIZE - 1; ++i) {
if(history[POS(i)].keyState == keyState && history[POS(i)].keyActivationId == activationId) {
if (history[POS(++i)].keyState != NULL) {
return &history[POS(i)];
}
return NULL;
}
}
return NULL;
}

bool KeyHistory_WasLastMultitap()
{
return lastPress.doubletapState >= DoubletapState_Multitap;
uint8_t KeyHistory_GetMultitapCount(const key_state_t *keyState, uint8_t activationId) {
for (uint8_t i = 0; i < HISTORY_SIZE - 1; ++i) {
if(history[POS(i)].keyState == keyState && history[POS(i)].keyActivationId == activationId) {
return history[POS(i)].multiTapCount;
}
}
return 0;
}
16 changes: 14 additions & 2 deletions right/src/key_history.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,26 @@

// Typedefs:

typedef struct {
const key_state_t *keyState;
uint32_t timestamp;
uint8_t multiTapCount : 6;
uint8_t keyActivationId : 4;
bool multiTapBreaker : 1;
} key_press_event_t;

// Variables:

// Functions:

// Recording events
void KeyHistory_RecordPress(const key_state_t *keyState);
void KeyHistory_RecordRelease(const key_state_t *keyState);
bool KeyHistory_WasLastDoubletap();
bool KeyHistory_WasLastMultitap();

// Querying whole events
const key_press_event_t * KeyHistory_GetPreceedingPress(const key_state_t *keyState, uint8_t activationId);

// Querying specific info
uint8_t KeyHistory_GetMultitapCount(const key_state_t *keyState, uint8_t activationId);

#endif
2 changes: 1 addition & 1 deletion right/src/key_states.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
bool debouncing : 1;
secondary_role_state_t secondaryState : 2;
bool padding : 1; // This allows the KEY_INACTIVE() macro to not trigger false because of sequence
uint8_t activationId: 4;
uint8_t activationId : 4;
} key_state_t;

// Variables:
Expand Down
2 changes: 1 addition & 1 deletion right/src/layer_switcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ void LayerSwitcher_DoubleTapToggle(layer_id_t layer, key_state_t *keyState) {

if(KeyState_ActivatedNow(keyState)) {
LayerStack_LegacyPop(layer);
if (KeyHistory_WasLastDoubletap()) {
if (KeyHistory_GetMultitapCount(keyState, keyState->activationId) % 2 == 0) {
LayerStack_LegacyPush(layer);
doubleTapSwitchLayerKey = keyState;
doubleTapSwitchLayerTriggerTime = Timer_GetCurrentTime();
Expand Down
4 changes: 2 additions & 2 deletions right/src/macro_events.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,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();
Expand All @@ -67,7 +67,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);
}
}
}
Expand Down
18 changes: 14 additions & 4 deletions right/src/macros/commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -550,12 +550,22 @@ static macro_result_t processRecordMacroDelayCommand()
return MacroResult_Finished;
}

static bool processIfDoubletapCommand(bool negate)
static bool processIfDoubletapCommand(parser_context_t *ctx, bool negate)
{
uint8_t n = 2;
if (ConsumeToken(ctx, "taps")) {
n = Macros_ConsumeInt(ctx);
}

if (Macros_DryRun) {
return true;
}
return S->ms.isDoubletap != negate;

if (S->ms.currentMacroKey == NULL) {
return false;
}

return (S->ms.multitapCount % n == 0) != negate;
}

static bool processIfModifierCommand(bool negate, uint8_t modmask)
Expand Down Expand Up @@ -2093,9 +2103,9 @@ static macro_result_t processCommand(parser_context_t* ctx)
case CommandId_if:
PROCESS_CONDITION(processIfCommand(ctx))
case CommandId_ifDoubletap:
PROCESS_CONDITION(processIfDoubletapCommand(false))
PROCESS_CONDITION(processIfDoubletapCommand(ctx, false))
case CommandId_ifNotDoubletap:
PROCESS_CONDITION(processIfDoubletapCommand(true))
PROCESS_CONDITION(processIfDoubletapCommand(ctx, true))
case CommandId_ifInterrupted:
PROCESS_CONDITION(processIfInterruptedCommand(false))
case CommandId_ifNotInterrupted:
Expand Down
66 changes: 55 additions & 11 deletions right/src/macros/core.c
Original file line number Diff line number Diff line change
Expand Up @@ -419,10 +419,21 @@ macro_result_t Macros_ExecMacro(uint8_t macroIndex)
return MacroResult_JumpedForward;
}

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();
Expand All @@ -436,7 +447,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;
}

Expand All @@ -446,7 +457,8 @@ uint8_t initMacro(
uint16_t argumentOffset,
uint8_t keyActivationId,
uint8_t parentMacroSlot,
const char *inlineText
const char *inlineText,
const macro_state_t *parentMacroState
) {
if (!macroIsValid(index) || !findFreeStateSlot() || !findFreeScopeStateSlot()) {
return 255;
Expand All @@ -460,10 +472,30 @@ 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();

if(parentMacroState != NULL) {
S->ms.currentMacroStartTime = parentMacroState->ms.currentMacroStartTime;
S->ms.multitapCount = parentMacroState->ms.multitapCount;
S->ms.previousKeyPressTime = parentMacroState->ms.previousKeyPressTime;
S->ms.previousKeyId = parentMacroState->ms.previousKeyId;
S->ms.secondaryRoleState = parentMacroState->ms.secondaryRoleState;
}
else {
S->ms.currentMacroStartTime = CurrentPostponedTime;
S->ms.multitapCount = 0;
S->ms.previousKeyPressTime = 0;
S->ms.previousKeyId = 255;
if (keyState != NULL) {
S->ms.multitapCount = KeyHistory_GetMultitapCount(keyState, keyActivationId);
const key_press_event_t * const prevEvt = KeyHistory_GetPreceedingPress(keyState, keyActivationId);
if (prevEvt != NULL) {
S->ms.previousKeyId = Utils_KeyStateToKeyId(prevEvt->keyState);
S->ms.previousKeyPressTime = prevEvt->timestamp;
}
}
}

// If inline text is provided, set up the action before resetToAddressZero
if (inlineText != NULL) {
Expand All @@ -488,18 +520,19 @@ uint8_t initMacro(


//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;
Expand All @@ -523,14 +556,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);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the forward declaration, these had to move here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer forward declarations, since moving code wreaks havoc in git archeology sessions.

But this is an unimportant nitpick. Feel free to leave it as is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not be happy with having to maintain the parameter list in two places. If you are okay with that, I will put the functions back and do forward declaration.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am ok with that actually 😃

}

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, MacroIndex_None, 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, MacroIndex_None, NULL, NULL);

if (slotIndex == 255) {
S = NULL;
Expand Down Expand Up @@ -611,7 +655,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, MacroIndex_None, NULL, NULL);

if (slotIndex == 255) {
return slotIndex;
Expand Down
Loading
Loading