Chorded Escape (`q`+`w`) - example configuration & feature brainstorming

I made an attempt to configure a chorded Escape. (Chord meaning several keys pressed together. In UHK smart macros, this is expressed with ifShortcut and ifNotShortcut commands.)

In my case, pressing q+w together results in Esc.

I created this macro, chordscape:

ifKeyActive $keyId.q {
    ifShortcut timeoutIn 100 $keyId.w final holdKey escape
    else final holdKey q
}
ifKeyActive $keyId.w {
    ifShortcut timeoutIn 100 $keyId.q final holdKey escape
    else final holdKey w
}

Note the use of holdKey instead of tapKey, so autorepeat will still work for q, w and also, for the chorded Escape.

I then assigned it to the q and w keys:
image

You could have separate macros for the q and for the w key, but I would like to maintain just a single place for this functionality. And I don’t want to blow up the amount of macros needed for one function.

My wishes for the UHK firmware:

I really think chords should be easier to configure. It is weird that you have to map them to each of the possible starting keys, and that I had to make two separate parts in the macro, one for each starting key. Chords should be specified independent of the order of press, and especially independent of the starting key.

The Kanata software solves this by having a separate section for chords. (It is labelled ‘-experimental’ because it is fairly new, but already works well.) This section will be processed before any layer processing:

(defcfg chords-v2-min-idle-experimental 50)
(defchordsv2-experimental
  (q w) esc 100 all-released (maxtend)
)

(q w) is the list of input keys that make up the chord.
esc is the action taken when the chord triggers.
100 is the maximum wait time in ms for the input keys to be pressed together.

This makes it much easier to define the chords you want, and what actions they should have. It does not matter in which order you press them: as long as the keys are pressed down within the interval defined (100 ms in the above example), and only after a minimum idle time has elapsed with no keys depressed (to prevent accidential activation of chords during rapid typing, 50 ms in the above example), then the corresponding action is activated.

Finally, you can also define exclusion layers (maxtend in the above example) where the chord will never trigger.

Here is an idea how this could work for the UHK:

First, imagine in Agent you can click a checkbox on each key on the base layer

This key can trigger a chord.

I would just click this checkbox for the q and w keys. The rest of primary / secondary configuration would stay the same. Agent would still show the keys as q and w, maybe with a small symbol indicating that these keys can also trigger chords.

In a primitive implementation, whenever a key is pressed that has the “chord trigger” checked, the firmware would execute a specific macro ($onChordTrigger) that can do all sorts of ifShortcut checks. That processing would be made simpler with two new commands checkChords and ifChord, iterating over potential chords, and executing matching actions. Finally, a third new command, continueProcessingLayers would continue with regular layer mapping and actions if none of the chords mapped (or that is done automatically on return of the $onChordTrigger macro).

$onChordTrigger:

checkChords timeoutIn 100 {
  ifChord $keyId.q $keyId.w final holdKey escape
  ifChord $keyId.graveAccentAndTilde $keyId.1 final exec tripleGraveComment
}
continueProcessingLayers

checkChords works as a timed loop around the ifChord statements. ifChord checks for activation of all the keys and executes the corresponding statement if all the keys have been activated. Finally, continueProcessingLayers runs normal actions for the keys depressed during the checkChords loop.

Note: I don’t fully understand the postponer in the current firmware, so maybe this is already scriptable in macros today. But I didn’t see it. (@kareltucek ?)

Simpler and easier chord configuration:

In a better implementation, you would not have to write that $onChordTrigger macro. In Agent, there would be a section “Chords” (for each keymap?) where you can list all the keys that together make a chord. (Only keys that had the trigger box checked can be used here.) You would see a list of chords, and [Add], [Configure] and [Remove] buttons.

chord action
q w chordscape [Configure] [Remove]
[Add another chord]

Each chord definition lists all the keys for that chord, and a macro to be executed when the chord triggers. Or, instead of always referring to a macro, it would offer the same actions as for any key:

image

Global parameters for chords that would be configured are:

  • minimum chord idle time before chords can be active, suggested default: 50 ms
  • maximum chord wait time until normal key processing continues, suggested default: 100 ms

Keys that are marked as chord trigger keys will see a delay of up to maximum chord wait time before they trigger a press (i.e. like the timeoutIn parameter of ifShortcut). If such a key gets pressed and released again before that maximum chord wait time, then it’s tap will activate immediately. As a result, you will not experience any visible delay during normal typing (short taps).

Now you don’t need the “this key can trigger a chord” checkbox anymore. It can be calculated from the list of keys that were added to the chord list. In this variant, you would just add your chords to the chord list, and Agent would then show the chord trigger symbol on the relevant keys of the base layer. (I guess only the base layer…?)

The firmware would be extended to automatically run those chord checks before regular key handling.

A further enhancement (optional) would allow configuration of the layers on which a chord is active:

chord action layers …
q w chordscape base, fn, mod [Configure] [Remove]
[Add another chord]

In this variant, chords would only be recognised if one of the defined layers is active. This would even allow different actions for the same chord on different layers. Probably overkill, but comes out of this implementation almost automatically.

2 Likes

Well.

onChordTrigger is an interesting idea, to which I have a few objections:

  • macros are still parsed very naively and the above code means reparsing the entire macro (possible hundreds of lines) on every key press. I am not willing to accept this for performance reasons.
  • it still needs Agent gui support, doesn’t it? (The “this key can trigger a chord” checkbox.)
  • it sounds somewhat complicated

Note: I don’t fully understand the postponer in the current firmware, so maybe this is already scriptable in macros today. But I didn’t see it. (@kareltucek ?)

I don’t think we have macro commands for such a generic handling.

In a better implementation

If we want this feature, we should go with the “better” implementation - i.e., having a new, dedicated lookup table that would contain the list of chords and actions. This could be interfaced either via gui, or (maybe for a proof of concept implementation) just registered via macros from $onInit. Something like registerChord KEYID+ ACTION (e.g., registerChord $keyId.q $keyId.w keystroke escape).


@mlac should we pursue this?

If so, then we need to devise some reasonable way to implement it. I.e.: How should the lookup table look? How much space should we reserve? Can we figure a representation that doesn’t need to reserve a huge amount of RAM?

Sounds good, I think that makes a lot of sense. I guess you would manage the lookup table as a trie or Patricia trie. The firmware would have to build that from the registerChord commands.

I don’t have the bandwidth to look into this nowadays. But after things settle down, I want to compile a list of often-used macro templates and a specification. Then, we should refine the list and brainstorm about the required macro engine core features, probably including this one.