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#[cfg(test)]
977mod tests {
978    use super::*;
979
980    fn now() -> Instant {
981        Instant::now()
982    }
983
984    fn esc_press() -> KeyEvent {
985        KeyEvent::new(KeyCode::Escape)
986    }
987
988    fn key_press(code: KeyCode) -> KeyEvent {
989        KeyEvent::new(code)
990    }
991
992    fn esc_release() -> KeyEvent {
993        KeyEvent::new(KeyCode::Escape).with_kind(KeyEventKind::Release)
994    }
995
996    const MS_50: Duration = Duration::from_millis(50);
997    const MS_100: Duration = Duration::from_millis(100);
998    const MS_200: Duration = Duration::from_millis(200);
999    const MS_300: Duration = Duration::from_millis(300);
1000
1001    // --- Basic sequence tests ---
1002
1003    #[test]
1004    fn single_esc_returns_pending() {
1005        let mut detector = SequenceDetector::with_defaults();
1006        let t = now();
1007
1008        let output = detector.feed(&esc_press(), t);
1009        assert_eq!(output, SequenceOutput::Pending);
1010        assert!(detector.is_pending());
1011    }
1012
1013    #[test]
1014    fn esc_esc_within_timeout() {
1015        let mut detector = SequenceDetector::with_defaults();
1016        let t = now();
1017
1018        detector.feed(&esc_press(), t);
1019        let output = detector.feed(&esc_press(), t + MS_100);
1020
1021        assert_eq!(output, SequenceOutput::EscEsc);
1022        assert!(!detector.is_pending());
1023    }
1024
1025    #[test]
1026    fn esc_esc_at_timeout_boundary() {
1027        let mut detector = SequenceDetector::with_defaults();
1028        let t = now();
1029
1030        detector.feed(&esc_press(), t);
1031        // Exactly at 250ms boundary
1032        let output = detector.feed(&esc_press(), t + Duration::from_millis(250));
1033
1034        assert_eq!(output, SequenceOutput::EscEsc);
1035    }
1036
1037    #[test]
1038    fn esc_esc_past_timeout() {
1039        let mut detector = SequenceDetector::with_defaults();
1040        let t = now();
1041
1042        detector.feed(&esc_press(), t);
1043        // Past 250ms timeout (251ms)
1044        let output = detector.feed(&esc_press(), t + Duration::from_millis(251));
1045
1046        // First Esc timed out, second Esc starts new sequence
1047        assert_eq!(output, SequenceOutput::Esc);
1048        assert!(detector.is_pending()); // New sequence started
1049    }
1050
1051    #[test]
1052    fn timeout_check_emits_pending_esc() {
1053        let mut detector = SequenceDetector::with_defaults();
1054        let t = now();
1055
1056        detector.feed(&esc_press(), t);
1057
1058        // Before timeout
1059        assert!(detector.check_timeout(t + MS_200).is_none());
1060        assert!(detector.is_pending());
1061
1062        // After timeout (251ms)
1063        let output = detector.check_timeout(t + Duration::from_millis(251));
1064        assert_eq!(output, Some(SequenceOutput::Esc));
1065        assert!(!detector.is_pending());
1066    }
1067
1068    #[test]
1069    fn other_key_interrupts_sequence() {
1070        let mut detector = SequenceDetector::with_defaults();
1071        let t = now();
1072
1073        detector.feed(&esc_press(), t);
1074        let output = detector.feed(&key_press(KeyCode::Char('a')), t + MS_100);
1075
1076        // Pending Esc is emitted
1077        assert_eq!(output, SequenceOutput::Esc);
1078        assert!(!detector.is_pending());
1079    }
1080
1081    #[test]
1082    fn non_esc_key_passes_through() {
1083        let mut detector = SequenceDetector::with_defaults();
1084        let t = now();
1085
1086        let output = detector.feed(&key_press(KeyCode::Char('x')), t);
1087        assert_eq!(output, SequenceOutput::PassThrough);
1088    }
1089
1090    #[test]
1091    fn release_event_passes_through() {
1092        let mut detector = SequenceDetector::with_defaults();
1093        let t = now();
1094
1095        let output = detector.feed(&esc_release(), t);
1096        assert_eq!(output, SequenceOutput::PassThrough);
1097        assert!(!detector.is_pending());
1098    }
1099
1100    #[test]
1101    fn release_during_pending_passes_through() {
1102        let mut detector = SequenceDetector::with_defaults();
1103        let t = now();
1104
1105        detector.feed(&esc_press(), t);
1106        let output = detector.feed(&esc_release(), t + MS_50);
1107
1108        // Release is ignored; still pending
1109        assert_eq!(output, SequenceOutput::PassThrough);
1110        assert!(detector.is_pending());
1111    }
1112
1113    // --- Config tests ---
1114
1115    #[test]
1116    fn custom_timeout() {
1117        let config = SequenceConfig::default().with_timeout(Duration::from_millis(100));
1118        let mut detector = SequenceDetector::new(config);
1119        let t = now();
1120
1121        detector.feed(&esc_press(), t);
1122        // 150ms is past 100ms timeout
1123        let output = detector.feed(&esc_press(), t + Duration::from_millis(150));
1124
1125        assert_eq!(output, SequenceOutput::Esc);
1126    }
1127
1128    #[test]
1129    fn disabled_sequences() {
1130        let config = SequenceConfig::default().disable_sequences();
1131        let mut detector = SequenceDetector::new(config);
1132        let t = now();
1133
1134        // First Esc immediately emits Esc
1135        let output = detector.feed(&esc_press(), t);
1136        assert_eq!(output, SequenceOutput::Esc);
1137        assert!(!detector.is_pending());
1138
1139        // Second Esc also immediately emits Esc
1140        let output = detector.feed(&esc_press(), t + MS_50);
1141        assert_eq!(output, SequenceOutput::Esc);
1142    }
1143
1144    #[test]
1145    fn disabled_sequences_passthrough() {
1146        let config = SequenceConfig::default().disable_sequences();
1147        let mut detector = SequenceDetector::new(config);
1148        let t = now();
1149
1150        let output = detector.feed(&key_press(KeyCode::Char('a')), t);
1151        assert_eq!(output, SequenceOutput::PassThrough);
1152    }
1153
1154    #[test]
1155    fn config_default_values() {
1156        let config = SequenceConfig::default();
1157        assert_eq!(config.esc_seq_timeout, Duration::from_millis(250));
1158        assert_eq!(config.esc_debounce, Duration::from_millis(50));
1159        assert!(!config.disable_sequences);
1160    }
1161
1162    #[test]
1163    fn config_builder_chain() {
1164        let config = SequenceConfig::default()
1165            .with_timeout(Duration::from_millis(300))
1166            .with_debounce(Duration::from_millis(100))
1167            .disable_sequences();
1168
1169        assert_eq!(config.esc_seq_timeout, Duration::from_millis(300));
1170        assert_eq!(config.esc_debounce, Duration::from_millis(100));
1171        assert!(config.disable_sequences);
1172    }
1173
1174    // --- Reset tests ---
1175
1176    #[test]
1177    fn reset_clears_pending() {
1178        let mut detector = SequenceDetector::with_defaults();
1179        let t = now();
1180
1181        detector.feed(&esc_press(), t);
1182        assert!(detector.is_pending());
1183
1184        detector.reset();
1185        assert!(!detector.is_pending());
1186
1187        // After reset, new Esc starts fresh
1188        let output = detector.feed(&esc_press(), t + MS_100);
1189        assert_eq!(output, SequenceOutput::Pending);
1190    }
1191
1192    #[test]
1193    fn reset_discards_pending_esc() {
1194        let mut detector = SequenceDetector::with_defaults();
1195        let t = now();
1196
1197        detector.feed(&esc_press(), t);
1198        detector.reset();
1199
1200        // Timeout check should not emit anything
1201        assert!(detector.check_timeout(t + MS_300).is_none());
1202    }
1203
1204    // --- Edge cases ---
1205
1206    #[test]
1207    fn rapid_triple_esc() {
1208        let mut detector = SequenceDetector::with_defaults();
1209        let t = now();
1210
1211        // First Esc
1212        let out1 = detector.feed(&esc_press(), t);
1213        assert_eq!(out1, SequenceOutput::Pending);
1214
1215        // Second Esc -> EscEsc
1216        let out2 = detector.feed(&esc_press(), t + MS_50);
1217        assert_eq!(out2, SequenceOutput::EscEsc);
1218
1219        // Third Esc -> starts new sequence
1220        let out3 = detector.feed(&esc_press(), t + MS_100);
1221        assert_eq!(out3, SequenceOutput::Pending);
1222    }
1223
1224    #[test]
1225    fn alternating_esc_and_key() {
1226        let mut detector = SequenceDetector::with_defaults();
1227        let t = now();
1228
1229        // Esc -> pending
1230        detector.feed(&esc_press(), t);
1231
1232        // 'a' -> emits Esc
1233        let out1 = detector.feed(&key_press(KeyCode::Char('a')), t + MS_50);
1234        assert_eq!(out1, SequenceOutput::Esc);
1235
1236        // Esc -> pending again
1237        let out2 = detector.feed(&esc_press(), t + MS_100);
1238        assert_eq!(out2, SequenceOutput::Pending);
1239
1240        // 'b' -> emits Esc
1241        let out3 = detector.feed(&key_press(KeyCode::Char('b')), t + MS_200);
1242        assert_eq!(out3, SequenceOutput::Esc);
1243    }
1244
1245    #[test]
1246    fn enter_key_interrupts() {
1247        let mut detector = SequenceDetector::with_defaults();
1248        let t = now();
1249
1250        detector.feed(&esc_press(), t);
1251        let output = detector.feed(&key_press(KeyCode::Enter), t + MS_100);
1252
1253        assert_eq!(output, SequenceOutput::Esc);
1254    }
1255
1256    #[test]
1257    fn function_key_interrupts() {
1258        let mut detector = SequenceDetector::with_defaults();
1259        let t = now();
1260
1261        detector.feed(&esc_press(), t);
1262        let output = detector.feed(&key_press(KeyCode::F(1)), t + MS_100);
1263
1264        assert_eq!(output, SequenceOutput::Esc);
1265    }
1266
1267    #[test]
1268    fn arrow_key_interrupts() {
1269        let mut detector = SequenceDetector::with_defaults();
1270        let t = now();
1271
1272        detector.feed(&esc_press(), t);
1273        let output = detector.feed(&key_press(KeyCode::Up), t + MS_100);
1274
1275        assert_eq!(output, SequenceOutput::Esc);
1276    }
1277
1278    #[test]
1279    fn config_getter_and_setter() {
1280        let mut detector = SequenceDetector::with_defaults();
1281        assert_eq!(
1282            detector.config().esc_seq_timeout,
1283            Duration::from_millis(250)
1284        );
1285
1286        let new_config = SequenceConfig::default().with_timeout(Duration::from_millis(500));
1287        detector.set_config(new_config);
1288
1289        assert_eq!(
1290            detector.config().esc_seq_timeout,
1291            Duration::from_millis(500)
1292        );
1293    }
1294
1295    #[test]
1296    fn set_config_preserves_pending_state() {
1297        let mut detector = SequenceDetector::with_defaults();
1298        let t = now();
1299
1300        detector.feed(&esc_press(), t);
1301        assert!(detector.is_pending());
1302
1303        // Change config while pending
1304        detector.set_config(SequenceConfig::default().with_timeout(Duration::from_millis(500)));
1305
1306        // Still pending
1307        assert!(detector.is_pending());
1308
1309        // New timeout applies
1310        let output = detector.feed(&esc_press(), t + MS_300);
1311        assert_eq!(output, SequenceOutput::EscEsc); // Within new 500ms timeout
1312    }
1313
1314    #[test]
1315    fn debug_format() {
1316        let detector = SequenceDetector::with_defaults();
1317        let dbg = format!("{:?}", detector);
1318        assert!(dbg.contains("SequenceDetector"));
1319    }
1320
1321    #[test]
1322    fn config_debug_format() {
1323        let config = SequenceConfig::default();
1324        let dbg = format!("{:?}", config);
1325        assert!(dbg.contains("SequenceConfig"));
1326    }
1327
1328    #[test]
1329    fn output_debug_and_eq() {
1330        assert_eq!(SequenceOutput::Pending, SequenceOutput::Pending);
1331        assert_eq!(SequenceOutput::Esc, SequenceOutput::Esc);
1332        assert_eq!(SequenceOutput::EscEsc, SequenceOutput::EscEsc);
1333        assert_eq!(SequenceOutput::PassThrough, SequenceOutput::PassThrough);
1334        assert_ne!(SequenceOutput::Esc, SequenceOutput::EscEsc);
1335
1336        let dbg = format!("{:?}", SequenceOutput::EscEsc);
1337        assert!(dbg.contains("EscEsc"));
1338    }
1339
1340    // --- Stress / property-like tests ---
1341
1342    #[test]
1343    fn no_stuck_state() {
1344        let mut detector = SequenceDetector::with_defaults();
1345        let t = now();
1346
1347        // Many operations should always return to Idle eventually
1348        for i in 0..100 {
1349            let offset = Duration::from_millis(i * 10);
1350            if i % 3 == 0 {
1351                detector.feed(&esc_press(), t + offset);
1352            } else {
1353                detector.feed(&key_press(KeyCode::Char('x')), t + offset);
1354            }
1355        }
1356
1357        // Force timeout check - must be well past the last event (990ms) + timeout (250ms)
1358        detector.check_timeout(t + Duration::from_secs(2));
1359
1360        // Should be idle
1361        assert!(!detector.is_pending());
1362    }
1363
1364    #[test]
1365    fn deterministic_output() {
1366        // Same inputs should produce same outputs
1367        let config = SequenceConfig::default();
1368        let t = now();
1369
1370        let mut d1 = SequenceDetector::new(config.clone());
1371        let mut d2 = SequenceDetector::new(config);
1372
1373        let events = [
1374            (esc_press(), t),
1375            (esc_press(), t + MS_100),
1376            (key_press(KeyCode::Char('a')), t + MS_200),
1377            (esc_press(), t + MS_300),
1378        ];
1379
1380        for (event, time) in &events {
1381            let out1 = d1.feed(event, *time);
1382            let out2 = d2.feed(event, *time);
1383            assert_eq!(out1, out2);
1384        }
1385    }
1386
1387    // =========================================================================
1388    // ActionMapper Tests
1389    // =========================================================================
1390
1391    mod action_mapper_tests {
1392        use super::*;
1393        use crate::event::Modifiers;
1394
1395        fn ctrl_c() -> KeyEvent {
1396            KeyEvent::new(KeyCode::Char('c')).with_modifiers(Modifiers::CTRL)
1397        }
1398
1399        fn ctrl_d() -> KeyEvent {
1400            KeyEvent::new(KeyCode::Char('d')).with_modifiers(Modifiers::CTRL)
1401        }
1402
1403        fn ctrl_q() -> KeyEvent {
1404            KeyEvent::new(KeyCode::Char('q')).with_modifiers(Modifiers::CTRL)
1405        }
1406
1407        fn idle_state() -> AppState {
1408            AppState::default()
1409        }
1410
1411        fn input_state() -> AppState {
1412            AppState::new().with_input(true)
1413        }
1414
1415        fn task_state() -> AppState {
1416            AppState::new().with_task(true)
1417        }
1418
1419        fn modal_state() -> AppState {
1420            AppState::new().with_modal(true)
1421        }
1422
1423        fn overlay_state() -> AppState {
1424            AppState::new().with_overlay(true)
1425        }
1426
1427        // --- Ctrl+C tests (policy priorities 2-5) ---
1428
1429        #[test]
1430        fn test_ctrl_c_clears_nonempty_input() {
1431            let mut mapper = ActionMapper::with_defaults();
1432            let t = now();
1433
1434            let action = mapper.map(&ctrl_c(), &input_state(), t);
1435            assert_eq!(action, Some(Action::ClearInput));
1436        }
1437
1438        #[test]
1439        fn test_ctrl_c_cancels_running_task() {
1440            let mut mapper = ActionMapper::with_defaults();
1441            let t = now();
1442
1443            let action = mapper.map(&ctrl_c(), &task_state(), t);
1444            assert_eq!(action, Some(Action::CancelTask));
1445        }
1446
1447        #[test]
1448        fn test_ctrl_c_quits_when_idle() {
1449            let mut mapper = ActionMapper::with_defaults();
1450            let t = now();
1451
1452            let action = mapper.map(&ctrl_c(), &idle_state(), t);
1453            assert_eq!(action, Some(Action::Quit));
1454        }
1455
1456        #[test]
1457        fn test_ctrl_c_dismisses_modal() {
1458            let mut mapper = ActionMapper::with_defaults();
1459            let t = now();
1460
1461            let action = mapper.map(&ctrl_c(), &modal_state(), t);
1462            assert_eq!(action, Some(Action::DismissModal));
1463        }
1464
1465        #[test]
1466        fn test_ctrl_c_modal_priority_over_input() {
1467            let mut mapper = ActionMapper::with_defaults();
1468            let t = now();
1469
1470            // Both modal and input are set
1471            let state = AppState::new().with_modal(true).with_input(true);
1472            let action = mapper.map(&ctrl_c(), &state, t);
1473            assert_eq!(action, Some(Action::DismissModal));
1474        }
1475
1476        #[test]
1477        fn test_ctrl_c_input_priority_over_task() {
1478            let mut mapper = ActionMapper::with_defaults();
1479            let t = now();
1480
1481            let state = AppState::new().with_input(true).with_task(true);
1482            let action = mapper.map(&ctrl_c(), &state, t);
1483            assert_eq!(action, Some(Action::ClearInput));
1484        }
1485
1486        #[test]
1487        fn test_ctrl_c_idle_config_noop() {
1488            let config = ActionConfig::default().with_ctrl_c_idle(CtrlCIdleAction::Noop);
1489            let mut mapper = ActionMapper::new(config);
1490            let t = now();
1491
1492            let action = mapper.map(&ctrl_c(), &idle_state(), t);
1493            assert_eq!(action, None); // Noop returns None
1494        }
1495
1496        #[test]
1497        fn test_ctrl_c_idle_config_bell() {
1498            let config = ActionConfig::default().with_ctrl_c_idle(CtrlCIdleAction::Bell);
1499            let mut mapper = ActionMapper::new(config);
1500            let t = now();
1501
1502            let action = mapper.map(&ctrl_c(), &idle_state(), t);
1503            assert_eq!(action, Some(Action::Bell));
1504        }
1505
1506        // --- Ctrl+D and Ctrl+Q tests (policy priorities 10-11) ---
1507
1508        #[test]
1509        fn test_ctrl_d_soft_quit() {
1510            let mut mapper = ActionMapper::with_defaults();
1511            let t = now();
1512
1513            let action = mapper.map(&ctrl_d(), &idle_state(), t);
1514            assert_eq!(action, Some(Action::SoftQuit));
1515        }
1516
1517        #[test]
1518        fn test_ctrl_d_ignores_state() {
1519            let mut mapper = ActionMapper::with_defaults();
1520            let t = now();
1521
1522            // Ctrl+D always does SoftQuit regardless of state
1523            let action = mapper.map(&ctrl_d(), &modal_state(), t);
1524            assert_eq!(action, Some(Action::SoftQuit));
1525
1526            let action = mapper.map(&ctrl_d(), &input_state(), t);
1527            assert_eq!(action, Some(Action::SoftQuit));
1528        }
1529
1530        #[test]
1531        fn test_ctrl_q_hard_quit() {
1532            let mut mapper = ActionMapper::with_defaults();
1533            let t = now();
1534
1535            let action = mapper.map(&ctrl_q(), &idle_state(), t);
1536            assert_eq!(action, Some(Action::HardQuit));
1537        }
1538
1539        #[test]
1540        fn test_ctrl_q_ignores_state() {
1541            let mut mapper = ActionMapper::with_defaults();
1542            let t = now();
1543
1544            // Ctrl+Q always does HardQuit regardless of state
1545            let action = mapper.map(&ctrl_q(), &modal_state(), t);
1546            assert_eq!(action, Some(Action::HardQuit));
1547        }
1548
1549        // --- Esc tests (policy priorities 1, 6-8) ---
1550
1551        #[test]
1552        fn test_esc_dismisses_modal() {
1553            let mut mapper = ActionMapper::with_defaults();
1554            let t = now();
1555
1556            // First Esc: pending
1557            let action1 = mapper.map(&esc_press(), &modal_state(), t);
1558            assert_eq!(action1, None);
1559
1560            // Timeout: emit Esc action
1561            let action2 = mapper.check_timeout(&modal_state(), t + MS_300);
1562            assert_eq!(action2, Some(Action::DismissModal));
1563        }
1564
1565        #[test]
1566        fn test_esc_clears_input_no_modal() {
1567            let mut mapper = ActionMapper::with_defaults();
1568            let t = now();
1569
1570            mapper.map(&esc_press(), &input_state(), t);
1571            let action = mapper.check_timeout(&input_state(), t + MS_300);
1572            assert_eq!(action, Some(Action::ClearInput));
1573        }
1574
1575        #[test]
1576        fn test_esc_cancels_task_empty_input() {
1577            let mut mapper = ActionMapper::with_defaults();
1578            let t = now();
1579
1580            mapper.map(&esc_press(), &task_state(), t);
1581            let action = mapper.check_timeout(&task_state(), t + MS_300);
1582            assert_eq!(action, Some(Action::CancelTask));
1583        }
1584
1585        #[test]
1586        fn test_esc_closes_overlay() {
1587            let mut mapper = ActionMapper::with_defaults();
1588            let t = now();
1589
1590            mapper.map(&esc_press(), &overlay_state(), t);
1591            let action = mapper.check_timeout(&overlay_state(), t + MS_300);
1592            assert_eq!(action, Some(Action::CloseOverlay));
1593        }
1594
1595        #[test]
1596        fn test_esc_modal_priority_over_overlay() {
1597            let mut mapper = ActionMapper::with_defaults();
1598            let t = now();
1599
1600            let state = AppState::new().with_modal(true).with_overlay(true);
1601            mapper.map(&esc_press(), &state, t);
1602            let action = mapper.check_timeout(&state, t + MS_300);
1603            assert_eq!(action, Some(Action::DismissModal));
1604        }
1605
1606        #[test]
1607        fn test_esc_passthrough_when_idle() {
1608            let mut mapper = ActionMapper::with_defaults();
1609            let t = now();
1610
1611            mapper.map(&esc_press(), &idle_state(), t);
1612            let action = mapper.check_timeout(&idle_state(), t + MS_300);
1613            assert_eq!(action, Some(Action::PassThrough));
1614        }
1615
1616        // --- Esc Esc tests (policy priority 9) ---
1617
1618        #[test]
1619        fn test_esc_esc_within_timeout() {
1620            let mut mapper = ActionMapper::with_defaults();
1621            let t = now();
1622
1623            mapper.map(&esc_press(), &idle_state(), t);
1624            let action = mapper.map(&esc_press(), &idle_state(), t + MS_100);
1625            assert_eq!(action, Some(Action::ToggleTreeView));
1626        }
1627
1628        #[test]
1629        fn test_esc_esc_ignores_state() {
1630            let mut mapper = ActionMapper::with_defaults();
1631            let t = now();
1632
1633            // Esc Esc always toggles tree view regardless of state
1634            mapper.map(&esc_press(), &modal_state(), t);
1635            let action = mapper.map(&esc_press(), &modal_state(), t + MS_100);
1636            assert_eq!(action, Some(Action::ToggleTreeView));
1637        }
1638
1639        #[test]
1640        fn test_esc_esc_timeout_expired() {
1641            let mut mapper = ActionMapper::with_defaults();
1642            let t = now();
1643
1644            mapper.map(&esc_press(), &input_state(), t);
1645            // Past 250ms timeout
1646            let action = mapper.map(&esc_press(), &input_state(), t + MS_300);
1647
1648            // First Esc timed out -> ClearInput, second starts new pending
1649            assert_eq!(action, Some(Action::ClearInput));
1650            assert!(mapper.is_pending_esc());
1651        }
1652
1653        // --- Esc then other key ---
1654
1655        #[test]
1656        fn test_esc_then_other_key() {
1657            let mut mapper = ActionMapper::with_defaults();
1658            let t = now();
1659
1660            mapper.map(&esc_press(), &input_state(), t);
1661            let action = mapper.map(&key_press(KeyCode::Char('a')), &input_state(), t + MS_50);
1662
1663            // Pending Esc is emitted
1664            assert_eq!(action, Some(Action::ClearInput));
1665        }
1666
1667        // --- Other keys passthrough ---
1668
1669        #[test]
1670        fn test_regular_key_passthrough() {
1671            let mut mapper = ActionMapper::with_defaults();
1672            let t = now();
1673
1674            let action = mapper.map(&key_press(KeyCode::Char('x')), &idle_state(), t);
1675            assert_eq!(action, Some(Action::PassThrough));
1676        }
1677
1678        #[test]
1679        fn test_release_event_passthrough() {
1680            let mut mapper = ActionMapper::with_defaults();
1681            let t = now();
1682
1683            let release = KeyEvent::new(KeyCode::Char('x')).with_kind(KeyEventKind::Release);
1684            let action = mapper.map(&release, &idle_state(), t);
1685            assert_eq!(action, Some(Action::PassThrough));
1686        }
1687
1688        // --- State helper tests ---
1689
1690        #[test]
1691        fn test_app_state_builders() {
1692            let state = AppState::new()
1693                .with_input(true)
1694                .with_task(true)
1695                .with_modal(true)
1696                .with_overlay(true);
1697
1698            assert!(state.input_nonempty);
1699            assert!(state.task_running);
1700            assert!(state.modal_open);
1701            assert!(state.view_overlay);
1702            assert!(!state.is_idle());
1703        }
1704
1705        #[test]
1706        fn test_app_state_is_idle() {
1707            assert!(AppState::default().is_idle());
1708            assert!(!AppState::new().with_input(true).is_idle());
1709            assert!(!AppState::new().with_task(true).is_idle());
1710            assert!(!AppState::new().with_modal(true).is_idle());
1711            // view_overlay doesn't affect is_idle
1712            assert!(AppState::new().with_overlay(true).is_idle());
1713        }
1714
1715        // --- Action enum tests ---
1716
1717        #[test]
1718        fn test_action_consumes_event() {
1719            assert!(Action::ClearInput.consumes_event());
1720            assert!(Action::CancelTask.consumes_event());
1721            assert!(Action::Quit.consumes_event());
1722            assert!(!Action::PassThrough.consumes_event());
1723        }
1724
1725        #[test]
1726        fn test_action_is_quit() {
1727            assert!(Action::Quit.is_quit());
1728            assert!(Action::SoftQuit.is_quit());
1729            assert!(Action::HardQuit.is_quit());
1730            assert!(!Action::ClearInput.is_quit());
1731            assert!(!Action::PassThrough.is_quit());
1732        }
1733
1734        // --- Config tests ---
1735
1736        #[test]
1737        fn test_ctrl_c_idle_action_from_str() {
1738            assert_eq!(
1739                CtrlCIdleAction::from_str_opt("quit"),
1740                Some(CtrlCIdleAction::Quit)
1741            );
1742            assert_eq!(
1743                CtrlCIdleAction::from_str_opt("QUIT"),
1744                Some(CtrlCIdleAction::Quit)
1745            );
1746            assert_eq!(
1747                CtrlCIdleAction::from_str_opt("noop"),
1748                Some(CtrlCIdleAction::Noop)
1749            );
1750            assert_eq!(
1751                CtrlCIdleAction::from_str_opt("none"),
1752                Some(CtrlCIdleAction::Noop)
1753            );
1754            assert_eq!(
1755                CtrlCIdleAction::from_str_opt("ignore"),
1756                Some(CtrlCIdleAction::Noop)
1757            );
1758            assert_eq!(
1759                CtrlCIdleAction::from_str_opt("bell"),
1760                Some(CtrlCIdleAction::Bell)
1761            );
1762            assert_eq!(
1763                CtrlCIdleAction::from_str_opt("beep"),
1764                Some(CtrlCIdleAction::Bell)
1765            );
1766            assert_eq!(CtrlCIdleAction::from_str_opt("invalid"), None);
1767        }
1768
1769        #[test]
1770        fn test_ctrl_c_idle_action_to_action() {
1771            assert_eq!(CtrlCIdleAction::Quit.to_action(), Some(Action::Quit));
1772            assert_eq!(CtrlCIdleAction::Noop.to_action(), None);
1773            assert_eq!(CtrlCIdleAction::Bell.to_action(), Some(Action::Bell));
1774        }
1775
1776        #[test]
1777        fn test_action_config_builder() {
1778            let config = ActionConfig::default()
1779                .with_sequence_config(SequenceConfig::default().with_timeout(MS_100))
1780                .with_ctrl_c_idle(CtrlCIdleAction::Bell);
1781
1782            assert_eq!(config.sequence_config.esc_seq_timeout, MS_100);
1783            assert_eq!(config.ctrl_c_idle_action, CtrlCIdleAction::Bell);
1784        }
1785
1786        // --- Reset tests ---
1787
1788        #[test]
1789        fn test_mapper_reset() {
1790            let mut mapper = ActionMapper::with_defaults();
1791            let t = now();
1792
1793            mapper.map(&esc_press(), &idle_state(), t);
1794            assert!(mapper.is_pending_esc());
1795
1796            mapper.reset();
1797            assert!(!mapper.is_pending_esc());
1798        }
1799
1800        // --- Determinism / property tests ---
1801
1802        #[test]
1803        fn test_deterministic_action_mapping() {
1804            let t = now();
1805
1806            let mut m1 = ActionMapper::with_defaults();
1807            let mut m2 = ActionMapper::with_defaults();
1808
1809            let events = [
1810                (ctrl_c(), input_state()),
1811                (ctrl_d(), modal_state()),
1812                (ctrl_q(), idle_state()),
1813            ];
1814
1815            for (event, state) in &events {
1816                let a1 = m1.map(event, state, t);
1817                let a2 = m2.map(event, state, t);
1818                assert_eq!(a1, a2);
1819            }
1820        }
1821
1822        #[test]
1823        fn test_uppercase_ctrl_keys() {
1824            let mut mapper = ActionMapper::with_defaults();
1825            let t = now();
1826
1827            // Ctrl+C with uppercase 'C' should also work
1828            let ctrl_c_upper = KeyEvent::new(KeyCode::Char('C')).with_modifiers(Modifiers::CTRL);
1829            let action = mapper.map(&ctrl_c_upper, &idle_state(), t);
1830            assert_eq!(action, Some(Action::Quit));
1831        }
1832
1833        // --- Validation tests ---
1834
1835        #[test]
1836        fn test_sequence_config_validation_clamps_high_timeout() {
1837            let config = SequenceConfig::default()
1838                .with_timeout(Duration::from_millis(1000)) // Too high
1839                .validated();
1840
1841            // Should clamp to MAX_ESC_SEQ_TIMEOUT_MS (400ms)
1842            assert_eq!(config.esc_seq_timeout.as_millis(), 400);
1843        }
1844
1845        #[test]
1846        fn test_sequence_config_validation_clamps_low_timeout() {
1847            let config = SequenceConfig::default()
1848                .with_timeout(Duration::from_millis(50)) // Too low
1849                .validated();
1850
1851            // Should clamp to MIN_ESC_SEQ_TIMEOUT_MS (150ms)
1852            assert_eq!(config.esc_seq_timeout.as_millis(), 150);
1853        }
1854
1855        #[test]
1856        fn test_sequence_config_validation_clamps_high_debounce() {
1857            let config = SequenceConfig::default()
1858                .with_debounce(Duration::from_millis(200)) // Too high
1859                .validated();
1860
1861            // Should clamp to MAX_ESC_DEBOUNCE_MS (100ms)
1862            assert_eq!(config.esc_debounce.as_millis(), 100);
1863        }
1864
1865        #[test]
1866        fn test_sequence_config_validation_debounce_not_exceeds_timeout() {
1867            let config = SequenceConfig::default()
1868                .with_timeout(Duration::from_millis(150))
1869                .with_debounce(Duration::from_millis(200)) // Higher than timeout
1870                .validated();
1871
1872            // Debounce should be clamped to min(100, 150) = 100,
1873            // but also can't exceed timeout (150)
1874            // Since debounce max is 100 and timeout is 150, debounce = 100
1875            assert!(config.esc_debounce <= config.esc_seq_timeout);
1876        }
1877
1878        #[test]
1879        fn test_sequence_config_is_valid() {
1880            assert!(SequenceConfig::default().is_valid());
1881
1882            // Invalid: timeout too high
1883            let invalid = SequenceConfig::default().with_timeout(Duration::from_millis(500));
1884            assert!(!invalid.is_valid());
1885
1886            // Valid after validation
1887            assert!(invalid.validated().is_valid());
1888        }
1889
1890        #[test]
1891        fn test_sequence_config_constants() {
1892            // Verify constants match spec
1893            assert_eq!(DEFAULT_ESC_SEQ_TIMEOUT_MS, 250);
1894            assert_eq!(MIN_ESC_SEQ_TIMEOUT_MS, 150);
1895            assert_eq!(MAX_ESC_SEQ_TIMEOUT_MS, 400);
1896            assert_eq!(DEFAULT_ESC_DEBOUNCE_MS, 50);
1897            assert_eq!(MIN_ESC_DEBOUNCE_MS, 0);
1898            assert_eq!(MAX_ESC_DEBOUNCE_MS, 100);
1899        }
1900
1901        #[test]
1902        fn test_action_config_validated() {
1903            let config = ActionConfig::default()
1904                .with_sequence_config(
1905                    SequenceConfig::default().with_timeout(Duration::from_millis(1000)),
1906                )
1907                .validated();
1908
1909            // Sequence config should be validated
1910            assert_eq!(config.sequence_config.esc_seq_timeout.as_millis(), 400);
1911        }
1912    }
1913}