Skip to content

Commit 5f4b8b9

Browse files
committed
rename to tap-hold-order, add require-prior-idle support, add multi-key test, add comments
1 parent 280fe44 commit 5f4b8b9

File tree

7 files changed

+125
-19
lines changed

7 files changed

+125
-19
lines changed

cfg_samples/kanata.kbd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,12 @@ If you need help, please feel welcome to ask in the GitHub discussions.
570570
;; This is useful for home row mods where fast typing should not trigger modifiers.
571571
utk (tap-hold-tap-keys 200 200 u @msc (a o e))
572572

573+
;; tap-hold-order resolves by release order instead of timeout.
574+
;; tap: a hold: lctl buffer: 50ms (fast typing grace period)
575+
aor (tap-hold-order 200 50 a lctl)
576+
;; With require-prior-idle: short-circuit to tap during typing streaks
577+
;; aor2 (tap-hold-order 200 50 a lctl 150)
578+
573579
;; tap for capslk, hold for lctl
574580
cap (tap-hold 200 200 caps lctl)
575581

docs/config.adoc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2018,6 +2018,7 @@ results in `$tap-action` activating.
20182018
(tap-hold-except-keys $tap-repress-timeout $hold-timeout $tap-action $hold-action $tap-keys)
20192019
(tap-hold-tap-keys $tap-repress-timeout $hold-timeout $tap-action $hold-action $tap-keys)
20202020
(tap-hold-opposite-hand $timeout $tap-action $hold-action [options...])
2021+
(tap-hold-order $tap-repress-timeout $buffer-ms $tap-action $hold-action [$require-prior-idle-ms])
20212022
----
20222023

20232024
[cols="1,2"]
@@ -2073,6 +2074,17 @@ This is useful for home row mods where fast typing should not trigger modifiers.
20732074
| Resolves to `$hold-action` when a key from the opposite hand (per `defhands`) is pressed.
20742075
Requires a `defhands` directive. Supports list-form options for fine-grained control.
20752076
See <<defhands and tap-hold-opposite-hand>> below.
2077+
2078+
| `tap-hold-order`
2079+
| Resolves purely by key release order, with no timeout.
2080+
If the tap-hold key is released before the other key, activates `$tap-action`;
2081+
if the other key is released first (while the tap-hold key is still held), activates `$hold-action`.
2082+
`$buffer-ms` is a grace period after the tap-hold key is pressed during which
2083+
release-order logic is ignored so that fast typing resolves as tap.
2084+
Optionally, `$require-prior-idle-ms` can short-circuit to tap when a different key
2085+
was pressed within that many milliseconds before the tap-hold key;
2086+
`require-prior-idle` checks prior typing activity before release-order logic begins,
2087+
whereas `buffer-ms` creates an unconditional tap-only window after the key is pressed.
20762088
|===
20772089
**Description**
20782090

keyberon/src/action.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ pub enum HoldTapConfig<'a> {
6767
/// The buffer field specifies a grace period in ticks (ms) after the
6868
/// initial press during which release-order logic is ignored and fast
6969
/// typing will resolve as Tap.
70-
ReleaseOrder { buffer: u16 },
70+
Order {
71+
buffer: u16,
72+
require_prior_idle_ms: u16,
73+
},
7174
/// If there is a press and release of another key, the hold
7275
/// action is activated.
7376
///
@@ -107,7 +110,7 @@ impl Debug for HoldTapConfig<'_> {
107110
match self {
108111
HoldTapConfig::Default => f.write_str("Default"),
109112
HoldTapConfig::HoldOnOtherKeyPress => f.write_str("HoldOnOtherKeyPress"),
110-
HoldTapConfig::ReleaseOrder { .. } => f.write_str("ReleaseOrder"),
113+
HoldTapConfig::Order { .. } => f.write_str("Order"),
111114
HoldTapConfig::PermissiveHold => f.write_str("PermissiveHold"),
112115
HoldTapConfig::Custom(_) => f.write_str("Custom"),
113116
}
@@ -121,7 +124,7 @@ impl PartialEq for HoldTapConfig<'_> {
121124
(HoldTapConfig::Default, HoldTapConfig::Default)
122125
| (HoldTapConfig::HoldOnOtherKeyPress, HoldTapConfig::HoldOnOtherKeyPress)
123126
| (HoldTapConfig::PermissiveHold, HoldTapConfig::PermissiveHold) => true,
124-
(HoldTapConfig::ReleaseOrder { .. }, HoldTapConfig::ReleaseOrder { .. }) => true,
127+
(HoldTapConfig::Order { .. }, HoldTapConfig::Order { .. }) => true,
125128
_ => false,
126129
}
127130
}

