1use 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(result) = self.handle_overlay_toolbar_key(event) {
239 return Some(result);
240 }
241
242 if let Some(ref mut prompt) = self.active_window_mut().prompt {
243 let result = prompt.dispatch_input(event, &mut ctx);
244 if result != InputResult::Ignored {
247 self.process_deferred_actions(ctx);
248 return Some(result);
249 }
250 }
251 }
252
253 if self.popups_capture_keys() {
259 if let Some(action) = self.resolve_completion_popup_action(event) {
264 self.process_deferred_actions(ctx);
265 if let Err(e) = self.handle_action(action) {
266 tracing::warn!("Completion popup action failed: {}", e);
267 }
268 return Some(InputResult::Consumed);
269 }
270
271 if self.global_popups.top().is_some_and(|p| {
276 matches!(
277 p.resolver,
278 crate::view::popup::PopupResolver::WorkspaceTrust
279 )
280 }) {
281 if let Some(result) = self.handle_workspace_trust_key(event) {
282 return Some(result);
283 }
284 }
285
286 if self.global_popups.is_visible() {
290 let result = self.global_popups.dispatch_input(event, &mut ctx);
291 self.process_deferred_actions(ctx);
292 if result != InputResult::Ignored {
293 return Some(result);
294 }
295 return None;
298 }
299
300 if self.active_state().popups.is_visible() {
302 let result = self
303 .active_state_mut()
304 .popups
305 .dispatch_input(event, &mut ctx);
306 self.process_deferred_actions(ctx);
307 if result != InputResult::Ignored {
312 return Some(result);
313 }
314 }
315 }
316
317 None
318 }
319
320 pub fn process_deferred_actions(&mut self, ctx: InputContext) {
322 if let Some(msg) = ctx.status_message {
324 self.set_status_message(msg);
325 }
326
327 for action in ctx.deferred_actions {
329 if let Err(e) = self.execute_deferred_action(action) {
330 self.set_status_message(
331 t!("error.deferred_action", error = e.to_string()).to_string(),
332 );
333 }
334 }
335 }
336
337 fn execute_deferred_action(&mut self, action: DeferredAction) -> AnyhowResult<()> {
339 match action {
340 DeferredAction::CloseSettings { save } => {
342 if save {
343 self.save_settings();
344 }
345 self.close_settings(false);
346 }
347 DeferredAction::PasteToSettings => {
348 if let Some(text) = self.clipboard.paste() {
349 if !text.is_empty() {
350 if let Some(settings) = &mut self.settings_state {
351 if let Some(dialog) = settings.entry_dialog_mut() {
352 dialog.insert_str(&text);
353 }
354 }
355 }
356 }
357 }
358 DeferredAction::OpenConfigFile { layer } => {
359 self.open_config_file(layer)?;
360 }
361
362 DeferredAction::CloseMenu => {
364 self.close_menu_with_auto_hide();
365 }
366 DeferredAction::ExecuteMenuAction { action, args } => {
367 if let Some(kb_action) = self.menu_action_to_action(&action, args) {
369 self.handle_action(kb_action)?;
370 }
371 }
372
373 DeferredAction::ClosePrompt => {
375 self.cancel_prompt();
376 }
377 DeferredAction::ConfirmPrompt => {
378 self.handle_action(Action::PromptConfirm)?;
379 }
380 DeferredAction::UpdatePromptSuggestions => {
381 self.update_prompt_suggestions();
382 }
383 DeferredAction::PromptHistoryPrev => {
384 self.prompt_history_prev();
385 }
386 DeferredAction::PromptHistoryNext => {
387 self.prompt_history_next();
388 }
389 DeferredAction::PreviewThemeFromPrompt => {
390 if let Some(prompt) = &self.active_window_mut().prompt {
391 if matches!(
392 prompt.prompt_type,
393 crate::view::prompt::PromptType::SelectTheme { .. }
394 ) {
395 let theme_name = prompt.input.clone();
396 self.preview_theme(&theme_name);
397 }
398 }
399 }
400 DeferredAction::PromptSelectionChanged { selected_index } => {
401 let plugin_custom_type =
403 self.active_window()
404 .prompt
405 .as_ref()
406 .and_then(|p| match &p.prompt_type {
407 crate::view::prompt::PromptType::Plugin { custom_type } => {
408 Some(custom_type.clone())
409 }
410 _ => None,
411 });
412 if let Some(custom_type) = plugin_custom_type {
413 self.plugin_manager.read().unwrap().run_hook(
414 "prompt_selection_changed",
415 crate::services::plugins::hooks::HookArgs::PromptSelectionChanged {
416 prompt_type: custom_type.clone(),
417 selected_index,
418 },
419 );
420 }
421 }
422
423 DeferredAction::ClosePopup => {
425 self.handle_popup_cancel();
431 }
432 DeferredAction::ConfirmPopup => {
433 self.handle_action(Action::PopupConfirm)?;
434 }
435 DeferredAction::PopupTypeChar(c) => {
436 self.handle_popup_type_char(c);
437 }
438 DeferredAction::PopupBackspace => {
439 self.handle_popup_backspace();
440 }
441 DeferredAction::CopyToClipboard(text) => {
442 self.clipboard.copy(text);
443 self.set_status_message(t!("clipboard.copied").to_string());
444 }
445
446 DeferredAction::ExecuteAction(kb_action) => {
448 self.handle_action(kb_action)?;
449 }
450
451 DeferredAction::InsertCharAndUpdate(c) => {
453 if let Some(ref mut prompt) = self.active_window_mut().prompt {
454 prompt.insert_char(c);
455 }
456 self.update_prompt_suggestions();
457 }
458
459 DeferredAction::FileBrowserSelectPrev => {
461 if let Some(state) = &mut self.active_window_mut().file_open_state {
462 state.select_prev();
463 }
464 }
465 DeferredAction::FileBrowserSelectNext => {
466 if let Some(state) = &mut self.active_window_mut().file_open_state {
467 state.select_next();
468 }
469 }
470 DeferredAction::FileBrowserPageUp => {
471 if let Some(state) = &mut self.active_window_mut().file_open_state {
472 state.page_up(10);
473 }
474 }
475 DeferredAction::FileBrowserPageDown => {
476 if let Some(state) = &mut self.active_window_mut().file_open_state {
477 state.page_down(10);
478 }
479 }
480 DeferredAction::FileBrowserConfirm => {
481 self.handle_file_open_action(&Action::PromptConfirm);
484 }
485 DeferredAction::FileBrowserAcceptSuggestion => {
486 self.handle_file_open_action(&Action::PromptAcceptSuggestion);
487 }
488 DeferredAction::FileBrowserGoParent => {
489 let parent = self
491 .active_window_mut()
492 .file_open_state
493 .as_ref()
494 .and_then(|s| s.current_dir.parent())
495 .map(|p| p.to_path_buf());
496 if let Some(parent_path) = parent {
497 self.load_file_open_directory(parent_path);
498 }
499 }
500 DeferredAction::FileBrowserUpdateFilter => {
501 self.update_file_open_filter();
502 }
503 DeferredAction::FileBrowserToggleHidden => {
504 self.file_open_toggle_hidden();
505 }
506
507 DeferredAction::InteractiveReplaceKey(c) => {
509 self.handle_interactive_replace_key(c)?;
510 }
511 DeferredAction::CancelInteractiveReplace => {
512 self.cancel_prompt();
513 self.active_window_mut().interactive_replace_state = None;
514 }
515
516 DeferredAction::ToggleKeyboardCapture => {
518 self.active_window_mut().keyboard_capture =
519 !self.active_window_mut().keyboard_capture;
520 if self.active_window_mut().keyboard_capture {
521 self.set_status_message(
522 "Keyboard capture ON - all keys go to terminal (F9 to toggle)".to_string(),
523 );
524 } else {
525 self.set_status_message(
526 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
527 );
528 }
529 }
530 DeferredAction::SendTerminalKey(code, modifiers) => {
531 self.active_window_mut().send_terminal_key(code, modifiers);
532 }
533 DeferredAction::SendTerminalMouse {
534 col,
535 row,
536 kind,
537 modifiers,
538 } => {
539 self.active_window_mut()
540 .send_terminal_mouse(col, row, kind, modifiers);
541 }
542 DeferredAction::ExitTerminalMode { explicit } => {
543 self.active_window_mut().terminal_mode = false;
544 self.active_window_mut().key_context =
545 crate::input::keybindings::KeyContext::Normal;
546 if explicit {
547 let buf = self.active_buffer();
549 self.active_window_mut().terminal_mode_resume.remove(&buf);
550 {
551 let __b = self.active_buffer();
552 self.active_window_mut().sync_terminal_to_buffer(__b);
553 };
554 self.set_status_message(
555 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
556 );
557 }
558 }
559 DeferredAction::EnterScrollbackMode => {
560 self.active_window_mut().terminal_mode = false;
561 self.active_window_mut().key_context =
562 crate::input::keybindings::KeyContext::Normal;
563 {
564 let __b = self.active_buffer();
565 self.active_window_mut().sync_terminal_to_buffer(__b);
566 };
567 self.set_status_message(
568 "Scrollback mode - use PageUp/Down to scroll (Ctrl+Space to resume)"
569 .to_string(),
570 );
571 self.handle_action(Action::MovePageUp)?;
573 }
574 DeferredAction::EnterTerminalMode => {
575 self.enter_terminal_mode();
576 }
577 }
578
579 Ok(())
580 }
581
582 fn menu_action_to_action(
584 &self,
585 action_name: &str,
586 args: std::collections::HashMap<String, serde_json::Value>,
587 ) -> Option<Action> {
588 if let Some(action) = Action::from_str(action_name, &args) {
590 return Some(action);
591 }
592
593 Some(Action::PluginAction(action_name.to_string()))
595 }
596
597 fn prompt_history_prev(&mut self) {
599 let prompt_info = self
601 .active_window()
602 .prompt
603 .as_ref()
604 .map(|p| (p.prompt_type.clone(), p.input.clone()));
605
606 if let Some((prompt_type, current_input)) = prompt_info {
607 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
609 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
610 if let Some(entry) = history.navigate_prev(¤t_input) {
611 if let Some(ref mut prompt) = self.active_window_mut().prompt {
612 prompt.set_input(entry);
613 }
614 }
615 }
616 }
617 }
618 }
619
620 fn prompt_history_next(&mut self) {
622 let prompt_type = self
623 .active_window()
624 .prompt
625 .as_ref()
626 .map(|p| p.prompt_type.clone());
627
628 if let Some(prompt_type) = prompt_type {
629 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
631 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
632 if let Some(entry) = history.navigate_next() {
633 if let Some(ref mut prompt) = self.active_window_mut().prompt {
634 prompt.set_input(entry);
635 }
636 }
637 }
638 }
639 }
640 }
641
642 fn overlay_toolbar_keys(&self) -> Vec<String> {
645 self.active_chrome()
646 .prompt_toolbar_hits
647 .iter()
648 .map(|(k, _)| k.clone())
649 .collect()
650 }
651
652 fn cycle_overlay_focus(&mut self, forward: bool) -> bool {
656 if !self.overlay_prompt_active() {
657 return false;
658 }
659 let has_toolbar = self
660 .active_window()
661 .prompt
662 .as_ref()
663 .is_some_and(|p| p.toolbar_widget.is_some());
664 if !has_toolbar {
665 return false;
666 }
667 let keys = self.overlay_toolbar_keys();
668 if keys.is_empty() {
669 return false;
670 }
671 let cur = self
672 .active_window()
673 .prompt
674 .as_ref()
675 .and_then(|p| p.toolbar_focus.clone());
676 let next: Option<String> = match cur {
678 None => Some(if forward {
679 keys[0].clone()
680 } else {
681 keys[keys.len() - 1].clone()
682 }),
683 Some(k) => match keys.iter().position(|x| x == &k) {
684 Some(i) if forward => keys.get(i + 1).cloned(), Some(i) => {
686 if i == 0 {
687 None
688 } else {
689 keys.get(i - 1).cloned()
690 }
691 }
692 None => None, },
694 };
695 if let Some(p) = self.active_window_mut().prompt.as_mut() {
696 p.toolbar_focus = next;
697 }
698 true
699 }
700
701 fn activate_focused_overlay_toggle(&mut self) {
705 let key = self
706 .active_window()
707 .prompt
708 .as_ref()
709 .and_then(|p| p.toolbar_focus.clone());
710 if let Some(key) = key {
711 self.toggle_overlay_toolbar_widget(&key);
712 }
713 }
714
715 pub(crate) fn toggle_overlay_toolbar_widget(&mut self, key: &str) {
723 if key.is_empty() {
724 return;
725 }
726 let event: Option<(&'static str, serde_json::Value)> = {
729 let Some(prompt) = self.active_window_mut().prompt.as_mut() else {
730 return;
731 };
732 let Some(spec) = prompt.toolbar_widget.as_mut() else {
733 return;
734 };
735 match crate::widgets::find_widget_by_key(spec, key) {
736 Some(fresh_core::api::WidgetSpec::Toggle { checked, .. }) => {
737 let nv = !*checked;
738 crate::widgets::set_toggle_checked_in_spec(spec, key, nv);
739 Some(("toggle", serde_json::json!({ "checked": nv })))
740 }
741 Some(fresh_core::api::WidgetSpec::Button { .. }) => {
742 Some(("activate", serde_json::json!({})))
743 }
744 _ => None,
745 }
746 };
747 let Some((event_type, payload)) = event else {
748 return;
749 };
750 #[cfg(feature = "plugins")]
751 {
752 let pm = self.plugin_manager.read().unwrap();
753 if pm.has_hook_handlers("widget_event") {
754 pm.run_hook(
755 "widget_event",
756 crate::services::plugins::hooks::HookArgs::WidgetEvent {
757 panel_id: 0,
758 widget_key: key.to_string(),
759 event_type: event_type.to_string(),
760 payload,
761 },
762 );
763 }
764 }
765 #[cfg(not(feature = "plugins"))]
766 {
767 let _ = (event_type, payload);
768 }
769 }
770
771 fn handle_overlay_toolbar_key(&mut self, event: &KeyEvent) -> Option<InputResult> {
776 use crossterm::event::{KeyCode, KeyModifiers};
777 if !self.overlay_prompt_active() {
778 return None;
779 }
780 let has_toolbar = self
781 .active_window()
782 .prompt
783 .as_ref()
784 .is_some_and(|p| p.toolbar_widget.is_some());
785 if !has_toolbar {
786 return None;
787 }
788 let focused = self
789 .active_window()
790 .prompt
791 .as_ref()
792 .is_some_and(|p| p.toolbar_focus.is_some());
793 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
794 match event.code {
795 KeyCode::BackTab => {
796 self.cycle_overlay_focus(false);
797 Some(InputResult::Consumed)
798 }
799 KeyCode::Tab => {
800 self.cycle_overlay_focus(!shift);
801 Some(InputResult::Consumed)
802 }
803 KeyCode::Char(' ') | KeyCode::Enter if focused => {
804 self.activate_focused_overlay_toggle();
805 Some(InputResult::Consumed)
806 }
807 KeyCode::Up
812 | KeyCode::Down
813 | KeyCode::PageUp
814 | KeyCode::PageDown
815 | KeyCode::Char(_)
816 if focused =>
817 {
818 if let Some(p) = self.active_window_mut().prompt.as_mut() {
819 p.toolbar_focus = None;
820 }
821 None
822 }
823 _ => None,
824 }
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831
832 #[test]
833 fn test_deferred_action_close_menu() {
834 let action = DeferredAction::CloseMenu;
837 assert!(matches!(action, DeferredAction::CloseMenu));
838 }
839
840 #[test]
841 fn test_deferred_action_execute_menu_action() {
842 let action = DeferredAction::ExecuteMenuAction {
843 action: "save".to_string(),
844 args: std::collections::HashMap::new(),
845 };
846 if let DeferredAction::ExecuteMenuAction { action: name, .. } = action {
847 assert_eq!(name, "save");
848 } else {
849 panic!("Expected ExecuteMenuAction");
850 }
851 }
852}