rvaiya / keyd

A key remapping daemon for linux.
MIT License
3.02k stars 174 forks source link

[BUG] releasing modifer before releasing timeout() key does not apply modifier #722

Open lobre opened 7 months ago

lobre commented 7 months ago

Sorry again for that title that is really hard to grasp at first sight.

I realize that each time I come across a weird behavior in keyd, I struggle to frame it correctly and give a name that would allow anybody to quickly understand the problem at a glance. I guess it is because details matter a lot in such software. Anyway, let me explain my problem.

First and as always, here is an example:

[ids]
*

# apply meta-x when hitting x more that 1s
x = timeout(x, 1000, M-x)

# trivial/default but just for the sake of the example
z = z
control = layer(control)

What I want is rather simple in that example. I want x to give x is held less than 1 second, and if held more, I want the meta version of x.

Now let’s try to combine that with ctrl. If I take the example of z, which does not have any specific behavior attached. When I hold ctrl and then press z, ctrl-z is directly sent at press (and not at release).

Now, let’s try to do it with x instead. As there is a timeout, keyd cannot know at press if it should send ctrl-x or ctrl-meta-x. So it has to act differently this time and wait for the release event, to be able to compute the amount of time spent holding the key.

Let me show in this asci art what it means (it should be read from left to right).


 ctrl         ctrl        
   │            ▲         
   │    250ms   │         
   ▼───▲─────┬──┘         
       │     │            
       │     ▼            
       x     x ◄───ctrl-x 

Here, ctrl gets sent, then x gets pressed but nothing happens yet. Within 250 ms, x gets released and here, keyd understands that the user wants to send ctrl-x. Finally, ctrl gets released.

All that context to be able to demonstrate the behavior that I think is weird. Let’s alter slightly the previous example to say that ctrl is released before x has been released.

Here is again the asci art.


 ctrl     ctrl         
   │        ▲          
   │        │          
   ▼───▲────┴───┐      
       │  250ms │      
       │        ▼      
       x        x ◄──x 

Here you can quickly see that ctrl gets released before x. As we are still under 1 second, we still don’t want the meta version of x. However, as a result, only x is sent, instead of ctrl-x. Whether this is normal is debatable.

I personally think that if the key cannot be determined at press, the modifier version of the key should still apply even if the modifier gets released in the meantime, before the actual key. Mentally, the user thinks that the modifier is applied when pressing the second key. So if he releases the modifier before in a fast motion, it is obvious to me that he does not want to type the key itself.

What do you think about this? This might also apply to other key overloading mechanisms besides timeout.

rvaiya commented 6 months ago

Thanks for the clear exposition, it makes discussing the issue much easier :).

An intrinsic part of adding time based disambiguation to a key involves severing the relationship between the physical keystroke and the actual input event.

The crux of your issue seems to be that there is a discrepancy between the physical sequence:

<control down> <x down> <control up> <x up>

and the produced logical sequence:

<control down> <control up> <x down> <x up>

While your particular use case would benefit from the modifier present during the physical event propagating through to the logical sequence, there are probably actions and use cases for which it would be undesirable.

timeout() is a very simple action, it simply executes the supplied action on either side of the timeout once the action can be disambiguated, so it is arguably doing its job.

One way to solve your particular issue would be to explicitly overload x in the control layer:

E.g

[main]
x = timeout(x, 1000, C-M-x)

[control]
x = timeout(C-x, 1000, C-M-x)

granted this is a bit ugly and potentially cumbersome given a large enough set of bindings. Consider however that it is at least possible to achieve (and relatively easy to comprehend). Making this default behaviour would make the reverse impossible.

This is also consistent with keyd's general view that bindings are resolved by physical events, but their consequences can be downstream of those physical events.

The goal of keyd is to provide as simple (but no simpler) a set of primitives that can be used to implement most useful behaviour, but some problems are inherently tricky and require the user to grok the problem domain.

Having said that, I am more inclined to view this as a possible bug in the overloadt* set of actions, since those are more intimately tied to the goal of letter overloading, and are thus more constrained (one of the arguments must always be a layer). However a similar solution as above can also be deployed in those cases and I haven't considered all of the implications of propagating modifier state.

P.S

Sorry for taking so long to address this. I am slowly working my way through the large backlog of issues.

lobre commented 6 months ago

[main] x = timeout(x, 1000, C-M-x)

Is there a mistake here and did you mean M-x solely?

Otherwise, I like the way you framed the problem. It is a different and broader view than mine, and I also like that you guide your design with simplicity, avoiding any behaviour that would sound a bit "magic".

I still struggle to have a complete understanding of the impacts of answering the problem the way I imagine. I guess it makes more sense for you as you have worked in this domain space for longer. I do have the feeling that changing the behaviour to what I propose could potentially break a lot of other workflows, but those are unknown to me. I only have a clear view of my own workflow as I have thought about it for quite some time now. So I can only trust you.

Each time I post an issue about what I don't manage to do, there is always an existing "solution" that is more or less clean. I did not see this one coming as I thought it was something tricky. But I do like the proposed solution, except for the high verbosity it will require.

If we had a way to abstract the configuration somehow and create templates/functions or whatever we want to call them, this would help a lot for all of those.

Here, I posted my example for control, but in my current configuration, I do have two other cases where this problem happens. One for altgr, and one for my number layout. I cannot imagine the size of my configuration if I decide to override each of the 30 keys in each layer to implement that correctly (without talking about the readability). Because as you said, each person will have to leverage basic building blocks and have a precise knowledge of the problem space to implement "correct" solutions. More verbosity makes understanding what happens more difficult.

Do you already have leads about this verbosity problem? (I saw it coming back in different issues).

Thank you for taking the time to answer.

tobiasBora commented 4 months ago

I think I also got bitten by this same bug.

If I do:

g = overloadt(arrows, g, 1000)

to create a mod key (same bug with the other variations of overload), and press:

press altgr, press g, release g, release altgr

I get what I would expect, i.e. altgr-g (i.e. ' in bépo).

But if I do it in a rolling way, i.e.:

press altgr, press g, release altgr, release g

then it actually sends g instead (i.e. , in bépo).

I would love to see this fixed, as it makes me basically unable to use this key as a lead key since I'm really used to type it in a rolling way.

Thanks!