Skip to main content

smart_keymap_core/key/
tap_hold.rs

1use core::fmt::Debug;
2use core::ops::Index;
3
4use serde::Deserialize;
5
6use crate::input;
7use crate::key;
8use crate::keymap;
9use crate::slice::Slice;
10
11/// Reference for a tap_hold key.
12#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
13pub struct Ref(pub u8);
14
15/// Maximum number of *extra* tap-hold profiles beyond the default (profile 0).
16///
17/// Profile indices on keys are `0` ([`Config::default_profile`])
18///  then `1..` into [`Config::profiles`].
19/// Total usable profiles = `1 + MAX_EXTRA_PROFILES`.
20pub const MAX_EXTRA_PROFILES: usize = 7;
21
22/// Maximum keymap indices listed in one profile's `hold_trigger_key_positions`.
23///
24/// Sparse opposite-hand lists are expected (on the order of ~10),
25///  not a dense mask of every key on the board.
26pub const MAX_HOLD_TRIGGER_POSITIONS: usize = 16;
27
28/// One tap-hold behavior profile (timeout, interrupt flavor, idle gate, hold triggers, quick-tap).
29///
30/// Profile 0 is [`Config::default_profile`];
31///  extra profiles live in [`Config::profiles`]
32///  and are selected per key via [`Key::profile`].
33#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
34pub struct Profile {
35    /// The timeout (in number of milliseconds) for a tap-hold key to resolve as hold.
36    ///
37    /// Counted from the physical press of this keymap index.
38    /// When this tap-hold is nested under another pending key
39    ///  (e.g. chorded passthrough), the keymap folds already-elapsed
40    ///  time into the scheduled delay so the configured timeout is not additive.
41    ///
42    /// When `None`, the tap/hold decision does not timeout;
43    ///  the key resolves only on release (as tap) or interruption
44    ///  (depending on [InterruptResponse]).
45    #[serde(default = "default_timeout")]
46    pub timeout: Option<u16>,
47
48    /// How the tap-hold key should respond to interruptions.
49    #[serde(default = "default_interrupt_response")]
50    pub interrupt_response: InterruptResponse,
51
52    /// Amount of time (in milliseconds) the keymap must have been idle
53    ///  in order for tap hold to support 'hold' functionality.
54    ///
55    /// This reduces disruption from unexpected hold resolutions
56    ///  when typing quickly.
57    #[serde(default)]
58    pub required_idle_time: Option<u16>,
59
60    /// When set, only these keymap indices may force an interrupt-as-hold resolution.
61    /// When unset, any interrupting key may force hold
62    ///  (subject to [InterruptResponse]).
63    ///
64    /// JSON field name matches the Nickel authoring surface (`hold_trigger_key_positions`).
65    /// Length is bounded by [`MAX_HOLD_TRIGGER_POSITIONS`].
66    #[serde(default, rename = "hold_trigger_key_positions")]
67    pub hold_trigger_positions: Option<Slice<u16, MAX_HOLD_TRIGGER_POSITIONS>>,
68
69    /// If this tap-hold key is pressed again within this many milliseconds of its
70    /// previous press (ZMK `quick-tap-ms`), immediately resolve as tap.
71    ///
72    /// Useful for hold-to-repeat on the tap letter (e.g. backspace) without
73    /// waiting for the hold timeout. `None` disables (default).
74    ///
75    /// Unlike [`Self::required_idle_time`] (any recent activity), this is
76    /// scoped to re-presses of the same keymap index.
77    #[serde(default)]
78    pub quick_tap_ms: Option<u16>,
79
80    /// Speculative HID while this tap-hold is pending.
81    ///
82    /// - [`PendingOutput::NoOutput`] (default): no HID until tap vs hold settles.
83    /// - [`PendingOutput::Hold`]: emit the hold binding's HID while still pending
84    ///   (ZMK `hold-while-undecided` / QMK Speculative Hold /
85    ///   FAK `eager_decision = 'hold`).
86    #[serde(default = "default_pending_output")]
87    pub pending_output: PendingOutput,
88}
89
90impl Profile {
91    /// Constructs a new default [Profile].
92    pub const fn new() -> Self {
93        DEFAULT_PROFILE
94    }
95
96    /// Whether an interrupt from `other_keymap_index` may force hold.
97    ///
98    /// When [`Self::hold_trigger_positions`] is set,
99    ///  only those indices count as hold triggers.
100    /// When unset, any other key may force hold.
101    pub const fn allows_hold_trigger(&self, other_keymap_index: u16) -> bool {
102        match self.hold_trigger_positions {
103            None => true,
104            Some(positions) => {
105                let list = positions.as_slice();
106                let mut i = 0;
107                while i < list.len() {
108                    if list[i] == other_keymap_index {
109                        return true;
110                    }
111                    i += 1;
112                }
113                false
114            }
115        }
116    }
117}
118
119impl Default for Profile {
120    fn default() -> Self {
121        DEFAULT_PROFILE
122    }
123}
124
125/// A key with tap-hold functionality.
126#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
127pub struct Key<R> {
128    /// The 'tap' key.
129    pub tap: R,
130    /// The 'hold' key.
131    pub hold: R,
132    /// Behavior profile index: `0` = [`Config::default_profile`]; `1..` = [`Config::profiles`].
133    #[serde(default)]
134    pub profile: u8,
135    /// HID of the hold binding when it is a keyboard key that is safe to emit
136    ///  while pending.
137    ///
138    /// Nickel fills this in keymap codegen
139    ///  (`ncl/smart_keys/tap_hold/keymap-codegen.ncl`)
140    ///  from the hold leaf.
141    /// LeftGUI / RightGUI and non-keyboard holds are `None`.
142    /// Used when [`Profile::pending_output`] is [`PendingOutput::Hold`].
143    #[serde(default)]
144    pub hold_output: Option<key::KeyOutput>,
145}
146
147impl<R> Key<R> {
148    /// Constructs a new tap-hold key using the default profile (0).
149    pub const fn new(tap: R, hold: R) -> Key<R> {
150        Key {
151            tap,
152            hold,
153            profile: 0,
154            hold_output: None,
155        }
156    }
157
158    /// Constructs a tap-hold key that uses the given behavior profile index.
159    pub const fn with_profile(tap: R, hold: R, profile: u8) -> Key<R> {
160        Key {
161            tap,
162            hold,
163            profile,
164            hold_output: None,
165        }
166    }
167}
168
169#[cfg(feature = "std")]
170impl<R: Default> Default for Key<R> {
171    fn default() -> Self {
172        Key {
173            tap: R::default(),
174            hold: R::default(),
175            profile: 0,
176            hold_output: None,
177        }
178    }
179}
180
181/// Speculative output while tap-hold is pending.
182#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
183pub enum PendingOutput {
184    /// No speculative output (default).
185    NoOutput,
186    /// Emit hold binding while still pending.
187    Hold,
188}
189
190/// How the tap hold key should respond to interruptions (input events from other keys).
191#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
192pub enum InterruptResponse {
193    /// The tap-hold key ignores other key presses/taps.
194    /// (Only resolves to hold on timeout).
195    Ignore,
196    /// The tap-hold key resolves as "hold" when interrupted by a key press.
197    HoldOnKeyPress,
198    /// The tap-hold key resolves as "hold" when interrupted by a key tap.
199    /// (Another key was pressed and released).
200    HoldOnKeyTap,
201}
202
203/// Configuration settings for tap hold keys.
204///
205/// [`Config::default_profile`] is **profile 0**.
206/// Optional [`Config::profiles`] are extra behaviors at indices `1..`.
207/// Keys select a profile via [`Key::profile`].
208///
209/// Nickel authoring keeps profile-0 knobs flat on `config.tap_hold`
210///  (`timeout`, `interrupt_response`, …);
211///  lowering nests them as `default_profile` in JSON.
212#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
213pub struct Config {
214    /// Default behavior (profile index 0).
215    #[serde(default = "default_profile_value")]
216    pub default_profile: Profile,
217
218    /// Extra behavior profiles (indices `1..=len`).
219    #[serde(default)]
220    pub profiles: Slice<Profile, MAX_EXTRA_PROFILES>,
221}
222
223/// The default timeout.
224pub const DEFAULT_TIMEOUT: u16 = 200;
225
226/// The default interrupt response.
227pub const DEFAULT_INTERRUPT_RESPONSE: InterruptResponse = InterruptResponse::Ignore;
228
229fn default_timeout() -> Option<u16> {
230    Some(DEFAULT_TIMEOUT)
231}
232
233fn default_interrupt_response() -> InterruptResponse {
234    DEFAULT_INTERRUPT_RESPONSE
235}
236
237/// The default pending output.
238pub const DEFAULT_PENDING_OUTPUT: PendingOutput = PendingOutput::NoOutput;
239
240fn default_pending_output() -> PendingOutput {
241    DEFAULT_PENDING_OUTPUT
242}
243
244fn default_profile_value() -> Profile {
245    DEFAULT_PROFILE
246}
247
248/// Default profile (also the contents of [`DEFAULT_CONFIG`]'s default profile).
249pub const DEFAULT_PROFILE: Profile = Profile {
250    timeout: Some(DEFAULT_TIMEOUT),
251    interrupt_response: DEFAULT_INTERRUPT_RESPONSE,
252    required_idle_time: None,
253    hold_trigger_positions: None,
254    quick_tap_ms: None,
255    pending_output: DEFAULT_PENDING_OUTPUT,
256};
257
258/// Default tap hold config.
259pub const DEFAULT_CONFIG: Config = Config {
260    default_profile: DEFAULT_PROFILE,
261    profiles: Slice::from_slice(&[]),
262};
263
264impl Config {
265    /// Constructs a new default [Config].
266    pub const fn new() -> Self {
267        DEFAULT_CONFIG
268    }
269
270    /// Resolves the behavior profile for `profile_id`.
271    ///
272    /// - `0` → [`Self::default_profile`]
273    /// - `1..` → [`Self::profiles`] entry `id - 1`
274    ///
275    /// Out-of-range ids fall back to the default profile.
276    pub const fn profile(&self, profile_id: u8) -> Profile {
277        if profile_id == 0 {
278            return self.default_profile;
279        }
280        let idx = (profile_id - 1) as usize;
281        let extras = self.profiles.as_slice();
282        if idx < extras.len() {
283            extras[idx]
284        } else {
285            self.default_profile
286        }
287    }
288}
289
290impl Default for Config {
291    /// Returns the default context.
292    fn default() -> Self {
293        DEFAULT_CONFIG
294    }
295}
296
297/// Context for [Key].
298#[derive(Debug, Clone, Copy, PartialEq)]
299pub struct Context {
300    config: Config,
301    idle_time_ms: u32,
302    time_ms: u32,
303    recent_presses: [(u16, u32); keymap::MAX_RECENT_PRESSES],
304    recent_press_count: u8,
305}
306
307impl Context {
308    /// Constructs a context from the given config
309    pub const fn from_config(config: Config) -> Context {
310        Context {
311            config,
312            idle_time_ms: 0,
313            time_ms: 0,
314            recent_presses: [(0, 0); keymap::MAX_RECENT_PRESSES],
315            recent_press_count: 0,
316        }
317    }
318
319    /// Re-construct from context's [Config], clearing idle-time tracking.
320    pub fn reset(&mut self) {
321        *self = Self::from_config(self.config);
322    }
323
324    /// Updates the context with the given keymap context.
325    pub fn update_keymap_context(
326        &mut self,
327        keymap::KeymapContext {
328            idle_time_ms,
329            time_ms,
330            recent_presses,
331            recent_press_count,
332            ..
333        }: &keymap::KeymapContext,
334    ) {
335        self.idle_time_ms = *idle_time_ms;
336        self.time_ms = *time_ms;
337        self.recent_presses = *recent_presses;
338        self.recent_press_count = *recent_press_count;
339    }
340
341    /// Returns the resolved [Profile] for `profile_id`.
342    pub const fn profile(&self, profile_id: u8) -> Profile {
343        self.config.profile(profile_id)
344    }
345
346    fn last_press_time_ms(&self, keymap_index: u16) -> Option<u32> {
347        self.recent_presses[..self.recent_press_count as usize]
348            .iter()
349            .rev()
350            .find(|(ki, _)| *ki == keymap_index)
351            .map(|(_, t)| *t)
352    }
353
354    /// Whether a re-press of `keymap_index` falls within the profile's `quick_tap_ms`.
355    fn is_quick_tap(&self, profile: &Profile, keymap_index: u16) -> bool {
356        profile
357            .quick_tap_ms
358            .zip(self.last_press_time_ms(keymap_index))
359            .is_some_and(|(quick_tap_ms, last_t)| {
360                self.time_ms.saturating_sub(last_t) < quick_tap_ms as u32
361            })
362    }
363}
364
365/// The state of a tap-hold key.
366#[derive(Debug, Clone, Copy, PartialEq)]
367pub enum TapHoldState {
368    /// Resolved as tap.
369    Tap,
370    /// Resolved as hold.
371    Hold,
372}
373
374/// Events emitted by a tap-hold key.
375#[derive(Debug, Clone, Copy, PartialEq)]
376pub enum Event {
377    /// Event indicating the key has been held long enough to resolve as hold.
378    TapHoldTimeout,
379}
380
381/// The state of a pressed tap-hold key.
382#[derive(Debug, Clone, PartialEq)]
383pub struct PendingKeyState {
384    // For tracking 'tap' interruptions
385    other_pressed_keymap_index: Option<u16>,
386    /// Speculative HID while undecided ([`PendingOutput::Hold`]).
387    output: Option<key::KeyOutput>,
388}
389
390impl PendingKeyState {
391    /// Constructs the initial pressed key state
392    fn new() -> PendingKeyState {
393        PendingKeyState {
394            other_pressed_keymap_index: None,
395            output: None,
396        }
397    }
398
399    /// Constructs with speculative HID.
400    fn new_with_output(output: Option<key::KeyOutput>) -> Self {
401        Self {
402            other_pressed_keymap_index: None,
403            output,
404        }
405    }
406
407    /// Compute whether the tap-hold key should resolve as tap or hold,
408    ///  given the behavior profile, the current state, and the key event.
409    fn hold_resolution(
410        &self,
411        profile: &Profile,
412        keymap_index: u16,
413        event: key::Event<Event>,
414    ) -> Option<TapHoldState> {
415        match profile.interrupt_response {
416            InterruptResponse::HoldOnKeyPress => {
417                match event {
418                    key::Event::Input(input::Event::Press { keymap_index: ki }) => {
419                        // Interruption resolves as Hold
420                        //  when the interrupting key is a hold trigger position (or triggers unset).
421                        if profile.allows_hold_trigger(ki) {
422                            Some(TapHoldState::Hold)
423                        } else {
424                            None
425                        }
426                    }
427                    key::Event::Input(input::Event::Release { keymap_index: ki }) => {
428                        if keymap_index == ki {
429                            // TapHold: not interrupted; resolved as tap.
430                            Some(TapHoldState::Tap)
431                        } else {
432                            None
433                        }
434                    }
435                    key::Event::Key {
436                        key_event: Event::TapHoldTimeout,
437                        ..
438                    } => {
439                        // Key held long enough to resolve as hold.
440                        Some(TapHoldState::Hold)
441                    }
442                    _ => None,
443                }
444            }
445            InterruptResponse::HoldOnKeyTap => {
446                match event {
447                    key::Event::Input(input::Event::Release { keymap_index: ki }) => {
448                        if keymap_index == ki {
449                            // TapHold: not interrupted; resolved as tap.
450                            Some(TapHoldState::Tap)
451                        } else if Some(ki) == self.other_pressed_keymap_index
452                            && profile.allows_hold_trigger(ki)
453                        {
454                            // TapHold: interrupted by key tap (press + release); resolved as hold.
455                            Some(TapHoldState::Hold)
456                        } else {
457                            None
458                        }
459                    }
460                    key::Event::Key {
461                        key_event: Event::TapHoldTimeout,
462                        ..
463                    } => {
464                        // Key held long enough to resolve as hold.
465                        Some(TapHoldState::Hold)
466                    }
467                    _ => None,
468                }
469            }
470            InterruptResponse::Ignore => {
471                match event {
472                    key::Event::Input(input::Event::Release { keymap_index: ki }) => {
473                        if keymap_index == ki {
474                            // TapHold: not interrupted; resolved as tap.
475                            Some(TapHoldState::Tap)
476                        } else {
477                            None
478                        }
479                    }
480                    key::Event::Key {
481                        key_event: Event::TapHoldTimeout,
482                        ..
483                    } => {
484                        // Key held long enough to resolve as hold.
485                        Some(TapHoldState::Hold)
486                    }
487                    _ => None,
488                }
489            }
490        }
491    }
492
493    /// Returns at most 2 events
494    pub fn handle_event(
495        &mut self,
496        profile: &Profile,
497        keymap_index: u16,
498        event: key::Event<Event>,
499    ) -> Option<TapHoldState> {
500        // Check for interrupting taps
501        // (track other key press)
502        if let key::Event::Input(input::Event::Press { keymap_index: ki }) = event {
503            self.other_pressed_keymap_index = Some(ki);
504        }
505
506        // Resolve tap-hold state per the event.
507        self.hold_resolution(profile, keymap_index, event)
508    }
509}
510
511/// Key state for tap_hold keys. (Not used).
512#[derive(Debug, Clone, Copy, PartialEq)]
513pub struct KeyState;
514
515/// The [key::System] implementation for tap hold keys.
516#[derive(Debug, Clone, Copy, PartialEq)]
517pub struct System<R, Keys: Index<usize, Output = Key<R>>> {
518    keys: Keys,
519}
520
521impl<R, Keys: Index<usize, Output = Key<R>>> System<R, Keys> {
522    /// Constructs a new [System] with the given key data.
523    pub const fn new(key_data: Keys) -> Self {
524        Self { keys: key_data }
525    }
526
527    fn new_pending_key(
528        &self,
529        profile: &Profile,
530        keymap_index: u16,
531        hold_output: Option<key::KeyOutput>,
532    ) -> (PendingKeyState, Option<key::ScheduledEvent<Event>>) {
533        let pending = match profile.pending_output {
534            PendingOutput::Hold => PendingKeyState::new_with_output(hold_output),
535            PendingOutput::NoOutput => PendingKeyState::new(),
536        };
537        let scheduled = profile.timeout.map(|timeout| {
538            key::ScheduledEvent::after(
539                timeout,
540                key::Event::key_event(keymap_index, Event::TapHoldTimeout),
541            )
542        });
543        (pending, scheduled)
544    }
545
546    fn resolve_as_tap(
547        &self,
548        key_index: u8,
549    ) -> (
550        key::PressedKeyResult<R, PendingKeyState, KeyState>,
551        key::KeyEvents<Event>,
552    )
553    where
554        R: Copy,
555    {
556        let Key {
557            tap: tap_key_ref, ..
558        } = self.keys[key_index as usize];
559        (
560            key::PressedKeyResult::NewPressedKey(key::NewPressedKey::key(tap_key_ref)),
561            key::KeyEvents::no_events(),
562        )
563    }
564}
565
566impl<R: Copy + Debug, Keys: Debug + Index<usize, Output = Key<R>>> key::System<R>
567    for System<R, Keys>
568{
569    type Ref = Ref;
570    type Context = Context;
571    type Event = Event;
572    type PendingKeyState = PendingKeyState;
573    type KeyState = KeyState;
574
575    fn new_pressed_key(
576        &self,
577        keymap_index: u16,
578        context: &Self::Context,
579        Ref(key_index): Ref,
580    ) -> (
581        key::PressedKeyResult<R, Self::PendingKeyState, Self::KeyState>,
582        key::KeyEvents<Self::Event>,
583    ) {
584        let key_def = &self.keys[key_index as usize];
585        let profile = context.profile(key_def.profile);
586
587        // ZMK quick-tap: re-press within window of prior press → force tap.
588        if context.is_quick_tap(&profile, keymap_index) {
589            return self.resolve_as_tap(key_index);
590        }
591
592        match profile.required_idle_time {
593            Some(required_idle_time) => {
594                if context.idle_time_ms >= required_idle_time as u32 {
595                    // Keymap has been idle long enough; use pending tap-hold key state.
596                    let (th_pks, maybe_sch_ev) =
597                        self.new_pending_key(&profile, keymap_index, key_def.hold_output);
598                    let pk = key::PressedKeyResult::Pending(th_pks);
599                    let pke = match maybe_sch_ev {
600                        Some(sch_ev) => {
601                            key::KeyEvents::scheduled_event(sch_ev.into_scheduled_event())
602                        }
603                        None => key::KeyEvents::no_events(),
604                    };
605                    (pk, pke)
606                } else {
607                    // Keymap has not been idle for long enough;
608                    // immediately resolve as tap.
609                    self.resolve_as_tap(key_index)
610                }
611            }
612            None => {
613                // Idle time not considered. Use pending tap-hold key state.
614                let (th_pks, maybe_sch_ev) =
615                    self.new_pending_key(&profile, keymap_index, key_def.hold_output);
616                let pk = key::PressedKeyResult::Pending(th_pks);
617                let pke = match maybe_sch_ev {
618                    Some(sch_ev) => key::KeyEvents::scheduled_event(sch_ev.into_scheduled_event()),
619                    None => key::KeyEvents::no_events(),
620                };
621                (pk, pke)
622            }
623        }
624    }
625
626    fn update_pending_state(
627        &self,
628        pending_state: &mut Self::PendingKeyState,
629        keymap_index: u16,
630        context: &Self::Context,
631        Ref(key_index): Ref,
632        event: key::Event<Self::Event>,
633    ) -> (Option<key::NewPressedKey<R>>, key::KeyEvents<Self::Event>) {
634        let key_def = &self.keys[key_index as usize];
635        let profile = context.profile(key_def.profile);
636        let th_state = pending_state.handle_event(&profile, keymap_index, event);
637        if let Some(th_state) = th_state {
638            let Key { tap, hold, .. } = *key_def;
639            let new_key_ref = match th_state {
640                key::tap_hold::TapHoldState::Tap => tap,
641                key::tap_hold::TapHoldState::Hold => hold,
642            };
643
644            (
645                Some(key::NewPressedKey::key(new_key_ref)),
646                key::KeyEvents::no_events(),
647            )
648        } else {
649            (None, key::KeyEvents::no_events())
650        }
651    }
652
653    fn update_state(
654        &self,
655        _key_state: &mut Self::KeyState,
656        _ref: &Self::Ref,
657        _context: &Self::Context,
658        _keymap_index: u16,
659        _event: key::Event<Self::Event>,
660    ) -> key::KeyEvents<Self::Event> {
661        panic!() // tap_hold has no key state
662    }
663
664    fn key_output(
665        &self,
666        _key_ref: &Self::Ref,
667        _key_state: &Self::KeyState,
668    ) -> Option<key::KeyOutput> {
669        panic!() // tap_hold has no key state
670    }
671
672    fn pending_output(&self, pending_key_state: &Self::PendingKeyState) -> Option<key::KeyOutput> {
673        pending_key_state.output
674    }
675}
676
677#[cfg(test)]
678mod tests {
679    use super::*;
680
681    use crate::key::System as _;
682    use crate::keymap::KeymapContext;
683
684    const TAP: u8 = 1;
685    const HOLD: u8 = 2;
686    const KEYMAP_INDEX: u16 = 0;
687    const OTHER_INDEX: u16 = 1;
688
689    fn context_with(config: Config) -> Context {
690        Context::from_config(config)
691    }
692
693    fn default_context() -> Context {
694        context_with(Config::new())
695    }
696
697    fn config(
698        timeout: Option<u16>,
699        interrupt_response: InterruptResponse,
700        required_idle_time: Option<u16>,
701    ) -> Config {
702        Config {
703            default_profile: Profile {
704                timeout,
705                interrupt_response,
706                required_idle_time,
707                hold_trigger_positions: None,
708                quick_tap_ms: None,
709                pending_output: PendingOutput::NoOutput,
710            },
711            profiles: Slice::from_slice(&[]),
712        }
713    }
714
715    fn system() -> System<u8, [Key<u8>; 1]> {
716        System::new([Key::new(TAP, HOLD)])
717    }
718
719    fn timeout_event(keymap_index: u16) -> key::Event<Event> {
720        key::Event::key_event(keymap_index, Event::TapHoldTimeout)
721    }
722
723    fn press(keymap_index: u16) -> key::Event<Event> {
724        key::Event::Input(input::Event::Press { keymap_index })
725    }
726
727    fn release(keymap_index: u16) -> key::Event<Event> {
728        key::Event::Input(input::Event::Release { keymap_index })
729    }
730
731    // --- sizeof ---
732
733    #[test]
734    fn test_sizeof_ref() {
735        assert_eq!(1, core::mem::size_of::<Ref>());
736    }
737
738    #[test]
739    fn test_sizeof_event() {
740        assert_eq!(0, core::mem::size_of::<Event>());
741    }
742
743    // --- PendingKeyState: Ignore ---
744
745    #[test]
746    fn ignore_own_release_resolves_as_tap() {
747        // Assemble
748        let ctx = context_with(config(
749            Some(DEFAULT_TIMEOUT),
750            InterruptResponse::Ignore,
751            None,
752        ));
753        let mut pks = PendingKeyState::new();
754
755        // Act
756        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, release(KEYMAP_INDEX));
757
758        // Assert
759        assert_eq!(Some(TapHoldState::Tap), resolution);
760    }
761
762    #[test]
763    fn ignore_timeout_resolves_as_hold() {
764        // Assemble
765        let ctx = context_with(config(
766            Some(DEFAULT_TIMEOUT),
767            InterruptResponse::Ignore,
768            None,
769        ));
770        let mut pks = PendingKeyState::new();
771
772        // Act
773        let resolution =
774            pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, timeout_event(KEYMAP_INDEX));
775
776        // Assert
777        assert_eq!(Some(TapHoldState::Hold), resolution);
778    }
779
780    #[test]
781    fn ignore_other_press_does_not_resolve() {
782        // Assemble
783        let ctx = context_with(config(
784            Some(DEFAULT_TIMEOUT),
785            InterruptResponse::Ignore,
786            None,
787        ));
788        let mut pks = PendingKeyState::new();
789
790        // Act
791        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, press(OTHER_INDEX));
792
793        // Assert
794        assert_eq!(None, resolution);
795    }
796
797    #[test]
798    fn ignore_other_release_does_not_resolve() {
799        // Assemble
800        let ctx = context_with(config(
801            Some(DEFAULT_TIMEOUT),
802            InterruptResponse::Ignore,
803            None,
804        ));
805        let mut pks = PendingKeyState::new();
806
807        // Act
808        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, release(OTHER_INDEX));
809
810        // Assert
811        assert_eq!(None, resolution);
812    }
813
814    // --- PendingKeyState: HoldOnKeyPress ---
815
816    #[test]
817    fn hold_on_key_press_other_press_resolves_as_hold() {
818        // Assemble
819        let ctx = context_with(config(
820            Some(DEFAULT_TIMEOUT),
821            InterruptResponse::HoldOnKeyPress,
822            None,
823        ));
824        let mut pks = PendingKeyState::new();
825
826        // Act
827        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, press(OTHER_INDEX));
828
829        // Assert
830        assert_eq!(Some(TapHoldState::Hold), resolution);
831    }
832
833    #[test]
834    fn hold_on_key_press_own_release_resolves_as_tap() {
835        // Assemble
836        let ctx = context_with(config(
837            Some(DEFAULT_TIMEOUT),
838            InterruptResponse::HoldOnKeyPress,
839            None,
840        ));
841        let mut pks = PendingKeyState::new();
842
843        // Act
844        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, release(KEYMAP_INDEX));
845
846        // Assert
847        assert_eq!(Some(TapHoldState::Tap), resolution);
848    }
849
850    #[test]
851    fn hold_on_key_press_timeout_resolves_as_hold() {
852        // Assemble
853        let ctx = context_with(config(
854            Some(DEFAULT_TIMEOUT),
855            InterruptResponse::HoldOnKeyPress,
856            None,
857        ));
858        let mut pks = PendingKeyState::new();
859
860        // Act
861        let resolution =
862            pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, timeout_event(KEYMAP_INDEX));
863
864        // Assert
865        assert_eq!(Some(TapHoldState::Hold), resolution);
866    }
867
868    #[test]
869    fn hold_on_key_press_other_release_does_not_resolve() {
870        // Assemble
871        let ctx = context_with(config(
872            Some(DEFAULT_TIMEOUT),
873            InterruptResponse::HoldOnKeyPress,
874            None,
875        ));
876        let mut pks = PendingKeyState::new();
877
878        // Act
879        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, release(OTHER_INDEX));
880
881        // Assert
882        assert_eq!(None, resolution);
883    }
884
885    // --- PendingKeyState: HoldOnKeyTap ---
886
887    #[test]
888    fn hold_on_key_tap_own_release_resolves_as_tap() {
889        // Assemble
890        let ctx = context_with(config(
891            Some(DEFAULT_TIMEOUT),
892            InterruptResponse::HoldOnKeyTap,
893            None,
894        ));
895        let mut pks = PendingKeyState::new();
896
897        // Act
898        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, release(KEYMAP_INDEX));
899
900        // Assert
901        assert_eq!(Some(TapHoldState::Tap), resolution);
902    }
903
904    #[test]
905    fn hold_on_key_tap_timeout_resolves_as_hold() {
906        // Assemble
907        let ctx = context_with(config(
908            Some(DEFAULT_TIMEOUT),
909            InterruptResponse::HoldOnKeyTap,
910            None,
911        ));
912        let mut pks = PendingKeyState::new();
913
914        // Act
915        let resolution =
916            pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, timeout_event(KEYMAP_INDEX));
917
918        // Assert
919        assert_eq!(Some(TapHoldState::Hold), resolution);
920    }
921
922    #[test]
923    fn hold_on_key_tap_other_press_does_not_resolve() {
924        // Assemble
925        let ctx = context_with(config(
926            Some(DEFAULT_TIMEOUT),
927            InterruptResponse::HoldOnKeyTap,
928            None,
929        ));
930        let mut pks = PendingKeyState::new();
931
932        // Act
933        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, press(OTHER_INDEX));
934
935        // Assert
936        assert_eq!(None, resolution);
937    }
938
939    #[test]
940    fn hold_on_key_tap_other_tap_resolves_as_hold() {
941        // Assemble
942        let ctx = context_with(config(
943            Some(DEFAULT_TIMEOUT),
944            InterruptResponse::HoldOnKeyTap,
945            None,
946        ));
947        let mut pks = PendingKeyState::new();
948        let _ = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, press(OTHER_INDEX));
949
950        // Act
951        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, release(OTHER_INDEX));
952
953        // Assert
954        assert_eq!(Some(TapHoldState::Hold), resolution);
955    }
956
957    #[test]
958    fn hold_on_key_tap_unmatched_other_release_does_not_resolve() {
959        // Assemble: no prior press of OTHER_INDEX tracked
960        let ctx = context_with(config(
961            Some(DEFAULT_TIMEOUT),
962            InterruptResponse::HoldOnKeyTap,
963            None,
964        ));
965        let mut pks = PendingKeyState::new();
966
967        // Act
968        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, release(OTHER_INDEX));
969
970        // Assert
971        assert_eq!(None, resolution);
972    }
973
974    #[test]
975    fn hold_on_key_tap_release_of_different_key_does_not_resolve() {
976        // Assemble: track press of index 1, then release index 2
977        let ctx = context_with(config(
978            Some(DEFAULT_TIMEOUT),
979            InterruptResponse::HoldOnKeyTap,
980            None,
981        ));
982        let mut pks = PendingKeyState::new();
983        let _ = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, press(OTHER_INDEX));
984
985        // Act
986        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, release(2));
987
988        // Assert
989        assert_eq!(None, resolution);
990    }
991
992    // --- System::new_pressed_key ---
993
994    #[test]
995    fn new_pressed_key_is_pending_by_default() {
996        // Assemble
997        let system = system();
998        let ctx = default_context();
999
1000        // Act
1001        let (pkr, _) = system.new_pressed_key(KEYMAP_INDEX, &ctx, Ref(0));
1002
1003        // Assert
1004        assert!(matches!(pkr, key::PressedKeyResult::Pending(_)));
1005    }
1006
1007    #[test]
1008    fn new_pressed_key_schedules_default_timeout() {
1009        // Assemble
1010        let system = system();
1011        let ctx = default_context();
1012
1013        // Act
1014        let (_, events) = system.new_pressed_key(KEYMAP_INDEX, &ctx, Ref(0));
1015
1016        // Assert
1017        let expected = key::KeyEvents::scheduled_event(key::ScheduledEvent::after(
1018            DEFAULT_TIMEOUT,
1019            timeout_event(KEYMAP_INDEX),
1020        ));
1021        assert_eq!(expected, events);
1022    }
1023
1024    #[test]
1025    fn new_pressed_key_with_no_timeout_schedules_nothing() {
1026        // Assemble
1027        let system = system();
1028        let ctx = context_with(config(None, InterruptResponse::Ignore, None));
1029
1030        // Act
1031        let (_, events) = system.new_pressed_key(KEYMAP_INDEX, &ctx, Ref(0));
1032
1033        // Assert
1034        assert_eq!(key::KeyEvents::no_events(), events);
1035    }
1036
1037    #[test]
1038    fn new_pressed_key_with_no_timeout_is_still_pending() {
1039        // Assemble
1040        let system = system();
1041        let ctx = context_with(config(None, InterruptResponse::Ignore, None));
1042
1043        // Act
1044        let (pkr, _) = system.new_pressed_key(KEYMAP_INDEX, &ctx, Ref(0));
1045
1046        // Assert
1047        assert!(matches!(pkr, key::PressedKeyResult::Pending(_)));
1048    }
1049
1050    #[test]
1051    fn new_pressed_key_resolves_as_tap_when_idle_time_insufficient() {
1052        // Assemble
1053        let system = system();
1054        let mut ctx = context_with(config(
1055            Some(DEFAULT_TIMEOUT),
1056            InterruptResponse::Ignore,
1057            Some(100),
1058        ));
1059        ctx.idle_time_ms = 50;
1060
1061        // Act
1062        let (pkr, _) = system.new_pressed_key(KEYMAP_INDEX, &ctx, Ref(0));
1063
1064        // Assert
1065        assert_eq!(
1066            key::PressedKeyResult::NewPressedKey(key::NewPressedKey::key(TAP)),
1067            pkr
1068        );
1069    }
1070
1071    #[test]
1072    fn new_pressed_key_insufficient_idle_emits_no_events() {
1073        // Assemble
1074        let system = system();
1075        let mut ctx = context_with(config(
1076            Some(DEFAULT_TIMEOUT),
1077            InterruptResponse::Ignore,
1078            Some(100),
1079        ));
1080        ctx.idle_time_ms = 50;
1081
1082        // Act
1083        let (_, events) = system.new_pressed_key(KEYMAP_INDEX, &ctx, Ref(0));
1084
1085        // Assert
1086        assert_eq!(key::KeyEvents::no_events(), events);
1087    }
1088
1089    #[test]
1090    fn new_pressed_key_is_pending_when_idle_time_sufficient() {
1091        // Assemble
1092        let system = system();
1093        let mut ctx = context_with(config(
1094            Some(DEFAULT_TIMEOUT),
1095            InterruptResponse::Ignore,
1096            Some(100),
1097        ));
1098        ctx.idle_time_ms = 100;
1099
1100        // Act
1101        let (pkr, _) = system.new_pressed_key(KEYMAP_INDEX, &ctx, Ref(0));
1102
1103        // Assert
1104        assert!(matches!(pkr, key::PressedKeyResult::Pending(_)));
1105    }
1106
1107    #[test]
1108    fn new_pressed_key_schedules_timeout_when_idle_time_sufficient() {
1109        // Assemble
1110        let system = system();
1111        let mut ctx = context_with(config(
1112            Some(DEFAULT_TIMEOUT),
1113            InterruptResponse::Ignore,
1114            Some(100),
1115        ));
1116        ctx.idle_time_ms = 100;
1117
1118        // Act
1119        let (_, events) = system.new_pressed_key(KEYMAP_INDEX, &ctx, Ref(0));
1120
1121        // Assert
1122        let expected = key::KeyEvents::scheduled_event(key::ScheduledEvent::after(
1123            DEFAULT_TIMEOUT,
1124            timeout_event(KEYMAP_INDEX),
1125        ));
1126        assert_eq!(expected, events);
1127    }
1128
1129    // --- System::update_pending_state ---
1130
1131    #[test]
1132    fn update_pending_own_release_resolves_to_tap_ref() {
1133        // Assemble
1134        let system = system();
1135        let ctx = default_context();
1136        let mut pks = PendingKeyState::new();
1137
1138        // Act
1139        let (resolved, _) = system.update_pending_state(
1140            &mut pks,
1141            KEYMAP_INDEX,
1142            &ctx,
1143            Ref(0),
1144            release(KEYMAP_INDEX),
1145        );
1146
1147        // Assert
1148        assert_eq!(Some(key::NewPressedKey::key(TAP)), resolved);
1149    }
1150
1151    #[test]
1152    fn update_pending_timeout_resolves_to_hold_ref() {
1153        // Assemble
1154        let system = system();
1155        let ctx = default_context();
1156        let mut pks = PendingKeyState::new();
1157
1158        // Act
1159        let (resolved, _) = system.update_pending_state(
1160            &mut pks,
1161            KEYMAP_INDEX,
1162            &ctx,
1163            Ref(0),
1164            timeout_event(KEYMAP_INDEX),
1165        );
1166
1167        // Assert
1168        assert_eq!(Some(key::NewPressedKey::key(HOLD)), resolved);
1169    }
1170
1171    #[test]
1172    fn update_pending_hold_on_press_interrupt_resolves_to_hold_ref() {
1173        // Assemble
1174        let system = system();
1175        let ctx = context_with(config(
1176            Some(DEFAULT_TIMEOUT),
1177            InterruptResponse::HoldOnKeyPress,
1178            None,
1179        ));
1180        let mut pks = PendingKeyState::new();
1181
1182        // Act
1183        let (resolved, _) =
1184            system.update_pending_state(&mut pks, KEYMAP_INDEX, &ctx, Ref(0), press(OTHER_INDEX));
1185
1186        // Assert
1187        assert_eq!(Some(key::NewPressedKey::key(HOLD)), resolved);
1188    }
1189
1190    #[test]
1191    fn update_pending_non_resolving_event_yields_none() {
1192        // Assemble
1193        let system = system();
1194        let ctx = default_context();
1195        let mut pks = PendingKeyState::new();
1196
1197        // Act
1198        let (resolved, _) =
1199            system.update_pending_state(&mut pks, KEYMAP_INDEX, &ctx, Ref(0), press(OTHER_INDEX));
1200
1201        // Assert
1202        assert_eq!(None, resolved);
1203    }
1204
1205    #[test]
1206    fn update_pending_resolution_emits_no_events() {
1207        // Assemble
1208        let system = system();
1209        let ctx = default_context();
1210        let mut pks = PendingKeyState::new();
1211
1212        // Act
1213        let (_, events) = system.update_pending_state(
1214            &mut pks,
1215            KEYMAP_INDEX,
1216            &ctx,
1217            Ref(0),
1218            release(KEYMAP_INDEX),
1219        );
1220
1221        // Assert
1222        assert_eq!(key::KeyEvents::no_events(), events);
1223    }
1224
1225    // --- Context ---
1226
1227    #[test]
1228    fn context_update_keymap_context_sets_idle_time() {
1229        // Assemble
1230        let mut ctx = default_context();
1231        let km_ctx = KeymapContext {
1232            idle_time_ms: 42,
1233            ..KeymapContext::new()
1234        };
1235
1236        // Act
1237        ctx.update_keymap_context(&km_ctx);
1238
1239        // Assert
1240        assert_eq!(42, ctx.idle_time_ms);
1241    }
1242
1243    #[test]
1244    fn context_reset_clears_idle_time() {
1245        // Assemble
1246        let mut ctx = default_context();
1247        ctx.idle_time_ms = 99;
1248
1249        // Act
1250        ctx.reset();
1251
1252        // Assert
1253        assert_eq!(0, ctx.idle_time_ms);
1254    }
1255
1256    #[test]
1257    fn context_reset_preserves_config() {
1258        // Assemble
1259        let config = config(None, InterruptResponse::HoldOnKeyPress, Some(50));
1260        let mut ctx = context_with(config);
1261        ctx.idle_time_ms = 99;
1262
1263        // Act
1264        ctx.reset();
1265
1266        // Assert
1267        assert_eq!(config, ctx.config);
1268    }
1269
1270    // --- profiles ---
1271
1272    #[test]
1273    fn test_profile_zero_is_default_fields() {
1274        // Assemble: non-default knobs only on default_profile.
1275        let config = Config {
1276            default_profile: Profile {
1277                timeout: Some(50),
1278                interrupt_response: InterruptResponse::HoldOnKeyPress,
1279                required_idle_time: Some(10),
1280                hold_trigger_positions: None,
1281                quick_tap_ms: None,
1282                pending_output: PendingOutput::NoOutput,
1283            },
1284            profiles: Slice::from_slice(&[]),
1285        };
1286
1287        // Act
1288        let p = config.profile(0);
1289
1290        // Assert: profile 0 is default_profile by identity and field values.
1291        assert_eq!(p, config.default_profile);
1292        assert_eq!(p.timeout, Some(50));
1293        assert_eq!(p.interrupt_response, InterruptResponse::HoldOnKeyPress);
1294        assert_eq!(p.required_idle_time, Some(10));
1295    }
1296
1297    #[test]
1298    fn test_profile_extra_lookup() {
1299        // Assemble: one extra profile at index 1.
1300        let extra = Profile {
1301            timeout: Some(300),
1302            interrupt_response: InterruptResponse::HoldOnKeyTap,
1303            required_idle_time: None,
1304            hold_trigger_positions: None,
1305            quick_tap_ms: None,
1306            pending_output: PendingOutput::NoOutput,
1307        };
1308        let config = Config {
1309            default_profile: Profile {
1310                timeout: Some(200),
1311                interrupt_response: InterruptResponse::Ignore,
1312                required_idle_time: None,
1313                hold_trigger_positions: None,
1314                quick_tap_ms: None,
1315                pending_output: PendingOutput::NoOutput,
1316            },
1317            profiles: Slice::from_slice(&[extra]),
1318        };
1319
1320        // Act / Assert: index 1 is the extra profile.
1321        assert_eq!(config.profile(1), extra);
1322    }
1323
1324    #[test]
1325    fn test_profile_out_of_range_falls_back_to_default() {
1326        // Assemble: only default_profile populated.
1327        let config = Config {
1328            default_profile: Profile {
1329                timeout: Some(200),
1330                interrupt_response: InterruptResponse::Ignore,
1331                required_idle_time: None,
1332                hold_trigger_positions: None,
1333                quick_tap_ms: None,
1334                pending_output: PendingOutput::NoOutput,
1335            },
1336            profiles: Slice::from_slice(&[]),
1337        };
1338
1339        // Act / Assert: unknown id uses default_profile.
1340        assert_eq!(config.profile(9), config.default_profile);
1341    }
1342
1343    #[test]
1344    fn test_key_new_uses_profile_zero() {
1345        // Assemble / Act
1346        let key = Key::new(0u8, 1u8);
1347
1348        // Assert
1349        assert_eq!(key.profile, 0);
1350    }
1351
1352    #[test]
1353    fn test_key_with_profile_stores_index() {
1354        // Assemble / Act
1355        let key = Key::with_profile(0u8, 1u8, 2);
1356
1357        // Assert
1358        assert_eq!(key.profile, 2);
1359    }
1360
1361    // --- hold_trigger_positions ---
1362
1363    #[test]
1364    fn allows_hold_trigger_when_unset_accepts_any_index() {
1365        // Assemble: no hold_trigger_positions on the profile.
1366        let profile = Profile::new();
1367
1368        // Act / Assert: any keymap index may force hold.
1369        assert!(profile.allows_hold_trigger(0));
1370        assert!(profile.allows_hold_trigger(99));
1371    }
1372
1373    #[test]
1374    fn allows_hold_trigger_when_set_accepts_trigger_position() {
1375        // Assemble: hold trigger positions {2, 3}.
1376        let profile = Profile {
1377            hold_trigger_positions: Some(Slice::from_slice(&[2, 3])),
1378            ..Profile::new()
1379        };
1380
1381        // Act / Assert
1382        assert!(profile.allows_hold_trigger(2));
1383        assert!(profile.allows_hold_trigger(3));
1384    }
1385
1386    #[test]
1387    fn allows_hold_trigger_when_set_rejects_non_trigger_position() {
1388        // Assemble: hold trigger positions {2, 3}.
1389        let profile = Profile {
1390            hold_trigger_positions: Some(Slice::from_slice(&[2, 3])),
1391            ..Profile::new()
1392        };
1393
1394        // Act / Assert
1395        assert!(!profile.allows_hold_trigger(0));
1396        assert!(!profile.allows_hold_trigger(1));
1397    }
1398
1399    #[test]
1400    fn hold_on_key_press_non_trigger_press_does_not_resolve() {
1401        // Assemble: HoldOnKeyPress with hold trigger positions {2}; pending TH.
1402        let ctx = context_with(Config {
1403            default_profile: Profile {
1404                timeout: Some(DEFAULT_TIMEOUT),
1405                interrupt_response: InterruptResponse::HoldOnKeyPress,
1406                required_idle_time: None,
1407                hold_trigger_positions: Some(Slice::from_slice(&[2])),
1408                quick_tap_ms: None,
1409                pending_output: PendingOutput::NoOutput,
1410            },
1411            profiles: Slice::from_slice(&[]),
1412        });
1413        let mut pks = PendingKeyState::new();
1414
1415        // Act: interrupt from a non-trigger position (OTHER_INDEX = 1).
1416        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, press(OTHER_INDEX));
1417
1418        // Assert: still pending (not forced to hold).
1419        assert_eq!(None, resolution);
1420    }
1421
1422    #[test]
1423    fn hold_on_key_press_trigger_position_resolves_as_hold() {
1424        // Assemble: HoldOnKeyPress with hold trigger positions {2}.
1425        let ctx = context_with(Config {
1426            default_profile: Profile {
1427                timeout: Some(DEFAULT_TIMEOUT),
1428                interrupt_response: InterruptResponse::HoldOnKeyPress,
1429                required_idle_time: None,
1430                hold_trigger_positions: Some(Slice::from_slice(&[2])),
1431                quick_tap_ms: None,
1432                pending_output: PendingOutput::NoOutput,
1433            },
1434            profiles: Slice::from_slice(&[]),
1435        });
1436        let mut pks = PendingKeyState::new();
1437
1438        // Act: interrupt from trigger position 2.
1439        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, press(2));
1440
1441        // Assert: resolves as hold.
1442        assert_eq!(Some(TapHoldState::Hold), resolution);
1443    }
1444
1445    #[test]
1446    fn hold_on_key_tap_non_trigger_tap_does_not_resolve() {
1447        // Assemble: HoldOnKeyTap with hold trigger positions {2}; track non-trigger press.
1448        let ctx = context_with(Config {
1449            default_profile: Profile {
1450                timeout: Some(DEFAULT_TIMEOUT),
1451                interrupt_response: InterruptResponse::HoldOnKeyTap,
1452                required_idle_time: None,
1453                hold_trigger_positions: Some(Slice::from_slice(&[2])),
1454                quick_tap_ms: None,
1455                pending_output: PendingOutput::NoOutput,
1456            },
1457            profiles: Slice::from_slice(&[]),
1458        });
1459        let mut pks = PendingKeyState::new();
1460        let _ = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, press(OTHER_INDEX));
1461
1462        // Act: complete the non-trigger key's tap (release).
1463        let resolution = pks.handle_event(&ctx.profile(0), KEYMAP_INDEX, release(OTHER_INDEX));
1464
1465        // Assert: tap of a non-trigger key does not force hold.
1466        assert_eq!(None, resolution);
1467    }
1468
1469    #[test]
1470    fn profile_zero_includes_hold_trigger_positions() {
1471        // Assemble: default_profile carries hold trigger positions.
1472        let triggers = Slice::from_slice(&[2, 3]);
1473        let config = Config {
1474            default_profile: Profile {
1475                timeout: Some(DEFAULT_TIMEOUT),
1476                interrupt_response: InterruptResponse::HoldOnKeyPress,
1477                required_idle_time: None,
1478                hold_trigger_positions: Some(triggers),
1479                quick_tap_ms: None,
1480                pending_output: PendingOutput::NoOutput,
1481            },
1482            profiles: Slice::from_slice(&[]),
1483        };
1484
1485        // Act
1486        let p = config.profile(0);
1487
1488        // Assert: profile 0 exposes the same hold trigger positions.
1489        assert_eq!(p.hold_trigger_positions, Some(triggers));
1490    }
1491
1492    #[test]
1493    fn profile_extra_includes_hold_trigger_positions() {
1494        // Assemble: extra profile (index 1) with its own hold trigger positions.
1495        let triggers = Slice::from_slice(&[5]);
1496        let extra = Profile {
1497            timeout: Some(300),
1498            interrupt_response: InterruptResponse::HoldOnKeyTap,
1499            required_idle_time: None,
1500            hold_trigger_positions: Some(triggers),
1501            quick_tap_ms: None,
1502            pending_output: PendingOutput::NoOutput,
1503        };
1504        let config = Config {
1505            default_profile: Profile::new(),
1506            profiles: Slice::from_slice(&[extra]),
1507        };
1508
1509        // Act / Assert
1510        assert_eq!(config.profile(1).hold_trigger_positions, Some(triggers));
1511    }
1512
1513    // --- quick_tap_ms ---
1514
1515    #[test]
1516    fn is_quick_tap_when_repress_within_window() {
1517        // Assemble: profile with quick_tap_ms; prior press of same index recorded.
1518        let mut ctx = context_with(Config {
1519            default_profile: Profile {
1520                quick_tap_ms: Some(175),
1521                ..Profile::new()
1522            },
1523            profiles: Slice::from_slice(&[]),
1524        });
1525        ctx.update_keymap_context(&KeymapContext {
1526            time_ms: 100,
1527            idle_time_ms: 50,
1528            pressed_modifiers: key::KeyboardModifiers::NONE,
1529            recent_presses: {
1530                let mut presses = [(0, 0); keymap::MAX_RECENT_PRESSES];
1531                presses[0] = (KEYMAP_INDEX, 50);
1532                presses
1533            },
1534            recent_press_count: 1,
1535        });
1536
1537        // Act / Assert: 50ms since last press of KEYMAP_INDEX < 175.
1538        assert!(ctx.is_quick_tap(&ctx.profile(0), KEYMAP_INDEX));
1539    }
1540
1541    #[test]
1542    fn is_quick_tap_false_after_window() {
1543        // Assemble: prior press outside quick_tap_ms window.
1544        let mut ctx = context_with(Config {
1545            default_profile: Profile {
1546                quick_tap_ms: Some(100),
1547                ..Profile::new()
1548            },
1549            profiles: Slice::from_slice(&[]),
1550        });
1551        ctx.update_keymap_context(&KeymapContext {
1552            time_ms: 200,
1553            idle_time_ms: 150,
1554            pressed_modifiers: key::KeyboardModifiers::NONE,
1555            recent_presses: {
1556                let mut presses = [(0, 0); keymap::MAX_RECENT_PRESSES];
1557                presses[0] = (KEYMAP_INDEX, 50);
1558                presses
1559            },
1560            recent_press_count: 1,
1561        });
1562
1563        // Act / Assert: 150ms since last press >= 100.
1564        assert!(!ctx.is_quick_tap(&ctx.profile(0), KEYMAP_INDEX));
1565    }
1566
1567    #[test]
1568    fn is_quick_tap_false_for_other_keymap_index() {
1569        // Assemble: recent press is a different index.
1570        let mut ctx = context_with(Config {
1571            default_profile: Profile {
1572                quick_tap_ms: Some(175),
1573                ..Profile::new()
1574            },
1575            profiles: Slice::from_slice(&[]),
1576        });
1577        ctx.update_keymap_context(&KeymapContext {
1578            time_ms: 100,
1579            idle_time_ms: 50,
1580            pressed_modifiers: key::KeyboardModifiers::NONE,
1581            recent_presses: {
1582                let mut presses = [(0, 0); keymap::MAX_RECENT_PRESSES];
1583                presses[0] = (OTHER_INDEX, 50);
1584                presses
1585            },
1586            recent_press_count: 1,
1587        });
1588
1589        // Act / Assert: no prior press of KEYMAP_INDEX in the ring.
1590        assert!(!ctx.is_quick_tap(&ctx.profile(0), KEYMAP_INDEX));
1591    }
1592
1593    #[test]
1594    fn is_quick_tap_disabled_when_unset() {
1595        // Assemble: default profile has quick_tap_ms = None.
1596        let mut ctx = default_context();
1597        ctx.update_keymap_context(&KeymapContext {
1598            time_ms: 100,
1599            idle_time_ms: 50,
1600            pressed_modifiers: key::KeyboardModifiers::NONE,
1601            recent_presses: {
1602                let mut presses = [(0, 0); keymap::MAX_RECENT_PRESSES];
1603                presses[0] = (KEYMAP_INDEX, 50);
1604                presses
1605            },
1606            recent_press_count: 1,
1607        });
1608
1609        // Act / Assert
1610        assert!(!ctx.is_quick_tap(&ctx.profile(0), KEYMAP_INDEX));
1611    }
1612
1613    #[test]
1614    fn new_pressed_key_quick_tap_resolves_as_tap() {
1615        // Assemble: quick_tap_ms set; same index pressed recently.
1616        let mut ctx = context_with(Config {
1617            default_profile: Profile {
1618                quick_tap_ms: Some(175),
1619                ..Profile::new()
1620            },
1621            profiles: Slice::from_slice(&[]),
1622        });
1623        ctx.update_keymap_context(&KeymapContext {
1624            time_ms: 100,
1625            idle_time_ms: 999,
1626            pressed_modifiers: key::KeyboardModifiers::NONE,
1627            recent_presses: {
1628                let mut presses = [(0, 0); keymap::MAX_RECENT_PRESSES];
1629                presses[0] = (KEYMAP_INDEX, 50);
1630                presses
1631            },
1632            recent_press_count: 1,
1633        });
1634        let sys = system();
1635
1636        // Act
1637        let (pkr, _) = sys.new_pressed_key(KEYMAP_INDEX, &ctx, Ref(0));
1638
1639        // Assert: immediate tap (not pending), even though idle is long.
1640        assert!(matches!(
1641            pkr,
1642            key::PressedKeyResult::NewPressedKey(key::NewPressedKey::Key(TAP))
1643        ));
1644    }
1645
1646    #[test]
1647    fn profile_zero_includes_quick_tap_ms() {
1648        // Assemble
1649        let config = Config {
1650            default_profile: Profile {
1651                quick_tap_ms: Some(175),
1652                ..Profile::new()
1653            },
1654            profiles: Slice::from_slice(&[]),
1655        };
1656
1657        // Act / Assert
1658        assert_eq!(config.profile(0).quick_tap_ms, Some(175));
1659    }
1660}