Skip to main content

fresh/input/
handler.rs

1//! Hierarchical Input Handling System
2//!
3//! This module provides a tree-based input dispatch system where input events
4//! flow through a hierarchy of handlers. The design follows these principles:
5//!
6//! 1. **Leaf-first, bubble up**: Input is dispatched to the deepest focused
7//!    element first. If not consumed, it bubbles up to parents.
8//!
9//! 2. **Explicit consumption**: Handlers return `InputResult::Consumed` to stop
10//!    propagation or `InputResult::Ignored` to let parents try.
11//!
12//! 3. **Modals consume by default**: Modal dialogs (Settings, Prompts) should
13//!    return `Consumed` for unhandled keys to prevent input leakage.
14//!
15//! 4. **No capture phase**: Unlike DOM events, there's no capture phase.
16//!    This keeps the model simple and predictable.
17//!
18//! ## Example
19//!
20//! ```ignore
21//! impl InputHandler for MyPanel {
22//!     fn handle_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
23//!         // Let focused child try first
24//!         if let Some(child) = self.focused_child_mut() {
25//!             if child.handle_input(event, ctx) == InputResult::Consumed {
26//!                 return InputResult::Consumed;
27//!             }
28//!         }
29//!
30//!         // Handle at this level
31//!         match event.code {
32//!             KeyCode::Up => { self.move_up(); InputResult::Consumed }
33//!             KeyCode::Down => { self.move_down(); InputResult::Consumed }
34//!             _ => InputResult::Ignored // Let parent handle
35//!         }
36//!     }
37//! }
38//! ```
39
40use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
41
42/// Mouse event kinds for terminal forwarding.
43/// Simplified from crossterm's MouseEventKind to capture what we need.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum TerminalMouseEventKind {
46    /// Button press
47    Down(TerminalMouseButton),
48    /// Button release
49    Up(TerminalMouseButton),
50    /// Mouse drag with button held
51    Drag(TerminalMouseButton),
52    /// Mouse movement (no button)
53    Moved,
54    /// Scroll up
55    ScrollUp,
56    /// Scroll down
57    ScrollDown,
58}
59
60/// Mouse buttons for terminal forwarding.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum TerminalMouseButton {
63    Left,
64    Right,
65    Middle,
66}
67
68/// Result of handling an input event.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum InputResult {
71    /// The input was handled - stop propagation.
72    Consumed,
73    /// The input was not handled - try parent.
74    Ignored,
75}
76
77impl InputResult {
78    /// Returns true if the input was consumed.
79    pub fn is_consumed(self) -> bool {
80        self == InputResult::Consumed
81    }
82
83    /// Combines two results - consumed if either is consumed.
84    pub fn or(self, other: InputResult) -> InputResult {
85        if self == InputResult::Consumed || other == InputResult::Consumed {
86            InputResult::Consumed
87        } else {
88            InputResult::Ignored
89        }
90    }
91}
92
93/// Context passed to input handlers, providing access to shared state.
94#[derive(Default)]
95pub struct InputContext {
96    /// Status message to display (set by handlers).
97    pub status_message: Option<String>,
98    /// Actions to execute after input handling (for deferred operations).
99    pub deferred_actions: Vec<DeferredAction>,
100}
101
102impl InputContext {
103    pub fn new() -> Self {
104        Self::default()
105    }
106
107    pub fn set_status(&mut self, msg: impl Into<String>) {
108        self.status_message = Some(msg.into());
109    }
110
111    pub fn defer(&mut self, action: DeferredAction) {
112        self.deferred_actions.push(action);
113    }
114}
115
116/// Actions that need to be executed after input handling completes.
117/// These are operations that require mutable access to Editor.
118#[derive(Debug, Clone)]
119pub enum DeferredAction {
120    // Settings actions
121    CloseSettings {
122        save: bool,
123    },
124    /// Paste text from clipboard into the active settings input
125    PasteToSettings,
126    /// Open the config file for the specified layer in the editor
127    OpenConfigFile {
128        layer: crate::config_io::ConfigLayer,
129    },
130
131    // Menu actions
132    CloseMenu,
133    ExecuteMenuAction {
134        action: String,
135        args: std::collections::HashMap<String, serde_json::Value>,
136    },
137
138    // Prompt actions
139    ClosePrompt,
140    ConfirmPrompt,
141    UpdatePromptSuggestions,
142    PromptHistoryPrev,
143    PromptHistoryNext,
144    /// Preview theme from the current prompt input (for SelectTheme)
145    PreviewThemeFromPrompt,
146    /// Notify plugin that prompt selection changed (for live preview in Live Grep, etc.)
147    PromptSelectionChanged {
148        selected_index: usize,
149    },
150
151    // Popup actions
152    ClosePopup,
153    ConfirmPopup,
154    /// Type a character while completion popup is open (for type-to-filter)
155    PopupTypeChar(char),
156    /// Backspace while completion popup is open (for type-to-filter)
157    PopupBackspace,
158    /// Copy text to clipboard (from popup text selection)
159    CopyToClipboard(String),
160
161    // File browser actions
162    FileBrowserSelectPrev,
163    FileBrowserSelectNext,
164    FileBrowserPageUp,
165    FileBrowserPageDown,
166    FileBrowserConfirm,
167    FileBrowserAcceptSuggestion,
168    FileBrowserGoParent,
169    FileBrowserUpdateFilter,
170    FileBrowserToggleHidden,
171
172    // Interactive replace actions
173    InteractiveReplaceKey(char),
174    CancelInteractiveReplace,
175
176    // Terminal mode actions
177    ToggleKeyboardCapture,
178    SendTerminalKey(crossterm::event::KeyCode, crossterm::event::KeyModifiers),
179    /// Send a mouse event to the terminal PTY.
180    /// Fields: (col, row, event_kind, button, modifiers)
181    /// Coordinates are terminal-relative (0-based from terminal content area).
182    SendTerminalMouse {
183        col: u16,
184        row: u16,
185        kind: TerminalMouseEventKind,
186        modifiers: crossterm::event::KeyModifiers,
187    },
188    ExitTerminalMode {
189        explicit: bool,
190    },
191    EnterScrollbackMode,
192    EnterTerminalMode,
193
194    // Generic action execution
195    ExecuteAction(crate::input::keybindings::Action),
196
197    // Insert character (for prompts that need to update suggestions)
198    InsertCharAndUpdate(char),
199}
200
201/// Trait for elements that can handle input events.
202///
203/// Implementors should:
204/// 1. First delegate to `focused_child_mut()` if it exists
205/// 2. Handle keys relevant to this element
206/// 3. Return `Consumed` or `Ignored` appropriately
207/// 4. Modal elements should return `Consumed` for unhandled keys
208pub trait InputHandler {
209    /// Handle a key event. Returns whether the event was consumed.
210    fn handle_key_event(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult;
211
212    /// Get the currently focused child handler, if any.
213    fn focused_child(&self) -> Option<&dyn InputHandler> {
214        None
215    }
216
217    /// Get the currently focused child handler mutably, if any.
218    fn focused_child_mut(&mut self) -> Option<&mut dyn InputHandler> {
219        None
220    }
221
222    /// Whether this handler is modal (consumes all unhandled input).
223    fn is_modal(&self) -> bool {
224        false
225    }
226
227    /// Dispatch input through this handler and its children.
228    /// This is the main entry point - it handles the bubble-up logic.
229    fn dispatch_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
230        // First, let the deepest focused child try
231        if let Some(child) = self.focused_child_mut() {
232            let result = child.dispatch_input(event, ctx);
233            if result == InputResult::Consumed {
234                return InputResult::Consumed;
235            }
236        }
237
238        // Child didn't consume, try this handler
239        let result = self.handle_key_event(event, ctx);
240        if result == InputResult::Consumed {
241            return InputResult::Consumed;
242        }
243
244        // If explicitly ignored, pass through (even for modal handlers)
245        // This allows modal handlers to opt-out of consuming specific keys
246        // (e.g., Ctrl+P to toggle Quick Open while it's open)
247        if result == InputResult::Ignored {
248            return InputResult::Ignored;
249        }
250
251        // If modal and result is not explicitly Ignored, consume to prevent leaking
252        if self.is_modal() {
253            return InputResult::Consumed;
254        }
255
256        InputResult::Ignored
257    }
258}
259
260/// Helper to check for common key combinations.
261pub fn is_key(event: &KeyEvent, code: KeyCode) -> bool {
262    event.code == code && event.modifiers.is_empty()
263}
264
265pub fn is_key_with_ctrl(event: &KeyEvent, c: char) -> bool {
266    event.code == KeyCode::Char(c) && event.modifiers == KeyModifiers::CONTROL
267}
268
269pub fn is_key_with_shift(event: &KeyEvent, code: KeyCode) -> bool {
270    event.code == code && event.modifiers == KeyModifiers::SHIFT
271}
272
273pub fn is_key_with_alt(event: &KeyEvent, code: KeyCode) -> bool {
274    event.code == code && event.modifiers == KeyModifiers::ALT
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_input_result_or() {
283        assert_eq!(
284            InputResult::Consumed.or(InputResult::Consumed),
285            InputResult::Consumed
286        );
287        assert_eq!(
288            InputResult::Consumed.or(InputResult::Ignored),
289            InputResult::Consumed
290        );
291        assert_eq!(
292            InputResult::Ignored.or(InputResult::Consumed),
293            InputResult::Consumed
294        );
295        assert_eq!(
296            InputResult::Ignored.or(InputResult::Ignored),
297            InputResult::Ignored
298        );
299    }
300
301    #[test]
302    fn test_is_consumed() {
303        assert!(InputResult::Consumed.is_consumed());
304        assert!(!InputResult::Ignored.is_consumed());
305    }
306
307    /// Test handler that tracks what it returns
308    struct TestModalHandler {
309        returns_ignored: bool,
310    }
311
312    impl InputHandler for TestModalHandler {
313        fn handle_key_event(&mut self, _event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
314            if self.returns_ignored {
315                InputResult::Ignored
316            } else {
317                InputResult::Consumed
318            }
319        }
320
321        fn is_modal(&self) -> bool {
322            true
323        }
324    }
325
326    #[test]
327    fn test_modal_handler_respects_ignored() {
328        // When modal handler returns Ignored, dispatch_input should also return Ignored
329        let mut handler = TestModalHandler {
330            returns_ignored: true,
331        };
332        let mut ctx = InputContext::new();
333        let event = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL);
334
335        let result = handler.dispatch_input(&event, &mut ctx);
336        assert_eq!(
337            result,
338            InputResult::Ignored,
339            "Modal handler should respect Ignored result"
340        );
341    }
342
343    #[test]
344    fn test_modal_handler_consumes_unknown_keys() {
345        // When modal handler returns Consumed, dispatch_input should also return Consumed
346        let mut handler = TestModalHandler {
347            returns_ignored: false,
348        };
349        let mut ctx = InputContext::new();
350        let event = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
351
352        let result = handler.dispatch_input(&event, &mut ctx);
353        assert_eq!(
354            result,
355            InputResult::Consumed,
356            "Modal handler should consume handled keys"
357        );
358    }
359}