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}