Skip to main content

fresh/app/
input_dispatch.rs

1//! Input dispatch using the hierarchical InputHandler system.
2//!
3//! This module provides the bridge between Editor and the InputHandler trait,
4//! dispatching input to modal components and processing deferred actions.
5
6use super::terminal_input::{should_enter_terminal_mode, TerminalModeInputHandler};
7use super::Editor;
8use crate::input::handler::{DeferredAction, InputContext, InputHandler, InputResult};
9use crate::input::keybindings::Action;
10use crate::view::file_browser_input::FileBrowserInputHandler;
11use crate::view::query_replace_input::QueryReplaceConfirmInputHandler;
12use crate::view::ui::MenuInputHandler;
13use anyhow::Result as AnyhowResult;
14use crossterm::event::KeyEvent;
15use rust_i18n::t;
16
17impl Editor {
18    /// Dispatch input when in terminal mode.
19    ///
20    /// Returns `Some(InputResult)` if terminal mode handled the input,
21    /// `None` if not in terminal mode or if a modal is active.
22    pub fn dispatch_terminal_input(&mut self, event: &KeyEvent) -> Option<InputResult> {
23        // Skip if we're in a prompt/popup (those need to handle keys normally)
24        let in_modal = self.is_prompting()
25            || self.active_state().popups.is_visible()
26            || self.menu_state.active_menu.is_some()
27            || self.settings_state.as_ref().is_some_and(|s| s.visible)
28            || self.calibration_wizard.is_some();
29
30        if in_modal {
31            return None;
32        }
33
34        // Handle terminal mode input
35        if self.terminal_mode {
36            let mut ctx = InputContext::new();
37            let mut handler =
38                TerminalModeInputHandler::new(self.keyboard_capture, &self.keybindings);
39            let result = handler.dispatch_input(event, &mut ctx);
40            self.process_deferred_actions(ctx);
41            return Some(result);
42        }
43
44        // Check for keys that should re-enter terminal mode from read-only view
45        if self.is_terminal_buffer(self.active_buffer()) && should_enter_terminal_mode(event) {
46            self.enter_terminal_mode();
47            return Some(InputResult::Consumed);
48        }
49
50        None
51    }
52
53    /// Dispatch input to the appropriate modal handler.
54    ///
55    /// Returns `Some(InputResult)` if a modal handled the input,
56    /// `None` if no modal is active and input should be handled normally.
57    pub fn dispatch_modal_input(&mut self, event: &KeyEvent) -> Option<InputResult> {
58        let mut ctx = InputContext::new();
59
60        // Settings has highest priority
61        if let Some(ref mut settings) = self.settings_state {
62            if settings.visible {
63                let result = settings.dispatch_input(event, &mut ctx);
64                self.process_deferred_actions(ctx);
65                return Some(result);
66            }
67        }
68
69        // Calibration wizard is next (modal, blocks all other input)
70        if self.calibration_wizard.is_some() {
71            let result = self.handle_calibration_input(event);
72            return Some(result);
73        }
74
75        // Menu is next
76        if self.menu_state.active_menu.is_some() {
77            let all_menus: Vec<crate::config::Menu> = self
78                .menus
79                .menus
80                .iter()
81                .chain(self.menu_state.plugin_menus.iter())
82                .cloned()
83                .collect();
84
85            let mut handler = MenuInputHandler::new(&mut self.menu_state, &all_menus);
86            let result = handler.dispatch_input(event, &mut ctx);
87            self.process_deferred_actions(ctx);
88            return Some(result);
89        }
90
91        // Prompt is next
92        if self.prompt.is_some() {
93            // Check for Alt+key keybindings first (before prompt consumes them as modal)
94            if event
95                .modifiers
96                .contains(crossterm::event::KeyModifiers::ALT)
97            {
98                if let crossterm::event::KeyCode::Char(_) = event.code {
99                    let action = self
100                        .keybindings
101                        .resolve(event, crate::input::keybindings::KeyContext::Prompt);
102                    if !matches!(action, Action::None) {
103                        // Handle the action (ignore errors for modal context)
104                        let _ = self.handle_action(action);
105                        return Some(InputResult::Consumed);
106                    }
107                }
108            }
109
110            // File browser prompts use FileBrowserInputHandler
111            if self.is_file_open_active() {
112                if let (Some(ref mut file_state), Some(ref mut prompt)) =
113                    (&mut self.file_open_state, &mut self.prompt)
114                {
115                    let mut handler = FileBrowserInputHandler::new(file_state, prompt);
116                    let result = handler.dispatch_input(event, &mut ctx);
117                    self.process_deferred_actions(ctx);
118                    return Some(result);
119                }
120            }
121
122            // QueryReplaceConfirm prompts use QueryReplaceConfirmInputHandler
123            use crate::view::prompt::PromptType;
124            let is_query_replace_confirm = self
125                .prompt
126                .as_ref()
127                .is_some_and(|p| p.prompt_type == PromptType::QueryReplaceConfirm);
128            if is_query_replace_confirm {
129                let mut handler = QueryReplaceConfirmInputHandler::new();
130                let result = handler.dispatch_input(event, &mut ctx);
131                self.process_deferred_actions(ctx);
132                return Some(result);
133            }
134
135            if let Some(ref mut prompt) = self.prompt {
136                let result = prompt.dispatch_input(event, &mut ctx);
137                // Only return and process deferred actions if the prompt handled the input
138                // If Ignored, fall through to check global keybindings
139                if result != InputResult::Ignored {
140                    self.process_deferred_actions(ctx);
141                    return Some(result);
142                }
143            }
144        }
145
146        // Popup is next
147        if self.active_state().popups.is_visible() {
148            let result = self
149                .active_state_mut()
150                .popups
151                .dispatch_input(event, &mut ctx);
152            self.process_deferred_actions(ctx);
153            return Some(result);
154        }
155
156        None
157    }
158
159    /// Process deferred actions collected during input handling.
160    pub fn process_deferred_actions(&mut self, ctx: InputContext) {
161        // Set status message if provided
162        if let Some(msg) = ctx.status_message {
163            self.set_status_message(msg);
164        }
165
166        // Process each deferred action
167        for action in ctx.deferred_actions {
168            if let Err(e) = self.execute_deferred_action(action) {
169                self.set_status_message(
170                    t!("error.deferred_action", error = e.to_string()).to_string(),
171                );
172            }
173        }
174    }
175
176    /// Execute a single deferred action.
177    fn execute_deferred_action(&mut self, action: DeferredAction) -> AnyhowResult<()> {
178        match action {
179            // Settings actions
180            DeferredAction::CloseSettings { save } => {
181                if save {
182                    self.save_settings();
183                }
184                self.close_settings(false);
185            }
186            DeferredAction::PasteToSettings => {
187                if let Some(text) = self.clipboard.paste() {
188                    if !text.is_empty() {
189                        if let Some(settings) = &mut self.settings_state {
190                            if let Some(dialog) = settings.entry_dialog_mut() {
191                                dialog.insert_str(&text);
192                            }
193                        }
194                    }
195                }
196            }
197            DeferredAction::OpenConfigFile { layer } => {
198                self.open_config_file(layer)?;
199            }
200
201            // Menu actions
202            DeferredAction::CloseMenu => {
203                self.close_menu_with_auto_hide();
204            }
205            DeferredAction::ExecuteMenuAction { action, args } => {
206                // Convert menu action to keybinding Action and execute
207                if let Some(kb_action) = self.menu_action_to_action(&action, args) {
208                    self.handle_action(kb_action)?;
209                }
210            }
211
212            // Prompt actions
213            DeferredAction::ClosePrompt => {
214                self.cancel_prompt();
215            }
216            DeferredAction::ConfirmPrompt => {
217                self.handle_action(Action::PromptConfirm)?;
218            }
219            DeferredAction::UpdatePromptSuggestions => {
220                self.update_prompt_suggestions();
221            }
222            DeferredAction::PromptHistoryPrev => {
223                self.prompt_history_prev();
224            }
225            DeferredAction::PromptHistoryNext => {
226                self.prompt_history_next();
227            }
228            DeferredAction::PreviewThemeFromPrompt => {
229                if let Some(prompt) = &self.prompt {
230                    if matches!(
231                        prompt.prompt_type,
232                        crate::view::prompt::PromptType::SelectTheme { .. }
233                    ) {
234                        let theme_name = prompt.input.clone();
235                        self.preview_theme(&theme_name);
236                    }
237                }
238            }
239            DeferredAction::PromptSelectionChanged { selected_index } => {
240                // Fire hook for plugin prompts so they can update live preview
241                if let Some(prompt) = &self.prompt {
242                    if let crate::view::prompt::PromptType::Plugin { custom_type } =
243                        &prompt.prompt_type
244                    {
245                        self.plugin_manager.run_hook(
246                            "prompt_selection_changed",
247                            crate::services::plugins::hooks::HookArgs::PromptSelectionChanged {
248                                prompt_type: custom_type.clone(),
249                                selected_index,
250                            },
251                        );
252                    }
253                }
254            }
255
256            // Popup actions
257            DeferredAction::ClosePopup => {
258                self.hide_popup();
259            }
260            DeferredAction::ConfirmPopup => {
261                self.handle_action(Action::PopupConfirm)?;
262            }
263            DeferredAction::CompletionEnterKey => {
264                use crate::config::AcceptSuggestionOnEnter;
265                match self.config.editor.accept_suggestion_on_enter {
266                    AcceptSuggestionOnEnter::On => {
267                        // Enter always accepts
268                        self.handle_action(Action::PopupConfirm)?;
269                    }
270                    AcceptSuggestionOnEnter::Off => {
271                        // Enter inserts newline - close popup and insert newline
272                        self.hide_popup();
273                        self.handle_action(Action::InsertNewline)?;
274                    }
275                    AcceptSuggestionOnEnter::Smart => {
276                        // Accept if completion differs from typed text
277                        // For now, we check if there's a selected item with data
278                        // that differs from what's in the buffer
279                        let should_accept = self
280                            .active_state()
281                            .popups
282                            .top()
283                            .and_then(|p| p.selected_item())
284                            .map(|item| {
285                                // If there's selection data, accept the completion
286                                item.data.is_some()
287                            })
288                            .unwrap_or(false);
289
290                        if should_accept {
291                            self.handle_action(Action::PopupConfirm)?;
292                        } else {
293                            self.hide_popup();
294                            self.handle_action(Action::InsertNewline)?;
295                        }
296                    }
297                }
298            }
299            DeferredAction::PopupTypeChar(c) => {
300                self.handle_popup_type_char(c);
301            }
302            DeferredAction::PopupBackspace => {
303                self.handle_popup_backspace();
304            }
305            DeferredAction::CopyToClipboard(text) => {
306                self.clipboard.copy(text);
307                self.set_status_message(t!("clipboard.copied").to_string());
308            }
309
310            // Generic action execution
311            DeferredAction::ExecuteAction(kb_action) => {
312                self.handle_action(kb_action)?;
313            }
314
315            // Character insertion with suggestion update
316            DeferredAction::InsertCharAndUpdate(c) => {
317                if let Some(ref mut prompt) = self.prompt {
318                    prompt.insert_char(c);
319                }
320                self.update_prompt_suggestions();
321            }
322
323            // File browser actions
324            DeferredAction::FileBrowserSelectPrev => {
325                if let Some(state) = &mut self.file_open_state {
326                    state.select_prev();
327                }
328            }
329            DeferredAction::FileBrowserSelectNext => {
330                if let Some(state) = &mut self.file_open_state {
331                    state.select_next();
332                }
333            }
334            DeferredAction::FileBrowserPageUp => {
335                if let Some(state) = &mut self.file_open_state {
336                    state.page_up(10);
337                }
338            }
339            DeferredAction::FileBrowserPageDown => {
340                if let Some(state) = &mut self.file_open_state {
341                    state.page_down(10);
342                }
343            }
344            DeferredAction::FileBrowserConfirm => {
345                // Must call handle_file_open_action directly to get proper
346                // file browser behavior (e.g., project switch triggering restart)
347                self.handle_file_open_action(&Action::PromptConfirm);
348            }
349            DeferredAction::FileBrowserAcceptSuggestion => {
350                self.handle_file_open_action(&Action::PromptAcceptSuggestion);
351            }
352            DeferredAction::FileBrowserGoParent => {
353                // Navigate to parent directory
354                let parent = self
355                    .file_open_state
356                    .as_ref()
357                    .and_then(|s| s.current_dir.parent())
358                    .map(|p| p.to_path_buf());
359                if let Some(parent_path) = parent {
360                    self.load_file_open_directory(parent_path);
361                }
362            }
363            DeferredAction::FileBrowserUpdateFilter => {
364                self.update_file_open_filter();
365            }
366            DeferredAction::FileBrowserToggleHidden => {
367                self.file_open_toggle_hidden();
368            }
369
370            // Interactive replace actions
371            DeferredAction::InteractiveReplaceKey(c) => {
372                self.handle_interactive_replace_key(c)?;
373            }
374            DeferredAction::CancelInteractiveReplace => {
375                self.cancel_prompt();
376                self.interactive_replace_state = None;
377            }
378
379            // Terminal mode actions
380            DeferredAction::ToggleKeyboardCapture => {
381                self.keyboard_capture = !self.keyboard_capture;
382                if self.keyboard_capture {
383                    self.set_status_message(
384                        "Keyboard capture ON - all keys go to terminal (F9 to toggle)".to_string(),
385                    );
386                } else {
387                    self.set_status_message(
388                        "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
389                    );
390                }
391            }
392            DeferredAction::SendTerminalKey(code, modifiers) => {
393                self.send_terminal_key(code, modifiers);
394            }
395            DeferredAction::SendTerminalMouse {
396                col,
397                row,
398                kind,
399                modifiers,
400            } => {
401                self.send_terminal_mouse(col, row, kind, modifiers);
402            }
403            DeferredAction::ExitTerminalMode { explicit } => {
404                self.terminal_mode = false;
405                self.key_context = crate::input::keybindings::KeyContext::Normal;
406                if explicit {
407                    // User explicitly exited - don't auto-resume when switching back
408                    self.terminal_mode_resume.remove(&self.active_buffer());
409                    self.sync_terminal_to_buffer(self.active_buffer());
410                    self.set_status_message(
411                        "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
412                    );
413                }
414            }
415            DeferredAction::EnterScrollbackMode => {
416                self.terminal_mode = false;
417                self.key_context = crate::input::keybindings::KeyContext::Normal;
418                self.sync_terminal_to_buffer(self.active_buffer());
419                self.set_status_message(
420                    "Scrollback mode - use PageUp/Down to scroll (Ctrl+Space to resume)"
421                        .to_string(),
422                );
423                // Scroll up using normal buffer scrolling
424                self.handle_action(Action::MovePageUp)?;
425            }
426            DeferredAction::EnterTerminalMode => {
427                self.enter_terminal_mode();
428            }
429        }
430
431        Ok(())
432    }
433
434    /// Convert a menu action string to a keybinding Action.
435    fn menu_action_to_action(
436        &self,
437        action_name: &str,
438        args: std::collections::HashMap<String, serde_json::Value>,
439    ) -> Option<Action> {
440        // Try to parse as a built-in action first
441        if let Some(action) = Action::from_str(action_name, &args) {
442            return Some(action);
443        }
444
445        // Otherwise treat as a plugin action
446        Some(Action::PluginAction(action_name.to_string()))
447    }
448
449    /// Navigate to previous history entry in prompt.
450    fn prompt_history_prev(&mut self) {
451        // Get the prompt type and current input
452        let prompt_info = self
453            .prompt
454            .as_ref()
455            .map(|p| (p.prompt_type.clone(), p.input.clone()));
456
457        if let Some((prompt_type, current_input)) = prompt_info {
458            // Get the history key for this prompt type
459            if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
460                if let Some(history) = self.prompt_histories.get_mut(&key) {
461                    if let Some(entry) = history.navigate_prev(&current_input) {
462                        if let Some(ref mut prompt) = self.prompt {
463                            prompt.set_input(entry);
464                        }
465                    }
466                }
467            }
468        }
469    }
470
471    /// Navigate to next history entry in prompt.
472    fn prompt_history_next(&mut self) {
473        let prompt_type = self.prompt.as_ref().map(|p| p.prompt_type.clone());
474
475        if let Some(prompt_type) = prompt_type {
476            // Get the history key for this prompt type
477            if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
478                if let Some(history) = self.prompt_histories.get_mut(&key) {
479                    if let Some(entry) = history.navigate_next() {
480                        if let Some(ref mut prompt) = self.prompt {
481                            prompt.set_input(entry);
482                        }
483                    }
484                }
485            }
486        }
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493
494    #[test]
495    fn test_deferred_action_close_menu() {
496        // This is a basic structure test - full integration tests
497        // would require a complete Editor setup
498        let action = DeferredAction::CloseMenu;
499        assert!(matches!(action, DeferredAction::CloseMenu));
500    }
501
502    #[test]
503    fn test_deferred_action_execute_menu_action() {
504        let action = DeferredAction::ExecuteMenuAction {
505            action: "save".to_string(),
506            args: std::collections::HashMap::new(),
507        };
508        if let DeferredAction::ExecuteMenuAction { action: name, .. } = action {
509            assert_eq!(name, "save");
510        } else {
511            panic!("Expected ExecuteMenuAction");
512        }
513    }
514}