Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: Oneshot() with timeout #277

Closed
herrsimon opened this issue Aug 10, 2022 · 21 comments
Closed

Feature request: Oneshot() with timeout #277

herrsimon opened this issue Aug 10, 2022 · 21 comments

Comments

@herrsimon
Copy link
Contributor

I'd like to propose an expiration mechanism for oneshot, say oneshot2(layer, timeout) for the sake of discussion, which behaves like oneshot but only if the next key is pressed within timeout ms. The naming is suboptimal, as the two at the end usually signals the ability to supply a macro to be executed (and who knows if at some point a use case warranting the introduction of oneshot2(layer, macro) will be found), but I couldn't come up with something better except the more cumbersome layer_timeout.

Repeated calls to oneshot2() with the same layer argument should reset the timer to the last supplied timeout, while any other action activating or deactivating a layer with a currently attached timer should remove this timer. Also, from an implementation perspective, it is probably a good idea to only allow a single expiring layer at a time, i.e. calling oneshot2() while a layer different from the one given as argument has an active timer would deactivate this other layer.

This would address the following use cases:

  1. Self-deactivating (nested) leader keys

The oneshot2() action would allow a user to change his mind after pressing the corresponding key. Currently, “cancelling” a oneshot action without visible consequences necessitates an additional keypress (typically some modifier key).

More concretely, I would like to toggle some layer by tapping shift twice, i.e.

[main]
leftshift = overload(shift,oneshot(shift))

[shift]
leftshift = toggle(layerA)

[layerA]

leftshift = toggle(layerA)

After testing it for a while, I temporarily removed this functionality as it often happened that I tapped shift because I wanted to start some sentence and then changed my mind. When I then wanted to continue to type and hence pressed shift again, layerA would activate, which was a bit annoying. If oneshot(shift) could be replaced with oneshot2(shift, 500), this problem would not occur.

  1. Automatically activate an expiring navigation layer while moving a trackpoint or physical mouse (QMK has a similar feature for keyboards with a builtin trackpoint/mouse, which I find very useful)

Usually, after moving the mouse one has to click somewhere. If keyd would be able to activate an expiring navigation layer (typically with the mouse buttons on the home row) while moving a trackpoint or external mouse, one could replace the physical mouse buttons by proper keyboard buttons without the need to use some additional modifier press or explicitly activating a layer.