keyberon/src/layout.rs

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> {
544544
return Some(WaitingAction::Hold);
545545
}
546546
}
547-
HoldTapConfig::ReleaseOrder { buffer } => {
547+
HoldTapConfig::Order { 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.
@@ -555,6 +555,7 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> {
555555
let mut queued = queued.iter();
556556
while let Some(q) = queued.next() {
557557
if q.event.is_press() {
558+
// Elapsed ticks since this key entered the queue, compared against buffer window.
558559
let press_tick = self.ticks.saturating_sub(q.since);
559560
if press_tick < buffer {
560561
continue;
@@ -1869,6 +1870,30 @@ impl<'a, const C: usize, const R: usize, T: 'a + Copy + std::fmt::Debug> Layout<
18691870
return custom;
18701871
}
18711872
}
1873+
// Per-action require-prior-idle for tap-hold-order.
1874+
if let HoldTapConfig::Order {
1875+
require_prior_idle_ms,
1876+
..
1877+
} = config
1878+
{
1879+
if *require_prior_idle_ms > 0 {
1880+
let prior_idle_tap = self
1881+
.historical_inputs
1882+
.iter_hevents()
1883+
.find(|prior| {
1884+
prior.event.0 == REAL_KEY_ROW && prior.event != coord
1885+
})
1886+
.is_some_and(|prior| {
1887+
prior.ticks_since_occurrence <= *require_prior_idle_ms
1888+
});
1889+
if prior_idle_tap {
1890+
let custom =
1891+
self.do_action(tap, coord, delay, is_oneshot, layer_stack);
1892+
self.last_press_tracker.update_coord(coord);
1893+
return custom;
1894+
}
1895+
}
1896+
}
18721897
let mut custom = CustomEvent::NoEvent;
18731898
if *tap_hold_interval == 0
18741899
|| coord != self.last_press_tracker.coord
@@ -2534,7 +2559,7 @@ mod test {
25342559
}
25352560

25362561
#[test]
2537-
fn release_order_clean_tap() {
2562+
fn order_clean_tap() {
25382563
// Press and release modifier with no other keys → Tap.
25392564
static LAYERS: Layers<2, 1> = &[[[
25402565
HoldTap(&HoldTapAction {
@@ -2543,7 +2568,7 @@ mod test {
25432568
hold: k(LAlt),
25442569
timeout_action: k(Space),
25452570
tap: k(Space),
2546-
config: HoldTapConfig::ReleaseOrder { buffer: 0 },
2571+
config: HoldTapConfig::Order { buffer: 0, require_prior_idle_ms: 0 },
25472572
tap_hold_interval: 0,
25482573
}),
25492574
k(Enter),
@@ -2565,7 +2590,7 @@ mod test {
25652590
}
25662591

25672592
#[test]
2568-
fn release_order_hold() {
2593+
fn order_hold() {
25692594
// Modifier down → other down → other up first → Hold.
25702595
static LAYERS: Layers<2, 1> = &[[[
25712596
HoldTap(&HoldTapAction {
@@ -2574,7 +2599,7 @@ mod test {
25742599
hold: k(LAlt),
25752600
timeout_action: k(Space),
25762601
tap: k(Space),
2577-
config: HoldTapConfig::ReleaseOrder { buffer: 0 },
2602+
config: HoldTapConfig::Order { buffer: 0, require_prior_idle_ms: 0 },
25782603
tap_hold_interval: 0,
25792604
}),
25802605
k(Enter),
@@ -2602,7 +2627,7 @@ mod test {
26022627
}
26032628

26042629
#[test]
2605-
fn release_order_tap() {
2630+
fn order_tap() {
26062631
// Modifier down → other down → modifier up first → Tap.
26072632
static LAYERS: Layers<2, 1> = &[[[
26082633
HoldTap(&HoldTapAction {
@@ -2611,7 +2636,7 @@ mod test {
26112636
hold: k(LAlt),
26122637
timeout_action: k(Space),
26132638
tap: k(Space),
2614-
config: HoldTapConfig::ReleaseOrder { buffer: 0 },
2639+
config: HoldTapConfig::Order { buffer: 0, require_prior_idle_ms: 0 },
26152640
tap_hold_interval: 0,
26162641
}),
26172642
k(Enter),
@@ -2632,6 +2657,58 @@ mod test {
26322657
assert_keys(&[Space, Enter], layout.keycodes());
26332658
}
26342659

2660+
#[test]
2661+
fn order_multi_key_hold() {
2662+
// TH down → A down → B down → A up (while B still held) → TH up.
2663+
// A's press+release cycle completes while TH is held → Hold.
2664+
static LAYERS: Layers<3, 1> = &[[[
2665+
HoldTap(&HoldTapAction {
2666+
on_press_reset_timeout_to: None,
2667+
timeout: u16::MAX,
2668+
hold: k(LAlt),
2669+
timeout_action: k(Space),
2670+
tap: k(Space),
2671+
config: HoldTapConfig::Order { buffer: 0, require_prior_idle_ms: 0 },
2672+
tap_hold_interval: 0,
2673+
}),
2674+
k(Enter),
2675+
k(Tab),
2676+
]]];
2677+
let mut layout = Layout::new(LAYERS);
2678+
2679+
// TH down
2680+
layout.event(Press(0, 0));
2681+
assert_eq!(CustomEvent::NoEvent, layout.tick());
2682+
assert_keys(&[], layout.keycodes());
2683+
// A down
2684+
layout.event(Press(0, 1));
2685+
assert_eq!(CustomEvent::NoEvent, layout.tick());
2686+
assert_keys(&[], layout.keycodes());
2687+
// B down
2688+
layout.event(Press(0, 2));
2689+
assert_eq!(CustomEvent::NoEvent, layout.tick());
2690+
assert_keys(&[], layout.keycodes());
2691+
// A up — A's press+release cycle is complete → Hold resolves
2692+
layout.event(Release(0, 1));
2693+
assert_eq!(CustomEvent::NoEvent, layout.tick());
2694+
assert_keys(&[LAlt], layout.keycodes());
2695+
// Queued keys replay: Enter press, Tab press, Enter release
2696+
assert_eq!(CustomEvent::NoEvent, layout.tick());
2697+
assert_keys(&[LAlt, Enter], layout.keycodes());
2698+
assert_eq!(CustomEvent::NoEvent, layout.tick());
2699+
assert_keys(&[LAlt, Enter, Tab], layout.keycodes());
2700+
assert_eq!(CustomEvent::NoEvent, layout.tick());
2701+
assert_keys(&[LAlt, Tab], layout.keycodes());
2702+
// Release B
2703+
layout.event(Release(0, 2));
2704+
assert_eq!(CustomEvent::NoEvent, layout.tick());
2705+
assert_keys(&[LAlt], layout.keycodes());
2706+
// Release TH
2707+
layout.event(Release(0, 0));
2708+
assert_eq!(CustomEvent::NoEvent, layout.tick());
2709+
assert_keys(&[], layout.keycodes());
2710+
}
2711+
26352712
#[test]
26362713
fn permissive_hold() {
26372714
static LAYERS: Layers<2, 1> = &[[[

parser/src/cfg/list_actions.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ pub const CLIPBOARD_RESTORE: &str = "clipboard-restore";
136136
pub const CLIPBOARD_SAVE_SET: &str = "clipboard-save-set";
137137
pub const CLIPBOARD_SAVE_CMD_SET: &str = "clipboard-save-cmd-set";
138138
pub const CLIPBOARD_SAVE_SWAP: &str = "clipboard-save-swap";
139-
pub const TAP_HOLD_RELEASE_ORDER: &str = "tap-hold-release-order";
139+
pub const TAP_HOLD_ORDER: &str = "tap-hold-order";
140140
pub const TAP_HOLD_OPPOSITE_HAND: &str = "tap-hold-opposite-hand";
141141

142142
pub fn is_list_action(ac: &str) -> bool {
@@ -273,7 +273,7 @@ pub fn is_list_action(ac: &str) -> bool {
273273
CLIPBOARD_SAVE_SET,
274274
CLIPBOARD_SAVE_CMD_SET,
275275
CLIPBOARD_SAVE_SWAP,
276-
TAP_HOLD_RELEASE_ORDER,
276+
TAP_HOLD_ORDER,
277277
TAP_HOLD_OPPOSITE_HAND,
278278
];
279279
LIST_ACTIONS.contains(&ac)

parser/src/cfg/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1508,7 +1508,7 @@ fn parse_action_list(ac: &[SExpr], s: &ParserState) -> Result<&'static KanataAct
15081508
TAP_HOLD_PRESS | TAP_HOLD_PRESS_A => {
15091509
parse_tap_hold(&ac[1..], s, HoldTapConfig::HoldOnOtherKeyPress)
15101510
}
1511-
TAP_HOLD_RELEASE_ORDER => parse_tap_hold_release_order(&ac[1..], s),
1511+
TAP_HOLD_ORDER => parse_tap_hold_order(&ac[1..], s),
15121512
TAP_HOLD_RELEASE | TAP_HOLD_RELEASE_A => {
15131513
parse_tap_hold(&ac[1..], s, HoldTapConfig::PermissiveHold)
15141514
}

parser/src/cfg/tap_hold.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,29 +94,37 @@ pub(crate) fn parse_tap_hold_timeout(
9494
}))))
9595
}
9696

97-
pub(crate) fn parse_tap_hold_release_order(
97+
pub(crate) fn parse_tap_hold_order(
9898
ac_params: &[SExpr],
9999
s: &ParserState,
100100
) -> Result<&'static KanataAction> {
101-
if ac_params.len() != 4 {
101+
if ac_params.len() != 4 && ac_params.len() != 5 {
102102
bail!(
103-
r"tap-hold-release-order expects 4 items after it, got {}.
103+
r"tap-hold-order expects 4-5 items after it, got {}.
104104
Params in order:
105-
<tap-repress-timeout> <buffer-ms> <tap-action> <hold-action>",
105+
<tap-repress-timeout> <buffer-ms> <tap-action> <hold-action> [require-prior-idle-ms]",
106106
ac_params.len(),
107107
)
108108
}
109109
let tap_repress_timeout = parse_u16(&ac_params[0], s, "tap repress timeout")?;
110110
let buffer = parse_u16(&ac_params[1], s, "buffer")?;
111111
let tap_action = parse_action(&ac_params[2], s)?;
112112
let hold_action = parse_action(&ac_params[3], s)?;
113+
let require_prior_idle_ms = if ac_params.len() == 5 {
114+
parse_u16(&ac_params[4], s, "require-prior-idle-ms")?
115+
} else {
116+
0
117+
};
113118
if matches!(tap_action, Action::HoldTap { .. }) {
114119
bail!("tap-hold does not work in the tap-action of tap-hold")
115120
}
116121
Ok(s.a.sref(Action::HoldTap(s.a.sref(HoldTapAction {
117-
config: HoldTapConfig::ReleaseOrder { buffer },
122+
config: HoldTapConfig::Order {
123+
buffer,
124+
require_prior_idle_ms,
125+
},
118126
tap_hold_interval: tap_repress_timeout,
119-
timeout: u16::MAX,
127+
timeout: u16::MAX, // Resolution is purely event-driven, not timeout-based.
120128
tap: *tap_action,
121129
hold: *hold_action,
122130
timeout_action: *tap_action,

0 commit comments

Comments
 (0)