This document is a record of designing/braindumping for the improvement to the sequences feature to add chord support. It is left in an informal and disorganized state — as opposed to a being presentable design doc — out of laziness. Apologies ahead of time if you read this and it's hard to follow, feel free to contribute a PR to create a new and more polished doc.
The desire is to be able to add output chords to a sequence.
The effect of this is that: (S-(a b))
can be differentiated from (S-a b)
.
Today, chords are not supported at all.
The two sequences above could be written as (lsft a b)
;
however, the code today has no way to decide the difference
between lsft
being applied to only a
or to both a
and b
.
The feature will codenamed "seqchords" for brevity in this document.
Today, the sequence (lsft a b c)
doesn't care when the lsft
,
or even a
or b
are released relative to when the subsequent keys
are pressed. However, with seqchords, the code could be potentially changed to
make sequences release-aware.
It seems a little difficult to integrate
this into the trie structure used to track sequences though.
With an implementation that is release-aware,
it seems like the code would need to figure out how to conditionally
add release events to the trie, depending if the seq was
(lsft a)
or (S-a)
For now, I think a different approach would be better.
The current sequence type is Vec<u16>
since keys don't fit into u8
.
However, there are fewer than 1024 (2^10) keys total.
That means there are 6 bits to play with.
6 bits are enough for the types of modifiers (of which there are 4),
but differentiating both sides (increases to 8).
Perhaps one only cares to use both left and right shifts though, and maybe
both left and right alts.
One could also use a u32
instead, but that seems unnecessary for now.
I see no backwards compatibility issues if one
desired that change in the future.
With this in mind, while modifiers are held, set the upper unused bits of
the stored u16
values.
This does mean that (lsft a b)
behaves differently
with vs. without seqchords.
Unless maybe the code automatically generates the various permutations
of this type of sequence, but that seems complicated.
Or maybe have a u16
with a special bit pattern that could be used
to differentiate between (S-(a b))
and (lsft a b)
.
For now, let's say that the bit pattern is 0xFFFF
.
If a modifier is pressed and the the sequence [..., <mod>, 0xFFFF]
exists in the trie: continue processing the sequence in mod-aware mode.
OR for simplicity, just say "screw backwards compatibility" and force users to be clear about what they mean and define the extra permutations, if they want them. I prefer this.
Let's begin the description of the new data format. Since shifted keys seem like they will be the main use case for seqchords, only that will be described in this document for now. Here are the numerical key values relevant to the examples.
a: 0x001E
b: 0x0030
.lsft: 0x002A
.
This differs by OS, but that's not important.
The transformation of (lsft a b)
to a sequence in the trie today
looks like:
[0x002A, 0x001E, 0x0030]
This will remain unchanged with seqchords.
Let's say that chorded keys using lsft
will have the otherwise-unused MSB (bit 15) set.
The transformation of some sequences using chords will be:
(S-(a b)) => [0x802A, 0x801E, 0x8030]
(S-a b) => [0x802A, 0x801E, 0x0030]
(S-a S-b) => [0x802A, 0x801E, 0x802A, 0x8030]
Notably, lsft
is modifying its own upper bits.
This should simplify the implementation logic
so that the code does not need to add a special-case check
that the newly-pressed key is itself a modifier.
One may need to define different sequences if one wishes to use both
left and right shifts to be able to trigger these shifted sequences.
The syntax does not exist today, but maybe (S-(a b))
and (RS-(a b))
as an example for left and right shifts.
The reason different sequences would be required is because the
sequence->trie check operates on the integers that correspond to the keycodes.
Consideration: maybe there could be transformations for the right modifier keys to ensure they get translated to the left modifier keys. This seems like it could be a sensible default to start with. If a change is desired in the future to not do this transformation, it doesn't seem too difficult to add a configuration item to do so. For now that will be left out, deferring to the YAGNI principle.
Thinking back on the topic of backwards compatibility, I'm scrapping that idea of special bit patterns. I thought of a probably-better way: backtracking with modifier cancellation.
By default when seqchords gets added,
the modified bit patterns will be used
to check in the trie for valid sequences.
However, with a defcfg
item sequence-backtrack-modcancel
— which should be yes
by default for back-compat reasons —
if the code encounters an invalid sequence with the modded bit pattern,
it will try again with the unmodded bit pattern, and only if that does not
match will sequence-mode end with an invalid termination.
This backtracking can be turned off if desired,
e.g. if it behaves badly in some future seqchords use cases.