Of course, for this functionality one would need to enable calling actions via IPC in addition (which is planned to be implemented if I'm not mistaken), so that some script could monitor for mouse move uevents and issue the corresponding oneshot2() call.

@nsbgn
Copy link

nsbgn commented Aug 10, 2022

I have also thought about this, for the same use case. Its absence hasn't caused me so much grief (yet) to have done a feature request but needless to say I do like the idea.

but I couldn't come up with something better

What about not changing the name at all and just have an optional argument?

@herrsimon
Copy link
Contributor Author

What about not changing the name at all and just have an optional argument?

That's actually another thing I wanted to propose: Remove all 2-versions of existing actions and instead allow optional arguments. This would require slightly more involved parsing logic which would be more than set off by a much cleaner end user experience. The 2-versions could be declared as deprecated and accepted as well for a while.

@rvaiya
Copy link
Owner

rvaiya commented Aug 11, 2022

How about something like timeout2(<action1>, <time>, <action2>), where <time> corresponds to the time elapsed between the bound key and the next one? <action1> would only get executed if the next key is struck before the timeout elapses, while <action2> would get executed if no other keys are struck before the timeout expires.

You could then implement a decaying oneshot like so:

timeout2(oneshot(shift), 500, noop)

It could potentially have other uses like the time based sequence logic described here.

Remove all 2-versions of existing actions and instead allow optional arguments.

Optional arguments were implemented at one point before they were abandoned in favour of the current syntax. Aside from making parsing a bit easier, it also provides a visual way to identify more involved actions (since it isn't immediately obvious what the extraneous arguments do) and makes it easier to search the man page.

It's also worth noting that some actions (e.g macro2) can't be cleanly disambiguated anyway, and that not all actions have a macro variant.

@nsbgn
Copy link

nsbgn commented Aug 11, 2022

Even better, of course. Should have been my first thought. Nicely symmetrical.

I do think the ...2 naming is a bit unsatisfying. time_hold/hold_timer for timeout and time_tap/tap_timer for timeout2 would be more descriptive. But that would deprecate existing syntax, which is undesirable. Maybe there's another option, like timeout(action1, hold 500, action2) vs timeout(action1, tap 500, action2), with hold being the default

@herrsimon
Copy link
Contributor Author

timeout2 sounds perfect! Regarding the naming: While I still prefer optional arguments over descriptive names and descriptive names over number suffixes (in the manpage one could first treat the case where no optional arguments are given, this is for example common in the emacs documentation) for the purely subjective reason that it appears to look more elegant in the configuration file, it is probably true that number suffixes or descriptive names are easier to understand. Of course I can arrange with any of the three solutions.

@herrsimon
Copy link
Contributor Author

One more remark:

For consistency, as one can already define defaults for macro (and specify the relevant arguments specifically via macro2), how about a hybrid approach: Implement timeout_tap and timeout_hold which only take two action arguments, applying some default hold_timeout and tap_timeout configurable in the global section, and then also introducing timeout_tap2 as well as timeout_hold2, both taking a timeout as a third argument. The timeout action could remain valid (with keyd issuing a deprecation warning) for backward compatibility.

@rvaiya
Copy link
Owner

rvaiya commented Aug 12, 2022

I do think the ...2 naming is a bit unsatisfying.

Agreed. The hard part is coming up with a good name.

time_hold/hold_timer for timeout and time_tap/tap_timer for timeout2 would be more descriptive.

Something like timeout-hold makes sense for timeout since the timer corresponds to the length of time the key is depressed, but timeout2's timeout corresponds to the time between successive keys and probably shouldn't be named timeout-tap. I can't think of a good name which concisely captures its behaviour (maybe timeout-next or perhaps collapse-wave-function/schrodinger-key?).

But that would deprecate existing syntax, which is undesirable.

I have no objection to doing this if it improves readability. The old names can be kept for backwards compatibility.

Implement timeout_tap and timeout_hold which only take two action arguments,
applying some default hold_timeout and tap_timeout configurable in the global
section, and then also introducing timeout_tap2 as well as timeout_hold2, both
taking a timeout as a third argument. The timeout action could remain valid
(with keyd issuing a deprecation warning) for backward compatibility.

I think this adds a bit too much complexity. Most users won't bother with timeouts, and those that do should probably understand precisely how the timeouts are applied. Forcing them to be as explicit as possible is a good thing.

@herrsimon
Copy link
Contributor Author

I can't think of a good name which concisely captures its behaviour (maybe timeout-next or perhaps collapse-wave-function/schrodinger-key?).

While schrodinger-key is of course very appealing, I think timeout-next is fine. What also crossed my mind was ifelse-hold/ifelse-next with the timeout as first argument.

I think this adds a bit too much complexity. Most users won't bother with timeouts, and those that do should probably understand precisely how the timeouts are applied. Forcing them to be as explicit as possible is a good thing.

On second thought I agree with you.

@nsbgn
Copy link

nsbgn commented Aug 12, 2022

Something like timeout-hold makes sense for timeout since the timer corresponds to the length of time the key is depressed, but timeout2's timeout corresponds to the time between successive keys and probably shouldn't be named timeout-tap.

If anything, the 'tap-timeout', to me, makes more sense. A timeout feels like something that happens unless something else happens, whereas a 'hold-timeout' (or 'timer') requires the user to continue pressing (...but I realize you can also interpret that as 'nothing else happening'). In any case, how else is the user supposed to interpret a timeout on a tap? But intuitions differ, of course. And I'm not a native speaker, so what do I know :)

I can't think of a good name which concisely captures its behaviour (maybe timeout-next or perhaps collapse-wave-function/schrodinger-key?).

Ah, next reminds me of kmonad and I also remember it being not immediately clear what that meant :P Since you mentioned the word "between", you could also do timeout-between? I'm going to run this as a background process in my mind today. EDIT: timeout-pause?

What also crossed my mind was ifelse-hold/ifelse-next with the timeout as first argument.

Wait, what would that look like?

@herrsimon
Copy link
Contributor Author

How about timeout-idle?

@herrsimon
Copy link
Contributor Author

herrsimon commented Aug 12, 2022

Another proposal (inspired by the leading spreadsheet software I detest but am forced to use at work regularly):

if-held-uninterrupted(time, if_action, else_action)

if-held(time, if_action, else_action)

if-idle(time, if_action, else_action)

where if_action is executed if the key is held for time ms or keyd is idle for time ms and else_action is executed otherwise. The difference between if-held-uninterrupted and if-held would be that a keypress within time interrupts the timer and executes else_action (equivalent to the current timeout(else_action, time, if_action); also see #278). Even more suggestive variants are if-held-uninterrupted-for, if-held-for and if-idle-for, but that might be a bit too much.

EDIT: corrected my confusion with interruption

@nsbgn
Copy link

nsbgn commented Aug 13, 2022

I really like timeout-idle. It's by far the most intuitive option to me right now.

if-held and if-idle are also intuitive, but not more or less so than the current timeout(action1, interval, action2) syntax. I gather that this further split is mostly to accommodate possible additional timeout mechanisms? I hope your use case for that is more sane than #81 :P Either way it seems to me that having only timeout-idle+timeout-hold is clearest, because of that natural duality --- with future specializations remaining possible as optional arguments/suffixed names.

@herrsimon
Copy link
Contributor Author

I'm now also in favour of timeout-idle.

@slakkenhuis I think my use case (homerow modifiers) is sane and I'm vouching for full control of the way interrupting key presses or taps are handled during the timeout window. This could also be of interest for you, as it would solve at least some of the issues you had in #81. Any support in my attempts to persuade @rvaiya in #278 would be greatly appreciated...

@nsbgn
Copy link

nsbgn commented Aug 16, 2022

Sure, I'll think when I get some time. Be warned that my involvement may backfire, as rvaiya's relative conservatism on this matter has proved pretty persuasive to me :P

Anyway, apart from these syntax concerns, that issue can be considered wholly separately from this one, right?

@herrsimon
Copy link
Contributor Author

Sure, I'll think when I get some time.

Thank you!

Be warned that my involvement may backfire, as rvaiya's relative conservatism on this matter has proved pretty persuasive to me :P

I'm actually very thankful for this conservatism and don't even want to imagine what ugly beast keyd would have become if all my feature requests had been blindly implemented. Thinking things through is part of my job, but keyd (and warpd) get me so excited that I regularly throw all good principles over board. The question is, if that's good or bad... At least I have the excuse of not having a cs background.

Anyway, apart from these syntax concerns, that issue can be considered wholly separately from this one, right?

Yes.

@rvaiya
Copy link
Owner

rvaiya commented Aug 19, 2022

If anything, the 'tap-timeout', to me, makes more sense.

The problem with the name is that it doesn't necessarily have anything to do with whether or not the key is tapped. The sequences <a down> <a up> <100 ms> <b down> and <a down> <100 ms> <b down> <a up> would both produce the same output given something like a = timeout2(c, 100, d). A more descriptive name might be something like resolve-on-next-key, but even that isn't fully descriptive and is quite verbose.

In some ways a neutral name like timeout2, which communicates nothing, has an advantage, since it forces the user to consult the man page for what is generally quite an involved thing to be doing in the first place.

Ah, next reminds me of kmonad and I also remember it being not immediately clear what that meant :P

Case in point :P.

Since you mentioned the word "between", you could also do timeout-between? I'm going to run this as a background process in my mind today. EDIT: timeout-pause?

timeout-between is a bit nicer, but it still isn't immediately obvious what it does. Between what? In this case between the key being bound and the next keystroke, but one could be forgiven for thinking it might be between the last key and the present one.

if-held(time, if_action, else_action)

I assume this is the new name for timeout. I don't think this is superior to timeout since the name doesn't suggest it has anything to do with time (i.e how it is different from overload)

The difference between if-held-uninterrupted and if-held would be that a keypress within time interrupts the timer and executes else_action (equivalent to the current timeout(else_action, time, if_action);

In which case I believe if-held-uninterrupted would be a less generic form of timeout2 (which doesn't care whether or not the key is released). I also think the name is a bit ambiguous. One could be forgiven for thinking that that 'uninterrupted' means 'if the key is not interrupted, do X'. It's not obvious (to me) that the action proactively blocks the next key from being processed until the timeout is resolved.

I really like timeout-idle. It's by far the most intuitive option to me right now.

Can you explain the rationale?

if-held and if-idle are also intuitive, but not more or less so than the current timeout(action1, interval, action2) syntax.

+1.

Thinking things through is part of my job, but keyd (and warpd) get me so excited that I regularly throw all good principles over board. The question is, if that's good or bad... At least I have the excuse of not having a cs background.

You needn't be so hard on yourself. Taste is subjective after all ;). My opinions are just that.

While some of these names might be improvements, none of them indicate what the action does in isolation. The user would still have to have read the man page to properly disambiguate them from other possible interpretations. Given that this is the case, I am still inclined to think more neutral names (like timeout/timeout2) might be better in the end.

@nsbgn
Copy link

nsbgn commented Aug 19, 2022

The problem with the name is that it doesn't necessarily have anything to do with whether or not the key is tapped. The sequences <a down> <a up> <100 ms> <b down> and <a down> <100 ms> <b down> <a up> would both produce the same output given something like a = timeout2(c, 100, d).
...
Between what? In this case between the key being bound and the next keystroke, but one could be forgiven for thinking it might be between the last key and the present one.

I agree, tap and between are suboptimal --- and while I see your point that you'd rather force users to read the manual, I maintain that either of these are at least easier to commit to memory than 2 :P Even if that semantic connection is tenuous, any connection (as long as it's not arbitrary or actively confusing) makes manual-reading easier.

The user would still have to have read the man page to properly disambiguate them from other possible interpretations. Given that this is the case, I am still inclined to think more neutral names might be better in the end.

I disagree there: while I might have to read the man page once to understand what timeout-idle and timeout-hold do, I will probably have to read it every time to remember which is timeout and which is timeout2. That might be an issue with my memory but I'm willing to bet that I'm not the only one.

If there was an actively counterintuitive interpretation for the suffixes, I'd be more inclined to agree, but while that might have been the case for tap and between, I don't think it is for idle and similar.

Can you explain the rationale?

  • timeout-holdis a timeout on the time that, after pressing the relevant key, you subsequently hold it.
  • timeout-idle is a timeout on the time that, after pressing the relevant key, you subsequently do nothing. (timeout-wait would also work)

While the user could surely strain themselves to find a different interpretation, I can't think of any that are obvious. And timeout by itself is certainly more ambiguous: what are you timing out?

@herrsimon
Copy link
Contributor Author

if-held(time, if_action, else_action)

I assume this is the new name for timeout. I don't think this is superior to timeout since the name doesn't suggest it has anything to do with time (i.e how it is different from overload)

This should be timeout, but completely ignoring any interrupting keypresses. I share your criticism, how about the more verbose if-held-for(time, if_action, else_action)? I think that the meaning would be very clear.

The difference between if-held-uninterrupted and if-held would be that a keypress within time interrupts the timer and executes else_action (equivalent to the current timeout(else_action, time, if_action);

In which case I believe if-held-uninterrupted would be a less generic form of timeout2 (which doesn't care whether or not the key is released). I also think the name is a bit ambiguous. One could be forgiven for thinking that that 'uninterrupted' means 'if the key is not interrupted, do X'. It's not obvious (to me) that the action proactively blocks the next key from being processed until the timeout is resolved.

This should actually be the substitute of the current timeout and again, one could be more verbose and name it if-held-uninterrupted-for.

Regarding

while I see your point that you'd rather force users to read the manual, I maintain that either of these are at least easier to commit to memory than 2 :P Even if that semantic connection is tenuous, any connection (as long as it's not arbitrary or actively confusing) makes manual-reading easier.

and

I disagree there: while I might have to read the man page once to understand what timeout-idle and timeout-hold do, I will probably have to read it every time to remember which is timeout and which is timeout2. That might be an issue with my memory but I'm willing to bet that I'm not the only one.

I agree to both of @slakkenhuis's points. Good notation would be self-explanatory for existing users. Optimal notation is self-explanatory for (most) new users, at least those with some experience in the world of programmable keyboards. One of the strong points of keyd is its simple configuration file format, and I think that it's possible to reach the point where new users reading some configuration example somewhere immediately understand what's going on.

  • timeout-holdis a timeout on the time that, after pressing the relevant key, you subsequently hold it.
  • timeout-idle is a timeout on the time that, after pressing the relevant key, you subsequently do nothing. (timeout-wait would also work)

Exactly. But to repeat,: As timeout-hold would ignore any interrupting key presses, none of the two actions would mirror the current timeout. What makes things worse is that depending on the outcome of #278, some more names might be needed, as there is more than one case of key interruption left.

@nsbgn
Copy link

nsbgn commented Aug 20, 2022

As timeout-hold would ignore any interrupting key presses, none of the two actions would mirror the current timeout.

While I haven't formed feelings on #278 yet (asap!) I do strongly feel that the current timeout behaviour should be the default for holding, as it is the one that most users want --- or at least should be steered towards. Even if other hold behaviours might make sense depending on your level of comfort with the dark arts, interruptable timeouts as they are now are the only ones that can make for a natural typing experience even without conscious fine-tuning and maniacal cackling on the part of the user.

(By the way, it just occurred to me that both timeouts can also be combined: timeout(action1, hold-timeout, action2[, idle-timeout, action3]). I'm not necessarily advocating for that yet but it might be easier to reason about, preserves current syntax and avoids the contentious topic of naming)

@herrsimon
Copy link
Contributor Author

While I haven't formed feelings on #278 yet (asap!) I do strongly feel that the current timeout behaviour should be the default for holding, as it is the one that most users want --- or at least should be steered towards.

While it might be true that most users actually want the current timeout mechanism, I see no objective reason to steer them towards a specific interrupt behaviour. It depends a lot on the actual use case, and overloading a modifier key like shift is very different from overloading a letter key.

interruptable timeouts as they are now are the only ones that can make for a natural typing experience even without conscious fine-tuning and maniacal cackling on the part of the user.

Well, here I again think that many people would disagree with you. But even assuming that interruptable timeouts should be preferred, why should choosing the first action on interrupt be more natural than choosing the second action? A slowly typing person, only creating isolated keypresses without any rolls, might actually find it more natural that something like f = timeout(f, 200, layer(shift)) makes f act like shift as soon as it is interrupted by any other key.

(By the way, it just occurred to me that both timeouts can also be combined: timeout(action1, hold-timeout, action2[, idle-timeout, action3]). I'm not necessarily advocating for that yet but it might be easier to reason about, preserves current syntax and avoids the contentious topic of naming)

Combining the current timeout with the planned timeout2 can unfortunately not cover all use cases (see the answer at #278 I'm going to write soon).

However, as @rvaiya wrote, let's continue the discussion about timeout-hold over at #278 and focus on the idle behaviour here.

@rvaiya
Copy link
Owner

rvaiya commented Oct 8, 2022

After some additional thought I decided to add a global oneshot_timeout option. Feel free to open another issue if you feel this is insufficient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants