rvaiya / keyd

A key remapping daemon for linux.
MIT License
2.72k stars 157 forks source link

Feature request: Fine tuning for overload #278

Closed herrsimon closed 2 years ago

herrsimon commented 2 years ago

I started experimenting with home row mods again, this time on QMK using its fine tuning options and was finally able to get a usable configuration.

The crux for me (as I type rather fast) is to only enable the modifier behaviour if the overloaded key has been held for a certain amount of time (independently of other pressed keys). Unfortunately, I don't think that this is currently possible on keyd using a combination of overload() and timeout() (please correct me if I'm wrong).

To explain better what I mean, let's assume that the f key should output an f if tapped and act as shift if held while another key is pressed.

The event sequence when I want to input J is then

1. f down
2. j down
3. f up
4. j up

which is problematic as those precise events are regularly generated as well upon entering fj.

To disambiguate, one would need to examine how long the f key has been held down and then apply the mod if some threshold has been exceeded.

I thought about an overload2(layer, action, tap_time) action, which only shifts the layer if the key is held down for at least tap_time ms (independently of whether other keys are pressed before the key is released).

An implementation of this could be to waits for tap_time ms upon key press, buffering all occuring key events during that time. If at any point a keyup event of the overloaded key is seen, execute action and then process the buffered key events on the current layer. If tap_time expired without such a keyup event, process the buffered (and all other incoming) key events on layer until the keyup event occurs.

For the same reasons as in my other recent feature request, overload2 might not be the best name for this and a more user friendly way would simply be to allow tap_time as an optional argument (otherwise applying a default which the user could modify in the [globals] section).

rvaiya commented 2 years ago

The crux for me (as I type rather fast) is to only enable the modifier behaviour if the overloaded key has been held for a certain amount of time (independently of other pressed keys). [...] To disambiguate, one would need to examine how long the f key has been held down and then apply the mod if some threshold has been exceeded.

I suspect I have misunderstood something. timeout(f, 300, layer(shift)) should do precisely that, no? This was timeout's motivating application (a thread to which I believe you were party)

The above should produce J given the sequence:

f down
<x ms>
j down
f up
j up

and fj given

f down
<y ms>
j down
f up
j up

for all x > 300ms and all y <= 300ms

herrsimon commented 2 years ago

You are of course perfectly right, sorry for spamming the issue tracker and thanks for the extensive answer! I apparently applied some sort of tunnel vision regarding timeout, as I have exclusively used it in combination with overload in order to “cancel” overload's tapping macro after some time, i.e. in the form

rightshift = timeout(overload(shift, S-0),300,layer(shift))
rvaiya commented 2 years ago

If you find it confusing, then I definitely need to document it better. I've been meaning to add some recipes for common tasks.

herrsimon commented 2 years ago

It's not confusing at all, I should simply take more time reading things properly. Actually I also read your solution too fast and didn't try it out, because after just doing so I noticed that it actually doesn't solve the problem (this was also reassuring for me, as the malfunctioning of my brain is apparently not as bad as I thought...).

So what I was asking for is that the switching takes place based on the time f is held down, independently of the press of j. As I'm not sure if it's clear, let me rephrase: If f is held down for a certain amount of time, all keys pressed between the push and release of f should have a modifier applied, otherwise, another action should take place. The only difference to the current timeout thus is that another keypress should not interrupt the timer. Of course, as all later occuring keypresses have to be buffered for the given timeout, this would create a noticeable delay for the user.

This would yet be another type of timeout command, now on to finding a suitable name!

herrsimon commented 2 years ago

