Skip to content

Commit 07bed9e

Browse files
malpernclaude
andauthored
feat: add per-action require-prior-idle override for tap-hold (#1969)
## Summary Adds a per-action `(require-prior-idle <ms>)` option to all `tap-hold` variants, allowing individual actions to override the global `tap-hold-require-prior-idle` defcfg value. Closes #1967. ## Motivation With the global `tap-hold-require-prior-idle` from #1960, users can't mix tap-hold behaviors — home-row mods benefit from idle detection, but layer-tap keys (e.g., tab/number-layer) need immediate activation even during fast typing. This is the per-action granularity follow-up noted in #1960. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 72b7f1a commit 07bed9e

File tree

7 files changed

+494
-16
lines changed

7 files changed

+494
-16
lines changed

docs/config.adoc

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2074,6 +2074,11 @@ This is useful for home row mods where fast typing should not trigger modifiers.
20742074
Requires a `defhands` directive. Supports list-form options for fine-grained control.
20752075
See <<defhands and tap-hold-opposite-hand>> below.
20762076
|===
2077+
2078+
All `tap-hold` variants support an optional trailing `(require-prior-idle <ms>)` option
2079+
to override the global <<tap-hold-require-prior-idle>> setting for that specific action.
2080+
Use `(require-prior-idle 0)` to disable idle detection for a specific key.
2081+
20772082
**Description**
20782083

20792084
The `+tap-hold+` action allows you to have one action/key for a "tap" and a
@@ -4158,6 +4163,47 @@ The default value is `0` (disabled) and the unit is milliseconds.
41584163
`tap-hold` configuration (e.g., `tap-hold-opposite-hand`, `concurrent-tap-hold`)
41594164
is consulted.
41604165

4166+
==== Per-action override
4167+
4168+
The global `tap-hold-require-prior-idle` value can be overridden on individual
4169+
`tap-hold` actions using the `(require-prior-idle <ms>)` option.
4170+
This is useful when most keys benefit from idle detection (e.g., home-row mods)
4171+
but specific keys (e.g., a layer-tap key) need immediate activation even during
4172+
fast typing.
4173+
4174+
The option can be appended to any `tap-hold` variant as a trailing
4175+
`(keyword value)` list:
4176+
4177+
.Example: disable for a specific key
4178+
[source]
4179+
----
4180+
(defcfg tap-hold-require-prior-idle 150)
4181+
(defalias
4182+
;; HRMs use the global 150ms threshold
4183+
a (tap-hold 200 200 a lmet)
4184+
s (tap-hold 200 200 s lalt)
4185+
;; Layer key disables idle detection for immediate activation
4186+
nav (tap-hold 200 200 tab (layer-toggle nav) (require-prior-idle 0))
4187+
)
4188+
----
4189+
4190+
.Example: enable for a specific key without a global setting
4191+
[source]
4192+
----
4193+
(defalias
4194+
a (tap-hold 200 200 a lmet (require-prior-idle 150))
4195+
)
4196+
----
4197+
4198+
- `(require-prior-idle 0)` disables the feature for that action,
4199+
even when a global threshold is set.
4200+
- A non-zero value enables or changes the threshold for that action only.
4201+
- When no per-action override is specified, the global `defcfg` value is used.
4202+
- Works with all `tap-hold` variants: `tap-hold`, `tap-hold-press`,
4203+
`tap-hold-release`, `tap-hold-press-timeout`, `tap-hold-release-timeout`,
4204+
`tap-hold-release-keys`, `tap-hold-except-keys`, `tap-hold-tap-keys`,
4205+
and `tap-hold-opposite-hand`.
4206+
41614207
[[override-release-on-activation]]
41624208
=== override-release-on-activation
41634209

keyberon/src/action.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ where
177177
/// because a human might have a slow release but they did
178178
/// indeed want a hold to activate.
179179
pub on_press_reset_timeout_to: Option<std::num::NonZeroU16>,
180+
/// Per-action override for the global `tap_hold_require_prior_idle` setting.
181+
/// If `Some(n)`, uses `n` instead of the global value (0 = disabled for this action).
182+
/// If `None`, falls back to the global `defcfg` value.
183+
pub require_prior_idle: Option<u16>,
180184
}
181185

182186
/// Define one shot key behaviour.

