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
1077impl fmt::Display for KeyCombo {
1078    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1079        let mut modifiers = self.modifiers;
1080        let key = match self.code {
1081            KeyCode::Char(c) if c.is_alphabetic() && modifiers.contains(Modifiers::SHIFT) => {
1082                modifiers.remove(Modifiers::SHIFT);
1083                c.to_uppercase().collect::<String>()
1084            }
1085            KeyCode::Char(' ') => "Space".to_string(),
1086            KeyCode::Char(c) => c.to_string(),
1087            KeyCode::Enter => "Enter".to_string(),
1088            KeyCode::Escape => "Esc".to_string(),
1089            KeyCode::Backspace => "Backspace".to_string(),
1090            KeyCode::Tab => "Tab".to_string(),
1091            KeyCode::BackTab => "BackTab".to_string(),
1092            KeyCode::Delete => "Delete".to_string(),
1093            KeyCode::Insert => "Insert".to_string(),
1094            KeyCode::Home => "Home".to_string(),
1095            KeyCode::End => "End".to_string(),
1096            KeyCode::PageUp => "PageUp".to_string(),
1097            KeyCode::PageDown => "PageDown".to_string(),
1098            KeyCode::Up => "Up".to_string(),
1099            KeyCode::Down => "Down".to_string(),
1100            KeyCode::Left => "Left".to_string(),
1101            KeyCode::Right => "Right".to_string(),
1102            KeyCode::F(n) => format!("F{n}"),
1103            KeyCode::Null => "Null".to_string(),
1104            KeyCode::MediaPlayPause => "MediaPlayPause".to_string(),
1105            KeyCode::MediaStop => "MediaStop".to_string(),
1106            KeyCode::MediaNextTrack => "MediaNextTrack".to_string(),
1107            KeyCode::MediaPrevTrack => "MediaPrevTrack".to_string(),
1108        };
1109        for (flag, name) in [
1110            (Modifiers::CTRL, "Ctrl"),
1111            (Modifiers::ALT, "Alt"),
1112            (Modifiers::SHIFT, "Shift"),
1113            (Modifiers::SUPER, "Super"),
1114        ] {
1115            if modifiers.contains(flag) {
1116                write!(f, "{name}+")?;
1117            }
1118        }
1119        f.write_str(&key)
1120    }
1121}
1122
1123/// Parse a key name: a single character, or a named key (case-insensitive:
1124/// `Enter`, `Esc`, `Tab`, `BackTab`, `Backspace`, `Delete`, `Insert`, `Home`,
1125/// `End`, `PageUp`, `PageDown`, `Up`, `Down`, `Left`, `Right`, `Space`,
1126/// `F1`..`F24`, media keys).
1127fn parse_key_name(name: &str) -> Result<KeyCode, KeyParseError> {
1128    let mut chars = name.chars();
1129    if let (Some(c), None) = (chars.next(), chars.next()) {
1130        return Ok(KeyCode::Char(c));
1131    }
1132    let lower = name.to_ascii_lowercase();
1133    let code = match lower.as_str() {
1134        "enter" | "return" => KeyCode::Enter,
1135        "esc" | "escape" => KeyCode::Escape,
1136        "backspace" => KeyCode::Backspace,
1137        "tab" => KeyCode::Tab,
1138        "backtab" => KeyCode::BackTab,
1139        "delete" | "del" => KeyCode::Delete,
1140        "insert" | "ins" => KeyCode::Insert,
1141        "home" => KeyCode::Home,
1142        "end" => KeyCode::End,
1143        "pageup" | "pgup" => KeyCode::PageUp,
1144        "pagedown" | "pgdn" => KeyCode::PageDown,
1145        "up" => KeyCode::Up,
1146        "down" => KeyCode::Down,
1147        "left" => KeyCode::Left,
1148        "right" => KeyCode::Right,
1149        "space" => KeyCode::Char(' '),
1150        "null" => KeyCode::Null,
1151        "mediaplaypause" => KeyCode::MediaPlayPause,
1152        "mediastop" => KeyCode::MediaStop,
1153        "medianexttrack" => KeyCode::MediaNextTrack,
1154        "mediaprevtrack" => KeyCode::MediaPrevTrack,
1155        other => {
1156            if let Some(digits) = other.strip_prefix('f')
1157                && let Ok(n) = digits.parse::<u8>()
1158                && (1..=24).contains(&n)
1159            {
1160                KeyCode::F(n)
1161            } else {
1162                return Err(KeyParseError::UnknownKey(name.to_string()));
1163            }
1164        }
1165    };
1166    Ok(code)
1167}
1168
1169impl FromStr for KeyCombo {
1170    type Err = KeyParseError;
1171
1172    /// Parse `Ctrl+x`, `Shift+Tab`, `F12`, `g`, `Ctrl++` (the plus key).
1173    /// Modifier names are case-insensitive (`Ctrl`/`Control`, `Alt`/`Opt`/
1174    /// `Option`, `Shift`, `Super`/`Cmd`/`Meta`/`Win`).
1175    fn from_str(s: &str) -> Result<Self, Self::Err> {
1176        let s = s.trim();
1177        if s.is_empty() {
1178            return Err(KeyParseError::EmptyKey);
1179        }
1180        let (modifier_part, key_part) = if s == "+" {
1181            ("", "+")
1182        } else if let Some(stripped) = s.strip_suffix('+') {
1183            (stripped.trim_end_matches('+'), "+")
1184        } else if let Some((modifiers, key)) = s.rsplit_once('+') {
1185            (modifiers, key)
1186        } else {
1187            ("", s)
1188        };
1189        let mut modifiers = Modifiers::NONE;
1190        for part in modifier_part.split('+').filter(|p| !p.is_empty()) {
1191            modifiers |= match part.to_ascii_lowercase().as_str() {
1192                "ctrl" | "control" => Modifiers::CTRL,
1193                "alt" | "opt" | "option" => Modifiers::ALT,
1194                "shift" => Modifiers::SHIFT,
1195                "super" | "cmd" | "meta" | "win" => Modifiers::SUPER,
1196                _ => return Err(KeyParseError::UnknownModifier(part.to_string())),
1197            };
1198        }
1199        if key_part.is_empty() {
1200            return Err(KeyParseError::EmptyKey);
1201        }
1202        Ok(Self::new(parse_key_name(key_part)?, modifiers))
1203    }
1204}
1205
1206/// One to [`Chord::MAX_LEN`] combos pressed in sequence (`g g`, `Ctrl+x Ctrl+s`).
1207#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1208pub struct Chord(Vec<KeyCombo>);
1209
1210impl Chord {
1211    /// Longest supported chord.
1212    pub const MAX_LEN: usize = 4;
1213
1214    /// A one-combo chord.
1215    #[must_use]
1216    pub fn single(combo: KeyCombo) -> Self {
1217        Self(vec![combo])
1218    }
1219
1220    /// Build a chord from combos (1..=[`Chord::MAX_LEN`]).
1221    pub fn new(combos: Vec<KeyCombo>) -> Result<Self, KeyParseError> {
1222        if combos.is_empty() {
1223            Err(KeyParseError::EmptyKey)
1224        } else if combos.len() > Self::MAX_LEN {
1225            Err(KeyParseError::TooManyKeys(combos.len()))
1226        } else {
1227            Ok(Self(combos))
1228        }
1229    }
1230
1231    /// Parse a whitespace-separated chord such as `"g g"` or `"Ctrl+x Ctrl+s"`.
1232    pub fn parse(s: &str) -> Result<Self, KeyParseError> {
1233        s.parse()
1234    }
1235
1236    /// The combos in order.
1237    #[must_use]
1238    pub fn combos(&self) -> &[KeyCombo] {
1239        &self.0
1240    }
1241
1242    /// Number of combos.
1243    #[must_use]
1244    pub fn len(&self) -> usize {
1245        self.0.len()
1246    }
1247
1248    /// Never true for a chord built through the constructors; provided for
1249    /// API completeness.
1250    #[must_use]
1251    pub fn is_empty(&self) -> bool {
1252        self.0.is_empty()
1253    }
1254
1255    /// Whether this chord is a strict prefix of `other` (`g` of `g g`).
1256    #[must_use]
1257    pub fn is_prefix_of(&self, other: &Self) -> bool {
1258        self.0.len() < other.0.len() && other.0.starts_with(&self.0)
1259    }
1260}
1261
1262impl FromStr for Chord {
1263    type Err = KeyParseError;
1264
1265    fn from_str(s: &str) -> Result<Self, Self::Err> {
1266        let combos = s
1267            .split_whitespace()
1268            .map(str::parse)
1269            .collect::<Result<Vec<KeyCombo>, _>>()?;
1270        Self::new(combos)
1271    }
1272}
1273
1274impl fmt::Display for Chord {
1275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1276        for (i, combo) in self.0.iter().enumerate() {
1277            if i > 0 {
1278                f.write_str(" ")?;
1279            }
1280            write!(f, "{combo}")?;
1281        }
1282        Ok(())
1283    }
1284}
1285
1286/// Binding priority level; higher wins for the same chord.
1287#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
1288#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1289pub enum Priority {
1290    /// Application-wide default.
1291    #[default]
1292    Global = 0,
1293    /// Active when the app is in a particular mode.
1294    Mode = 1,
1295    /// Owned by the focused widget.
1296    Widget = 2,
1297}
1298
1299/// An interned context name (see [`KeyMap::context`]).
1300#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1301pub struct ContextId(pub u32);
1302
1303/// Identifier of one binding inside a [`KeyMap`].
1304#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1305pub struct BindingId(pub u32);
1306
1307impl fmt::Display for BindingId {
1308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1309        write!(f, "#{}", self.0)
1310    }
1311}
1312
1313/// One chord bound to an action.
1314#[derive(Debug, Clone)]
1315pub struct Binding<A> {
1316    /// Identifier assigned by the map.
1317    pub id: BindingId,
1318    /// The chord that triggers the action.
1319    pub chord: Chord,
1320    /// The action to dispatch.
1321    pub action: A,
1322    /// Priority level.
1323    pub priority: Priority,
1324    /// Context the binding is limited to (`None` = always applicable).
1325    pub context: Option<ContextId>,
1326    /// Human-readable label for help bars and conflict reports.
1327    pub label: Option<String>,
1328}
1329
1330/// Lower bound of the chord timeout (ms).
1331pub const MIN_CHORD_TIMEOUT_MS: u64 = 200;
1332/// Upper bound of the chord timeout (ms).
1333pub const MAX_CHORD_TIMEOUT_MS: u64 = 5000;
1334/// Default chord timeout (ms).
1335pub const DEFAULT_CHORD_TIMEOUT_MS: u64 = 1000;
1336
1337/// Timing configuration of a [`KeyMap`].
1338#[derive(Debug, Clone)]
1339pub struct KeyMapConfig {
1340    /// How long a pending chord prefix waits for its next key.
1341    pub chord_timeout: Duration,
1342    /// Esc / Esc Esc detection settings for the dispatcher's detector.
1343    pub esc: SequenceConfig,
1344}
1345
1346impl Default for KeyMapConfig {
1347    fn default() -> Self {
1348        Self {
1349            chord_timeout: Duration::from_millis(DEFAULT_CHORD_TIMEOUT_MS),
1350            esc: SequenceConfig::default(),
1351        }
1352    }
1353}
1354
1355impl KeyMapConfig {
1356    /// Set the chord timeout, clamped to `200..=5000` ms.
1357    #[must_use]
1358    pub fn with_chord_timeout(mut self, timeout: Duration) -> Self {
1359        let ms = timeout.as_millis().clamp(
1360            u128::from(MIN_CHORD_TIMEOUT_MS),
1361            u128::from(MAX_CHORD_TIMEOUT_MS),
1362        );
1363        self.chord_timeout = Duration::from_millis(ms as u64);
1364        self
1365    }
1366
1367    /// Set the Esc sequence configuration.
1368    #[must_use]
1369    pub fn with_esc(mut self, esc: SequenceConfig) -> Self {
1370        self.esc = esc;
1371        self
1372    }
1373}
1374
1375/// Result of [`KeyMap::lookup`].
1376#[derive(Debug, Clone, Copy)]
1377pub struct Lookup<'a, A> {
1378    /// The winning binding for exactly this chord, if any.
1379    pub exact: Option<&'a Binding<A>>,
1380    /// Number of applicable bindings whose chord starts with this chord and
1381    /// is longer (a pending prefix must wait for them).
1382    pub longer: usize,
1383}
1384
1385impl<A> Lookup<'_, A> {
1386    /// Neither an exact binding nor a longer chord.
1387    #[must_use]
1388    pub fn is_none(&self) -> bool {
1389        self.exact.is_none() && self.longer == 0
1390    }
1391}
1392
1393/// A binding conflict found by [`KeyMap::conflicts`].
1394#[derive(Debug, Clone, PartialEq, Eq)]
1395pub enum Conflict {
1396    /// Same chord, same context, different priority: `winner` hides `loser`.
1397    Shadowed {
1398        winner: BindingId,
1399        loser: BindingId,
1400        chord: Chord,
1401    },
1402    /// `short` is a strict prefix of `long`, so `short` fires only after the
1403    /// chord timeout or a non-extending key.
1404    PrefixCollision {
1405        short: BindingId,
1406        long: BindingId,
1407        short_chord: Chord,
1408        long_chord: Chord,
1409    },
1410    /// Same chord, context and priority: the later binding wins.
1411    Duplicate {
1412        first: BindingId,
1413        second: BindingId,
1414        chord: Chord,
1415    },
1416}
1417
1418/// Every conflict in a map, with a one-line warning per item.
1419#[derive(Debug, Clone, Default, PartialEq, Eq)]
1420pub struct ConflictReport {
1421    /// The conflicts, in map order.
1422    pub items: Vec<Conflict>,
1423}
1424
1425impl ConflictReport {
1426    /// No conflicts.
1427    #[must_use]
1428    pub fn is_empty(&self) -> bool {
1429        self.items.is_empty()
1430    }
1431
1432    /// Number of conflicts.
1433    #[must_use]
1434    pub fn len(&self) -> usize {
1435        self.items.len()
1436    }
1437}
1438
1439impl fmt::Display for ConflictReport {
1440    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1441        for item in &self.items {
1442            match item {
1443                Conflict::Shadowed {
1444                    winner,
1445                    loser,
1446                    chord,
1447                } => writeln!(
1448                    f,
1449                    "warning: binding {winner} shadows binding {loser} on `{chord}` (higher priority)"
1450                )?,
1451                Conflict::PrefixCollision {
1452                    short,
1453                    long,
1454                    short_chord,
1455                    long_chord,
1456                } => writeln!(
1457                    f,
1458                    "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"
1459                )?,
1460                Conflict::Duplicate {
1461                    first,
1462                    second,
1463                    chord,
1464                } => writeln!(
1465                    f,
1466                    "warning: bindings {first} and {second} both bind `{chord}` at the same priority; the later one wins"
1467                )?,
1468            }
1469        }
1470        Ok(())
1471    }
1472}
1473
1474/// A declarative binding map: chords to actions with priorities and
1475/// contexts. Actions are any `Clone` type (usually an app enum).
1476#[derive(Debug, Clone)]
1477pub struct KeyMap<A> {
1478    bindings: Vec<Binding<A>>,
1479    contexts: Vec<String>,
1480    config: KeyMapConfig,
1481    next_id: u32,
1482}
1483
1484impl<A> Default for KeyMap<A> {
1485    fn default() -> Self {
1486        Self::new()
1487    }
1488}
1489
1490impl<A> KeyMap<A> {
1491    /// An empty map with default timing.
1492    #[must_use]
1493    pub fn new() -> Self {
1494        Self::with_config(KeyMapConfig::default())
1495    }
1496
1497    /// An empty map with the given timing.
1498    #[must_use]
1499    pub fn with_config(config: KeyMapConfig) -> Self {
1500        Self {
1501            bindings: Vec::new(),
1502            contexts: Vec::new(),
1503            config,
1504            next_id: 0,
1505        }
1506    }
1507
1508    /// Timing configuration.
1509    #[must_use]
1510    pub fn config(&self) -> &KeyMapConfig {
1511        &self.config
1512    }
1513
1514    /// Intern a context name; the same name always yields the same id.
1515    pub fn context(&mut self, name: &str) -> ContextId {
1516        if let Some(index) = self.contexts.iter().position(|n| n == name) {
1517            return ContextId(index as u32);
1518        }
1519        self.contexts.push(name.to_string());
1520        ContextId((self.contexts.len() - 1) as u32)
1521    }
1522
1523    /// Name of an interned context.
1524    #[must_use]
1525    pub fn context_name(&self, id: ContextId) -> Option<&str> {
1526        self.contexts.get(id.0 as usize).map(String::as_str)
1527    }
1528
1529    /// Bind a chord at [`Priority::Global`] with no context.
1530    pub fn bind(&mut self, chord: Chord, action: A) -> BindingId {
1531        self.bind_in(chord, action, Priority::Global, None)
1532    }
1533
1534    /// Bind a chord with an explicit priority and optional context.
1535    pub fn bind_in(
1536        &mut self,
1537        chord: Chord,
1538        action: A,
1539        priority: Priority,
1540        context: Option<ContextId>,
1541    ) -> BindingId {
1542        let id = BindingId(self.next_id);
1543        self.next_id += 1;
1544        self.bindings.push(Binding {
1545            id,
1546            chord,
1547            action,
1548            priority,
1549            context,
1550            label: None,
1551        });
1552        id
1553    }
1554
1555    /// Attach a label to a binding; `false` if the id is unknown.
1556    pub fn set_label(&mut self, id: BindingId, label: impl Into<String>) -> bool {
1557        match self.bindings.iter_mut().find(|b| b.id == id) {
1558            Some(binding) => {
1559                binding.label = Some(label.into());
1560                true
1561            }
1562            None => false,
1563        }
1564    }
1565
1566    /// Remove a binding.
1567    pub fn unbind(&mut self, id: BindingId) -> Option<Binding<A>> {
1568        let index = self.bindings.iter().position(|b| b.id == id)?;
1569        Some(self.bindings.remove(index))
1570    }
1571
1572    /// All bindings in bind order.
1573    #[must_use]
1574    pub fn bindings(&self) -> &[Binding<A>] {
1575        &self.bindings
1576    }
1577
1578    /// A binding by id.
1579    #[must_use]
1580    pub fn get(&self, id: BindingId) -> Option<&Binding<A>> {
1581        self.bindings.iter().find(|b| b.id == id)
1582    }
1583
1584    /// Number of bindings.
1585    #[must_use]
1586    pub fn len(&self) -> usize {
1587        self.bindings.len()
1588    }
1589
1590    /// Whether the map has no bindings.
1591    #[must_use]
1592    pub fn is_empty(&self) -> bool {
1593        self.bindings.is_empty()
1594    }
1595
1596    fn applies(binding: &Binding<A>, active: &[ContextId]) -> bool {
1597        binding
1598            .context
1599            .is_none_or(|context| active.contains(&context))
1600    }
1601
1602    /// Ranking used to pick a winner among bindings for the same chord:
1603    /// active context beats none, then priority, then recency.
1604    fn rank(binding: &Binding<A>) -> (bool, Priority, BindingId) {
1605        (binding.context.is_some(), binding.priority, binding.id)
1606    }
1607
1608    /// Resolve `chord` against the bindings applicable under `active`
1609    /// contexts: the winning exact binding and how many longer bound chords
1610    /// start with it.
1611    #[must_use]
1612    pub fn lookup(&self, chord: &Chord, active: &[ContextId]) -> Lookup<'_, A> {
1613        let mut exact: Option<&Binding<A>> = None;
1614        let mut longer = 0;
1615        for binding in &self.bindings {
1616            if !Self::applies(binding, active) {
1617                continue;
1618            }
1619            if binding.chord == *chord {
1620                if exact.is_none_or(|current| Self::rank(binding) > Self::rank(current)) {
1621                    exact = Some(binding);
1622                }
1623            } else if chord.is_prefix_of(&binding.chord) {
1624                longer += 1;
1625            }
1626        }
1627        Lookup { exact, longer }
1628    }
1629
1630    /// Report shadowed, duplicate, and prefix-colliding bindings.
1631    #[must_use]
1632    pub fn conflicts(&self) -> ConflictReport {
1633        let mut items = Vec::new();
1634        for (i, a) in self.bindings.iter().enumerate() {
1635            for b in &self.bindings[i + 1..] {
1636                if a.chord == b.chord {
1637                    if a.context != b.context {
1638                        // A context-specific override is the intended use.
1639                        continue;
1640                    }
1641                    if a.priority == b.priority {
1642                        items.push(Conflict::Duplicate {
1643                            first: a.id,
1644                            second: b.id,
1645                            chord: a.chord.clone(),
1646                        });
1647                    } else {
1648                        let (winner, loser) = if a.priority > b.priority {
1649                            (a.id, b.id)
1650                        } else {
1651                            (b.id, a.id)
1652                        };
1653                        items.push(Conflict::Shadowed {
1654                            winner,
1655                            loser,
1656                            chord: a.chord.clone(),
1657                        });
1658                    }
1659                } else if a.chord.is_prefix_of(&b.chord) {
1660                    items.push(Conflict::PrefixCollision {
1661                        short: a.id,
1662                        long: b.id,
1663                        short_chord: a.chord.clone(),
1664                        long_chord: b.chord.clone(),
1665                    });
1666                } else if b.chord.is_prefix_of(&a.chord) {
1667                    items.push(Conflict::PrefixCollision {
1668                        short: b.id,
1669                        long: a.id,
1670                        short_chord: b.chord.clone(),
1671                        long_chord: a.chord.clone(),
1672                    });
1673                }
1674            }
1675        }
1676        ConflictReport { items }
1677    }
1678}
1679
1680/// What the dispatcher decided for one key event or tick.
1681#[derive(Debug, Clone, PartialEq, Eq)]
1682pub enum Dispatch<A> {
1683    /// A binding fired.
1684    Action {
1685        action: A,
1686        binding: BindingId,
1687        chord: Chord,
1688    },
1689    /// The key extended a chord prefix; waiting for more keys or the timeout.
1690    Pending { prefix: Chord },
1691    /// The key matched nothing (and could not extend a chord).
1692    Unbound(KeyEvent),
1693    /// A pending prefix was abandoned (timeout or a non-extending key).
1694    Expired { prefix: Chord },
1695    /// Esc sequence detector output for an unbound Esc / Esc Esc.
1696    Esc(SequenceOutput),
1697}
1698
1699/// Counters for evidence rows and hint-usage feedback.
1700#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1701pub struct DispatchStats {
1702    /// Bindings fired.
1703    pub dispatched: u64,
1704    /// Keys that entered a chord prefix.
1705    pub pending: u64,
1706    /// Prefixes abandoned.
1707    pub expired: u64,
1708    /// Keys that matched nothing.
1709    pub unbound: u64,
1710    /// Esc / Esc Esc verdicts handed back unbound (see [`Dispatch::Esc`]).
1711    pub esc: u64,
1712}
1713
1714fn action_dispatch<A: Clone>(binding: &Binding<A>, chord: Chord) -> Dispatch<A> {
1715    Dispatch::Action {
1716        action: binding.action.clone(),
1717        binding: binding.id,
1718        chord,
1719    }
1720}
1721
1722/// Chord-aware dispatcher over a [`KeyMap`].
1723///
1724/// Feed every key event through [`feed`](Self::feed) and call
1725/// [`tick`](Self::tick) once per frame so pending chords and the Esc timer
1726/// expire; every call returns the decisions to act on. See the module
1727/// section header for the resolution rules.
1728#[derive(Debug)]
1729pub struct KeyDispatcher<A> {
1730    map: KeyMap<A>,
1731    pending: Vec<KeyCombo>,
1732    pending_since: Option<Instant>,
1733    esc: SequenceDetector,
1734    active_contexts: Vec<ContextId>,
1735    stats: DispatchStats,
1736}
1737
1738impl<A: Clone> KeyDispatcher<A> {
1739    /// A dispatcher over `map` with no active contexts.
1740    #[must_use]
1741    pub fn new(map: KeyMap<A>) -> Self {
1742        let esc = SequenceDetector::new(map.config().esc.clone());
1743        Self {
1744            map,
1745            pending: Vec::new(),
1746            pending_since: None,
1747            esc,
1748            active_contexts: Vec::new(),
1749            stats: DispatchStats::default(),
1750        }
1751    }
1752
1753    /// The underlying map.
1754    #[must_use]
1755    pub fn map(&self) -> &KeyMap<A> {
1756        &self.map
1757    }
1758
1759    /// Mutable access to the map (rebinding at runtime).
1760    pub fn map_mut(&mut self) -> &mut KeyMap<A> {
1761        &mut self.map
1762    }
1763
1764    /// Replace the set of active contexts (focused widget, mode, ...).
1765    pub fn set_active_contexts(&mut self, contexts: &[ContextId]) {
1766        self.active_contexts.clear();
1767        self.active_contexts.extend_from_slice(contexts);
1768    }
1769
1770    /// Currently active contexts.
1771    #[must_use]
1772    pub fn active_contexts(&self) -> &[ContextId] {
1773        &self.active_contexts
1774    }
1775
1776    /// The chord prefix currently waiting for more keys.
1777    #[must_use]
1778    pub fn pending_prefix(&self) -> Option<Chord> {
1779        Chord::new(self.pending.clone()).ok()
1780    }
1781
1782    /// Counters so far.
1783    #[must_use]
1784    pub fn stats(&self) -> DispatchStats {
1785        self.stats
1786    }
1787
1788    /// Drop any pending prefix and Esc state.
1789    pub fn reset(&mut self) {
1790        self.pending.clear();
1791        self.pending_since = None;
1792        self.esc.reset();
1793    }
1794
1795    /// Process one key event.
1796    pub fn feed(&mut self, key: &KeyEvent, now: Instant) -> Vec<Dispatch<A>> {
1797        let mut out = Vec::with_capacity(2);
1798
1799        if key.code == KeyCode::Escape {
1800            if key.kind == KeyEventKind::Press {
1801                self.flush_pending(&mut out, false);
1802            }
1803            match self.esc.feed(key, now) {
1804                // Repeat / release of Esc: nothing sequence-related to do.
1805                SequenceOutput::PassThrough => {}
1806                output => {
1807                    self.dispatch_esc(output, &mut out);
1808                    return out;
1809                }
1810            }
1811        }
1812
1813        match key.kind {
1814            KeyEventKind::Release => {
1815                self.stats.unbound += 1;
1816                out.push(Dispatch::Unbound(*key));
1817                return out;
1818            }
1819            KeyEventKind::Repeat => {
1820                // A held key re-fires its own single-key binding, never a chord.
1821                let single = Chord::single(KeyCombo::from_event(key));
1822                let fired = if self.pending.is_empty() {
1823                    self.map
1824                        .lookup(&single, &self.active_contexts)
1825                        .exact
1826                        .map(|binding| action_dispatch(binding, single))
1827                } else {
1828                    None
1829                };
1830                match fired {
1831                    Some(dispatch) => {
1832                        self.stats.dispatched += 1;
1833                        out.push(dispatch);
1834                    }
1835                    None => {
1836                        self.stats.unbound += 1;
1837                        out.push(Dispatch::Unbound(*key));
1838                    }
1839                }
1840                return out;
1841            }
1842            KeyEventKind::Press => {}
1843        }
1844
1845        let combo = KeyCombo::from_event(key);
1846        if self.try_extend(combo, now, &mut out) {
1847            return out;
1848        }
1849
1850        // The key cannot extend the prefix: flush it, then start over with the
1851        // key on its own so it can fire or begin a new chord.
1852        if !self.pending.is_empty() {
1853            self.flush_pending(&mut out, false);
1854            if self.try_extend(combo, now, &mut out) {
1855                return out;
1856            }
1857        }
1858
1859        self.stats.unbound += 1;
1860        out.push(Dispatch::Unbound(*key));
1861        out
1862    }
1863
1864    /// Expire a pending prefix past the chord timeout and drive the Esc timer.
1865    pub fn tick(&mut self, now: Instant) -> Vec<Dispatch<A>> {
1866        let mut out = Vec::new();
1867        if let Some(since) = self.pending_since
1868            && now.saturating_duration_since(since) >= self.map.config.chord_timeout
1869        {
1870            self.flush_pending(&mut out, true);
1871        }
1872        if let Some(output) = self.esc.check_timeout(now) {
1873            self.dispatch_esc(output, &mut out);
1874        }
1875        out
1876    }
1877
1878    /// Try to treat `combo` as the next key of the pending prefix. Returns
1879    /// `false` when the extended chord matches nothing (nothing is emitted).
1880    fn try_extend(&mut self, combo: KeyCombo, now: Instant, out: &mut Vec<Dispatch<A>>) -> bool {
1881        if self.pending.len() >= Chord::MAX_LEN {
1882            return false;
1883        }
1884        let mut candidate = self.pending.clone();
1885        candidate.push(combo);
1886        let chord = Chord(candidate);
1887        let lookup = self.map.lookup(&chord, &self.active_contexts);
1888        if let Some(binding) = lookup.exact
1889            && lookup.longer == 0
1890        {
1891            let dispatch = action_dispatch(binding, chord);
1892            self.pending.clear();
1893            self.pending_since = None;
1894            self.stats.dispatched += 1;
1895            out.push(dispatch);
1896            return true;
1897        }
1898        if lookup.exact.is_some() || lookup.longer > 0 {
1899            self.pending.clone_from(&chord.0);
1900            self.pending_since = Some(now);
1901            self.stats.pending += 1;
1902            out.push(Dispatch::Pending { prefix: chord });
1903            return true;
1904        }
1905        false
1906    }
1907
1908    /// Fire the pending prefix if it is bound, otherwise report it expired;
1909    /// on a timeout the expiry is reported first so the delay is visible.
1910    fn flush_pending(&mut self, out: &mut Vec<Dispatch<A>>, timed_out: bool) {
1911        if self.pending.is_empty() {
1912            return;
1913        }
1914        let prefix = Chord(std::mem::take(&mut self.pending));
1915        self.pending_since = None;
1916        let fired = self
1917            .map
1918            .lookup(&prefix, &self.active_contexts)
1919            .exact
1920            .map(|binding| action_dispatch(binding, prefix.clone()));
1921        match fired {
1922            Some(dispatch) => {
1923                if timed_out {
1924                    self.stats.expired += 1;
1925                    out.push(Dispatch::Expired { prefix });
1926                }
1927                self.stats.dispatched += 1;
1928                out.push(dispatch);
1929            }
1930            None => {
1931                self.stats.expired += 1;
1932                out.push(Dispatch::Expired { prefix });
1933            }
1934        }
1935    }
1936
1937    /// Route a detector verdict: a bound `Esc` / `Esc Esc` fires its binding,
1938    /// anything else is handed back as [`Dispatch::Esc`].
1939    fn dispatch_esc(&mut self, output: SequenceOutput, out: &mut Vec<Dispatch<A>>) {
1940        let esc = KeyCombo::key(KeyCode::Escape);
1941        let bound = match output {
1942            SequenceOutput::Esc => Some(Chord::single(esc)),
1943            SequenceOutput::EscEsc => Chord::new(vec![esc, esc]).ok(),
1944            SequenceOutput::Pending | SequenceOutput::PassThrough => None,
1945        };
1946        let fired = bound.and_then(|chord| {
1947            self.map
1948                .lookup(&chord, &self.active_contexts)
1949                .exact
1950                .map(|binding| action_dispatch(binding, chord.clone()))
1951        });
1952        match fired {
1953            Some(dispatch) => {
1954                self.stats.dispatched += 1;
1955                out.push(dispatch);
1956            }
1957            None => {
1958                self.stats.esc += 1;
1959                out.push(Dispatch::Esc(output));
1960            }
1961        }
1962    }
1963}
1964
1965// ---------------------------------------------------------------------------
1966// Serialization (feature `serde`): human-editable keymap files
1967// ---------------------------------------------------------------------------
1968
1969/// On-disk shape of a [`KeyMap`] (feature `serde`): chords as text, contexts
1970/// by name, the chord timeout in milliseconds. Esc timing is not part of the
1971/// file; it follows [`SequenceConfig`] (defaults and `FTUI_DISABLE_ESC_SEQ`).
1972///
1973/// ```toml
1974/// chord_timeout_ms = 750
1975///
1976/// [[bindings]]
1977/// chord = "Ctrl+x Ctrl+s"
1978/// action = "Save"
1979/// priority = "Mode"
1980/// label = "save"
1981///
1982/// [[bindings]]
1983/// chord = "Enter"
1984/// action = "Newline"
1985/// priority = "Widget"
1986/// context = "editor"
1987/// ```
1988#[cfg(feature = "serde")]
1989#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1990#[serde(deny_unknown_fields)]
1991pub struct KeyMapFile<A> {
1992    /// Chord timeout in milliseconds (clamped to `200..=5000` on load).
1993    #[serde(default = "default_chord_timeout_ms")]
1994    pub chord_timeout_ms: u64,
1995    /// Bindings in bind order.
1996    #[serde(default = "Vec::new")]
1997    pub bindings: Vec<BindingFile<A>>,
1998}
1999
2000#[cfg(feature = "serde")]
2001fn default_chord_timeout_ms() -> u64 {
2002    DEFAULT_CHORD_TIMEOUT_MS
2003}
2004
2005/// One binding in a [`KeyMapFile`].
2006#[cfg(feature = "serde")]
2007#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2008#[serde(deny_unknown_fields)]
2009pub struct BindingFile<A> {
2010    /// Chord text, e.g. `"g g"` or `"Ctrl+x Ctrl+s"`.
2011    pub chord: String,
2012    /// The action (any serde type; usually a unit-variant enum).
2013    pub action: A,
2014    /// Priority level (default `Global`).
2015    #[serde(default)]
2016    pub priority: Priority,
2017    /// Context name (default: none).
2018    #[serde(default, skip_serializing_if = "Option::is_none")]
2019    pub context: Option<String>,
2020    /// Help label (default: none).
2021    #[serde(default, skip_serializing_if = "Option::is_none")]
2022    pub label: Option<String>,
2023}
2024
2025/// Why a [`KeyMapFile`] could not become a [`KeyMap`].
2026#[cfg(feature = "serde")]
2027#[derive(Debug, Clone, PartialEq, Eq)]
2028pub enum KeyMapFileError {
2029    /// A binding's chord text did not parse.
2030    Chord {
2031        /// Index of the binding in the file.
2032        index: usize,
2033        /// The offending chord text.
2034        chord: String,
2035        /// Parse error.
2036        source: KeyParseError,
2037    },
2038}
2039
2040#[cfg(feature = "serde")]
2041impl fmt::Display for KeyMapFileError {
2042    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2043        match self {
2044            Self::Chord {
2045                index,
2046                chord,
2047                source,
2048            } => write!(f, "binding {index} (`{chord}`): {source}"),
2049        }
2050    }
2051}
2052
2053#[cfg(feature = "serde")]
2054impl std::error::Error for KeyMapFileError {}
2055
2056#[cfg(feature = "serde")]
2057impl<A: Clone> KeyMap<A> {
2058    /// The file representation (contexts by name, chords as text).
2059    #[must_use]
2060    pub fn to_file(&self) -> KeyMapFile<A> {
2061        KeyMapFile {
2062            chord_timeout_ms: self.config.chord_timeout.as_millis() as u64,
2063            bindings: self
2064                .bindings
2065                .iter()
2066                .map(|binding| BindingFile {
2067                    chord: binding.chord.to_string(),
2068                    action: binding.action.clone(),
2069                    priority: binding.priority,
2070                    context: binding
2071                        .context
2072                        .and_then(|id| self.context_name(id))
2073                        .map(str::to_string),
2074                    label: binding.label.clone(),
2075                })
2076                .collect(),
2077        }
2078    }
2079
2080    /// Build a map from its file representation, interning context names and
2081    /// parsing chords; a bad chord names the offending binding.
2082    pub fn from_file(file: KeyMapFile<A>) -> Result<Self, KeyMapFileError> {
2083        let config = KeyMapConfig::default()
2084            .with_chord_timeout(Duration::from_millis(file.chord_timeout_ms));
2085        let mut map = Self::with_config(config);
2086        for (index, entry) in file.bindings.into_iter().enumerate() {
2087            let chord = Chord::parse(&entry.chord).map_err(|source| KeyMapFileError::Chord {
2088                index,
2089                chord: entry.chord.clone(),
2090                source,
2091            })?;
2092            let context = entry.context.as_deref().map(|name| map.context(name));
2093            let id = map.bind_in(chord, entry.action, entry.priority, context);
2094            if let Some(label) = entry.label {
2095                map.set_label(id, label);
2096            }
2097        }
2098        Ok(map)
2099    }
2100}
2101
2102#[cfg(feature = "serde")]
2103impl<A: Clone + serde::Serialize> serde::Serialize for KeyMap<A> {
2104    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2105        self.to_file().serialize(serializer)
2106    }
2107}
2108
2109#[cfg(feature = "serde")]
2110impl<'de, A: Clone + serde::Deserialize<'de>> serde::Deserialize<'de> for KeyMap<A> {
2111    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2112        let file = KeyMapFile::<A>::deserialize(deserializer)?;
2113        Self::from_file(file).map_err(serde::de::Error::custom)
2114    }
2115}
2116
2117#[cfg(test)]
2118mod keymap_tests {
2119    use super::*;
2120    use proptest::prelude::*;
2121
2122    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
2123    enum Act {
2124        GoTop,
2125        Help,
2126        Save,
2127        Quit,
2128        Submit,
2129        Newline,
2130        Global,
2131        Mode,
2132        Widget,
2133        Down,
2134    }
2135
2136    fn press(c: char) -> KeyEvent {
2137        KeyEvent::new(KeyCode::Char(c))
2138    }
2139
2140    fn kind(mut event: KeyEvent, kind: KeyEventKind) -> KeyEvent {
2141        event.kind = kind;
2142        event
2143    }
2144
2145    fn ms(n: u64) -> Duration {
2146        Duration::from_millis(n)
2147    }
2148
2149    fn chord(s: &str) -> Chord {
2150        Chord::parse(s).unwrap_or_else(|e| panic!("{s}: {e}"))
2151    }
2152
2153    fn actions<A: Clone>(dispatches: &[Dispatch<A>]) -> Vec<A> {
2154        dispatches
2155            .iter()
2156            .filter_map(|d| match d {
2157                Dispatch::Action { action, .. } => Some(action.clone()),
2158                _ => None,
2159            })
2160            .collect()
2161    }
2162
2163    #[test]
2164    fn combo_parse_display_and_normalization() {
2165        let ctrl_x: KeyCombo = "Ctrl+x".parse().unwrap();
2166        assert_eq!(ctrl_x, KeyCombo::new(KeyCode::Char('x'), Modifiers::CTRL));
2167        assert_eq!(ctrl_x.to_string(), "Ctrl+x");
2168
2169        // Shift+a, A and a terminal reporting Char('A')+SHIFT are one combo.
2170        let shift_a: KeyCombo = "shift+a".parse().unwrap();
2171        assert_eq!(shift_a, "A".parse().unwrap());
2172        assert_eq!(shift_a, KeyCombo::new(KeyCode::Char('A'), Modifiers::SHIFT));
2173        assert_eq!(shift_a.to_string(), "A");
2174
2175        assert_eq!("F12".parse::<KeyCombo>().unwrap().code, KeyCode::F(12));
2176        assert_eq!(
2177            "Space".parse::<KeyCombo>().unwrap().code,
2178            KeyCode::Char(' ')
2179        );
2180        assert_eq!(
2181            "Ctrl+Alt+Delete".parse::<KeyCombo>().unwrap().to_string(),
2182            "Ctrl+Alt+Delete"
2183        );
2184        assert_eq!(
2185            "Shift+Tab".parse::<KeyCombo>().unwrap().to_string(),
2186            "Shift+Tab"
2187        );
2188        // The plus key itself.
2189        assert_eq!("+".parse::<KeyCombo>().unwrap().code, KeyCode::Char('+'));
2190        let ctrl_plus: KeyCombo = "Ctrl++".parse().unwrap();
2191        assert_eq!(
2192            ctrl_plus,
2193            KeyCombo::new(KeyCode::Char('+'), Modifiers::CTRL)
2194        );
2195
2196        assert_eq!(
2197            "Hyper+x".parse::<KeyCombo>(),
2198            Err(KeyParseError::UnknownModifier("Hyper".into()))
2199        );
2200        assert_eq!(
2201            "Banana".parse::<KeyCombo>(),
2202            Err(KeyParseError::UnknownKey("Banana".into()))
2203        );
2204        assert_eq!("".parse::<KeyCombo>(), Err(KeyParseError::EmptyKey));
2205        assert_eq!(
2206            "F0".parse::<KeyCombo>(),
2207            Err(KeyParseError::UnknownKey("F0".into()))
2208        );
2209    }
2210
2211    #[test]
2212    fn chord_parse_prefix_and_limits() {
2213        let gg = chord("g g");
2214        let g = chord("g");
2215        assert_eq!(gg.len(), 2);
2216        assert_eq!(gg.to_string(), "g g");
2217        assert!(g.is_prefix_of(&gg));
2218        assert!(!gg.is_prefix_of(&g));
2219        assert!(!g.is_prefix_of(&g), "a chord is not its own prefix");
2220        assert_eq!(chord("Ctrl+x Ctrl+s").to_string(), "Ctrl+x Ctrl+s");
2221        assert_eq!(Chord::parse(""), Err(KeyParseError::EmptyKey));
2222        assert_eq!(
2223            Chord::parse("a b c d e"),
2224            Err(KeyParseError::TooManyKeys(5))
2225        );
2226    }
2227
2228    #[test]
2229    fn chord_completes_within_timeout() {
2230        let mut map = KeyMap::new();
2231        map.bind(chord("g g"), Act::GoTop);
2232        map.bind(chord("x"), Act::Save);
2233        let mut dispatcher = KeyDispatcher::new(map);
2234        let t0 = Instant::now();
2235
2236        let first = dispatcher.feed(&press('g'), t0);
2237        assert_eq!(first, vec![Dispatch::Pending { prefix: chord("g") }]);
2238        assert_eq!(dispatcher.pending_prefix(), Some(chord("g")));
2239        assert!(
2240            dispatcher.tick(t0 + ms(300)).is_empty(),
2241            "still inside the timeout"
2242        );
2243
2244        let second = dispatcher.feed(&press('g'), t0 + ms(300));
2245        assert_eq!(actions(&second), vec![Act::GoTop]);
2246        assert_eq!(dispatcher.pending_prefix(), None);
2247        assert_eq!(dispatcher.stats().dispatched, 1);
2248        assert_eq!(dispatcher.stats().pending, 1);
2249    }
2250
2251    #[test]
2252    fn chord_expires_after_timeout() {
2253        // Prefix that is itself bound: expiry fires it.
2254        let mut map = KeyMap::new();
2255        map.bind(chord("g g"), Act::GoTop);
2256        map.bind(chord("g"), Act::Help);
2257        let mut dispatcher = KeyDispatcher::new(map);
2258        let t0 = Instant::now();
2259        assert_eq!(
2260            dispatcher.feed(&press('g'), t0),
2261            vec![Dispatch::Pending { prefix: chord("g") }]
2262        );
2263        assert!(dispatcher.tick(t0 + ms(999)).is_empty());
2264        let expired = dispatcher.tick(t0 + ms(1000));
2265        assert_eq!(expired[0], Dispatch::Expired { prefix: chord("g") });
2266        assert_eq!(actions(&expired), vec![Act::Help]);
2267        assert_eq!(dispatcher.stats().expired, 1);
2268
2269        // Prefix that is not bound: expiry only.
2270        let mut map = KeyMap::new();
2271        map.bind(chord("g g"), Act::GoTop);
2272        let mut dispatcher = KeyDispatcher::new(map);
2273        dispatcher.feed(&press('g'), t0);
2274        assert_eq!(
2275            dispatcher.tick(t0 + ms(5000)),
2276            vec![Dispatch::Expired { prefix: chord("g") }]
2277        );
2278        assert_eq!(dispatcher.pending_prefix(), None);
2279    }
2280
2281    #[test]
2282    fn single_key_fires_while_chord_pending() {
2283        let mut map = KeyMap::new();
2284        map.bind(chord("g g"), Act::GoTop);
2285        map.bind(chord("x"), Act::Save);
2286        let mut dispatcher = KeyDispatcher::new(map);
2287        let t0 = Instant::now();
2288        dispatcher.feed(&press('g'), t0);
2289        let out = dispatcher.feed(&press('x'), t0 + ms(10));
2290        assert_eq!(out[0], Dispatch::Expired { prefix: chord("g") });
2291        assert_eq!(
2292            actions(&out),
2293            vec![Act::Save],
2294            "x is never blocked by the pending g"
2295        );
2296
2297        // Same with a bound prefix: it fires first, then the single key.
2298        let mut map = KeyMap::new();
2299        map.bind(chord("g g"), Act::GoTop);
2300        map.bind(chord("g"), Act::Help);
2301        map.bind(chord("x"), Act::Save);
2302        let mut dispatcher = KeyDispatcher::new(map);
2303        dispatcher.feed(&press('g'), t0);
2304        let out = dispatcher.feed(&press('x'), t0 + ms(10));
2305        assert_eq!(actions(&out), vec![Act::Help, Act::Save]);
2306
2307        // A non-extending key that starts another chord goes pending itself.
2308        let mut map = KeyMap::new();
2309        map.bind(chord("g g"), Act::GoTop);
2310        map.bind(chord("z z"), Act::Quit);
2311        let mut dispatcher = KeyDispatcher::new(map);
2312        dispatcher.feed(&press('g'), t0);
2313        let out = dispatcher.feed(&press('z'), t0 + ms(10));
2314        assert_eq!(
2315            out,
2316            vec![
2317                Dispatch::Expired { prefix: chord("g") },
2318                Dispatch::Pending { prefix: chord("z") }
2319            ]
2320        );
2321    }
2322
2323    #[test]
2324    fn prefix_with_own_binding_fires_on_flush() {
2325        // `g` is bound both on its own and as the prefix of `g g`. A following
2326        // key that cannot extend the prefix flushes it. Because this is not a
2327        // timeout, the prefix's own binding fires with no `Expired`, and the
2328        // non-extending key is then reported unbound.
2329        let mut map = KeyMap::new();
2330        let one = map.bind(chord("g"), Act::Help);
2331        map.bind(chord("g g"), Act::GoTop);
2332        let mut dispatcher = KeyDispatcher::new(map);
2333        let t0 = Instant::now();
2334
2335        assert_eq!(
2336            dispatcher.feed(&press('g'), t0),
2337            vec![Dispatch::Pending { prefix: chord("g") }]
2338        );
2339        let out = dispatcher.feed(&press('x'), t0 + ms(10));
2340        assert_eq!(
2341            out,
2342            vec![
2343                Dispatch::Action {
2344                    action: Act::Help,
2345                    binding: one,
2346                    chord: chord("g"),
2347                },
2348                Dispatch::Unbound(press('x')),
2349            ]
2350        );
2351        assert_eq!(dispatcher.pending_prefix(), None);
2352        assert_eq!(
2353            dispatcher.stats().expired,
2354            0,
2355            "a bound prefix flushed by a non-extending key does not expire"
2356        );
2357        assert_eq!(dispatcher.stats().dispatched, 1);
2358    }
2359
2360    #[test]
2361    fn widget_beats_mode_beats_global() {
2362        let mut map = KeyMap::new();
2363        let g = map.bind_in(chord("s"), Act::Global, Priority::Global, None);
2364        let m = map.bind_in(chord("s"), Act::Mode, Priority::Mode, None);
2365        let w = map.bind_in(chord("s"), Act::Widget, Priority::Widget, None);
2366        let lookup = map.lookup(&chord("s"), &[]);
2367        assert_eq!(lookup.exact.map(|b| b.id), Some(w));
2368        assert_eq!(lookup.longer, 0);
2369
2370        let mut dispatcher = KeyDispatcher::new(map);
2371        assert_eq!(
2372            actions(&dispatcher.feed(&press('s'), Instant::now())),
2373            vec![Act::Widget]
2374        );
2375
2376        let report = dispatcher.map().conflicts();
2377        assert_eq!(report.len(), 3, "{report}");
2378        assert!(report.items.contains(&Conflict::Shadowed {
2379            winner: w,
2380            loser: g,
2381            chord: chord("s")
2382        }));
2383        assert!(report.items.contains(&Conflict::Shadowed {
2384            winner: m,
2385            loser: g,
2386            chord: chord("s")
2387        }));
2388        assert!(report.items.contains(&Conflict::Shadowed {
2389            winner: w,
2390            loser: m,
2391            chord: chord("s")
2392        }));
2393        assert_eq!(report.to_string().lines().count(), 3);
2394
2395        // Removing the winner promotes the next.
2396        dispatcher.map_mut().unbind(w);
2397        assert_eq!(
2398            actions(&dispatcher.feed(&press('s'), Instant::now())),
2399            vec![Act::Mode]
2400        );
2401    }
2402
2403    #[test]
2404    fn active_context_beats_contextless_even_at_lower_priority() {
2405        let mut map = KeyMap::new();
2406        let text_input = map.context("text_input");
2407        assert_eq!(map.context("text_input"), text_input, "interned once");
2408        assert_eq!(map.context_name(text_input), Some("text_input"));
2409        map.bind_in(chord("Enter"), Act::Submit, Priority::Widget, None);
2410        map.bind_in(
2411            chord("Enter"),
2412            Act::Newline,
2413            Priority::Global,
2414            Some(text_input),
2415        );
2416        assert!(
2417            map.conflicts().is_empty(),
2418            "a context override is not a conflict"
2419        );
2420
2421        let mut dispatcher = KeyDispatcher::new(map);
2422        let enter = KeyEvent::new(KeyCode::Enter);
2423        let t0 = Instant::now();
2424        assert_eq!(actions(&dispatcher.feed(&enter, t0)), vec![Act::Submit]);
2425        dispatcher.set_active_contexts(&[text_input]);
2426        assert_eq!(actions(&dispatcher.feed(&enter, t0)), vec![Act::Newline]);
2427        dispatcher.set_active_contexts(&[]);
2428        assert_eq!(actions(&dispatcher.feed(&enter, t0)), vec![Act::Submit]);
2429    }
2430
2431    #[test]
2432    fn conflicts_reports_shadowed_prefix_and_duplicate() {
2433        let mut map = KeyMap::new();
2434        let long = map.bind(chord("g g"), Act::GoTop);
2435        let short = map.bind(chord("g"), Act::Help);
2436        let q1 = map.bind(chord("q"), Act::Quit);
2437        let q2 = map.bind(chord("q"), Act::Quit);
2438        map.set_label(q2, "quit");
2439        assert_eq!(map.get(q2).and_then(|b| b.label.as_deref()), Some("quit"));
2440
2441        let report = map.conflicts();
2442        assert_eq!(report.len(), 2, "{report}");
2443        assert_eq!(
2444            report.items[0],
2445            Conflict::PrefixCollision {
2446                short,
2447                long,
2448                short_chord: chord("g"),
2449                long_chord: chord("g g"),
2450            }
2451        );
2452        assert_eq!(
2453            report.items[1],
2454            Conflict::Duplicate {
2455                first: q1,
2456                second: q2,
2457                chord: chord("q")
2458            }
2459        );
2460        let text = report.to_string();
2461        assert_eq!(text.lines().count(), 2);
2462        assert!(
2463            text.contains("warning: binding #1 (`g`) is a prefix of binding #0 (`g g`)"),
2464            "{text}"
2465        );
2466        assert!(text.contains("the later one wins"), "{text}");
2467
2468        // The later duplicate wins at dispatch.
2469        assert_eq!(map.lookup(&chord("q"), &[]).exact.map(|b| b.id), Some(q2));
2470    }
2471
2472    #[test]
2473    fn repeat_refires_single_key_binding_but_never_extends_a_chord() {
2474        let mut map = KeyMap::new();
2475        map.bind(chord("j"), Act::Down);
2476        map.bind(chord("g g"), Act::GoTop);
2477        let mut dispatcher = KeyDispatcher::new(map);
2478        let t0 = Instant::now();
2479
2480        let held = kind(press('j'), KeyEventKind::Repeat);
2481        assert_eq!(actions(&dispatcher.feed(&held, t0)), vec![Act::Down]);
2482
2483        dispatcher.feed(&press('g'), t0);
2484        let repeat_g = kind(press('g'), KeyEventKind::Repeat);
2485        assert_eq!(
2486            dispatcher.feed(&repeat_g, t0 + ms(10)),
2487            vec![Dispatch::Unbound(repeat_g)]
2488        );
2489        assert_eq!(
2490            dispatcher.pending_prefix(),
2491            Some(chord("g")),
2492            "repeat left the prefix alone"
2493        );
2494
2495        let released = kind(press('j'), KeyEventKind::Release);
2496        assert_eq!(
2497            dispatcher.feed(&released, t0 + ms(20)),
2498            vec![Dispatch::Unbound(released)]
2499        );
2500    }
2501
2502    #[test]
2503    fn esc_goes_through_the_sequence_detector() {
2504        let mut map = KeyMap::new();
2505        map.bind(chord("Esc"), Act::Quit);
2506        map.bind(chord("Esc Esc"), Act::Help);
2507        map.bind(chord("g g"), Act::GoTop);
2508        let mut dispatcher = KeyDispatcher::new(map);
2509        let esc = KeyEvent::new(KeyCode::Escape);
2510        let t0 = Instant::now();
2511
2512        // A lone Esc waits for the detector window, then fires its binding.
2513        assert_eq!(
2514            dispatcher.feed(&esc, t0),
2515            vec![Dispatch::Esc(SequenceOutput::Pending)]
2516        );
2517        assert_eq!(actions(&dispatcher.tick(t0 + ms(300))), vec![Act::Quit]);
2518
2519        // Esc Esc inside the window fires the double binding.
2520        let t1 = t0 + ms(1000);
2521        dispatcher.feed(&esc, t1);
2522        assert_eq!(
2523            actions(&dispatcher.feed(&esc, t1 + ms(100))),
2524            vec![Act::Help]
2525        );
2526
2527        // Esc cancels a pending chord prefix first.
2528        let t2 = t0 + ms(3000);
2529        dispatcher.feed(&press('g'), t2);
2530        let out = dispatcher.feed(&esc, t2 + ms(10));
2531        assert_eq!(out[0], Dispatch::Expired { prefix: chord("g") });
2532        assert_eq!(dispatcher.pending_prefix(), None);
2533
2534        // Unbound Esc surfaces the detector verdict.
2535        let mut plain = KeyDispatcher::new(KeyMap::<Act>::new());
2536        plain.feed(&esc, t0);
2537        assert_eq!(
2538            plain.tick(t0 + ms(300)),
2539            vec![Dispatch::Esc(SequenceOutput::Esc)]
2540        );
2541    }
2542
2543    /// A map round-trips through TOML and JSON with chords as text, contexts
2544    /// by name and the timeout in milliseconds; a bad hand-written chord is
2545    /// reported with its binding index.
2546    #[cfg(feature = "serde")]
2547    #[test]
2548    fn keymap_round_trips_through_toml_and_json() {
2549        #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2550        enum Action {
2551            Quit,
2552            Save,
2553            Newline,
2554        }
2555
2556        let mut map = KeyMap::with_config(KeyMapConfig::default().with_chord_timeout(ms(750)));
2557        let editor = map.context("editor");
2558        let quit = map.bind(chord("q"), Action::Quit);
2559        map.set_label(quit, "quit");
2560        map.bind_in(chord("Ctrl+x Ctrl+s"), Action::Save, Priority::Mode, None);
2561        map.bind_in(
2562            chord("Enter"),
2563            Action::Newline,
2564            Priority::Widget,
2565            Some(editor),
2566        );
2567
2568        let text = toml::to_string(&map).expect("serialize to TOML");
2569        assert!(text.contains("chord_timeout_ms = 750"), "{text}");
2570        assert!(text.contains("chord = \"Ctrl+x Ctrl+s\""), "{text}");
2571        assert!(text.contains("context = \"editor\""), "{text}");
2572        assert!(text.contains("label = \"quit\""), "{text}");
2573
2574        let back: KeyMap<Action> = toml::from_str(&text).expect("parse TOML");
2575        assert_eq!(back.config().chord_timeout, ms(750));
2576        assert_eq!(back.len(), 3);
2577        assert_eq!(back.bindings()[0].label.as_deref(), Some("quit"));
2578        assert_eq!(back.bindings()[1].priority, Priority::Mode);
2579        assert_eq!(back.bindings()[1].chord, chord("Ctrl+x Ctrl+s"));
2580        let editor_back = back.bindings()[2].context.expect("context restored");
2581        assert_eq!(back.context_name(editor_back), Some("editor"));
2582        assert_eq!(
2583            back.lookup(&chord("Enter"), &[editor_back])
2584                .exact
2585                .map(|b| &b.action),
2586            Some(&Action::Newline)
2587        );
2588        assert!(
2589            back.lookup(&chord("Enter"), &[]).is_none(),
2590            "the context binding stays inactive outside its context"
2591        );
2592
2593        let json = serde_json::to_string(&map).expect("serialize to JSON");
2594        let back_json: KeyMap<Action> = serde_json::from_str(&json).expect("parse JSON");
2595        assert_eq!(back_json.len(), 3);
2596        assert_eq!(back_json.bindings()[2].action, Action::Newline);
2597
2598        let bad =
2599            "chord_timeout_ms = 500\n\n[[bindings]]\nchord = \"Hyper+q\"\naction = \"Quit\"\n";
2600        let err = toml::from_str::<KeyMap<Action>>(bad)
2601            .expect_err("bad chord must fail")
2602            .to_string();
2603        assert!(err.contains("binding 0") && err.contains("Hyper"), "{err}");
2604
2605        let minimal: KeyMap<Action> =
2606            toml::from_str("[[bindings]]\nchord = \"q\"\naction = \"Quit\"\n")
2607                .expect("defaults fill in");
2608        assert_eq!(minimal.config().chord_timeout, ms(DEFAULT_CHORD_TIMEOUT_MS));
2609        assert_eq!(minimal.bindings()[0].priority, Priority::Global);
2610    }
2611
2612    /// Unknown keys in a keymap file are rejected (the typo names itself), and
2613    /// a bad chord names its binding index.
2614    #[cfg(feature = "serde")]
2615    #[test]
2616    fn toml_rejects_unknown_field_and_bad_chord() {
2617        #[derive(Debug, Clone, serde::Deserialize)]
2618        enum Action {
2619            Quit,
2620        }
2621
2622        let unknown_top =
2623            toml::from_str::<KeyMap<Action>>("chord_timeout_ms = 500\ntypo_field = 3\n")
2624                .expect_err("an unknown top-level field must be rejected")
2625                .to_string();
2626        assert!(unknown_top.contains("typo_field"), "{unknown_top}");
2627
2628        let unknown_binding = toml::from_str::<KeyMap<Action>>(
2629            "[[bindings]]\nchord = \"q\"\naction = \"Quit\"\nchrod = \"x\"\n",
2630        )
2631        .expect_err("an unknown binding field must be rejected")
2632        .to_string();
2633        assert!(unknown_binding.contains("chrod"), "{unknown_binding}");
2634
2635        let bad_chord = toml::from_str::<KeyMap<Action>>(
2636            "[[bindings]]\nchord = \"Nope+q\"\naction = \"Quit\"\n",
2637        )
2638        .expect_err("a bad chord must be rejected")
2639        .to_string();
2640        assert!(
2641            bad_chord.contains("binding 0") && bad_chord.contains("Nope"),
2642            "{bad_chord}"
2643        );
2644    }
2645
2646    /// The keymap example embedded in the keybinding policy doc (and shipped as
2647    /// a fixture) parses into exactly the map it describes, so the docs and the
2648    /// parser cannot silently drift apart.
2649    #[cfg(feature = "serde")]
2650    #[test]
2651    fn toml_example_in_docs_parses() {
2652        #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
2653        enum Action {
2654            Save,
2655            Newline,
2656            Top,
2657        }
2658
2659        const EXAMPLE: &str = include_str!("../tests/fixtures/keymap_example.toml");
2660        let map: KeyMap<Action> =
2661            toml::from_str(EXAMPLE).expect("documented keymap example must parse");
2662
2663        assert_eq!(map.config().chord_timeout, ms(750));
2664        assert_eq!(map.len(), 3);
2665
2666        let save = &map.bindings()[0];
2667        assert_eq!(save.action, Action::Save);
2668        assert_eq!(save.chord, chord("Ctrl+x Ctrl+s"));
2669        assert_eq!(save.priority, Priority::Mode);
2670        assert_eq!(save.label.as_deref(), Some("save"));
2671
2672        let newline = &map.bindings()[1];
2673        assert_eq!(newline.action, Action::Newline);
2674        assert_eq!(newline.priority, Priority::Widget);
2675        let editor = newline.context.expect("editor context restored");
2676        assert_eq!(map.context_name(editor), Some("editor"));
2677        assert_eq!(
2678            map.lookup(&chord("Enter"), &[editor])
2679                .exact
2680                .map(|binding| &binding.action),
2681            Some(&Action::Newline)
2682        );
2683        assert!(
2684            map.lookup(&chord("Enter"), &[]).is_none(),
2685            "the editor binding stays inactive outside its context"
2686        );
2687
2688        assert_eq!(map.bindings()[2].chord, chord("g g"));
2689        assert_eq!(map.bindings()[2].action, Action::Top);
2690        let report = map.conflicts();
2691        assert!(report.is_empty(), "{report}");
2692    }
2693
2694    fn arb_code() -> impl Strategy<Value = KeyCode> {
2695        prop_oneof![
2696            prop::sample::select(vec![
2697                'a', 'b', 'q', 'x', 'z', 'A', 'Q', '1', '9', '+', '-', '.', '/', ' ',
2698            ])
2699            .prop_map(KeyCode::Char),
2700            (1u8..=24).prop_map(KeyCode::F),
2701            prop::sample::select(vec![
2702                KeyCode::Enter,
2703                KeyCode::Escape,
2704                KeyCode::Backspace,
2705                KeyCode::Tab,
2706                KeyCode::BackTab,
2707                KeyCode::Delete,
2708                KeyCode::Insert,
2709                KeyCode::Home,
2710                KeyCode::End,
2711                KeyCode::PageUp,
2712                KeyCode::PageDown,
2713                KeyCode::Up,
2714                KeyCode::Down,
2715                KeyCode::Left,
2716                KeyCode::Right,
2717                KeyCode::Null,
2718                KeyCode::MediaPlayPause,
2719                KeyCode::MediaStop,
2720                KeyCode::MediaNextTrack,
2721                KeyCode::MediaPrevTrack,
2722            ]),
2723        ]
2724    }
2725
2726    fn arb_mods() -> impl Strategy<Value = Modifiers> {
2727        (0u8..16).prop_map(|bits| {
2728            let mut modifiers = Modifiers::NONE;
2729            if bits & 0b0001 != 0 {
2730                modifiers |= Modifiers::CTRL;
2731            }
2732            if bits & 0b0010 != 0 {
2733                modifiers |= Modifiers::ALT;
2734            }
2735            if bits & 0b0100 != 0 {
2736                modifiers |= Modifiers::SHIFT;
2737            }
2738            if bits & 0b1000 != 0 {
2739                modifiers |= Modifiers::SUPER;
2740            }
2741            modifiers
2742        })
2743    }
2744
2745    fn arb_small_chord() -> impl Strategy<Value = Chord> {
2746        prop::collection::vec(
2747            prop::sample::select(vec!['a', 'b', 'c', 'd'])
2748                .prop_map(|c| KeyCombo::key(KeyCode::Char(c))),
2749            1..=3usize,
2750        )
2751        .prop_map(|combos| Chord::new(combos).expect("1..=3 combos is a valid chord"))
2752    }
2753
2754    fn arb_key() -> impl Strategy<Value = KeyEvent> {
2755        let code = prop_oneof![
2756            Just(KeyCode::Char('a')),
2757            Just(KeyCode::Char('b')),
2758            Just(KeyCode::Char('c')),
2759            Just(KeyCode::Enter),
2760            Just(KeyCode::Escape),
2761        ];
2762        let kind = prop_oneof![
2763            Just(KeyEventKind::Press),
2764            Just(KeyEventKind::Repeat),
2765            Just(KeyEventKind::Release),
2766        ];
2767        (code, kind).prop_map(|(code, kind)| KeyEvent {
2768            code,
2769            modifiers: Modifiers::NONE,
2770            kind,
2771        })
2772    }
2773
2774    proptest! {
2775        #![proptest_config(ProptestConfig::with_cases(1000))]
2776
2777        /// No key is ever swallowed: every feed yields at least one dispatch,
2778        /// and draining the timers afterwards never panics or leaks a prefix.
2779        #[test]
2780        fn every_fed_key_yields_a_dispatch(
2781            keys in proptest::collection::vec(arb_key(), 1..20),
2782            gaps in proptest::collection::vec(0u64..1500, 1..20),
2783        ) {
2784            let mut map = KeyMap::new();
2785            map.bind(chord("a b"), Act::GoTop);
2786            map.bind(chord("a"), Act::Help);
2787            map.bind(chord("c c c"), Act::Save);
2788            map.bind(chord("Enter"), Act::Submit);
2789            let mut dispatcher = KeyDispatcher::new(map);
2790            let mut now = Instant::now();
2791            for (key, gap) in keys.iter().zip(gaps.iter().cycle()) {
2792                now += ms(*gap);
2793                let out = dispatcher.feed(key, now);
2794                prop_assert!(!out.is_empty(), "{key:?} produced nothing");
2795                let _ = dispatcher.tick(now);
2796            }
2797            let _ = dispatcher.tick(now + ms(10_000));
2798            prop_assert_eq!(dispatcher.pending_prefix(), None);
2799            let stats = dispatcher.stats();
2800            prop_assert!(
2801                stats.dispatched + stats.pending + stats.expired + stats.unbound + stats.esc > 0
2802            );
2803        }
2804
2805        /// `Display` and `FromStr` are inverse over the normalized combo
2806        /// domain: parsing a combo's own text yields the same combo back.
2807        #[test]
2808        fn combo_display_parse_round_trip(code in arb_code(), mods in arb_mods()) {
2809            let combo = KeyCombo::new(code, mods);
2810            let text = combo.to_string();
2811            let parsed: KeyCombo = text
2812                .parse()
2813                .unwrap_or_else(|e| panic!("`{text}` did not re-parse: {e}"));
2814            prop_assert_eq!(parsed, combo, "text = `{}`", text);
2815        }
2816
2817        /// An unambiguous lookup never depends on the order bindings were
2818        /// inserted; only reported conflicts may change a winner.
2819        #[test]
2820        fn lookup_is_deterministic_under_shuffle(
2821            raw in prop::collection::vec((arb_small_chord(), any::<u64>()), 1..12),
2822            queries in prop::collection::vec(arb_small_chord(), 1..8),
2823        ) {
2824            // Keep only the first occurrence of each chord: a duplicate is a
2825            // reported conflict, not something lookup must resolve by order.
2826            let mut seen = std::collections::BTreeSet::new();
2827            let mut entries: Vec<(Chord, u64)> = Vec::new();
2828            for (ch, key) in raw {
2829                if seen.insert(ch.to_string()) {
2830                    entries.push((ch, key));
2831                }
2832            }
2833            let build = |order: &[(Chord, u64)]| {
2834                let mut map = KeyMap::new();
2835                for (ch, _) in order {
2836                    map.bind(ch.clone(), ch.to_string());
2837                }
2838                map
2839            };
2840            let in_order = build(&entries);
2841            let mut shuffled = entries.clone();
2842            shuffled.sort_by_key(|(_, key)| *key);
2843            let reordered = build(&shuffled);
2844            for query in &queries {
2845                let a = in_order.lookup(query, &[]);
2846                let b = reordered.lookup(query, &[]);
2847                prop_assert_eq!(
2848                    a.exact.map(|binding| binding.action.clone()),
2849                    b.exact.map(|binding| binding.action.clone()),
2850                    "winner changed for `{}`",
2851                    query
2852                );
2853                prop_assert_eq!(a.longer, b.longer, "longer count changed for `{}`", query);
2854            }
2855        }
2856    }
2857}
2858
2859#[cfg(test)]
2860mod tests {
2861    use super::*;
2862
2863    fn now() -> Instant {
2864        Instant::now()
2865    }
2866
2867    fn esc_press() -> KeyEvent {
2868        KeyEvent::new(KeyCode::Escape)
2869    }
2870
2871    fn key_press(code: KeyCode) -> KeyEvent {
2872        KeyEvent::new(code)
2873    }
2874
2875    fn esc_release() -> KeyEvent {
2876        KeyEvent::new(KeyCode::Escape).with_kind(KeyEventKind::Release)
2877    }
2878
2879    const MS_50: Duration = Duration::from_millis(50);
2880    const MS_100: Duration = Duration::from_millis(100);
2881    const MS_200: Duration = Duration::from_millis(200);
2882    const MS_300: Duration = Duration::from_millis(300);
2883
2884    // --- Basic sequence tests ---
2885
2886    #[test]
2887    fn single_esc_returns_pending() {
2888        let mut detector = SequenceDetector::with_defaults();
2889        let t = now();
2890
2891        let output = detector.feed(&esc_press(), t);
2892        assert_eq!(output, SequenceOutput::Pending);
2893        assert!(detector.is_pending());
2894    }
2895
2896    #[test]
2897    fn esc_esc_within_timeout() {
2898        let mut detector = SequenceDetector::with_defaults();
2899        let t = now();
2900
2901        detector.feed(&esc_press(), t);
2902        let output = detector.feed(&esc_press(), t + MS_100);
2903
2904        assert_eq!(output, SequenceOutput::EscEsc);
2905        assert!(!detector.is_pending());
2906    }
2907
2908    #[test]
2909    fn esc_esc_at_timeout_boundary() {
2910        let mut detector = SequenceDetector::with_defaults();
2911        let t = now();
2912
2913        detector.feed(&esc_press(), t);
2914        // Exactly at 250ms boundary
2915        let output = detector.feed(&esc_press(), t + Duration::from_millis(250));
2916
2917        assert_eq!(output, SequenceOutput::EscEsc);
2918    }
2919
2920    #[test]
2921    fn esc_esc_past_timeout() {
2922        let mut detector = SequenceDetector::with_defaults();
2923        let t = now();
2924
2925        detector.feed(&esc_press(), t);
2926        // Past 250ms timeout (251ms)
2927        let output = detector.feed(&esc_press(), t + Duration::from_millis(251));
2928
2929        // First Esc timed out, second Esc starts new sequence
2930        assert_eq!(output, SequenceOutput::Esc);
2931        assert!(detector.is_pending()); // New sequence started
2932    }
2933
2934    #[test]
2935    fn timeout_check_emits_pending_esc() {
2936        let mut detector = SequenceDetector::with_defaults();
2937        let t = now();
2938
2939        detector.feed(&esc_press(), t);
2940
2941        // Before timeout
2942        assert!(detector.check_timeout(t + MS_200).is_none());
2943        assert!(detector.is_pending());
2944
2945        // After timeout (251ms)
2946        let output = detector.check_timeout(t + Duration::from_millis(251));
2947        assert_eq!(output, Some(SequenceOutput::Esc));
2948        assert!(!detector.is_pending());
2949    }
2950
2951    #[test]
2952    fn other_key_interrupts_sequence() {
2953        let mut detector = SequenceDetector::with_defaults();
2954        let t = now();
2955
2956        detector.feed(&esc_press(), t);
2957        let output = detector.feed(&key_press(KeyCode::Char('a')), t + MS_100);
2958
2959        // Pending Esc is emitted
2960        assert_eq!(output, SequenceOutput::Esc);
2961        assert!(!detector.is_pending());
2962    }
2963
2964    #[test]
2965    fn non_esc_key_passes_through() {
2966        let mut detector = SequenceDetector::with_defaults();
2967        let t = now();
2968
2969        let output = detector.feed(&key_press(KeyCode::Char('x')), t);
2970        assert_eq!(output, SequenceOutput::PassThrough);
2971    }
2972
2973    #[test]
2974    fn release_event_passes_through() {
2975        let mut detector = SequenceDetector::with_defaults();
2976        let t = now();
2977
2978        let output = detector.feed(&esc_release(), t);
2979        assert_eq!(output, SequenceOutput::PassThrough);
2980        assert!(!detector.is_pending());
2981    }
2982
2983    #[test]
2984    fn release_during_pending_passes_through() {
2985        let mut detector = SequenceDetector::with_defaults();
2986        let t = now();
2987
2988        detector.feed(&esc_press(), t);
2989        let output = detector.feed(&esc_release(), t + MS_50);
2990
2991        // Release is ignored; still pending
2992        assert_eq!(output, SequenceOutput::PassThrough);
2993        assert!(detector.is_pending());
2994    }
2995
2996    // --- Config tests ---
2997
2998    #[test]
2999    fn custom_timeout() {
3000        let config = SequenceConfig::default().with_timeout(Duration::from_millis(100));
3001        let mut detector = SequenceDetector::new(config);
3002        let t = now();
3003
3004        detector.feed(&esc_press(), t);
3005        // 150ms is past 100ms timeout
3006        let output = detector.feed(&esc_press(), t + Duration::from_millis(150));
3007
3008        assert_eq!(output, SequenceOutput::Esc);
3009    }
3010
3011    #[test]
3012    fn disabled_sequences() {
3013        let config = SequenceConfig::default().disable_sequences();
3014        let mut detector = SequenceDetector::new(config);
3015        let t = now();
3016
3017        // First Esc immediately emits Esc
3018        let output = detector.feed(&esc_press(), t);
3019        assert_eq!(output, SequenceOutput::Esc);
3020        assert!(!detector.is_pending());
3021
3022        // Second Esc also immediately emits Esc
3023        let output = detector.feed(&esc_press(), t + MS_50);
3024        assert_eq!(output, SequenceOutput::Esc);
3025    }
3026
3027    #[test]
3028    fn disabled_sequences_passthrough() {
3029        let config = SequenceConfig::default().disable_sequences();
3030        let mut detector = SequenceDetector::new(config);
3031        let t = now();
3032
3033        let output = detector.feed(&key_press(KeyCode::Char('a')), t);
3034        assert_eq!(output, SequenceOutput::PassThrough);
3035    }
3036
3037    #[test]
3038    fn config_default_values() {
3039        let config = SequenceConfig::default();
3040        assert_eq!(config.esc_seq_timeout, Duration::from_millis(250));
3041        assert_eq!(config.esc_debounce, Duration::from_millis(50));
3042        assert!(!config.disable_sequences);
3043    }
3044
3045    #[test]
3046    fn config_builder_chain() {
3047        let config = SequenceConfig::default()
3048            .with_timeout(Duration::from_millis(300))
3049            .with_debounce(Duration::from_millis(100))
3050            .disable_sequences();
3051
3052        assert_eq!(config.esc_seq_timeout, Duration::from_millis(300));
3053        assert_eq!(config.esc_debounce, Duration::from_millis(100));
3054        assert!(config.disable_sequences);
3055    }
3056
3057    // --- Reset tests ---
3058
3059    #[test]
3060    fn reset_clears_pending() {
3061        let mut detector = SequenceDetector::with_defaults();
3062        let t = now();
3063
3064        detector.feed(&esc_press(), t);
3065        assert!(detector.is_pending());
3066
3067        detector.reset();
3068        assert!(!detector.is_pending());
3069
3070        // After reset, new Esc starts fresh
3071        let output = detector.feed(&esc_press(), t + MS_100);
3072        assert_eq!(output, SequenceOutput::Pending);
3073    }
3074
3075    #[test]
3076    fn reset_discards_pending_esc() {
3077        let mut detector = SequenceDetector::with_defaults();
3078        let t = now();
3079
3080        detector.feed(&esc_press(), t);
3081        detector.reset();
3082
3083        // Timeout check should not emit anything
3084        assert!(detector.check_timeout(t + MS_300).is_none());
3085    }
3086
3087    // --- Edge cases ---
3088
3089    #[test]
3090    fn rapid_triple_esc() {
3091        let mut detector = SequenceDetector::with_defaults();
3092        let t = now();
3093
3094        // First Esc
3095        let out1 = detector.feed(&esc_press(), t);
3096        assert_eq!(out1, SequenceOutput::Pending);
3097
3098        // Second Esc -> EscEsc
3099        let out2 = detector.feed(&esc_press(), t + MS_50);
3100        assert_eq!(out2, SequenceOutput::EscEsc);
3101
3102        // Third Esc -> starts new sequence
3103        let out3 = detector.feed(&esc_press(), t + MS_100);
3104        assert_eq!(out3, SequenceOutput::Pending);
3105    }
3106
3107    #[test]
3108    fn alternating_esc_and_key() {
3109        let mut detector = SequenceDetector::with_defaults();
3110        let t = now();
3111
3112        // Esc -> pending
3113        detector.feed(&esc_press(), t);
3114
3115        // 'a' -> emits Esc
3116        let out1 = detector.feed(&key_press(KeyCode::Char('a')), t + MS_50);
3117        assert_eq!(out1, SequenceOutput::Esc);
3118
3119        // Esc -> pending again
3120        let out2 = detector.feed(&esc_press(), t + MS_100);
3121        assert_eq!(out2, SequenceOutput::Pending);
3122
3123        // 'b' -> emits Esc
3124        let out3 = detector.feed(&key_press(KeyCode::Char('b')), t + MS_200);
3125        assert_eq!(out3, SequenceOutput::Esc);
3126    }
3127
3128    #[test]
3129    fn enter_key_interrupts() {
3130        let mut detector = SequenceDetector::with_defaults();
3131        let t = now();
3132
3133        detector.feed(&esc_press(), t);
3134        let output = detector.feed(&key_press(KeyCode::Enter), t + MS_100);
3135
3136        assert_eq!(output, SequenceOutput::Esc);
3137    }
3138
3139    #[test]
3140    fn function_key_interrupts() {
3141        let mut detector = SequenceDetector::with_defaults();
3142        let t = now();
3143
3144        detector.feed(&esc_press(), t);
3145        let output = detector.feed(&key_press(KeyCode::F(1)), t + MS_100);
3146
3147        assert_eq!(output, SequenceOutput::Esc);
3148    }
3149
3150    #[test]
3151    fn arrow_key_interrupts() {
3152        let mut detector = SequenceDetector::with_defaults();
3153        let t = now();
3154
3155        detector.feed(&esc_press(), t);
3156        let output = detector.feed(&key_press(KeyCode::Up), t + MS_100);
3157
3158        assert_eq!(output, SequenceOutput::Esc);
3159    }
3160
3161    #[test]
3162    fn config_getter_and_setter() {
3163        let mut detector = SequenceDetector::with_defaults();
3164        assert_eq!(
3165            detector.config().esc_seq_timeout,
3166            Duration::from_millis(250)
3167        );
3168
3169        let new_config = SequenceConfig::default().with_timeout(Duration::from_millis(500));
3170        detector.set_config(new_config);
3171
3172        assert_eq!(
3173            detector.config().esc_seq_timeout,
3174            Duration::from_millis(500)
3175        );
3176    }
3177
3178    #[test]
3179    fn set_config_preserves_pending_state() {
3180        let mut detector = SequenceDetector::with_defaults();
3181        let t = now();
3182
3183        detector.feed(&esc_press(), t);
3184        assert!(detector.is_pending());
3185
3186        // Change config while pending
3187        detector.set_config(SequenceConfig::default().with_timeout(Duration::from_millis(500)));
3188
3189        // Still pending
3190        assert!(detector.is_pending());
3191
3192        // New timeout applies
3193        let output = detector.feed(&esc_press(), t + MS_300);
3194        assert_eq!(output, SequenceOutput::EscEsc); // Within new 500ms timeout
3195    }
3196
3197    #[test]
3198    fn debug_format() {
3199        let detector = SequenceDetector::with_defaults();
3200        let dbg = format!("{:?}", detector);
3201        assert!(dbg.contains("SequenceDetector"));
3202    }
3203
3204    #[test]
3205    fn config_debug_format() {
3206        let config = SequenceConfig::default();
3207        let dbg = format!("{:?}", config);
3208        assert!(dbg.contains("SequenceConfig"));
3209    }
3210
3211    #[test]
3212    fn output_debug_and_eq() {
3213        assert_eq!(SequenceOutput::Pending, SequenceOutput::Pending);
3214        assert_eq!(SequenceOutput::Esc, SequenceOutput::Esc);
3215        assert_eq!(SequenceOutput::EscEsc, SequenceOutput::EscEsc);
3216        assert_eq!(SequenceOutput::PassThrough, SequenceOutput::PassThrough);
3217        assert_ne!(SequenceOutput::Esc, SequenceOutput::EscEsc);
3218
3219        let dbg = format!("{:?}", SequenceOutput::EscEsc);
3220        assert!(dbg.contains("EscEsc"));
3221    }
3222
3223    // --- Stress / property-like tests ---
3224
3225    #[test]
3226    fn no_stuck_state() {
3227        let mut detector = SequenceDetector::with_defaults();
3228        let t = now();
3229
3230        // Many operations should always return to Idle eventually
3231        for i in 0..100 {
3232            let offset = Duration::from_millis(i * 10);
3233            if i % 3 == 0 {
3234                detector.feed(&esc_press(), t + offset);
3235            } else {
3236                detector.feed(&key_press(KeyCode::Char('x')), t + offset);
3237            }
3238        }
3239
3240        // Force timeout check - must be well past the last event (990ms) + timeout (250ms)
3241        detector.check_timeout(t + Duration::from_secs(2));
3242
3243        // Should be idle
3244        assert!(!detector.is_pending());
3245    }
3246
3247    #[test]
3248    fn deterministic_output() {
3249        // Same inputs should produce same outputs
3250        let config = SequenceConfig::default();
3251        let t = now();
3252
3253        let mut d1 = SequenceDetector::new(config.clone());
3254        let mut d2 = SequenceDetector::new(config);
3255
3256        let events = [
3257            (esc_press(), t),
3258            (esc_press(), t + MS_100),
3259            (key_press(KeyCode::Char('a')), t + MS_200),
3260            (esc_press(), t + MS_300),
3261        ];
3262
3263        for (event, time) in &events {
3264            let out1 = d1.feed(event, *time);
3265            let out2 = d2.feed(event, *time);
3266            assert_eq!(out1, out2);
3267        }
3268    }
3269
3270    // =========================================================================
3271    // ActionMapper Tests
3272    // =========================================================================
3273
3274    mod action_mapper_tests {
3275        use super::*;
3276        use crate::event::Modifiers;
3277
3278        fn ctrl_c() -> KeyEvent {
3279            KeyEvent::new(KeyCode::Char('c')).with_modifiers(Modifiers::CTRL)
3280        }
3281
3282        fn ctrl_d() -> KeyEvent {
3283            KeyEvent::new(KeyCode::Char('d')).with_modifiers(Modifiers::CTRL)
3284        }
3285
3286        fn ctrl_q() -> KeyEvent {
3287            KeyEvent::new(KeyCode::Char('q')).with_modifiers(Modifiers::CTRL)
3288        }
3289
3290        fn idle_state() -> AppState {
3291            AppState::default()
3292        }
3293
3294        fn input_state() -> AppState {
3295            AppState::new().with_input(true)
3296        }
3297
3298        fn task_state() -> AppState {
3299            AppState::new().with_task(true)
3300        }
3301
3302        fn modal_state() -> AppState {
3303            AppState::new().with_modal(true)
3304        }
3305
3306        fn overlay_state() -> AppState {
3307            AppState::new().with_overlay(true)
3308        }
3309
3310        // --- Ctrl+C tests (policy priorities 2-5) ---
3311
3312        #[test]
3313        fn test_ctrl_c_clears_nonempty_input() {
3314            let mut mapper = ActionMapper::with_defaults();
3315            let t = now();
3316
3317            let action = mapper.map(&ctrl_c(), &input_state(), t);
3318            assert_eq!(action, Some(Action::ClearInput));
3319        }
3320
3321        #[test]
3322        fn test_ctrl_c_cancels_running_task() {
3323            let mut mapper = ActionMapper::with_defaults();
3324            let t = now();
3325
3326            let action = mapper.map(&ctrl_c(), &task_state(), t);
3327            assert_eq!(action, Some(Action::CancelTask));
3328        }
3329
3330        #[test]
3331        fn test_ctrl_c_quits_when_idle() {
3332            let mut mapper = ActionMapper::with_defaults();
3333            let t = now();
3334
3335            let action = mapper.map(&ctrl_c(), &idle_state(), t);
3336            assert_eq!(action, Some(Action::Quit));
3337        }
3338
3339        #[test]
3340        fn test_ctrl_c_dismisses_modal() {
3341            let mut mapper = ActionMapper::with_defaults();
3342            let t = now();
3343
3344            let action = mapper.map(&ctrl_c(), &modal_state(), t);
3345            assert_eq!(action, Some(Action::DismissModal));
3346        }
3347
3348        #[test]
3349        fn test_ctrl_c_modal_priority_over_input() {
3350            let mut mapper = ActionMapper::with_defaults();
3351            let t = now();
3352
3353            // Both modal and input are set
3354            let state = AppState::new().with_modal(true).with_input(true);
3355            let action = mapper.map(&ctrl_c(), &state, t);
3356            assert_eq!(action, Some(Action::DismissModal));
3357        }
3358
3359        #[test]
3360        fn test_ctrl_c_input_priority_over_task() {
3361            let mut mapper = ActionMapper::with_defaults();
3362            let t = now();
3363
3364            let state = AppState::new().with_input(true).with_task(true);
3365            let action = mapper.map(&ctrl_c(), &state, t);
3366            assert_eq!(action, Some(Action::ClearInput));
3367        }
3368
3369        #[test]
3370        fn test_ctrl_c_idle_config_noop() {
3371            let config = ActionConfig::default().with_ctrl_c_idle(CtrlCIdleAction::Noop);
3372            let mut mapper = ActionMapper::new(config);
3373            let t = now();
3374
3375            let action = mapper.map(&ctrl_c(), &idle_state(), t);
3376            assert_eq!(action, None); // Noop returns None
3377        }
3378
3379        #[test]
3380        fn test_ctrl_c_idle_config_bell() {
3381            let config = ActionConfig::default().with_ctrl_c_idle(CtrlCIdleAction::Bell);
3382            let mut mapper = ActionMapper::new(config);
3383            let t = now();
3384
3385            let action = mapper.map(&ctrl_c(), &idle_state(), t);
3386            assert_eq!(action, Some(Action::Bell));
3387        }
3388
3389        // --- Ctrl+D and Ctrl+Q tests (policy priorities 10-11) ---
3390
3391        #[test]
3392        fn test_ctrl_d_soft_quit() {
3393            let mut mapper = ActionMapper::with_defaults();
3394            let t = now();
3395
3396            let action = mapper.map(&ctrl_d(), &idle_state(), t);
3397            assert_eq!(action, Some(Action::SoftQuit));
3398        }
3399
3400        #[test]
3401        fn test_ctrl_d_ignores_state() {
3402            let mut mapper = ActionMapper::with_defaults();
3403            let t = now();
3404
3405            // Ctrl+D always does SoftQuit regardless of state
3406            let action = mapper.map(&ctrl_d(), &modal_state(), t);
3407            assert_eq!(action, Some(Action::SoftQuit));
3408
3409            let action = mapper.map(&ctrl_d(), &input_state(), t);
3410            assert_eq!(action, Some(Action::SoftQuit));
3411        }
3412
3413        #[test]
3414        fn test_ctrl_q_hard_quit() {
3415            let mut mapper = ActionMapper::with_defaults();
3416            let t = now();
3417
3418            let action = mapper.map(&ctrl_q(), &idle_state(), t);
3419            assert_eq!(action, Some(Action::HardQuit));
3420        }
3421
3422        #[test]
3423        fn test_ctrl_q_ignores_state() {
3424            let mut mapper = ActionMapper::with_defaults();
3425            let t = now();
3426
3427            // Ctrl+Q always does HardQuit regardless of state
3428            let action = mapper.map(&ctrl_q(), &modal_state(), t);
3429            assert_eq!(action, Some(Action::HardQuit));
3430        }
3431
3432        // --- Esc tests (policy priorities 1, 6-8) ---
3433
3434        #[test]
3435        fn test_esc_dismisses_modal() {
3436            let mut mapper = ActionMapper::with_defaults();
3437            let t = now();
3438
3439            // First Esc: pending
3440            let action1 = mapper.map(&esc_press(), &modal_state(), t);
3441            assert_eq!(action1, None);
3442
3443            // Timeout: emit Esc action
3444            let action2 = mapper.check_timeout(&modal_state(), t + MS_300);
3445            assert_eq!(action2, Some(Action::DismissModal));
3446        }
3447
3448        #[test]
3449        fn test_esc_clears_input_no_modal() {
3450            let mut mapper = ActionMapper::with_defaults();
3451            let t = now();
3452
3453            mapper.map(&esc_press(), &input_state(), t);
3454            let action = mapper.check_timeout(&input_state(), t + MS_300);
3455            assert_eq!(action, Some(Action::ClearInput));
3456        }
3457
3458        #[test]
3459        fn test_esc_cancels_task_empty_input() {
3460            let mut mapper = ActionMapper::with_defaults();
3461            let t = now();
3462
3463            mapper.map(&esc_press(), &task_state(), t);
3464            let action = mapper.check_timeout(&task_state(), t + MS_300);
3465            assert_eq!(action, Some(Action::CancelTask));
3466        }
3467
3468        #[test]
3469        fn test_esc_closes_overlay() {
3470            let mut mapper = ActionMapper::with_defaults();
3471            let t = now();
3472
3473            mapper.map(&esc_press(), &overlay_state(), t);
3474            let action = mapper.check_timeout(&overlay_state(), t + MS_300);
3475            assert_eq!(action, Some(Action::CloseOverlay));
3476        }
3477
3478        #[test]
3479        fn test_esc_modal_priority_over_overlay() {
3480            let mut mapper = ActionMapper::with_defaults();
3481            let t = now();
3482
3483            let state = AppState::new().with_modal(true).with_overlay(true);
3484            mapper.map(&esc_press(), &state, t);
3485            let action = mapper.check_timeout(&state, t + MS_300);
3486            assert_eq!(action, Some(Action::DismissModal));
3487        }
3488
3489        #[test]
3490        fn test_esc_passthrough_when_idle() {
3491            let mut mapper = ActionMapper::with_defaults();
3492            let t = now();
3493
3494            mapper.map(&esc_press(), &idle_state(), t);
3495            let action = mapper.check_timeout(&idle_state(), t + MS_300);
3496            assert_eq!(action, Some(Action::PassThrough));
3497        }
3498
3499        // --- Esc Esc tests (policy priority 9) ---
3500
3501        #[test]
3502        fn test_esc_esc_within_timeout() {
3503            let mut mapper = ActionMapper::with_defaults();
3504            let t = now();
3505
3506            mapper.map(&esc_press(), &idle_state(), t);
3507            let action = mapper.map(&esc_press(), &idle_state(), t + MS_100);
3508            assert_eq!(action, Some(Action::ToggleTreeView));
3509        }
3510
3511        #[test]
3512        fn test_esc_esc_ignores_state() {
3513            let mut mapper = ActionMapper::with_defaults();
3514            let t = now();
3515
3516            // Esc Esc always toggles tree view regardless of state
3517            mapper.map(&esc_press(), &modal_state(), t);
3518            let action = mapper.map(&esc_press(), &modal_state(), t + MS_100);
3519            assert_eq!(action, Some(Action::ToggleTreeView));
3520        }
3521
3522        #[test]
3523        fn test_esc_esc_timeout_expired() {
3524            let mut mapper = ActionMapper::with_defaults();
3525            let t = now();
3526
3527            mapper.map(&esc_press(), &input_state(), t);
3528            // Past 250ms timeout
3529            let action = mapper.map(&esc_press(), &input_state(), t + MS_300);
3530
3531            // First Esc timed out -> ClearInput, second starts new pending
3532            assert_eq!(action, Some(Action::ClearInput));
3533            assert!(mapper.is_pending_esc());
3534        }
3535
3536        // --- Esc then other key ---
3537
3538        #[test]
3539        fn test_esc_then_other_key() {
3540            let mut mapper = ActionMapper::with_defaults();
3541            let t = now();
3542
3543            mapper.map(&esc_press(), &input_state(), t);
3544            let action = mapper.map(&key_press(KeyCode::Char('a')), &input_state(), t + MS_50);
3545
3546            // Pending Esc is emitted
3547            assert_eq!(action, Some(Action::ClearInput));
3548        }
3549
3550        // --- Other keys passthrough ---
3551
3552        #[test]
3553        fn test_regular_key_passthrough() {
3554            let mut mapper = ActionMapper::with_defaults();
3555            let t = now();
3556
3557            let action = mapper.map(&key_press(KeyCode::Char('x')), &idle_state(), t);
3558            assert_eq!(action, Some(Action::PassThrough));
3559        }
3560
3561        #[test]
3562        fn test_release_event_passthrough() {
3563            let mut mapper = ActionMapper::with_defaults();
3564            let t = now();
3565
3566            let release = KeyEvent::new(KeyCode::Char('x')).with_kind(KeyEventKind::Release);
3567            let action = mapper.map(&release, &idle_state(), t);
3568            assert_eq!(action, Some(Action::PassThrough));
3569        }
3570
3571        // --- State helper tests ---
3572
3573        #[test]
3574        fn test_app_state_builders() {
3575            let state = AppState::new()
3576                .with_input(true)
3577                .with_task(true)
3578                .with_modal(true)
3579                .with_overlay(true);
3580
3581            assert!(state.input_nonempty);
3582            assert!(state.task_running);
3583            assert!(state.modal_open);
3584            assert!(state.view_overlay);
3585            assert!(!state.is_idle());
3586        }
3587
3588        #[test]
3589        fn test_app_state_is_idle() {
3590            assert!(AppState::default().is_idle());
3591            assert!(!AppState::new().with_input(true).is_idle());
3592            assert!(!AppState::new().with_task(true).is_idle());
3593            assert!(!AppState::new().with_modal(true).is_idle());
3594            // view_overlay doesn't affect is_idle
3595            assert!(AppState::new().with_overlay(true).is_idle());
3596        }
3597
3598        // --- Action enum tests ---
3599
3600        #[test]
3601        fn test_action_consumes_event() {
3602            assert!(Action::ClearInput.consumes_event());
3603            assert!(Action::CancelTask.consumes_event());
3604            assert!(Action::Quit.consumes_event());
3605            assert!(!Action::PassThrough.consumes_event());
3606        }
3607
3608        #[test]
3609        fn test_action_is_quit() {
3610            assert!(Action::Quit.is_quit());
3611            assert!(Action::SoftQuit.is_quit());
3612            assert!(Action::HardQuit.is_quit());
3613            assert!(!Action::ClearInput.is_quit());
3614            assert!(!Action::PassThrough.is_quit());
3615        }
3616
3617        // --- Config tests ---
3618
3619        #[test]
3620        fn test_ctrl_c_idle_action_from_str() {
3621            assert_eq!(
3622                CtrlCIdleAction::from_str_opt("quit"),
3623                Some(CtrlCIdleAction::Quit)
3624            );
3625            assert_eq!(
3626                CtrlCIdleAction::from_str_opt("QUIT"),
3627                Some(CtrlCIdleAction::Quit)
3628            );
3629            assert_eq!(
3630                CtrlCIdleAction::from_str_opt("noop"),
3631                Some(CtrlCIdleAction::Noop)
3632            );
3633            assert_eq!(
3634                CtrlCIdleAction::from_str_opt("none"),
3635                Some(CtrlCIdleAction::Noop)
3636            );
3637            assert_eq!(
3638                CtrlCIdleAction::from_str_opt("ignore"),
3639                Some(CtrlCIdleAction::Noop)
3640            );
3641            assert_eq!(
3642                CtrlCIdleAction::from_str_opt("bell"),
3643                Some(CtrlCIdleAction::Bell)
3644            );
3645            assert_eq!(
3646                CtrlCIdleAction::from_str_opt("beep"),
3647                Some(CtrlCIdleAction::Bell)
3648            );
3649            assert_eq!(CtrlCIdleAction::from_str_opt("invalid"), None);
3650        }
3651
3652        #[test]
3653        fn test_ctrl_c_idle_action_to_action() {
3654            assert_eq!(CtrlCIdleAction::Quit.to_action(), Some(Action::Quit));
3655            assert_eq!(CtrlCIdleAction::Noop.to_action(), None);
3656            assert_eq!(CtrlCIdleAction::Bell.to_action(), Some(Action::Bell));
3657        }
3658
3659        #[test]
3660        fn test_action_config_builder() {
3661            let config = ActionConfig::default()
3662                .with_sequence_config(SequenceConfig::default().with_timeout(MS_100))
3663                .with_ctrl_c_idle(CtrlCIdleAction::Bell);
3664
3665            assert_eq!(config.sequence_config.esc_seq_timeout, MS_100);
3666            assert_eq!(config.ctrl_c_idle_action, CtrlCIdleAction::Bell);
3667        }
3668
3669        // --- Reset tests ---
3670
3671        #[test]
3672        fn test_mapper_reset() {
3673            let mut mapper = ActionMapper::with_defaults();
3674            let t = now();
3675
3676            mapper.map(&esc_press(), &idle_state(), t);
3677            assert!(mapper.is_pending_esc());
3678
3679            mapper.reset();
3680            assert!(!mapper.is_pending_esc());
3681        }
3682
3683        // --- Determinism / property tests ---
3684
3685        #[test]
3686        fn test_deterministic_action_mapping() {
3687            let t = now();
3688
3689            let mut m1 = ActionMapper::with_defaults();
3690            let mut m2 = ActionMapper::with_defaults();
3691
3692            let events = [
3693                (ctrl_c(), input_state()),
3694                (ctrl_d(), modal_state()),
3695                (ctrl_q(), idle_state()),
3696            ];
3697
3698            for (event, state) in &events {
3699                let a1 = m1.map(event, state, t);
3700                let a2 = m2.map(event, state, t);
3701                assert_eq!(a1, a2);
3702            }
3703        }
3704
3705        #[test]
3706        fn test_uppercase_ctrl_keys() {
3707            let mut mapper = ActionMapper::with_defaults();
3708            let t = now();
3709
3710            // Ctrl+C with uppercase 'C' should also work
3711            let ctrl_c_upper = KeyEvent::new(KeyCode::Char('C')).with_modifiers(Modifiers::CTRL);
3712            let action = mapper.map(&ctrl_c_upper, &idle_state(), t);
3713            assert_eq!(action, Some(Action::Quit));
3714        }
3715
3716        // --- Validation tests ---
3717
3718        #[test]
3719        fn test_sequence_config_validation_clamps_high_timeout() {
3720            let config = SequenceConfig::default()
3721                .with_timeout(Duration::from_millis(1000)) // Too high
3722                .validated();
3723
3724            // Should clamp to MAX_ESC_SEQ_TIMEOUT_MS (400ms)
3725            assert_eq!(config.esc_seq_timeout.as_millis(), 400);
3726        }
3727
3728        #[test]
3729        fn test_sequence_config_validation_clamps_low_timeout() {
3730            let config = SequenceConfig::default()
3731                .with_timeout(Duration::from_millis(50)) // Too low
3732                .validated();
3733
3734            // Should clamp to MIN_ESC_SEQ_TIMEOUT_MS (150ms)
3735            assert_eq!(config.esc_seq_timeout.as_millis(), 150);
3736        }
3737
3738        #[test]
3739        fn test_sequence_config_validation_clamps_high_debounce() {
3740            let config = SequenceConfig::default()
3741                .with_debounce(Duration::from_millis(200)) // Too high
3742                .validated();
3743
3744            // Should clamp to MAX_ESC_DEBOUNCE_MS (100ms)
3745            assert_eq!(config.esc_debounce.as_millis(), 100);
3746        }
3747
3748        #[test]
3749        fn test_sequence_config_validation_debounce_not_exceeds_timeout() {
3750            let config = SequenceConfig::default()
3751                .with_timeout(Duration::from_millis(150))
3752                .with_debounce(Duration::from_millis(200)) // Higher than timeout
3753                .validated();
3754
3755            // Debounce should be clamped to min(100, 150) = 100,
3756            // but also can't exceed timeout (150)
3757            // Since debounce max is 100 and timeout is 150, debounce = 100
3758            assert!(config.esc_debounce <= config.esc_seq_timeout);
3759        }
3760
3761        #[test]
3762        fn test_sequence_config_is_valid() {
3763            assert!(SequenceConfig::default().is_valid());
3764
3765            // Invalid: timeout too high
3766            let invalid = SequenceConfig::default().with_timeout(Duration::from_millis(500));
3767            assert!(!invalid.is_valid());
3768
3769            // Valid after validation
3770            assert!(invalid.validated().is_valid());
3771        }
3772
3773        #[test]
3774        fn test_sequence_config_constants() {
3775            // Verify constants match spec
3776            assert_eq!(DEFAULT_ESC_SEQ_TIMEOUT_MS, 250);
3777            assert_eq!(MIN_ESC_SEQ_TIMEOUT_MS, 150);
3778            assert_eq!(MAX_ESC_SEQ_TIMEOUT_MS, 400);
3779            assert_eq!(DEFAULT_ESC_DEBOUNCE_MS, 50);
3780            assert_eq!(MIN_ESC_DEBOUNCE_MS, 0);
3781            assert_eq!(MAX_ESC_DEBOUNCE_MS, 100);
3782        }
3783
3784        #[test]
3785        fn test_action_config_validated() {
3786            let config = ActionConfig::default()
3787                .with_sequence_config(
3788                    SequenceConfig::default().with_timeout(Duration::from_millis(1000)),
3789                )
3790                .validated();
3791
3792            // Sequence config should be validated
3793            assert_eq!(config.sequence_config.esc_seq_timeout.as_millis(), 400);
3794        }
3795    }
3796}