Skip to content

Key Chords - #1606

Open
firngrod wants to merge 13 commits into
UltimateHackingKeyboard:masterfrom
firngrod:firngrod/key_chords
Open

Key Chords#1606
firngrod wants to merge 13 commits into
UltimateHackingKeyboard:masterfrom
firngrod:firngrod/key_chords

Conversation

@firngrod

@firngrod firngrod commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Added key chords as a macro accessible feature.

I have left a self-review with some additional context which may not be readily available from the code, along with some questions about implementation strategy preferences.

There are also a lot of openers for discussions in the code comments, so don't skip those, by the way.

@firngrod firngrod left a comment

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.

A couple of comments to start off the discussion.

Comment on lines +1308 to +1313
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) {

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.

Pretty sure I did this because I was getting misleading error messages.

Comment thread right/src/macros/core.c
Comment on lines +423 to +433
//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
);

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.

The same refactoring from #1507 because I need more initial state carry-over.

Comment thread right/src/macros/core.c
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.

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'm not a big fan of pulling the fact of chord activation out of the ether like this, transferring it indirectly through history. The same thing is done for doubletap though.
Avoiding it would probably require the previously discussed key event type in usb_report_updater.c.

}

if (!Chords_TryAddChord(layerId, keys, keyCount, &chordAction)) {
Macros_ReportErrorPos(ctx, "Could not add chords, no more free chord slots.");

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.

Presumptions of error cause from a bool. So far it's the only way it can fail, but maybe I should just do enum error codes to be proper?

@mhantsch mhantsch Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Either return bool and an additional &failReason parameter; or an enum as you suggest (0=OK, others are error codes).

Couldn't it also fail if the same chord (same keys) has already been registered?

.Macros_OneShotTimeout = 500,
.AutoShiftDelay = 0,
.ChordingDelay = 0,
.Chords_Timeout = 75,

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 use 30ms, @mhantsch mentioned using 100ms. Put it somewhere in the middle.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sounds reasonable as a default. Is it configurable somewhere?

Comment thread right/src/postponer.c
{
for (int i = 0; i < count; i++) {
consumeOneKeypress(suppress);
consumeOneKeypress();

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.

It was not even in the function definition anymore.

Comment thread right/src/usb_report_sender.c Outdated
}
}

static bool mouseButtonsChanged(void) {

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.

Warnings getting treated as errors. As far as I can tell, these value actually were unused.

Comment thread right/src/usb_report_updater.c Outdated
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) {

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.

The uncached action was only used for this purpose: To disable layer hold for secondary keys on keymap switch if the key was not mapped to the same layer on the new map. I cannot see any other reasonable case for this code. There is the unreasonable case of remapping the key from a macro while it's held.
I decided to remove that to get rid of passing the base action here because the base action has very little relation to the cached action when a chord is pressed. Also, pure layer holds, (not secondary keys) are not subject to the same disabling on map change.

} else {
KEY_TIMING(KeyTiming_RecordKeystroke(keyState, active, Timer_GetCurrentTime(), Timer_GetCurrentTime()));
keyState->current = active;
EventVector_Set(EventVector_NativeActionsPostponing);

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.

As discussed, keys can now only get activated one per cycle. Any more and the rest are postponed to future cycles.

Comment thread right/src/chords.h
Comment on lines +11 to +12
ChordApplicationType_LeadingKey,
ChordApplicationType_AllKeys,

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.

Technically, the leading keys version has no benefit that I can see over the other except for being cheaper to perform. Is that worth the complexity of the choice? Maybe there should only be the all keys version?

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.

On the other hand, the macro support for release detection for all-keys lifetime was pretty complex, basically the entirety of this commit, probably a fifth of the entire PR, so maybe that should be the one to go. Trouble is that I can actually conceive of real-world usecases for it, especially on the 60 where thumb keys are less abundant than on the 80. With chords, the mod button can be overloaded to be various layers depending on which key(s) it was chorded with on press, and the other key can be released while modkey is held to maintain the layer. Not really useful for me with my "No mapped combination keys" philosophy, but probably for someone else.

@firngrod
firngrod marked this pull request as ready for review July 28, 2026 06:48
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