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/// Result of handling an input event.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum InputResult {
45    /// The input was handled - stop propagation.
46    Consumed,
47    /// The input was not handled - try parent.
48    Ignored,
49}
50
51impl InputResult {
52    /// Returns true if the input was consumed.
53    pub fn is_consumed(self) -> bool {
54        self == InputResult::Consumed
55    }
56
57    /// Combines two results - consumed if either is consumed.
58    pub fn or(self, other: InputResult) -> InputResult {
59        if self == InputResult::Consumed || other == InputResult::Consumed {
60            InputResult::Consumed
61        } else {
62            InputResult::Ignored
63        }
64    }
65}
66
67/// Context passed to input handlers, providing access to shared state.
68#[derive(Default)]
69pub struct InputContext {
70    /// Status message to display (set by handlers).
71    pub status_message: Option<String>,
72    /// Actions to execute after input handling (for deferred operations).
73    pub deferred_actions: Vec<DeferredAction>,
74}
75
76impl InputContext {
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    pub fn set_status(&mut self, msg: impl Into<String>) {
82        self.status_message = Some(msg.into());
83    }
84
85    pub fn defer(&mut self, action: DeferredAction) {
86        self.deferred_actions.push(action);
87    }
88}
89
90/// Actions that need to be executed after input handling completes.
91/// These are operations that require mutable access to Editor.
92#[derive(Debug, Clone)]
93pub enum DeferredAction {
94    // Settings actions
95    CloseSettings {
96        save: bool,
97    },
98    /// Paste text from clipboard into the active settings input
99    PasteToSettings,
100    /// Open the config file for the specified layer in the editor
101    OpenConfigFile {
102        layer: crate::config_io::ConfigLayer,
103    },
104
105    // Menu actions
106    CloseMenu,
107    ExecuteMenuAction {
108        action: String,
109        args: std::collections::HashMap<String, serde_json::Value>,
110    },
111
112    // Prompt actions
113    ClosePrompt,
114    ConfirmPrompt,
115    UpdatePromptSuggestions,
116    PromptHistoryPrev,
117    PromptHistoryNext,
118    /// Preview theme from the current prompt input (for SelectTheme)
119    PreviewThemeFromPrompt,
120    /// Notify plugin that prompt selection changed (for live preview in Live Grep, etc.)
121    PromptSelectionChanged {
122        selected_index: usize,
123    },
124
125    // Popup actions
126    ClosePopup,
127    ConfirmPopup,
128    /// Type a character while completion popup is open (for type-to-filter)
129    PopupTypeChar(char),
130    /// Backspace while completion popup is open (for type-to-filter)
131    PopupBackspace,
132    /// Copy text to clipboard (from popup text selection)
133    CopyToClipboard(String),
134
135    // File browser actions
136    FileBrowserSelectPrev,
137    FileBrowserSelectNext,
138    FileBrowserPageUp,
139    FileBrowserPageDown,
140    FileBrowserConfirm,
141    FileBrowserAcceptSuggestion,
142    FileBrowserGoParent,
143    FileBrowserUpdateFilter,
144    FileBrowserToggleHidden,
145
146    // Interactive replace actions
147    InteractiveReplaceKey(char),
148    CancelInteractiveReplace,
149
150    // Terminal mode actions
151    ToggleKeyboardCapture,
152    SendTerminalKey(crossterm::event::KeyCode, crossterm::event::KeyModifiers),
153    ExitTerminalMode {
154        explicit: bool,
155    },
156    EnterScrollbackMode,
157    EnterTerminalMode,
158
159    // Generic action execution
160    ExecuteAction(crate::input::keybindings::Action),
161
162    // Insert character (for prompts that need to update suggestions)
163    InsertCharAndUpdate(char),
164}
165
166/// Trait for elements that can handle input events.
167///
168/// Implementors should:
169/// 1. First delegate to `focused_child_mut()` if it exists
170/// 2. Handle keys relevant to this element
171/// 3. Return `Consumed` or `Ignored` appropriately
172/// 4. Modal elements should return `Consumed` for unhandled keys
173pub trait InputHandler {
174    /// Handle a key event. Returns whether the event was consumed.
175    fn handle_key_event(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult;
176
177    /// Get the currently focused child handler, if any.
178    fn focused_child(&self) -> Option<&dyn InputHandler> {
179        None
180    }
181
182    /// Get the currently focused child handler mutably, if any.
183    fn focused_child_mut(&mut self) -> Option<&mut dyn InputHandler> {
184        None
185    }
186
187    /// Whether this handler is modal (consumes all unhandled input).
188    fn is_modal(&self) -> bool {
189        false
190    }
191
192    /// Dispatch input through this handler and its children.
193    /// This is the main entry point - it handles the bubble-up logic.
194    fn dispatch_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
195        // First, let the deepest focused child try
196        if let Some(child) = self.focused_child_mut() {
197            let result = child.dispatch_input(event, ctx);
198            if result == InputResult::Consumed {
199                return InputResult::Consumed;
200            }
201        }
202
203        // Child didn't consume, try this handler
204        let result = self.handle_key_event(event, ctx);
205        if result == InputResult::Consumed {
206            return InputResult::Consumed;
207        }
208
209        // If modal, consume even if we didn't handle it
210        if self.is_modal() {
211            return InputResult::Consumed;
212        }
213
214        InputResult::Ignored
215    }
216}
217
218/// Helper to check for common key combinations.
219pub fn is_key(event: &KeyEvent, code: KeyCode) -> bool {
220    event.code == code && event.modifiers.is_empty()
221}
222
223pub fn is_key_with_ctrl(event: &KeyEvent, c: char) -> bool {
224    event.code == KeyCode::Char(c) && event.modifiers == KeyModifiers::CONTROL
225}
226
227pub fn is_key_with_shift(event: &KeyEvent, code: KeyCode) -> bool {
228    event.code == code && event.modifiers == KeyModifiers::SHIFT
229}
230
231pub fn is_key_with_alt(event: &KeyEvent, code: KeyCode) -> bool {
232    event.code == code && event.modifiers == KeyModifiers::ALT
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_input_result_or() {
241        assert_eq!(
242            InputResult::Consumed.or(InputResult::Consumed),
243            InputResult::Consumed
244        );
245        assert_eq!(
246            InputResult::Consumed.or(InputResult::Ignored),
247            InputResult::Consumed
248        );
249        assert_eq!(
250            InputResult::Ignored.or(InputResult::Consumed),
251            InputResult::Consumed
252        );
253        assert_eq!(
254            InputResult::Ignored.or(InputResult::Ignored),
255            InputResult::Ignored
256        );
257    }
258
259    #[test]
260    fn test_is_consumed() {
261        assert!(InputResult::Consumed.is_consumed());
262        assert!(!InputResult::Ignored.is_consumed());
263    }
264}