manna-harbour / qmk_firmware

See the "forkreadme" branch or the following link for a description of branches maintained in this fork.
https://github.com/manna-harbour/qmk_firmware/blob/forkreadme/readme.org
GNU General Public License v2.0
281 stars 64 forks source link

endgame: multi-mod crossover chords and typing streaks #56

Open sunaku opened 1 year ago

sunaku commented 1 year ago

This patch enhances home row mods & chords handling to avoid accidental misfires for a more natural typing experience. :relieved::raised_hands: Don't be dismayed by GitHub's display of "10K+ commits" and "5K+ files changed" in this PR: πŸ’ refer to the raw diff instead.

:star:Note: I've created a Vial version of this PR as well as a native QMK version of this PR to make it easy for users to try it out. :gift:

This PR supersedes #48 and #54 by implementing configurable multi-mod chord taps with uni/bilateral handling, delayed mods to solve #27, "eager mods" for mod-click mouse usage, and typing streaks for a natural typing rhythm. :star_struck: See the Flowchart below for a visualization of this PR's logic (see my article for its motivation) and the Documentation below for each #define.

Usage #

Here is the relevant portion of my config.h file which activates these features to provide the best typing experience I've felt since switching to Miryoku's home row mods 2+ years ago, as detailed in Taming home row mods with Bilateral Combinations:

/* QMK */
#define TAPPING_TERM 200
#define IGNORE_MOD_TAP_INTERRUPT /* for rolling on mod-tap keys */

/* Miryoku */
#define BILATERAL_COMBINATIONS_LIMIT_CHORD_TO_N_KEYS 4 /* GUI, Alt, Ctrl, Shift */
#define BILATERAL_COMBINATIONS_DELAY_MODS_THAT_MATCH MOD_MASK_GUI
#define BILATERAL_COMBINATIONS_DELAY_MATCHED_MODS_BY 120  /* ms */
#define BILATERAL_COMBINATIONS_ALLOW_CROSSOVER_AFTER 80   /* ms */
#define BILATERAL_COMBINATIONS_ALLOW_SAMESIDED_AFTER 3000 /* ms */
#define BILATERAL_COMBINATIONS_TYPING_STREAK_TIMEOUT 160  /* ms */
#define BILATERAL_COMBINATIONS_TYPING_STREAK_MODMASK (~MOD_MASK_SHIFT)

You also need to add the following line to your rules.mk file to enable QMK's deferred execution for delayed mod activation:

DEFERRED_EXEC_ENABLE = yes

Tutorial #

Clone a fresh copy of QMK and merge this PR into it (or skip down to the git remote command if you already have a clone):

$ git clone https://github.com/qmk/qmk_firmware --recurse-submodules --shallow-submodules
$ cd qmk_firmware
$ git remote add sunaku https://github.com/sunaku/qmk_firmware.git
$ git fetch sunaku miryoku_bilateral
$ git merge sunaku/miryoku_bilateral --no-edit

Now follow the instructions in the Usage section above:

  1. Add the provided snippet to your keymap's specific config.h file.
  2. Add the provided snippet to your keymap's specific rules.mk file.

Finally, build your keymap's specific firmware using make or QMK toolbox as usual.

Flowchart #

flowchart

Three main user actions drive the logic: tapping, holding, and releasing keys. Everything happens depending on what keys they hold and how long they hold them.

Documentation #

To enable bilateral combinations:

  1. Add the following line to your config.h file:
#define BILATERAL_COMBINATIONS
  1. Add the following line to your rules.mk file to enable QMK's deferred execution facility.
DEFERRED_EXEC_ENABLE = yes

To enable same-sided combinations (which start on one side of the keyboard and end on the same side, such as RSFT_T(KC_J) and RCTL_T(KC_K) in the abbreviation "jk" which stands for "just kidding"), add the following line to your config.h and define a value: hold times greater than that value will permit same-sided combinations. For example, if you typed RSFT_T(KC_J) and RCTL_T(KC_K) faster than the defined value, the keys KC_J and KC_K would be sent to the computer. In contrast, if you typed slower than the defined value, the keys RSFT(KC_K) would be sent to the computer.

