jtroo / kanata

Improve keyboard comfort and usability with advanced customization
GNU Lesser General Public License v3.0
2.86k stars 124 forks source link

Act on a key combo/chord (vs. outputting a key combo/chord) #99

Closed br-lemes closed 1 year ago

br-lemes commented 2 years ago

The Artsey.io keyboard is based on combos, pressing two or more keys at the same time to produce another. For example, pressing a and r keys produces f.

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?

jtroo commented 2 years 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
)
jtroo commented 2 years ago

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.

br-lemes commented 2 years ago

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?

br-lemes commented 2 years ago

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.

jtroo commented 2 years ago

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.

jtroo commented 2 years ago

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 {
Johni0702 commented 1 year ago

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:

mini-ARTSEY ```lisp ;; ARTSEY MINI 0.2 https://github.com/artseyio/artsey/issues/7 (defsrc q w e a s d ) (deflayer base (chord base A) (chord base R) (chord base T) (chord base S) (chord base E) (chord base Y) ) (deflayer meta (chord meta A) (chord meta R) (chord meta T) (chord meta S) (chord meta E) (chord meta Y) ) (defchords base 200 (A R T S E Y) (layer-switch meta) (A R T ) (one-shot 2000 lsft) ( S E Y) spc (A ) a ( R T S ) b ( R S ) c (A E Y) d ( E ) e (A R ) f (A E ) g ( S Y) h ( R E ) i ( T S E ) j ( T E ) k ( S E ) l ( R T ) m ( E Y) n (A S ) o (A R Y) p ( T Y) q ( R ) r ( S ) s ( T ) t (A T ) u (A T E ) v ( T S ) w (A Y) x ( Y) y ( R S E ) z ) (defchords meta 200 (A R T S E Y) (layer-switch base) ( S E Y) spc (A R T ) caps ;; TODO should technically be shift lock, not caps; probably need to use fake keys for that? (A R ) bspc ( R T ) del ( S E ) C-c ( E Y) C-v (A ) home ( R ) up ( T ) end ( S ) left ( E ) down ( Y) rght ) ```
jtroo commented 1 year ago

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 🙂

mklcp commented 1 year ago

@Johni0702 Any updates? This is a killer-feature for me and your proposal looks really promising.

nightscape commented 1 year ago

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/

mklcp commented 1 year ago

This is possible with multi I think, at least in KMonad it's possible

gerhard-h commented 1 year ago
(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 commented 1 year ago

@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.

br-lemes commented 1 year ago

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)?

jtroo commented 1 year ago

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
                });
            }
jtroo commented 1 year ago

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.

jtroo commented 1 year ago

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.

br-lemes commented 1 year ago

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.

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.

jtroo commented 1 year ago

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!

br-lemes commented 1 year ago

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.

jtroo commented 1 year ago

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 🙂

mklcp commented 1 year ago

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:

  1. 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.

  2. 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.

jtroo commented 1 year ago

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.


Regarding the proposal corresponding to the first config

Config of first proposal: click to expand ``` (defsrc a s l m) (deflayer test 1 2 3 4) (defchords (test sym nav) 200 (a s) lctl (l m) lctl ) ```

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.


Regarding the proposal corresponding to the second config

Config of second proposal: click to expand ``` (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 ) ```

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.

mklcp commented 1 year ago

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.

mklcp commented 1 year ago

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.

br-lemes commented 1 year ago

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.

jtroo commented 1 year ago

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.

https://github.com/jtroo/kanata/blob/707f38560b1a14e8cc4b99ff343d898fc3254c4b/keyberon/src/layout.rs#L428

mklcp commented 1 year ago

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.

jtroo commented 1 year ago

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.

mklcp commented 1 year ago

Yeah that's the problem, for more than a single key it doesn't really make sense

xsrvmy commented 1 year ago

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.

jtroo commented 1 year ago

@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:

xsrvmy commented 1 year ago

I think QMK does the first option. But in the mean time the second option is enough to make combo-heavy layouts more viable.

Zireael07 commented 1 year ago

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.

jtroo commented 1 year ago

@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:

Config example ``` (defcfg sequence-input-mode visible-backspaced ) (defalias n1 (multi sldr (macro 1 kp1)) n2 (multi sldr (macro 1 kp2)) ;; ... ) (defsrc kp1 kp2 ) (deflayer base @n1 @n2 ) (deffakekeys hello (macro h e l l o) bye (macro b y e) ) (defseq hello (kp1 kp1 kp1) bye (kp1 kp2 kp2) ) ```

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.

Zireael07 commented 1 year ago

Yes, three taps of numpad 1 equal entering A, that kind of a thing.

jtroo commented 1 year ago

@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.

jtroo commented 1 year ago

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.