keyd: [RFC] Proposal: uninterruptible overload timeouts (overload2)

Genesis: #278, #31, and #84.

Presently the only support for time based disambiguation of tap/hold is in the form of the timeout action, which allows the user to specify a time that the bound key must be depressed before being interpreted as being held. Crucially the key is unambiguously interpreted as a tap if an intervening keystroke occurs before the timeout has expired.

While this is useful for overloading keys which are commonly struck in isolation (e.g space), it does not account for keys struck in quick succession, which must allow for the possibility of interleaved events.

Specifically, an attempt to type <a> <b> (tapping behaviour), can produce any of:

1. <a down> <b down> <b up> <a up>
2. <a down> <b down> <a up> <b up>

3. <a down> <a up> <b down> <b up> 

while the attempt to type <a>+<b> (a ‘hold’), can produce:

<a down> <b down> <b up> <a up> (1)
<a down> <b down> <a up> <b up> (2)

Proposal:

To distinguish between the two possible interpretations, we introduce a new variant of overload: overload2(<layer>, <action>, <timeout>) (called timeout3 in #278)

The binding is resolved immediately after either:

a. The timeout expires (<layer>) or b. The bound key is released (<action>)

This differs from timeout in that intervening key strokes are ignored (achieved by queueing them). This comes at the cost of additional visual latency, but potentially allows for commonly struck keys, to serve a dual purpose.

The proposed behaviour appears to be equivalent to QMK’s default tap/hold feature.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 20 (17 by maintainers)

Commits related to this issue

Most upvoted comments

is that the creator of keyd has very high standards and can’t easily be tricked into introducing feature bloat or overly complex syntax.

Flattery will get you nowhere 😉.

It was not my intent to flatter you, and as the above conclusion will be drawn by anybody following this (and related) threads, I think that it can be characterized as being objective. Wait - this now sounds like flattery again, damn…

I think that all your doubts and criticisms about the use cases are due to a misunderstanding (which could have been prevented by more clarity from my side). I was always assuming that the user makes no pause at all but instead continues typing while holding a key. My reasoning was that when one has a typing style which allows to use overload2 to overload a letter key with a modifier, then this very ability can be used to overload a letter key with anything else as well (such as inserting a space before shifting).

To get the timing right, one would simply have to think about modifying the next key with the current one. Detailed with the dot example: When trying to enter “. B” (as occuring in “end. Beginning”, then instead of thinking to type “dot space S-b”, one instead thinks of “dot-b”, modifying the b with a dot and thus holding the dot key a bit longer. In the same way, “3.6” would not result in a mistyping as long as one thinks about “3 dot 6” when entering (and not “3 dot-6”). I realize that some people consider this as some obscure vodoo magic with unstable results, but apparently there are (a few?) living counterexamples, myself included.

Proposal to move forward: QMK should already support this, so I will thoroughly test the described use cases (including auto-shift) on a QMK keyboard and then report back here.

As a side note (you will probably call me crazy): I’m playing with the idea of keeping overload2 for homerow modifiers and in addition put combos on the homerow for quick access to additional layers.

Thanks a lot, I will test later and also setup IRC (never used it before).

@herrsimon

I haven’t had time to read your latest post in full yet, but I’ve gone ahead and added overload2 and overload3 in their proposed form to the latest commit (see the man page). Having some concrete primitives to play with might help inform your opinions about their utility.

These are not yet part of a point release and are still subject to change, so I am open to persuasion on both the naming and whether or not the first argument should be an action.

I would also like to invite both you and @slakkenhuis to idle on the IRC channel (mentioned in the readme) since a lot of these conversations seem to involve lengthy exchanges that might benefit from reduced round trip times.

<a down> <b down> <200ms> the last term was supposed to mean that 200ms have passed since the beginning of the sequence (since )

<200ms> is relative to the last event (<b down>), not to the beginning of the sequence. So <a down> <b down> <1000ms> corresponds to <a down> immediately followed by <b down> and then a 1 second pause. My remarks in #278 make use of this to illustrate why the location of the pause (potentially) matters.

This explains a lot of misunderstandings! I will use this meaning from now on (my recent feature requeste #310 still uses the alternative interpretation, but I have added an explicit explanation). For the issue at hand, it would be useful to also have some notation for absolute time increments (relative to the beginning of the sequence), maybe <<200ms>>?

It should read:

The binding is resolved immediately after either: a. The timeout expires (<layer>) or b. The bound key is released (<action>)

Perfect!

I’m having trouble keeping track.

That makes two of us 😛

My apologies again, I will try to focus on overload2 here (case 1).

I think the problem we are really contending with here is naming. From what I can tell all of the outlined cases with the exception of 1 (which the proposed action is intended to address) and 4 (which needs to be justified) are already supported.

Yes, so am I correct in stating that we all agree for the need of case 1? I will open a separate issue regarding case 4.

Could you clarify how this is meant? Should only be executed if the timeout expires or the bound key is released, whatever happens first?

Correct.

I think there is still some misunderstanding as with the above logic <action 2> would never be executed. Shouldn’t the correct logic be that in the statement overload2(<action 1>, <action 2>, <timeout>) the first action is executed when the key is held for at least <timeout> ms and the second action otherwise, with the first action firing as soon as <timeout> ms have elapsed (without waiting for key release)?

QMK’s default behaviour for mod taps (like SFT_T) is different from other dual role keys (like LT). For mod taps, an interrupting key press before the timeout by default applies the modifier (selects when transferred to overload2)

If my understanding is correct, I believe this is what QMK calls ‘permissive hold’. It is roughly equivalent to the existing timeout action when used in conjunction with overload. When I said the proposed overload2 appears to be equivalent to the default QMK tap_hold functionality, I was referring specifically to this.

What you describe is indeed the default behaviour but only for non-mod tap keys. The latter play a special role and by default apply the modifier when interrupted (this is described in the IGNORE_MOD_TAP_INTERRUPT section further down). This distinction is rather confusing and probably an artifact of the incremental development process. Anyway, this discussion is not relevant here.

As an aside, your notation is a bit ambiguous. I have thus far assumed it is roughly equivalent to:

<a down> <x_1 ms> <b down> <x_2 ms> <b up> <x_3 ms> <a up>

for x_1 + x_2 + x_3 >= timeout ms

but it might be better if you follow my notation (using concrete in-line time intervals as events), since makes it easier to identify precisely which interval(s) are being discussed.

I tried to follow your notation. So far, when I wrote something like

<a down> <b down> <200ms>

the last term was supposed to mean that 200ms have passed since the beginning of the sequence (since <a down>). This is your notation, correct? If not, I will adapt.

Returning to the discussion, I still see advantages when adding a fourth argument to overload2, specifying the interrupt handling logic

It’s not clear that this offers any additional expressive power, or is markedly different from just having different actions (overload, overload2, overload3, etc). All of these behaviours are different from one another and still require dedicated code paths.

Another good argument for different actions is consistency, as keyd is already using this style for everything else.

Actually I am starting to think overload2(<layer>, <action>, <timeout>) makes more sense than overload2(<action1>, <action2>, <timeout>), since there isn’t really another hold action that makes sense in this context.

I agree to this.

Unlike timeout and the proposed timeout2 (which are general purpose), overload2 is only really useful for key overloading. Having said that, it might make sense to add some syntactic sugar for the common cases of timeout(<key>, <timeout>, layer(<layer>)) and timeout(overload(<layer>, <key>), <timeout>, layer(<layer>)) in the form of overload3 and overload4 (or something similar), which begins to approximate your ‘interrupt argument’ syntax.

This is a good idea. In general, I’ve been thinking for quite some time about introducing some dedicated C-style configuration file preprocessing step (to which the alias processing could be offloaded as well), so that repeated but syntactically similar declarations can be written in a more concise and understandable way, but this is a completely separate issue. I will open a new thread once these thoughts have matured.

In any case, the most important thing in my opinion is to add proper documentation in a dedicated section regarding dual role keys,

+1. There is a general paucity of documentation.

I’m happy to volunteer for this if desired.

This would be greatly appreciated, though I’m not sure how I want to split up the documentation yet (I will probably add a dedicated FAQ section).

If it is externalized, the manual should contain a reference. Just let me know what you prefer once everything is implemented.

Wait, now I feel we’re discussing multiple things again. I realise that it makes some sense to discuss them together because a design decision in one will affect the others, but I’m having trouble keeping track. Case 1 would be for home row modifiers, I can see that, and uninterruptable timeouts would address it. Case 4 would also address case 1, but with the added benefit of resolving the ambiguity on the second key’s keyup, not just on timeout. Am I correct in suggesting that having case 4 available would make case 1 obsolete for you? (I must say that case 4 makes me want to experiment with these timeouts again.) And also in saying that your interrupt handling 1-4 is completely independent from your earlier suggestion of interrupt handling 0-4?

I want to spend some time reading the buildup to this properly (and replying with some semblance of brevity).

You mentioned:

I still think we are discussing too many things at once. I’ve created a dedicated issue for timeout3 (https://github.com/rvaiya/keyd/issues/309) and will shortly create one for timout2.

timeout2 is supposed to be purely in #277 and timeout3 is supposed to be purely in #278, though, right? Just making sure I’m not missing anything that separates this #309 from #278 apart from the more concrete problem statement. EDIT: Oh, do you mean that this issue strictly deals with uninterruptable timeouts, whereas #278 additionally floated the idea of selecting interrupting behaviour, i.e. choosing the first or second function on interrupt?