#define BILATERAL_COMBINATIONS_ALLOW_SAMESIDED_AFTER 500

To enable crossover bilateral combinations (which start on one side of the keyboard and cross over to the other side, such as RSFT_T(KC_J) and LGUI_T(KC_A) in the word "jam"), add the following line to your config.h and define a value: hold times greater than that value will permit crossover bilateral combinations. For example, if you typed RSFT_T(KC_J) and LGUI_T(KC_A) faster than the defined value, the keys KC_J and KC_A would be sent to the computer. In contrast, if you typed slower than the defined value, the keys RSFT(KC_A) would be sent to the computer.

#define BILATERAL_COMBINATIONS_ALLOW_CROSSOVER_AFTER 75

To delay the registration of certain modifiers (such as KC_LGUI and KC_RGUI, which are considered to be "flashing mods" because they suddenly "flash" or pop up the "Start Menu" in Microsoft Windows) during bilateral combinations, you can define a BILATERAL_COMBINATIONS_DELAY_MODS_THAT_MATCH setting specifying which modifiers should be delayed, and a BILATERAL_COMBINATIONS_DELAY_MATCHED_MODS_BY setting specifying how long that delay (measured in milliseconds) should be.

  1. Add the following line to your config.h and define a bitwise mask that matches the modifiers you want to delay. For example, here we are defining the mask to only match the GUI and ALT modifiers.
#define BILATERAL_COMBINATIONS_DELAY_MODS_THAT_MATCH (MOD_MASK_GUI|MOD_MASK_ALT) /* GUI and ALT modifiers */
  1. Add the following line to your config.h and define a timeout value (measured in milliseconds) that specifies how long modifiers matched by BILATERAL_COMBINATIONS_DELAY_MODS_THAT_MATCH should be delayed. For example, here we are defining the timeout to be 100 milliseconds long.
#define BILATERAL_COMBINATIONS_DELAY_MATCHED_MODS_BY 100

To suppress mod-tap holds within a typing streak, add the following line to your config.h and define a timeout value: a typing streak ends when this much time passes after the last key in the streak is tapped. Until such time has passed, mod-tap holds are converted into regular taps. The default value of this definition is 0, which disables this feature entirely. Overall, this feature is similar in spirit to ZMK's global-quick-tap feature.

#define BILATERAL_COMBINATIONS_TYPING_STREAK_TIMEOUT 175

If you wish to target only certain modifiers (instead of all possible modifiers) for the typing streak timeout setting described above, add the following line to your config.h and define a bit mask: only those modifiers that match this mask will be governed by the typing streak timeout. For example, to exempt Shift modifiers from the typing streak timeout while still targeting all other modifiers, you can specify the following mask.

#define BILATERAL_COMBINATIONS_TYPING_STREAK_MODMASK (~MOD_MASK_SHIFT)

To monitor activations in the background, enable debugging, enable the console, enable terminal bell, add #define DEBUG_ACTION to config.h, and use something like the following shell command line:

hid_listen | sed -u 's/BILATERAL_COMBINATIONS: change/&\a/g'

Commits #

Although this PR is polluted with irrelevant commit history (which was merged in from the newer QMK release 0.18.6+ that provides the deferred execution facility needed by this PR), you can filter out the noise by looking only at my own commits. :bowtie:

sunaku commented 1 year ago

I have fixed some corner cases, simplified the configuration, improved chording support, and upgraded to QMK 0.19.10. Eager mods are now enabled by default (so that mod-clicks Just Work out of the box) but you can delay them via #define settings.

For example, here is a diff showing how my personal configuration settings have changed since I originally submitted this PR:

-#define BILATERAL_COMBINATIONS_EAGERMODS 1
-#define BILATERAL_COMBINATIONS_EAGERMASK (~MOD_MASK_GUI)
-#define BILATERAL_COMBINATIONS_DEFERMODS 100
-#define BILATERAL_COMBINATIONS_CROSSOVER 75
-#define BILATERAL_COMBINATIONS_SAMESIDED 3000
-#define BILATERAL_COMBINATIONS_CHORDSIZE 4 // one side GUI, Alt, Shift, Control
+#define BILATERAL_COMBINATIONS_DELAY_MODS_THAT_MATCH MOD_MASK_GUI
+#define BILATERAL_COMBINATIONS_DELAY_MATCHED_MODS_BY 100
+#define BILATERAL_COMBINATIONS_ALLOW_CROSSOVER_AFTER 75
+#define BILATERAL_COMBINATIONS_ALLOW_SAMESIDED_AFTER 3000

