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

Expand the support for custom tap-hold function #128

Open
jtroo opened this issue Sep 3, 2022 · 24 comments
Open

Expand the support for custom tap-hold function #128

jtroo opened this issue Sep 3, 2022 · 24 comments
Labels
enhancement New feature or request PRs welcome jtroo has no plans to work on this at present, but PRs are welcome

Comments

@jtroo
Copy link
Owner

jtroo commented Sep 3, 2022

Insipration: kmonad/kmonad#351

Interesting code:

keyberon's HoldTapConfig::Custom
https://github.com/jtroo/keyberon/blob/d3f529a797122d45758574e8f2b7b0daef29cdb4/src/action.rs#L74

keyberon's Stacked
https://github.com/jtroo/keyberon/blob/d3f529a797122d45758574e8f2b7b0daef29cdb4/src/layout.rs#L476

keyberon's Event
https://github.com/jtroo/keyberon/blob/d3f529a797122d45758574e8f2b7b0daef29cdb4/src/layout.rs#L69

The function passed into custom has the following info:

  • timing (ms) of events since the hold-tap key was pressed with since
  • the up/down status of the key, in event
  • the OsCode being pressed - the j index of event maps to OsCode.
@jtroo jtroo added the enhancement New feature or request label Sep 3, 2022
@jtroo
Copy link
Owner Author

jtroo commented Sep 3, 2022

Related issue in keyberon for additional inspiration:
TeXitoi/keyberon#35

@jtroo
Copy link
Owner Author

jtroo commented Sep 3, 2022

@SignSpice here's the issue, for your interest

@SignSpice
Copy link

Cool!

For me I am looking to unify my mac and linux boxes, so I'll work on the macOS support first.

@jtroo jtroo added the PRs welcome jtroo has no plans to work on this at present, but PRs are welcome label Nov 18, 2022
@jtroo jtroo removed the PRs welcome jtroo has no plans to work on this at present, but PRs are welcome label Mar 13, 2023
@jtroo
Copy link
Owner Author

jtroo commented Mar 13, 2023

I added some support for this in #343, which is in release 1.3.0-prerelease-2. It only makes use of key information right now; no customizable timing information. However, having the existing example code with all the type system trickery being done should help others play around with the code if desired.

@jtroo jtroo added the PRs welcome jtroo has no plans to work on this at present, but PRs are welcome label Mar 18, 2023
@jtroo jtroo changed the title Add support for custom tap-hold function Expand the support for custom tap-hold function Mar 18, 2023
@cyxae
Copy link
Contributor

cyxae commented Dec 6, 2023

Hi!

An idea that was suggested to me (which apparently is available in ZMK), is to allow to specify a blacklist for a tap-hold key.

Indeed, to avoid rolling errors, I don’t want my left Ctrl homerow mod to be triggered with keys on the left side of the keyboard, but only with the ones on the right side.

This could look like this:

  ;; Home-row mods
  ;; Must be hold long enough (200ms) to become a modifier.
  ss (tap-hold-except-keys 200 200 s lmet (q w e r t a s d f g z x c v b))
  dd (tap-hold-except-keys 200 200 d lalt (q w e r t a s d f g z x c v b))
  ff (tap-hold-except-keys 200 200 f lctl (q w e r t a s d f g z x c v b))
  jj (tap-hold-except-keys 200 200 j rctl (y u i o p h j k l ; n m , . /))
  kk (tap-hold-except-keys 200 200 k lalt (y u i o p h j k l ; n m , . /))
  ll (tap-hold-except-keys 200 200 l rmet (y u i o p h j k l ; n m , . /))

So whatever is the state of the timeout, if I press F and then Q it outputs fq and not Ctrl+Q

I don’t know Rust, so I’ve made a very sketchy and buggy implementation of what I want, but if someone is interested there you go cyxae@27d2371

@jtroo
Copy link
Owner Author

jtroo commented Dec 6, 2023

Hi @cyxae, the action you've added sounds suspiciously similar to the existing tap-hold-release-keys functionality, but I guess the difference is there is that no early hold activation like with tap-hold-release.

There is definitely room for simplifying, but thanks for sharing the code! If you'd like to polish it up and make a PR, the big thing I would change is to see how parse_tap_hold_release_keys and https://github.com/jtroo/kanata/blob/main/parser/src/cfg/custom_tap_hold.rs work, and do it that way which should hopefully reduce the amount of code that needs to be added to accomodate the change.

@cyxae
Copy link
Contributor

cyxae commented Dec 7, 2023

