Skip to content

Commit ced3c92

Browse files
tompassarelliclaude
andcommitted
tap-hold-release-order: add buffer parameter for fast typing grace period
Key presses within the buffer (ms) after the hold-tap key press are ignored by release-order logic, resolving as Tap regardless of release order. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a248108 commit ced3c92

File tree

3 files changed

+25
-14
lines changed

3 files changed

+25
-14
lines changed

keyberon/src/action.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ pub enum HoldTapConfig<'a> {
6464
/// Resolves based on release order after both keys are down.
6565
/// If the other key releases first (modifier still held) → Hold.
6666
/// If the modifier releases first (other key still held) → Tap.
67-
/// No timers, no thresholds — purely event-driven.
68-
ReleaseOrder,
67+
/// The buffer field specifies a grace period in ticks (ms) after the
68+
/// initial press during which release-order logic is ignored and fast
69+
/// typing will resolve as Tap.
70+
ReleaseOrder { buffer: u16 },
6971
/// If there is a press and release of another key, the hold
7072
/// action is activated.
7173
///
@@ -105,7 +107,7 @@ impl Debug for HoldTapConfig<'_> {
105107
match self {
106108
HoldTapConfig::Default => f.write_str("Default"),
107109
HoldTapConfig::HoldOnOtherKeyPress => f.write_str("HoldOnOtherKeyPress"),
108-
HoldTapConfig::ReleaseOrder => f.write_str("ReleaseOrder"),
110+
HoldTapConfig::ReleaseOrder { .. } => f.write_str("ReleaseOrder"),
109111
HoldTapConfig::PermissiveHold => f.write_str("PermissiveHold"),
110112
HoldTapConfig::Custom(_) => f.write_str("Custom"),
111113
}
@@ -119,7 +121,7 @@ impl PartialEq for HoldTapConfig<'_> {
119121
(HoldTapConfig::Default, HoldTapConfig::Default)
120122
| (HoldTapConfig::HoldOnOtherKeyPress, HoldTapConfig::HoldOnOtherKeyPress)
121123
| (HoldTapConfig::PermissiveHold, HoldTapConfig::PermissiveHold) => true,
122-
(HoldTapConfig::ReleaseOrder, HoldTapConfig::ReleaseOrder) => true,
124+
(HoldTapConfig::ReleaseOrder { .. }, HoldTapConfig::ReleaseOrder { .. }) => true,
123125
_ => false,
124126
}
125127
}

keyberon/src/layout.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -544,13 +544,21 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> {
544544
return Some(WaitingAction::Hold);
545545
}
546546
}
547-
HoldTapConfig::ReleaseOrder => {
547+
HoldTapConfig::ReleaseOrder { buffer } => {
548548
// Like PermissiveHold: if another key was pressed AND released
549549
// (while modifier is still held), resolve as Hold.
550550
// If modifier is released first, the fallthrough below handles Tap.
551+
//
552+
// Buffer: key presses that occurred within `buffer` ticks of the
553+
// hold-tap key press are ignored by release-order logic, allowing
554+
// fast typing to resolve as Tap regardless of release order.
551555
let mut queued = queued.iter();
552556
while let Some(q) = queued.next() {
553557
if q.event.is_press() {
558+
let press_tick = self.ticks.saturating_sub(q.since);
559+
if press_tick < buffer {
560+
continue;
561+
}
554562
let (i, j) = q.event.coord();
555563
let target = Event::Release(i, j);
556564
if queued.clone().any(|q| q.event == target) {
@@ -2535,7 +2543,7 @@ mod test {
25352543
hold: k(LAlt),
25362544
timeout_action: k(Space),
25372545
tap: k(Space),
2538-
config: HoldTapConfig::ReleaseOrder,
2546+
config: HoldTapConfig::ReleaseOrder { buffer: 0 },
25392547
tap_hold_interval: 0,
25402548
}),
25412549
k(Enter),
@@ -2566,7 +2574,7 @@ mod test {
25662574
hold: k(LAlt),
25672575
timeout_action: k(Space),
25682576
tap: k(Space),
2569-
config: HoldTapConfig::ReleaseOrder,
2577+
config: HoldTapConfig::ReleaseOrder { buffer: 0 },
25702578
tap_hold_interval: 0,
25712579
}),
25722580
k(Enter),
@@ -2603,7 +2611,7 @@ mod test {
26032611
hold: k(LAlt),
26042612
timeout_action: k(Space),
26052613
tap: k(Space),
2606-
config: HoldTapConfig::ReleaseOrder,
2614+
config: HoldTapConfig::ReleaseOrder { buffer: 0 },
26072615
tap_hold_interval: 0,
26082616
}),
26092617
k(Enter),

parser/src/cfg/tap_hold.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,21 +98,22 @@ pub(crate) fn parse_tap_hold_release_order(
9898
ac_params: &[SExpr],
9999
s: &ParserState,
100100
) -> Result<&'static KanataAction> {
101-
if ac_params.len() != 2 {
101+
if ac_params.len() != 3 {
102102
bail!(
103-
r"tap-hold-release-order expects 2 items after it, got {}.
103+
r"tap-hold-release-order expects 3 items after it, got {}.
104104
Params in order:
105-
<tap-action> <hold-action>",
105+
<buffer-ms> <tap-action> <hold-action>",
106106
ac_params.len(),
107107
)
108108
}
109-
let tap_action = parse_action(&ac_params[0], s)?;
110-
let hold_action = parse_action(&ac_params[1], s)?;
109+
let buffer = parse_u16(&ac_params[0], s, "buffer")?;
110+
let tap_action = parse_action(&ac_params[1], s)?;
111+
let hold_action = parse_action(&ac_params[2], s)?;
111112
if matches!(tap_action, Action::HoldTap { .. }) {
112113
bail!("tap-hold does not work in the tap-action of tap-hold")
113114
}
114115
Ok(s.a.sref(Action::HoldTap(s.a.sref(HoldTapAction {
115-
config: HoldTapConfig::ReleaseOrder,
116+
config: HoldTapConfig::ReleaseOrder { buffer },
116117
tap_hold_interval: 0,
117118
timeout: u16::MAX,
118119
tap: *tap_action,

0 commit comments

Comments
 (0)