I have also updated the original description of this PR (located at the top of this page) with this new information accordingly.

sunaku commented 1 year ago

Inspired by ZMK's global-quick-tap feature, I've implemented a typing streak timeout setting that suppresses home row mods while actively typing:

#define BILATERAL_COMBINATIONS_TYPING_STREAK_TIMEOUT 160  /* ms */

However, this tends to obstruct the Shift modifier when typing parentheses or punctuation marks such as ! and ? at the end of a sentence; and it requires a dedicated Shift key as a workaround, per @urob's "timeless" mods for ZMK. So I went further and exempted Shift modifiers from typing streaks in a bitwise mask:

#define BILATERAL_COMBINATIONS_TYPING_STREAK_MODMASK (~MOD_MASK_SHIFT)

With all this, typing feels natural again! 🀯 No more unconscious fears about accidentally triggering home row mods. 😌 It’s a complete game changer! 🀩

sunaku commented 1 year ago

I've added a Tutorial section to this PR's description to make it easier for newcomers to try this out from scratch, as follows. Although this long-lived PR may seem complex, the underlying patch for this PR (compared to QMK mainline) is much simpler.

Clone a fresh copy of QMK and merge this PR (or skip down to the git remote command if you already have a QMK clone):

$ git clone https://github.com/qmk/qmk_firmware --recurse-submodules --shallow-submodules
$ cd qmk_firmware
$ git remote add sunaku https://github.com/sunaku/qmk_firmware.git
$ git fetch sunaku miryoku_bilateral
$ git merge sunaku/miryoku_bilateral --no-edit

Add this line to your keyboard's specific rules.mk file:

DEFERRED_EXEC_ENABLE = yes

Add this snippet to your keyboard's specific config.h file:

/* QMK */
#define TAPPING_TERM 200
#define IGNORE_MOD_TAP_INTERRUPT /* for rolling on mod-tap keys */

/* Miryoku */
#define BILATERAL_COMBINATIONS_LIMIT_CHORD_TO_N_KEYS 4 /* GUI, Alt, Ctrl, Shift */
#define BILATERAL_COMBINATIONS_DELAY_MODS_THAT_MATCH MOD_MASK_GUI
#define BILATERAL_COMBINATIONS_DELAY_MATCHED_MODS_BY 120  /* ms */
#define BILATERAL_COMBINATIONS_ALLOW_CROSSOVER_AFTER 80   /* ms */
#define BILATERAL_COMBINATIONS_ALLOW_SAMESIDED_AFTER 3000 /* ms */
#define BILATERAL_COMBINATIONS_TYPING_STREAK_TIMEOUT 160  /* ms */
#define BILATERAL_COMBINATIONS_TYPING_STREAK_MODMASK (~MOD_MASK_SHIFT)

Finally, build your keyboard's specific firmware using QMK toolbox, as usual.

sunaku commented 1 year ago

I'm also maintaining a Vial version of this PR as an alternative for those who can't use the native QMK version of this PR. :gift:

third774 commented 1 year ago

Thank you so much for sharing this! I've run into one minor annoyance that you might already be aware of. I'm using PERMISSIVE_HOLD together with IGNORE_MOD_TAP_INTERRUPT, and it seems like all the normal nested taps are working correctly, but the nested tap of Shift + ' is not sending " as expected. I need to actually wait the full TAPPING_TERM in order to do the double quote. Here's my full config.h file:

// Copyright 2019 Manna Harbour
// https://github.com/manna-harbour/miryoku

// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.

#pragma once

#include "custom_config.h"

// default but used in macros
#undef TAPPING_TERM
// 200ms = 60wpm
#define TAPPING_TERM 200

