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    /// Enter key in completion popup - may confirm or insert newline based on config
155    CompletionEnterKey,
156    /// Type a character while completion popup is open (for type-to-filter)
157    PopupTypeChar(char),
158    /// Backspace while completion popup is open (for type-to-filter)
159    PopupBackspace,
160    /// Copy text to clipboard (from popup text selection)
161    CopyToClipboard(String),
162
163    // File browser actions
164    FileBrowserSelectPrev,
165    FileBrowserSelectNext,
166    FileBrowserPageUp,
167    FileBrowserPageDown,
168    FileBrowserConfirm,
169    FileBrowserAcceptSuggestion,
170    FileBrowserGoParent,
171    FileBrowserUpdateFilter,
172    FileBrowserToggleHidden,
173
174    // Interactive replace actions
175    InteractiveReplaceKey(char),
176    CancelInteractiveReplace,
177
178    // Terminal mode actions
179    ToggleKeyboardCapture,
180    SendTerminalKey(crossterm::event::KeyCode, crossterm::event::KeyModifiers),
181    /// Send a mouse event to the terminal PTY.
182    /// Fields: (col, row, event_kind, button, modifiers)
183    /// Coordinates are terminal-relative (0-based from terminal content area).
184    SendTerminalMouse {
185        col: u16,
186        row: u16,
187        kind: TerminalMouseEventKind,
188        modifiers: crossterm::event::KeyModifiers,
189    },
190    ExitTerminalMode {
191        explicit: bool,
192    },
193    EnterScrollbackMode,
194    EnterTerminalMode,
195
196    // Generic action execution
197    ExecuteAction(crate::input::keybindings::Action),
198
199    // Insert character (for prompts that need to update suggestions)
200    InsertCharAndUpdate(char),
201}
202
203/// Trait for elements that can handle input events.
204///
205/// Implementors should:
206/// 1. First delegate to `focused_child_mut()` if it exists
207/// 2. Handle keys relevant to this element
208/// 3. Return `Consumed` or `Ignored` appropriately
209/// 4. Modal elements should return `Consumed` for unhandled keys
210pub trait InputHandler {
211    /// Handle a key event. Returns whether the event was consumed.
212    fn handle_key_event(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult;
213
214    /// Get the currently focused child handler, if any.
215    fn focused_child(&self) -> Option<&dyn InputHandler> {
216        None
217    }
218
219    /// Get the currently focused child handler mutably, if any.
220    fn focused_child_mut(&mut self) -> Option<&mut dyn InputHandler> {
221        None
222    }
223
224    /// Whether this handler is modal (consumes all unhandled input).
225    fn is_modal(&self) -> bool {
226        false
227    }
228
229    /// Dispatch input through this handler and its children.
230    /// This is the main entry point - it handles the bubble-up logic.
231    fn dispatch_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
232        // First, let the deepest focused child try
233        if let Some(child) = self.focused_child_mut() {
234            let result = child.dispatch_input(event, ctx);
235            if result == InputResult::Consumed {
236                return InputResult::Consumed;
237            }
238        }
239
240        // Child didn't consume, try this handler
241        let result = self.handle_key_event(event, ctx);
242        if result == InputResult::Consumed {
243            return InputResult::Consumed;
244        }
245
246        // If explicitly ignored, pass through (even for modal handlers)
247        // This allows modal handlers to opt-out of consuming specific keys
248        // (e.g., Ctrl+P to toggle Quick Open while it's open)
249        if result == InputResult::Ignored {
250            return InputResult::Ignored;
251        }
252
253        // If modal and result is not explicitly Ignored, consume to prevent leaking
254        if self.is_modal() {
255            return InputResult::Consumed;
256        }
257
258        InputResult::Ignored
259    }
260}
261
262/// Helper to check for common key combinations.
263pub fn is_key(event: &KeyEvent, code: KeyCode) -> bool {
264    event.code == code && event.modifiers.is_empty()
265}
266
267pub fn is_key_with_ctrl(event: &KeyEvent, c: char) -> bool {
268    event.code == KeyCode::Char(c) && event.modifiers == KeyModifiers::CONTROL
269}
270
271pub fn is_key_with_shift(event: &KeyEvent, code: KeyCode) -> bool {
272    event.code == code && event.modifiers == KeyModifiers::SHIFT
273}
274
275pub fn is_key_with_alt(event: &KeyEvent, code: KeyCode) -> bool {
276    event.code == code && event.modifiers == KeyModifiers::ALT
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_input_result_or() {
285        assert_eq!(
286            InputResult::Consumed.or(InputResult::Consumed),
287            InputResult::Consumed
288        );
289        assert_eq!(
290            InputResult::Consumed.or(InputResult::Ignored),
291            InputResult::Consumed
292        );
293        assert_eq!(
294            InputResult::Ignored.or(InputResult::Consumed),
295            InputResult::Consumed
296        );
297        assert_eq!(
298            InputResult::Ignored.or(InputResult::Ignored),
299            InputResult::Ignored
300        );
301    }
302
303    #[test]
304    fn test_is_consumed() {
305        assert!(InputResult::Consumed.is_consumed());
306        assert!(!InputResult::Ignored.is_consumed());
307    }
308
309    /// Test handler that tracks what it returns
310    struct TestModalHandler {
311        returns_ignored: bool,
312    }
313
314    impl InputHandler for TestModalHandler {
315        fn handle_key_event(&mut self, _event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
316            if self.returns_ignored {
317                InputResult::Ignored
318            } else {
319                InputResult::Consumed
320            }
321        }
322
323        fn is_modal(&self) -> bool {
324            true
325        }
326    }
327
328    #[test]
329    fn test_modal_handler_respects_ignored() {
330        // When modal handler returns Ignored, dispatch_input should also return Ignored
331        let mut handler = TestModalHandler {
332            returns_ignored: true,
333        };
334        let mut ctx = InputContext::new();
335        let event = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL);
336
337        let result = handler.dispatch_input(&event, &mut ctx);
338        assert_eq!(
339            result,
340            InputResult::Ignored,
341            "Modal handler should respect Ignored result"
342        );
343    }
344
345    #[test]
346    fn test_modal_handler_consumes_unknown_keys() {
347        // When modal handler returns Consumed, dispatch_input should also return Consumed
348        let mut handler = TestModalHandler {
349            returns_ignored: false,
350        };
351        let mut ctx = InputContext::new();
352        let event = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
353
354        let result = handler.dispatch_input(&event, &mut ctx);
355        assert_eq!(
356            result,
357            InputResult::Consumed,
358            "Modal handler should consume handled keys"
359        );
360    }
361}