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 if let Some(dialog) = settings.entry_dialog_mut() {
370 dialog.insert_str(&text);
371 }
372 }
373 }
374 }
375 }
376 DeferredAction::OpenConfigFile { layer } => {
377 self.open_config_file(layer)?;
378 }
379
380 DeferredAction::CloseMenu => {
382 self.close_menu_with_auto_hide();
383 }
384 DeferredAction::ExecuteMenuAction { action, args } => {
385 if let Some(kb_action) = self.menu_action_to_action(&action, args) {
387 self.handle_action(kb_action)?;
388 }
389 }
390
391 DeferredAction::ClosePrompt => {
393 self.cancel_prompt();
394 }
395 DeferredAction::ConfirmPrompt => {
396 self.handle_action(Action::PromptConfirm)?;
397 }
398 DeferredAction::UpdatePromptSuggestions => {
399 self.update_prompt_suggestions();
400 }
401 DeferredAction::PromptHistoryPrev => {
402 self.prompt_history_prev();
403 }
404 DeferredAction::PromptHistoryNext => {
405 self.prompt_history_next();
406 }
407 DeferredAction::PreviewThemeFromPrompt => {
408 if let Some(prompt) = &self.active_window_mut().prompt {
409 if matches!(
410 prompt.prompt_type,
411 crate::view::prompt::PromptType::SelectTheme { .. }
412 ) {
413 let theme_name = prompt.input.clone();
414 self.preview_theme(&theme_name);
415 }
416 }
417 }
418 DeferredAction::PromptSelectionChanged { selected_index } => {
419 let plugin_custom_type =
421 self.active_window()
422 .prompt
423 .as_ref()
424 .and_then(|p| match &p.prompt_type {
425 crate::view::prompt::PromptType::Plugin { custom_type } => {
426 Some(custom_type.clone())
427 }
428 _ => None,
429 });
430 if let Some(custom_type) = plugin_custom_type {
431 self.plugin_manager.read().unwrap().run_hook(
432 "prompt_selection_changed",
433 crate::services::plugins::hooks::HookArgs::PromptSelectionChanged {
434 prompt_type: custom_type.clone(),
435 selected_index,
436 },
437 );
438 }
439 }
440
441 DeferredAction::ClosePopup => {
443 self.handle_popup_cancel();
449 }
450 DeferredAction::ConfirmPopup => {
451 self.handle_action(Action::PopupConfirm)?;
452 }
453 DeferredAction::PopupTypeChar(c) => {
454 self.handle_popup_type_char(c);
455 }
456 DeferredAction::PopupBackspace => {
457 self.handle_popup_backspace();
458 }
459 DeferredAction::CopyToClipboard(text) => {
460 self.clipboard.copy(text);
461 self.set_status_message(t!("clipboard.copied").to_string());
462 }
463
464 DeferredAction::ExecuteAction(kb_action) => {
466 self.handle_action(kb_action)?;
467 }
468
469 DeferredAction::InsertCharAndUpdate(c) => {
471 if let Some(ref mut prompt) = self.active_window_mut().prompt {
472 prompt.insert_char(c);
473 }
474 self.update_prompt_suggestions();
475 }
476
477 DeferredAction::FileBrowserSelectPrev => {
479 if let Some(state) = &mut self.active_window_mut().file_open_state {
480 state.select_prev();
481 }
482 }
483 DeferredAction::FileBrowserSelectNext => {
484 if let Some(state) = &mut self.active_window_mut().file_open_state {
485 state.select_next();
486 }
487 }
488 DeferredAction::FileBrowserPageUp => {
489 if let Some(state) = &mut self.active_window_mut().file_open_state {
490 state.page_up(10);
491 }
492 }
493 DeferredAction::FileBrowserPageDown => {
494 if let Some(state) = &mut self.active_window_mut().file_open_state {
495 state.page_down(10);
496 }
497 }
498 DeferredAction::FileBrowserConfirm => {
499 self.handle_file_open_action(&Action::PromptConfirm);
502 }
503 DeferredAction::FileBrowserAcceptSuggestion => {
504 self.handle_file_open_action(&Action::PromptAcceptSuggestion);
505 }
506 DeferredAction::FileBrowserGoParent => {
507 let parent = self
509 .active_window_mut()
510 .file_open_state
511 .as_ref()
512 .and_then(|s| s.current_dir.parent())
513 .map(|p| p.to_path_buf());
514 if let Some(parent_path) = parent {
515 self.load_file_open_directory(parent_path);
516 }
517 }
518 DeferredAction::FileBrowserUpdateFilter => {
519 self.update_file_open_filter();
520 }
521 DeferredAction::FileBrowserToggleHidden => {
522 self.file_open_toggle_hidden();
523 }
524
525 DeferredAction::InteractiveReplaceKey(c) => {
527 self.handle_interactive_replace_key(c)?;
528 }
529 DeferredAction::CancelInteractiveReplace => {
530 self.cancel_prompt();
531 self.active_window_mut().interactive_replace_state = None;
532 }
533
534 DeferredAction::ToggleKeyboardCapture => {
536 self.active_window_mut().keyboard_capture =
537 !self.active_window_mut().keyboard_capture;
538 if self.active_window_mut().keyboard_capture {
539 self.set_status_message(
540 "Keyboard capture ON - all keys go to terminal (F9 to toggle)".to_string(),
541 );
542 } else {
543 self.set_status_message(
544 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
545 );
546 }
547 }
548 DeferredAction::SendTerminalKey(code, modifiers) => {
549 self.active_window_mut().send_terminal_key(code, modifiers);
550 }
551 DeferredAction::SendTerminalMouse {
552 col,
553 row,
554 kind,
555 modifiers,
556 } => {
557 self.active_window_mut()
558 .send_terminal_mouse(col, row, kind, modifiers);
559 }
560 DeferredAction::ExitTerminalMode { explicit } => {
561 self.active_window_mut().terminal_mode = false;
562 self.active_window_mut().key_context =
563 crate::input::keybindings::KeyContext::Normal;
564 if explicit {
565 let buf = self.active_buffer();
567 self.active_window_mut().terminal_mode_resume.remove(&buf);
568 {
569 let __b = self.active_buffer();
570 self.active_window_mut().sync_terminal_to_buffer(__b);
571 };
572 self.set_status_message(
573 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
574 );
575 }
576 }
577 DeferredAction::EnterScrollbackMode => {
578 self.active_window_mut().terminal_mode = false;
579 self.active_window_mut().key_context =
580 crate::input::keybindings::KeyContext::Normal;
581 {
582 let __b = self.active_buffer();
583 self.active_window_mut().sync_terminal_to_buffer(__b);
584 };
585 self.set_status_message(
586 "Scrollback mode - use PageUp/Down to scroll (Ctrl+Space to resume)"
587 .to_string(),
588 );
589 self.handle_action(Action::MovePageUp)?;
591 }
592 DeferredAction::EnterTerminalMode => {
593 self.enter_terminal_mode();
594 }
595 }
596
597 Ok(())
598 }
599
600 fn menu_action_to_action(
602 &self,
603 action_name: &str,
604 args: std::collections::HashMap<String, serde_json::Value>,
605 ) -> Option<Action> {
606 if let Some(action) = Action::from_str(action_name, &args) {
608 return Some(action);
609 }
610
611 Some(Action::PluginAction(action_name.to_string()))
613 }
614
615 fn prompt_history_prev(&mut self) {
617 let prompt_info = self
619 .active_window()
620 .prompt
621 .as_ref()
622 .map(|p| (p.prompt_type.clone(), p.input.clone()));
623
624 if let Some((prompt_type, current_input)) = prompt_info {
625 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
627 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
628 if let Some(entry) = history.navigate_prev(¤t_input) {
629 if let Some(ref mut prompt) = self.active_window_mut().prompt {
630 prompt.set_input(entry);
631 }
632 }
633 }
634 }
635 }
636 }
637
638 fn prompt_history_next(&mut self) {
640 let prompt_type = self
641 .active_window()
642 .prompt
643 .as_ref()
644 .map(|p| p.prompt_type.clone());
645
646 if let Some(prompt_type) = prompt_type {
647 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
649 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
650 if let Some(entry) = history.navigate_next() {
651 if let Some(ref mut prompt) = self.active_window_mut().prompt {
652 prompt.set_input(entry);
653 }
654 }
655 }
656 }
657 }
658 }
659
660 fn overlay_toolbar_keys(&self) -> Vec<String> {
663 self.active_chrome()
664 .prompt_toolbar_hits
665 .iter()
666 .map(|(k, _)| k.clone())
667 .collect()
668 }
669
670 fn cycle_overlay_focus(&mut self, forward: bool) -> bool {
674 if !self.overlay_prompt_active() {
675 return false;
676 }
677 let has_toolbar = self
678 .active_window()
679 .prompt
680 .as_ref()
681 .is_some_and(|p| p.toolbar_widget.is_some());
682 if !has_toolbar {
683 return false;
684 }
685 let keys = self.overlay_toolbar_keys();
686 if keys.is_empty() {
687 return false;
688 }
689 let cur = self
690 .active_window()
691 .prompt
692 .as_ref()
693 .and_then(|p| p.toolbar_focus.clone());
694 let next: Option<String> = match cur {
696 None => Some(if forward {
697 keys[0].clone()
698 } else {
699 keys[keys.len() - 1].clone()
700 }),
701 Some(k) => match keys.iter().position(|x| x == &k) {
702 Some(i) if forward => keys.get(i + 1).cloned(), Some(i) => {
704 if i == 0 {
705 None
706 } else {
707 keys.get(i - 1).cloned()
708 }
709 }
710 None => None, },
712 };
713 if let Some(p) = self.active_window_mut().prompt.as_mut() {
714 p.toolbar_focus = next;
715 }
716 true
717 }
718
719 fn activate_focused_overlay_toggle(&mut self) {
723 let key = self
724 .active_window()
725 .prompt
726 .as_ref()
727 .and_then(|p| p.toolbar_focus.clone());
728 if let Some(key) = key {
729 self.toggle_overlay_toolbar_widget(&key);
730 }
731 }
732
733 pub(crate) fn toggle_overlay_toolbar_widget(&mut self, key: &str) {
741 if key.is_empty() {
742 return;
743 }
744 let event: Option<(&'static str, serde_json::Value)> = {
747 let Some(prompt) = self.active_window_mut().prompt.as_mut() else {
748 return;
749 };
750 let Some(spec) = prompt.toolbar_widget.as_mut() else {
751 return;
752 };
753 match crate::widgets::find_widget_by_key(spec, key) {
754 Some(fresh_core::api::WidgetSpec::Toggle { checked, .. }) => {
755 let nv = !*checked;
756 crate::widgets::set_toggle_checked_in_spec(spec, key, nv);
757 Some(("toggle", serde_json::json!({ "checked": nv })))
758 }
759 Some(fresh_core::api::WidgetSpec::Button { .. }) => {
760 Some(("activate", serde_json::json!({})))
761 }
762 _ => None,
763 }
764 };
765 let Some((event_type, payload)) = event else {
766 return;
767 };
768 #[cfg(feature = "plugins")]
769 {
770 let pm = self.plugin_manager.read().unwrap();
771 if pm.has_hook_handlers("widget_event") {
772 pm.run_hook(
773 "widget_event",
774 crate::services::plugins::hooks::HookArgs::WidgetEvent {
775 panel_id: 0,
776 widget_key: key.to_string(),
777 event_type: event_type.to_string(),
778 payload,
779 },
780 );
781 }
782 }
783 #[cfg(not(feature = "plugins"))]
784 {
785 let _ = (event_type, payload);
786 }
787 }
788
789 fn handle_overlay_toolbar_key(&mut self, event: &KeyEvent) -> Option<InputResult> {
794 use crossterm::event::{KeyCode, KeyModifiers};
795 if !self.overlay_prompt_active() {
796 return None;
797 }
798 let has_toolbar = self
799 .active_window()
800 .prompt
801 .as_ref()
802 .is_some_and(|p| p.toolbar_widget.is_some());
803 if !has_toolbar {
804 return None;
805 }
806 let focused = self
807 .active_window()
808 .prompt
809 .as_ref()
810 .is_some_and(|p| p.toolbar_focus.is_some());
811 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
812 match event.code {
813 KeyCode::BackTab => {
814 self.cycle_overlay_focus(false);
815 Some(InputResult::Consumed)
816 }
817 KeyCode::Tab => {
818 self.cycle_overlay_focus(!shift);
819 Some(InputResult::Consumed)
820 }
821 KeyCode::Char(' ') | KeyCode::Enter if focused => {
822 self.activate_focused_overlay_toggle();
823 Some(InputResult::Consumed)
824 }
825 KeyCode::Up
830 | KeyCode::Down
831 | KeyCode::PageUp
832 | KeyCode::PageDown
833 | KeyCode::Char(_)
834 if focused =>
835 {
836 if let Some(p) = self.active_window_mut().prompt.as_mut() {
837 p.toolbar_focus = None;
838 }
839 None
840 }
841 _ => None,
842 }
843 }
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849
850 #[test]
851 fn test_deferred_action_close_menu() {
852 let action = DeferredAction::CloseMenu;
855 assert!(matches!(action, DeferredAction::CloseMenu));
856 }
857
858 #[test]
859 fn test_deferred_action_execute_menu_action() {
860 let action = DeferredAction::ExecuteMenuAction {
861 action: "save".to_string(),
862 args: std::collections::HashMap::new(),
863 };
864 if let DeferredAction::ExecuteMenuAction { action: name, .. } = action {
865 assert_eq!(name, "save");
866 } else {
867 panic!("Expected ExecuteMenuAction");
868 }
869 }
870}