// Prevent normal rollover on alphas from accidentally triggering mods.
#define IGNORE_MOD_TAP_INTERRUPT
// Allow nested taps to register instantly
#define PERMISSIVE_HOLD

// Enable rapid switch from tap to hold, disables double tap hold auto-repeat.
#define QUICK_TAP_TERM 0

// Auto Shift
#define NO_AUTO_SHIFT_ALPHA
#define AUTO_SHIFT_TIMEOUT TAPPING_TERM
#define AUTO_SHIFT_NO_SETUP

// Mouse key speed and acceleration.
#undef MOUSEKEY_DELAY
#define MOUSEKEY_DELAY          0
#undef MOUSEKEY_INTERVAL
#define MOUSEKEY_INTERVAL       16
#undef MOUSEKEY_WHEEL_DELAY
#define MOUSEKEY_WHEEL_DELAY    0
#undef MOUSEKEY_MAX_SPEED
#define MOUSEKEY_MAX_SPEED      6
#undef MOUSEKEY_TIME_TO_MAX
#define MOUSEKEY_TIME_TO_MAX    64

#define BILATERAL_COMBINATIONS_LIMIT_CHORD_TO_N_KEYS 4 /* GUI, Alt, Ctrl, Shift */
// Must be > 0, but don't want this limited really at all
#define BILATERAL_COMBINATIONS_ALLOW_CROSSOVER_AFTER 1   /* ms */
#define BILATERAL_COMBINATIONS_ALLOW_SAMESIDED_AFTER 3000 /* ms */
// 200ms = 60wpm
#define BILATERAL_COMBINATIONS_TYPING_STREAK_TIMEOUT 200  /* ms */
#define BILATERAL_COMBINATIONS_TYPING_STREAK_MODMASK (~MOD_MASK_SHIFT)
#define BILATERAL_COMBINATIONS

// Thumb Combos
#if defined (MIRYOKU_KLUDGE_THUMBCOMBOS)
  #define COMBO_COUNT 8
  #define COMBO_TERM 200
  #define EXTRA_SHORT_COMBOS
#endif

Please let me know if you're not able to reproduce the issue, happy to share more info if needed!

EDIT: Curiously, the same also applies to Shift+, for <, but not Shift+. for > or Shift+/ for ?.

sunaku commented 1 year ago

Interesting, I can't imagine why only a subset of shifted combinations would be affected. Try disabling the AutoShift feature?

third774 commented 1 year ago

Ah, that fixed it! πŸ™πŸ»

third774 commented 1 year ago

There's one other minor annoyance I've come across that might be more directly related to the bilateral combinators. If I hold ⌘ and press tab a few times, then decide I want to also hold ⇧ in addition to (but without releasing) ⌘ while pressing tab to go backwards in the quick switcher, the ⇧ key is not registering at all. I wondered if it was because it was within the BILATERAL_COMBINATIONS_ALLOW_SAMESIDED_AFTER but after holding shift for 3 seconds it doesn't seem to matter, the ⇧ key press is still not recognized. I also tried compiling without #define BILATERAL_COMBINATIONS_TYPING_STREAK_MODMASK (~MOD_MASK_SHIFT), but that didn't seem to matter either.

If I remove all of the BILATERAL_COMBINATIONS* definitions though, it works as expected.

If this never gets fixed I will just live with it, this typing experience is that good! 🀩

Thanks again for making it!

sunaku commented 1 year ago

I was able to reproduce your issue: since your configuration didn't define it, a default catch-all value was being supplied for BILATERAL_COMBINATIONS_DELAY_MODS_THAT_MATCH that wrongfully matched all modifiers. Fixed now in commit a92f39e9e79787ac6ae8a48a262c7b4f7e78261b.

third774 commented 1 year ago

Amazing! Thanks again! πŸ₯°

sagepeppermint commented 1 year ago

Hi, i'm having the issue you've mentioned in https://github.com/manna-harbour/qmk_firmware/pull/48#issuecomment-1275291610 where holding a mod-tap button i still have to wait for the tapping term timeout before i can ctrl+click. Is that intended ?

LeonB commented 1 year ago

