joshgoebel / keyszer

a smart, flexible keymapper for X11 (a fork/reboot of xkeysnail )
Other
69 stars 15 forks source link

Support double taps #126

Open 0jrp0 opened 1 year ago

0jrp0 commented 1 year ago

Is your request related to a specific problem you're having?

I'm trying to move my vim usage to be more centered on the keyboard and stop jumping up to ESC to leave insert mode.

The solution you'd prefer or feature you'd like to see added...

I would like to map caps lock to ctrl and then map a double tap of caps lock to the escape key. It wasn't clear from the docs whether keyszer supported double taps.

Any alternative solutions you considered...

I am trying to move to ctrl-c in the meantime. But it's been hard since I keep missing the long press of control and typing 'c' inadvertently.

joshgoebel commented 1 year ago

and stop jumping up to ESC to leave insert mode.

What about jk, which I know a lot of people use?

then map a double tap of caps lock to the escape key

We don't currently have any concept of multiple keypresses constituting a single "event", such as a double or triple tap, etc. That's a nifty idea though.

You could possibly build something like this though by hand with a keymap (and custom function with internal state) - but you'd have to use caps lock as a KEY, not a mod - since keys are the only things you can track discrete presses of in a keymap.

It's possible I'm thinking of #92 and things I could imagine but haven't implementer... since to do this now you'd need to capture ALL taps of caps lock and then (if a double tap was detected based on timing, etc) you'd have to switch to a nested keymap that applied CTRL to whatever key you might hit next.

RedBearAK commented 1 year ago

You could possibly build something like this though by hand with a keymap (and custom function with internal state)

I thought this would be fun/educational thing to try and proof-of-concept with a custom function in my config. I was wrong. Well, partially.

It's not too difficult to use global variables to save the state of when the key was tapped and emit a key (or combo) only when a specified key is "double-tapped" within a certain time period. But the trouble comes when you want to retain a single-tap function for the same key, while also watching for double-taps. You can do an expiring while loop, but the function has to return before the next key press could affect a global variable. So the next key press can't affect the initial key press loop.

This feels like something that will need an asynchronous type of function that can run in the background after the first tap, watching for the second (or third, fourth, fifth) tap, and then emit the single-tap result only if the defined time interval passes without another tap of the same key.

I'm envisioning a syntax somewhat similar to bind, with a keyword like multitap. But more like a function that accepts multiple parameters.

C("CapsLock"): multitap( C("1"), C("2"), C("3"), C("4"), C("5") )
or...
C("CapsLock"): multitap( C("1"), None, C("3"), None, C("5") )
or... 
C("CapsLock"): multitap( C("x"), C("y") )

So for each parameter in order, that would be the key/combo that gets emitted for N taps.

There is an optional accessibility feature in Windows to activate "sticky keys" by pressing Shift five times. So there is precedence for a quintuple-tap function, where you really want to avoid triggering it accidentally.

As far as the time interval, it would need to be configurable, but the minimum that won't give you a hand cramp is about 0.2s, while a "relaxed" interval is more like 0.3s per tap.

In the case of defining only a single-tap function and a quintuple-tap function for the same key (example below), the single-tap function would still need to be delayed for at least the double-tap interval, and probably should be discarded entirely if the user double/triple/quadruple-taps, even if they don't go all the way to the quintuple-tap.

C("CapsLock"): multitap( C("1"), None, None, None, C("5") )

I'm certainly interested to see this kind of functionality added, but at the moment it's beyond my abilities to implement (beyond the simple case of only supporting the double-tap-within-interval, with no single-tap function possible).

RedBearAK commented 1 year ago

@0jrp0

Here is a working "dumb" function that will detect a double-tap and return an output key (or combo, or macro) only if the key is double-tapped within a reasonable time window. The function is of limited usefulness if you want to retain a single-tap function for the key (as discussed above). The key you use will not have any single-tap function (when the chosen keymap is active).

I have managed to make it resilient against taps that are too slow, or too fast (as in a held key that starts repeating). But how effective that is will depend on your keyboard's repeat delay and repeat rate setting. If the delay setting is too short, there will be one possibly unwanted "double-tap" output as the key starts rapidly repeating. But under normal circumstances it should work as expected to give a key a function only when double-tapped.


tapTime1 = time.time()
tapInterval = 0.24
tapCount = 0
last_dt_combo = None

def isDoubleTap(dt_combo):
    def _isDoubleTap():
        global tapTime1
        global tapInterval
        global tapCount
        global last_dt_combo
        _tapTime = time.time()
        # This first "if" block has a logic defect, if a different key in the
        # same keymap is also set up to send the same "dt_combo" value.
        if tapCount == 1 and last_dt_combo != dt_combo:
            debug(f'## isDoubleTap: \n\tDifferent combo: \n\t{last_dt_combo, dt_combo=}')
            last_dt_combo = None
            tapCount = 0
        # 2nd tap beyond time interval? Treat as new double-tap cycle.
        if tapCount == 1 and _tapTime - tapTime1 >= tapInterval:
            debug(f'## isDoubleTap: \n\tTime diff (too long): \n\t{_tapTime - tapTime1=}')
            tapCount = 0
        # Try to keep held key from producing repeats of dt_combo.
        # If repeat rate very slow or delay very short, this won't work well. 
        if tapCount == 1 and _tapTime - tapTime1 < 0.07:
            debug(f'## isDoubleTap: \n\tTime diff (too short): \n\t{_tapTime - tapTime1=}')
            tapCount = 0
            return None
        # 2nd tap within interval window? Reset cycle & send dt_combo.
        if tapCount == 1 and _tapTime - tapTime1 < tapInterval:
            debug(f'## isDoubleTap: \n\tTime diff (just right): \n\t{_tapTime - tapTime1=}')
            tapCount = 0
            tapTime1 = 0.0
            return dt_combo
        # New cycle? Set count = 1, tapTime1 = now. Send nothing. 
        if tapCount == 0:
            debug(f'## isDoubleTap: \n\tTime diff (1st cycle): \n\t{_tapTime - tapTime1=}')
            last_dt_combo = dt_combo
            tapCount = 1
            tapTime1 = _tapTime
            return None
    return _isDoubleTap

keymap("GNOME Text Editor", {
    C("CapsLock"):              isDoubleTap([C("2"),C("Enter")]),               # CapsLock double-tap = "2", Enter
    C("Grave"):                 isDoubleTap([C("Shift-X"),C("Enter")]),         # Grave double-tap = Capital "X", Enter
}, when = lambda ctx: ctx.wm_class.casefold() == "gnome-text-editor")

If you want a CapsLock double-tap to be Ctrl+C, it would be like this:

    C("CapsLock"):              isDoubleTap( C("LC-c") ),               # CapsLock double-tap = Ctrl+C

Place the variables and function before the start of the first keymap and you should be able to call it with a key from within any keymap.