A solution regarding the naming (see #277) would be to introduce timeout-hold and timeout-hold-interrupt (the latter being the current timeout), or just timeout-hold with an added boolean argument which chooses if interruption on other key press takes place or not.

rvaiya commented 2 years ago

The only difference to the current timeout thus is that another keypress should not interrupt the timer.

Can you provide an example of when this would be useful? It seems odd not to resolve the timeout on the next keypress, as it allows for pathological cases like this:

f down j down j up <300ms> -> C-j

herrsimon commented 2 years ago

The use case is still the one I was describing, namely home row modifiers, and your pathological use case is in fact exactly what I would expect (see below).

First let me provide more details for the use case (let's take fj as above, where f should double function as shift when held down longer than a certain threshold). When comparing the two intended inputs fj and S-j (i.e. J) of a fast typing user, they both yield the same input sequence, namely

f down
j down
f up
j up

The only difference is that f is held longer when it should act as a modifier, as the user intentionally holds it down in this case, whereas the overlap in the fj-case only occurs because of the user's typing speed, which causes j to be pressed while f hasn't been depressed.

Regarding the pathological use case: I think this is an unavoidable side effect. One could think about only applying the modifier if f is released before j, but then f could no longer serve to apply shift to multiple following keys. If one thinks about f to act as a delayed modifier, the pathological situation becomes perfectly normal.

I can't test it right now but suspect that QMK internally does exactly what I just described.

EDIT: above I of course meant that f should function as shift (not space) when held

rvaiya commented 2 years ago

whereas the overlap in the fj-case only occurs because of the user's typing speed, which causes j to be pressed while f hasn't been [released].

This is the crux of the problem. Most touch typists don't actually release the first key before striking the second.

Regarding the pathological use case: I think this is an unavoidable side effect.

Indeed. This was discussed at length in #81 (and #34) with @slakkenhuis when timeouts were originally introduced. As outlined in those threads, I remain unconvinced that the inherent ambiguity, latency, and additional complexity make it a worthwhile tradeoff, but I am open to persuasion on the point.

One could think about only applying the modifier if f is released before j, but then f could no longer serve to apply shift to multiple following keys.

It's possible I've misunderstood something, but I can't think of a reason anyone would want to do this.

herrsimon commented 2 years ago

Thanks for the links, I will read through them carefully and then write another reply. In the mean time, regarding persuasion: What I'd like to enable is IGNORE_MOD_TAP_INTERRUPT in QMK terms, a lengthy explanation of the reasoning is given here, expanding on the relevant QMK documentation. As I wrote above, for me it's the only possibility to get usable homerow modifiers as I type rather fast.

Given that QMK offers a few other tuning options (in particular PERMISSIVE_HOLD, which might be better for other users), maybe a more general overload action is in fact a better solution than the timeout without interrupt.

herrsimon commented 2 years ago

Alright, so I read #81 and #34 and also spent some time researching the topic, here are the results (apologies in advance for anther very long post).

Summary

If you're in a hurry, a summary of this whole post is that proper overloading should support configurable timeouts and configurable interrupting key logic, with both being equally important. While Keyd already supports timeouts, it forces a specific interrupt logic on the user which is not optimal in all cases, sometimes even resulting in the overload being unusable.

Generic overloading

Overloading letter keys is much more intricate than overloading modifier keys and crucially depends on the specific users' typing habits. There is hence no universal solution and instead, the other popular “advanced” key remappers I know offer a sufficiently general mechanism to fine tune the behaviour.

If we agree that keyd should also offer usable letter (or space) key overloading, then, synthesizing what's been implemented in these other remappers, keyd's overload mechanism should be able to decide whether to activate the layer or an action based on the following parameters:

  1. hold time of the overloaded key
  2. press of an interrupting key within timeout
  3. press and release of an interrupting key within timeout

This is nicely explained and illustrated in ZMK's documentation.

Specifically, the following interrupting key logic should be configurable (in addition to the usual hold timeout):

I) Decide what to do purely on the hold time (ignoring any interrupting key presses) II) Interrupting key press within timeout triggers layer activation III) Interrupting key press within timeout triggers action IV) Interrupting key press and release within timeout triggers layer activation V) Interrupting key press and release within timeout triggers action

Reasoning (which I find convincing) for all these five cases can be found in the references at the end.

Current keyd support and implementation notes

If I'm not mistaken, the only case keyd already covers is III, via

timeout(<action>, <timeout>, overload(<layer>, <action>))

or

timeout(<action>, <timeout>, layer(<layer>))

depending on whether one wants action to be executed if the overloaded key is held in isolation longer than timeout or not.