Anyone else having this issue? I have MT(MOD_LCTL, KC_ESC) on capslock and in combination with this patch when holding the capslock key + A for example ALSO sends the escape keycode:

Screenshot 2023-07-13 at 17 06 40
sunaku commented 1 year ago

holding a mod-tap button i still have to wait for the tapping term timeout before i can ctrl+click

That's correct: modifiers are activated only after holding down for the initial TAPPING_TERM, and then the same-sided and crossover timeouts are stacked on top of TAPPING_TERM. Notably, the TAPPING_TERM still governs how long you need to hold a mod-tap key in order to make QMK trigger its hold behavior (as opposed to its tap behavior). Thereafter, my enhancement kicks in and performs further processing---so it's stacked on top of the already expended TAPPING_TERM.

For example, imagine that TAPPING_TERM was 1 and SAMESIDED was 2 and CROSSOVER was 4. Then, to activate same-sided mods you would need to hold the mod-tap key for 1 + 2 = 3 milliseconds. Similarly, to activate crossover mods, you would need to hold the mod-tap key for 1 + 4 = 5 milliseconds. However, note that the mod is eagerly applied as soon as the mod-tap key is held for 1ms --- meaning that there's no extra waiting for mod activation. This allows for fast mod-click mouse usage, such as Shift-clicking multiple items in a file manager.

Sigvah commented 1 year ago

Nice! Just started to use it, works well so far! but I think you should add #define BILATERAL_COMBINATIONS to usage where you show your config.

hilsonp commented 1 year ago

Hello,

Thank you for your work.

Unfortunatelly, the merge fails on my fresh qmk clone...

$ git merge sunaku/miryoku_bilateral --no-edit
Auto-merging docs/tap_hold.md
Auto-merging quantum/action.c
CONFLICT (content): Merge conflict in quantum/action.c
Automatic merge failed; fix conflicts and then commit the result.

Probably an upstream update that conflicts. Could you pull this change ?

Thank you !

christoph-cullmann commented 10 months ago

Hi, have you interest in try to upstream that to QMK? I think that would be appreciated by a lot of people.

nstetter commented 9 months ago

I very much support the notion of upstreaming this feature. Is there anything that would help you in this regard?

LarssonMartin1998 commented 8 months ago

Is there any way that I can restrict the bilinear combinations from some keys? For instance, I want to be able to press my tab key when holding down both of my alt keycodes, but right now I can only do it whilst holding down my right alt.

sunaku commented 8 months ago

Hey everyone, thanks for your interest in upstreaming this patch to QMK -- I'm short on free time (which is otherwise preoccupied with my ZMK port of this home row mods disambiguation logic, specifically for my new keyboard) so I hope to revisit this by the summer. Cheers.

sunaku commented 8 months ago

Is there any way that I can restrict the bilinear combinations from some keys? For instance, I want to be able to press my tab key when holding down both of my alt keycodes, but right now I can only do it whilst holding down my right alt.

Yes, you should be able to edit the bilateral_combinations_left() function body (or the calls to it) to handle your scenario.

BlueDrink9 commented 8 months ago

fyi the current merge conflict is trivial to resolve. Just need to delete the conflict markers to resolve. IGNORE_MOD_TAP_INTERRUPT no longer exists in upstream qmk. It is now default. My branch fixes it https://github.com/sunaku/qmk_firmware/pull/1

BlueDrink9 commented 7 months ago

People coming across this thread may also be interested in Achordion for a user-space library (patchless/standard QMK) with similar goals to this patch.

isaac-8601 commented 6 months ago

I'm noticing, on Dvorak, that I can't use the left-side shift to type capital D, B, or F. However, using the right-side shift for those keys works. It's as if those keys are interpreted as being on the left side of the keyboard. I don't have this issue with I, X, or Y combined with right-side shift.

autoferrit commented 4 months ago

any chance getting this more updated per @BlueDrink9 's comment? https://github.com/manna-harbour/qmk_firmware/pull/56#issuecomment-1963051827

BlueDrink9 commented 4 months ago

it's easy as to do yourself fyi. Alternatively, I've found achordion a really good replacement