Open jtroo opened 2 years ago
Related issue in keyberon for additional inspiration: https://github.com/TeXitoi/keyberon/issues/35
@SignSpice here's the issue, for your interest
Cool!
For me I am looking to unify my mac and linux boxes, so I'll work on the macOS support first.
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.
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 https://github.com/cyxae/kanata/commit/27d23711e9226f652da2e47a6e2e642f2a0f5c8d
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.
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 !
I updated my code as you proposed, here is the potential PR https://github.com/jtroo/kanata/compare/main...cyxae:kanata:main
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 atap-hold-except-keys
will outputfq
.
I like the idea to have an easier fast-typing-control (easier than https://github.com/jtroo/kanata/issues/502#issuecomment-1779875881 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))
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 ^^
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.
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).
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
It's certainly possible 🙂.
The code that parses tap-hold-release-keys
is here:
For context on what seems to be the motivation for handling on release, as an interesting read:
@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?
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.
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
@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.,
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
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.
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)
)
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.
https://github.com/jtroo/kanata/issues/502
The solution I wrote - and still use - doesn't check for two taps but instead any tap-activation instead of hold-activation.
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)
Insipration: https://github.com/kmonad/kmonad/issues/351
Interesting code:
keyberon's
HoldTapConfig::Custom
https://github.com/jtroo/keyberon/blob/d3f529a797122d45758574e8f2b7b0daef29cdb4/src/action.rs#L74keyberon's
Stacked
https://github.com/jtroo/keyberon/blob/d3f529a797122d45758574e8f2b7b0daef29cdb4/src/layout.rs#L476keyberon's
Event
https://github.com/jtroo/keyberon/blob/d3f529a797122d45758574e8f2b7b0daef29cdb4/src/layout.rs#L69The function passed into custom has the following info:
since
event
OsCode
being pressed - thej
index ofevent
maps toOsCode
.