Skip to main content

ftui_core/
key_sequence.rs

1#![forbid(unsafe_code)]
2
3//! Key sequence interpreter for multi-key sequences (bd-2vne.2).
4//!
5//! This module provides a stateful interpreter for detecting key sequences like
6//! Esc Esc, independent of the low-level input parsing. It operates on the
7//! [`KeyEvent`] stream and uses a configurable timeout window to detect sequences.
8//!
9//! # Design
10//!
11//! ## Invariants
12//! 1. Sequences are always non-empty when emitted.
13//! 2. The timeout window is measured from the first key in a potential sequence.
14//! 3. If no sequence is detected within the timeout, the buffered key(s) are
15//!    emitted individually via [`KeySequenceAction::Emit`].
16//! 4. Non-blocking: [`KeySequenceAction::Pending`] signals that more input is
17//!    needed, but the caller can continue with other work.
18//!
19//! ## Failure Modes
20//! - If the timeout expires mid-sequence, buffered keys are flushed as individual
21//!   [`Emit`](KeySequenceAction::Emit) actions (graceful degradation).
22//! - Unknown keys that don't match any sequence pattern are passed through immediately.
23//!
24//! # Example
25//!
26//! ```
27//! use ftui_core::key_sequence::{KeySequenceInterpreter, KeySequenceConfig, KeySequenceAction};
28//! use ftui_core::event::{KeyEvent, KeyCode, KeyEventKind, Modifiers};
29//! use std::time::{Duration, Instant};
30//!
31//! let config = KeySequenceConfig::default();
32//! let mut interp = KeySequenceInterpreter::new(config);
33//!
34//! let esc = KeyEvent {
35//!     code: KeyCode::Escape,
36//!     modifiers: Modifiers::NONE,
37//!     kind: KeyEventKind::Press,
38//! };
39//!
40//! let now = Instant::now();
41//!
42//! // First Esc: pending (waiting for potential second Esc)
43//! let action = interp.feed(&esc, now);
44//! assert!(matches!(action, KeySequenceAction::Pending));
45//!
46//! // Second Esc within timeout: emit sequence
47//! let action = interp.feed(&esc, now + Duration::from_millis(100));
48//! assert!(matches!(action, KeySequenceAction::EmitSequence { .. }));
49//! ```
50
51use web_time::{Duration, Instant};
52
53use crate::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
54
55// ---------------------------------------------------------------------------
56// Configuration
57// ---------------------------------------------------------------------------
58
59/// Configuration for key sequence detection.
60#[derive(Debug, Clone)]
61pub struct KeySequenceConfig {
62    /// Time window for sequence completion (default: 250ms).
63    ///
64    /// If a second key doesn't arrive within this window, the first key
65    /// is emitted as a single key event.
66    pub sequence_timeout: Duration,
67
68    /// Whether to detect Esc Esc sequences (default: true).
69    pub detect_double_escape: bool,
70}
71
72impl Default for KeySequenceConfig {
73    fn default() -> Self {
74        Self {
75            sequence_timeout: Duration::from_millis(250),
76            detect_double_escape: true,
77        }
78    }
79}
80
81impl KeySequenceConfig {
82    /// Create a config with a custom timeout.
83    #[must_use]
84    pub fn with_timeout(timeout: Duration) -> Self {
85        Self {
86            sequence_timeout: timeout,
87            ..Default::default()
88        }
89    }
90}
91
92// ---------------------------------------------------------------------------
93// KeySequenceKind
94// ---------------------------------------------------------------------------
95
96/// Recognized key sequence patterns.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub enum KeySequenceKind {
99    /// Double Escape (Esc Esc) - typically used for tree view toggle.
100    DoubleEscape,
101}
102
103impl KeySequenceKind {
104    /// Human-readable name for this sequence.
105    #[must_use]
106    pub const fn name(&self) -> &'static str {
107        match self {
108            Self::DoubleEscape => "Esc Esc",
109        }
110    }
111}
112
113// ---------------------------------------------------------------------------
114// KeySequenceAction
115// ---------------------------------------------------------------------------
116
117/// Action returned by the key sequence interpreter.
118#[derive(Debug, Clone, PartialEq)]
119pub enum KeySequenceAction {
120    /// Emit the key event immediately (no sequence detected).
121    Emit(KeyEvent),
122
123    /// A complete key sequence was detected.
124    EmitSequence {
125        /// The kind of sequence that was detected.
126        kind: KeySequenceKind,
127        /// The raw key events that formed the sequence.
128        keys: Vec<KeyEvent>,
129    },
130
131    /// Waiting for more keys to complete a potential sequence.
132    ///
133    /// The caller should continue with other work; the interpreter will
134    /// return the buffered keys if the timeout expires.
135    Pending,
136}
137
138impl KeySequenceAction {
139    /// Returns true if this action requires the caller to wait for more input.
140    #[must_use]
141    pub const fn is_pending(&self) -> bool {
142        matches!(self, Self::Pending)
143    }
144
145    /// Returns true if this action emits a sequence.
146    #[must_use]
147    pub const fn is_sequence(&self) -> bool {
148        matches!(self, Self::EmitSequence { .. })
149    }
150}
151
152// ---------------------------------------------------------------------------
153// KeySequenceInterpreter
154// ---------------------------------------------------------------------------
155
156/// Stateful interpreter for multi-key sequences.
157///
158/// Feed key events via [`feed`](Self::feed) and periodically call
159/// [`check_timeout`](Self::check_timeout) to handle expired sequences.
160pub struct KeySequenceInterpreter {
161    config: KeySequenceConfig,
162
163    /// Buffer for pending keys.
164    buffer: Vec<KeyEvent>,
165
166    /// Timestamp of the first key in the current buffer.
167    buffer_start: Option<Instant>,
168}
169
170impl std::fmt::Debug for KeySequenceInterpreter {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        f.debug_struct("KeySequenceInterpreter")
173            .field("buffer_len", &self.buffer.len())
174            .field("has_pending", &self.buffer_start.is_some())
175            .finish()
176    }
177}
178
179impl KeySequenceInterpreter {
180    /// Create a new key sequence interpreter with the given configuration.
181    #[must_use]
182    pub fn new(config: KeySequenceConfig) -> Self {
183        Self {
184            config,
185            buffer: Vec::with_capacity(4),
186            buffer_start: None,
187        }
188    }
189
190    /// Create a new interpreter with default configuration.
191    #[must_use]
192    pub fn with_defaults() -> Self {
193        Self::new(KeySequenceConfig::default())
194    }
195
196    /// Feed a key event into the interpreter.
197    ///
198    /// Returns an action indicating what the caller should do:
199    /// - [`Emit`](KeySequenceAction::Emit): Pass this key through immediately
200    /// - [`EmitSequence`](KeySequenceAction::EmitSequence): A sequence was detected
201    /// - [`Pending`](KeySequenceAction::Pending): Waiting for more keys
202    ///
203    /// # Key Event Filtering
204    ///
205    /// Only key press events are processed. Release and repeat events are
206    /// passed through immediately.
207    ///
208    /// # Timeout Handling
209    ///
210    /// This method does NOT automatically handle timeouts. Callers should
211    /// periodically call [`check_timeout`](Self::check_timeout) (e.g., on tick)
212    /// to flush expired sequences. If a timeout has expired and you call `feed()`
213    /// without calling `check_timeout()` first, buffered keys may be lost.
214    pub fn feed(&mut self, event: &KeyEvent, now: Instant) -> KeySequenceAction {
215        // Only process key press events
216        if event.kind != KeyEventKind::Press {
217            return KeySequenceAction::Emit(*event);
218        }
219
220        // Check if this key could start or continue a sequence
221        match self.try_sequence(event, now) {
222            SequenceResult::Complete(kind) => {
223                // Add the completing key to the sequence
224                self.buffer.push(*event);
225                let keys = std::mem::take(&mut self.buffer);
226                self.buffer_start = None;
227                KeySequenceAction::EmitSequence { kind, keys }
228            }
229            SequenceResult::Continue => {
230                if self.buffer.is_empty() {
231                    self.buffer_start = Some(now);
232                }
233                self.buffer.push(*event);
234                KeySequenceAction::Pending
235            }
236            SequenceResult::NoMatch => {
237                // This key doesn't match any sequence pattern
238                // If we have buffered keys, we need to flush them first
239                if !self.buffer.is_empty() {
240                    // The buffered keys didn't form a sequence, flush them
241                    // For now, just clear and pass through the new key
242                    // The caller should have called check_timeout to get the buffered keys
243                    self.buffer.clear();
244                    self.buffer_start = None;
245                }
246                KeySequenceAction::Emit(*event)
247            }
248        }
249    }
250
251    /// Check if the sequence timeout has expired.
252    ///
253    /// Call this periodically (e.g., on tick) to flush expired sequences.
254    /// Returns buffered keys as individual [`Emit`](KeySequenceAction::Emit) actions
255    /// if the timeout has expired.
256    ///
257    /// Returns `None` if no timeout has expired or no keys are pending.
258    pub fn check_timeout(&mut self, now: Instant) -> Option<Vec<KeySequenceAction>> {
259        if let Some(start) = self.buffer_start {
260            if now.duration_since(start) >= self.config.sequence_timeout {
261                // Timeout expired - flush all buffered keys
262                let actions: Vec<_> = self.buffer.drain(..).map(KeySequenceAction::Emit).collect();
263                self.buffer_start = None;
264                if actions.is_empty() {
265                    None
266                } else {
267                    Some(actions)
268                }
269            } else {
270                None
271            }
272        } else {
273            None
274        }
275    }
276
277    /// Returns true if there are pending keys waiting for a potential sequence.
278    #[must_use]
279    pub fn has_pending(&self) -> bool {
280        self.buffer_start.is_some()
281    }
282
283    /// Get the time remaining until the current pending sequence times out.
284    ///
285    /// Returns `None` if there are no pending keys.
286    #[must_use]
287    pub fn time_until_timeout(&self, now: Instant) -> Option<Duration> {
288        self.buffer_start.map(|start| {
289            let elapsed = now.duration_since(start);
290            self.config.sequence_timeout.saturating_sub(elapsed)
291        })
292    }
293
294    /// Reset all state, discarding any pending keys.
295    pub fn reset(&mut self) {
296        self.buffer.clear();
297        self.buffer_start = None;
298    }
299
300    /// Flush any pending keys immediately as individual emit actions.
301    ///
302    /// Useful when the application needs to ensure all keys are processed
303    /// before a state transition (e.g., on focus loss).
304    pub fn flush(&mut self) -> Vec<KeySequenceAction> {
305        let actions: Vec<_> = self.buffer.drain(..).map(KeySequenceAction::Emit).collect();
306        self.buffer_start = None;
307        actions
308    }
309
310    /// Get a reference to the current configuration.
311    #[must_use]
312    pub fn config(&self) -> &KeySequenceConfig {
313        &self.config
314    }
315
316    /// Update the configuration.
317    ///
318    /// Note: This does not affect keys already in the buffer.
319    pub fn set_config(&mut self, config: KeySequenceConfig) {
320        self.config = config;
321    }
322}
323
324// ---------------------------------------------------------------------------
325// Internal
326// ---------------------------------------------------------------------------
327
328/// Result of trying to match a sequence pattern.
329#[derive(Debug, Clone, Copy)]
330enum SequenceResult {
331    /// A complete sequence was detected.
332    Complete(KeySequenceKind),
333    /// Key could be part of a sequence, continue buffering.
334    Continue,
335    /// Key doesn't match any sequence pattern.
336    NoMatch,
337}
338
339impl KeySequenceInterpreter {
340    /// Try to match the current key against known sequence patterns.
341    fn try_sequence(&self, event: &KeyEvent, _now: Instant) -> SequenceResult {
342        // Double Escape detection
343        if self.config.detect_double_escape
344            && event.code == KeyCode::Escape
345            && event.modifiers == Modifiers::NONE
346        {
347            // Check if we already have an Escape in the buffer
348            if self.buffer.len() == 1
349                && self.buffer[0].code == KeyCode::Escape
350                && self.buffer[0].modifiers == Modifiers::NONE
351            {
352                return SequenceResult::Complete(KeySequenceKind::DoubleEscape);
353            }
354            // This could be the first Escape of a double-escape sequence
355            if self.buffer.is_empty() {
356                return SequenceResult::Continue;
357            }
358        }
359
360        // No sequence pattern matched
361        SequenceResult::NoMatch
362    }
363}
364
365// ---------------------------------------------------------------------------
366// Tests
367// ---------------------------------------------------------------------------
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    fn now() -> Instant {
374        Instant::now()
375    }
376
377    fn esc() -> KeyEvent {
378        KeyEvent {
379            code: KeyCode::Escape,
380            modifiers: Modifiers::NONE,
381            kind: KeyEventKind::Press,
382        }
383    }
384
385    fn key(c: char) -> KeyEvent {
386        KeyEvent {
387            code: KeyCode::Char(c),
388            modifiers: Modifiers::NONE,
389            kind: KeyEventKind::Press,
390        }
391    }
392
393    fn key_release(c: char) -> KeyEvent {
394        KeyEvent {
395            code: KeyCode::Char(c),
396            modifiers: Modifiers::NONE,
397            kind: KeyEventKind::Release,
398        }
399    }
400
401    const MS_50: Duration = Duration::from_millis(50);
402    const MS_100: Duration = Duration::from_millis(100);
403    const MS_300: Duration = Duration::from_millis(300);
404
405    // --- Double Escape tests ---
406
407    #[test]
408    fn double_escape_within_timeout() {
409        let mut interp = KeySequenceInterpreter::with_defaults();
410        let t = now();
411
412        // First Esc
413        let action = interp.feed(&esc(), t);
414        assert!(matches!(action, KeySequenceAction::Pending));
415        assert!(interp.has_pending());
416
417        // Second Esc within timeout
418        let action = interp.feed(&esc(), t + MS_100);
419        assert!(matches!(
420            action,
421            KeySequenceAction::EmitSequence {
422                kind: KeySequenceKind::DoubleEscape,
423                ..
424            }
425        ));
426        assert!(!interp.has_pending());
427    }
428
429    #[test]
430    fn single_escape_timeout() {
431        let mut interp = KeySequenceInterpreter::with_defaults();
432        let t = now();
433
434        // First Esc
435        let action = interp.feed(&esc(), t);
436        assert!(matches!(action, KeySequenceAction::Pending));
437
438        // Timeout check after 300ms (default timeout is 250ms)
439        let actions = interp.check_timeout(t + MS_300);
440        assert!(actions.is_some());
441        let actions = actions.unwrap();
442        assert_eq!(actions.len(), 1);
443        assert!(matches!(actions[0], KeySequenceAction::Emit(_)));
444    }
445
446    #[test]
447    fn escape_then_different_key() {
448        let mut interp = KeySequenceInterpreter::with_defaults();
449        let t = now();
450
451        // First Esc
452        let action = interp.feed(&esc(), t);
453        assert!(matches!(action, KeySequenceAction::Pending));
454
455        // Different key - should emit immediately (Esc was cleared by timeout logic)
456        let action = interp.feed(&key('a'), t + MS_50);
457        assert!(matches!(action, KeySequenceAction::Emit(_)));
458        assert!(!interp.has_pending());
459    }
460
461    #[test]
462    fn non_escape_key_passes_through() {
463        let mut interp = KeySequenceInterpreter::with_defaults();
464        let t = now();
465
466        let action = interp.feed(&key('x'), t);
467        assert!(matches!(action, KeySequenceAction::Emit(_)));
468        assert!(!interp.has_pending());
469    }
470
471    #[test]
472    fn key_release_passes_through() {
473        let mut interp = KeySequenceInterpreter::with_defaults();
474        let t = now();
475
476        let action = interp.feed(&key_release('x'), t);
477        assert!(matches!(action, KeySequenceAction::Emit(_)));
478        assert!(!interp.has_pending());
479    }
480
481    #[test]
482    fn modified_escape_passes_through() {
483        let mut interp = KeySequenceInterpreter::with_defaults();
484        let t = now();
485
486        // Ctrl+Escape should not start a sequence
487        let ctrl_esc = KeyEvent {
488            code: KeyCode::Escape,
489            modifiers: Modifiers::CTRL,
490            kind: KeyEventKind::Press,
491        };
492
493        let action = interp.feed(&ctrl_esc, t);
494        assert!(matches!(action, KeySequenceAction::Emit(_)));
495        assert!(!interp.has_pending());
496    }
497
498    // --- Configuration tests ---
499
500    #[test]
501    fn custom_timeout() {
502        let config = KeySequenceConfig::with_timeout(Duration::from_millis(100));
503        let mut interp = KeySequenceInterpreter::new(config);
504        let t = now();
505
506        // First Esc
507        interp.feed(&esc(), t);
508        assert!(interp.has_pending());
509
510        // Before timeout (50ms)
511        assert!(interp.check_timeout(t + MS_50).is_none());
512
513        // After timeout (150ms > 100ms)
514        let actions = interp.check_timeout(t + Duration::from_millis(150));
515        assert!(actions.is_some());
516    }
517
518    #[test]
519    fn disabled_double_escape() {
520        let config = KeySequenceConfig {
521            detect_double_escape: false,
522            ..Default::default()
523        };
524        let mut interp = KeySequenceInterpreter::new(config);
525        let t = now();
526
527        // First Esc - should pass through immediately since detection is disabled
528        let action = interp.feed(&esc(), t);
529        assert!(matches!(action, KeySequenceAction::Emit(_)));
530        assert!(!interp.has_pending());
531    }
532
533    // --- Helper method tests ---
534
535    #[test]
536    fn time_until_timeout() {
537        let mut interp = KeySequenceInterpreter::with_defaults();
538        let t = now();
539
540        assert!(interp.time_until_timeout(t).is_none());
541
542        interp.feed(&esc(), t);
543        let remaining = interp.time_until_timeout(t + MS_100);
544        assert!(remaining.is_some());
545        let remaining = remaining.unwrap();
546        // Default timeout is 250ms, we're at 100ms, so ~150ms remaining
547        assert!(remaining >= Duration::from_millis(140));
548        assert!(remaining <= Duration::from_millis(160));
549    }
550
551    #[test]
552    fn reset_clears_state() {
553        let mut interp = KeySequenceInterpreter::with_defaults();
554        let t = now();
555
556        interp.feed(&esc(), t);
557        assert!(interp.has_pending());
558
559        interp.reset();
560        assert!(!interp.has_pending());
561    }
562
563    #[test]
564    fn flush_returns_pending_keys() {
565        let mut interp = KeySequenceInterpreter::with_defaults();
566        let t = now();
567
568        interp.feed(&esc(), t);
569        assert!(interp.has_pending());
570
571        let actions = interp.flush();
572        assert_eq!(actions.len(), 1);
573        assert!(matches!(actions[0], KeySequenceAction::Emit(_)));
574        assert!(!interp.has_pending());
575    }
576
577    #[test]
578    fn flush_on_empty_returns_empty() {
579        let mut interp = KeySequenceInterpreter::with_defaults();
580        let actions = interp.flush();
581        assert!(actions.is_empty());
582    }
583
584    #[test]
585    fn config_getter_and_setter() {
586        let mut interp = KeySequenceInterpreter::with_defaults();
587
588        assert_eq!(interp.config().sequence_timeout, Duration::from_millis(250));
589
590        let new_config = KeySequenceConfig::with_timeout(Duration::from_millis(500));
591        interp.set_config(new_config);
592
593        assert_eq!(interp.config().sequence_timeout, Duration::from_millis(500));
594    }
595
596    #[test]
597    fn debug_format() {
598        let interp = KeySequenceInterpreter::with_defaults();
599        let dbg = format!("{:?}", interp);
600        assert!(dbg.contains("KeySequenceInterpreter"));
601    }
602
603    // --- Action method tests ---
604
605    #[test]
606    fn action_is_pending() {
607        assert!(KeySequenceAction::Pending.is_pending());
608        assert!(!KeySequenceAction::Emit(esc()).is_pending());
609        assert!(
610            !KeySequenceAction::EmitSequence {
611                kind: KeySequenceKind::DoubleEscape,
612                keys: vec![],
613            }
614            .is_pending()
615        );
616    }
617
618    #[test]
619    fn action_is_sequence() {
620        assert!(!KeySequenceAction::Pending.is_sequence());
621        assert!(!KeySequenceAction::Emit(esc()).is_sequence());
622        assert!(
623            KeySequenceAction::EmitSequence {
624                kind: KeySequenceKind::DoubleEscape,
625                keys: vec![],
626            }
627            .is_sequence()
628        );
629    }
630
631    // --- KeySequenceKind tests ---
632
633    #[test]
634    fn sequence_kind_name() {
635        assert_eq!(KeySequenceKind::DoubleEscape.name(), "Esc Esc");
636    }
637
638    // --- Default config tests ---
639
640    #[test]
641    fn default_config_values() {
642        let config = KeySequenceConfig::default();
643        assert_eq!(config.sequence_timeout, Duration::from_millis(250));
644        assert!(config.detect_double_escape);
645    }
646
647    // --- Edge cases ---
648
649    #[test]
650    fn triple_escape_produces_sequence_then_pending() {
651        let mut interp = KeySequenceInterpreter::with_defaults();
652        let t = now();
653
654        // First Esc - pending
655        let action = interp.feed(&esc(), t);
656        assert!(matches!(action, KeySequenceAction::Pending));
657
658        // Second Esc - sequence
659        let action = interp.feed(&esc(), t + MS_50);
660        assert!(matches!(
661            action,
662            KeySequenceAction::EmitSequence {
663                kind: KeySequenceKind::DoubleEscape,
664                ..
665            }
666        ));
667
668        // Third Esc - starts new sequence (pending)
669        let action = interp.feed(&esc(), t + MS_100);
670        assert!(matches!(action, KeySequenceAction::Pending));
671    }
672
673    #[test]
674    fn sequence_keys_are_captured() {
675        let mut interp = KeySequenceInterpreter::with_defaults();
676        let t = now();
677
678        interp.feed(&esc(), t);
679        let action = interp.feed(&esc(), t + MS_50);
680
681        if let KeySequenceAction::EmitSequence { keys, kind } = action {
682            // Buffer should contain BOTH escape keys that formed the sequence
683            assert_eq!(keys.len(), 2, "Sequence should capture both keys");
684            assert_eq!(keys[0].code, KeyCode::Escape);
685            assert_eq!(keys[1].code, KeyCode::Escape);
686            assert_eq!(kind, KeySequenceKind::DoubleEscape);
687        } else {
688            panic!("Expected EmitSequence");
689        }
690    }
691
692    #[test]
693    fn rapid_non_escape_keys() {
694        let mut interp = KeySequenceInterpreter::with_defaults();
695        let t = now();
696
697        // Rapid regular keys should all pass through immediately
698        for (i, c) in "hello".chars().enumerate() {
699            let action = interp.feed(&key(c), t + Duration::from_millis(i as u64 * 10));
700            assert!(
701                matches!(action, KeySequenceAction::Emit(_)),
702                "Key '{}' should pass through",
703                c
704            );
705        }
706        assert!(!interp.has_pending());
707    }
708}