Closed br-lemes closed 1 year ago
Regarding the keyberon crate's README, I think the wording might be a bit confusing since I'm not aware of any functionality similar to artsey.io.
It probably intends to mean something more like: "press a single key to output a chord of multiple keys together".
Regarding the artsey.io keyboard though, seems like its functionality can be replicated with the tap-hold-*
actions if desired, though it would require more setup and may not have the exact same key order behaviour.
e.g. (untested)
(defsrc
a
r
)
(deflayer base
(tap-hold-release 200 200 a (layer-toggle chorded))
(tap-hold-release 200 200 r (layer-toggle chorded))
)
(deflayer chorded
f
f
)
Looking more into the artsey page, I think any chord that involves more than two keys might be challenging/impossible to replicate right now.
Seems like it could be possible for a program like kanata to have an "artsey" mode that can still wrap the keyberon state machine. I don't think the current processing loop can be easily adapted, so I would write a new processing loop.
Regarding the artsey.io keyboard though, seems like its functionality can be replicated with the
tap-hold-*
actions if desired, though it would require more setup and may not have the exact same key order behaviour.e.g. (untested)
(defsrc a r ) (deflayer base (tap-hold-release 200 200 a (layer-toggle chorded)) (tap-hold-release 200 200 r (layer-toggle chorded)) ) (deflayer chorded f f )
I hadn't thought about using layers to do this. And what I have in mind is a little more complicated than Artsey.io. Artsey.io only uses combos. I wanted to combine combos with tap-hold.
Tap d => d Hold d => Ctrl Tap f => f Hold f => Shift Tap d+f => g
I managed to do this behavior with kmonad
as follows and worked perfectly:
(defsrc d f)
(deflayer base
(tap-hold-next 50 d (layer-toggle g) :timeout-button (tap-hold-next-release 250 d lctl))
(tap-hold-next 50 f (layer-toggle g) :timeout-button (tap-hold-next-release 250 f lsft))
)
(deflayer g g g)
There is no :timeout-button
in the released version of kmonad
. Compiling kmonad
is a pain and I have a lot of other issues with kmonad
unfortunately.
As long as I know there is no such timeout in kanata
and nesting tap-hold would be a problem, am I right?
Looking more into the artsey page, I think any chord that involves more than two keys might be challenging/impossible to replicate right now.
My use case will be only two keys. I think it'll be complicated having lots of combos like Artsey.io.
Yea nested tap-hold is disallowed right now because the keyberon library doesn't handle the use case correctly from what I could tell.
I think something similar to timeout-button could be added to keyberon's alternate HoldTap behaviours without too much trouble, though it isn't supported today.
I tested re-enabling nested tap-hold just now and it seems like it may actually work as-is. I think the previously non-working cases were with nesting in the tap
position. It may not work exactly as it does in kmonad though.
config:
(defsrc
d f
)
(deflayer test
(tap-hold-press 200 200 d (tap-hold-release 200 200 (layer-toggle g) lctl))
(tap-hold-press 200 200 f (tap-hold-release 200 200 (layer-toggle g) lsft))
)
(deflayer g g g)
To test it yourself you can comment out this area:
let hold_action = parse_action(&ac_params[3], parsed_state)?;
// if matches!(tap_action, Action::HoldTap { .. }) || matches!(hold_action, Action::HoldTap { .. })
// {
// bail!("tap-hold is not allowed inside of tap-hold")
// }
Ok(sref(Action::HoldTap(sref(HoldTapAction {
Having dabbled with steno (via Plover) in the past, this feature has always been something I wanted, but modding firmware written in C or or kmonad written in Haskell has always been too high of a barrier for me. So, upon finding this awesome project, I couldn't help but start hacking on this (also gave me a good excuse to get back into Rust).
My initial use-case is being able to switch between 10 i3 workspaces with just four fingers (one for switching to my i3 layer and three for pressing the chord). Though I'm sure I'll find plenty of other cases where I can combine a bunch of mutually exclusive keys into more compact chords, so I'd want the ability to specify multiple independent groups of chords that may or may not overlay wrt physical keys (but on different layers).
Taking into account the above tap-hold example as well as my requirements and having skimmed the discussion in the corresponding kmonad issue, I figured the best way to implement it would be to allow the user to define multiple named chord groups and have a new action to trigger a specific key in a given group:
(defsrc q w)
(deflayer test (chord example a) (chord example b))
;; Example group of chords that
(defchords example 200
;; prints `a` when just `a` is pressed
(a) a
;; prints `c` when `a` and `b` are pressed together
(a b) c
)
Having a new action allows one to intuitively nest them inside tab-hold
and the like, as well as making it very clear how chord groups and keys are linked, i.e. you can easily see where a given group of chords is used and what a given key will do when pressed.
Though with the current implementation (and I don't see an easy way to change that) a small caveat applies: Nested actions are actually only relevant for the first key of a chord. Once a chord has been activated by the first key, it'll block processing of the event queue (very similar to how tab-hold and tab-dance do it) and consume events (as well as consider them to be part of the chord) for all physical keys that have a relevant chord
reference anywhere in their action-tree. I can't think of any use-case where you'd not want that though.
The result of the chord will be output as soon as you either press another non-chord key, or release a chord-key, or the timeout (200
in above example) expires, or once there is no more ambiguity (that is, there's an exact match for the keys you pressed and there isn't another chord that also shares those keys; e.g. if you press a
in the example, it won't immediately do a
because (a b)
also contains a
; once you additionally press b
, then there's only a single chord with that combination and so it will immediately trigger the output).
The result of the chord will stop being output once you release whichever key triggered the chord. I kind of want to change it to only stop once all keys have been released but that requires extra code and I can't think of any use-case to justify that.
If that approach sounds good, I'll clean the code up a bit and send a PR for it some day.
The tab-hold example from above (at least if I understood it correctly) looks like this and appears to function exactly as I'd expect:
(defchords dfg 200
(d) d
(f) f
(d f) g
)
(defalias
test-d (tap-hold 200 200 (chord dfg d) lctl)
test-f (tap-hold 200 200 (chord dfg f) lsft)
)
(defsrc d f)
(deflayer test @test-d @test-f)
And here's how you'd implement mini-artsey:
Very cool! The approach sounds good to me. Working similarly to tap-dance or tap-hold when starting a chord, and otherwise not interrupting processing, seems like a great approach.
Please be welcome to open a PR at your leisure, whenever you're ready 🙂
@Johni0702 Any updates? This is a killer-feature for me and your proposal looks really promising.
If it is possible to output multiple characters in sequence as a result, one could also implement chording as e.g. done in the fantastic (although Windows only) https://github.com/psoukie/zipchord/
This is possible with multi
I think, at least in KMonad it's possible
(defchords dfg 200
(d) d
(f) f
(d f) g
)
though defchords
would be very close to if-elseif
? A feature I already missed out on.
@Johni0702 Any updates? This is a killer-feature for me and your proposal looks really promising.
Oh, sorry about that. I was quite unhappy with some parts of the implementation but couldn't figure out anything better and then forgot about it. I've now opened #261 for it.
If it is possible to output multiple characters in sequence as a result, one could also implement chording as e.g. done in the fantastic (although Windows only) https://github.com/psoukie/zipchord/
You could totally do that (it can accept any of kanata's actions except for another chord
):
(defchords main 200
(o u) (multi y o u spc)
(c n) (multi c a n spc)
(o) o
(u) u
(c) c
(n) n
)
Though idk how zipchord deals with rolling, i.e. if you quickly tying something that contains o
and u
one after the other, you might not release o
before you press u
, and then it'd insert you
because it's technically a chord.
And you'd ofc still be missing all the other features like expanding ordered abbreviations, smart spacing, real-time hints, etc.
I tested your pull request and that's nice. But rolling is a problem. For example:
(defchords dfg 200 (d) d (f) f (d f) g ) (defalias test-d (tap-hold 200 200 (chord dfg d) lctl) test-f (tap-hold 200 200 (chord dfg f) lsft) ) (defsrc d f) (deflayer test @test-d @test-f)
If I try to type df -h
command what I got is g -h
. Even if I use 1 as timeout.
I use tap-hold-release
for home row mods because when rolling I release the first key before the second key. Maybe this works for combo/chord too (I mean the release order)?
If I try to type df -h command what I got is g -h. Even if I use 1 as timeout.
Haven't tested it, but I think the cause in the code is some unintended, emergent behaviour of combining the tap-hold and chord states. When the tap-hold
action completes, recognizing that it will do the tap action, it will then activate chord mode with d
. As soon as that happens, the code will see that f
has already pressed, so even with timeout being 1
, the code won't activate only the d
.
A way to resolve this issue would be to make use of delay
in:
pub struct WaitingState<T: 'static> {
// snipped
delay: u16,
// snipped
}
and since
in:
pub struct Stacked {
event: Event,
since: u16,
}
to modify the processing to handle this case correctly. The places to modify would probably be:
fn handle_chord(
&self,
config: &ChordsGroup<T>,
stacked: &mut Stack,
) -> Option<(WaitingAction, &'static Action<T>)> {
// snipped
}
and:
&Chords(chords) => {
self.tap_hold_tracker.coord = coord;
self.waiting = Some(WaitingState {
// snipped
timeout: chords.timeout, // maybe change to: timeout: chords.timeout.saturating_sub(delay), ?
// snipped
});
}
In Stacked
, since
is the number of ticks that a key press has remained unprocessed. This same number is used as delay
when that key press gets processed as an action, to save how long that key's processing was delayed in WaitingState
.
I believe the latest commit in the PR should fix the chord timing issue described. There is a different timing issue I've now come across though where d spc
outputs d
(space followed by d). Looks like this existed before the latest changes though, so it's not caused by them.
Edit: oh wait nevermind, it's because spc
wasn't mapped in defsrc
. Derp lol. Seems to all be working according to what I expect.
If I try to type df -h command what I got is g -h. Even if I use 1 as timeout.
Haven't tested it, but I think the cause in the code is some unintended, emergent behaviour of combining the tap-hold and chord states. When the
tap-hold
action completes, recognizing that it will do the tap action, it will then activate chord mode withd
. As soon as that happens, the code will see thatf
has already pressed, so even with timeout being1
, the code won't activate only thed
.
You're right. With the original pull request code if I don't use tap-hold
it works as expected. But with your new commits it's working with tap-hold
just fine.
But now I have another isssue. Consider this:
(defsrc q w f p)
(defchords leftUp 15
(f) (tap-hold-release 150 250 f (macro '))
(p) (tap-hold-release 150 250 p (macro [))
(f p) (tap-hold-release 150 250 b (macro 102d))
)
(defalias
q (tap-hold-release 150 250 q (macro esc))
w (tap-hold-release 150 250 w (macro `))
d (chord leftUp d)
f (chord leftUp f)
)
(deflayer test
@q @w @f @p
)
All works as expected.
tap f = f hold f = ' tap p = p hold p = [ tap f + p = b hold f + p = 102d
Rolling f and p outputs f and p.
The problem is rolling a key not in a chord and holding a key in a chord. As example, quick tap q and hold p get q and repeating p. As if a tap q and press-release-press p within the timeout.
The problem is rolling a key not in a chord and holding a key in a chord. As example, quick tap q and hold p get q and repeating p. As if a tap q and press-release-press p within the timeout.
I've added a new commit that should fix this issue @br-lemes 🙂. Works locally for what I've tested at least!
Great! That worked. In fact, this feature is working exceptionally well!
I was giving up this idea of mixing input chords (combos) with tap and hold thinking it wouldn't work at all or have a complicated usability. But it seems promising now.
Other than outstanding documentation questions, I was happy with the PR so it's been merged. Filed a different issue for potential documentation changes. I'll close this issue as completed in a few days if there's no more activity 🙂
Just bethinking how to improve this improvement:
As I understand it, this PR is based on the 3rd option of https://github.com/kmonad/kmonad/issues/157#issuecomment-774503161
This solution gives more granularity because each touch that should be part of a chord are specified layer-wise. However consider this example: I want to implement home-row modifiers via chording that is symmetrical. For example, I want that the chord a s
redirects to the Control key, and same for l m
(here the letters are understood as keycodes, based on a qwerty keyboard).
As input-chording is currently implemented, I need to do write:
(defsrc a s l m)
(deflayer home-control
(chord example a) (chord example s) (chord example l) (chord example m)
)
(defchords example 200
(a) a
(s) s
(a s) lctl
(l) l
(m) m
(l m) lctl
)
Which is redundant. Since in a defchords
block, the letters between parenthesis represent free variables, rather than keycodes, it would be more efficient to write:
(defsrc a s l m)
(deflayer home-control
(chord example x) (chord example y) (chord example x) (chord example y)
)
(defchords example 200
(x) a
(y) s
(x y) lctl
)
So (chord example x)
means that this key will access the x
variable in the chord example
.
But this doesn't work, because then the key of keycode l
outputs a
and m
outputs s
. But when used solo, we want them to output l
and m
.
This is not a big issue. My point is: the PR as it is doesn't make full use of being able to specify chord layer-wise on given keys (via the chord
keyword), as the defchords
block is too precise about which key to output when only one key of the chord is pressed. This is the problem illustrated above. Because
(defchords example 200
(x) a
(y) s
(x y) lctl
)
force to precise what happens when we press only one key of a chord, represented by the free variables x
and y
, i.e. a
and s
which is wrong if the chord is reused elsewhere, here on l
and m
So.. while this allows for more granularity, it doen't really to reuse chords elsewhere. But it painfully forces to use the chord
keyword to precise where the chord should be used.
This is not a big issue. It is not optimal though.
It could be simpler: If you want the defchords
keyword to operates solely on given layers, you could write, with a simple syntax:
(defsrc a s l m)
(deflayer test 1 2 3 4)
(defchords (test sym nav) 200
(a s) lctl
(l m) lctl
)
Here the defchords
block is different: it is not named anymore. Rather, its first argument specifies the name of the layers where it will act on (here test
, nav
, and sym
), and defines the chord via keys from defsrc
as input ("(a s l m)
" in the example above), that is, pressing (a s)
is a chord will output lctl
.
In the definition of the layer test
, 1
says that the a
keycode from defsrc
will redirect to the 1
keycode when pressed without being part of a chord. Same for 2
, 3
, 4
. No need to use the chord
keyword that calls the chord to be used on a given key.
Or it could allow more complexity while making more use of the chord
keyword: there is also the possibility to make this proposal more complex: in this case, both defchords
and chord
are kept, but now chord
takes an additional argument that will be the fallback when only one key of the chord is pressed.
Taking back my example of home-row control:
(defsrc a s l m)
(deflayer home-control
(chord example x a) (chord example y s) (chord example x l) (chord example y m)
)
(defchords example 200
(x y) lctl
)
(chord example x a)
means that the key will be part of the chord "example
", and will access the variable x
in this chord, while just outputing the key a
if the chord is not activated. Here the l
key correctly outputs l
while accessing the example
chord when used with m
.
Just to be clear, I'm not saying this PR should be changed. I'm really happy that kanata now has chording. Just trying to imagine the different possibilities.
The only advantages I can think the code of Johni0702 has are:
It allows to specify complex chords all in one place, e.g. in the mini-ARTSEY, all of the implementation happens in the defchords
blocks. On the contrary, with the "simpler" option I described, the 1-key cases are moved to deflayer
block, e.g. it is not need to write (A ) a
in the defchords
block.
The "simpler" option I described makes use of defsrc
keys, so if I wanted to implement the mini-artsey with this option, I would have written
(defsrc
q w e
a s d
)
(deflayer base
a r t
s e y
)
(deflayer meta
home up end
left down rght
)
(defchords (base) 200
(q w e a s d) (layer-switch meta)
(q w e ) (one-shot 2000 lsft)
( a s d) spc
( w e a ) b
( w a ) c
(q s d) d
(q w ) f
(q s ) g
( a d) h
( w s ) i
( e a s ) j
( e s ) k
( a s ) l
( w e ) m
( s d) n
(q a ) o
(q w d) p
( e d) q
(q e ) u
(q e s ) v
( e a ) w
(q d) x
( w a s ) z
)
(defchords (meta) 200
(q w e a s d) (layer-switch base)
( a s d) spc
(q w e ) caps ;; todo should technically be shift lock, not caps; probably need to use fake keys fow that?
(q w ) bspc
( w e ) del
( a s ) c-c
( s d) c-v
)
This is uglier, but just adding aliases inside defchords
could solve this.
Thanks for sharing your thoughts mklcp!
Your proposals to remove some redundancy in the syntax are interesting, though I'm not sure that they're a clear win.
I don't think that this proposal composes well when used with multi-function keys, e.g. combining with tap-hold
. Here the chording is implicit, and one can't choose to do chording on e.g. only the hold action of a tap-hold
.
The explicit chording here is better, so that the chord action can be nested inside of a different action like tap-hold
. If one desired, I guess xx
could be used to say that a key action should not do anything on its own. I think both this proposal and the existing functionality are equally expressive.
For my preference, I like the existing functionality more, with the reasoning that it feels cleaner from a design perspective. In the existing functionality, the chord action acts only on a chord group, which makes sense to me. With the second proposal, a chord action acts on a chord group and also has an associated backup action, which I like less. The redundancy improvement of your proposal is well noted though. One could implement a chord-v2
action that implements your proposed behaviour, and have it alongside the current chord
action, to play around with. The underlying implementation would support both, I think. The chord-v2
syntax would need additional logic to use the existing code, but it should be possible, I think (without having thought too hard about it). The existing syntax maps very closely to how it's implemented in the keyberon library's state machine.
I don't think that this proposal composes well when used with multi-function keys, e.g. combining with tap-hold. Here the chording is implicit, and one can't choose to do chording on e.g. only the hold action of a tap-hold.
Makes sense !
As for the second proposal, the main use is to have less duplicated code.
Which can also be solved by having full-blown lisp macros.
And not really a pressing issue anyway.
Furthermore I agree with you that the current version is cleaner.
I found another use of this proposal: it solves https://github.com/kmonad/kmonad/issues/487
The issue is that some non-programmable keyboards have fancy keys that don't have a dedicated keycode. The example described in the kmonad issue is a microsoft keyboard that has an office key that emits KEY_LEFTCTRL + KEY_LEFTALT + KEY_LEFTMETA + KEY_LEFTSHIFT at the same time (under 1ms).
I've tested one of those with showkey
, evtest
, xev
and even kanata
, and it seems like the keyboard is hardwired to redirect a press of this office key to a press of the 4 modifiers.
But thanks to input-chords / combos, it's possible to create a chord that acts on lctl+lalt+lmet+lsft.
Here is a code that I've tested successfully.
(defcfg
)
(defsrc
lsft lctl lmet lalt spc
)
(deflayer base
@cS @cC @cM @cA @cs
)
(defchords m 10
(S) lsft
(C) lctl
(M) lmet
(A) lalt
(s) spc
(C S A M) C-v ;; The office key presses lmet, lalt, lsft and lctl under 1ms (this can be seen using kanata --debug)
(C S A M s) prnt ;; However, the emoji key presses lmet+lalt+lsft+lctl under 1ms, then spc under 10ms, so the timeout was chosen to 10ms
)
(defalias
cC (chord m C)
cA (chord m A)
cS (chord m S)
cM (chord m M)
cs (chord m s)
)
This remaps the office key to C-v
and the emoji key to prnt
.
The problem is rolling a key not in a chord and holding a key in a chord. As example, quick tap q and hold p get q and repeating p. As if a tap q and press-release-press p within the timeout.
I've added a new commit that should fix this issue @br-lemes slightly_smiling_face. Works locally for what I've tested at least!
Although this commit solved the main issue here, I still think that considering the key releasing order helps to mitigate the rolling issue. If I use less than 15 of defchords timeout I get some wanted combos not being recognized as combos and at the same time have some rolling recognized as combos. I have no such issue with tap-hold-release because when rolling I release the first key before the second key and that's not considered a hold. Would be useful if I have an option which when releasing the first key before the second key it's not considered a combo as well.
That doesn't sound like it's in the "spirit" of key combos, though I do understand the use case.
You can try playing around with the code linked below to see what feels right, and the behaviour could be customized accordingly when you find something that works.
For my preference, I like the existing functionality more, with the reasoning that it feels cleaner from a design perspective. In the existing functionality, the chord action acts only on a chord group, which makes sense to me. With the second proposal, a chord action acts on a chord group and also has an associated backup action, which I like less. The redundancy improvement of your proposal is well noted though.
Going back on this second proposal, would it make sense to you to allow _
in defchords
?
e.g.
(defsrc w e i o)
(deflayer base
(chord example w)
(chord example e)
(chord example w)
(chord example e)
)
(defchords example 20
(w) _
(e) _
(w e) bspc
)
So that you can use (chord example w)
on w
and e
key to get bspc
when pressed together, but w
and e
when pressed separately. So that this chord can be reused elsewhere and still have the underlying key, so pressing i
will output i
, o
will output o
, but i
and o
simultaneously will outbut bspc
.
would it make sense to you to allow _ in defchords
It could be possible. One point against it is inconsistency - to me the _
doesn't seem like it would make much sense in chords involving more than a single key. Or maybe it does make sense - I'm not sure how it should be interpreted though.
Yeah that's the problem, for more than a single key it doesn't really make sense
Another problem I'm getting atm is that if an undefined combo is hit, the result is no keycode being sent. The more sensible behaviour is to try and break down the combo and send in keydown order.
@xsrvmy
It's a reasonable ask that an undefined chord try to be resolved to something that has an output by using its constituent parts. Your suggestion is one option, so it's good to be precise about what your suggestion means and evaluate it against other options.
For example, let's say we have a chord group with keys (h j k l)
. The full set (h j k l)
is not defined with an action, but the user has pressed all of h, j, k, l in the listed order, so now kanata needs to break down the combo. How should it work?
The options I've thought of:
l
is figuratively released, and if (h j k)
is a valid chord, that action will activate
(l)
by itself is a valid that then activates after (h j k)
is finished(h j k)
is not a chord, instead activate (h j)
which is a valid chord.(k l)
together, and if not, evaluate (k)
, then (l)
(h)
, (j)
, (k)
, (l)
I think QMK does the first option. But in the mean time the second option is enough to make combo-heavy layouts more viable.
Just to be clear, this allows chords such as (num1 num1 num1 a) and (num1 num2 num2 b) and (num2 num2 num2 c) - you get the idea? My friend has written a custom program (Windows API so Windows only) that basically lets him use the numpad to enter text, T9 style, and we've been looking for YEARS for ways to make his thing multi-platform.
@Zireael07 with your example (num1 num1 num1 a)
, does that mean 3 taps of num1
(the numpad version of 1)? The chording described in this issue is for pressing multiple keys at the same time, so I don't think it's what you're describing.
However, it sounds like what you want could be possible with other features.
The feature I have in mind is sequences.
There's also some discussion in this issue about that that may be of interest:
In particular, you can do something like this:
This gives me the thought that there may need to be another sequence input mode visible-always-backspaced
that backspaces even when an invalid sequence is found. In any case, this is a bit off topic, but I would be happy to continue the topic in a new issue or discussion.
Yes, three taps of numpad 1 equal entering A, that kind of a thing.
@xsrvmy The PR #339 has a change to improve unmapped chord handling, if you want to try it out. I haven't tested it in practice myself yet, but going by the tests at least, seems to be working as intended.
Having done a few iterations on this, I think it's ok to close this since the feature is implemented.
For more fixes or improvements, let's go with new discussions/issues.
The Artsey.io keyboard is based on combos, pressing two or more keys at the same time to produce another. For example, pressing
a
andr
keys producesf
.The
keyberon
read-me lists among others features: "Chording multiple keys together to act as a single key".Is this feature the same idea used by Artsey.io? Is it possible to do the same with
kanata
?