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}