Skip to main content

ftui_core/
keybinding.rs

1#![forbid(unsafe_code)]
2
3//! Keybinding sequence detection and action mapping.
4//!
5//! This module implements the keybinding policy specification (bd-2vne.1) for
6//! detecting multi-key sequences like Esc Esc and mapping keys to actions based
7//! on application state.
8//!
9//! # Key Concepts
10//!
11//! - **SequenceDetector**: State machine that detects Esc Esc sequences with
12//!   configurable timeout. Single Esc is emitted after timeout or when another
13//!   key is pressed.
14//!
15//! - **SequenceConfig**: Configuration for sequence detection including timeout
16//!   windows and debounce settings.
17//!
18//! - **ActionMapper**: Maps key events to high-level actions based on application
19//!   state (input buffer, running tasks, modals, overlays). Integrates with
20//!   SequenceDetector to handle Esc sequences.
21//!
22//! - **AppState**: Runtime state flags that affect action resolution.
23//!
24//! - **Action**: High-level commands like ClearInput, CancelTask, ToggleTreeView.
25//!
26//! # State Machine
27//!
28//! ```text
29//!                                     ┌─────────────────────────────────────┐
30//!                                     │                                     │
31//!                                     ▼                                     │
32//! ┌──────────┐   Esc   ┌────────────────────┐  timeout    ┌─────────┐      │
33//! │  Idle    │───────▶│  AwaitingSecondEsc  │────────────▶│ Emit(Esc)│      │
34//! └──────────┘         └────────────────────┘              └─────────┘      │
35//!      ▲                        │                                           │
36//!      │                        │ Esc (within timeout)                      │
37//!      │                        ▼                                           │
38//!      │               ┌─────────────────┐                                  │
39//!      │               │ Emit(EscEsc)    │──────────────────────────────────┘
40//!      │               └─────────────────┘
41//!      │
42//!      │  other key
43//!      └───────────────────────────────────────────────────────────────────
44//! ```
45//!
46//! # Example
47//!
48//! ```
49//! use std::time::{Duration, Instant};
50//! use ftui_core::keybinding::{SequenceDetector, SequenceConfig, SequenceOutput};
51//! use ftui_core::event::{KeyCode, KeyEvent, Modifiers, KeyEventKind};
52//!
53//! let mut detector = SequenceDetector::new(SequenceConfig::default());
54//! let now = Instant::now();
55//!
56//! // First Esc: starts the sequence
57//! let esc = KeyEvent::new(KeyCode::Escape);
58//! let output = detector.feed(&esc, now);
59//! assert!(matches!(output, SequenceOutput::Pending));
60//!
61//! // Second Esc within timeout: emits EscEsc
62//! let later = now + Duration::from_millis(100);
63//! let output = detector.feed(&esc, later);
64//! assert!(matches!(output, SequenceOutput::EscEsc));
65//! ```
66//!
67//! # Action Mapping Example
68//!
69//! ```
70//! use std::time::Instant;
71//! use ftui_core::keybinding::{ActionMapper, ActionConfig, AppState, Action};
72//! use ftui_core::event::{KeyCode, KeyEvent, Modifiers};
73//!
74//! let mut mapper = ActionMapper::new(ActionConfig::default());
75//! let now = Instant::now();
76//!
77//! // Ctrl+C with non-empty input: clears input
78//! let state = AppState { input_nonempty: true, ..Default::default() };
79//! let ctrl_c = KeyEvent::new(KeyCode::Char('c')).with_modifiers(Modifiers::CTRL);
80//! let action = mapper.map(&ctrl_c, &state, now);
81//! assert!(matches!(action, Some(Action::ClearInput)));
82//!
83//! // Ctrl+C with empty input and no task: quits (by default)
84//! let idle_state = AppState::default();
85//! let action = mapper.map(&ctrl_c, &idle_state, now);
86//! assert!(matches!(action, Some(Action::Quit)));
87//! ```
88
89use web_time::{Duration, Instant};
90
91use crate::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
92
93// ---------------------------------------------------------------------------
94// Configuration Constants
95// ---------------------------------------------------------------------------
96
97/// Default timeout for detecting Esc Esc sequence.
98pub const DEFAULT_ESC_SEQ_TIMEOUT_MS: u64 = 250;
99
100/// Minimum allowed value for Esc sequence timeout.
101pub const MIN_ESC_SEQ_TIMEOUT_MS: u64 = 150;
102
103/// Maximum allowed value for Esc sequence timeout.
104pub const MAX_ESC_SEQ_TIMEOUT_MS: u64 = 400;
105
106/// Default debounce before emitting single Esc.
107pub const DEFAULT_ESC_DEBOUNCE_MS: u64 = 50;
108
109/// Minimum allowed value for Esc debounce.
110pub const MIN_ESC_DEBOUNCE_MS: u64 = 0;
111
112/// Maximum allowed value for Esc debounce.
113pub const MAX_ESC_DEBOUNCE_MS: u64 = 100;
114
115// ---------------------------------------------------------------------------
116// Configuration
117// ---------------------------------------------------------------------------
118
119/// Configuration for the sequence detector.
120///
121/// # Timing Defaults
122///
123/// | Setting | Default | Range | Description |
124/// |---------|---------|-------|-------------|
125/// | `esc_seq_timeout` | 250ms | 150-400ms | Window for detecting Esc Esc |
126/// | `esc_debounce` | 50ms | 0-100ms | Minimum wait before single Esc |
127///
128/// # Environment Variables
129///
130/// | Variable | Type | Default | Description |
131/// |----------|------|---------|-------------|
132/// | `FTUI_ESC_SEQ_TIMEOUT_MS` | u64 | 250 | Esc Esc detection window |
133/// | `FTUI_ESC_DEBOUNCE_MS` | u64 | 50 | Minimum Esc wait |
134/// | `FTUI_DISABLE_ESC_SEQ` | bool | false | Disable multi-key sequences |
135///
136/// # Example
137///
138/// ```bash
139/// # Faster double-tap detection (200ms window)
140/// export FTUI_ESC_SEQ_TIMEOUT_MS=200
141///
142/// # Disable Esc Esc entirely (for strict terminals)
143/// export FTUI_DISABLE_ESC_SEQ=1
144/// ```
145#[derive(Debug, Clone)]
146pub struct SequenceConfig {
147    /// Maximum gap between Esc presses to detect Esc Esc sequence.
148    /// Default: 250ms.
149    pub esc_seq_timeout: Duration,
150
151    /// Minimum debounce before emitting single Esc.
152    /// Default: 50ms.
153    pub esc_debounce: Duration,
154
155    /// Whether to disable multi-key sequences entirely.
156    /// When true, all Esc keys are immediately emitted as single Esc.
157    /// Default: false.
158    pub disable_sequences: bool,
159}
160
161impl Default for SequenceConfig {
162    fn default() -> Self {
163        Self {
164            esc_seq_timeout: Duration::from_millis(DEFAULT_ESC_SEQ_TIMEOUT_MS),
165            esc_debounce: Duration::from_millis(DEFAULT_ESC_DEBOUNCE_MS),
166            disable_sequences: false,
167        }
168    }
169}
170
171impl SequenceConfig {
172    /// Create a new config with custom timeout.
173    #[must_use]
174    pub fn with_timeout(mut self, timeout: Duration) -> Self {
175        self.esc_seq_timeout = timeout;
176        self
177    }
178
179    /// Create a new config with custom debounce.
180    #[must_use]
181    pub fn with_debounce(mut self, debounce: Duration) -> Self {
182        self.esc_debounce = debounce;
183        self
184    }
185
186    /// Disable sequence detection (treat all Esc as single).
187    #[must_use]
188    pub fn disable_sequences(mut self) -> Self {
189        self.disable_sequences = true;
190        self
191    }
192
193    /// Load config from environment variables.
194    ///
195    /// Reads:
196    /// - `FTUI_ESC_SEQ_TIMEOUT_MS`: Esc Esc detection window in milliseconds
197    /// - `FTUI_ESC_DEBOUNCE_MS`: Minimum Esc wait in milliseconds
198    /// - `FTUI_DISABLE_ESC_SEQ`: Set to "1" or "true" to disable sequences
199    ///
200    /// Values are automatically clamped to valid ranges.
201    #[must_use]
202    pub fn from_env() -> Self {
203        let mut config = Self::default();
204
205        if let Ok(val) = std::env::var("FTUI_ESC_SEQ_TIMEOUT_MS")
206            && let Ok(ms) = val.parse::<u64>()
207        {
208            config.esc_seq_timeout = Duration::from_millis(ms);
209        }
210
211        if let Ok(val) = std::env::var("FTUI_ESC_DEBOUNCE_MS")
212            && let Ok(ms) = val.parse::<u64>()
213        {
214            config.esc_debounce = Duration::from_millis(ms);
215        }
216
217        if let Ok(val) = std::env::var("FTUI_DISABLE_ESC_SEQ") {
218            config.disable_sequences = val == "1" || val.eq_ignore_ascii_case("true");
219        }
220
221        config.validated()
222    }
223
224    /// Validate and clamp values to safe ranges.
225    ///
226    /// Returns a new config with:
227    /// - `esc_seq_timeout` clamped to 150-400ms
228    /// - `esc_debounce` clamped to 0-100ms
229    /// - `esc_debounce` <= `esc_seq_timeout` (debounce is capped at timeout)
230    ///
231    /// # Example
232    ///
233    /// ```
234    /// use ftui_core::keybinding::SequenceConfig;
235    /// use std::time::Duration;
236    ///
237    /// let config = SequenceConfig::default()
238    ///     .with_timeout(Duration::from_millis(1000))  // Too high
239    ///     .validated();
240    ///
241    /// // Clamped to max 400ms
242    /// assert_eq!(config.esc_seq_timeout.as_millis(), 400);
243    /// ```
244    #[must_use]
245    pub fn validated(mut self) -> Self {
246        // Clamp timeout to valid range
247        let timeout_ms = self.esc_seq_timeout.as_millis() as u64;
248        let clamped_timeout = timeout_ms.clamp(MIN_ESC_SEQ_TIMEOUT_MS, MAX_ESC_SEQ_TIMEOUT_MS);
249        self.esc_seq_timeout = Duration::from_millis(clamped_timeout);
250
251        // Clamp debounce to valid range
252        let debounce_ms = self.esc_debounce.as_millis() as u64;
253        let clamped_debounce = debounce_ms.clamp(MIN_ESC_DEBOUNCE_MS, MAX_ESC_DEBOUNCE_MS);
254
255        // Ensure debounce <= timeout (debounce shouldn't exceed the timeout window)
256        let final_debounce = clamped_debounce.min(clamped_timeout);
257        self.esc_debounce = Duration::from_millis(final_debounce);
258
259        self
260    }
261
262    /// Check if values are within valid ranges.
263    #[must_use]
264    pub fn is_valid(&self) -> bool {
265        let timeout_ms = self.esc_seq_timeout.as_millis() as u64;
266        let debounce_ms = self.esc_debounce.as_millis() as u64;
267
268        (MIN_ESC_SEQ_TIMEOUT_MS..=MAX_ESC_SEQ_TIMEOUT_MS).contains(&timeout_ms)
269            && (MIN_ESC_DEBOUNCE_MS..=MAX_ESC_DEBOUNCE_MS).contains(&debounce_ms)
270            && debounce_ms <= timeout_ms
271    }
272}
273
274// ---------------------------------------------------------------------------
275// Sequence Output
276// ---------------------------------------------------------------------------
277
278/// Output from the sequence detector after processing a key event.
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub enum SequenceOutput {
281    /// No action yet; waiting for timeout or more input.
282    Pending,
283
284    /// Single Escape key was detected.
285    Esc,
286
287    /// Double Escape (Esc Esc) sequence was detected.
288    EscEsc,
289
290    /// Pass through the original key event (not part of a sequence).
291    PassThrough,
292}
293
294// ---------------------------------------------------------------------------
295// Sequence Detector
296// ---------------------------------------------------------------------------
297
298/// Internal state of the sequence detector.
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300enum DetectorState {
301    /// Idle: waiting for input.
302    Idle,
303
304    /// First Esc received; waiting for second or timeout.
305    AwaitingSecondEsc { first_esc_time: Instant },
306}
307
308/// Stateful detector for multi-key sequences (currently Esc Esc).
309///
310/// This detector transforms a stream of [`KeyEvent`]s into [`SequenceOutput`]s,
311/// detecting Esc Esc sequences with configurable timeout handling.
312///
313/// # Usage
314///
315/// Call [`feed`](SequenceDetector::feed) for each key event. The detector returns:
316/// - `Pending`: First Esc received, waiting for more input or timeout.
317/// - `Esc`: Single Esc was detected (after timeout or other key).
318/// - `EscEsc`: Double Esc sequence was detected.
319/// - `PassThrough`: Key is not Esc, pass through to normal handling.
320///
321/// Call [`check_timeout`](SequenceDetector::check_timeout) periodically (e.g., on
322/// tick) to emit pending single Esc after timeout expires.
323#[derive(Debug)]
324pub struct SequenceDetector {
325    config: SequenceConfig,
326    state: DetectorState,
327}
328
329impl SequenceDetector {
330    /// Create a new sequence detector with the given configuration.
331    #[must_use]
332    pub fn new(config: SequenceConfig) -> Self {
333        Self {
334            config,
335            state: DetectorState::Idle,
336        }
337    }
338
339    /// Create a new sequence detector with default configuration.
340    #[must_use]
341    pub fn with_defaults() -> Self {
342        Self::new(SequenceConfig::default())
343    }
344
345    /// Process a key event and return the sequence output.
346    ///
347    /// Only key press events are considered; repeat and release are ignored.
348    pub fn feed(&mut self, event: &KeyEvent, now: Instant) -> SequenceOutput {
349        // Only process press events
350        if event.kind != KeyEventKind::Press {
351            return SequenceOutput::PassThrough;
352        }
353
354        // If sequences are disabled, handle Esc immediately
355        if self.config.disable_sequences {
356            return if event.code == KeyCode::Escape {
357                SequenceOutput::Esc
358            } else {
359                SequenceOutput::PassThrough
360            };
361        }
362
363        match self.state {
364            DetectorState::Idle => {
365                if event.code == KeyCode::Escape {
366                    // First Esc: transition to awaiting second
367                    self.state = DetectorState::AwaitingSecondEsc {
368                        first_esc_time: now,
369                    };
370                    SequenceOutput::Pending
371                } else {
372                    // Non-Esc key: pass through
373                    SequenceOutput::PassThrough
374                }
375            }
376
377            DetectorState::AwaitingSecondEsc { first_esc_time } => {
378                let elapsed = now.saturating_duration_since(first_esc_time);
379
380                if event.code == KeyCode::Escape {
381                    // Second Esc received
382                    if elapsed <= self.config.esc_seq_timeout {
383                        // Within timeout: emit EscEsc
384                        self.state = DetectorState::Idle;
385                        SequenceOutput::EscEsc
386                    } else {
387                        // Past timeout: first Esc already timed out, this starts new
388                        self.state = DetectorState::AwaitingSecondEsc {
389                            first_esc_time: now,
390                        };
391                        SequenceOutput::Esc
392                    }
393                } else {
394                    // Other key received: emit pending Esc, then pass through
395                    // The caller should handle the Esc first, then re-feed this key
396                    self.state = DetectorState::Idle;
397                    // Return Esc; caller must re-feed the current key
398                    SequenceOutput::Esc
399                }
400            }
401        }
402    }
403
404    /// Check for timeout and emit pending Esc if expired.
405    ///
406    /// Call this periodically (e.g., on tick) to handle the case where
407    /// the user pressed Esc once and is waiting.
408    ///
409    /// Returns `Some(SequenceOutput::Esc)` if timeout expired,
410    /// `None` otherwise.
411    pub fn check_timeout(&mut self, now: Instant) -> Option<SequenceOutput> {
412        if let DetectorState::AwaitingSecondEsc { first_esc_time } = self.state {
413            let elapsed = now.saturating_duration_since(first_esc_time);
414            if elapsed > self.config.esc_seq_timeout {
415                self.state = DetectorState::Idle;
416                return Some(SequenceOutput::Esc);
417            }
418        }
419        None
420    }
421
422    /// Whether the detector is waiting for a second Esc.
423    #[must_use]
424    pub fn is_pending(&self) -> bool {
425        matches!(self.state, DetectorState::AwaitingSecondEsc { .. })
426    }
427
428    /// Reset the detector to idle state.
429    ///
430    /// Any pending Esc is discarded.
431    pub fn reset(&mut self) {
432        self.state = DetectorState::Idle;
433    }
434
435    /// Get a reference to the current configuration.
436    #[must_use]
437    pub fn config(&self) -> &SequenceConfig {
438        &self.config
439    }
440
441    /// Update the configuration.
442    ///
443    /// Does not reset pending state.
444    pub fn set_config(&mut self, config: SequenceConfig) {
445        self.config = config;
446    }
447}
448
449// ---------------------------------------------------------------------------
450// Application State
451// ---------------------------------------------------------------------------
452
453/// Runtime state flags that affect keybinding resolution.
454///
455/// These flags are queried at the moment a key event is resolved to an action.
456/// The priority of actions changes based on these flags per the policy spec.
457#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
458pub struct AppState {
459    /// True if the text input buffer contains characters.
460    pub input_nonempty: bool,
461
462    /// True if a background task/command is executing.
463    pub task_running: bool,
464
465    /// True if a modal dialog or overlay is visible.
466    pub modal_open: bool,
467
468    /// True if a secondary view (tree, debug, HUD) is active.
469    pub view_overlay: bool,
470}
471
472impl AppState {
473    /// Create a new state with all flags false.
474    #[must_use]
475    pub const fn new() -> Self {
476        Self {
477            input_nonempty: false,
478            task_running: false,
479            modal_open: false,
480            view_overlay: false,
481        }
482    }
483
484    /// Set input_nonempty flag.
485    #[must_use]
486    pub const fn with_input(mut self, nonempty: bool) -> Self {
487        self.input_nonempty = nonempty;
488        self
489    }
490
491    /// Set task_running flag.
492    #[must_use]
493    pub const fn with_task(mut self, running: bool) -> Self {
494        self.task_running = running;
495        self
496    }
497
498    /// Set modal_open flag.
499    #[must_use]
500    pub const fn with_modal(mut self, open: bool) -> Self {
501        self.modal_open = open;
502        self
503    }
504
505    /// Set view_overlay flag.
506    #[must_use]
507    pub const fn with_overlay(mut self, active: bool) -> Self {
508        self.view_overlay = active;
509        self
510    }
511
512    /// Check if in idle state (no input, no task, no modal).
513    #[must_use]
514    pub const fn is_idle(&self) -> bool {
515        !self.input_nonempty && !self.task_running && !self.modal_open
516    }
517}
518
519// ---------------------------------------------------------------------------
520// Actions
521// ---------------------------------------------------------------------------
522
523/// High-level actions that can result from keybinding resolution.
524///
525/// These actions are returned by the [`ActionMapper`] and should be handled
526/// by the application's event loop.
527#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
528pub enum Action {
529    /// Empty the input buffer, keep cursor at start.
530    ClearInput,
531
532    /// Send cancel signal to running task, update status.
533    CancelTask,
534
535    /// Close topmost modal, return focus to parent.
536    DismissModal,
537
538    /// Deactivate view overlay (tree view, debug HUD).
539    CloseOverlay,
540
541    /// Toggle the tree/file view overlay.
542    ToggleTreeView,
543
544    /// Clean exit via quit command.
545    Quit,
546
547    /// Quit if idle, otherwise cancel current operation.
548    SoftQuit,
549
550    /// Immediate quit (bypass confirmation if any).
551    HardQuit,
552
553    /// Emit terminal bell (BEL character).
554    Bell,
555
556    /// Forward event to focused widget/input.
557    ///
558    /// This indicates the key should be passed through to normal input handling.
559    PassThrough,
560}
561
562impl Action {
563    /// Check if this action consumes the event (vs passing through).
564    #[must_use]
565    pub const fn consumes_event(&self) -> bool {
566        !matches!(self, Action::PassThrough)
567    }
568
569    /// Check if this is a quit-related action.
570    #[must_use]
571    pub const fn is_quit(&self) -> bool {
572        matches!(self, Action::Quit | Action::SoftQuit | Action::HardQuit)
573    }
574}
575
576// ---------------------------------------------------------------------------
577// Ctrl+C Idle Action
578// ---------------------------------------------------------------------------
579
580/// Behavior when Ctrl+C is pressed with empty input and no running task.
581#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
582pub enum CtrlCIdleAction {
583    /// Exit the application.
584    #[default]
585    Quit,
586
587    /// Do nothing.
588    Noop,
589
590    /// Emit terminal bell (BEL).
591    Bell,
592}
593
594impl CtrlCIdleAction {
595    /// Parse from string (environment variable value).
596    #[must_use]
597    pub fn from_str_opt(s: &str) -> Option<Self> {
598        match s.to_lowercase().as_str() {
599            "quit" => Some(Self::Quit),
600            "noop" | "none" | "ignore" => Some(Self::Noop),
601            "bell" | "beep" => Some(Self::Bell),
602            _ => None,
603        }
604    }
605
606    /// Convert to the corresponding Action (or None for Noop).
607    #[must_use]
608    pub const fn to_action(self) -> Option<Action> {
609        match self {
610            Self::Quit => Some(Action::Quit),
611            Self::Noop => None,
612            Self::Bell => Some(Action::Bell),
613        }
614    }
615}
616
617// ---------------------------------------------------------------------------
618// Action Configuration
619// ---------------------------------------------------------------------------
620
621/// Configuration for action mapping behavior.
622///
623/// This struct combines sequence detection settings with keybinding behavior
624/// configuration. It controls how keys like Ctrl+C, Ctrl+D, Esc, and Esc Esc
625/// are interpreted based on application state.
626///
627/// # Environment Variables
628///
629/// | Variable | Type | Default | Description |
630/// |----------|------|---------|-------------|
631/// | `FTUI_CTRL_C_IDLE_ACTION` | string | "quit" | Action when Ctrl+C in idle state |
632/// | `FTUI_ESC_SEQ_TIMEOUT_MS` | u64 | 250 | Esc Esc detection window |
633/// | `FTUI_ESC_DEBOUNCE_MS` | u64 | 50 | Minimum Esc wait |
634/// | `FTUI_DISABLE_ESC_SEQ` | bool | false | Disable Esc Esc sequences |
635///
636/// # Example: Configure via environment
637///
638/// ```bash
639/// # Make Ctrl+C do nothing when idle (instead of quit)
640/// export FTUI_CTRL_C_IDLE_ACTION=noop
641///
642/// # Or make it beep
643/// export FTUI_CTRL_C_IDLE_ACTION=bell
644///
645/// # Faster double-Esc detection
646/// export FTUI_ESC_SEQ_TIMEOUT_MS=200
647/// ```
648///
649/// # Example: Configure in code
650///
651/// ```
652/// use ftui_core::keybinding::{ActionConfig, CtrlCIdleAction, SequenceConfig};
653/// use std::time::Duration;
654///
655/// let config = ActionConfig::default()
656///     .with_ctrl_c_idle(CtrlCIdleAction::Bell)
657///     .with_sequence_config(
658///         SequenceConfig::default()
659///             .with_timeout(Duration::from_millis(200))
660///     );
661/// ```
662#[derive(Debug, Clone)]
663pub struct ActionConfig {
664    /// Sequence detection configuration (timeouts, debounce, disable flag).
665    pub sequence_config: SequenceConfig,
666
667    /// Action when Ctrl+C pressed with empty input and no task.
668    ///
669    /// - `Quit` (default): Exit the application
670    /// - `Noop`: Do nothing
671    /// - `Bell`: Emit terminal bell
672    pub ctrl_c_idle_action: CtrlCIdleAction,
673}
674
675impl Default for ActionConfig {
676    fn default() -> Self {
677        Self {
678            sequence_config: SequenceConfig::default(),
679            ctrl_c_idle_action: CtrlCIdleAction::Quit,
680        }
681    }
682}
683
684impl ActionConfig {
685    /// Create config with custom sequence settings.
686    #[must_use]
687    pub fn with_sequence_config(mut self, config: SequenceConfig) -> Self {
688        self.sequence_config = config;
689        self
690    }
691
692    /// Set Ctrl+C idle action.
693    #[must_use]
694    pub fn with_ctrl_c_idle(mut self, action: CtrlCIdleAction) -> Self {
695        self.ctrl_c_idle_action = action;
696        self
697    }
698
699    /// Load config from environment variables.
700    ///
701    /// Reads:
702    /// - `FTUI_CTRL_C_IDLE_ACTION`: "quit", "noop", or "bell"
703    /// - Plus all environment variables from [`SequenceConfig::from_env`]
704    #[must_use]
705    pub fn from_env() -> Self {
706        let mut config = Self {
707            sequence_config: SequenceConfig::from_env(),
708            ctrl_c_idle_action: CtrlCIdleAction::Quit,
709        };
710
711        if let Ok(val) = std::env::var("FTUI_CTRL_C_IDLE_ACTION")
712            && let Some(action) = CtrlCIdleAction::from_str_opt(&val)
713        {
714            config.ctrl_c_idle_action = action;
715        }
716
717        config
718    }
719
720    /// Validate and return a config with clamped sequence values.
721    ///
722    /// Delegates to [`SequenceConfig::validated`] for timing bounds.
723    #[must_use]
724    pub fn validated(mut self) -> Self {
725        self.sequence_config = self.sequence_config.validated();
726        self
727    }
728}
729
730// ---------------------------------------------------------------------------
731// Action Mapper
732// ---------------------------------------------------------------------------
733
734/// Maps key events to high-level actions based on application state.
735///
736/// The `ActionMapper` integrates the sequence detector and implements the
737/// priority table from the keybinding policy specification (bd-2vne.1).
738///
739/// # Priority Order
740///
741/// Actions are resolved in priority order (first match wins):
742///
743/// | Priority | Condition | Key | Action |
744/// |----------|-----------|-----|--------|
745/// | 1 | `modal_open` | Esc | DismissModal |
746/// | 2 | `modal_open` | Ctrl+C | DismissModal |
747/// | 3 | `input_nonempty` | Ctrl+C | ClearInput |
748/// | 4 | `task_running` | Ctrl+C | CancelTask |
749/// | 5 | idle | Ctrl+C | Quit (configurable) |
750/// | 6 | `view_overlay` | Esc | CloseOverlay |
751/// | 7 | `input_nonempty` | Esc | ClearInput |
752/// | 8 | `task_running` | Esc | CancelTask |
753/// | 9 | always | Esc Esc | ToggleTreeView |
754/// | 10 | always | Ctrl+D | SoftQuit |
755/// | 11 | always | Ctrl+Q | HardQuit |
756///
757/// # Usage
758///
759/// ```
760/// use std::time::Instant;
761/// use ftui_core::keybinding::{ActionMapper, ActionConfig, AppState, Action};
762/// use ftui_core::event::{KeyCode, KeyEvent, Modifiers};
763///
764/// let mut mapper = ActionMapper::new(ActionConfig::default());
765/// let now = Instant::now();
766/// let state = AppState::default();
767///
768/// let key = KeyEvent::new(KeyCode::Char('q')).with_modifiers(Modifiers::CTRL);
769/// let action = mapper.map(&key, &state, now);
770/// assert!(matches!(action, Some(Action::HardQuit)));
771/// ```
772#[derive(Debug)]
773pub struct ActionMapper {
774    config: ActionConfig,
775    sequence_detector: SequenceDetector,
776}
777
778impl ActionMapper {
779    /// Create a new action mapper with the given configuration.
780    #[must_use]
781    pub fn new(config: ActionConfig) -> Self {
782        let sequence_detector = SequenceDetector::new(config.sequence_config.clone());
783        Self {
784            config,
785            sequence_detector,
786        }
787    }
788
789    /// Create a new action mapper with default configuration.
790    #[must_use]
791    pub fn with_defaults() -> Self {
792        Self::new(ActionConfig::default())
793    }
794
795    /// Create a new action mapper loading config from environment.
796    #[must_use]
797    pub fn from_env() -> Self {
798        Self::new(ActionConfig::from_env())
799    }
800
801    /// Map a key event to an action based on current application state.
802    ///
803    /// Returns `Some(action)` if the key resolves to an action, or `None`
804    /// if the event should be ignored (e.g., Noop on Ctrl+C when idle).
805    ///
806    /// # Arguments
807    ///
808    /// * `event` - The key event to process
809    /// * `state` - Current application state flags
810    /// * `now` - Current timestamp for sequence detection
811    pub fn map(&mut self, event: &KeyEvent, state: &AppState, now: Instant) -> Option<Action> {
812        // Only process press events
813        if event.kind != KeyEventKind::Press {
814            return Some(Action::PassThrough);
815        }
816
817        // Check for Ctrl+C, Ctrl+D, Ctrl+Q first (they don't participate in sequences)
818        if event.modifiers.contains(Modifiers::CTRL)
819            && let KeyCode::Char(c) = event.code
820        {
821            match c.to_ascii_lowercase() {
822                'c' => return self.resolve_ctrl_c(state),
823                'd' => return Some(Action::SoftQuit),
824                'q' => return Some(Action::HardQuit),
825                _ => {}
826            }
827        }
828
829        // Handle Escape through sequence detector
830        if event.code == KeyCode::Escape && event.modifiers == Modifiers::NONE {
831            return self.handle_esc_sequence(state, now);
832        }
833
834        // For non-Esc keys, check if we have a pending Esc
835        let seq_output = self.sequence_detector.feed(event, now);
836        match seq_output {
837            SequenceOutput::Esc => {
838                // Pending Esc was interrupted; resolve it and note the key is consumed
839                // The caller should re-feed the current key after handling Esc
840                // For now we return the Esc action; the current key is lost
841                // This matches the spec: "emit pending Esc first, then process"
842                self.resolve_single_esc(state)
843            }
844            SequenceOutput::Pending => {
845                // Should not happen for non-Esc keys
846                Some(Action::PassThrough)
847            }
848            SequenceOutput::EscEsc => {
849                // Should not happen for non-Esc keys
850                Some(Action::ToggleTreeView)
851            }
852            SequenceOutput::PassThrough => Some(Action::PassThrough),
853        }
854    }
855
856    /// Handle Escape key through the sequence detector.
857    fn handle_esc_sequence(&mut self, state: &AppState, now: Instant) -> Option<Action> {
858        let esc_event = KeyEvent::new(KeyCode::Escape);
859        let output = self.sequence_detector.feed(&esc_event, now);
860
861        match output {
862            SequenceOutput::Pending => {
863                // First Esc received, waiting for second
864                // Don't emit action yet; the event loop should call check_timeout
865                None
866            }
867            SequenceOutput::Esc => {
868                // Single Esc detected (either timeout or past timeout second Esc)
869                self.resolve_single_esc(state)
870            }
871            SequenceOutput::EscEsc => {
872                // Double Esc sequence detected
873                Some(Action::ToggleTreeView)
874            }
875            SequenceOutput::PassThrough => {
876                // Should not happen for Esc
877                Some(Action::PassThrough)
878            }
879        }
880    }
881
882    /// Resolve Ctrl+C based on state.
883    fn resolve_ctrl_c(&self, state: &AppState) -> Option<Action> {
884        // Priority 2: modal_open -> DismissModal
885        if state.modal_open {
886            return Some(Action::DismissModal);
887        }
888
889        // Priority 3: input_nonempty -> ClearInput
890        if state.input_nonempty {
891            return Some(Action::ClearInput);
892        }
893
894        // Priority 4: task_running -> CancelTask
895        if state.task_running {
896            return Some(Action::CancelTask);
897        }
898
899        // Priority 5: idle -> configurable action
900        self.config.ctrl_c_idle_action.to_action()
901    }
902
903    /// Resolve single Esc based on state.
904    fn resolve_single_esc(&self, state: &AppState) -> Option<Action> {
905        // Priority 1: modal_open -> DismissModal
906        if state.modal_open {
907            return Some(Action::DismissModal);
908        }
909
910        // Priority 6: view_overlay -> CloseOverlay
911        if state.view_overlay {
912            return Some(Action::CloseOverlay);
913        }
914
915        // Priority 7: input_nonempty -> ClearInput
916        if state.input_nonempty {
917            return Some(Action::ClearInput);
918        }
919
920        // Priority 8: task_running -> CancelTask
921        if state.task_running {
922            return Some(Action::CancelTask);
923        }
924
925        // No action for Esc in idle state
926        Some(Action::PassThrough)
927    }
928
929    /// Check for sequence timeout and return pending action if expired.
930    ///
931    /// Call this periodically (e.g., on tick) to handle single Esc after
932    /// the timeout window closes.
933    ///
934    /// # Arguments
935    ///
936    /// * `state` - Current application state flags
937    /// * `now` - Current timestamp
938    pub fn check_timeout(&mut self, state: &AppState, now: Instant) -> Option<Action> {
939        if let Some(SequenceOutput::Esc) = self.sequence_detector.check_timeout(now) {
940            return self.resolve_single_esc(state);
941        }
942        None
943    }
944
945    /// Whether the mapper is waiting for a second Esc.
946    #[must_use]
947    pub fn is_pending_esc(&self) -> bool {
948        self.sequence_detector.is_pending()
949    }
950
951    /// Reset the sequence detector state.
952    ///
953    /// Any pending Esc is discarded.
954    pub fn reset(&mut self) {
955        self.sequence_detector.reset();
956    }
957
958    /// Get a reference to the current configuration.
959    #[must_use]
960    pub fn config(&self) -> &ActionConfig {
961        &self.config
962    }
963
964    /// Update the configuration.
965    pub fn set_config(&mut self, config: ActionConfig) {
966        self.sequence_detector
967            .set_config(config.sequence_config.clone());
968        self.config = config;
969    }
970}
971
972// ---------------------------------------------------------------------------
973// Tests
974// ---------------------------------------------------------------------------
975
976// ===========================================================================
977// Declarative keymaps: combos, chords, priorities, contexts, conflict
978// detection, and a chord-aware dispatcher
979// ===========================================================================
980//
981// Resolution order, in prose (the keybinding policy spec copies this):
982//
983// 1. A key press extends the pending prefix. If the extended chord is bound
984//    and no longer bound chord starts with it, the binding fires at once.
985//    If a longer bound chord starts with it (`g` while `g g` is bound), the
986//    dispatcher waits: the exact binding fires on the chord timeout or when a
987//    key arrives that cannot extend the chord, so single-key shortcuts are
988//    never blocked, only delayed while a real chord is possible.
989// 2. A key that cannot extend the pending prefix flushes it (the prefix
990//    fires if it is bound, otherwise it is reported as expired) and is then
991//    processed on its own.
992// 3. Among bindings for the same chord, one attached to an active context
993//    beats a context-free one, then the higher `Priority` wins, then the most
994//    recently bound. `KeyMap::conflicts` reports every case that needs the
995//    tie-break so shadowing is visible instead of silent.
996// 4. `Repeat` events re-fire a single-key binding but never extend a chord;
997//    `Release` events are reported as unbound.
998// 5. `Esc` goes through the embedded `SequenceDetector` (one Esc timer per
999//    dispatcher); `Esc` and `Esc Esc` can be bound like any chord.
1000
1001use std::fmt;
1002use std::str::FromStr;
1003
1004/// Why a key, combo, or chord string could not be parsed.
1005#[derive(Debug, Clone, PartialEq, Eq)]
1006pub enum KeyParseError {
1007    /// The key name (or the whole chord) was empty.
1008    EmptyKey,
1009    /// A key name that matches no [`KeyCode`].
1010    UnknownKey(String),
1011    /// A modifier name other than `Ctrl`, `Alt`, `Shift`, `Super`.
1012    UnknownModifier(String),
1013    /// More than [`Chord::MAX_LEN`] combos in one chord.
1014    TooManyKeys(usize),
1015}
1016
1017impl fmt::Display for KeyParseError {
1018    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1019        match self {
1020            Self::EmptyKey => f.write_str("empty key"),
1021            Self::UnknownKey(name) => write!(f, "unknown key `{name}`"),
1022            Self::UnknownModifier(name) => write!(f, "unknown modifier `{name}`"),
1023            Self::TooManyKeys(n) => {
1024                write!(f, "chord has {n} keys; the maximum is {}", Chord::MAX_LEN)
1025            }
1026        }
1027    }
1028}
1029
1030impl std::error::Error for KeyParseError {}
1031
1032/// A single key press with its modifiers (`Ctrl+x`, `Shift+Tab`, `F12`, `g`).
1033///
1034/// Combos are normalized so that `Shift+a`, `A`, and a terminal that reports
1035/// `Char('A')` with the Shift bit all compare equal: alphabetic characters
1036/// are stored lowercase with [`Modifiers::SHIFT`] set.
1037#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1038pub struct KeyCombo {
1039    /// The key.
1040    pub code: KeyCode,
1041    /// Modifier keys held.
1042    pub modifiers: Modifiers,
1043}
1044
1045impl KeyCombo {
1046    /// Build a normalized combo.
1047    #[must_use]
1048    pub fn new(code: KeyCode, modifiers: Modifiers) -> Self {
1049        match code {
1050            KeyCode::Char(c) if c.is_alphabetic() && c.is_uppercase() => Self {
1051                code: KeyCode::Char(c.to_lowercase().next().unwrap_or(c)),
1052                modifiers: modifiers | Modifiers::SHIFT,
1053            },
1054            _ => Self { code, modifiers },
1055        }
1056    }
1057
1058    /// A combo without modifiers.
1059    #[must_use]
1060    pub fn key(code: KeyCode) -> Self {
1061        Self::new(code, Modifiers::NONE)
1062    }
1063
1064    /// The combo a key event represents (its kind is ignored).
1065    #[must_use]
1066    pub fn from_event(event: &KeyEvent) -> Self {
1067        Self::new(event.code, event.modifiers)
1068    }
1069
1070    /// Whether `event` presses this combo (any kind).
1071    #[must_use]
1072    pub fn matches(&self, event: &KeyEvent) -> bool {
1073        Self::from_event(event) == *self
1074    }
1075}
1076
1077/// Whether writing `c` uppercased is a faithful way to spell its SHIFT.
1078///
1079/// [`KeyCombo::new`] canonicalizes an uppercase letter to lowercase plus
1080/// SHIFT, and [`Display`](fmt::Display) inverts that by uppercasing and
1081/// dropping the modifier. The inverse only exists when uppercasing gives
1082/// exactly one character, different from `c`, that lowercases back to it.
1083/// Three kinds of letter break it, and `is_alphabetic()` admits all three:
1084///
1085/// - Caseless ones. `中` uppercases to itself, so the SHIFT simply vanished
1086///   and `Shift+中` printed as `中`.
1087/// - Ones whose uppercase is several characters. `ß` uppercases to `SS`,
1088///   which is not a key name, so `Shift+ß` printed as `SS` and no longer
1089///   parsed at all.
1090/// - Ones that do not lowercase back. `ı` uppercases to `I`, which
1091///   lowercases to `i`, so the round trip would land on the wrong letter.
1092///
1093/// In each case SHIFT stays a modifier and the letter is printed as itself.
1094fn shift_folds_into(c: char) -> bool {
1095    let mut upper = c.to_uppercase();
1096    match (upper.next(), upper.next()) {
1097        (Some(u), None) => u != c && u.to_lowercase().eq(std::iter::once(c)),
1098        _ => false,
1099    }
1100}
1101
1102impl fmt::Display for KeyCombo {
1103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1104        let mut modifiers = self.modifiers;
1105        let key = match self.code {
1106            KeyCode::Char(c) if modifiers.contains(Modifiers::SHIFT) && shift_folds_into(c) => {
1107                modifiers.remove(Modifiers::SHIFT);
1108                c.to_uppercase().collect::<String>()
1109            }
1110            KeyCode::Char(' ') => "Space".to_string(),
1111            KeyCode::Char(c) => c.to_string(),
1112            KeyCode::Enter => "Enter".to_string(),
1113            KeyCode::Escape => "Esc".to_string(),
1114            KeyCode::Backspace => "Backspace".to_string(),
1115            KeyCode::Tab => "Tab".to_string(),
1116            KeyCode::BackTab => "BackTab".to_string(),
1117            KeyCode::Delete => "Delete".to_string(),
1118            KeyCode::Insert => "Insert".to_string(),
1119            KeyCode::Home => "Home".to_string(),
1120            KeyCode::End => "End".to_string(),
1121            KeyCode::PageUp => "PageUp".to_string(),
1122            KeyCode::PageDown => "PageDown".to_string(),
1123            KeyCode::Up => "Up".to_string(),
1124            KeyCode::Down => "Down".to_string(),
1125            KeyCode::Left => "Left".to_string(),
1126            KeyCode::Right => "Right".to_string(),
1127            KeyCode::F(n) => format!("F{n}"),
1128            KeyCode::Null => "Null".to_string(),
1129            KeyCode::MediaPlayPause => "MediaPlayPause".to_string(),
1130            KeyCode::MediaStop => "MediaStop".to_string(),
1131            KeyCode::MediaNextTrack => "MediaNextTrack".to_string(),
1132            KeyCode::MediaPrevTrack => "MediaPrevTrack".to_string(),
1133        };
1134        for (flag, name) in [
1135            (Modifiers::CTRL, "Ctrl"),
1136            (Modifiers::ALT, "Alt"),
1137            (Modifiers::SHIFT, "Shift"),
1138            (Modifiers::SUPER, "Super"),
1139        ] {
1140            if modifiers.contains(flag) {
1141                write!(f, "{name}+")?;
1142            }
1143        }
1144        f.write_str(&key)
1145    }
1146}
1147
1148/// Parse a key name: a single character, or a named key (case-insensitive:
1149/// `Enter`, `Esc`, `Tab`, `BackTab`, `Backspace`, `Delete`, `Insert`, `Home`,
1150/// `End`, `PageUp`, `PageDown`, `Up`, `Down`, `Left`, `Right`, `Space`,
1151/// `F1`..`F24`, media keys).
1152fn parse_key_name(name: &str) -> Result<KeyCode, KeyParseError> {
1153    let mut chars = name.chars();
1154    if let (Some(c), None) = (chars.next(), chars.next()) {
1155        return Ok(KeyCode::Char(c));
1156    }
1157    let lower = name.to_ascii_lowercase();
1158    let code = match lower.as_str() {
1159        "enter" | "return" => KeyCode::Enter,
1160        "esc" | "escape" => KeyCode::Escape,
1161        "backspace" => KeyCode::Backspace,
1162        "tab" => KeyCode::Tab,
1163        "backtab" => KeyCode::BackTab,
1164        "delete" | "del" => KeyCode::Delete,
1165        "insert" | "ins" => KeyCode::Insert,
1166        "home" => KeyCode::Home,
1167        "end" => KeyCode::End,
1168        "pageup" | "pgup" => KeyCode::PageUp,
1169        "pagedown" | "pgdn" => KeyCode::PageDown,
1170        "up" => KeyCode::Up,
1171        "down" => KeyCode::Down,
1172        "left" => KeyCode::Left,
1173        "right" => KeyCode::Right,
1174        "space" => KeyCode::Char(' '),
1175        "null" => KeyCode::Null,
1176        "mediaplaypause" => KeyCode::MediaPlayPause,
1177        "mediastop" => KeyCode::MediaStop,
1178        "medianexttrack" => KeyCode::MediaNextTrack,
1179        "mediaprevtrack" => KeyCode::MediaPrevTrack,
1180        other => {
1181            if let Some(digits) = other.strip_prefix('f')
1182                && let Ok(n) = digits.parse::<u8>()
1183                && (1..=24).contains(&n)
1184            {
1185                KeyCode::F(n)
1186            } else {
1187                return Err(KeyParseError::UnknownKey(name.to_string()));
1188            }
1189        }
1190    };
1191    Ok(code)
1192}
1193
1194impl FromStr for KeyCombo {
1195    type Err = KeyParseError;
1196
1197    /// Parse `Ctrl+x`, `Shift+Tab`, `F12`, `g`, `Ctrl++` (the plus key).
1198    /// Modifier names are case-insensitive (`Ctrl`/`Control`, `Alt`/`Opt`/
1199    /// `Option`, `Shift`, `Super`/`Cmd`/`Meta`/`Win`).
1200    fn from_str(s: &str) -> Result<Self, Self::Err> {
1201        // Only ASCII whitespace pads a binding in a config file. `trim()`
1202        // takes Unicode whitespace too, which ate the key itself whenever the
1203        // key *was* one: macOS sends U+00A0 for Option+Space, and
1204        // `"Alt+\u{a0}"` trimmed to `"Alt+"`, which the trailing-plus branch
1205        // below then read as the plus key. A bare `"\u{a0}"` trimmed to
1206        // nothing and failed as an empty key.
1207        let s = s.trim_matches(|c: char| c.is_ascii_whitespace());
1208        if s.is_empty() {
1209            return Err(KeyParseError::EmptyKey);
1210        }
1211        let (modifier_part, key_part) = if s == "+" {
1212            ("", "+")
1213        } else if let Some(stripped) = s.strip_suffix('+') {
1214            (stripped.trim_end_matches('+'), "+")
1215        } else if let Some((modifiers, key)) = s.rsplit_once('+') {
1216            (modifiers, key)
1217        } else {
1218            ("", s)
1219        };
1220        let mut modifiers = Modifiers::NONE;
1221        for part in modifier_part.split('+').filter(|p| !p.is_empty()) {
1222            modifiers |= match part.to_ascii_lowercase().as_str() {
1223                "ctrl" | "control" => Modifiers::CTRL,
1224                "alt" | "opt" | "option" => Modifiers::ALT,
1225                "shift" => Modifiers::SHIFT,
1226                "super" | "cmd" | "meta" | "win" => Modifiers::SUPER,
1227                _ => return Err(KeyParseError::UnknownModifier(part.to_string())),
1228            };
1229        }
1230        if key_part.is_empty() {
1231            return Err(KeyParseError::EmptyKey);
1232        }
1233        Ok(Self::new(parse_key_name(key_part)?, modifiers))
1234    }
1235}
1236
1237/// One to [`Chord::MAX_LEN`] combos pressed in sequence (`g g`, `Ctrl+x Ctrl+s`).
1238#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1239pub struct Chord(Vec<KeyCombo>);
1240
1241impl Chord {
1242    /// Longest supported chord.
1243    pub const MAX_LEN: usize = 4;
1244
1245    /// A one-combo chord.
1246    #[must_use]
1247    pub fn single(combo: KeyCombo) -> Self {
1248        Self(vec![combo])
1249    }
1250
1251    /// Build a chord from combos (1..=[`Chord::MAX_LEN`]).
1252    pub fn new(combos: Vec<KeyCombo>) -> Result<Self, KeyParseError> {
1253        if combos.is_empty() {
1254            Err(KeyParseError::EmptyKey)
1255        } else if combos.len() > Self::MAX_LEN {
1256            Err(KeyParseError::TooManyKeys(combos.len()))
1257        } else {
1258            Ok(Self(combos))
1259        }
1260    }
1261
1262    /// Parse a whitespace-separated chord such as `"g g"` or `"Ctrl+x Ctrl+s"`.
1263    pub fn parse(s: &str) -> Result<Self, KeyParseError> {
1264        s.parse()
1265    }
1266
1267    /// The combos in order.
1268    #[must_use]
1269    pub fn combos(&self) -> &[KeyCombo] {
1270        &self.0
1271    }
1272
1273    /// Number of combos.
1274    #[must_use]
1275    pub fn len(&self) -> usize {
1276        self.0.len()
1277    }
1278
1279    /// Never true for a chord built through the constructors; provided for
1280    /// API completeness.
1281    #[must_use]
1282    pub fn is_empty(&self) -> bool {
1283        self.0.is_empty()
1284    }
1285
1286    /// Whether this chord is a strict prefix of `other` (`g` of `g g`).
1287    #[must_use]
1288    pub fn is_prefix_of(&self, other: &Self) -> bool {
1289        self.0.len() < other.0.len() && other.0.starts_with(&self.0)
1290    }
1291}
1292
1293impl FromStr for Chord {
1294    type Err = KeyParseError;
1295
1296    fn from_str(s: &str) -> Result<Self, Self::Err> {
1297        let combos = s
1298            .split_whitespace()
1299            .map(str::parse)
1300            .collect::<Result<Vec<KeyCombo>, _>>()?;
1301        Self::new(combos)
1302    }
1303}
1304
1305impl fmt::Display for Chord {
1306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1307        for (i, combo) in self.0.iter().enumerate() {
1308            if i > 0 {
1309                f.write_str(" ")?;
1310            }
1311            write!(f, "{combo}")?;
1312        }
1313        Ok(())
1314    }
1315}
1316
1317/// Binding priority level; higher wins for the same chord.
1318#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
1319#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1320pub enum Priority {
1321    /// Application-wide default.
1322    #[default]
1323    Global = 0,
1324    /// Active when the app is in a particular mode.
1325    Mode = 1,
1326    /// Owned by the focused widget.
1327    Widget = 2,
1328}
1329
1330/// An interned context name (see [`KeyMap::context`]).
1331#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1332pub struct ContextId(pub u32);
1333
1334/// Identifier of one binding inside a [`KeyMap`].
1335#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1336pub struct BindingId(pub u32);
1337
1338impl fmt::Display for BindingId {
1339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1340        write!(f, "#{}", self.0)
1341    }
1342}
1343
1344/// One chord bound to an action.
1345#[derive(Debug, Clone)]
1346pub struct Binding<A> {
1347    /// Identifier assigned by the map.
1348    pub id: BindingId,
1349    /// The chord that triggers the action.
1350    pub chord: Chord,
1351    /// The action to dispatch.
1352    pub action: A,
1353    /// Priority level.
1354    pub priority: Priority,
1355    /// Context the binding is limited to (`None` = always applicable).
1356    pub context: Option<ContextId>,
1357    /// Human-readable label for help bars and conflict reports.
1358    pub label: Option<String>,
1359}
1360
1361/// Lower bound of the chord timeout (ms).
1362pub const MIN_CHORD_TIMEOUT_MS: u64 = 200;
1363/// Upper bound of the chord timeout (ms).
1364pub const MAX_CHORD_TIMEOUT_MS: u64 = 5000;
1365/// Default chord timeout (ms).
1366pub const DEFAULT_CHORD_TIMEOUT_MS: u64 = 1000;
1367
1368/// Timing configuration of a [`KeyMap`].
1369#[derive(Debug, Clone)]
1370pub struct KeyMapConfig {
1371    /// How long a pending chord prefix waits for its next key.
1372    pub chord_timeout: Duration,
1373    /// Esc / Esc Esc detection settings for the dispatcher's detector.
1374    pub esc: SequenceConfig,
1375}
1376
1377impl Default for KeyMapConfig {
1378    fn default() -> Self {
1379        Self {
1380            chord_timeout: Duration::from_millis(DEFAULT_CHORD_TIMEOUT_MS),
1381            esc: SequenceConfig::default(),
1382        }
1383    }
1384}
1385
1386impl KeyMapConfig {
1387    /// Set the chord timeout, clamped to `200..=5000` ms.
1388    #[must_use]
1389    pub fn with_chord_timeout(mut self, timeout: Duration) -> Self {
1390        let ms = timeout.as_millis().clamp(
1391            u128::from(MIN_CHORD_TIMEOUT_MS),
1392            u128::from(MAX_CHORD_TIMEOUT_MS),
1393        );
1394        self.chord_timeout = Duration::from_millis(ms as u64);
1395        self
1396    }
1397
1398    /// Set the Esc sequence configuration.
1399    #[must_use]
1400    pub fn with_esc(mut self, esc: SequenceConfig) -> Self {
1401        self.esc = esc;
1402        self
1403    }
1404}
1405
1406/// Result of [`KeyMap::lookup`].
1407#[derive(Debug, Clone, Copy)]
1408pub struct Lookup<'a, A> {
1409    /// The winning binding for exactly this chord, if any.
1410    pub exact: Option<&'a Binding<A>>,
1411    /// Number of applicable bindings whose chord starts with this chord and
1412    /// is longer (a pending prefix must wait for them).
1413    pub longer: usize,
1414}
1415
1416impl<A> Lookup<'_, A> {
1417    /// Neither an exact binding nor a longer chord.
1418    #[must_use]
1419    pub fn is_none(&self) -> bool {
1420        self.exact.is_none() && self.longer == 0
1421    }
1422}
1423
1424/// A binding conflict found by [`KeyMap::conflicts`].
1425#[derive(Debug, Clone, PartialEq, Eq)]
1426pub enum Conflict {
1427    /// Same chord, same context, different priority: `winner` hides `loser`.
1428    Shadowed {
1429        winner: BindingId,
1430        loser: BindingId,
1431        chord: Chord,
1432    },
1433    /// `short` is a strict prefix of `long`, so `short` fires only after the
1434    /// chord timeout or a non-extending key.
1435    PrefixCollision {
1436        short: BindingId,
1437        long: BindingId,
1438        short_chord: Chord,
1439        long_chord: Chord,
1440    },
1441    /// Same chord, context and priority: the later binding wins.
1442    Duplicate {
1443        first: BindingId,
1444        second: BindingId,
1445        chord: Chord,
1446    },
1447}
1448
1449/// Every conflict in a map, with a one-line warning per item.
1450#[derive(Debug, Clone, Default, PartialEq, Eq)]
1451pub struct ConflictReport {
1452    /// The conflicts, in map order.
1453    pub items: Vec<Conflict>,
1454}
1455
1456impl ConflictReport {
1457    /// No conflicts.
1458    #[must_use]
1459    pub fn is_empty(&self) -> bool {
1460        self.items.is_empty()
1461    }
1462
1463    /// Number of conflicts.
1464    #[must_use]
1465    pub fn len(&self) -> usize {
1466        self.items.len()
1467    }
1468}
1469
1470impl fmt::Display for ConflictReport {
1471    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1472        for item in &self.items {
1473            match item {
1474                Conflict::Shadowed {
1475                    winner,
1476                    loser,
1477                    chord,
1478                } => writeln!(
1479                    f,
1480                    "warning: binding {winner} shadows binding {loser} on `{chord}` (higher priority)"
1481                )?,
1482                Conflict::PrefixCollision {
1483                    short,
1484                    long,
1485                    short_chord,
1486                    long_chord,
1487                } => writeln!(
1488                    f,
1489                    "warning: binding {short} (`{short_chord}`) is a prefix of binding {long} (`{long_chord}`); it fires only after the chord timeout or a non-extending key"
1490                )?,
1491                Conflict::Duplicate {
1492                    first,
1493                    second,
1494                    chord,
1495                } => writeln!(
1496                    f,
1497                    "warning: bindings {first} and {second} both bind `{chord}` at the same priority; the later one wins"
1498                )?,
1499            }
1500        }
1501        Ok(())
1502    }
1503}
1504
1505/// A declarative binding map: chords to actions with priorities and
1506/// contexts. Actions are any `Clone` type (usually an app enum).
1507#[derive(Debug, Clone)]
1508pub struct KeyMap<A> {
1509    bindings: Vec<Binding<A>>,
1510    contexts: Vec<String>,
1511    config: KeyMapConfig,
1512    next_id: u32,
1513}
1514
1515impl<A> Default for KeyMap<A> {
1516    fn default() -> Self {
1517        Self::new()
1518    }
1519}
1520
1521impl<A> KeyMap<A> {
1522    /// An empty map with default timing.
1523    #[must_use]
1524    pub fn new() -> Self {
1525        Self::with_config(KeyMapConfig::default())
1526    }
1527
1528    /// An empty map with the given timing.
1529    #[must_use]
1530    pub fn with_config(config: KeyMapConfig) -> Self {
1531        Self {
1532            bindings: Vec::new(),
1533            contexts: Vec::new(),
1534            config,
1535            next_id: 0,
1536        }
1537    }
1538
1539    /// Timing configuration.
1540    #[must_use]
1541    pub fn config(&self) -> &KeyMapConfig {
1542        &self.config
1543    }
1544
1545    /// Intern a context name; the same name always yields the same id.
1546    pub fn context(&mut self, name: &str) -> ContextId {
1547        if let Some(index) = self.contexts.iter().position(|n| n == name) {
1548            return ContextId(index as u32);
1549        }
1550        self.contexts.push(name.to_string());
1551        ContextId((self.contexts.len() - 1) as u32)
1552    }
1553
1554    /// Name of an interned context.
1555    #[must_use]
1556    pub fn context_name(&self, id: ContextId) -> Option<&str> {
1557        self.contexts.get(id.0 as usize).map(String::as_str)
1558    }
1559
1560    /// Bind a chord at [`Priority::Global`] with no context.
1561    pub fn bind(&mut self, chord: Chord, action: A) -> BindingId {
1562        self.bind_in(chord, action, Priority::Global, None)
1563    }
1564
1565    /// Bind a chord with an explicit priority and optional context.
1566    pub fn bind_in(
1567        &mut self,
1568        chord: Chord,
1569        action: A,
1570        priority: Priority,
1571        context: Option<ContextId>,
1572    ) -> BindingId {
1573        let id = BindingId(self.next_id);
1574        self.next_id += 1;
1575        self.bindings.push(Binding {
1576            id,
1577            chord,
1578            action,
1579            priority,
1580            context,
1581            label: None,
1582        });
1583        id
1584    }
1585
1586    /// Attach a label to a binding; `false` if the id is unknown.
1587    pub fn set_label(&mut self, id: BindingId, label: impl Into<String>) -> bool {
1588        match self.bindings.iter_mut().find(|b| b.id == id) {
1589            Some(binding) => {
1590                binding.label = Some(label.into());
1591                true
1592            }
1593            None => false,
1594        }
1595    }
1596
1597    /// Remove a binding.
1598    pub fn unbind(&mut self, id: BindingId) -> Option<Binding<A>> {
1599        let index = self.bindings.iter().position(|b| b.id == id)?;
1600        Some(self.bindings.remove(index))
1601    }
1602
1603    /// All bindings in bind order.
1604    #[must_use]
1605    pub fn bindings(&self) -> &[Binding<A>] {
1606        &self.bindings
1607    }
1608
1609    /// A binding by id.
1610    #[must_use]
1611    pub fn get(&self, id: BindingId) -> Option<&Binding<A>> {
1612        self.bindings.iter().find(|b| b.id == id)
1613    }
1614
1615    /// Number of bindings.
1616    #[must_use]
1617    pub fn len(&self) -> usize {
1618        self.bindings.len()
1619    }
1620
1621    /// Whether the map has no bindings.
1622    #[must_use]
1623    pub fn is_empty(&self) -> bool {
1624        self.bindings.is_empty()
1625    }
1626
1627    fn applies(binding: &Binding<A>, active: &[ContextId]) -> bool {
1628        binding
1629            .context
1630            .is_none_or(|context| active.contains(&context))
1631    }
1632
1633    /// Ranking used to pick a winner among bindings for the same chord:
1634    /// active context beats none, then priority, then recency.
1635    fn rank(binding: &Binding<A>) -> (bool, Priority, BindingId) {
1636        (binding.context.is_some(), binding.priority, binding.id)
1637    }
1638
1639    /// Resolve `chord` against the bindings applicable under `active`
1640    /// contexts: the winning exact binding and how many longer bound chords
1641    /// start with it.
1642    #[must_use]
1643    pub fn lookup(&self, chord: &Chord, active: &[ContextId]) -> Lookup<'_, A> {
1644        let mut exact: Option<&Binding<A>> = None;
1645        let mut longer = 0;
1646        for binding in &self.bindings {
1647            if !Self::applies(binding, active) {
1648                continue;
1649            }
1650            if binding.chord == *chord {
1651                if exact.is_none_or(|current| Self::rank(binding) > Self::rank(current)) {
1652                    exact = Some(binding);
1653                }
1654            } else if chord.is_prefix_of(&binding.chord) {
1655                longer += 1;
1656            }
1657        }
1658        Lookup { exact, longer }
1659    }
1660
1661    /// Report shadowed, duplicate, and prefix-colliding bindings.
1662    #[must_use]
1663    pub fn conflicts(&self) -> ConflictReport {
1664        let mut items = Vec::new();
1665        for (i, a) in self.bindings.iter().enumerate() {
1666            for b in &self.bindings[i + 1..] {
1667                if a.chord == b.chord {
1668                    if a.context != b.context {
1669                        // A context-specific override is the intended use.
1670                        continue;
1671                    }
1672                    if a.priority == b.priority {
1673                        items.push(Conflict::Duplicate {
1674                            first: a.id,
1675                            second: b.id,
1676                            chord: a.chord.clone(),
1677                        });
1678                    } else {
1679                        let (winner, loser) = if a.priority > b.priority {
1680                            (a.id, b.id)
1681                        } else {
1682                            (b.id, a.id)
1683                        };
1684                        items.push(Conflict::Shadowed {
1685                            winner,
1686                            loser,
1687                            chord: a.chord.clone(),
1688                        });
1689                    }
1690                } else if a.chord.is_prefix_of(&b.chord) {
1691                    items.push(Conflict::PrefixCollision {
1692                        short: a.id,
1693                        long: b.id,
1694                        short_chord: a.chord.clone(),
1695                        long_chord: b.chord.clone(),
1696                    });
1697                } else if b.chord.is_prefix_of(&a.chord) {
1698                    items.push(Conflict::PrefixCollision {
1699                        short: b.id,
1700                        long: a.id,
1701                        short_chord: b.chord.clone(),
1702                        long_chord: a.chord.clone(),
1703                    });
1704                }
1705            }
1706        }
1707        ConflictReport { items }
1708    }
1709}
1710
1711/// What the dispatcher decided for one key event or tick.
1712#[derive(Debug, Clone, PartialEq, Eq)]
1713pub enum Dispatch<A> {
1714    /// A binding fired.
1715    Action {
1716        action: A,
1717        binding: BindingId,
1718        chord: Chord,
1719    },
1720    /// The key extended a chord prefix; waiting for more keys or the timeout.
1721    Pending { prefix: Chord },
1722    /// The key matched nothing (and could not extend a chord).
1723    Unbound(KeyEvent),
1724    /// A pending prefix was abandoned (timeout or a non-extending key).
1725    Expired { prefix: Chord },
1726    /// Esc sequence detector output for an unbound Esc / Esc Esc.
1727    Esc(SequenceOutput),
1728}
1729
1730/// Counters for evidence rows and hint-usage feedback.
1731#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1732pub struct DispatchStats {
1733    /// Bindings fired.
1734    pub dispatched: u64,
1735    /// Keys that entered a chord prefix.
1736    pub pending: u64,
1737    /// Prefixes abandoned.
1738    pub expired: u64,
1739    /// Keys that matched nothing.
1740    pub unbound: u64,
1741    /// Esc / Esc Esc verdicts handed back unbound (see [`Dispatch::Esc`]).
1742    pub esc: u64,
1743}
1744
1745fn action_dispatch<A: Clone>(binding: &Binding<A>, chord: Chord) -> Dispatch<A> {
1746    Dispatch::Action {
1747        action: binding.action.clone(),
1748        binding: binding.id,
1749        chord,
1750    }
1751}
1752
1753/// Chord-aware dispatcher over a [`KeyMap`].
1754///
1755/// Feed every key event through [`feed`](Self::feed) and call
1756/// [`tick`](Self::tick) once per frame so pending chords and the Esc timer
1757/// expire; every call returns the decisions to act on. See the module
1758/// section header for the resolution rules.
1759#[derive(Debug)]
1760pub struct KeyDispatcher<A> {
1761    map: KeyMap<A>,
1762    pending: Vec<KeyCombo>,
1763    pending_since: Option<Instant>,
1764    esc: SequenceDetector,
1765    active_contexts: Vec<ContextId>,
1766    stats: DispatchStats,
1767}
1768
1769impl<A: Clone> KeyDispatcher<A> {
1770    /// A dispatcher over `map` with no active contexts.
1771    #[must_use]
1772    pub fn new(map: KeyMap<A>) -> Self {
1773        let esc = SequenceDetector::new(map.config().esc.clone());
1774        Self {
1775            map,
1776            pending: Vec::new(),
1777            pending_since: None,
1778            esc,
1779            active_contexts: Vec::new(),
1780            stats: DispatchStats::default(),
1781        }
1782    }
1783
1784    /// The underlying map.
1785    #[must_use]
1786    pub fn map(&self) -> &KeyMap<A> {
1787        &self.map
1788    }
1789
1790    /// Mutable access to the map (rebinding at runtime).
1791    pub fn map_mut(&mut self) -> &mut KeyMap<A> {
1792        &mut self.map
1793    }
1794
1795    /// Replace the set of active contexts (focused widget, mode, ...).
1796    pub fn set_active_contexts(&mut self, contexts: &[ContextId]) {
1797        self.active_contexts.clear();
1798        self.active_contexts.extend_from_slice(contexts);
1799    }
1800
1801    /// Currently active contexts.
1802    #[must_use]
1803    pub fn active_contexts(&self) -> &[ContextId] {
1804        &self.active_contexts
1805    }
1806
1807    /// The chord prefix currently waiting for more keys.
1808    #[must_use]
1809    pub fn pending_prefix(&self) -> Option<Chord> {
1810        Chord::new(self.pending.clone()).ok()
1811    }
1812
1813    /// Counters so far.
1814    #[must_use]
1815    pub fn stats(&self) -> DispatchStats {
1816        self.stats
1817    }
1818
1819    /// Drop any pending prefix and Esc state.
1820    pub fn reset(&mut self) {
1821        self.pending.clear();
1822        self.pending_since = None;
1823        self.esc.reset();
1824    }
1825
1826    /// Process one key event.
1827    pub fn feed(&mut self, key: &KeyEvent, now: Instant) -> Vec<Dispatch<A>> {
1828        let mut out = Vec::with_capacity(2);
1829
1830        if key.code == KeyCode::Escape {
1831            if key.kind == KeyEventKind::Press {
1832                self.flush_pending(&mut out, false);
1833            }
1834            match self.esc.feed(key, now) {
1835                // Repeat / release of Esc: nothing sequence-related to do.
1836                SequenceOutput::PassThrough => {}
1837                output => {
1838                    self.dispatch_esc(output, &mut out);
1839                    return out;
1840                }
1841            }
1842        }
1843
1844        match key.kind {
1845            KeyEventKind::Release => {
1846                self.stats.unbound += 1;
1847                out.push(Dispatch::Unbound(*key));
1848                return out;
1849            }
1850            KeyEventKind::Repeat => {
1851                // A held key re-fires its own single-key binding, never a chord.
1852                let single = Chord::single(KeyCombo::from_event(key));
1853                let fired = if self.pending.is_empty() {
1854                    self.map
1855                        .lookup(&single, &self.active_contexts)
1856                        .exact
1857                        .map(|binding| action_dispatch(binding, single))
1858                } else {
1859                    None
1860                };
1861                match fired {
1862                    Some(dispatch) => {
1863                        self.stats.dispatched += 1;
1864                        out.push(dispatch);
1865                    }
1866                    None => {
1867                        self.stats.unbound += 1;
1868                        out.push(Dispatch::Unbound(*key));
1869                    }
1870                }
1871                return out;
1872            }
1873            KeyEventKind::Press => {}
1874        }
1875
1876        let combo = KeyCombo::from_event(key);
1877        if self.try_extend(combo, now, &mut out) {
1878            return out;
1879        }
1880
1881        // The key cannot extend the prefix: flush it, then start over with the
1882        // key on its own so it can fire or begin a new chord.
1883        if !self.pending.is_empty() {
1884            self.flush_pending(&mut out, false);
1885            if self.try_extend(combo, now, &mut out) {
1886                return out;
1887            }
1888        }
1889
1890        self.stats.unbound += 1;
1891        out.push(Dispatch::Unbound(*key));
1892        out
1893    }
1894
1895    /// Expire a pending prefix past the chord timeout and drive the Esc timer.
1896    pub fn tick(&mut self, now: Instant) -> Vec<Dispatch<A>> {
1897        let mut out = Vec::new();
1898        if let Some(since) = self.pending_since
1899            && now.saturating_duration_since(since) >= self.map.config.chord_timeout
1900        {
1901            self.flush_pending(&mut out, true);
1902        }
1903        if let Some(output) = self.esc.check_timeout(now) {
1904            self.dispatch_esc(output, &mut out);
1905        }
1906        out
1907    }
1908
1909    /// Try to treat `combo` as the next key of the pending prefix. Returns
1910    /// `false` when the extended chord matches nothing (nothing is emitted).
1911    fn try_extend(&mut self, combo: KeyCombo, now: Instant, out: &mut Vec<Dispatch<A>>) -> bool {
1912        if self.pending.len() >= Chord::MAX_LEN {
1913            return false;
1914        }
1915        let mut candidate = self.pending.clone();
1916        candidate.push(combo);
1917        let chord = Chord(candidate);
1918        let lookup = self.map.lookup(&chord, &self.active_contexts);
1919        if let Some(binding) = lookup.exact
1920            && lookup.longer == 0
1921        {
1922            let dispatch = action_dispatch(binding, chord);
1923            self.pending.clear();
1924            self.pending_since = None;
1925            self.stats.dispatched += 1;
1926            out.push(dispatch);
1927            return true;
1928        }
1929        if lookup.exact.is_some() || lookup.longer > 0 {
1930            self.pending.clone_from(&chord.0);
1931            self.pending_since = Some(now);
1932            self.stats.pending += 1;
1933            out.push(Dispatch::Pending { prefix: chord });
1934            return true;
1935        }
1936        false
1937    }
1938
1939    /// Fire the pending prefix if it is bound, otherwise report it expired;
1940    /// on a timeout the expiry is reported first so the delay is visible.
1941    fn flush_pending(&mut self, out: &mut Vec<Dispatch<A>>, timed_out: bool) {
1942        if self.pending.is_empty() {
1943            return;
1944        }
1945        let prefix = Chord(std::mem::take(&mut self.pending));
1946        self.pending_since = None;
1947        let fired = self
1948            .map
1949            .lookup(&prefix, &self.active_contexts)
1950            .exact
1951            .map(|binding| action_dispatch(binding, prefix.clone()));
1952        match fired {
1953            Some(dispatch) => {
1954                if timed_out {
1955                    self.stats.expired += 1;
1956                    out.push(Dispatch::Expired { prefix });
1957                }
1958                self.stats.dispatched += 1;
1959                out.push(dispatch);
1960            }
1961            None => {
1962                self.stats.expired += 1;
1963                out.push(Dispatch::Expired { prefix });
1964            }
1965        }
1966    }
1967
1968    /// Route a detector verdict: a bound `Esc` / `Esc Esc` fires its binding,
1969    /// anything else is handed back as [`Dispatch::Esc`].
1970    fn dispatch_esc(&mut self, output: SequenceOutput, out: &mut Vec<Dispatch<A>>) {
1971        let esc = KeyCombo::key(KeyCode::Escape);
1972        let bound = match output {
1973            SequenceOutput::Esc => Some(Chord::single(esc)),
1974            SequenceOutput::EscEsc => Chord::new(vec![esc, esc]).ok(),
1975            SequenceOutput::Pending | SequenceOutput::PassThrough => None,
1976        };
1977        let fired = bound.and_then(|chord| {
1978            self.map
1979                .lookup(&chord, &self.active_contexts)
1980                .exact
1981                .map(|binding| action_dispatch(binding, chord.clone()))
1982        });
1983        match fired {
1984            Some(dispatch) => {
1985                self.stats.dispatched += 1;
1986                out.push(dispatch);
1987            }
1988            None => {
1989                self.stats.esc += 1;
1990                out.push(Dispatch::Esc(output));
1991            }
1992        }
1993    }
1994}
1995
1996// ---------------------------------------------------------------------------
1997// Serialization (feature `serde`): human-editable keymap files
1998// ---------------------------------------------------------------------------
1999
2000/// On-disk shape of a [`KeyMap`] (feature `serde`): chords as text, contexts
2001/// by name, the chord timeout in milliseconds. Esc timing is not part of the
2002/// file; it follows [`SequenceConfig`] (defaults and `FTUI_DISABLE_ESC_SEQ`).
2003///
2004/// ```toml
2005/// chord_timeout_ms = 750
2006///
2007/// [[bindings]]
2008/// chord = "Ctrl+x Ctrl+s"
2009/// action = "Save"
2010/// priority = "Mode"
2011/// label = "save"
2012///
2013/// [[bindings]]
2014/// chord = "Enter"
2015/// action = "Newline"
2016/// priority = "Widget"
2017/// context = "editor"
2018/// ```
2019#[cfg(feature = "serde")]
2020#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2021#[serde(deny_unknown_fields)]
2022pub struct KeyMapFile<A> {
2023    /// Chord timeout in milliseconds (clamped to `200..=5000` on load).
2024    #[serde(default = "default_chord_timeout_ms")]
2025    pub chord_timeout_ms: u64,
2026    /// Bindings in bind order.
2027    #[serde(default = "Vec::new")]
2028    pub bindings: Vec<BindingFile<A>>,
2029}
2030
2031#[cfg(feature = "serde")]
2032fn default_chord_timeout_ms() -> u64 {
2033    DEFAULT_CHORD_TIMEOUT_MS
2034}
2035
2036/// One binding in a [`KeyMapFile`].
2037#[cfg(feature = "serde")]
2038#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2039#[serde(deny_unknown_fields)]
2040pub struct BindingFile<A> {
2041    /// Chord text, e.g. `"g g"` or `"Ctrl+x Ctrl+s"`.
2042    pub chord: String,
2043    /// The action (any serde type; usually a unit-variant enum).
2044    pub action: A,
2045    /// Priority level (default `Global`).
2046    #[serde(default)]
2047    pub priority: Priority,
2048    /// Context name (default: none).
2049    #[serde(default, skip_serializing_if = "Option::is_none")]
2050    pub context: Option<String>,
2051    /// Help label (default: none).
2052    #[serde(default, skip_serializing_if = "Option::is_none")]
2053    pub label: Option<String>,
2054}
2055
2056/// Why a [`KeyMapFile`] could not become a [`KeyMap`].
2057#[cfg(feature = "serde")]
2058#[derive(Debug, Clone, PartialEq, Eq)]
2059pub enum KeyMapFileError {
2060    /// A binding's chord text did not parse.
2061    Chord {
2062        /// Index of the binding in the file.
2063        index: usize,
2064        /// The offending chord text.
2065        chord: String,
2066        /// Parse error.
2067        source: KeyParseError,
2068    },
2069}
2070
2071#[cfg(feature = "serde")]
2072impl fmt::Display for KeyMapFileError {
2073    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2074        match self {
2075            Self::Chord {
2076                index,
2077                chord,
2078                source,
2079            } => write!(f, "binding {index} (`{chord}`): {source}"),
2080        }
2081    }
2082}
2083
2084#[cfg(feature = "serde")]
2085impl std::error::Error for KeyMapFileError {}
2086
2087#[cfg(feature = "serde")]
2088impl<A: Clone> KeyMap<A> {
2089    /// The file representation (contexts by name, chords as text).
2090    #[must_use]
2091    pub fn to_file(&self) -> KeyMapFile<A> {
2092        KeyMapFile {
2093            chord_timeout_ms: self.config.chord_timeout.as_millis() as u64,
2094            bindings: self
2095                .bindings
2096                .iter()
2097                .map(|binding| BindingFile {
2098                    chord: binding.chord.to_string(),
2099                    action: binding.action.clone(),
2100                    priority: binding.priority,
2101                    context: binding
2102                        .context
2103                        .and_then(|id| self.context_name(id))
2104                        .map(str::to_string),
2105                    label: binding.label.clone(),
2106                })
2107                .collect(),
2108        }
2109    }
2110
2111    /// Build a map from its file representation, interning context names and
2112    /// parsing chords; a bad chord names the offending binding.
2113    pub fn from_file(file: KeyMapFile<A>) -> Result<Self, KeyMapFileError> {
2114        let config = KeyMapConfig::default()
2115            .with_chord_timeout(Duration::from_millis(file.chord_timeout_ms));
2116        let mut map = Self::with_config(config);
2117        for (index, entry) in file.bindings.into_iter().enumerate() {
2118            let chord = Chord::parse(&entry.chord).map_err(|source| KeyMapFileError::Chord {
2119                index,
2120                chord: entry.chord.clone(),
2121                source,
2122            })?;
2123            let context = entry.context.as_deref().map(|name| map.context(name));
2124            let id = map.bind_in(chord, entry.action, entry.priority, context);
2125            if let Some(label) = entry.label {
2126                map.set_label(id, label);
2127            }
2128        }
2129        Ok(map)
2130    }
2131}
2132
2133#[cfg(feature = "serde")]
2134impl<A: Clone + serde::Serialize> serde::Serialize for KeyMap<A> {
2135    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2136        self.to_file().serialize(serializer)
2137    }
2138}
2139
2140#[cfg(feature = "serde")]
2141impl<'de, A: Clone + serde::Deserialize<'de>> serde::Deserialize<'de> for KeyMap<A> {
2142    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2143        let file = KeyMapFile::<A>::deserialize(deserializer)?;
2144        Self::from_file(file).map_err(serde::de::Error::custom)
2145    }
2146}
2147
2148#[cfg(test)]
2149mod keymap_tests {
2150    use super::*;
2151    use proptest::prelude::*;
2152
2153    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
2154    enum Act {
2155        GoTop,
2156        Help,
2157        Save,
2158        Quit,
2159        Submit,
2160        Newline,
2161        Global,
2162        Mode,
2163        Widget,
2164        Down,
2165    }
2166
2167    fn press(c: char) -> KeyEvent {
2168        KeyEvent::new(KeyCode::Char(c))
2169    }
2170
2171    fn kind(mut event: KeyEvent, kind: KeyEventKind) -> KeyEvent {
2172        event.kind = kind;
2173        event
2174    }
2175
2176    fn ms(n: u64) -> Duration {
2177        Duration::from_millis(n)
2178    }
2179
2180    fn chord(s: &str) -> Chord {
2181        Chord::parse(s).unwrap_or_else(|e| panic!("{s}: {e}"))
2182    }
2183
2184    fn actions<A: Clone>(dispatches: &[Dispatch<A>]) -> Vec<A> {
2185        dispatches
2186            .iter()
2187            .filter_map(|d| match d {
2188                Dispatch::Action { action, .. } => Some(action.clone()),
2189                _ => None,
2190            })
2191            .collect()
2192    }
2193
2194    /// `Display` folds SHIFT into an uppercase letter, which only works when
2195    /// uppercasing is a faithful single-character inverse. Three classes of
2196    /// letter are not, and `is_alphabetic()` admitted all three.
2197    #[test]
2198    fn shift_stays_a_modifier_when_uppercasing_would_not_round_trip() {
2199        let shifted = |c| KeyCombo::new(KeyCode::Char(c), Modifiers::SHIFT);
2200
2201        // Caseless: `中` uppercases to itself, so the SHIFT used to vanish
2202        // and `Shift+中` printed as plain `中`.
2203        assert_eq!(shifted('中').to_string(), "Shift+中");
2204        // Several characters: `ß` uppercases to `SS`, which printed as `SS`
2205        // and then failed to parse at all.
2206        assert_eq!(shifted('ß').to_string(), "Shift+ß");
2207        // Does not lowercase back: `ı` uppercases to `I`, whose lowercase is
2208        // `i`, so the round trip would have landed on the wrong letter.
2209        assert_eq!(shifted('ı').to_string(), "Shift+ı");
2210        // The ordinary case is unchanged.
2211        assert_eq!(shifted('a').to_string(), "A");
2212
2213        for c in ['中', 'ß', 'ı', 'a'] {
2214            let combo = shifted(c);
2215            let text = combo.to_string();
2216            assert_eq!(text.parse::<KeyCombo>().unwrap(), combo, "{text:?}");
2217        }
2218    }
2219
2220    /// `trim()` takes Unicode whitespace, which ate the key when the key was
2221    /// one. macOS sends U+00A0 for Option+Space.
2222    #[test]
2223    fn a_unicode_space_is_a_key_and_not_padding() {
2224        let nbsp = KeyCode::Char('\u{a0}');
2225        let alt_space = KeyCombo::new(nbsp, Modifiers::ALT);
2226        let text = alt_space.to_string();
2227        // Used to trim to "Alt+" and come back as Alt plus the *plus* key.
2228        assert_eq!(text.parse::<KeyCombo>().unwrap(), alt_space);
2229        assert_ne!(
2230            text.parse::<KeyCombo>().unwrap(),
2231            KeyCombo::new(KeyCode::Char('+'), Modifiers::ALT)
2232        );
2233
2234        // A bare one used to trim away to nothing and fail as an empty key.
2235        assert_eq!(
2236            "\u{a0}".parse::<KeyCombo>().unwrap(),
2237            KeyCombo::new(nbsp, Modifiers::NONE)
2238        );
2239        // ASCII padding around a binding is still ignored.
2240        assert_eq!(
2241            "  Ctrl+x \t".parse::<KeyCombo>().unwrap(),
2242            KeyCombo::new(KeyCode::Char('x'), Modifiers::CTRL)
2243        );
2244    }
2245
2246    #[test]
2247    fn combo_parse_display_and_normalization() {
2248        let ctrl_x: KeyCombo = "Ctrl+x".parse().unwrap();
2249        assert_eq!(ctrl_x, KeyCombo::new(KeyCode::Char('x'), Modifiers::CTRL));
2250        assert_eq!(ctrl_x.to_string(), "Ctrl+x");
2251
2252        // Shift+a, A and a terminal reporting Char('A')+SHIFT are one combo.
2253        let shift_a: KeyCombo = "shift+a".parse().unwrap();
2254        assert_eq!(shift_a, "A".parse().unwrap());
2255        assert_eq!(shift_a, KeyCombo::new(KeyCode::Char('A'), Modifiers::SHIFT));
2256        assert_eq!(shift_a.to_string(), "A");
2257
2258        assert_eq!("F12".parse::<KeyCombo>().unwrap().code, KeyCode::F(12));
2259        assert_eq!(
2260            "Space".parse::<KeyCombo>().unwrap().code,
2261            KeyCode::Char(' ')
2262        );
2263        assert_eq!(
2264            "Ctrl+Alt+Delete".parse::<KeyCombo>().unwrap().to_string(),
2265            "Ctrl+Alt+Delete"
2266        );
2267        assert_eq!(
2268            "Shift+Tab".parse::<KeyCombo>().unwrap().to_string(),
2269            "Shift+Tab"
2270        );
2271        // The plus key itself.
2272        assert_eq!("+".parse::<KeyCombo>().unwrap().code, KeyCode::Char('+'));
2273        let ctrl_plus: KeyCombo = "Ctrl++".parse().unwrap();
2274        assert_eq!(
2275            ctrl_plus,
2276            KeyCombo::new(KeyCode::Char('+'), Modifiers::CTRL)
2277        );
2278
2279        assert_eq!(
2280            "Hyper+x".parse::<KeyCombo>(),
2281            Err(KeyParseError::UnknownModifier("Hyper".into()))
2282        );
2283        assert_eq!(
2284            "Banana".parse::<KeyCombo>(),
2285            Err(KeyParseError::UnknownKey("Banana".into()))
2286        );
2287        assert_eq!("".parse::<KeyCombo>(), Err(KeyParseError::EmptyKey));
2288        assert_eq!(
2289            "F0".parse::<KeyCombo>(),
2290            Err(KeyParseError::UnknownKey("F0".into()))
2291        );
2292    }
2293
2294    #[test]
2295    fn chord_parse_prefix_and_limits() {
2296        let gg = chord("g g");
2297        let g = chord("g");
2298        assert_eq!(gg.len(), 2);
2299        assert_eq!(gg.to_string(), "g g");
2300        assert!(g.is_prefix_of(&gg));
2301        assert!(!gg.is_prefix_of(&g));
2302        assert!(!g.is_prefix_of(&g), "a chord is not its own prefix");
2303        assert_eq!(chord("Ctrl+x Ctrl+s").to_string(), "Ctrl+x Ctrl+s");
2304        assert_eq!(Chord::parse(""), Err(KeyParseError::EmptyKey));
2305        assert_eq!(
2306            Chord::parse("a b c d e"),
2307            Err(KeyParseError::TooManyKeys(5))
2308        );
2309    }
2310
2311    #[test]
2312    fn chord_completes_within_timeout() {
2313        let mut map = KeyMap::new();
2314        map.bind(chord("g g"), Act::GoTop);
2315        map.bind(chord("x"), Act::Save);
2316        let mut dispatcher = KeyDispatcher::new(map);
2317        let t0 = Instant::now();
2318
2319        let first = dispatcher.feed(&press('g'), t0);
2320        assert_eq!(first, vec![Dispatch::Pending { prefix: chord("g") }]);
2321        assert_eq!(dispatcher.pending_prefix(), Some(chord("g")));
2322        assert!(
2323            dispatcher.tick(t0 + ms(300)).is_empty(),
2324            "still inside the timeout"
2325        );
2326
2327        let second = dispatcher.feed(&press('g'), t0 + ms(300));
2328        assert_eq!(actions(&second), vec![Act::GoTop]);
2329        assert_eq!(dispatcher.pending_prefix(), None);
2330        assert_eq!(dispatcher.stats().dispatched, 1);
2331        assert_eq!(dispatcher.stats().pending, 1);
2332    }
2333
2334    #[test]
2335    fn chord_expires_after_timeout() {
2336        // Prefix that is itself bound: expiry fires it.
2337        let mut map = KeyMap::new();
2338        map.bind(chord("g g"), Act::GoTop);
2339        map.bind(chord("g"), Act::Help);
2340        let mut dispatcher = KeyDispatcher::new(map);
2341        let t0 = Instant::now();
2342        assert_eq!(
2343            dispatcher.feed(&press('g'), t0),
2344            vec![Dispatch::Pending { prefix: chord("g") }]
2345        );
2346        assert!(dispatcher.tick(t0 + ms(999)).is_empty());
2347        let expired = dispatcher.tick(t0 + ms(1000));
2348        assert_eq!(expired[0], Dispatch::Expired { prefix: chord("g") });
2349        assert_eq!(actions(&expired), vec![Act::Help]);
2350        assert_eq!(dispatcher.stats().expired, 1);
2351
2352        // Prefix that is not bound: expiry only.
2353        let mut map = KeyMap::new();
2354        map.bind(chord("g g"), Act::GoTop);
2355        let mut dispatcher = KeyDispatcher::new(map);
2356        dispatcher.feed(&press('g'), t0);
2357        assert_eq!(
2358            dispatcher.tick(t0 + ms(5000)),
2359            vec![Dispatch::Expired { prefix: chord("g") }]
2360        );
2361        assert_eq!(dispatcher.pending_prefix(), None);
2362    }
2363
2364    #[test]
2365    fn single_key_fires_while_chord_pending() {
2366        let mut map = KeyMap::new();
2367        map.bind(chord("g g"), Act::GoTop);
2368        map.bind(chord("x"), Act::Save);
2369        let mut dispatcher = KeyDispatcher::new(map);
2370        let t0 = Instant::now();
2371        dispatcher.feed(&press('g'), t0);
2372        let out = dispatcher.feed(&press('x'), t0 + ms(10));
2373        assert_eq!(out[0], Dispatch::Expired { prefix: chord("g") });
2374        assert_eq!(
2375            actions(&out),
2376            vec![Act::Save],
2377            "x is never blocked by the pending g"
2378        );
2379
2380        // Same with a bound prefix: it fires first, then the single key.
2381        let mut map = KeyMap::new();
2382        map.bind(chord("g g"), Act::GoTop);
2383        map.bind(chord("g"), Act::Help);
2384        map.bind(chord("x"), Act::Save);
2385        let mut dispatcher = KeyDispatcher::new(map);
2386        dispatcher.feed(&press('g'), t0);
2387        let out = dispatcher.feed(&press('x'), t0 + ms(10));
2388        assert_eq!(actions(&out), vec![Act::Help, Act::Save]);
2389
2390        // A non-extending key that starts another chord goes pending itself.
2391        let mut map = KeyMap::new();
2392        map.bind(chord("g g"), Act::GoTop);
2393        map.bind(chord("z z"), Act::Quit);
2394        let mut dispatcher = KeyDispatcher::new(map);
2395        dispatcher.feed(&press('g'), t0);
2396        let out = dispatcher.feed(&press('z'), t0 + ms(10));
2397        assert_eq!(
2398            out,
2399            vec![
2400                Dispatch::Expired { prefix: chord("g") },
2401                Dispatch::Pending { prefix: chord("z") }
2402            ]
2403        );
2404    }
2405
2406    #[test]
2407    fn prefix_with_own_binding_fires_on_flush() {
2408        // `g` is bound both on its own and as the prefix of `g g`. A following
2409        // key that cannot extend the prefix flushes it. Because this is not a
2410        // timeout, the prefix's own binding fires with no `Expired`, and the
2411        // non-extending key is then reported unbound.
2412        let mut map = KeyMap::new();
2413        let one = map.bind(chord("g"), Act::Help);
2414        map.bind(chord("g g"), Act::GoTop);
2415        let mut dispatcher = KeyDispatcher::new(map);
2416        let t0 = Instant::now();
2417
2418        assert_eq!(
2419            dispatcher.feed(&press('g'), t0),
2420            vec![Dispatch::Pending { prefix: chord("g") }]
2421        );
2422        let out = dispatcher.feed(&press('x'), t0 + ms(10));
2423        assert_eq!(
2424            out,
2425            vec![
2426                Dispatch::Action {
2427                    action: Act::Help,
2428                    binding: one,
2429                    chord: chord("g"),
2430                },
2431                Dispatch::Unbound(press('x')),
2432            ]
2433        );
2434        assert_eq!(dispatcher.pending_prefix(), None);
2435        assert_eq!(
2436            dispatcher.stats().expired,
2437            0,
2438            "a bound prefix flushed by a non-extending key does not expire"
2439        );
2440        assert_eq!(dispatcher.stats().dispatched, 1);
2441    }
2442
2443    #[test]
2444    fn widget_beats_mode_beats_global() {
2445        let mut map = KeyMap::new();
2446        let g = map.bind_in(chord("s"), Act::Global, Priority::Global, None);
2447        let m = map.bind_in(chord("s"), Act::Mode, Priority::Mode, None);
2448        let w = map.bind_in(chord("s"), Act::Widget, Priority::Widget, None);
2449        let lookup = map.lookup(&chord("s"), &[]);
2450        assert_eq!(lookup.exact.map(|b| b.id), Some(w));
2451        assert_eq!(lookup.longer, 0);
2452
2453        let mut dispatcher = KeyDispatcher::new(map);
2454        assert_eq!(
2455            actions(&dispatcher.feed(&press('s'), Instant::now())),
2456            vec![Act::Widget]
2457        );
2458
2459        let report = dispatcher.map().conflicts();
2460        assert_eq!(report.len(), 3, "{report}");
2461        assert!(report.items.contains(&Conflict::Shadowed {
2462            winner: w,
2463            loser: g,
2464            chord: chord("s")
2465        }));
2466        assert!(report.items.contains(&Conflict::Shadowed {
2467            winner: m,
2468            loser: g,
2469            chord: chord("s")
2470        }));
2471        assert!(report.items.contains(&Conflict::Shadowed {
2472            winner: w,
2473            loser: m,
2474            chord: chord("s")
2475        }));
2476        assert_eq!(report.to_string().lines().count(), 3);
2477
2478        // Removing the winner promotes the next.
2479        dispatcher.map_mut().unbind(w);
2480        assert_eq!(
2481            actions(&dispatcher.feed(&press('s'), Instant::now())),
2482            vec![Act::Mode]
2483        );
2484    }
2485
2486    #[test]
2487    fn active_context_beats_contextless_even_at_lower_priority() {
2488        let mut map = KeyMap::new();
2489        let text_input = map.context("text_input");
2490        assert_eq!(map.context("text_input"), text_input, "interned once");
2491        assert_eq!(map.context_name(text_input), Some("text_input"));
2492        map.bind_in(chord("Enter"), Act::Submit, Priority::Widget, None);
2493        map.bind_in(
2494            chord("Enter"),
2495            Act::Newline,
2496            Priority::Global,
2497            Some(text_input),
2498        );
2499        assert!(
2500            map.conflicts().is_empty(),
2501            "a context override is not a conflict"
2502        );
2503
2504        let mut dispatcher = KeyDispatcher::new(map);
2505        let enter = KeyEvent::new(KeyCode::Enter);
2506        let t0 = Instant::now();
2507        assert_eq!(actions(&dispatcher.feed(&enter, t0)), vec![Act::Submit]);
2508        dispatcher.set_active_contexts(&[text_input]);
2509        assert_eq!(actions(&dispatcher.feed(&enter, t0)), vec![Act::Newline]);
2510        dispatcher.set_active_contexts(&[]);
2511        assert_eq!(actions(&dispatcher.feed(&enter, t0)), vec![Act::Submit]);
2512    }
2513
2514    #[test]
2515    fn conflicts_reports_shadowed_prefix_and_duplicate() {
2516        let mut map = KeyMap::new();
2517        let long = map.bind(chord("g g"), Act::GoTop);
2518        let short = map.bind(chord("g"), Act::Help);
2519        let q1 = map.bind(chord("q"), Act::Quit);
2520        let q2 = map.bind(chord("q"), Act::Quit);
2521        map.set_label(q2, "quit");
2522        assert_eq!(map.get(q2).and_then(|b| b.label.as_deref()), Some("quit"));
2523
2524        let report = map.conflicts();
2525        assert_eq!(report.len(), 2, "{report}");
2526        assert_eq!(
2527            report.items[0],
2528            Conflict::PrefixCollision {
2529                short,
2530                long,
2531                short_chord: chord("g"),
2532                long_chord: chord("g g"),
2533            }
2534        );
2535        assert_eq!(
2536            report.items[1],
2537            Conflict::Duplicate {
2538                first: q1,
2539                second: q2,
2540                chord: chord("q")
2541            }
2542        );
2543        let text = report.to_string();
2544        assert_eq!(text.lines().count(), 2);
2545        assert!(
2546            text.contains("warning: binding #1 (`g`) is a prefix of binding #0 (`g g`)"),
2547            "{text}"
2548        );
2549        assert!(text.contains("the later one wins"), "{text}");
2550
2551        // The later duplicate wins at dispatch.
2552        assert_eq!(map.lookup(&chord("q"), &[]).exact.map(|b| b.id), Some(q2));
2553    }
2554
2555    #[test]
2556    fn repeat_refires_single_key_binding_but_never_extends_a_chord() {
2557        let mut map = KeyMap::new();
2558        map.bind(chord("j"), Act::Down);
2559        map.bind(chord("g g"), Act::GoTop);
2560        let mut dispatcher = KeyDispatcher::new(map);
2561        let t0 = Instant::now();
2562
2563        let held = kind(press('j'), KeyEventKind::Repeat);
2564        assert_eq!(actions(&dispatcher.feed(&held, t0)), vec![Act::Down]);
2565
2566        dispatcher.feed(&press('g'), t0);
2567        let repeat_g = kind(press('g'), KeyEventKind::Repeat);
2568        assert_eq!(
2569            dispatcher.feed(&repeat_g, t0 + ms(10)),
2570            vec![Dispatch::Unbound(repeat_g)]
2571        );
2572        assert_eq!(
2573            dispatcher.pending_prefix(),
2574            Some(chord("g")),
2575            "repeat left the prefix alone"
2576        );
2577
2578        let released = kind(press('j'), KeyEventKind::Release);
2579        assert_eq!(
2580            dispatcher.feed(&released, t0 + ms(20)),
2581            vec![Dispatch::Unbound(released)]
2582        );
2583    }
2584
2585    #[test]
2586    fn esc_goes_through_the_sequence_detector() {
2587        let mut map = KeyMap::new();
2588        map.bind(chord("Esc"), Act::Quit);
2589        map.bind(chord("Esc Esc"), Act::Help);
2590        map.bind(chord("g g"), Act::GoTop);
2591        let mut dispatcher = KeyDispatcher::new(map);
2592        let esc = KeyEvent::new(KeyCode::Escape);
2593        let t0 = Instant::now();
2594
2595        // A lone Esc waits for the detector window, then fires its binding.
2596        assert_eq!(
2597            dispatcher.feed(&esc, t0),
2598            vec![Dispatch::Esc(SequenceOutput::Pending)]
2599        );
2600        assert_eq!(actions(&dispatcher.tick(t0 + ms(300))), vec![Act::Quit]);
2601
2602        // Esc Esc inside the window fires the double binding.
2603        let t1 = t0 + ms(1000);
2604        dispatcher.feed(&esc, t1);
2605        assert_eq!(
2606            actions(&dispatcher.feed(&esc, t1 + ms(100))),
2607            vec![Act::Help]
2608        );
2609
2610        // Esc cancels a pending chord prefix first.
2611        let t2 = t0 + ms(3000);
2612        dispatcher.feed(&press('g'), t2);
2613        let out = dispatcher.feed(&esc, t2 + ms(10));
2614        assert_eq!(out[0], Dispatch::Expired { prefix: chord("g") });
2615        assert_eq!(dispatcher.pending_prefix(), None);
2616
2617        // Unbound Esc surfaces the detector verdict.
2618        let mut plain = KeyDispatcher::new(KeyMap::<Act>::new());
2619        plain.feed(&esc, t0);
2620        assert_eq!(
2621            plain.tick(t0 + ms(300)),
2622            vec![Dispatch::Esc(SequenceOutput::Esc)]
2623        );
2624    }
2625
2626    /// A map round-trips through TOML and JSON with chords as text, contexts
2627    /// by name and the timeout in milliseconds; a bad hand-written chord is
2628    /// reported with its binding index.
2629    #[cfg(feature = "serde")]
2630    #[test]
2631    fn keymap_round_trips_through_toml_and_json() {
2632        #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2633        enum Action {
2634            Quit,
2635            Save,
2636            Newline,
2637        }
2638
2639        let mut map = KeyMap::with_config(KeyMapConfig::default().with_chord_timeout(ms(750)));
2640        let editor = map.context("editor");
2641        let quit = map.bind(chord("q"), Action::Quit);
2642        map.set_label(quit, "quit");
2643        map.bind_in(chord("Ctrl+x Ctrl+s"), Action::Save, Priority::Mode, None);
2644        map.bind_in(
2645            chord("Enter"),
2646            Action::Newline,
2647            Priority::Widget,
2648            Some(editor),
2649        );
2650
2651        let text = toml::to_string(&map).expect("serialize to TOML");
2652        assert!(text.contains("chord_timeout_ms = 750"), "{text}");
2653        assert!(text.contains("chord = \"Ctrl+x Ctrl+s\""), "{text}");
2654        assert!(text.contains("context = \"editor\""), "{text}");
2655        assert!(text.contains("label = \"quit\""), "{text}");
2656
2657        let back: KeyMap<Action> = toml::from_str(&text).expect("parse TOML");
2658        assert_eq!(back.config().chord_timeout, ms(750));
2659        assert_eq!(back.len(), 3);
2660        assert_eq!(back.bindings()[0].label.as_deref(), Some("quit"));
2661        assert_eq!(back.bindings()[1].priority, Priority::Mode);
2662        assert_eq!(back.bindings()[1].chord, chord("Ctrl+x Ctrl+s"));
2663        let editor_back = back.bindings()[2].context.expect("context restored");
2664        assert_eq!(back.context_name(editor_back), Some("editor"));
2665        assert_eq!(
2666            back.lookup(&chord("Enter"), &[editor_back])
2667                .exact
2668                .map(|b| &b.action),
2669            Some(&Action::Newline)
2670        );
2671        assert!(
2672            back.lookup(&chord("Enter"), &[]).is_none(),
2673            "the context binding stays inactive outside its context"
2674        );
2675
2676        let json = serde_json::to_string(&map).expect("serialize to JSON");
2677        let back_json: KeyMap<Action> = serde_json::from_str(&json).expect("parse JSON");
2678        assert_eq!(back_json.len(), 3);
2679        assert_eq!(back_json.bindings()[2].action, Action::Newline);
2680
2681        let bad =
2682            "chord_timeout_ms = 500\n\n[[bindings]]\nchord = \"Hyper+q\"\naction = \"Quit\"\n";
2683        let err = toml::from_str::<KeyMap<Action>>(bad)
2684            .expect_err("bad chord must fail")
2685            .to_string();
2686        assert!(err.contains("binding 0") && err.contains("Hyper"), "{err}");
2687
2688        let minimal: KeyMap<Action> =
2689            toml::from_str("[[bindings]]\nchord = \"q\"\naction = \"Quit\"\n")
2690                .expect("defaults fill in");
2691        assert_eq!(minimal.config().chord_timeout, ms(DEFAULT_CHORD_TIMEOUT_MS));
2692        assert_eq!(minimal.bindings()[0].priority, Priority::Global);
2693    }
2694
2695    /// Unknown keys in a keymap file are rejected (the typo names itself), and
2696    /// a bad chord names its binding index.
2697    #[cfg(feature = "serde")]
2698    #[test]
2699    fn toml_rejects_unknown_field_and_bad_chord() {
2700        #[derive(Debug, Clone, serde::Deserialize)]
2701        enum Action {
2702            Quit,
2703        }
2704
2705        let unknown_top =
2706            toml::from_str::<KeyMap<Action>>("chord_timeout_ms = 500\ntypo_field = 3\n")
2707                .expect_err("an unknown top-level field must be rejected")
2708                .to_string();
2709        assert!(unknown_top.contains("typo_field"), "{unknown_top}");
2710
2711        let unknown_binding = toml::from_str::<KeyMap<Action>>(
2712            "[[bindings]]\nchord = \"q\"\naction = \"Quit\"\nchrod = \"x\"\n",
2713        )
2714        .expect_err("an unknown binding field must be rejected")
2715        .to_string();
2716        assert!(unknown_binding.contains("chrod"), "{unknown_binding}");
2717
2718        let bad_chord = toml::from_str::<KeyMap<Action>>(
2719            "[[bindings]]\nchord = \"Nope+q\"\naction = \"Quit\"\n",
2720        )
2721        .expect_err("a bad chord must be rejected")
2722        .to_string();
2723        assert!(
2724            bad_chord.contains("binding 0") && bad_chord.contains("Nope"),
2725            "{bad_chord}"
2726        );
2727    }
2728
2729    /// The keymap example embedded in the keybinding policy doc (and shipped as
2730    /// a fixture) parses into exactly the map it describes, so the docs and the
2731    /// parser cannot silently drift apart.
2732    #[cfg(feature = "serde")]
2733    #[test]
2734    fn toml_example_in_docs_parses() {
2735        #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
2736        enum Action {
2737            Save,
2738            Newline,
2739            Top,
2740        }
2741
2742        const EXAMPLE: &str = include_str!("../tests/fixtures/keymap_example.toml");
2743        let map: KeyMap<Action> =
2744            toml::from_str(EXAMPLE).expect("documented keymap example must parse");
2745
2746        assert_eq!(map.config().chord_timeout, ms(750));
2747        assert_eq!(map.len(), 3);
2748
2749        let save = &map.bindings()[0];
2750        assert_eq!(save.action, Action::Save);
2751        assert_eq!(save.chord, chord("Ctrl+x Ctrl+s"));
2752        assert_eq!(save.priority, Priority::Mode);
2753        assert_eq!(save.label.as_deref(), Some("save"));
2754
2755        let newline = &map.bindings()[1];
2756        assert_eq!(newline.action, Action::Newline);
2757        assert_eq!(newline.priority, Priority::Widget);
2758        let editor = newline.context.expect("editor context restored");
2759        assert_eq!(map.context_name(editor), Some("editor"));
2760        assert_eq!(
2761            map.lookup(&chord("Enter"), &[editor])
2762                .exact
2763                .map(|binding| &binding.action),
2764            Some(&Action::Newline)
2765        );
2766        assert!(
2767            map.lookup(&chord("Enter"), &[]).is_none(),
2768            "the editor binding stays inactive outside its context"
2769        );
2770
2771        assert_eq!(map.bindings()[2].chord, chord("g g"));
2772        assert_eq!(map.bindings()[2].action, Action::Top);
2773        let report = map.conflicts();
2774        assert!(report.is_empty(), "{report}");
2775    }
2776
2777    fn arb_code() -> impl Strategy<Value = KeyCode> {
2778        prop_oneof![
2779            prop::sample::select(vec![
2780                'a', 'b', 'q', 'x', 'z', 'A', 'Q', '1', '9', '+', '-', '.', '/', ' ',
2781                // Letters where uppercasing is not a faithful inverse of the
2782                // lowercasing `KeyCombo::new` applies, and a space that is
2783                // not the ASCII one. Every one of these broke the round trip
2784                // while the sample above was all ASCII.
2785                'ß',  // uppercases to two characters, `SS`
2786                'ı',  // uppercases to `I`, which lowercases to `i`
2787                '中', // caseless: uppercases to itself
2788                'é', 'İ', '\u{a0}', // Option+Space on macOS
2789            ])
2790            .prop_map(KeyCode::Char),
2791            (1u8..=24).prop_map(KeyCode::F),
2792            prop::sample::select(vec![
2793                KeyCode::Enter,
2794                KeyCode::Escape,
2795                KeyCode::Backspace,
2796                KeyCode::Tab,
2797                KeyCode::BackTab,
2798                KeyCode::Delete,
2799                KeyCode::Insert,
2800                KeyCode::Home,
2801                KeyCode::End,
2802                KeyCode::PageUp,
2803                KeyCode::PageDown,
2804                KeyCode::Up,
2805                KeyCode::Down,
2806                KeyCode::Left,
2807                KeyCode::Right,
2808                KeyCode::Null,
2809                KeyCode::MediaPlayPause,
2810                KeyCode::MediaStop,
2811                KeyCode::MediaNextTrack,
2812                KeyCode::MediaPrevTrack,
2813            ]),
2814        ]
2815    }
2816
2817    fn arb_mods() -> impl Strategy<Value = Modifiers> {
2818        (0u8..16).prop_map(|bits| {
2819            let mut modifiers = Modifiers::NONE;
2820            if bits & 0b0001 != 0 {
2821                modifiers |= Modifiers::CTRL;
2822            }
2823            if bits & 0b0010 != 0 {
2824                modifiers |= Modifiers::ALT;
2825            }
2826            if bits & 0b0100 != 0 {
2827                modifiers |= Modifiers::SHIFT;
2828            }
2829            if bits & 0b1000 != 0 {
2830                modifiers |= Modifiers::SUPER;
2831            }
2832            modifiers
2833        })
2834    }
2835
2836    fn arb_small_chord() -> impl Strategy<Value = Chord> {
2837        prop::collection::vec(
2838            prop::sample::select(vec!['a', 'b', 'c', 'd'])
2839                .prop_map(|c| KeyCombo::key(KeyCode::Char(c))),
2840            1..=3usize,
2841        )
2842        .prop_map(|combos| Chord::new(combos).expect("1..=3 combos is a valid chord"))
2843    }
2844
2845    fn arb_key() -> impl Strategy<Value = KeyEvent> {
2846        let code = prop_oneof![
2847            Just(KeyCode::Char('a')),
2848            Just(KeyCode::Char('b')),
2849            Just(KeyCode::Char('c')),
2850            Just(KeyCode::Enter),
2851            Just(KeyCode::Escape),
2852        ];
2853        let kind = prop_oneof![
2854            Just(KeyEventKind::Press),
2855            Just(KeyEventKind::Repeat),
2856            Just(KeyEventKind::Release),
2857        ];
2858        (code, kind).prop_map(|(code, kind)| KeyEvent {
2859            code,
2860            modifiers: Modifiers::NONE,
2861            kind,
2862        })
2863    }
2864
2865    proptest! {
2866        #![proptest_config(ProptestConfig::with_cases(1000))]
2867
2868        /// No key is ever swallowed: every feed yields at least one dispatch,
2869        /// and draining the timers afterwards never panics or leaks a prefix.
2870        #[test]
2871        fn every_fed_key_yields_a_dispatch(
2872            keys in proptest::collection::vec(arb_key(), 1..20),
2873            gaps in proptest::collection::vec(0u64..1500, 1..20),
2874        ) {
2875            let mut map = KeyMap::new();
2876            map.bind(chord("a b"), Act::GoTop);
2877            map.bind(chord("a"), Act::Help);
2878            map.bind(chord("c c c"), Act::Save);
2879            map.bind(chord("Enter"), Act::Submit);
2880            let mut dispatcher = KeyDispatcher::new(map);
2881            let mut now = Instant::now();
2882            for (key, gap) in keys.iter().zip(gaps.iter().cycle()) {
2883                now += ms(*gap);
2884                let out = dispatcher.feed(key, now);
2885                prop_assert!(!out.is_empty(), "{key:?} produced nothing");
2886                let _ = dispatcher.tick(now);
2887            }
2888            let _ = dispatcher.tick(now + ms(10_000));
2889            prop_assert_eq!(dispatcher.pending_prefix(), None);
2890            let stats = dispatcher.stats();
2891            prop_assert!(
2892                stats.dispatched + stats.pending + stats.expired + stats.unbound + stats.esc > 0
2893            );
2894        }
2895
2896        /// `Display` and `FromStr` are inverse over the normalized combo
2897        /// domain: parsing a combo's own text yields the same combo back.
2898        #[test]
2899        fn combo_display_parse_round_trip(code in arb_code(), mods in arb_mods()) {
2900            let combo = KeyCombo::new(code, mods);
2901            let text = combo.to_string();
2902            let parsed: KeyCombo = text
2903                .parse()
2904                .unwrap_or_else(|e| panic!("`{text}` did not re-parse: {e}"));
2905            prop_assert_eq!(parsed, combo, "text = `{}`", text);
2906        }
2907
2908        /// An unambiguous lookup never depends on the order bindings were
2909        /// inserted; only reported conflicts may change a winner.
2910        #[test]
2911        fn lookup_is_deterministic_under_shuffle(
2912            raw in prop::collection::vec((arb_small_chord(), any::<u64>()), 1..12),
2913            queries in prop::collection::vec(arb_small_chord(), 1..8),
2914        ) {
2915            // Keep only the first occurrence of each chord: a duplicate is a
2916            // reported conflict, not something lookup must resolve by order.
2917            let mut seen = std::collections::BTreeSet::new();
2918            let mut entries: Vec<(Chord, u64)> = Vec::new();
2919            for (ch, key) in raw {
2920                if seen.insert(ch.to_string()) {
2921                    entries.push((ch, key));
2922                }
2923            }
2924            let build = |order: &[(Chord, u64)]| {
2925                let mut map = KeyMap::new();
2926                for (ch, _) in order {
2927                    map.bind(ch.clone(), ch.to_string());
2928                }
2929                map
2930            };
2931            let in_order = build(&entries);
2932            let mut shuffled = entries.clone();
2933            shuffled.sort_by_key(|(_, key)| *key);
2934            let reordered = build(&shuffled);
2935            for query in &queries {
2936                let a = in_order.lookup(query, &[]);
2937                let b = reordered.lookup(query, &[]);
2938                prop_assert_eq!(
2939                    a.exact.map(|binding| binding.action.clone()),
2940                    b.exact.map(|binding| binding.action.clone()),
2941                    "winner changed for `{}`",
2942                    query
2943                );
2944                prop_assert_eq!(a.longer, b.longer, "longer count changed for `{}`", query);
2945            }
2946        }
2947    }
2948}
2949
2950#[cfg(test)]
2951mod tests {
2952    use super::*;
2953
2954    fn now() -> Instant {
2955        Instant::now()
2956    }
2957
2958    fn esc_press() -> KeyEvent {
2959        KeyEvent::new(KeyCode::Escape)
2960    }
2961
2962    fn key_press(code: KeyCode) -> KeyEvent {
2963        KeyEvent::new(code)
2964    }
2965
2966    fn esc_release() -> KeyEvent {
2967        KeyEvent::new(KeyCode::Escape).with_kind(KeyEventKind::Release)
2968    }
2969
2970    const MS_50: Duration = Duration::from_millis(50);
2971    const MS_100: Duration = Duration::from_millis(100);
2972    const MS_200: Duration = Duration::from_millis(200);
2973    const MS_300: Duration = Duration::from_millis(300);
2974
2975    // --- Basic sequence tests ---
2976
2977    #[test]
2978    fn single_esc_returns_pending() {
2979        let mut detector = SequenceDetector::with_defaults();
2980        let t = now();
2981
2982        let output = detector.feed(&esc_press(), t);
2983        assert_eq!(output, SequenceOutput::Pending);
2984        assert!(detector.is_pending());
2985    }
2986
2987    #[test]
2988    fn esc_esc_within_timeout() {
2989        let mut detector = SequenceDetector::with_defaults();
2990        let t = now();
2991
2992        detector.feed(&esc_press(), t);
2993        let output = detector.feed(&esc_press(), t + MS_100);
2994
2995        assert_eq!(output, SequenceOutput::EscEsc);
2996        assert!(!detector.is_pending());
2997    }
2998
2999    #[test]
3000    fn esc_esc_at_timeout_boundary() {
3001        let mut detector = SequenceDetector::with_defaults();
3002        let t = now();
3003
3004        detector.feed(&esc_press(), t);
3005        // Exactly at 250ms boundary
3006        let output = detector.feed(&esc_press(), t + Duration::from_millis(250));
3007
3008        assert_eq!(output, SequenceOutput::EscEsc);
3009    }
3010
3011    #[test]
3012    fn esc_esc_past_timeout() {
3013        let mut detector = SequenceDetector::with_defaults();
3014        let t = now();
3015
3016        detector.feed(&esc_press(), t);
3017        // Past 250ms timeout (251ms)
3018        let output = detector.feed(&esc_press(), t + Duration::from_millis(251));
3019
3020        // First Esc timed out, second Esc starts new sequence
3021        assert_eq!(output, SequenceOutput::Esc);
3022        assert!(detector.is_pending()); // New sequence started
3023    }
3024
3025    #[test]
3026    fn timeout_check_emits_pending_esc() {
3027        let mut detector = SequenceDetector::with_defaults();
3028        let t = now();
3029
3030        detector.feed(&esc_press(), t);
3031
3032        // Before timeout
3033        assert!(detector.check_timeout(t + MS_200).is_none());
3034        assert!(detector.is_pending());
3035
3036        // After timeout (251ms)
3037        let output = detector.check_timeout(t + Duration::from_millis(251));
3038        assert_eq!(output, Some(SequenceOutput::Esc));
3039        assert!(!detector.is_pending());
3040    }
3041
3042    #[test]
3043    fn other_key_interrupts_sequence() {
3044        let mut detector = SequenceDetector::with_defaults();
3045        let t = now();
3046
3047        detector.feed(&esc_press(), t);
3048        let output = detector.feed(&key_press(KeyCode::Char('a')), t + MS_100);
3049
3050        // Pending Esc is emitted
3051        assert_eq!(output, SequenceOutput::Esc);
3052        assert!(!detector.is_pending());
3053    }
3054
3055    #[test]
3056    fn non_esc_key_passes_through() {
3057        let mut detector = SequenceDetector::with_defaults();
3058        let t = now();
3059
3060        let output = detector.feed(&key_press(KeyCode::Char('x')), t);
3061        assert_eq!(output, SequenceOutput::PassThrough);
3062    }
3063
3064    #[test]
3065    fn release_event_passes_through() {
3066        let mut detector = SequenceDetector::with_defaults();
3067        let t = now();
3068
3069        let output = detector.feed(&esc_release(), t);
3070        assert_eq!(output, SequenceOutput::PassThrough);
3071        assert!(!detector.is_pending());
3072    }
3073
3074    #[test]
3075    fn release_during_pending_passes_through() {
3076        let mut detector = SequenceDetector::with_defaults();
3077        let t = now();
3078
3079        detector.feed(&esc_press(), t);
3080        let output = detector.feed(&esc_release(), t + MS_50);
3081
3082        // Release is ignored; still pending
3083        assert_eq!(output, SequenceOutput::PassThrough);
3084        assert!(detector.is_pending());
3085    }
3086
3087    // --- Config tests ---
3088
3089    #[test]
3090    fn custom_timeout() {
3091        let config = SequenceConfig::default().with_timeout(Duration::from_millis(100));
3092        let mut detector = SequenceDetector::new(config);
3093        let t = now();
3094
3095        detector.feed(&esc_press(), t);
3096        // 150ms is past 100ms timeout
3097        let output = detector.feed(&esc_press(), t + Duration::from_millis(150));
3098
3099        assert_eq!(output, SequenceOutput::Esc);
3100    }
3101
3102    #[test]
3103    fn disabled_sequences() {
3104        let config = SequenceConfig::default().disable_sequences();
3105        let mut detector = SequenceDetector::new(config);
3106        let t = now();
3107
3108        // First Esc immediately emits Esc
3109        let output = detector.feed(&esc_press(), t);
3110        assert_eq!(output, SequenceOutput::Esc);
3111        assert!(!detector.is_pending());
3112
3113        // Second Esc also immediately emits Esc
3114        let output = detector.feed(&esc_press(), t + MS_50);
3115        assert_eq!(output, SequenceOutput::Esc);
3116    }
3117
3118    #[test]
3119    fn disabled_sequences_passthrough() {
3120        let config = SequenceConfig::default().disable_sequences();
3121        let mut detector = SequenceDetector::new(config);
3122        let t = now();
3123
3124        let output = detector.feed(&key_press(KeyCode::Char('a')), t);
3125        assert_eq!(output, SequenceOutput::PassThrough);
3126    }
3127
3128    #[test]
3129    fn config_default_values() {
3130        let config = SequenceConfig::default();
3131        assert_eq!(config.esc_seq_timeout, Duration::from_millis(250));
3132        assert_eq!(config.esc_debounce, Duration::from_millis(50));
3133        assert!(!config.disable_sequences);
3134    }
3135
3136    #[test]
3137    fn config_builder_chain() {
3138        let config = SequenceConfig::default()
3139            .with_timeout(Duration::from_millis(300))
3140            .with_debounce(Duration::from_millis(100))
3141            .disable_sequences();
3142
3143        assert_eq!(config.esc_seq_timeout, Duration::from_millis(300));
3144        assert_eq!(config.esc_debounce, Duration::from_millis(100));
3145        assert!(config.disable_sequences);
3146    }
3147
3148    // --- Reset tests ---
3149
3150    #[test]
3151    fn reset_clears_pending() {
3152        let mut detector = SequenceDetector::with_defaults();
3153        let t = now();
3154
3155        detector.feed(&esc_press(), t);
3156        assert!(detector.is_pending());
3157
3158        detector.reset();
3159        assert!(!detector.is_pending());
3160
3161        // After reset, new Esc starts fresh
3162        let output = detector.feed(&esc_press(), t + MS_100);
3163        assert_eq!(output, SequenceOutput::Pending);
3164    }
3165
3166    #[test]
3167    fn reset_discards_pending_esc() {
3168        let mut detector = SequenceDetector::with_defaults();
3169        let t = now();
3170
3171        detector.feed(&esc_press(), t);
3172        detector.reset();
3173
3174        // Timeout check should not emit anything
3175        assert!(detector.check_timeout(t + MS_300).is_none());
3176    }
3177
3178    // --- Edge cases ---
3179
3180    #[test]
3181    fn rapid_triple_esc() {
3182        let mut detector = SequenceDetector::with_defaults();
3183        let t = now();
3184
3185        // First Esc
3186        let out1 = detector.feed(&esc_press(), t);
3187        assert_eq!(out1, SequenceOutput::Pending);
3188
3189        // Second Esc -> EscEsc
3190        let out2 = detector.feed(&esc_press(), t + MS_50);
3191        assert_eq!(out2, SequenceOutput::EscEsc);
3192
3193        // Third Esc -> starts new sequence
3194        let out3 = detector.feed(&esc_press(), t + MS_100);
3195        assert_eq!(out3, SequenceOutput::Pending);
3196    }
3197
3198    #[test]
3199    fn alternating_esc_and_key() {
3200        let mut detector = SequenceDetector::with_defaults();
3201        let t = now();
3202
3203        // Esc -> pending
3204        detector.feed(&esc_press(), t);
3205
3206        // 'a' -> emits Esc
3207        let out1 = detector.feed(&key_press(KeyCode::Char('a')), t + MS_50);
3208        assert_eq!(out1, SequenceOutput::Esc);
3209
3210        // Esc -> pending again
3211        let out2 = detector.feed(&esc_press(), t + MS_100);
3212        assert_eq!(out2, SequenceOutput::Pending);
3213
3214        // 'b' -> emits Esc
3215        let out3 = detector.feed(&key_press(KeyCode::Char('b')), t + MS_200);
3216        assert_eq!(out3, SequenceOutput::Esc);
3217    }
3218
3219    #[test]
3220    fn enter_key_interrupts() {
3221        let mut detector = SequenceDetector::with_defaults();
3222        let t = now();
3223
3224        detector.feed(&esc_press(), t);
3225        let output = detector.feed(&key_press(KeyCode::Enter), t + MS_100);
3226
3227        assert_eq!(output, SequenceOutput::Esc);
3228    }
3229
3230    #[test]
3231    fn function_key_interrupts() {
3232        let mut detector = SequenceDetector::with_defaults();
3233        let t = now();
3234
3235        detector.feed(&esc_press(), t);
3236        let output = detector.feed(&key_press(KeyCode::F(1)), t + MS_100);
3237
3238        assert_eq!(output, SequenceOutput::Esc);
3239    }
3240
3241    #[test]
3242    fn arrow_key_interrupts() {
3243        let mut detector = SequenceDetector::with_defaults();
3244        let t = now();
3245
3246        detector.feed(&esc_press(), t);
3247        let output = detector.feed(&key_press(KeyCode::Up), t + MS_100);
3248
3249        assert_eq!(output, SequenceOutput::Esc);
3250    }
3251
3252    #[test]
3253    fn config_getter_and_setter() {
3254        let mut detector = SequenceDetector::with_defaults();
3255        assert_eq!(
3256            detector.config().esc_seq_timeout,
3257            Duration::from_millis(250)
3258        );
3259
3260        let new_config = SequenceConfig::default().with_timeout(Duration::from_millis(500));
3261        detector.set_config(new_config);
3262
3263        assert_eq!(
3264            detector.config().esc_seq_timeout,
3265            Duration::from_millis(500)
3266        );
3267    }
3268
3269    #[test]
3270    fn set_config_preserves_pending_state() {
3271        let mut detector = SequenceDetector::with_defaults();
3272        let t = now();
3273
3274        detector.feed(&esc_press(), t);
3275        assert!(detector.is_pending());
3276
3277        // Change config while pending
3278        detector.set_config(SequenceConfig::default().with_timeout(Duration::from_millis(500)));
3279
3280        // Still pending
3281        assert!(detector.is_pending());
3282
3283        // New timeout applies
3284        let output = detector.feed(&esc_press(), t + MS_300);
3285        assert_eq!(output, SequenceOutput::EscEsc); // Within new 500ms timeout
3286    }
3287
3288    #[test]
3289    fn debug_format() {
3290        let detector = SequenceDetector::with_defaults();
3291        let dbg = format!("{:?}", detector);
3292        assert!(dbg.contains("SequenceDetector"));
3293    }
3294
3295    #[test]
3296    fn config_debug_format() {
3297        let config = SequenceConfig::default();
3298        let dbg = format!("{:?}", config);
3299        assert!(dbg.contains("SequenceConfig"));
3300    }
3301
3302    #[test]
3303    fn output_debug_and_eq() {
3304        assert_eq!(SequenceOutput::Pending, SequenceOutput::Pending);
3305        assert_eq!(SequenceOutput::Esc, SequenceOutput::Esc);
3306        assert_eq!(SequenceOutput::EscEsc, SequenceOutput::EscEsc);
3307        assert_eq!(SequenceOutput::PassThrough, SequenceOutput::PassThrough);
3308        assert_ne!(SequenceOutput::Esc, SequenceOutput::EscEsc);
3309
3310        let dbg = format!("{:?}", SequenceOutput::EscEsc);
3311        assert!(dbg.contains("EscEsc"));
3312    }
3313
3314    // --- Stress / property-like tests ---
3315
3316    #[test]
3317    fn no_stuck_state() {
3318        let mut detector = SequenceDetector::with_defaults();
3319        let t = now();
3320
3321        // Many operations should always return to Idle eventually
3322        for i in 0..100 {
3323            let offset = Duration::from_millis(i * 10);
3324            if i % 3 == 0 {
3325                detector.feed(&esc_press(), t + offset);
3326            } else {
3327                detector.feed(&key_press(KeyCode::Char('x')), t + offset);
3328            }
3329        }
3330
3331        // Force timeout check - must be well past the last event (990ms) + timeout (250ms)
3332        detector.check_timeout(t + Duration::from_secs(2));
3333
3334        // Should be idle
3335        assert!(!detector.is_pending());
3336    }
3337
3338    #[test]
3339    fn deterministic_output() {
3340        // Same inputs should produce same outputs
3341        let config = SequenceConfig::default();
3342        let t = now();
3343
3344        let mut d1 = SequenceDetector::new(config.clone());
3345        let mut d2 = SequenceDetector::new(config);
3346
3347        let events = [
3348            (esc_press(), t),
3349            (esc_press(), t + MS_100),
3350            (key_press(KeyCode::Char('a')), t + MS_200),
3351            (esc_press(), t + MS_300),
3352        ];
3353
3354        for (event, time) in &events {
3355            let out1 = d1.feed(event, *time);
3356            let out2 = d2.feed(event, *time);
3357            assert_eq!(out1, out2);
3358        }
3359    }
3360
3361    // =========================================================================
3362    // ActionMapper Tests
3363    // =========================================================================
3364
3365    mod action_mapper_tests {
3366        use super::*;
3367        use crate::event::Modifiers;
3368
3369        fn ctrl_c() -> KeyEvent {
3370            KeyEvent::new(KeyCode::Char('c')).with_modifiers(Modifiers::CTRL)
3371        }
3372
3373        fn ctrl_d() -> KeyEvent {
3374            KeyEvent::new(KeyCode::Char('d')).with_modifiers(Modifiers::CTRL)
3375        }
3376
3377        fn ctrl_q() -> KeyEvent {
3378            KeyEvent::new(KeyCode::Char('q')).with_modifiers(Modifiers::CTRL)
3379        }
3380
3381        fn idle_state() -> AppState {
3382            AppState::default()
3383        }
3384
3385        fn input_state() -> AppState {
3386            AppState::new().with_input(true)
3387        }
3388
3389        fn task_state() -> AppState {
3390            AppState::new().with_task(true)
3391        }
3392
3393        fn modal_state() -> AppState {
3394            AppState::new().with_modal(true)
3395        }
3396
3397        fn overlay_state() -> AppState {
3398            AppState::new().with_overlay(true)
3399        }
3400
3401        // --- Ctrl+C tests (policy priorities 2-5) ---
3402
3403        #[test]
3404        fn test_ctrl_c_clears_nonempty_input() {
3405            let mut mapper = ActionMapper::with_defaults();
3406            let t = now();
3407
3408            let action = mapper.map(&ctrl_c(), &input_state(), t);
3409            assert_eq!(action, Some(Action::ClearInput));
3410        }
3411
3412        #[test]
3413        fn test_ctrl_c_cancels_running_task() {
3414            let mut mapper = ActionMapper::with_defaults();
3415            let t = now();
3416
3417            let action = mapper.map(&ctrl_c(), &task_state(), t);
3418            assert_eq!(action, Some(Action::CancelTask));
3419        }
3420
3421        #[test]
3422        fn test_ctrl_c_quits_when_idle() {
3423            let mut mapper = ActionMapper::with_defaults();
3424            let t = now();
3425
3426            let action = mapper.map(&ctrl_c(), &idle_state(), t);
3427            assert_eq!(action, Some(Action::Quit));
3428        }
3429
3430        #[test]
3431        fn test_ctrl_c_dismisses_modal() {
3432            let mut mapper = ActionMapper::with_defaults();
3433            let t = now();
3434
3435            let action = mapper.map(&ctrl_c(), &modal_state(), t);
3436            assert_eq!(action, Some(Action::DismissModal));
3437        }
3438
3439        #[test]
3440        fn test_ctrl_c_modal_priority_over_input() {
3441            let mut mapper = ActionMapper::with_defaults();
3442            let t = now();
3443
3444            // Both modal and input are set
3445            let state = AppState::new().with_modal(true).with_input(true);
3446            let action = mapper.map(&ctrl_c(), &state, t);
3447            assert_eq!(action, Some(Action::DismissModal));
3448        }
3449
3450        #[test]
3451        fn test_ctrl_c_input_priority_over_task() {
3452            let mut mapper = ActionMapper::with_defaults();
3453            let t = now();
3454
3455            let state = AppState::new().with_input(true).with_task(true);
3456            let action = mapper.map(&ctrl_c(), &state, t);
3457            assert_eq!(action, Some(Action::ClearInput));
3458        }
3459
3460        #[test]
3461        fn test_ctrl_c_idle_config_noop() {
3462            let config = ActionConfig::default().with_ctrl_c_idle(CtrlCIdleAction::Noop);
3463            let mut mapper = ActionMapper::new(config);
3464            let t = now();
3465
3466            let action = mapper.map(&ctrl_c(), &idle_state(), t);
3467            assert_eq!(action, None); // Noop returns None
3468        }
3469
3470        #[test]
3471        fn test_ctrl_c_idle_config_bell() {
3472            let config = ActionConfig::default().with_ctrl_c_idle(CtrlCIdleAction::Bell);
3473            let mut mapper = ActionMapper::new(config);
3474            let t = now();
3475
3476            let action = mapper.map(&ctrl_c(), &idle_state(), t);
3477            assert_eq!(action, Some(Action::Bell));
3478        }
3479
3480        // --- Ctrl+D and Ctrl+Q tests (policy priorities 10-11) ---
3481
3482        #[test]
3483        fn test_ctrl_d_soft_quit() {
3484            let mut mapper = ActionMapper::with_defaults();
3485            let t = now();
3486
3487            let action = mapper.map(&ctrl_d(), &idle_state(), t);
3488            assert_eq!(action, Some(Action::SoftQuit));
3489        }
3490
3491        #[test]
3492        fn test_ctrl_d_ignores_state() {
3493            let mut mapper = ActionMapper::with_defaults();
3494            let t = now();
3495
3496            // Ctrl+D always does SoftQuit regardless of state
3497            let action = mapper.map(&ctrl_d(), &modal_state(), t);
3498            assert_eq!(action, Some(Action::SoftQuit));
3499
3500            let action = mapper.map(&ctrl_d(), &input_state(), t);
3501            assert_eq!(action, Some(Action::SoftQuit));
3502        }
3503
3504        #[test]
3505        fn test_ctrl_q_hard_quit() {
3506            let mut mapper = ActionMapper::with_defaults();
3507            let t = now();
3508
3509            let action = mapper.map(&ctrl_q(), &idle_state(), t);
3510            assert_eq!(action, Some(Action::HardQuit));
3511        }
3512
3513        #[test]
3514        fn test_ctrl_q_ignores_state() {
3515            let mut mapper = ActionMapper::with_defaults();
3516            let t = now();
3517
3518            // Ctrl+Q always does HardQuit regardless of state
3519            let action = mapper.map(&ctrl_q(), &modal_state(), t);
3520            assert_eq!(action, Some(Action::HardQuit));
3521        }
3522
3523        // --- Esc tests (policy priorities 1, 6-8) ---
3524
3525        #[test]
3526        fn test_esc_dismisses_modal() {
3527            let mut mapper = ActionMapper::with_defaults();
3528            let t = now();
3529
3530            // First Esc: pending
3531            let action1 = mapper.map(&esc_press(), &modal_state(), t);
3532            assert_eq!(action1, None);
3533
3534            // Timeout: emit Esc action
3535            let action2 = mapper.check_timeout(&modal_state(), t + MS_300);
3536            assert_eq!(action2, Some(Action::DismissModal));
3537        }
3538
3539        #[test]
3540        fn test_esc_clears_input_no_modal() {
3541            let mut mapper = ActionMapper::with_defaults();
3542            let t = now();
3543
3544            mapper.map(&esc_press(), &input_state(), t);
3545            let action = mapper.check_timeout(&input_state(), t + MS_300);
3546            assert_eq!(action, Some(Action::ClearInput));
3547        }
3548
3549        #[test]
3550        fn test_esc_cancels_task_empty_input() {
3551            let mut mapper = ActionMapper::with_defaults();
3552            let t = now();
3553
3554            mapper.map(&esc_press(), &task_state(), t);
3555            let action = mapper.check_timeout(&task_state(), t + MS_300);
3556            assert_eq!(action, Some(Action::CancelTask));
3557        }
3558
3559        #[test]
3560        fn test_esc_closes_overlay() {
3561            let mut mapper = ActionMapper::with_defaults();
3562            let t = now();
3563
3564            mapper.map(&esc_press(), &overlay_state(), t);
3565            let action = mapper.check_timeout(&overlay_state(), t + MS_300);
3566            assert_eq!(action, Some(Action::CloseOverlay));
3567        }
3568
3569        #[test]
3570        fn test_esc_modal_priority_over_overlay() {
3571            let mut mapper = ActionMapper::with_defaults();
3572            let t = now();
3573
3574            let state = AppState::new().with_modal(true).with_overlay(true);
3575            mapper.map(&esc_press(), &state, t);
3576            let action = mapper.check_timeout(&state, t + MS_300);
3577            assert_eq!(action, Some(Action::DismissModal));
3578        }
3579
3580        #[test]
3581        fn test_esc_passthrough_when_idle() {
3582            let mut mapper = ActionMapper::with_defaults();
3583            let t = now();
3584
3585            mapper.map(&esc_press(), &idle_state(), t);
3586            let action = mapper.check_timeout(&idle_state(), t + MS_300);
3587            assert_eq!(action, Some(Action::PassThrough));
3588        }
3589
3590        // --- Esc Esc tests (policy priority 9) ---
3591
3592        #[test]
3593        fn test_esc_esc_within_timeout() {
3594            let mut mapper = ActionMapper::with_defaults();
3595            let t = now();
3596
3597            mapper.map(&esc_press(), &idle_state(), t);
3598            let action = mapper.map(&esc_press(), &idle_state(), t + MS_100);
3599            assert_eq!(action, Some(Action::ToggleTreeView));
3600        }
3601
3602        #[test]
3603        fn test_esc_esc_ignores_state() {
3604            let mut mapper = ActionMapper::with_defaults();
3605            let t = now();
3606
3607            // Esc Esc always toggles tree view regardless of state
3608            mapper.map(&esc_press(), &modal_state(), t);
3609            let action = mapper.map(&esc_press(), &modal_state(), t + MS_100);
3610            assert_eq!(action, Some(Action::ToggleTreeView));
3611        }
3612
3613        #[test]
3614        fn test_esc_esc_timeout_expired() {
3615            let mut mapper = ActionMapper::with_defaults();
3616            let t = now();
3617
3618            mapper.map(&esc_press(), &input_state(), t);
3619            // Past 250ms timeout
3620            let action = mapper.map(&esc_press(), &input_state(), t + MS_300);
3621
3622            // First Esc timed out -> ClearInput, second starts new pending
3623            assert_eq!(action, Some(Action::ClearInput));
3624            assert!(mapper.is_pending_esc());
3625        }
3626
3627        // --- Esc then other key ---
3628
3629        #[test]
3630        fn test_esc_then_other_key() {
3631            let mut mapper = ActionMapper::with_defaults();
3632            let t = now();
3633
3634            mapper.map(&esc_press(), &input_state(), t);
3635            let action = mapper.map(&key_press(KeyCode::Char('a')), &input_state(), t + MS_50);
3636
3637            // Pending Esc is emitted
3638            assert_eq!(action, Some(Action::ClearInput));
3639        }
3640
3641        // --- Other keys passthrough ---
3642
3643        #[test]
3644        fn test_regular_key_passthrough() {
3645            let mut mapper = ActionMapper::with_defaults();
3646            let t = now();
3647
3648            let action = mapper.map(&key_press(KeyCode::Char('x')), &idle_state(), t);
3649            assert_eq!(action, Some(Action::PassThrough));
3650        }
3651
3652        #[test]
3653        fn test_release_event_passthrough() {
3654            let mut mapper = ActionMapper::with_defaults();
3655            let t = now();
3656
3657            let release = KeyEvent::new(KeyCode::Char('x')).with_kind(KeyEventKind::Release);
3658            let action = mapper.map(&release, &idle_state(), t);
3659            assert_eq!(action, Some(Action::PassThrough));
3660        }
3661
3662        // --- State helper tests ---
3663
3664        #[test]
3665        fn test_app_state_builders() {
3666            let state = AppState::new()
3667                .with_input(true)
3668                .with_task(true)
3669                .with_modal(true)
3670                .with_overlay(true);
3671
3672            assert!(state.input_nonempty);
3673            assert!(state.task_running);
3674            assert!(state.modal_open);
3675            assert!(state.view_overlay);
3676            assert!(!state.is_idle());
3677        }
3678
3679        #[test]
3680        fn test_app_state_is_idle() {
3681            assert!(AppState::default().is_idle());
3682            assert!(!AppState::new().with_input(true).is_idle());
3683            assert!(!AppState::new().with_task(true).is_idle());
3684            assert!(!AppState::new().with_modal(true).is_idle());
3685            // view_overlay doesn't affect is_idle
3686            assert!(AppState::new().with_overlay(true).is_idle());
3687        }
3688
3689        // --- Action enum tests ---
3690
3691        #[test]
3692        fn test_action_consumes_event() {
3693            assert!(Action::ClearInput.consumes_event());
3694            assert!(Action::CancelTask.consumes_event());
3695            assert!(Action::Quit.consumes_event());
3696            assert!(!Action::PassThrough.consumes_event());
3697        }
3698
3699        #[test]
3700        fn test_action_is_quit() {
3701            assert!(Action::Quit.is_quit());
3702            assert!(Action::SoftQuit.is_quit());
3703            assert!(Action::HardQuit.is_quit());
3704            assert!(!Action::ClearInput.is_quit());
3705            assert!(!Action::PassThrough.is_quit());
3706        }
3707
3708        // --- Config tests ---
3709
3710        #[test]
3711        fn test_ctrl_c_idle_action_from_str() {
3712            assert_eq!(
3713                CtrlCIdleAction::from_str_opt("quit"),
3714                Some(CtrlCIdleAction::Quit)
3715            );
3716            assert_eq!(
3717                CtrlCIdleAction::from_str_opt("QUIT"),
3718                Some(CtrlCIdleAction::Quit)
3719            );
3720            assert_eq!(
3721                CtrlCIdleAction::from_str_opt("noop"),
3722                Some(CtrlCIdleAction::Noop)
3723            );
3724            assert_eq!(
3725                CtrlCIdleAction::from_str_opt("none"),
3726                Some(CtrlCIdleAction::Noop)
3727            );
3728            assert_eq!(
3729                CtrlCIdleAction::from_str_opt("ignore"),
3730                Some(CtrlCIdleAction::Noop)
3731            );
3732            assert_eq!(
3733                CtrlCIdleAction::from_str_opt("bell"),
3734                Some(CtrlCIdleAction::Bell)
3735            );
3736            assert_eq!(
3737                CtrlCIdleAction::from_str_opt("beep"),
3738                Some(CtrlCIdleAction::Bell)
3739            );
3740            assert_eq!(CtrlCIdleAction::from_str_opt("invalid"), None);
3741        }
3742
3743        #[test]
3744        fn test_ctrl_c_idle_action_to_action() {
3745            assert_eq!(CtrlCIdleAction::Quit.to_action(), Some(Action::Quit));
3746            assert_eq!(CtrlCIdleAction::Noop.to_action(), None);
3747            assert_eq!(CtrlCIdleAction::Bell.to_action(), Some(Action::Bell));
3748        }
3749
3750        #[test]
3751        fn test_action_config_builder() {
3752            let config = ActionConfig::default()
3753                .with_sequence_config(SequenceConfig::default().with_timeout(MS_100))
3754                .with_ctrl_c_idle(CtrlCIdleAction::Bell);
3755
3756            assert_eq!(config.sequence_config.esc_seq_timeout, MS_100);
3757            assert_eq!(config.ctrl_c_idle_action, CtrlCIdleAction::Bell);
3758        }
3759
3760        // --- Reset tests ---
3761
3762        #[test]
3763        fn test_mapper_reset() {
3764            let mut mapper = ActionMapper::with_defaults();
3765            let t = now();
3766
3767            mapper.map(&esc_press(), &idle_state(), t);
3768            assert!(mapper.is_pending_esc());
3769
3770            mapper.reset();
3771            assert!(!mapper.is_pending_esc());
3772        }
3773
3774        // --- Determinism / property tests ---
3775
3776        #[test]
3777        fn test_deterministic_action_mapping() {
3778            let t = now();
3779
3780            let mut m1 = ActionMapper::with_defaults();
3781            let mut m2 = ActionMapper::with_defaults();
3782
3783            let events = [
3784                (ctrl_c(), input_state()),
3785                (ctrl_d(), modal_state()),
3786                (ctrl_q(), idle_state()),
3787            ];
3788
3789            for (event, state) in &events {
3790                let a1 = m1.map(event, state, t);
3791                let a2 = m2.map(event, state, t);
3792                assert_eq!(a1, a2);
3793            }
3794        }
3795
3796        #[test]
3797        fn test_uppercase_ctrl_keys() {
3798            let mut mapper = ActionMapper::with_defaults();
3799            let t = now();
3800
3801            // Ctrl+C with uppercase 'C' should also work
3802            let ctrl_c_upper = KeyEvent::new(KeyCode::Char('C')).with_modifiers(Modifiers::CTRL);
3803            let action = mapper.map(&ctrl_c_upper, &idle_state(), t);
3804            assert_eq!(action, Some(Action::Quit));
3805        }
3806
3807        // --- Validation tests ---
3808
3809        #[test]
3810        fn test_sequence_config_validation_clamps_high_timeout() {
3811            let config = SequenceConfig::default()
3812                .with_timeout(Duration::from_millis(1000)) // Too high
3813                .validated();
3814
3815            // Should clamp to MAX_ESC_SEQ_TIMEOUT_MS (400ms)
3816            assert_eq!(config.esc_seq_timeout.as_millis(), 400);
3817        }
3818
3819        #[test]
3820        fn test_sequence_config_validation_clamps_low_timeout() {
3821            let config = SequenceConfig::default()
3822                .with_timeout(Duration::from_millis(50)) // Too low
3823                .validated();
3824
3825            // Should clamp to MIN_ESC_SEQ_TIMEOUT_MS (150ms)
3826            assert_eq!(config.esc_seq_timeout.as_millis(), 150);
3827        }
3828
3829        #[test]
3830        fn test_sequence_config_validation_clamps_high_debounce() {
3831            let config = SequenceConfig::default()
3832                .with_debounce(Duration::from_millis(200)) // Too high
3833                .validated();
3834
3835            // Should clamp to MAX_ESC_DEBOUNCE_MS (100ms)
3836            assert_eq!(config.esc_debounce.as_millis(), 100);
3837        }
3838
3839        #[test]
3840        fn test_sequence_config_validation_debounce_not_exceeds_timeout() {
3841            let config = SequenceConfig::default()
3842                .with_timeout(Duration::from_millis(150))
3843                .with_debounce(Duration::from_millis(200)) // Higher than timeout
3844                .validated();
3845
3846            // Debounce should be clamped to min(100, 150) = 100,
3847            // but also can't exceed timeout (150)
3848            // Since debounce max is 100 and timeout is 150, debounce = 100
3849            assert!(config.esc_debounce <= config.esc_seq_timeout);
3850        }
3851
3852        #[test]
3853        fn test_sequence_config_is_valid() {
3854            assert!(SequenceConfig::default().is_valid());
3855
3856            // Invalid: timeout too high
3857            let invalid = SequenceConfig::default().with_timeout(Duration::from_millis(500));
3858            assert!(!invalid.is_valid());
3859
3860            // Valid after validation
3861            assert!(invalid.validated().is_valid());
3862        }
3863
3864        #[test]
3865        fn test_sequence_config_constants() {
3866            // Verify constants match spec
3867            assert_eq!(DEFAULT_ESC_SEQ_TIMEOUT_MS, 250);
3868            assert_eq!(MIN_ESC_SEQ_TIMEOUT_MS, 150);
3869            assert_eq!(MAX_ESC_SEQ_TIMEOUT_MS, 400);
3870            assert_eq!(DEFAULT_ESC_DEBOUNCE_MS, 50);
3871            assert_eq!(MIN_ESC_DEBOUNCE_MS, 0);
3872            assert_eq!(MAX_ESC_DEBOUNCE_MS, 100);
3873        }
3874
3875        #[test]
3876        fn test_action_config_validated() {
3877            let config = ActionConfig::default()
3878                .with_sequence_config(
3879                    SequenceConfig::default().with_timeout(Duration::from_millis(1000)),
3880                )
3881                .validated();
3882
3883            // Sequence config should be validated
3884            assert_eq!(config.sequence_config.esc_seq_timeout.as_millis(), 400);
3885        }
3886    }
3887}