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 if matches!(
67 self.active_window().key_context,
68 crate::input::keybindings::KeyContext::FileExplorer
69 ) {
70 return None;
71 }
72 let bypass_action = {
85 let keybindings = self.keybindings.read().unwrap();
86 let action = keybindings.resolve(event, KeyContext::Normal);
87 if self
88 .command_registry
89 .read()
90 .unwrap()
91 .is_terminal_bypass_action(&action)
92 {
93 Some(action)
94 } else {
95 None
96 }
97 };
98 if let Some(action) = bypass_action {
99 if let Err(e) = self.handle_action(action) {
100 tracing::warn!("terminal-bypass action failed: {e}");
101 }
102 return Some(InputResult::Consumed);
103 }
104 let mut ctx = InputContext::new();
105 let keyboard_capture = self.active_window().keyboard_capture;
106 let keybindings = self.keybindings.read().unwrap();
107 let mut handler = TerminalModeInputHandler::new(keyboard_capture, &keybindings);
108 let result = handler.dispatch_input(event, &mut ctx);
109 drop(keybindings);
110 self.process_deferred_actions(ctx);
111 return Some(result);
112 }
113
114 if self
117 .active_window()
118 .is_terminal_buffer(self.active_buffer())
119 && should_enter_terminal_mode(event)
120 {
121 self.enter_terminal_mode();
122 self.active_window_mut()
124 .send_terminal_key(event.code, event.modifiers);
125 return Some(InputResult::Consumed);
126 }
127
128 None
129 }
130
131 pub fn dispatch_modal_input(&mut self, event: &KeyEvent) -> Option<InputResult> {
136 let mut ctx = InputContext::new();
137
138 if let Some(ref mut settings) = self.settings_state {
140 if settings.visible {
141 let result = settings.dispatch_input(event, &mut ctx);
142 self.process_deferred_actions(ctx);
143 return Some(result);
144 }
145 }
146
147 if self.keybinding_editor.is_some() {
149 let result = self.handle_keybinding_editor_input(event);
150 return Some(result);
151 }
152
153 if self.calibration_wizard.is_some() {
155 let result = self.handle_calibration_input(event);
156 return Some(result);
157 }
158
159 if self.menu_state.active_menu.is_some() {
161 let all_menus: Vec<crate::config::Menu> = self
162 .menus
163 .menus
164 .iter()
165 .chain(self.menu_state.plugin_menus.iter())
166 .cloned()
167 .collect();
168
169 let mut handler = MenuInputHandler::new(&mut self.menu_state, &all_menus);
170 let result = handler.dispatch_input(event, &mut ctx);
171 self.process_deferred_actions(ctx);
172 return Some(result);
173 }
174
175 if self.active_window().prompt.is_some() {
177 if event
181 .modifiers
182 .contains(crossterm::event::KeyModifiers::ALT)
183 {
184 if let crossterm::event::KeyCode::Char(_) = event.code {
185 let prompt_action = self.keybindings.read().unwrap().resolve_in_context_only(
186 event,
187 crate::input::keybindings::KeyContext::Prompt,
188 );
189 if let Some(action) = prompt_action {
190 if self.is_file_open_active() && self.handle_file_open_action(&action) {
192 return Some(InputResult::Consumed);
193 }
194 if let Err(e) = self.handle_action(action) {
196 tracing::warn!("Prompt action failed: {}", e);
197 }
198 return Some(InputResult::Consumed);
199 }
200 }
201 }
202
203 if self.is_file_open_active() {
205 let active_window_id = self.active_window;
206 let __win = self
207 .windows
208 .get_mut(&active_window_id)
209 .expect("active window present");
210 if let (Some(ref mut file_state), Some(ref mut prompt)) =
211 (&mut __win.file_open_state, &mut __win.prompt)
212 {
213 let mut handler = FileBrowserInputHandler::new(file_state, prompt);
214 let result = handler.dispatch_input(event, &mut ctx);
215 self.process_deferred_actions(ctx);
216 return Some(result);
217 }
218 }
219
220 use crate::view::prompt::PromptType;
222 let is_query_replace_confirm = self
223 .active_window()
224 .prompt
225 .as_ref()
226 .is_some_and(|p| p.prompt_type == PromptType::QueryReplaceConfirm);
227 if is_query_replace_confirm {
228 let mut handler = QueryReplaceConfirmInputHandler::new();
229 let result = handler.dispatch_input(event, &mut ctx);
230 self.process_deferred_actions(ctx);
231 return Some(result);
232 }
233
234 if let Some(ref mut prompt) = self.active_window_mut().prompt {
235 let result = prompt.dispatch_input(event, &mut ctx);
236 if result != InputResult::Ignored {
239 self.process_deferred_actions(ctx);
240 return Some(result);
241 }
242 }
243 }
244
245 if self.popups_capture_keys() {
251 if let Some(action) = self.resolve_completion_popup_action(event) {
256 self.process_deferred_actions(ctx);
257 if let Err(e) = self.handle_action(action) {
258 tracing::warn!("Completion popup action failed: {}", e);
259 }
260 return Some(InputResult::Consumed);
261 }
262
263 if self.global_popups.top().is_some_and(|p| {
268 matches!(
269 p.resolver,
270 crate::view::popup::PopupResolver::WorkspaceTrust
271 )
272 }) {
273 if let Some(result) = self.handle_workspace_trust_key(event) {
274 return Some(result);
275 }
276 }
277
278 if self.global_popups.is_visible() {
282 let result = self.global_popups.dispatch_input(event, &mut ctx);
283 self.process_deferred_actions(ctx);
284 if result != InputResult::Ignored {
285 return Some(result);
286 }
287 return None;
290 }
291
292 if self.active_state().popups.is_visible() {
294 let result = self
295 .active_state_mut()
296 .popups
297 .dispatch_input(event, &mut ctx);
298 self.process_deferred_actions(ctx);
299 if result != InputResult::Ignored {
304 return Some(result);
305 }
306 }
307 }
308
309 None
310 }
311
312 pub fn process_deferred_actions(&mut self, ctx: InputContext) {
314 if let Some(msg) = ctx.status_message {
316 self.set_status_message(msg);
317 }
318
319 for action in ctx.deferred_actions {
321 if let Err(e) = self.execute_deferred_action(action) {
322 self.set_status_message(
323 t!("error.deferred_action", error = e.to_string()).to_string(),
324 );
325 }
326 }
327 }
328
329 fn execute_deferred_action(&mut self, action: DeferredAction) -> AnyhowResult<()> {
331 match action {
332 DeferredAction::CloseSettings { save } => {
334 if save {
335 self.save_settings();
336 }
337 self.close_settings(false);
338 }
339 DeferredAction::PasteToSettings => {
340 if let Some(text) = self.clipboard.paste() {
341 if !text.is_empty() {
342 if let Some(settings) = &mut self.settings_state {
343 if let Some(dialog) = settings.entry_dialog_mut() {
344 dialog.insert_str(&text);
345 }
346 }
347 }
348 }
349 }
350 DeferredAction::OpenConfigFile { layer } => {
351 self.open_config_file(layer)?;
352 }
353
354 DeferredAction::CloseMenu => {
356 self.close_menu_with_auto_hide();
357 }
358 DeferredAction::ExecuteMenuAction { action, args } => {
359 if let Some(kb_action) = self.menu_action_to_action(&action, args) {
361 self.handle_action(kb_action)?;
362 }
363 }
364
365 DeferredAction::ClosePrompt => {
367 self.cancel_prompt();
368 }
369 DeferredAction::ConfirmPrompt => {
370 self.handle_action(Action::PromptConfirm)?;
371 }
372 DeferredAction::UpdatePromptSuggestions => {
373 self.update_prompt_suggestions();
374 }
375 DeferredAction::PromptHistoryPrev => {
376 self.prompt_history_prev();
377 }
378 DeferredAction::PromptHistoryNext => {
379 self.prompt_history_next();
380 }
381 DeferredAction::PreviewThemeFromPrompt => {
382 if let Some(prompt) = &self.active_window_mut().prompt {
383 if matches!(
384 prompt.prompt_type,
385 crate::view::prompt::PromptType::SelectTheme { .. }
386 ) {
387 let theme_name = prompt.input.clone();
388 self.preview_theme(&theme_name);
389 }
390 }
391 }
392 DeferredAction::PromptSelectionChanged { selected_index } => {
393 let plugin_custom_type =
395 self.active_window()
396 .prompt
397 .as_ref()
398 .and_then(|p| match &p.prompt_type {
399 crate::view::prompt::PromptType::Plugin { custom_type } => {
400 Some(custom_type.clone())
401 }
402 _ => None,
403 });
404 if let Some(custom_type) = plugin_custom_type {
405 self.plugin_manager.read().unwrap().run_hook(
406 "prompt_selection_changed",
407 crate::services::plugins::hooks::HookArgs::PromptSelectionChanged {
408 prompt_type: custom_type.clone(),
409 selected_index,
410 },
411 );
412 }
413 }
414
415 DeferredAction::ClosePopup => {
417 self.handle_popup_cancel();
423 }
424 DeferredAction::ConfirmPopup => {
425 self.handle_action(Action::PopupConfirm)?;
426 }
427 DeferredAction::PopupTypeChar(c) => {
428 self.handle_popup_type_char(c);
429 }
430 DeferredAction::PopupBackspace => {
431 self.handle_popup_backspace();
432 }
433 DeferredAction::CopyToClipboard(text) => {
434 self.clipboard.copy(text);
435 self.set_status_message(t!("clipboard.copied").to_string());
436 }
437
438 DeferredAction::ExecuteAction(kb_action) => {
440 self.handle_action(kb_action)?;
441 }
442
443 DeferredAction::InsertCharAndUpdate(c) => {
445 if let Some(ref mut prompt) = self.active_window_mut().prompt {
446 prompt.insert_char(c);
447 }
448 self.update_prompt_suggestions();
449 }
450
451 DeferredAction::FileBrowserSelectPrev => {
453 if let Some(state) = &mut self.active_window_mut().file_open_state {
454 state.select_prev();
455 }
456 }
457 DeferredAction::FileBrowserSelectNext => {
458 if let Some(state) = &mut self.active_window_mut().file_open_state {
459 state.select_next();
460 }
461 }
462 DeferredAction::FileBrowserPageUp => {
463 if let Some(state) = &mut self.active_window_mut().file_open_state {
464 state.page_up(10);
465 }
466 }
467 DeferredAction::FileBrowserPageDown => {
468 if let Some(state) = &mut self.active_window_mut().file_open_state {
469 state.page_down(10);
470 }
471 }
472 DeferredAction::FileBrowserConfirm => {
473 self.handle_file_open_action(&Action::PromptConfirm);
476 }
477 DeferredAction::FileBrowserAcceptSuggestion => {
478 self.handle_file_open_action(&Action::PromptAcceptSuggestion);
479 }
480 DeferredAction::FileBrowserGoParent => {
481 let parent = self
483 .active_window_mut()
484 .file_open_state
485 .as_ref()
486 .and_then(|s| s.current_dir.parent())
487 .map(|p| p.to_path_buf());
488 if let Some(parent_path) = parent {
489 self.load_file_open_directory(parent_path);
490 }
491 }
492 DeferredAction::FileBrowserUpdateFilter => {
493 self.update_file_open_filter();
494 }
495 DeferredAction::FileBrowserToggleHidden => {
496 self.file_open_toggle_hidden();
497 }
498
499 DeferredAction::InteractiveReplaceKey(c) => {
501 self.handle_interactive_replace_key(c)?;
502 }
503 DeferredAction::CancelInteractiveReplace => {
504 self.cancel_prompt();
505 self.active_window_mut().interactive_replace_state = None;
506 }
507
508 DeferredAction::ToggleKeyboardCapture => {
510 self.active_window_mut().keyboard_capture =
511 !self.active_window_mut().keyboard_capture;
512 if self.active_window_mut().keyboard_capture {
513 self.set_status_message(
514 "Keyboard capture ON - all keys go to terminal (F9 to toggle)".to_string(),
515 );
516 } else {
517 self.set_status_message(
518 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
519 );
520 }
521 }
522 DeferredAction::SendTerminalKey(code, modifiers) => {
523 self.active_window_mut().send_terminal_key(code, modifiers);
524 }
525 DeferredAction::SendTerminalMouse {
526 col,
527 row,
528 kind,
529 modifiers,
530 } => {
531 self.active_window_mut()
532 .send_terminal_mouse(col, row, kind, modifiers);
533 }
534 DeferredAction::ExitTerminalMode { explicit } => {
535 self.active_window_mut().terminal_mode = false;
536 self.active_window_mut().key_context =
537 crate::input::keybindings::KeyContext::Normal;
538 if explicit {
539 let buf = self.active_buffer();
541 self.active_window_mut().terminal_mode_resume.remove(&buf);
542 {
543 let __b = self.active_buffer();
544 self.active_window_mut().sync_terminal_to_buffer(__b);
545 };
546 self.set_status_message(
547 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
548 );
549 }
550 }
551 DeferredAction::EnterScrollbackMode => {
552 self.active_window_mut().terminal_mode = false;
553 self.active_window_mut().key_context =
554 crate::input::keybindings::KeyContext::Normal;
555 {
556 let __b = self.active_buffer();
557 self.active_window_mut().sync_terminal_to_buffer(__b);
558 };
559 self.set_status_message(
560 "Scrollback mode - use PageUp/Down to scroll (Ctrl+Space to resume)"
561 .to_string(),
562 );
563 self.handle_action(Action::MovePageUp)?;
565 }
566 DeferredAction::EnterTerminalMode => {
567 self.enter_terminal_mode();
568 }
569 }
570
571 Ok(())
572 }
573
574 fn menu_action_to_action(
576 &self,
577 action_name: &str,
578 args: std::collections::HashMap<String, serde_json::Value>,
579 ) -> Option<Action> {
580 if let Some(action) = Action::from_str(action_name, &args) {
582 return Some(action);
583 }
584
585 Some(Action::PluginAction(action_name.to_string()))
587 }
588
589 fn prompt_history_prev(&mut self) {
591 let prompt_info = self
593 .active_window()
594 .prompt
595 .as_ref()
596 .map(|p| (p.prompt_type.clone(), p.input.clone()));
597
598 if let Some((prompt_type, current_input)) = prompt_info {
599 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
601 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
602 if let Some(entry) = history.navigate_prev(¤t_input) {
603 if let Some(ref mut prompt) = self.active_window_mut().prompt {
604 prompt.set_input(entry);
605 }
606 }
607 }
608 }
609 }
610 }
611
612 fn prompt_history_next(&mut self) {
614 let prompt_type = self
615 .active_window()
616 .prompt
617 .as_ref()
618 .map(|p| p.prompt_type.clone());
619
620 if let Some(prompt_type) = prompt_type {
621 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
623 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
624 if let Some(entry) = history.navigate_next() {
625 if let Some(ref mut prompt) = self.active_window_mut().prompt {
626 prompt.set_input(entry);
627 }
628 }
629 }
630 }
631 }
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 #[test]
640 fn test_deferred_action_close_menu() {
641 let action = DeferredAction::CloseMenu;
644 assert!(matches!(action, DeferredAction::CloseMenu));
645 }
646
647 #[test]
648 fn test_deferred_action_execute_menu_action() {
649 let action = DeferredAction::ExecuteMenuAction {
650 action: "save".to_string(),
651 args: std::collections::HashMap::new(),
652 };
653 if let DeferredAction::ExecuteMenuAction { action: name, .. } = action {
654 assert_eq!(name, "save");
655 } else {
656 panic!("Expected ExecuteMenuAction");
657 }
658 }
659}