What would be needed for the remaining four cases would hence be an interrupting-key dependent timeout. Thinking about implementation, I suspect that covering the above five cases for completely general actions will become very complex and ugly, if not infeasible, so maybe it is a good idea to at least forbid nesting timeouts. A simpler alternative could be to introduce an extended overload action (ZMK calls it hold-tap, which is a good name in my opinion), implicitly restricting one action to be layer. I read that overload once already had an optional timeout flag and suppose that it was removed after the more general timeout action was introduced.

In any case, one of the arguments would be a flag specifying the interrupt logic, for example

timeout2(<action1>, <timeout>, <action2>, <interrupt-handling-logic>)

in the general case. An interrupt-handling-logic value of 0 ignores (i.e. just buffers) interrupting key events until <timeout> has expired, a value of 1 and 2 chooses the first and second action, respectively, if an interrupting key press occurs within <timeout> and analogously for 3 and 4, with key press replaced by key press and release.

Use cases and yet another persuasion attempt

To repeat from earlier posts, a prime use case are still overloaded non-modifier keys, in particular letters and the space key. The mere fact that QMK, ZMK and Kmonad all offer not only timeout, but also interrupting key based logic (with different names and different implementations) in a prominent way, with extensive documentation, numerous long blog posts etc. does not seem to be pure coincidence and proves that these features are actually useful. But this last sentence is of course pure marketing without any hard facts.

Apart from that, it seems a bit deliberate that timeout always chooses one of these five possible interrupt handling behaviours, in the sense that I highly doubt one can give a valid argument that one of these behaviours is better than the four others. From an implementation viewpoint however, keyd's choice is of course the simplest, as it avoids having to keep an event queue.

From an end user perspective, given that there is already some interrupt logic implemented, it would seem much more natural to have full control over it.

Finally, I did some quick measurements and confirmed that when typing regular words, I'm basically never tapping letters in isolation. Instead, there is always an overlap (i.e. before I release a letter key, the next one has already been pressed). In addition, there is a significant difference in hold time (about 42ms on average) when using letter keys as actual letters or as a shift modifier (for other modifiers, the difference becomes larger). As this is almost twice the standard deviation from my average tapping and holding times, mistypings are highly unlikely to occur. So in my case, which I strongly believe to not being that uncommon, the ability to specify interrupt handling logic would enable overloading letter keys (and the space key) without any loss in typing speed.

Interrupt logic support in other advanced keymappers

herrsimon commented 2 years ago

Just a small addition: I'm actively using the described feature on a qmk keyboard since a few days (the whole homerow is overloaded) and it's working great so far. I'm also experimenting with so-called combo mods (two simultaneously held keys activate a layer), which keyd should already support. I'll continue to test and will then give more feedback if there is any interest.

nsbgn commented 2 years ago

As has been noted before, a lot here is reminiscent of #34 and #81. Back then, I ended up agreeing with rvaiya: the inevitable visual latency and (perhaps less inevitable) misfires due to inconsistent timing make for a hair-pulling typing experience for me personally.

The fact that ZMK/QMK/kmonad provide this behaviour is encouraging, but not decisive because their design is less opinionated, at the expense of simplicity and guidance. I certainly made a lot more unusable keyboards in kmonad than in keyd.

The one thing that crucially distinguishes your experience from mine:

I'm actively using the described feature on a qmk keyboard and it's working great so far.

The fact that you have made it work for you is convincing. I assume that you do experience some visual latency (ie normal characters don't appear instantaneously but with a delay measuring in some tenths of a second). Are you just not bothered by it?

I highly doubt one can give a valid argument that one of these behaviours is better than the four others.

Imho it is "better" in the sense that it gives a smooth typing experience with minimal visual latency and less fiddling with personal, millisecond-level sweet spots. That said, that does not mean that the other behaviours have no right to exist :)

So, while I do think your explanation makes intuitive sense, I remain convinced (as in #277) that the current behaviour is what users should be guided towards. Event queues may work for some, but users wishing to visit those depths can be expected to do some digging. That is, your <interrupt-handling-logic> would be an optional argument with a default value of 1.

All that said, I need some more time to digest this. And experimentation.

rvaiya commented 2 years ago

Apologies for the belated response, I was putting this off until I had some time to read and respond to it in full. Moving forward it might be better to proceed one use case at a time, to ensure we have a shared understanding of the problem and to create a tighter feedback loop (I myself have a habit of being too verbose at times :P).

While Keyd already supports timeouts, it forces a specific interrupt logic on the user.

A veritable feature ;).

