Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 10,000 commits into
base: bilateral-combinations
Choose a base branch
from

Conversation

sunaku
Copy link

@sunaku sunaku commented Nov 19, 2022

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

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

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

  • The green hexagons are external events representing a user's actions.
  • The blue ovals are a chord's main stages of life: begin, extend, end.
  • The pink folders are #define settings, documented individually below.
  • The yellow components are QMK functions, which I treat as system calls.
  • The gray boxes are internal storage mutations that track state changes.

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
Copy link
Author

sunaku commented Jan 29, 2023

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.

  • Removed BILATERAL_COMBINATIONS_CHORDSIZE setting since it's now hard-coded to the maximum allowed value in QMK.
  • Removed BILATERAL_COMBINATIONS_EAGERMODS setting since "eager mods" are now always enabled by default.
  • Renamed BILATERAL_COMBINATIONS_EAGERMASK setting to BILATERAL_COMBINATIONS_DELAY_MODS_THAT_MATCH and thereby inverted its meaning (it now specifies modifiers that should be delayed, not made eager).
  • Renamed BILATERAL_COMBINATIONS_DEFERMODS setting to BILATERAL_COMBINATIONS_DELAY_MATCHED_MODS_BY.
  • Renamed BILATERAL_COMBINATIONS_CROSSOVER setting to BILATERAL_COMBINATIONS_ALLOW_CROSSOVER_AFTER.
  • Renamed BILATERAL_COMBINATIONS_SAMESIDED setting to BILATERAL_COMBINATIONS_ALLOW_SAMESIDED_AFTER.

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 sunaku changed the title endgame: typing streak and multi-mod crossover chords endgame: multi-mod crossover chords and typing streaks Mar 10, 2023
@third774
Copy link

third774 commented May 8, 2023

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
Copy link
Author

sunaku commented May 9, 2023

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

third774 added a commit to third774/qmk_firmware that referenced this pull request May 9, 2023
This was causing issues with nested taps for " and <
manna-harbour#56 (comment)
@third774
Copy link

third774 commented May 9, 2023

Ah, that fixed it! 🙏🏻

@third774
Copy link

third774 commented May 10, 2023

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
Copy link
Author

sunaku commented May 18, 2023

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

@third774
Copy link

Amazing! Thanks again! 🥰

@sagepeppermint
Copy link

Hi, i'm having the issue you've mentioned in #48 (comment) 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
Copy link

LeonB commented Jul 13, 2023

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
Copy link
Author

sunaku commented Jul 25, 2023

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.

finrod09 added a commit to finrod09/qmk_userspace that referenced this pull request Sep 28, 2023
finrod09 added a commit to finrod09/qmk_userspace that referenced this pull request Sep 28, 2023
@Sigvah
Copy link

Sigvah commented Oct 19, 2023

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
Copy link

hilsonp commented Nov 6, 2023

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 !

JakubJatczak added a commit to JakubJatczak/qmk that referenced this pull request Dec 16, 2023
@christoph-cullmann
Copy link

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

@nstetter
Copy link

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

@LarssonMartin1998
Copy link

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
Copy link
Author

sunaku commented Feb 23, 2024

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
Copy link
Author

sunaku commented Feb 23, 2024

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
Copy link

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 sunaku#1

@BlueDrink9
Copy link

BlueDrink9 commented Mar 19, 2024

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
Copy link

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.

matthewtodd added a commit to matthewtodd/qmk_firmware that referenced this pull request May 5, 2024
@autoferrit
Copy link

any chance getting this more updated per @BlueDrink9 's comment? #56 (comment)

@BlueDrink9
Copy link

BlueDrink9 commented Jun 21, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet