Firngrod/key chords - #1624
Firngrod/key chords#1624firngrod wants to merge 21 commits into
Conversation
Allows effect to last until the last key Lacks support for macro checks for key release Also removed an unused parameter
firngrod
left a comment
There was a problem hiding this comment.
Conversation starters
| - `set chord[.LAYERID_BASIC] ACTION` Configures a chord with an action. If a layer is specified, the chord will be available when that layer is active, if not, it will be available on any layer. The same chord can be defined for different actions for different layers, including the `any` layer. If action is `none`, the chord is removed and the keys will work as individual keys if the chord is pressed, not as an empty action. | ||
| - `set chordTimeout <ms, 0-255 (INT)>` sets the interval within which all keys of a chord must be pressed in order for the chord to be activated. | ||
| - `set chordPriorIdleTime <ms, 0-65535 (INT)>` sets a limit to how soon a chord can be activated after a key has been pressed. This helps prevent accidental activations while typing rapidly. | ||
| - `set chordLifetime { leadingKey | allKeys }` sets how long a chord activation lasts. With `leadingKey`, the chord is considered released when earliest key pressed in the activation is released, with `allKeys`, the chord is considered released when all keys in the chord is released. |
There was a problem hiding this comment.
The main differentiator is that with allKeys, the release is deterministically when all keys are released, not while with leadingKey, it can realistically be any of the keys. This can allow for, for example, overloading of a mod key.
| 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) { |
There was a problem hiding this comment.
Pretty sure I did this because I was getting misleading error messages.
| macro_history_t MacroHistory[MACRO_HISTORY_POOL_SIZE]; | ||
| uint8_t MacroHistoryPosition = 0; | ||
|
|
There was a problem hiding this comment.
Dead code since doubletap was moved
| return lastPress.multiTapCount > 1; | ||
| } | ||
|
|
||
| uint8_t KeyHistory_GetChordActivationIdOfLastAction() |
There was a problem hiding this comment.
The implementation here fetches activationId from the chord rather than having a cached value. This would of course have to change if we ever want to have an actual histrory.
| { | ||
| for (int i = 0; i < count; i++) { | ||
| consumeOneKeypress(suppress); | ||
| consumeOneKeypress(); |
There was a problem hiding this comment.
It was not even in the function definition any more.
| // 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)) { |
There was a problem hiding this comment.
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. Deciding on a base layer action was tricky in the cases where the held key was a chord. Also, pure layer holds, (not secondary keys) are not subject to the same disabling on map change.
| DEBUG_KEY_LIFE(applied); | ||
| KEY_TIMING(KeyTiming_RecordKeystroke(keyState, active, Timer_GetCurrentTime(), Timer_GetCurrentTime())); | ||
| keyState->current = active; | ||
| EventVector_Set(EventVector_NativeActionsPostponing); |
There was a problem hiding this comment.
As we discussed in the issue thread, only one key can now get activated per cycle, with the rest being pushed on the queue before actual key evaluation, in order to eliminate cases where one key evaluating for chords or secondary roles would not see when another was pressed within the same cycle.
kareltucek
left a comment
There was a problem hiding this comment.
Just a couple comments from a first, very shallow pass.
It will take a few more days till I get back for a deeper pass.
| COMMAND = set simulateLowResScrolling BOOL | ||
| COMMAND = set i2cBaudRate <baud rate, default 100000(INT)> | ||
| COMMAND = set diagonalSpeedCompensation BOOL | ||
| COMMAND = set chordingDelay <time in ms (INT)> |
There was a problem hiding this comment.
(Someone should probably mention somewhere that this is an independent concept.)
There was a problem hiding this comment.
Chords have not been released. It's not too late to name them something else, though chord is the most natural name. Unfortunate that key cluster kind of rules out that name for chords.
| 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. |
There was a problem hiding this comment.
Interesting. I bit cryptic. I kind of like it as long as it doesn't pollute global namespace.
There was a problem hiding this comment.
I just like that it's extremely obvious what means pressed and what means released.
| 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? |
There was a problem hiding this comment.
I am in favor for grabbing a bit in the keyState for isHoldingChord.
There was a problem hiding this comment.
Will do that then. That will make release key handling a bit cheaper if chords are enabled with maintain until all keys released.
| return noneVar(); | ||
| } | ||
|
|
||
| if (!Chords_TryAddChord(layerId, keys, keyCount, &chordAction)) { |
There was a problem hiding this comment.
This is fine.
I would probably formulate the error so that it is not a strict statement - like ending with a question mark instead of a period, to at least give myself a hint to check the path in case it behaves weird.
| // 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. |
There was a problem hiding this comment.
I don't understand the storing activationIds idea, but I suspect it would take time for lookups at other places.
There was a problem hiding this comment.
If active chords record the key activation id for each key which "holds it", release checks for the chord can just compare key activation ids, as per normal key release procedure. That would remove the need for tracking releases like this, but that would cost 5 bytes per chord slot. I like isHoldingChord, unless I find out that it's bad somehow.
| EventVector_Unset(EventVector_StateMatrix); | ||
| EventVector_Unset(EventVector_NativeActionsPostponing); | ||
|
|
||
| for (uint8_t slotId=0; slotId<SLOT_COUNT; slotId++) { |
There was a problem hiding this comment.
From what I remember, this loop is quite performance critical.
For that reason, I had been keeping it one loop without separating the preprocessing into a separate one.
There was a problem hiding this comment.
Well, we need a solution for when 2 keys are pressed within the same cycle:
a and b are a chord and pressed in one cycle. a has lowest id and runs first, starts chord resolution but does not see b as it hasn't been dealt with yet, starts postponing, waiting for more keys. b is then postponed, but nothing causes a to be handled again until timeout.
Another option could be to always schedule a run the next cycle if postponing has been enabled and a key postponed because of it within the same cycle, though that could get tricky with where secondary keys set postponing. Alternatively, always trigger a cycle if a key is put in postponer?
There was a problem hiding this comment.
Just always scheduling another cycle whenever a key is put on postponer might be the best/cleanest idea? It may cost a bit of processing, but only while there's downtime anyway.
| applyLayerHolds(keyState, actionBase); | ||
| // apply base-layer holds, but only if the key is not currenty applying another action | ||
| // Also, don't apply layers from keys currently undergoing chord evaluation | ||
| if (chordRes != ChordResolution_Wait && cachedAction->action.type == KeyActionType_None) { |
There was a problem hiding this comment.
This changes layer hold semantics. See right/src/layer_switcher.c scenario comments.
This is getting a bit convoluted, as I had probably introduced some artifacts too over time.
I should probably back off, write some tests and see where all the discrepancies that I am seeing come from.
There was a problem hiding this comment.
Only for keys which have an action associated to them on layers in which they are pressed.
This code change targets the case where key a was pressed on layer mod with action keypress leftArrow, but a has a layer switch to fn on base layer, so when mod is released, a now holds leftArrow and toggles fn. It' an edge case.
I get the need for empty actions to work like that, so key release/press order is not strictly enforced on layer dancing, but I feel that if the key is bound on the first layer, the user has already forefeited clean layer dancing.
I will change it to only skip the layer hold if chord is awaiting resolution if you want a more conservative approach.
There was a problem hiding this comment.
What I'm not too fond of is the way layer switches are handled so many places. Would it be possible to recheck pressed keys when switching layer to base and fake an activation for the first pressed with a cached none-action and base layer layer switch action, if any? Then this case of layer handling could be removed, and the one in ApplyKeyAction could be the main one? It's also complicated, in a different way. Also, secondary role layer switches are still different.
| // 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. |
There was a problem hiding this comment.
I could probably treat the keystrokes the same as the macros and activate them on the last key, but I wasn't entirely sure if it would cause issues to have the first keys hold the action without an ActivatedNow round and then having the last key do an AcitvatedNow. Probably it's fine, didn't take the chance. If it is fine, this logic can be simplified.
|
I'm still fretting over the complexity of |
|
Something else that hit me - How are we feeling about the key non-carrying chord keys being registered as inactive in macros if they were pressed as part of a chord? For the |
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.