which is not optimal in all cases, sometimes even resulting in the overload being unusable.

I maintain that in conjunction with the proposed timeout2, the current implementation supports the majority of real world use cases with composition.

There is hence no universal solution and instead, the other popular “advanced” key remappers I know offer a sufficiently general mechanism to fine tune the behaviour.

I am personally of the opinion that many of these 'solutions' involve undesirable tradeoffs for what is fundamentally an intractable problem, but my views on the subject have already been stated in other threads, and I am not inclined to foist them upon the userbase.

Here I will contend that most of the use cases you describe are already covered by keyd's composable timeout (and the proposed timeout2) construct.

keyd's overload mechanism should be able to decide whether to activate the layer or an action based on the following parameters:

[...] Specifically, the following interrupting key logic should be configurable [...]

I) Decide what to do purely on the hold time (ignoring any interrupting key presses)

Addressed with timeout2(noop, <timeout>, <action>) (see my response below)

II) Interrupting key press within timeout triggers layer activation

Currently possible with timeout(overload(<layer>, <key>), <timeout>, <key>)

III) Interrupting key press within timeout triggers action

Just a more generic form of the above: timeout(<action>, <timeout>, <key>)

IV) Interrupting key press and release within timeout triggers layer activation

A little more involved, but possible like so:

timeout2(layer(stage2), <timeout>, <key>)`

[stage2]
<second key> = toggle(<target-layer>)

though I'm not sure why anyone would want to do this.

V) Interrupting key press and release within timeout triggers action

A more general form of IV:

timeout2(layer(stage2), <timeout>, <key>)`

[stage2]
<second key> = <action>

Current keyd support and implementation notes If I'm not mistaken, the only case keyd already covers is III

See above :P. It's possible I have misunderstood some of the behaviours you describe. Corrections are welcome.

so maybe it is a good idea to at least forbid nesting timeouts.

This is already forbidden.

I read that overload once already had an optional timeout flag and suppose that it was removed after the more general timeout action was introduced.

Indeed. The current design favours composition over having a dedicated action for every conceivable use case. Easy things should be easy, and more involved things should be possible (within reason).

timeout2(, , , ) in the [...] An interrupt-handling-logic value of 0 ignores (i.e. just buffers) interrupting key events until has expired, a value of 1 and 2 chooses the first and second action, respectively, if an interrupting key press occurs within and analogously for 3 and 4, with key press replaced by key press and release.

Can you provide a real world use case that this addresses?

a prime use case are still overloaded non-modifier keys

As discussed in #277, this is what timeout2 is intended to address.

The mere fact that QMK, ZMK and Kmonad all offer not only timeout [...] does not seem to be pure coincidence and proves that these features are actually useful.

Never underestimate the twin powers of rationalization and group think ;). Just because you can do something doesn't mean that you should. My personal views aside, I have yet to encounter a use case not covered by either timeout or timeout2.

Apart from that, it seems a bit deliberate that timeout always chooses one of these five possible interrupt handling behaviours.

As demonstrated, the timeout actions are flexible enough to handle the five cases you enumerated.

