diff --git a/plugins/Kaleidoscope-Qukeys/README.md b/plugins/Kaleidoscope-Qukeys/README.md index d146ae95c9..2369b60835 100644 --- a/plugins/Kaleidoscope-Qukeys/README.md +++ b/plugins/Kaleidoscope-Qukeys/README.md @@ -141,6 +141,39 @@ likely to generate errors and out-of-order events. > > Defaults to `75` (milliseconds). +### `.setEnableOppositeHandsRule(enable)` + +> When set to `true`, qukeys will only resolve to their alternate (modifier) +> value when the subsequent key press is on the opposite hand from the qukey. +> Same-hand rollover will always produce the primary (tap) value, preventing +> accidental modifier activation during normal typing. This implements the +> "opposite hands rule" popularized by QMK's Achordion and Chordal Hold +> features. +> +> Hand membership is determined by physical key position (column index), not by +> the key's logical assignment. This means remapping keys via Chrysalis or +> EEPROM does not affect the hand detection. On split keyboards like the +> Keyboardio Model 01/100, keys with column index less than the split column +> (default 8) are considered left-hand keys. +> +> Note that holding a qukey alone past the hold timeout will still activate the +> alternate (modifier) value regardless of this setting. This is intentional, as +> it allows using a modifier with a pointing device (e.g. `shift` + `click`). +> +> Defaults to `false` (disabled). + +### `.setSplitColumn(column)` + +> Sets the column index that divides left-hand keys from right-hand keys when +> the opposite-hands rule is enabled. Keys with a column index less than this +> value are considered left-hand keys; keys with a column index greater than or +> equal to this value are considered right-hand keys. +> +> For the Keyboardio Model 01 and Model 100, the correct value is `8` (columns +> 0-7 are the left hand, columns 8-15 are the right hand). +> +> Defaults to `8`. + ### `.activate()` ### `.deactivate()` ### `.toggle()` diff --git a/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.cpp b/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.cpp index 0e256b0264..685d6721cc 100644 --- a/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.cpp +++ b/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.cpp @@ -194,6 +194,15 @@ bool Qukeys::processQueue() { // Otherwise, we've found a subsequent key press, so we record it for the // overlap comparison later, unless we've already done so. if (next_keypress_index == 0) { + // If the opposite-hands rule is enabled and this subsequent key is on + // the same hand as the qukey, immediately flush as primary. There's no + // need to wait for the overlap comparison because same-hand rollover + // is never treated as a modifier chord. + if (require_opposite_hands_ && + !oppositeHands(queue_head_addr, event_queue_.addr(i))) { + flushEvent(queue_head_.primary_key); + return true; + } next_keypress_index = i; } continue; @@ -218,6 +227,14 @@ bool Qukeys::processQueue() { flushEvent(event_key); return true; } + // If the opposite-hands rule is enabled, and the subsequent key is on + // the same hand as the qukey, skip the overlap delay and flush as + // primary immediately. Same-hand rollover is always treated as typing. + if (require_opposite_hands_ && + !oppositeHands(queue_head_addr, event_queue_.addr(next_keypress_index))) { + flushEvent(queue_head_.primary_key); + return true; + } // Now we know the qukey has been released, but we need to check to see if // it's release should continue to be delayed during rollover -- if the // subsequent key is released soon enough after the qukey is released, it @@ -246,6 +263,15 @@ bool Qukeys::processQueue() { // not a key press, there must be one in the queue, so it shouldn't be // necessary to confirm that `j` is a actually a key press. if (event_queue_.addr(j) == event_queue_.addr(i)) { + // If the opposite-hands rule is enabled, only allow the alternate + // (modifier) value when the subsequent key is on the opposite hand + // from the qukey. Same-hand rollover always produces the primary + // (tap) value. + if (require_opposite_hands_ && + !oppositeHands(queue_head_addr, event_queue_.addr(i))) { + flushEvent(queue_head_.primary_key); + return true; + } // Next, verify that enough time has passed after the qukey was pressed // to make it eligible for its alternate value. This helps faster // typists avoid unintended modifiers in the output. @@ -494,6 +520,13 @@ bool Qukeys::shouldWaitForTapRepeat() { // ----------------------------------------------------------------------------- +// Return true if the two key addresses are on opposite hands, based on the +// configured split column. This is used by the opposite-hands rule to prevent +// same-hand rollover from triggering modifiers. +bool Qukeys::oppositeHands(KeyAddr a, KeyAddr b) const { + return (a.col() < split_column_) != (b.col() < split_column_); +} + // This function is here to provide the test for a SpaceCadet-type qukey, which // is any Qukey that has a modifier (including layer shifts) as its primary // value. diff --git a/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.h b/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.h index 29b5024197..4ab5a4a284 100644 --- a/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.h +++ b/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.h @@ -141,6 +141,25 @@ class Qukeys : public kaleidoscope::Plugin { minimum_prior_interval_ = min_interval; } + // Enable or disable the opposite-hands rule for home row mods. When enabled, + // a qukey will only resolve to its alternate (modifier) value when the + // subsequent key is on the opposite hand. Same-hand rollover always produces + // the primary (tap) value, preventing accidental modifier activation during + // normal typing. The hand boundary is determined by column index: columns + // 0 through (split_column - 1) are the left hand, and columns split_column + // and above are the right hand. + void setEnableOppositeHandsRule(bool enable) { + require_opposite_hands_ = enable; + } + + // Set the column index that divides left and right hands. Keys with column + // index less than this value are considered left-hand keys. Default is 8, + // which is correct for Keyboardio Model 01/100 (4x16 matrix, columns 0-7 + // left, 8-15 right). + void setSplitColumn(uint8_t col) { + split_column_ = col; + } + // Function for defining the array of qukeys data (in PROGMEM). It's a // template function that takes as its sole argument an array reference of // size `_qukeys_count`, so there's no need to use `sizeof` to calculate the @@ -210,6 +229,13 @@ class Qukeys : public kaleidoscope::Plugin { Key alternate_key{Key_Transparent}; } queue_head_; + // When true, qukeys will only resolve to their alternate value when the + // subsequent keypress is on the opposite hand. + bool require_opposite_hands_{false}; + + // The column index that divides left and right hands. + uint8_t split_column_{8}; + // Internal helper methods. bool processQueue(); void flushEvent(Key event_key); @@ -217,6 +243,7 @@ class Qukeys : public kaleidoscope::Plugin { bool isDualUseKey(Key key); bool releaseDelayed(uint16_t overlap_start, uint16_t overlap_end) const; bool isKeyAddrInQueueBeforeIndex(KeyAddr k, uint8_t index) const; + bool oppositeHands(KeyAddr a, KeyAddr b) const; // Tap-repeat feature support. struct {