fresh/app/
input_dispatch.rs1use super::terminal_input::{should_enter_terminal_mode, TerminalModeInputHandler};
7use super::Editor;
8use crate::input::handler::{DeferredAction, InputContext, InputHandler, InputResult};
9use crate::input::keybindings::{Action, KeyContext};
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 pub fn dispatch_terminal_input(&mut self, event: &KeyEvent) -> Option<InputResult> {
23 let in_modal = self.is_prompting()
31 || self.global_popups.is_visible()
32 || self.active_state().popups.is_visible()
33 || self.menu_state.active_menu.is_some()
34 || self.settings_state.as_ref().is_some_and(|s| s.visible)
35 || self.calibration_wizard.is_some()
36 || self.keybinding_editor.is_some()
37 || self.floating_widget_panel.is_some();
38
39 if in_modal {
40 return None;
41 }
42
43 if self.active_window().terminal_mode {
45 if !self
50 .active_window()
51 .is_terminal_buffer(self.active_buffer())
52 {
53 self.active_window_mut().terminal_mode = false;
54 self.active_window_mut().key_context =
55 crate::input::keybindings::KeyContext::Normal;
56 return None; }
58 let bypass_action = {
71 let keybindings = self.keybindings.read().unwrap();
72 let action = keybindings.resolve(event, KeyContext::Normal);
73 if self
74 .command_registry
75 .read()
76 .unwrap()
77 .is_terminal_bypass_action(&action)
78 {
79 Some(action)
80 } else {
81 None
82 }
83 };
84 if let Some(action) = bypass_action {
85 if let Err(e) = self.handle_action(action) {
86 tracing::warn!("terminal-bypass action failed: {e}");
87 }
88 return Some(InputResult::Consumed);
89 }
90 let mut ctx = InputContext::new();
91 let keyboard_capture = self.active_window().keyboard_capture;
92 let keybindings = self.keybindings.read().unwrap();
93 let mut handler = TerminalModeInputHandler::new(keyboard_capture, &keybindings);
94 let result = handler.dispatch_input(event, &mut ctx);
95 drop(keybindings);
96 self.process_deferred_actions(ctx);
97 return Some(result);
98 }
99
100 if self
103 .active_window()
104 .is_terminal_buffer(self.active_buffer())
105 && should_enter_terminal_mode(event)
106 {
107 self.enter_terminal_mode();
108 self.active_window_mut()
110 .send_terminal_key(event.code, event.modifiers);
111 return Some(InputResult::Consumed);
112 }
113
114 None
115 }
116
117 pub fn dispatch_modal_input(&mut self, event: &KeyEvent) -> Option<InputResult> {
122 let mut ctx = InputContext::new();
123
124 if let Some(ref mut settings) = self.settings_state {
126 if settings.visible {
127 let result = settings.dispatch_input(event, &mut ctx);
128 self.process_deferred_actions(ctx);
129 return Some(result);
130 }
131 }
132
133 if self.keybinding_editor.is_some() {
135 let result = self.handle_keybinding_editor_input(event);
136 return Some(result);
137 }
138
139 if self.calibration_wizard.is_some() {
141 let result = self.handle_calibration_input(event);
142 return Some(result);
143 }
144
145 if self.menu_state.active_menu.is_some() {
147 let all_menus: Vec<crate::config::Menu> = self
148 .menus
149 .menus
150 .iter()
151 .chain(self.menu_state.plugin_menus.iter())
152 .cloned()
153 .collect();
154
155 let mut handler = MenuInputHandler::new(&mut self.menu_state, &all_menus);
156 let result = handler.dispatch_input(event, &mut ctx);
157 self.process_deferred_actions(ctx);
158 return Some(result);
159 }
160
161 if self.active_window().prompt.is_some() {
163 if event
167 .modifiers
168 .contains(crossterm::event::KeyModifiers::ALT)
169 {
170 if let crossterm::event::KeyCode::Char(_) = event.code {
171 let prompt_action = self.keybindings.read().unwrap().resolve_in_context_only(
172 event,
173 crate::input::keybindings::KeyContext::Prompt,
174 );
175 if let Some(action) = prompt_action {
176 if self.is_file_open_active() && self.handle_file_open_action(&action) {
178 return Some(InputResult::Consumed);
179 }
180 if let Err(e) = self.handle_action(action) {
182 tracing::warn!("Prompt action failed: {}", e);
183 }
184 return Some(InputResult::Consumed);
185 }
186 }
187 }
188
189 if self.is_file_open_active() {
191 let active_window_id = self.active_window;
192 let __win = self
193 .windows
194 .get_mut(&active_window_id)
195 .expect("active window present");
196 if let (Some(ref mut file_state), Some(ref mut prompt)) =
197 (&mut __win.file_open_state, &mut __win.prompt)
198 {
199 let mut handler = FileBrowserInputHandler::new(file_state, prompt);
200 let result = handler.dispatch_input(event, &mut ctx);
201 self.process_deferred_actions(ctx);
202 return Some(result);
203 }
204 }
205
206 use crate::view::prompt::PromptType;
208 let is_query_replace_confirm = self
209 .active_window()
210 .prompt
211 .as_ref()
212 .is_some_and(|p| p.prompt_type == PromptType::QueryReplaceConfirm);
213 if is_query_replace_confirm {
214 let mut handler = QueryReplaceConfirmInputHandler::new();
215 let result = handler.dispatch_input(event, &mut ctx);
216 self.process_deferred_actions(ctx);
217 return Some(result);
218 }
219
220 if let Some(ref mut prompt) = self.active_window_mut().prompt {
221 let result = prompt.dispatch_input(event, &mut ctx);
222 if result != InputResult::Ignored {
225 self.process_deferred_actions(ctx);
226 return Some(result);
227 }
228 }
229 }
230
231 if self.popups_capture_keys() {
237 if let Some(action) = self.resolve_completion_popup_action(event) {
242 self.process_deferred_actions(ctx);
243 if let Err(e) = self.handle_action(action) {
244 tracing::warn!("Completion popup action failed: {}", e);
245 }
246 return Some(InputResult::Consumed);
247 }
248
249 if self.global_popups.is_visible() {
253 let result = self.global_popups.dispatch_input(event, &mut ctx);
254 self.process_deferred_actions(ctx);
255 if result != InputResult::Ignored {
256 return Some(result);
257 }
258 return None;
261 }
262
263 if self.active_state().popups.is_visible() {
265 let result = self
266 .active_state_mut()
267 .popups
268 .dispatch_input(event, &mut ctx);
269 self.process_deferred_actions(ctx);
270 if result != InputResult::Ignored {
275 return Some(result);
276 }
277 }
278 }
279
280 None
281 }
282
283 pub fn process_deferred_actions(&mut self, ctx: InputContext) {
285 if let Some(msg) = ctx.status_message {
287 self.set_status_message(msg);
288 }
289
290 for action in ctx.deferred_actions {
292 if let Err(e) = self.execute_deferred_action(action) {
293 self.set_status_message(
294 t!("error.deferred_action", error = e.to_string()).to_string(),
295 );
296 }
297 }
298 }
299
300 fn execute_deferred_action(&mut self, action: DeferredAction) -> AnyhowResult<()> {
302 match action {
303 DeferredAction::CloseSettings { save } => {
305 if save {
306 self.save_settings();
307 }
308 self.close_settings(false);
309 }
310 DeferredAction::PasteToSettings => {
311 if let Some(text) = self.clipboard.paste() {
312 if !text.is_empty() {
313 if let Some(settings) = &mut self.settings_state {
314 if let Some(dialog) = settings.entry_dialog_mut() {
315 dialog.insert_str(&text);
316 }
317 }
318 }
319 }
320 }
321 DeferredAction::OpenConfigFile { layer } => {
322 self.open_config_file(layer)?;
323 }
324
325 DeferredAction::CloseMenu => {
327 self.close_menu_with_auto_hide();
328 }
329 DeferredAction::ExecuteMenuAction { action, args } => {
330 if let Some(kb_action) = self.menu_action_to_action(&action, args) {
332 self.handle_action(kb_action)?;
333 }
334 }
335
336 DeferredAction::ClosePrompt => {
338 self.cancel_prompt();
339 }
340 DeferredAction::ConfirmPrompt => {
341 self.handle_action(Action::PromptConfirm)?;
342 }
343 DeferredAction::UpdatePromptSuggestions => {
344 self.update_prompt_suggestions();
345 }
346 DeferredAction::PromptHistoryPrev => {
347 self.prompt_history_prev();
348 }
349 DeferredAction::PromptHistoryNext => {
350 self.prompt_history_next();
351 }
352 DeferredAction::PreviewThemeFromPrompt => {
353 if let Some(prompt) = &self.active_window_mut().prompt {
354 if matches!(
355 prompt.prompt_type,
356 crate::view::prompt::PromptType::SelectTheme { .. }
357 ) {
358 let theme_name = prompt.input.clone();
359 self.preview_theme(&theme_name);
360 }
361 }
362 }
363 DeferredAction::PromptSelectionChanged { selected_index } => {
364 let plugin_custom_type =
366 self.active_window()
367 .prompt
368 .as_ref()
369 .and_then(|p| match &p.prompt_type {
370 crate::view::prompt::PromptType::Plugin { custom_type } => {
371 Some(custom_type.clone())
372 }
373 _ => None,
374 });
375 if let Some(custom_type) = plugin_custom_type {
376 self.plugin_manager.read().unwrap().run_hook(
377 "prompt_selection_changed",
378 crate::services::plugins::hooks::HookArgs::PromptSelectionChanged {
379 prompt_type: custom_type.clone(),
380 selected_index,
381 },
382 );
383 }
384 }
385
386 DeferredAction::ClosePopup => {
388 self.handle_popup_cancel();
394 }
395 DeferredAction::ConfirmPopup => {
396 self.handle_action(Action::PopupConfirm)?;
397 }
398 DeferredAction::PopupTypeChar(c) => {
399 self.handle_popup_type_char(c);
400 }
401 DeferredAction::PopupBackspace => {
402 self.handle_popup_backspace();
403 }
404 DeferredAction::CopyToClipboard(text) => {
405 self.clipboard.copy(text);
406 self.set_status_message(t!("clipboard.copied").to_string());
407 }
408
409 DeferredAction::ExecuteAction(kb_action) => {
411 self.handle_action(kb_action)?;
412 }
413
414 DeferredAction::InsertCharAndUpdate(c) => {
416 if let Some(ref mut prompt) = self.active_window_mut().prompt {
417 prompt.insert_char(c);
418 }
419 self.update_prompt_suggestions();
420 }
421
422 DeferredAction::FileBrowserSelectPrev => {
424 if let Some(state) = &mut self.active_window_mut().file_open_state {
425 state.select_prev();
426 }
427 }
428 DeferredAction::FileBrowserSelectNext => {
429 if let Some(state) = &mut self.active_window_mut().file_open_state {
430 state.select_next();
431 }
432 }
433 DeferredAction::FileBrowserPageUp => {
434 if let Some(state) = &mut self.active_window_mut().file_open_state {
435 state.page_up(10);
436 }
437 }
438 DeferredAction::FileBrowserPageDown => {
439 if let Some(state) = &mut self.active_window_mut().file_open_state {
440 state.page_down(10);
441 }
442 }
443 DeferredAction::FileBrowserConfirm => {
444 self.handle_file_open_action(&Action::PromptConfirm);
447 }
448 DeferredAction::FileBrowserAcceptSuggestion => {
449 self.handle_file_open_action(&Action::PromptAcceptSuggestion);
450 }
451 DeferredAction::FileBrowserGoParent => {
452 let parent = self
454 .active_window_mut()
455 .file_open_state
456 .as_ref()
457 .and_then(|s| s.current_dir.parent())
458 .map(|p| p.to_path_buf());
459 if let Some(parent_path) = parent {
460 self.load_file_open_directory(parent_path);
461 }
462 }
463 DeferredAction::FileBrowserUpdateFilter => {
464 self.update_file_open_filter();
465 }
466 DeferredAction::FileBrowserToggleHidden => {
467 self.file_open_toggle_hidden();
468 }
469
470 DeferredAction::InteractiveReplaceKey(c) => {
472 self.handle_interactive_replace_key(c)?;
473 }
474 DeferredAction::CancelInteractiveReplace => {
475 self.cancel_prompt();
476 self.active_window_mut().interactive_replace_state = None;
477 }
478
479 DeferredAction::ToggleKeyboardCapture => {
481 self.active_window_mut().keyboard_capture =
482 !self.active_window_mut().keyboard_capture;
483 if self.active_window_mut().keyboard_capture {
484 self.set_status_message(
485 "Keyboard capture ON - all keys go to terminal (F9 to toggle)".to_string(),
486 );
487 } else {
488 self.set_status_message(
489 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
490 );
491 }
492 }
493 DeferredAction::SendTerminalKey(code, modifiers) => {
494 self.active_window_mut().send_terminal_key(code, modifiers);
495 }
496 DeferredAction::SendTerminalMouse {
497 col,
498 row,
499 kind,
500 modifiers,
501 } => {
502 self.active_window_mut()
503 .send_terminal_mouse(col, row, kind, modifiers);
504 }
505 DeferredAction::ExitTerminalMode { explicit } => {
506 self.active_window_mut().terminal_mode = false;
507 self.active_window_mut().key_context =
508 crate::input::keybindings::KeyContext::Normal;
509 if explicit {
510 let buf = self.active_buffer();
512 self.active_window_mut().terminal_mode_resume.remove(&buf);
513 {
514 let __b = self.active_buffer();
515 self.active_window_mut().sync_terminal_to_buffer(__b);
516 };
517 self.set_status_message(
518 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
519 );
520 }
521 }
522 DeferredAction::EnterScrollbackMode => {
523 self.active_window_mut().terminal_mode = false;
524 self.active_window_mut().key_context =
525 crate::input::keybindings::KeyContext::Normal;
526 {
527 let __b = self.active_buffer();
528 self.active_window_mut().sync_terminal_to_buffer(__b);
529 };
530 self.set_status_message(
531 "Scrollback mode - use PageUp/Down to scroll (Ctrl+Space to resume)"
532 .to_string(),
533 );
534 self.handle_action(Action::MovePageUp)?;
536 }
537 DeferredAction::EnterTerminalMode => {
538 self.enter_terminal_mode();
539 }
540 }
541
542 Ok(())
543 }
544
545 fn menu_action_to_action(
547 &self,
548 action_name: &str,
549 args: std::collections::HashMap<String, serde_json::Value>,
550 ) -> Option<Action> {
551 if let Some(action) = Action::from_str(action_name, &args) {
553 return Some(action);
554 }
555
556 Some(Action::PluginAction(action_name.to_string()))
558 }
559
560 fn prompt_history_prev(&mut self) {
562 let prompt_info = self
564 .active_window()
565 .prompt
566 .as_ref()
567 .map(|p| (p.prompt_type.clone(), p.input.clone()));
568
569 if let Some((prompt_type, current_input)) = prompt_info {
570 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
572 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
573 if let Some(entry) = history.navigate_prev(¤t_input) {
574 if let Some(ref mut prompt) = self.active_window_mut().prompt {
575 prompt.set_input(entry);
576 }
577 }
578 }
579 }
580 }
581 }
582
583 fn prompt_history_next(&mut self) {
585 let prompt_type = self
586 .active_window()
587 .prompt
588 .as_ref()
589 .map(|p| p.prompt_type.clone());
590
591 if let Some(prompt_type) = prompt_type {
592 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
594 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
595 if let Some(entry) = history.navigate_next() {
596 if let Some(ref mut prompt) = self.active_window_mut().prompt {
597 prompt.set_input(entry);
598 }
599 }
600 }
601 }
602 }
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609
610 #[test]
611 fn test_deferred_action_close_menu() {
612 let action = DeferredAction::CloseMenu;
615 assert!(matches!(action, DeferredAction::CloseMenu));
616 }
617
618 #[test]
619 fn test_deferred_action_execute_menu_action() {
620 let action = DeferredAction::ExecuteMenuAction {
621 action: "save".to_string(),
622 args: std::collections::HashMap::new(),
623 };
624 if let DeferredAction::ExecuteMenuAction { action: name, .. } = action {
625 assert_eq!(name, "save");
626 } else {
627 panic!("Expected ExecuteMenuAction");
628 }
629 }
630}