Fundamentally there are two intervals of interest:

  1. The intrakey interval (that is, the time between a key's depression and its release [covered by timeout]).

    and

  2. The interkey interval (the time between two distinct keys [covered by timeout2]).

Taking these as primitives, it should be possible to compositionally realize most interesting applications. This approach differs from the QMK approach of implement all the things, but should have comparable expressive power.

Finally, I did some quick measurements and confirmed that when typing regular words, I'm basically never tapping letters in isolation. [...]

Indeed! I pointed this out here. It is the main impetus for timeout based solutions, but as I outlined in that post I think it is misguided.

So in my case, which I strongly believe to not being that uncommon, the ability to specify interrupt handling logic would enable overloading letter keys (and the space key) without any loss in typing speed.

I encourage you to actually try this (if you haven't already). I maintain that it will either force you to change your typing habits (lowering your speed ceiling), or give you modifier PTSD. Having said that, it should be facilitated by timeout2 as outlined above.

rvaiya commented 2 years ago

Sorry, I omitted the crucial bit of I in my solution, namely (ignoring any interrupting key presses).

Barring nesting (which I agree is ugly), this would indeed necessitate the addition of a third timeout (tentatively called timeout3) which ignores intervening keystrokes, as you proposed.

To summarize, the goal would be for:

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

and

<a down> <b down> <b up> <100ms> <a up>

to produce C-b and ab respectively, given a = timeout3(a, 200, layer(control)).


By contrast, the proposed timeout2 allows for distinguishing (among other things) between:

<a down> <100ms> <b down> <b up> <a up>

and

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

The idea being that the pause before <b> is struck is more indicative of the user's intention to use the first key as a modifier than the pause before <a> is released.

This method involves less visual latency than the first, but is presumably not as effective given the latter's ubiquity (though I still think neither is a good tradeoff).

herrsimon commented 2 years ago

First of all, thanks a lot for thinking about this @slakkenhuis

The fact that ZMK/QMK/kmonad provide this behaviour is encouraging, but not decisive because their design is less opinionated, at the expense of simplicity and guidance.

I think this is a valid and very important point. Before continuing the whole discussion, one should answer the question whether the requested level of customizability should be supported by keyd or not. While you (and @rvaiya) apparently didn't have a pleasant experience when overloading letter keys, for others, including myself, they are actually working great (I'm still experiencing some glitches I'm about to explain below, but these are known and qmk/zmk offer remedies). In my opinion, a completely customizable overload belongs to every good keymapping software and hence should be part of keyd as well (either realized through completely general timeout actions, or by directly extending overload).

The fact that you have made it work for you is convincing. I assume that you do experience some visual latency (ie normal characters don't appear instantaneously but with a delay measuring in some tenths of a second). Are you just not bothered by it?

There is of course some visual latency, but I actually got accustomed to it pretty quickly. I'm going to write more details in my reply to @rvaiya's comments.

I highly doubt one can give a valid argument that one of these behaviours is better than the four others.

Imho it is "better" in the sense that it gives a smooth typing experience with minimal visual latency and less fiddling with personal, millisecond-level sweet spots. That said, that does not mean that the other behaviours have no right to exist :)

Again, as I wrote in #277: Then I challenge you to give an objective reason which choosing the first action on interrupt is superior to choosing the second.

So, while I do think your explanation makes intuitive sense, I remain convinced (as in #277) that the current behaviour is what users should be guided towards. Event queues may work for some, but users wishing to visit those depths can be expected to do some digging. That is, your <interrupt-handling-logic> would be an optional argument with a default value of 1.

As I'm about to write in response to @rvaiya's reply, I also think that extending the overload action instead of introducing more generic timeout actions are probably the best way to proceed.

herrsimon commented 2 years ago

EDIT: corrected wrong description of desired nested tap functionality

Apologies for the belated response, I was putting this off until I had some time to read and respond to it in full. Moving forward it might be better to proceed one use case at a time, to ensure we have a shared understanding of the problem and to create a tighter feedback loop (I myself have a habit of being too verbose at times :P).

No problem at all, thanks a lot for taking the time to actually read my very long mental outpourings and responding to them in a very insightful way.

So before concentrating on one use case, let me just remark that I was apparently not clear enough with my explanations, causing some misunderstandings. Actually, your proposed solutions using timeout and the planned timeout2 would only work for cases II and III. I'm using homerow modifiers sucessfully since about two weeks now on QMK. There are some glitches, but ZMK (and I'm sure also QMK with some custom coding) offer remedies for them, which are already incorporated below.

Before proceeding, as a caveat: Even though I've been using mechanical keyboards for many years (with vanilla dvorak layout), I actively started tinkering with the layout nine months ago and haven't converged to a final configuration yet.

Let's continue the example where the f key is overloaded with shift and j is pressed after f. The question then is how to disambiguate between fj and J (S-j). The following cases are relevant for the discussion:

What I would like to have is the following behaviour:

  1. A nested tap should make f act as a modifier, i.e. output J, when j is tapped within the timeout. Also, keeping f pressed and tapping more than one character should make f act as shift. Therefore, indicating the instant in time where the timeout occurs by <T>, all of the following should output J:

The output in the above cases would appear immediately after the timeout or j_up, whatever comes first. In addition, the modifier should remain active until the key up event, i.e. the sequences

f_down j_down j_up <T> k_down k_up f_up
f_down j_down j_up k_down <T> k_up f_up

should produce JK.

  1. In a key roll, the modifier should be applied when f is held for at least the timeout or longer, independently of the events for j (as long as j_down occurs before f_up and j_up after f_up). Therefore, the sequences
    f_down <T> j_down f_up j_up
    f_down j_down <T> >f_up j_up
    f_down j_down <f_up> <T> j_up

    should result in J, J and fj, respectively. Here is the crucial point: When I type at my natural speed, without consciously altering my typing, I will input the second sequence when trying to enter J and the third one when trying to enter fj almost all the time (about a hand full of mistypings in the last week). Keyd however only supports the first sequence, which would require a conscious pause, slowing down typing.

While QMK fully supports point 2, I haven't found a way to get 1, which is why I will probably order some nice!nanos so that I can try the setup on ZMK (where it is called “balanced mode”).

While Keyd already supports timeouts, it forces a specific interrupt logic on the user.

A veritable feature ;).

Like I already replied to @slakkenhuis: I think that there should at least be a way to select the second action on interrupt, then. While I understand the reasoning behind the fact that an interrupt currently immediately selects an action, I think that it is completely arbitrary to select the first (unless one has a very specific use case in mind).

I am personally of the opinion that many of these 'solutions' involve undesirable tradeoffs for what is fundamentally an intractable problem, but my views on the subject have already been stated in other threads, and I am not inclined to foist them upon the userbase.

That last sentence is the thin straw of hope that drives my replies, in the hope that you have mercy with those users who yet have to find their holy grail.

Here I will contend that most of the use cases you describe are already covered by keyd's composable timeout (and the proposed timeout2) construct.

I think that we misunderstood each other regarding the desired functionality, mostly because I was not clear enough. The important point I didn't clearly state is that in cases II-V, the modifier is also activated when the timeout has expired. The described interrupt logic should occur on top of that. Also, when I wrote “action”, I meant it as a more generic substitute for emitting a letter, not activating a letter. Still:

II) Interrupting key press within timeout triggers layer activation