Hi, thank you for the answer !

You got it right, it looks like tap-hold-release-keys but there is another trick in addition to the "no early hold activation". What we want is that even when the timeout expires, the tap hold key pressed behaves as a "tap" if followed by one of the mentioned keys.

Example:
If I press F long enough to reach the timeout, and then I press Q, if F is a tap-hold-release-keys it will output Ctrl+Q whereas a tap-hold-except-keys will output fq.

This implies that the program should not enter the Waiting::Timeout state before it knows what is the next key event, to be able to interpret it as a tap if it is a key in the list. Hence the hacky skip_timeout boolean !

@cyxae
Copy link
Contributor

cyxae commented Dec 7, 2023

I updated my code as you proposed, here is the potential PR main...cyxae:kanata:main

@gerhard-h
Copy link
Contributor

Example:
If I press F long enough to reach the timeout, and then I press Q, if F is a tap-hold-release-keys it will output Ctrl+Q whereas a tap-hold-except-keys will output fq.

I like the idea to have an easier fast-typing-control (easier than #502 (comment) I mean)

I just wonder, would "hold only F long" produce "f" or "fff..." or nothing? I asume nothing?

will f-down pause f-up q-tap still produce "fq"?

Is there a way to overwrite the "except" if I hold it for "very long"? (asuming most people expect that)
like (tap-hold-except-keys 200 200 f (tap-hold-except-keys 500 500 lctl (f)) (q w e r t a s d f g z x c v b))

@cyxae
Copy link
Contributor

cyxae commented Dec 8, 2023

If I hold only F long, it produces the hold behavior, that is to say Ctrl.

Mhm for now the goal is not to allow overwrite of the except if the key is held for very long, that may be another feature in the future ^^

@BlueDrink9
Copy link

BlueDrink9 commented Mar 17, 2024

I'd say sunku's blog covers the ultimate desirable behaviour for things like home-row mods, (as this discussion shows, a main use of custom tap-hold.)

If you haven't used his qmk patch, and you use home-row mods, I highly recommend trying it. Homerow mods Just Work, with no change in typing technique and very few accidental activations.

@gerhard-h
Copy link
Contributor

I'd say sunku's blog covers the ultimate desirable behaviour for things like home-row mods, (as this discussion shows, a main use of custom tap-hold.)

sunku's article is a realy interessting read. So even after reading I'm not sure he really covered all homerow mod issues, I think only trying out will tell. (I am missing the propercase problem where you get OZ instead of Oz bcause of holding/chording the Shift key)

The great thing about a solution like this sunku's is, it can all be configured in defcfg without messing with the rest of your keyboard config, wich is really user friendly.

Still it is worth mentioning even today, you can already achieve all/most of this with kanata by auto switching
to a fast-typing layer and swichtching back with on-fakekey-idle. (I had to fix one additional chord thereafter, but this might depend on personal typing speed).

@AmmarAbouZor
Copy link

I wanted to ask if there is a way to hold multiple home-row modifiers while using tap-hold-release-keys without having to wait for hold timeout. Currently if I want to press Ctrl+Shift for example I must press the shift home-row key and wait for its timeout to pass before I can press the Ctrl home-row key.

I think this could be solved if the decision to use the tap considered on the key release event instead of the key press one

@jtroo
Copy link
Owner Author

jtroo commented Mar 29, 2024

It's certainly possible 🙂.

The code that parses tap-hold-release-keys is here:

kanata/parser/src/cfg/mod.rs

Lines 1346 to 1348 in 8b665b3

TAP_HOLD_RELEASE_KEYS => {
parse_tap_hold_keys(&ac[1..], s, "release", custom_tap_hold_release)
}

pub(crate) fn custom_tap_hold_release(
keys: &[OsCode],
a: &Allocations,
) -> &'static (dyn Fn(QueuedIter) -> (Option<WaitingAction>, bool) + Send + Sync) {

@jtroo
Copy link
Owner Author

jtroo commented Mar 29, 2024

For context on what seems to be the motivation for handling on release, as an interesting read:

Taken from: https://github.com/urob/zmk-config#timeless-homerow-mods

Homerow mods (aka "HRMs") can be a game changer -- at least in theory. In practice, they require some finicky timing: In its most naive implementation, in order to produce a "mod", they must be held longer than tapping-term-ms. In order to produce a "tap", they must be held less than tapping-term-ms. This requires very consistent typing speeds that, alas, I do not possess. Hence my quest for a "timer-less" HRM setup.2

After months of tweaking, I eventually ended up with a HRM setup that is essentially timer-less, resulting in virtually no misfires. Yet it provides a fluent typing experience with mostly no delays.

Let's suppose for a moment we set tapping-term-ms to something ridiculously large, say 5 seconds. This makes the configuration timer-less of sorts. But it has two problems: (1) To activate a mod we will have to hold the HRM keys for what feels like eternity. (2) During regular typing, there are delays between the press of a key and the time it appears on the screen.3 Enter two of ZMK's best configuration options:

  • To address the first problem, I use ZMK's balanced flavor, which produces a "hold" if another key is both pressed and released within the tapping-term. Because that is exactly what I normally do with HRMs, there is virtually never a need to wait past my long tapping term (see below for two exceptions).
  • To address the typing delay, I use ZMK's require-prior-idle-ms property, which immediately resolves a HRM as "tap" when it is pressed shortly after another key has been tapped. This all but completely eliminates the delay.

This is great but there are still a few rough edges:

  • When rolling keys, I sometimes unintentionally end up with "nested" key sequences: key 1 down, key 2 down and up, key 1 up. Because of the balanced flavor, this would falsely register key 1 as a mod. As a remedy, I use ZMK's positional hold-tap feature to force HRMs to always resolve as "tap" when the next key is on the same side of the keyboard. Problem solved.
  • ... or at least almost. By default, positional-hold-tap performs the positional check when the next key is pressed. This is not ideal, because it prevents combining multiple modifiers on the same hand. To fix this, I use the hold-trigger-on-release setting, which delays the positional-hold-tap decision until the next key's release. With the setting, multiple mods can be combined when held, while I still get the benefit from positional-hold-tap when keys are tapped.
  • So far, nothing of the configuration depends on the duration of tapping-term-ms. In practice, there are two reasons why I don't set it to infinity:
    • Sometimes, in rare circumstances, I want to combine a mod with a alpha-key on the same hand (e.g., when using the mouse with the other hand). My positional hold-tap configuration prevents this within the tapping term. By setting the tapping term to something large but not crazy large (I use 280ms), I can still use same-hand mod + alpha shortcuts by holding the mod for just a little while before tapping the alpha-key.
    • Sometimes, I want to press a modifier without another key (e.g., on Windows, tapping Win opens the search menu). Because the balanced flavour only kicks in when another key is pressed, this also requires waiting past tapping-term-ms.
  • Finally, it is worth noting that this setup works best in combination with a dedicated shift for capitalization during normal typing (I like sticky-shift on a home-thumb). This is because shifting alphas is the one scenario where pressing a mod may conflict with require-prior-idle-ms, which may result in false negatives when typing fast.

@AmmarAbouZor
Copy link

AmmarAbouZor commented Mar 30, 2024

@jtroo I've modified the code to handle the early tap case on release event, postponing it on press event, to keep the behavior as it was for the normal case. Here is a code snippet:

        move |mut queued: QueuedIter| -> (Option<WaitingAction>, bool) {
            let match_key = |j: u16| keys.iter().copied().map(u16::from).any(|j2| j2 == j);

            while let Some(q) = queued.next() {
                if q.event().is_release() {
                    let (_, j) = q.event().coord();
                    // If any key matches the input on key release event, do a tap right away.
                    if match_key(j) {
                        return (Some(WaitingAction::Tap), false);
                    }
                }
                if q.event().is_press() {
                    let (i, j) = q.event().coord();
                    // If any key matches the input on key press, postpone taking decision to
                    // key release
                    if match_key(j) {
                        return (None, false);
                    }
                    // Otherwise do the PermissiveHold algorithm.
                    let target = Event::Release(i, j);
                    if queued.clone().copied().any(|q| q.event() == target) {
                        return (Some(WaitingAction::Hold), false);
                    }
                }
            }
            (None, false)
        },

I think this is working as expected, and I think I can use the same implementation in custom_tap_hold_except(...) method. I'll provide a PR for that. The question is which branch should be the target for my PR?

@jtroo
Copy link
Owner Author

jtroo commented Mar 30, 2024

By inspection, the code does not look correct to me. The match_key check in the press branch will always return early and the release branch will never execute.

It seems to me that the correct code is simply to delete:

                // If any key matches the input on key press, postpone taking decision to
                // key release
                if match_key(j) {
                    return (None, false);
                }

The main branch is the target branch for PRs.


As a note, please do not change the behaviour of existing actions and instead add new variants.

@AmmarAbouZor
Copy link

Sorry I had the hold-time variable set to a big number which led to false positive by this naive implementation. I'll try to provide a new one.

Very thanks for the information

@eugenesvk
Copy link
Contributor

@AmmarAbouZor btw, there is a potentially helpful way to test such changes described here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc#test-your-config

You define an input file with the key events, e.g.,

  • press home-row-Shift
  • wait time 0.1s (< holdout of 0.5 sec)
  • press home-row-Control
  • ...

and get the output of what kanata will translate this into (in this case as far as I understood your issue, the Control will not trigger a mod), and then you can tweak kanata and see whether your new function will trigger it properly

@sooheon
Copy link

sooheon commented Sep 20, 2024

Still it is worth mentioning even today, you can already achieve all/most of this with kanata by auto switching to a fast-typing layer and swichtching back with on-fakekey-idle. (I had to fix one additional chord thereafter, but this might depend on personal typing speed).

Can you point me to more detail on this? I want to switch to fast-typing layer when any two taps have registered within N ms, and then switch back to hold-receptive layer after timeout.

@gerhard-h
Copy link
Contributor

I'm sure there is already an extensive thread about this, but I couldn't find it.

here is the core of my config
in general just wrap every key x in (multi x @.tp) example is j

I did some extra rolls control to favor
Fd over FD and
fd over D
df over ctl+f

the (multi @.base rctl) instead of just rctl looks like overkill but I asume I had good a reason.

(defcfg
   process-unmapped-keys yes
   delegate-to-first-layer yes
)

(defvirtualkeys
  to-base (layer-switch base)
)


(defalias
  .base (layer-while-held base)

  ;; @.tp diables tap-hold keys if typing rapidly by switching to a layer without home row mods and upper case but restore after a shord idle period
  .tp (multi
    (one-shot 95 (layer-while-held typing))
    (on-idle  95 tap-virtualkey to-base )
  )
  .tpf (multi
    (one-shot 145 lsft)
  )
 
;; *** HOME ROW MODS *** 
  .f   (tap-hold 1 180 (fork f (multi f nop7 @.tpf) (rsft)) (multi nop6 lsft) ) 
  .j   (tap-hold 1 160 (multi j @.tp) rsft ) 

  .d   (multi (tap-hold 1 300 (fork (fork d spc (nop6)) (unicode d) (nop7)) (multi @.base lctl) ) @.tp) 
  .k   (multi (tap-hold 1 300 k (multi @.base rctl) ) @.tp)
) 
(defsrc  
     d f  j k                
)

(deflayer base
 @.d  @.f     @.j   @.k    
 @.nav
)
;; *** fast Typing layer - prevents Upper Case within words when typing fast ***
(deflayer typing
   (unmod d) (unmod f)  (unmod j) (unmod k)        
)

@jtroo
Copy link
Owner Author

jtroo commented Sep 20, 2024

Still it is worth mentioning even today, you can already achieve all/most of this with kanata by auto switching to a fast-typing layer and swichtching back with on-fakekey-idle. (I had to fix one additional chord thereafter, but this might depend on personal typing speed).

Can you point me to more detail on this? I want to switch to fast-typing layer when any two taps have registered within N ms, and then switch back to hold-receptive layer after timeout.

#502

The solution I wrote - and still use - doesn't check for two taps but instead any tap-activation instead of hold-activation.

@gerhard-h
Copy link
Contributor

#502 thats the post I wanted to refer to. Also found my old post #375 that explains why I perfer one-shot over layer-switch

@gerhard-h
Copy link
Contributor

I want to switch to fast-typing layer when any two taps have registered within N ms, and then switch back to hold-receptive layer after timeout.

you could have a virtual key that "counts" taps

(defvirtualkeys
  one nop1
)

counting done like this for a letter a tap

(switch
  (nop1) (multi a @.tp) break
  ()  (multi a (on-press press-virtualkey one)  (on-idle  95 release-virtualkey one ) ) break
)

and @.tp must now release the counter (also all your hold action should release the counter)

  .tp (multi
    (layer-switch typing)
    (on-press release-virtualkey one )
    (on-idle  95 tap-virtualkey to-base )
  )

I still wonder if waiting for a second tap is better than doing it on the first tap

also while "on-idle-to-base" works (on-idle 95 release-virtualkey one ) may never trigger because nop1 is pressed and the keyboard might not be considered idle
(in that case you could go for a three layer aproach: base-layer, same-as-base-after-one-tap, fast-typing)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request PRs welcome jtroo has no plans to work on this at present, but PRs are welcome
Projects
None yet
Development

No branches or pull requests

8 participants