Skip to main content

smart_keymap_core/key/
sequence.rs

1//! Sequence keys (ordered key sequences).
2//!
3//! After a [Ref::SequenceStart](crate::key::sequence::Ref::SequenceStart) key arms sequence
4//! mode on [Context](crate::key::sequence::Context), subsequent presses of
5//! sequence member keys append to a buffer in
6//! [Context](crate::key::sequence::Context). An exact match resolves to a bound
7//! key (looked up from primary sequence keys).
8//!
9//! Behaviour (v1):
10//! - SequenceStart: no HID output; arms mode (or restarts if already armed).
11//! - Steps: press edges only; buffer lives on
12//!   [Context](crate::key::sequence::Context) (no pending session).
13//! - Timeout: per-step, refreshed on each valid step; exact match on timeout
14//!   resolves (first config entry wins); strict prefix only aborts.
15//! - Unknown / dead path: abort without sequence output.
16//! - When mode is inactive, member keys act as passthrough.
17
18use core::fmt::Debug;
19use core::ops::Index;
20
21use serde::Deserialize;
22
23use crate::{input, key, keymap, slice::Slice};
24
25/// Reference for a sequence key.
26#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
27pub enum Ref {
28    /// Primary sequence key (lowest index in a sequence definition).
29    ///
30    /// Owns the resolved nested key for each sequence where this index is primary.
31    /// Other sequence members are [`Ref::Auxiliary`].
32    Sequence(u8),
33    /// Non-primary sequence member.
34    Auxiliary(u8),
35    /// Arms (or restarts) sequence mode. JSON/Nickel token is `SequenceStart`.
36    SequenceStart,
37}
38
39/// Identifier of a sequence in [`Config::sequences`].
40pub type SequenceId = u8;
41
42/// Ordered keymap indices for one sequence.
43#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
44#[serde(from = "heapless::Vec<u16, MAX_SEQUENCE_LEN>")]
45pub struct SequenceIndices<const MAX_SEQUENCE_LEN: usize> {
46    indices: Slice<u16, MAX_SEQUENCE_LEN>,
47}
48
49impl<const MAX_SEQUENCE_LEN: usize> SequenceIndices<MAX_SEQUENCE_LEN> {
50    /// Constructs from a slice (must fit `MAX_SEQUENCE_LEN`).
51    pub const fn from_slice(indices: &[u16]) -> Self {
52        Self {
53            indices: Slice::from_slice(indices),
54        }
55    }
56
57    /// Indices as a slice.
58    pub const fn as_slice(&self) -> &[u16] {
59        self.indices.as_slice()
60    }
61}
62
63impl<const MAX_SEQUENCE_LEN: usize> From<heapless::Vec<u16, MAX_SEQUENCE_LEN>>
64    for SequenceIndices<MAX_SEQUENCE_LEN>
65{
66    fn from(v: heapless::Vec<u16, MAX_SEQUENCE_LEN>) -> Self {
67        Self::from_slice(&v)
68    }
69}
70
71/// Sequence definitions and timing.
72#[derive(Deserialize, Clone, Copy, PartialEq)]
73pub struct Config<const MAX_SEQUENCES: usize, const MAX_SEQUENCE_LEN: usize> {
74    /// Per-step timeout in milliseconds (refreshed on each valid step).
75    #[serde(default = "default_timeout")]
76    pub timeout: u16,
77
78    /// Sequences as ordered keymap index lists.
79    pub sequences: Slice<SequenceIndices<MAX_SEQUENCE_LEN>, MAX_SEQUENCES>,
80
81    /// Minimum idle time (ms) before SequenceStart can arm mode.
82    pub required_idle_time: Option<u16>,
83}
84
85impl<const MAX_SEQUENCES: usize, const MAX_SEQUENCE_LEN: usize> core::fmt::Debug
86    for Config<MAX_SEQUENCES, MAX_SEQUENCE_LEN>
87{
88    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
89        f.debug_struct("Config")
90            .field("timeout", &self.timeout)
91            .field("sequences", &self.sequences.as_slice())
92            .field("required_idle_time", &self.required_idle_time)
93            .finish()
94    }
95}
96
97/// Default per-step timeout (ms).
98pub const DEFAULT_TIMEOUT: u16 = 1000;
99
100const fn default_timeout() -> u16 {
101    DEFAULT_TIMEOUT
102}
103
104impl<const MAX_SEQUENCES: usize, const MAX_SEQUENCE_LEN: usize>
105    Config<MAX_SEQUENCES, MAX_SEQUENCE_LEN>
106{
107    /// Empty config with default timeout.
108    pub const fn new() -> Self {
109        Self {
110            timeout: DEFAULT_TIMEOUT,
111            sequences: Slice::from_slice(&[]),
112            required_idle_time: None,
113        }
114    }
115}
116
117impl<const MAX_SEQUENCES: usize, const MAX_SEQUENCE_LEN: usize> Default
118    for Config<MAX_SEQUENCES, MAX_SEQUENCE_LEN>
119{
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125/// Outcome of the most recent press while sequence mode was considered.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum PressOutcome {
128    /// Mode was not armed for this press.
129    Inactive,
130    /// Step accepted; waiting for more keys or timeout.
131    Continue,
132    /// Sequence completed with this id — emit bound key.
133    Resolved(SequenceId),
134    /// Aborted; no sequence output.
135    Aborted,
136}
137
138/// Global sequence mode state.
139#[derive(Clone, Copy, PartialEq)]
140pub struct Context<const MAX_SEQUENCES: usize, const MAX_SEQUENCE_LEN: usize> {
141    config: Config<MAX_SEQUENCES, MAX_SEQUENCE_LEN>,
142    mode_active: bool,
143    idle_time_ms: u32,
144    timeout_generation: u16,
145    buffer: [u16; MAX_SEQUENCE_LEN],
146    buffer_len: usize,
147    /// Set on each Input press; read by sequence keys in `new_pressed_key`.
148    last_press_outcome: PressOutcome,
149}
150
151impl<const MAX_SEQUENCES: usize, const MAX_SEQUENCE_LEN: usize> Debug
152    for Context<MAX_SEQUENCES, MAX_SEQUENCE_LEN>
153{
154    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
155        f.debug_struct("Context")
156            .field("config", &self.config)
157            .field("mode_active", &self.mode_active)
158            .field("idle_time_ms", &self.idle_time_ms)
159            .field("timeout_generation", &self.timeout_generation)
160            .field("buffer", &&self.buffer[..self.buffer_len])
161            .field("last_press_outcome", &self.last_press_outcome)
162            .finish()
163    }
164}
165
166impl<const MAX_SEQUENCES: usize, const MAX_SEQUENCE_LEN: usize>
167    Context<MAX_SEQUENCES, MAX_SEQUENCE_LEN>
168{
169    /// Constructs from config.
170    pub const fn from_config(config: Config<MAX_SEQUENCES, MAX_SEQUENCE_LEN>) -> Self {
171        Self {
172            config,
173            mode_active: false,
174            idle_time_ms: 0,
175            timeout_generation: 0,
176            buffer: [0; MAX_SEQUENCE_LEN],
177            buffer_len: 0,
178            last_press_outcome: PressOutcome::Inactive,
179        }
180    }
181
182    /// Clears runtime mode state; keeps config.
183    pub fn reset(&mut self) {
184        *self = Self::from_config(self.config);
185    }
186
187    /// Whether sequence mode is armed.
188    pub fn is_armed(&self) -> bool {
189        self.mode_active
190    }
191
192    /// Config reference.
193    pub fn config(&self) -> &Config<MAX_SEQUENCES, MAX_SEQUENCE_LEN> {
194        &self.config
195    }
196
197    /// Outcome of the latest input press (for sequence keys).
198    pub fn last_press_outcome(&self) -> PressOutcome {
199        self.last_press_outcome
200    }
201
202    fn sufficient_idle_time(&self) -> bool {
203        self.idle_time_ms >= self.config.required_idle_time.unwrap_or(0) as u32
204    }
205
206    fn bump_timeout(&mut self) -> u16 {
207        self.timeout_generation = self.timeout_generation.wrapping_add(1);
208        self.timeout_generation
209    }
210
211    fn arm(&mut self) {
212        self.mode_active = true;
213        self.buffer_len = 0;
214        self.bump_timeout();
215        self.last_press_outcome = PressOutcome::Inactive;
216    }
217
218    fn disarm(&mut self) {
219        self.mode_active = false;
220        self.buffer_len = 0;
221        self.bump_timeout();
222    }
223
224    fn buffer_slice(&self) -> &[u16] {
225        &self.buffer[..self.buffer_len]
226    }
227
228    fn schedule_timeout(&self, gen_id: u16) -> key::KeyEvents<Event> {
229        key::KeyEvents::scheduled_event(key::ScheduledEvent::after(
230            self.config.timeout,
231            key::Event::key_event(0, Event::Timeout(gen_id)),
232        ))
233    }
234
235    fn candidates_for_buffer(&self) -> heapless::Vec<SequenceId, MAX_SEQUENCES> {
236        let buffer = self.buffer_slice();
237        self.config
238            .sequences
239            .iter()
240            .enumerate()
241            .filter(|(_, seq)| {
242                let s = seq.as_slice();
243                s.len() >= buffer.len() && s[..buffer.len()] == *buffer
244            })
245            .map(|(id, _)| id as SequenceId)
246            .collect()
247    }
248
249    fn exact_match_id(&self, candidates: &[SequenceId]) -> Option<SequenceId> {
250        let buffer = self.buffer_slice();
251        candidates
252            .iter()
253            .copied()
254            .find(|&id| self.config.sequences[id as usize].as_slice() == buffer)
255    }
256
257    fn has_longer(&self, candidates: &[SequenceId]) -> bool {
258        candidates
259            .iter()
260            .any(|&id| self.config.sequences[id as usize].as_slice().len() > self.buffer_len)
261    }
262
263    /// Append a press and update [Self::last_press_outcome].
264    fn step_press(&mut self, keymap_index: u16) {
265        if self.buffer_len >= MAX_SEQUENCE_LEN {
266            self.disarm();
267            self.last_press_outcome = PressOutcome::Aborted;
268        } else {
269            self.buffer[self.buffer_len] = keymap_index;
270            self.buffer_len += 1;
271            let candidates = self.candidates_for_buffer();
272            match (
273                self.exact_match_id(&candidates),
274                self.has_longer(&candidates),
275            ) {
276                (Some(id), false) => {
277                    self.disarm();
278                    self.last_press_outcome = PressOutcome::Resolved(id);
279                }
280                (None, false) => {
281                    // No candidates (dead path).
282                    self.disarm();
283                    self.last_press_outcome = PressOutcome::Aborted;
284                }
285                _ => {
286                    // Still waiting (maybe exact+longer, or only longer).
287                    self.last_press_outcome = PressOutcome::Continue;
288                    self.bump_timeout();
289                }
290            }
291        }
292    }
293
294    /// Updates idle time from the keymap engine.
295    pub fn update_keymap_context(
296        &mut self,
297        keymap::KeymapContext { idle_time_ms, .. }: &keymap::KeymapContext,
298    ) {
299        self.idle_time_ms = *idle_time_ms;
300    }
301
302    fn handle_event(&mut self, event: key::Event<Event>) -> key::KeyEvents<Event> {
303        match event {
304            key::Event::Input(input::Event::Press { keymap_index }) => {
305                if self.mode_active {
306                    self.step_press(keymap_index);
307                    match self.last_press_outcome {
308                        PressOutcome::Continue => self.schedule_timeout(self.timeout_generation),
309                        PressOutcome::Resolved(id) => key::KeyEvents::event(key::Event::key_event(
310                            keymap_index,
311                            Event::SequenceResolved(id),
312                        )),
313                        PressOutcome::Aborted => key::KeyEvents::event(key::Event::key_event(
314                            keymap_index,
315                            Event::Aborted,
316                        )),
317                        PressOutcome::Inactive => key::KeyEvents::no_events(),
318                    }
319                } else {
320                    self.last_press_outcome = PressOutcome::Inactive;
321                    key::KeyEvents::no_events()
322                }
323            }
324            key::Event::Key {
325                key_event: Event::Arm,
326                ..
327            } => {
328                if self.sufficient_idle_time() {
329                    self.arm();
330                    self.schedule_timeout(self.timeout_generation)
331                } else {
332                    key::KeyEvents::no_events()
333                }
334            }
335            key::Event::Key {
336                key_event: Event::Restart,
337                ..
338            } => {
339                self.arm();
340                self.schedule_timeout(self.timeout_generation)
341            }
342            key::Event::Key {
343                key_event: Event::Timeout(gen),
344                ..
345            } => {
346                if self.mode_active && gen == self.timeout_generation {
347                    // v1: timeout always aborts without sequence output
348                    //  (including exact match).
349                    // Emitting a binding needs a press path; pure timeout has none.
350                    self.disarm();
351                    self.last_press_outcome = PressOutcome::Aborted;
352                }
353                key::KeyEvents::no_events()
354            }
355            _ => key::KeyEvents::no_events(),
356        }
357    }
358}
359
360impl<const MAX_SEQUENCES: usize, const MAX_SEQUENCE_LEN: usize> key::Context
361    for Context<MAX_SEQUENCES, MAX_SEQUENCE_LEN>
362{
363    type Event = Event;
364
365    fn handle_event(&mut self, event: key::Event<Self::Event>) -> key::KeyEvents<Self::Event> {
366        self.handle_event(event)
367    }
368
369    fn reset(&mut self) {
370        Context::reset(self);
371    }
372}
373
374/// Sequence family events.
375#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
376pub enum Event {
377    /// Arm sequence mode (from SequenceStart when inactive).
378    Arm,
379    /// Restart sequence mode (from SequenceStart when already armed).
380    Restart,
381    /// Timeout; generation must match context generation.
382    Timeout(u16),
383    /// A sequence resolved.
384    SequenceResolved(SequenceId),
385    /// Sequence aborted.
386    Aborted,
387}
388
389/// No pending state (buffer lives on [`Context`]).
390#[derive(Debug, Clone, Copy, PartialEq)]
391pub struct PendingKeyState;
392
393/// No key state for sequence keys (resolution nests to another ref).
394#[derive(Debug, Clone, Copy, PartialEq)]
395pub struct KeyState;
396
397/// Primary sequence key: bindings + passthrough.
398#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
399pub struct Key<R: Copy, const MAX_OVERLAPPING: usize> {
400    /// Sequences for which this key is primary (lowest index), with bound keys.
401    pub sequences: Slice<(SequenceId, R), MAX_OVERLAPPING>,
402    /// Key when sequence mode is inactive.
403    pub passthrough: R,
404}
405
406impl<R: Copy, const MAX_OVERLAPPING: usize> Key<R, MAX_OVERLAPPING> {
407    /// Constructs a primary sequence key.
408    pub const fn new(sequences: &[(SequenceId, R)], passthrough: R) -> Self {
409        Self {
410            sequences: Slice::from_slice(sequences),
411            passthrough,
412        }
413    }
414
415    /// Bound key for sequence id, if this primary owns it.
416    pub fn binding_for(&self, id: SequenceId) -> Option<R> {
417        self.sequences
418            .iter()
419            .find(|(sid, _)| *sid == id)
420            .map(|(_, r)| *r)
421    }
422}
423
424/// Non-primary sequence member.
425#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
426pub struct AuxiliaryKey<R: Copy> {
427    /// Key when sequence mode is inactive.
428    pub passthrough: R,
429}
430
431impl<R: Copy> AuxiliaryKey<R> {
432    /// Constructs an auxiliary sequence key.
433    pub const fn new(passthrough: R) -> Self {
434        Self { passthrough }
435    }
436}
437
438/// Sequence [key::System].
439#[derive(Debug, Clone, Copy, PartialEq)]
440pub struct System<
441    R: Copy + Debug + PartialEq,
442    Keys: Index<usize, Output = Key<R, MAX_OVERLAPPING>> + AsRef<[Key<R, MAX_OVERLAPPING>]>,
443    AuxiliaryKeys: Index<usize, Output = AuxiliaryKey<R>>,
444    const MAX_SEQUENCES: usize,
445    const MAX_SEQUENCE_LEN: usize,
446    const MAX_OVERLAPPING: usize,
447> {
448    keys: Keys,
449    auxiliary_keys: AuxiliaryKeys,
450}
451
452impl<
453        R: Copy + Debug + PartialEq,
454        Keys: Index<usize, Output = Key<R, MAX_OVERLAPPING>> + AsRef<[Key<R, MAX_OVERLAPPING>]>,
455        AuxiliaryKeys: Index<usize, Output = AuxiliaryKey<R>>,
456        const MAX_SEQUENCES: usize,
457        const MAX_SEQUENCE_LEN: usize,
458        const MAX_OVERLAPPING: usize,
459    > System<R, Keys, AuxiliaryKeys, MAX_SEQUENCES, MAX_SEQUENCE_LEN, MAX_OVERLAPPING>
460{
461    /// Constructs the system from key data arrays.
462    pub const fn new(keys: Keys, auxiliary_keys: AuxiliaryKeys) -> Self {
463        Self {
464            keys,
465            auxiliary_keys,
466        }
467    }
468
469    fn binding_for(&self, id: SequenceId) -> Option<R> {
470        self.keys.as_ref().iter().find_map(|k| k.binding_for(id))
471    }
472}
473
474impl<
475        R: Copy + Debug + PartialEq,
476        Keys: Debug + Index<usize, Output = Key<R, MAX_OVERLAPPING>> + AsRef<[Key<R, MAX_OVERLAPPING>]>,
477        AuxiliaryKeys: Debug + Index<usize, Output = AuxiliaryKey<R>>,
478        const MAX_SEQUENCES: usize,
479        const MAX_SEQUENCE_LEN: usize,
480        const MAX_OVERLAPPING: usize,
481    > key::System<R>
482    for System<R, Keys, AuxiliaryKeys, MAX_SEQUENCES, MAX_SEQUENCE_LEN, MAX_OVERLAPPING>
483{
484    type Ref = Ref;
485    type Context = Context<MAX_SEQUENCES, MAX_SEQUENCE_LEN>;
486    type Event = Event;
487    type PendingKeyState = PendingKeyState;
488    type KeyState = KeyState;
489
490    fn new_pressed_key(
491        &self,
492        keymap_index: u16,
493        context: &Self::Context,
494        key_ref: Ref,
495    ) -> (
496        key::PressedKeyResult<R, Self::PendingKeyState, Self::KeyState>,
497        key::KeyEvents<Self::Event>,
498    ) {
499        match key_ref {
500            Ref::SequenceStart => {
501                let ev = if context.is_armed() {
502                    // Was armed before this press;
503                    //  Context may have aborted on the start index step.
504                    // Restart either way.
505                    Event::Restart
506                } else {
507                    Event::Arm
508                };
509                let pke = key::KeyEvents::event(key::Event::key_event(keymap_index, ev));
510                (
511                    key::PressedKeyResult::NewPressedKey(key::NewPressedKey::NoOp),
512                    pke,
513                )
514            }
515            Ref::Sequence(i) | Ref::Auxiliary(i) => {
516                let passthrough = match key_ref {
517                    Ref::Sequence(idx) => self.keys[idx as usize].passthrough,
518                    Ref::Auxiliary(idx) => self.auxiliary_keys[idx as usize].passthrough,
519                    Ref::SequenceStart => unreachable!(),
520                };
521                let _ = i;
522
523                // Context already processed this Input press
524                //  (handle_event before new_pressed_key).
525                // Use last_press_outcome.
526                match context.last_press_outcome() {
527                    PressOutcome::Inactive => (
528                        key::PressedKeyResult::NewPressedKey(key::NewPressedKey::key(passthrough)),
529                        key::KeyEvents::no_events(),
530                    ),
531                    PressOutcome::Continue | PressOutcome::Aborted => (
532                        key::PressedKeyResult::NewPressedKey(key::NewPressedKey::NoOp),
533                        key::KeyEvents::no_events(),
534                    ),
535                    PressOutcome::Resolved(id) => {
536                        if let Some(r) = self.binding_for(id) {
537                            (
538                                key::PressedKeyResult::NewPressedKey(key::NewPressedKey::key(r)),
539                                key::KeyEvents::no_events(),
540                            )
541                        } else {
542                            (
543                                key::PressedKeyResult::NewPressedKey(key::NewPressedKey::NoOp),
544                                key::KeyEvents::no_events(),
545                            )
546                        }
547                    }
548                }
549            }
550        }
551    }
552
553    fn update_pending_state(
554        &self,
555        _pending_state: &mut Self::PendingKeyState,
556        _keymap_index: u16,
557        _context: &Self::Context,
558        _key_ref: Ref,
559        _event: key::Event<Self::Event>,
560    ) -> (Option<key::NewPressedKey<R>>, key::KeyEvents<Self::Event>) {
561        (None, key::KeyEvents::no_events())
562    }
563
564    fn update_state(
565        &self,
566        _key_state: &mut Self::KeyState,
567        _key_ref: &Self::Ref,
568        _context: &Self::Context,
569        _keymap_index: u16,
570        _event: key::Event<Self::Event>,
571    ) -> key::KeyEvents<Self::Event> {
572        key::KeyEvents::no_events()
573    }
574
575    fn key_output(
576        &self,
577        _key_ref: &Self::Ref,
578        _key_state: &Self::KeyState,
579    ) -> Option<key::KeyOutput> {
580        None
581    }
582}
583
584#[cfg(test)]
585#[allow(clippy::unwrap_used, clippy::expect_used)]
586mod tests {
587    use super::*;
588
589    const MAX_SEQUENCES: usize = 4;
590    const MAX_SEQUENCE_LEN: usize = 4;
591
592    type Ctx = Context<MAX_SEQUENCES, MAX_SEQUENCE_LEN>;
593
594    fn ctx_with(sequences: &[&[u16]]) -> Ctx {
595        match sequences.len() {
596            1 => Context::from_config(Config {
597                sequences: Slice::from_slice(&[SequenceIndices::from_slice(sequences[0])]),
598                ..Config::new()
599            }),
600            2 => Context::from_config(Config {
601                sequences: Slice::from_slice(&[
602                    SequenceIndices::from_slice(sequences[0]),
603                    SequenceIndices::from_slice(sequences[1]),
604                ]),
605                ..Config::new()
606            }),
607            _ => Context::from_config(Config::new()),
608        }
609    }
610
611    #[test]
612    fn start_arms_mode() {
613        let mut ctx = Ctx::from_config(Config::new());
614        let _ = ctx.handle_event(key::Event::key_event(0, Event::Arm));
615        assert!(ctx.is_armed());
616    }
617
618    #[test]
619    fn two_step_resolves() {
620        // Assemble: context with sequence [0, 1]
621        let mut ctx = ctx_with(&[&[0, 1]]);
622
623        // Act: start sequence, input 0, input 1.
624        let _ = ctx.handle_event(key::Event::key_event(9, Event::Arm));
625        assert!(ctx.is_armed());
626        let _ = ctx.handle_event(key::Event::Input(input::Event::Press { keymap_index: 0 }));
627        assert_eq!(ctx.last_press_outcome(), PressOutcome::Continue);
628        let _ = ctx.handle_event(key::Event::Input(input::Event::Press { keymap_index: 1 }));
629
630        // Assert: should resolve
631        assert_eq!(ctx.last_press_outcome(), PressOutcome::Resolved(0));
632        assert!(!ctx.is_armed());
633    }
634
635    #[test]
636    fn unknown_aborts() {
637        // Assemble: context with sequence [0, 1]
638        let mut ctx = ctx_with(&[&[0, 1]]);
639
640        // Act: start sequence, press 0, press 9 (not part of any sequence)
641        let _ = ctx.handle_event(key::Event::key_event(9, Event::Arm));
642        let _ = ctx.handle_event(key::Event::Input(input::Event::Press { keymap_index: 0 }));
643        let _ = ctx.handle_event(key::Event::Input(input::Event::Press { keymap_index: 9 }));
644
645        // Assert: should abort the sequence key
646        assert_eq!(ctx.last_press_outcome(), PressOutcome::Aborted);
647        assert!(!ctx.is_armed());
648    }
649
650    #[test]
651    fn timeout_aborts_strict_prefix() {
652        // Assemble: context with sequence [0, 1, 2]
653        let mut ctx = ctx_with(&[&[0, 1, 2]]);
654
655        // Act: start sequence, press 0; wait
656        let _ = ctx.handle_event(key::Event::key_event(9, Event::Arm));
657        let _ = ctx.handle_event(key::Event::Input(input::Event::Press { keymap_index: 0 }));
658        let gen = ctx.timeout_generation;
659        let _ = ctx.handle_event(key::Event::key_event(0, Event::Timeout(gen)));
660
661        // Assert: should abort the sequence key
662        assert_eq!(ctx.last_press_outcome(), PressOutcome::Aborted);
663        assert!(!ctx.is_armed());
664    }
665}