Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions plugins/Kaleidoscope-Qukeys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`
Expand Down
33 changes: 33 additions & 0 deletions plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines +197 to +205

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears to me that this clause should be outside of the parent condition, similar to the spacecadet check at L190

next_keypress_index = i;
}
continue;
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -210,13 +229,21 @@ 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};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be possible (and maybe better) to use the driver data instead of hard coding a number that just so happens to work for two of Keyboardio's keyboards. I think this should work:

Suggested change
uint8_t split_column_{8};
uint8_t split_column_ = kaleidoscope::Device::Props::KeyScannerProps::matrix_columns / 2;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try it out and let you know!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try it out and let you know!


// Internal helper methods.
bool processQueue();
void flushEvent(Key event_key);
bool isQukey(KeyAddr k);
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 {
Expand Down