Currently possible with timeout(overload(<layer>, <key>), <timeout>, <key>)

Due to the misunderstanding, it should read timeout(overload(<layer>, <key>), <timeout>, <layer>), but still you are of course completely right that this case is already covered.

III) Interrupting key press within timeout triggers action

Just a more generic form of the above: timeout(<action>, <timeout>, <key>)

Here I actually meant timeout(<key>, <timeout>, overload(<layer>, <key>)), but again you are completely right that this is already possible.

IV) Interrupting key press and release within timeout triggers layer activation

A little more involved, but possible like so:

timeout2(layer(stage2), <timeout>, <key>)`

[stage2]
<second key> = toggle(<target-layer>)

Again we apparently misunderstood each other. What I actually meant is that upon key press and release within timeout, the first key should act as a modifier (activate the layer). I think for this, an action akin to the planned timeout2 would be needed, which listens for a complete tap within the timeout, not just a key press.

though I'm not sure why anyone would want to do this.

To ensure that a nested tap always makes the first key act as a modifier (point 1 described above), independently of the timeout.

V) Interrupting key press and release within timeout triggers action

A more general form of IV: ...

What I actually meant is that upon key press and release within timeout, the first key should emit a letter (f in the above example) instead of acting as a modifier when the second key is tapped within the timeout and before the first one is released. Again, an action like timeout2 but listening for a complete tap within the timeout would achieve this.

Current keyd support and implementation notes If I'm not mistaken, the only case keyd already covers is III

So in fact, II and III are already covered, but I, IV and V are not. For I, as you already wrote in your second reply, an additional timeout action would be needed, ignoring all interrupts, and for IV and V and would need a version of the planned timeout2 which disambiguates based on key taps and not just presses.

so maybe it is a good idea to at least forbid nesting timeouts.

This is already forbidden.

...and it should be allowed (at least for depth one) to make the above solutions work.

Indeed. The current design favours composition over having a dedicated action for every conceivable use case. Easy things should be easy, and more involved things should be possible (within reason).

Completely agree!

timeout2(, , , ) in the [...] An interrupt-handling-logic value of 0 ignores (i.e. just buffers) interrupting key events until has expired, a value of 1 and 2 chooses the first and second action, respectively, if an interrupting key press occurs within and analogously for 3 and 4, with key press replaced by key press and release.

Can you provide a real world use case that this addresses?

I thought of this as a more straightforward alternative (also from an implementation point of view) to introducing all sorts of different timeout actions. The real world use case would still be the one described above. So for example in case II, instead of writing timeout(overload(<layer>, <key>), <timeout>, <layer>), the user could just state overload(<layer>, <key>, <timeout>, 1), and all other cases could simply be achieved by varying the last argument as described.

Never underestimate the twin powers of rationalization and group think ;). Just because you can do something doesn't mean that you should. My personal views aside, I have yet to encounter a use case not covered by either timeout or timeout2.

I hope that you're convinced now that such a use case exists, and at least doubt that this use case is obscure.

Fundamentally there are two intervals of interest:

  1. The _intra_key interval (that is, the time between a key's depression and its release [covered by timeout]).

and

  1. The _inter_key interval (the time between two distinct keys [covered by timeout2]).

Taking these as primitives, it should be possible to compositionally realize most interesting applications. This approach differs from the QMK approach of implement all the things, but should have comparable expressive power.

If I'm not mistaken, out of the five cases, this allows to cover two, and I still think that all five cases are useful. It all depends on the users' typing habits as well as the key to be overloaded (there's a big difference between overloading letter keys and modifier keys).

Regarding your last reply (the described timeout3):

To summarize, the goal would be for:

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

and

<a down> <b down> <b up> <100ms> <a up>

to produce C-b and ab respectively, given a = timeout3(a, 200, layer(control)).

I think so, but to be on the safe side, let's be very verbose: Disambiguation should be done purely based on the hold time of the a key, i.e. the intra-key time as you described it. So if a is held down for 200ms or more, it would act as control, and otherwise as a. Also, I think the more interesting cases are key rolls here. So the relevant sequences would be <a down> <b down> <200ms> <a up>> <b up>

and

<a down> <b down <100ms> <a up> <b up>

resulting in C-b and ab, respectively.

So to sum up: I'm vouching for either two more timeout commands (in addition to the planned timeout2), one of them ignoring interrupting key presses, the other one acting like timeout2, but listening for key taps instead of key presses within the timeout, or alternatively an extended overload2(<layer>, <action>, <timeout>, <interrupt-handling-logic>). Before writing this post, I was leaning towards overload2, but now I'm again completely torn between the two possibilities.

rvaiya commented 2 years ago

The following cases are relevant for the discussion:

f_down j_down j_up f_up (nested tap) f_down j_down f_up j_up (key roll)

What I would like to have is the following behaviour:

[...]
[...] all of the following should output J:

f_down j_down j_up f_up f_down j_down j_up f_up

It's not clear what role the timeout is serving in this example. Under which conditions involving a 'nested tap' is fj produced?

Your wording would suggest that if the timeout has not elapsed by <T> then fj is produced, which would imply that all of:

  1. <f down> <j down> <j up> <100ms> <f up>
  2. <f down> <j down> <100ms> <j up> <f up>
  3. <f down> <100ms> <j down> <j up> <f up>

yield tap behaviour (assuming a timeout of 200ms).

in which case it's not clear what the difference between:

f_down j_down j_up f_up

and

f_down j_down j_up f_up

is.

If my understanding is correct, then the behaviour you describe is identical to the proposed timeout3 (an 'uninterrupted timeout' disambiguating intention upon expiry (or key up)).

Having said that, it's not clear to me that this is the optimal approach.

For example, it's not obvious that:

<f down> <200ms> <j down> <j up> <f up>

should necessarily be treated the same as:

<f down> <j down> <j up> <200ms> <f up>

since the location of the pause potentially correlates with intention.

timeout2 can be used to specifically deal with the timeout between $k1{down}$ and $k2{down}$ (the interkey interval) without conflating it with cases 1 and 2. Given that QMK seems to favour timeout3 (which I believe is its default behaviour), I assume this is probably not an issue in practice (or no one has bothered to measure the alternatives).

Therefore, the sequences

f_down <T> j_down f_up j_up
f_down j_down <T> >f_up j_up
f_down j_down <f_up> <T> j_up

should result in J, J and fj, respectively.

I believe this should already be a byproduct of timeout3 ('resolve on timeout or $k1_up$').

While QMK fully supports point 2, I haven't found a way to get 1, which is why I will probably order some nice!nanos so that I can try the setup on ZMK (where it is called “balanced mode”).

Interesting. Are you suggesting QMK does not interpret:

<f down> <200ms> <j down>

as J (for timeout = 100ms)? This would seem like a deliberate exception. It has been a few years since I have experimented with QMK timeouts, so I am not familiar with their current behaviour.

Like I already replied to @slakkenhuis: I think that there should at least be a way to select the second action on interrupt. [...] I think that it is completely arbitrary to select the first (unless one has a very specific use case in mind).

I don't agree with this. The rationale behind interpreting the timeout interrupt ([$k1{down}$, $k2{down}$]) as a 'tap action' by default, is that holding a key is a deliberate action that the user should have to consciously pause to achieve.

That is given space=timeout(space, 500, layer(control)), you almost always want:

<space down> <400 ms> <j down> <space up>

to resolve as:

<space down> <j>

rather than:

C-j

The reverse only becomes desirable if you introduce more sophisticated timeouts in conjunction with event queuing (timeout3) to try and guess the user's intent, which introduces the host of issues previously discussed (visual latency, misfires, etc).

That last sentence is the thin straw of hope that drives my replies, in the hope that you have mercy with those users who yet have to find their holy grail.

You've persuaded me that my former obstinacy was unwarranted. Though I still think 'home row mods' are the domain of the swivel eyed loon ;).

I think that we misunderstood each other regarding the desired functionality, mostly because I was not clear enough.

No, it was my fault. I should have read your examples more clearly. The first one you provided clearly necessitates the addition of a third action. I was previously opposed to it because of my instinctive aversion to event queuing, but since there seems to be a fair amount demand for it, I will acquiesce.

IV) Interrupting key press and release within timeout triggers layer activation Again we apparently misunderstood each other. What I actually meant is that upon key press and release within timeout, the first key should act as a modifier (activate the layer). I think for this, an action akin to the planned timeout2 would be needed, which listens for a complete tap within the timeout, not just a key press.

In that case, how does this differ from timeout(overload(layer, key), 300ms, layer)?

instead of writing timeout(overload(, ), , ), the user could just state overload(, , , 1), and all other cases could simply be achieved by varying the last argument as described.

I am more inclined to favour the existing composition approach. If a real world use case can't be achieved using the existing (or proposed) actions, feel free to file a separate issue.

The _intra_key interval [...] The _inter_key interval [...]

If I'm not mistaken, out of the five cases, this allows to cover two, and I still think that all five cases are useful.

I think this is solved by distinguishing between the 'interrupted intrakey interval' ([$k1_{down}$, $first(k1up, k2{down})$] (i.e timeout)) and the 'uninterrupted intrakey interval' ([$k1{down}$,$k1{up}$] timeout3). The second can probably properly be called the 'intrakey interval', though the first probably deserves a better name, since it doesn't strictly deal with the time between key down and key up.

I think so, but to be on the safe side, let's be very verbose: Disambiguation should be done purely based on the hold time of the a key,

+1. This is the key bit. I think we are in agreement on this point.

the other one acting like timeout2, but listening for key taps instead of key presses within the timeout

I'm not entirely sure how this is different from the proposal outlined in #277, but this is probably not the place to discuss it.

I still think we are discussing too many things at once. I've created a dedicated issue for timeout3 (#309) and will shortly create one for timout2. If you think something remains uncovered feel free to post in those threads.

I'm closing this for now.