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 if self.presents_blocking_overlay() {
31 return None;
32 }
33
34 if self.active_window().terminal_mode {
36 if !self
41 .active_window()
42 .is_terminal_buffer(self.active_buffer())
43 {
44 self.active_window_mut().terminal_mode = false;
45 self.active_window_mut().key_context =
46 crate::input::keybindings::KeyContext::Normal;
47 return None; }
49 if matches!(
58 self.active_window().key_context,
59 crate::input::keybindings::KeyContext::FileExplorer
60 ) {
61 return None;
62 }
63 let bypass_action = {
76 let keybindings = self.keybindings.read().unwrap();
77 let action = keybindings.resolve(event, KeyContext::Normal);
78 if self
79 .command_registry
80 .read()
81 .unwrap()
82 .is_terminal_bypass_action(&action)
83 {
84 Some(action)
85 } else {
86 None
87 }
88 };
89 if let Some(action) = bypass_action {
90 if let Err(e) = self.handle_action(action) {
91 tracing::warn!("terminal-bypass action failed: {e}");
92 }
93 return Some(InputResult::Consumed);
94 }
95 let mut ctx = InputContext::new();
96 let keyboard_capture = self.active_window().keyboard_capture;
97 let keybindings = self.keybindings.read().unwrap();
98 let mut handler = TerminalModeInputHandler::new(keyboard_capture, &keybindings);
99 let result = handler.dispatch_input(event, &mut ctx);
100 drop(keybindings);
101 self.process_deferred_actions(ctx);
102 return Some(result);
103 }
104
105 if self
108 .active_window()
109 .is_terminal_buffer(self.active_buffer())
110 && should_enter_terminal_mode(event)
111 {
112 self.enter_terminal_mode();
113 self.active_window_mut()
115 .send_terminal_key(event.code, event.modifiers);
116 return Some(InputResult::Consumed);
117 }
118
119 None
120 }
121
122 fn dispatch_modal_keyboard(&mut self, event: &KeyEvent) -> Option<InputResult> {
131 use crate::app::overlay::LayerKind;
132
133 let kind = self.overlay_layers().iter().find_map(|l| match l.kind {
136 LayerKind::Settings
137 | LayerKind::KeybindingEditor
138 | LayerKind::CalibrationWizard
139 | LayerKind::Menu => Some(l.kind),
140 _ => None,
141 })?;
142 let mut ctx = InputContext::new();
143 Some(match kind {
144 LayerKind::Settings => {
145 let result = {
146 let settings = self
147 .settings_state
148 .as_mut()
149 .expect("Settings layer implies settings_state present");
150 settings.dispatch_input(event, &mut ctx)
151 };
152 self.process_deferred_actions(ctx);
153 result
154 }
155 LayerKind::KeybindingEditor => self.handle_keybinding_editor_input(event),
156 LayerKind::CalibrationWizard => self.handle_calibration_input(event),
157 LayerKind::Menu => {
158 let all_menus: Vec<crate::config::Menu> = self
159 .menus
160 .menus
161 .iter()
162 .chain(self.menu_state.plugin_menus.iter())
163 .cloned()
164 .collect();
165 let result = {
166 let mut handler = MenuInputHandler::new(&mut self.menu_state, &all_menus);
167 handler.dispatch_input(event, &mut ctx)
168 };
169 self.process_deferred_actions(ctx);
170 result
171 }
172 _ => unreachable!("find_map only returns the four capture-all kinds"),
173 })
174 }
175
176 pub fn dispatch_modal_input(&mut self, event: &KeyEvent) -> Option<InputResult> {
181 if let Some(result) = self.dispatch_modal_keyboard(event) {
188 return Some(result);
189 }
190
191 let mut ctx = InputContext::new();
192
193 if self.active_window().prompt.is_some() {
195 if event
199 .modifiers
200 .contains(crossterm::event::KeyModifiers::ALT)
201 {
202 if let crossterm::event::KeyCode::Char(_) = event.code {
203 let prompt_action = self.keybindings.read().unwrap().resolve_in_context_only(
204 event,
205 crate::input::keybindings::KeyContext::Prompt,
206 );
207 if let Some(action) = prompt_action {
208 if self.is_file_open_active() && self.handle_file_open_action(&action) {
210 return Some(InputResult::Consumed);
211 }
212 if let Err(e) = self.handle_action(action) {
214 tracing::warn!("Prompt action failed: {}", e);
215 }
216 return Some(InputResult::Consumed);
217 }
218 }
219 }
220
221 if self.is_file_open_active() {
223 let active_window_id = self.active_window;
224 let __win = self
225 .windows
226 .get_mut(&active_window_id)
227 .expect("active window present");
228 if let (Some(ref mut file_state), Some(ref mut prompt)) =
229 (&mut __win.file_open_state, &mut __win.prompt)
230 {
231 let mut handler = FileBrowserInputHandler::new(file_state, prompt);
232 let result = handler.dispatch_input(event, &mut ctx);
233 self.process_deferred_actions(ctx);
234 return Some(result);
235 }
236 }
237
238 use crate::view::prompt::PromptType;
240 let is_query_replace_confirm = self
241 .active_window()
242 .prompt
243 .as_ref()
244 .is_some_and(|p| p.prompt_type == PromptType::QueryReplaceConfirm);
245 if is_query_replace_confirm {
246 let mut handler = QueryReplaceConfirmInputHandler::new();
247 let result = handler.dispatch_input(event, &mut ctx);
248 self.process_deferred_actions(ctx);
249 return Some(result);
250 }
251
252 if let Some(result) = self.handle_overlay_toolbar_key(event) {
257 return Some(result);
258 }
259
260 if let Some(ref mut prompt) = self.active_window_mut().prompt {
261 let result = prompt.dispatch_input(event, &mut ctx);
262 if result != InputResult::Ignored {
265 self.process_deferred_actions(ctx);
266 return Some(result);
267 }
268 }
269 }
270
271 if self.popups_capture_keys() {
277 if let Some(action) = self.resolve_completion_popup_action(event) {
282 self.process_deferred_actions(ctx);
283 if let Err(e) = self.handle_action(action) {
284 tracing::warn!("Completion popup action failed: {}", e);
285 }
286 return Some(InputResult::Consumed);
287 }
288
289 if self.global_popups.top().is_some_and(|p| {
294 matches!(
295 p.resolver,
296 crate::view::popup::PopupResolver::WorkspaceTrust
297 )
298 }) {
299 if let Some(result) = self.handle_workspace_trust_key(event) {
300 return Some(result);
301 }
302 }
303
304 if self.global_popups.is_visible() {
308 let result = self.global_popups.dispatch_input(event, &mut ctx);
309 self.process_deferred_actions(ctx);
310 if result != InputResult::Ignored {
311 return Some(result);
312 }
313 return None;
316 }
317
318 if self.active_state().popups.is_visible() {
320 let result = self
321 .active_state_mut()
322 .popups
323 .dispatch_input(event, &mut ctx);
324 self.process_deferred_actions(ctx);
325 if result != InputResult::Ignored {
330 return Some(result);
331 }
332 }
333 }
334
335 None
336 }
337
338 pub fn process_deferred_actions(&mut self, ctx: InputContext) {
340 if let Some(msg) = ctx.status_message {
342 self.set_status_message(msg);
343 }
344
345 for action in ctx.deferred_actions {
347 if let Err(e) = self.execute_deferred_action(action) {
348 self.set_status_message(
349 t!("error.deferred_action", error = e.to_string()).to_string(),
350 );
351 }
352 }
353 }
354
355 fn execute_deferred_action(&mut self, action: DeferredAction) -> AnyhowResult<()> {
357 match action {
358 DeferredAction::CloseSettings { save } => {
360 if save {
361 self.save_settings();
362 }
363 self.close_settings(false);
364 }
365 DeferredAction::PasteToSettings => {
366 if let Some(text) = self.clipboard.paste() {
367 if !text.is_empty() {
368 if let Some(settings) = &mut self.settings_state {
369 settings.paste_into_focused_text(&text);
370 }
371 }
372 }
373 }
374 DeferredAction::OpenConfigFile { layer } => {
375 self.open_config_file(layer)?;
376 }
377
378 DeferredAction::CloseMenu => {
380 self.close_menu_with_auto_hide();
381 }
382 DeferredAction::ExecuteMenuAction { action, args } => {
383 if let Some(kb_action) = self.menu_action_to_action(&action, args) {
385 self.handle_action(kb_action)?;
386 }
387 }
388
389 DeferredAction::ClosePrompt => {
391 self.cancel_prompt();
392 }
393 DeferredAction::ConfirmPrompt => {
394 self.handle_action(Action::PromptConfirm)?;
395 }
396 DeferredAction::UpdatePromptSuggestions => {
397 self.update_prompt_suggestions();
398 }
399 DeferredAction::PromptHistoryPrev => {
400 self.prompt_history_prev();
401 }
402 DeferredAction::PromptHistoryNext => {
403 self.prompt_history_next();
404 }
405 DeferredAction::PreviewThemeFromPrompt => {
406 if let Some(prompt) = &self.active_window_mut().prompt {
407 if matches!(
408 prompt.prompt_type,
409 crate::view::prompt::PromptType::SelectTheme { .. }
410 ) {
411 let theme_name = prompt.input.clone();
412 self.preview_theme(&theme_name);
413 }
414 }
415 }
416 DeferredAction::PromptSelectionChanged { selected_index } => {
417 let plugin_custom_type =
419 self.active_window()
420 .prompt
421 .as_ref()
422 .and_then(|p| match &p.prompt_type {
423 crate::view::prompt::PromptType::Plugin { custom_type } => {
424 Some(custom_type.clone())
425 }
426 _ => None,
427 });
428 if let Some(custom_type) = plugin_custom_type {
429 self.plugin_manager.read().unwrap().run_hook(
430 "prompt_selection_changed",
431 crate::services::plugins::hooks::HookArgs::PromptSelectionChanged {
432 prompt_type: custom_type.clone(),
433 selected_index,
434 },
435 );
436 }
437 }
438
439 DeferredAction::ClosePopup => {
441 self.handle_popup_cancel();
447 }
448 DeferredAction::ConfirmPopup => {
449 self.handle_action(Action::PopupConfirm)?;
450 }
451 DeferredAction::PopupTypeChar(c) => {
452 self.handle_popup_type_char(c);
453 }
454 DeferredAction::PopupBackspace => {
455 self.handle_popup_backspace();
456 }
457 DeferredAction::CopyToClipboard(text) => {
458 self.clipboard.copy(text);
459 self.set_status_message(t!("clipboard.copied").to_string());
460 }
461
462 DeferredAction::ExecuteAction(kb_action) => {
464 self.handle_action(kb_action)?;
465 }
466
467 DeferredAction::InsertCharAndUpdate(c) => {
469 if let Some(ref mut prompt) = self.active_window_mut().prompt {
470 prompt.insert_char(c);
471 }
472 self.update_prompt_suggestions();
473 }
474
475 DeferredAction::FileBrowserSelectPrev => {
477 if let Some(state) = &mut self.active_window_mut().file_open_state {
478 state.select_prev();
479 }
480 }
481 DeferredAction::FileBrowserSelectNext => {
482 if let Some(state) = &mut self.active_window_mut().file_open_state {
483 state.select_next();
484 }
485 }
486 DeferredAction::FileBrowserPageUp => {
487 if let Some(state) = &mut self.active_window_mut().file_open_state {
488 state.page_up(10);
489 }
490 }
491 DeferredAction::FileBrowserPageDown => {
492 if let Some(state) = &mut self.active_window_mut().file_open_state {
493 state.page_down(10);
494 }
495 }
496 DeferredAction::FileBrowserConfirm => {
497 self.handle_file_open_action(&Action::PromptConfirm);
500 }
501 DeferredAction::FileBrowserAcceptSuggestion => {
502 self.handle_file_open_action(&Action::PromptAcceptSuggestion);
503 }
504 DeferredAction::FileBrowserGoParent => {
505 let parent = self
507 .active_window_mut()
508 .file_open_state
509 .as_ref()
510 .and_then(|s| s.current_dir.parent())
511 .map(|p| p.to_path_buf());
512 if let Some(parent_path) = parent {
513 self.load_file_open_directory(parent_path);
514 }
515 }
516 DeferredAction::FileBrowserUpdateFilter => {
517 self.update_file_open_filter();
518 }
519 DeferredAction::FileBrowserToggleHidden => {
520 self.file_open_toggle_hidden();
521 }
522
523 DeferredAction::InteractiveReplaceKey(c) => {
525 self.handle_interactive_replace_key(c)?;
526 }
527 DeferredAction::CancelInteractiveReplace => {
528 self.cancel_prompt();
529 self.active_window_mut().interactive_replace_state = None;
530 }
531
532 DeferredAction::ToggleKeyboardCapture => {
534 self.active_window_mut().keyboard_capture =
535 !self.active_window_mut().keyboard_capture;
536 if self.active_window_mut().keyboard_capture {
537 self.set_status_message(
538 "Keyboard capture ON - all keys go to terminal (F9 to toggle)".to_string(),
539 );
540 } else {
541 self.set_status_message(
542 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
543 );
544 }
545 }
546 DeferredAction::SendTerminalKey(code, modifiers) => {
547 self.active_window_mut().send_terminal_key(code, modifiers);
548 }
549 DeferredAction::SendTerminalMouse {
550 col,
551 row,
552 kind,
553 modifiers,
554 } => {
555 self.active_window_mut()
556 .send_terminal_mouse(col, row, kind, modifiers);
557 }
558 DeferredAction::ExitTerminalMode { explicit } => {
559 self.active_window_mut().terminal_mode = false;
560 self.active_window_mut().key_context =
561 crate::input::keybindings::KeyContext::Normal;
562 if explicit {
563 let buf = self.active_buffer();
565 self.active_window_mut().terminal_mode_resume.remove(&buf);
566 {
567 let __b = self.active_buffer();
568 self.active_window_mut().sync_terminal_to_buffer(__b);
569 };
570 self.set_status_message(
571 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
572 );
573 }
574 }
575 DeferredAction::EnterScrollbackMode => {
576 self.active_window_mut().terminal_mode = false;
577 self.active_window_mut().key_context =
578 crate::input::keybindings::KeyContext::Normal;
579 {
580 let __b = self.active_buffer();
581 self.active_window_mut().sync_terminal_to_buffer(__b);
582 };
583 self.set_status_message(
584 "Scrollback mode - use PageUp/Down to scroll (Ctrl+Space to resume)"
585 .to_string(),
586 );
587 self.handle_action(Action::MovePageUp)?;
589 }
590 DeferredAction::EnterTerminalMode => {
591 self.enter_terminal_mode();
592 }
593 }
594
595 Ok(())
596 }
597
598 fn menu_action_to_action(
600 &self,
601 action_name: &str,
602 args: std::collections::HashMap<String, serde_json::Value>,
603 ) -> Option<Action> {
604 if let Some(action) = Action::from_str(action_name, &args) {
606 return Some(action);
607 }
608
609 Some(Action::PluginAction(action_name.to_string()))
611 }
612
613 fn prompt_history_prev(&mut self) {
615 let prompt_info = self
617 .active_window()
618 .prompt
619 .as_ref()
620 .map(|p| (p.prompt_type.clone(), p.input.clone()));
621
622 if let Some((prompt_type, current_input)) = prompt_info {
623 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
625 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
626 if let Some(entry) = history.navigate_prev(¤t_input) {
627 if let Some(ref mut prompt) = self.active_window_mut().prompt {
628 prompt.set_input(entry);
629 }
630 }
631 }
632 }
633 }
634 }
635
636 fn prompt_history_next(&mut self) {
638 let prompt_type = self
639 .active_window()
640 .prompt
641 .as_ref()
642 .map(|p| p.prompt_type.clone());
643
644 if let Some(prompt_type) = prompt_type {
645 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
647 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
648 if let Some(entry) = history.navigate_next() {
649 if let Some(ref mut prompt) = self.active_window_mut().prompt {
650 prompt.set_input(entry);
651 }
652 }
653 }
654 }
655 }
656 }
657
658 fn overlay_toolbar_keys(&self) -> Vec<String> {
661 self.active_chrome()
662 .prompt_toolbar_hits
663 .iter()
664 .map(|(k, _)| k.clone())
665 .collect()
666 }
667
668 fn cycle_overlay_focus(&mut self, forward: bool) -> bool {
672 if !self.overlay_prompt_active() {
673 return false;
674 }
675 let has_toolbar = self
676 .active_window()
677 .prompt
678 .as_ref()
679 .is_some_and(|p| p.toolbar_widget.is_some());
680 if !has_toolbar {
681 return false;
682 }
683 let keys = self.overlay_toolbar_keys();
684 if keys.is_empty() {
685 return false;
686 }
687 let cur = self
688 .active_window()
689 .prompt
690 .as_ref()
691 .and_then(|p| p.toolbar_focus.clone());
692 let next: Option<String> = match cur {
694 None => Some(if forward {
695 keys[0].clone()
696 } else {
697 keys[keys.len() - 1].clone()
698 }),
699 Some(k) => match keys.iter().position(|x| x == &k) {
700 Some(i) if forward => keys.get(i + 1).cloned(), Some(i) => {
702 if i == 0 {
703 None
704 } else {
705 keys.get(i - 1).cloned()
706 }
707 }
708 None => None, },
710 };
711 if let Some(p) = self.active_window_mut().prompt.as_mut() {
712 p.toolbar_focus = next;
713 }
714 true
715 }
716
717 fn activate_focused_overlay_toggle(&mut self) {
721 let key = self
722 .active_window()
723 .prompt
724 .as_ref()
725 .and_then(|p| p.toolbar_focus.clone());
726 if let Some(key) = key {
727 self.toggle_overlay_toolbar_widget(&key);
728 }
729 }
730
731 pub(crate) fn toggle_overlay_toolbar_widget(&mut self, key: &str) {
739 if key.is_empty() {
740 return;
741 }
742 let event: Option<(&'static str, serde_json::Value)> = {
745 let Some(prompt) = self.active_window_mut().prompt.as_mut() else {
746 return;
747 };
748 let Some(spec) = prompt.toolbar_widget.as_mut() else {
749 return;
750 };
751 match crate::widgets::find_widget_by_key(spec, key) {
752 Some(fresh_core::api::WidgetSpec::Toggle { checked, .. }) => {
753 let nv = !*checked;
754 crate::widgets::set_toggle_checked_in_spec(spec, key, nv);
755 Some(("toggle", serde_json::json!({ "checked": nv })))
756 }
757 Some(fresh_core::api::WidgetSpec::Button { .. }) => {
758 Some(("activate", serde_json::json!({})))
759 }
760 _ => None,
761 }
762 };
763 let Some((event_type, payload)) = event else {
764 return;
765 };
766 #[cfg(feature = "plugins")]
767 {
768 let pm = self.plugin_manager.read().unwrap();
769 if pm.has_hook_handlers("widget_event") {
770 pm.run_hook(
771 "widget_event",
772 crate::services::plugins::hooks::HookArgs::WidgetEvent {
773 panel_id: 0,
774 widget_key: key.to_string(),
775 event_type: event_type.to_string(),
776 payload,
777 },
778 );
779 }
780 }
781 #[cfg(not(feature = "plugins"))]
782 {
783 let _ = (event_type, payload);
784 }
785 }
786
787 fn handle_overlay_toolbar_key(&mut self, event: &KeyEvent) -> Option<InputResult> {
792 use crossterm::event::{KeyCode, KeyModifiers};
793 if !self.overlay_prompt_active() {
794 return None;
795 }
796 let has_toolbar = self
797 .active_window()
798 .prompt
799 .as_ref()
800 .is_some_and(|p| p.toolbar_widget.is_some());
801 if !has_toolbar {
802 return None;
803 }
804 let focused = self
805 .active_window()
806 .prompt
807 .as_ref()
808 .is_some_and(|p| p.toolbar_focus.is_some());
809 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
810 match event.code {
811 KeyCode::BackTab => {
812 self.cycle_overlay_focus(false);
813 Some(InputResult::Consumed)
814 }
815 KeyCode::Tab => {
816 self.cycle_overlay_focus(!shift);
817 Some(InputResult::Consumed)
818 }
819 KeyCode::Char(' ') | KeyCode::Enter if focused => {
820 self.activate_focused_overlay_toggle();
821 Some(InputResult::Consumed)
822 }
823 KeyCode::Up
828 | KeyCode::Down
829 | KeyCode::PageUp
830 | KeyCode::PageDown
831 | KeyCode::Char(_)
832 if focused =>
833 {
834 if let Some(p) = self.active_window_mut().prompt.as_mut() {
835 p.toolbar_focus = None;
836 }
837 None
838 }
839 _ => None,
840 }
841 }
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847
848 #[test]
849 fn test_deferred_action_close_menu() {
850 let action = DeferredAction::CloseMenu;
853 assert!(matches!(action, DeferredAction::CloseMenu));
854 }
855
856 #[test]
857 fn test_deferred_action_execute_menu_action() {
858 let action = DeferredAction::ExecuteMenuAction {
859 action: "save".to_string(),
860 args: std::collections::HashMap::new(),
861 };
862 if let DeferredAction::ExecuteMenuAction { action: name, .. } = action {
863 assert_eq!(name, "save");
864 } else {
865 panic!("Expected ExecuteMenuAction");
866 }
867 }
868}