jtroo / kanata

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

Feature request: Configurable combo timing #407

Closed LeonKowarschickKenbun closed 5 months ago

LeonKowarschickKenbun commented 1 year ago

Is your feature request related to a problem? Please describe. I use combos quite excessively in my layout, both for certain symbols (äöüß), character sequences (=>, ->, ::) as well as things like backspace. However, currently, I seem to missfire my combos quite frequently. I've gotten used to very tight combo timings on my QMK and ZMK boards, so I avoid accidental triggers of the combos quite well there

Describe the solution you'd like Having the combo timings, and potentially the general combo detection algorithm, be more configurable would be great. When hacking around with my own implementation of combos a few years ago, I noticed that for certain combos, my timing would be very different from what it was for others, even varying in terms of the exact "inner" timings (time between first to last keypress, time between each press, time between first release vs last release, order of releases compared to keydown, etc). Having those be configurable on a per-combo level would of course be optimal, although I'd already be happy with a higher level of configuration for combos globally.

Describe alternatives you've considered

Additional context Thanks for making Kanata, and thanks for supporting Combos, and even bigger thanks for having great error reporting!

gerhard-h commented 1 year ago

I use the the timings from tap-hold to make my chords tappy (tap-hold 1 140 @chordq q)

prescientmoon commented 1 year ago

Hi @gerhard-h, could you be more explicit about what the exact behaviour produced by your snippet is? I'm not sure I understand how the features interact together. What happens if q is the second key in the chord for example? Do you have to put the tap hold on all the members of the chord?

jtroo commented 1 year ago

The timing in kanata should be decently reliable, within +-1 real life ms for the configured delay, assuming it's not a macro of spamming keys as fast as possible, since that has a lot of overhead on the operating system side. However, if the system is older/weaker and CPU usage is very high, this might still have issues. You can see the implementation of the timing handling here:

https://github.com/jtroo/kanata/blob/8c66a313f7cbb2bf374b000610f31f2a187e264d/src/kanata/mod.rs#L379

As for configuring chords more, one could add a timeout value per chord instead of the one per-group.

https://github.com/jtroo/kanata/blob/8c66a313f7cbb2bf374b000610f31f2a187e264d/keyberon/src/action.rs#L250

Then one would need to process that accordingly here:

https://github.com/jtroo/kanata/blob/8c66a313f7cbb2bf374b000610f31f2a187e264d/keyberon/src/layout.rs#L460

Finally, modify the chord parsing in this file to accept the new timeout per chord:

https://github.com/jtroo/kanata/blob/8c66a313f7cbb2bf374b000610f31f2a187e264d/src/cfg/mod.rs#L1693

One could add a new, different defchords item that is parsed differently from the existing one, for backwards compatibility reasons, or change the parsing of the existing one somehow in a backwards-compatible way.

gerhard-h commented 1 year ago

could you be more explicit about what the exact behaviour produced by your snippet is?

my goal was to only trigger the cord if both key are tapped but not hold, because q w shall become ! " when hold.

My original chord timer was 300 the a tap-hold timer of 140 made them tappier ( the defacto chord timer was now also 140). So the tap-hold timer in (tap-hold 1 140 @chordq S-1) can be used to cancel a chord early.

I was happy with that, but after your question I realized: the obvious way to make a chord tappy is to reduce the chord timer. I wonder if asymetric tap-hold timings could be usefull to role into the chord from one side but not from the other.

(defalias
  chordq (chord qw q) 
  chordw (chord qw w) 
)
(defchords qw 20
  (q      ) q
  (   w   ) w
  (q  w  ) @layerfix
)

now using aliases q (tap-hold 1 140 @chordq S-1) and w (tap-hold 140 140 @chordw S-2) tap q: q hold q: ! tap q w: @layerfix hold q w: !" or "!

Vermoot commented 10 months ago

Seconding OP's request. I use some combos with few, close keys ((h e) / on Colemak-DH, index and middle finger) and some with more keys, more spread out ((w f e i) S-1).

You can imagine for a combo like (h e) / I need a pretty tight combo term (sorry, I'm using QMK terminology since I'm not yet super familiar with kanata) as to not trigger the combo when I type the word "he" (which is a roll). This tight combo term is easily doable with such close keys on two adjacent fingers.

For a combo like (w f e i) S-1 though, the keys are more spread out, and most importantly spread over my two hands. Hitting those four keys within the combo term requires it to be more forgiving (so, longer), which is not a problem because there's no way I could be accidentally hitting those four keys simultaneously by accident.

So I need a tight combo term for the first one and a longer term for the second one, but this is all on my base layer so as it stands I can't differentiate them from within my (defchords base 50 ... ) definition.

Fred-Vatin commented 6 months ago

USE CASE

I have an actual case and the solution that would be required to make the things flawless in my opinion.

Check that sample dummy config:

(defcfg
  process-unmapped-keys yes
)

(defvar
  chords-timeout 35
)

(defsrc
  1
  s l
)

(defalias
  cs (chord test s)
  cl (chord test l)
)

(defchords test $chords-timeout
  (s   ) s
  (   l) l
  (s  l) del
)

(deflayer test
  lrld
  @cs @cl
)

With the actual implementation, the setting limitation to $chords-timeout produces two cases when you write the word else quickly.

  1. If $chords-timeout is too high (>50) then you won’t be able to write else sometimes because chord is triggered while pressing l then s in quick sequence.
  2. If $chords-timeout is too low (<50) then you might misfire your combo (del).

In my case I chose to set $chords-timeout to 35 to avoid triggering combo when not wanted (writing else fast). The downside being I need to be very accurate when wanting to trigger the combo.

I think this alternative between either misfire the combo or triggering it when not wanted could be solved by an idle timeout. Let’s call it input-chord-timeout. If no key were pressed during the input-chord-timeout then “play the chord” else ignore it. I think it could be a defcfg option or a third argument for defchords. I guess defcfg option would be enough.

This solution would solve the main topic. My previous config could be then:

(defcfg
  process-unmapped-keys yes
  input-chord-timeout 200

)

(defvar
  chords-timeout 100
)

(defsrc
  1
  s l
)

(defalias
  cs (chord test s)
  cl (chord test l)
)

(defchords test $chords-timeout
  (s   ) s
  (   l) l
  (s  l) del
)

(deflayer test
  lrld
  @cs @cl
)

$chords-timeout 100 would be confortable to trigger the chord while input-chord-timeout 200 would prevent user from “playing the chord” while writing fast.

jtroo commented 6 months ago

@Fred-Vatin You might be interested to have a look at this comment thread. The solution isn't as simple as input-chord-timeout but it is also more general-case and I think enables the use case in the similar manner.

jtroo commented 5 months ago

Adjacently fixed by https://github.com/jtroo/kanata/pull/916