keyberon/src/layout.rs

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1829,17 +1829,18 @@ impl<'a, const C: usize, const R: usize, T: 'a + Copy + std::fmt::Debug> Layout<
18291829
config,
18301830
tap_hold_interval,
18311831
on_press_reset_timeout_to,
1832+
require_prior_idle,
18321833
}) => {
18331834
// Typing streak detection: if a different physical key was pressed
18341835
// recently, resolve as tap immediately without entering WaitingState.
1835-
if self.tap_hold_require_prior_idle > 0 {
1836+
// Per-action override takes precedence over the global defcfg value.
1837+
let idle_threshold = require_prior_idle.unwrap_or(self.tap_hold_require_prior_idle);
1838+
if idle_threshold > 0 {
18361839
let prior_idle_tap = self
18371840
.historical_inputs
18381841
.iter_hevents()
18391842
.find(|prior| prior.event.0 == REAL_KEY_ROW && prior.event != coord)
1840-
.is_some_and(|prior| {
1841-
prior.ticks_since_occurrence <= self.tap_hold_require_prior_idle
1842-
});
1843+
.is_some_and(|prior| prior.ticks_since_occurrence <= idle_threshold);
18431844
if prior_idle_tap {
18441845
let custom = self.do_action(tap, coord, delay, is_oneshot, layer_stack);
18451846
self.last_press_tracker.update_coord(coord);
@@ -2298,6 +2299,7 @@ mod test {
22982299
[[
22992300
HoldTap(&HoldTapAction {
23002301
on_press_reset_timeout_to: None,
2302+
require_prior_idle: None,
23012303
timeout: 200,
23022304
hold: l(1),
23032305
tap: k(Space),
@@ -2307,6 +2309,7 @@ mod test {
23072309
}),
23082310
HoldTap(&HoldTapAction {
23092311
on_press_reset_timeout_to: None,
2312+
require_prior_idle: None,
23102313
timeout: 200,
23112314
hold: k(LCtrl),
23122315
timeout_action: k(LShift),
@@ -2352,6 +2355,7 @@ mod test {
23522355
[[
23532356
HoldTap(&HoldTapAction {
23542357
on_press_reset_timeout_to: None,
2358+
require_prior_idle: None,
23552359
timeout: 200,
23562360
hold: l(1),
23572361
tap: k(Space),
@@ -2361,6 +2365,7 @@ mod test {
23612365
}),
23622366
HoldTap(&HoldTapAction {
23632367
on_press_reset_timeout_to: None,
2368+
require_prior_idle: None,
23642369
timeout: 200,
23652370
hold: k(LCtrl),
23662371
timeout_action: k(LCtrl),
@@ -2405,6 +2410,7 @@ mod test {
24052410
static LAYERS: Layers<2, 1> = &[[[
24062411
HoldTap(&HoldTapAction {
24072412
on_press_reset_timeout_to: None,
2413+
require_prior_idle: None,
24082414
timeout: 200,
24092415
hold: k(LAlt),
24102416
timeout_action: k(LAlt),
@@ -2414,6 +2420,7 @@ mod test {
24142420
}),
24152421
HoldTap(&HoldTapAction {
24162422
on_press_reset_timeout_to: None,
2423+
require_prior_idle: None,
24172424
timeout: 20,
24182425
hold: k(LCtrl),
24192426
timeout_action: k(LCtrl),
@@ -2456,6 +2463,7 @@ mod test {
24562463
static LAYERS: Layers<2, 1> = &[[[
24572464
HoldTap(&HoldTapAction {
24582465
on_press_reset_timeout_to: None,
2466+
require_prior_idle: None,
24592467
timeout: 200,
24602468
hold: k(LAlt),
24612469
timeout_action: k(LAlt),
@@ -2515,6 +2523,7 @@ mod test {
25152523
static LAYERS: Layers<2, 1> = &[[[
25162524
HoldTap(&HoldTapAction {
25172525
on_press_reset_timeout_to: None,
2526+
require_prior_idle: None,
25182527
timeout: 200,
25192528
hold: k(LAlt),
25202529
timeout_action: k(LAlt),
@@ -2556,6 +2565,7 @@ mod test {
25562565
static LAYERS: Layers<3, 1> = &[[[
25572566
HoldTap(&HoldTapAction {
25582567
on_press_reset_timeout_to: None,
2568+
require_prior_idle: None,
25592569
timeout: 200,
25602570
hold: k(LAlt),
25612571
timeout_action: k(LAlt),
@@ -2565,6 +2575,7 @@ mod test {
25652575
}),
25662576
HoldTap(&HoldTapAction {
25672577
on_press_reset_timeout_to: None,
2578+
require_prior_idle: None,
25682579
timeout: 200,
25692580
hold: k(RAlt),
25702581
timeout_action: k(RAlt),
@@ -2574,6 +2585,7 @@ mod test {
25742585
}),
25752586
HoldTap(&HoldTapAction {
25762587
on_press_reset_timeout_to: None,
2588+
require_prior_idle: None,
25772589
timeout: 200,
25782590
hold: k(LCtrl),
25792591
timeout_action: k(LCtrl),
@@ -2739,6 +2751,7 @@ mod test {
27392751
static LAYERS: Layers<4, 1> = &[[[
27402752
HoldTap(&HoldTapAction {
27412753
on_press_reset_timeout_to: None,
2754+
require_prior_idle: None,
27422755
timeout: 200,
27432756
hold: k(Kb1),
27442757
timeout_action: k(Kb1),
@@ -2748,6 +2761,7 @@ mod test {
27482761
}),
27492762
HoldTap(&HoldTapAction {
27502763
on_press_reset_timeout_to: None,
2764+
require_prior_idle: None,
27512765
timeout: 200,
27522766
hold: k(Kb3),
27532767
timeout_action: k(Kb3),
@@ -2757,6 +2771,7 @@ mod test {
27572771
}),
27582772
HoldTap(&HoldTapAction {
27592773
on_press_reset_timeout_to: None,
2774+
require_prior_idle: None,
27602775
timeout: 200,
27612776
hold: k(Kb5),
27622777
timeout_action: k(Kb5),
@@ -2766,6 +2781,7 @@ mod test {
27662781
}),
27672782
HoldTap(&HoldTapAction {
27682783
on_press_reset_timeout_to: None,
2784+
require_prior_idle: None,
27692785
timeout: 200,
27702786
hold: k(Kb7),
27712787
timeout_action: k(Kb7),
@@ -2840,6 +2856,7 @@ mod test {
28402856
static LAYERS: Layers<2, 1> = &[[[
28412857
HoldTap(&HoldTapAction {
28422858
on_press_reset_timeout_to: None,
2859+
require_prior_idle: None,
28432860
timeout: 200,
28442861
hold: k(LAlt),
28452862
timeout_action: k(LAlt),
@@ -2896,6 +2913,7 @@ mod test {
28962913
static LAYERS: Layers<3, 1> = &[[[
28972914
HoldTap(&HoldTapAction {
28982915
on_press_reset_timeout_to: None,
2916+
require_prior_idle: None,
28992917
timeout: 200,
29002918
hold: k(LAlt),
29012919
timeout_action: k(LAlt),
@@ -2906,6 +2924,7 @@ mod test {
29062924
k(Enter),
29072925
HoldTap(&HoldTapAction {
29082926
on_press_reset_timeout_to: None,
2927+
require_prior_idle: None,
29092928
timeout: 200,
29102929
hold: k(LAlt),
29112930
timeout_action: k(LAlt),
@@ -3020,6 +3039,7 @@ mod test {
30203039
fn tap_hold_interval_short_hold() {
30213040
static LAYERS: Layers<1, 1> = &[[[HoldTap(&HoldTapAction {
30223041
on_press_reset_timeout_to: None,
3042+
require_prior_idle: None,
30233043
timeout: 50,
30243044
hold: k(LAlt),
30253045
timeout_action: k(LAlt),
@@ -3064,6 +3084,7 @@ mod test {
30643084
static LAYERS: Layers<2, 1> = &[[[
30653085
HoldTap(&HoldTapAction {
30663086
on_press_reset_timeout_to: None,
3087+
require_prior_idle: None,
30673088
timeout: 50,
30683089
hold: k(LAlt),
30693090
timeout_action: k(LAlt),
@@ -3073,6 +3094,7 @@ mod test {
30733094
}),
30743095
HoldTap(&HoldTapAction {
30753096
on_press_reset_timeout_to: None,
3097+
require_prior_idle: None,
30763098
timeout: 200,
30773099
hold: k(RAlt),
30783100
timeout_action: k(RAlt),
@@ -3591,6 +3613,7 @@ mod test {
35913613
}),
35923614
HoldTap(&HoldTapAction {
35933615
on_press_reset_timeout_to: None,
3616+
require_prior_idle: None,
35943617
timeout: 100,
35953618
hold: k(LAlt),
35963619
timeout_action: k(LAlt),
@@ -3657,6 +3680,7 @@ mod test {
36573680
}),
36583681
&HoldTap(&HoldTapAction {
36593682
on_press_reset_timeout_to: None,
3683+
require_prior_idle: None,
36603684
timeout: 100,
36613685
hold: k(LAlt),
36623686
timeout_action: k(LAlt),
@@ -4098,6 +4122,7 @@ mod test {
40984122
1,
40994123
&HoldTap(&HoldTapAction {
41004124
on_press_reset_timeout_to: None,
4125+
require_prior_idle: None,
41014126
timeout: 100,
41024127
hold: k(A),
41034128
timeout_action: k(A),
@@ -4110,6 +4135,7 @@ mod test {
41104135
2,
41114136
&HoldTap(&HoldTapAction {
41124137
on_press_reset_timeout_to: None,
4138+
require_prior_idle: None,
41134139
timeout: 100,
41144140
hold: k(B),
41154141
timeout_action: k(B),
@@ -4362,6 +4388,7 @@ mod test {
43624388
NoOp,
43634389
HoldTap(&HoldTapAction {
43644390
on_press_reset_timeout_to: None,
4391+
require_prior_idle: None,
43654392
timeout: 50,
43664393
hold: k(Space),
43674394
timeout_action: k(Space),
@@ -4410,6 +4437,7 @@ mod test {
44104437
NoOp,
44114438
HoldTap(&HoldTapAction {
44124439
on_press_reset_timeout_to: None,
4440+
require_prior_idle: None,
44134441
timeout: 50,
44144442
hold: Trans,
44154443
timeout_action: Trans,
@@ -4646,6 +4674,7 @@ mod test {
46464674
NoOp,
46474675
HoldTap(&HoldTapAction {
46484676
on_press_reset_timeout_to: None,
4677+
require_prior_idle: None,
46494678
timeout: 50,
46504679
hold: k(B),
46514680
timeout_action: k(B),
@@ -4660,6 +4689,7 @@ mod test {
46604689
Layer(3),
46614690
HoldTap(&HoldTapAction {
46624691
on_press_reset_timeout_to: None,
4692+
require_prior_idle: None,
46634693
timeout: 50,
46644694
hold: k(C),
46654695
timeout_action: k(C),
@@ -4674,6 +4704,7 @@ mod test {
46744704
NoOp,
46754705
HoldTap(&HoldTapAction {
46764706
on_press_reset_timeout_to: None,
4707+
require_prior_idle: None,
46774708
timeout: 50,
46784709
hold: k(D),
46794710
timeout_action: k(D),
@@ -4788,6 +4819,7 @@ mod test {
47884819
config: HoldTapConfig::Default,
47894820
tap_hold_interval: 0,
47904821
on_press_reset_timeout_to: None,
4822+
require_prior_idle: None,
47914823
})]]];
47924824
let mut layout = Layout::new(LAYERS);
47934825
// Nothing set initially.
@@ -4821,6 +4853,7 @@ mod test {
48214853
config: HoldTapConfig::Default,
48224854
tap_hold_interval: 0,
48234855
on_press_reset_timeout_to: None,
4856+
require_prior_idle: None,
48244857
}),
48254858
k(A),
48264859
]]];
@@ -4854,6 +4887,7 @@ mod test {
48544887
config: HoldTapConfig::PermissiveHold,
48554888
tap_hold_interval: 0,
48564889
on_press_reset_timeout_to: None,
4890+
require_prior_idle: None,
48574891
}),
48584892
k(A),
48594893
]]];
@@ -4889,6 +4923,7 @@ mod test {
48894923
config: HoldTapConfig::HoldOnOtherKeyPress,
48904924
tap_hold_interval: 0,
48914925
on_press_reset_timeout_to: None,
4926+
require_prior_idle: None,
48924927
}),
48934928
k(A),
48944929
]]];

0 commit comments

Comments
 (0)