Skip to content

Conversation

@firngrod
Copy link
Contributor

@firngrod firngrod commented Jan 15, 2026

I added a new feature to do timeout passively, as I mentioned on the forum, although I had not fully realized yet that it could be implemented this simply. Simply put, instead of applying the timeout action when timeout is reached, it will apply the timeout action once the key is released. If timeout action is primary or secondary, that will be a tap of the key, if timeout action is none, it will do nothing, The big difference here is that the key can be held for an arbitrarily long amount of time and still be used in a combo.
The main motivator is that I often press a secondary key to do a combo, then realize that it's on the same half. Now I can just wait for half a second and release without my screen drowning in menus and without having to time my combos tightly.
It also allows the timeout to be effectively disabled by setting passive timeout to primary.
One downside is that now we have some setting combinations which make little sense in practice. One that I can think of is passive timeout to secondary layer switch. It would effectively be identical to passive timeout to none. Maybe that could be added as a feature if someone wants it.

Also rewrote the main resolver to a fail fast style which removes a lot of ambiguity of where options apply.
EDIT: Oops, I used a word I didn't understand. It's not Fail Fast, it's just that I try to quit the function using as little info as possible. What do I know of programming terms, I never went to programming school.

This PR fixes everything mentioned in #1421

COMMAND = set secondaryRole.defaultStrategy { simple | advanced }
COMMAND = set secondaryRole.advanced.timeout <ms, 0-500 (INT)>
COMMAND = set secondaryRole.advanced.timeoutAction { primary | secondary | none }
COMMAND = set secondaryRole.advanced.timeoutType { active | passive } <defines whether the secondary role should perform it's timeout action when the timeout is reached or when the key is released after having been pressed beyond timeout without triggering secondary>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Naming is obviously up for debate. I see passive as the timeout passively change the primary action while active actively executes the timeout action. It is entirely reasonable to see it as passive as timeout executes passively while you do nothing and active is that the timeout executes on the activity of release. Probably different wording is in order.

Comment on lines +482 to +483
set secondaryRole.advanced.timeoutType passive
set secondaryRole.advanced.timeoutAction none
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are my settings, don't know if they should be recommended. The idea has been described on the Forum. Basically, mods only activate in combinations with a non-mod, otherwise not. If I need to activate a mod (combo) on it's own, I will activate it with a secondary key on the other half, but hold that key long enough to get a none timeout action. With active secondary timeout, I would definitely recommend these settings still.

```
set secondaryRole.defaultStrategy advanced
set secondaryRole.advanced.timeout 500
set secondaryRole.advanced.timeout 200
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The text below states 200 ms. On second thought, I should update the text instead. 200 ms timeout is aggressive.


void PostponerQuery_FindFirstPressed(postponer_buffer_record_type_t** press, postponer_buffer_record_type_t** release,
key_state_t *opposingKey)
void PostponerQuery_FindFirstPressed(const postponer_buffer_record_type_t** press, const key_state_t *opposingKey)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Release doesn't matter for the trigger on press and vice versa. We only actually need the time of the event to enforce safety margin. I don't know if we should reduce the function to just return a timestamp?

if ((*press)->event.type == PostponerEventType_PressKey
&& (*press)->event.key.keyState == (*release)->event.key.keyState ) {
const postponer_buffer_record_type_t * const press = &buffer[POS(j)];
if (press->event.type == PostponerEventType_PressKey
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here we still need to find the press to ensure that it's there.

static bool currentlyResolving = false;


static key_state_t *previousResolutionKey;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These values were only used for doubletap, so save some RAM and cycles there

}


#define RESOLVED(resolution) KEY_TIMING(KeyTiming_RecordComment(resolutionKey, resolution, __LINE__)) \
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not quite as readable output as before, but a lot easier to use these macros.

#define AWAITEVENT(timeout) sleepTimeoutStrategy(timeout); \
return SecondaryRoleState_DontKnowYet;

static secondary_role_state_t resolveCurrentKeyRoleIfDontKnowTimeout()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't try to compare the new lines to the removed ones. The entire thing is rewritten.

bool actionKeyWasPressedFirst = actionPress != NULL && dualRoleRelease != NULL && actionPress->time <= dualRoleRelease->time - Cfg.SecondaryRoles_AdvancedStrategySafetyMargin;
// see if we have an activating key event
const postponer_buffer_record_type_t *actionEvent = NULL;
const key_state_t * const opposingKey = acceptTriggersFromSameHalf ? NULL : resolutionKey;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Heavy const syntax

Comment on lines +191 to 193
if (actionEvent != NULL) {
RESOLVED(SecondaryRoleState_Secondary);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was so anticlimactic to write once I got here.

}

// otherwise, keep postponing until key action
return SecondaryRoleState_DontKnowYet;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The user can fill the queue while holding indefinitely. They will loose keystrokes. I'm okay with that.

@firngrod firngrod marked this pull request as ready for review January 16, 2026 17:37
@kareltucek
Copy link
Collaborator

kareltucek commented Jan 17, 2026

Btw., a bit of commit message philosophy, since I am doing git archeology right now: it would be nice if I could tell straight away what part of the codebase a commit belongs to just from the message. In the case of your PRs, it might mean just prefixing messages with a short codeword like "hrm". E.g., "hrm: Embarrassing".

This is just a general feedback. No need to rebase anything ;-).

@kareltucek
Copy link
Collaborator

(I am guilty too though: "pick c6198af # Reword some code.".)

@firngrod
Copy link
Contributor Author

Makes sense. Where I work, we do the related story prefix, so I always do a double take anyway while I try to remember what the story number was. Might as well harness that. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants