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;
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()
25 || self.global_popups.is_visible()
26 || self.active_state().popups.is_visible()
27 || self.menu_state.active_menu.is_some()
28 || self.settings_state.as_ref().is_some_and(|s| s.visible)
29 || self.calibration_wizard.is_some()
30 || self.keybinding_editor.is_some();
31
32 if in_modal {
33 return None;
34 }
35
36 if self.active_window().terminal_mode {
38 if !self
43 .active_window()
44 .is_terminal_buffer(self.active_buffer())
45 {
46 self.active_window_mut().terminal_mode = false;
47 self.active_window_mut().key_context =
48 crate::input::keybindings::KeyContext::Normal;
49 return None; }
51 let mut ctx = InputContext::new();
52 let keyboard_capture = self.active_window().keyboard_capture;
53 let keybindings = self.keybindings.read().unwrap();
54 let mut handler = TerminalModeInputHandler::new(keyboard_capture, &keybindings);
55 let result = handler.dispatch_input(event, &mut ctx);
56 drop(keybindings);
57 self.process_deferred_actions(ctx);
58 return Some(result);
59 }
60
61 if self
64 .active_window()
65 .is_terminal_buffer(self.active_buffer())
66 && should_enter_terminal_mode(event)
67 {
68 self.enter_terminal_mode();
69 self.active_window_mut()
71 .send_terminal_key(event.code, event.modifiers);
72 return Some(InputResult::Consumed);
73 }
74
75 None
76 }
77
78 pub fn dispatch_modal_input(&mut self, event: &KeyEvent) -> Option<InputResult> {
83 let mut ctx = InputContext::new();
84
85 if let Some(ref mut settings) = self.settings_state {
87 if settings.visible {
88 let result = settings.dispatch_input(event, &mut ctx);
89 self.process_deferred_actions(ctx);
90 return Some(result);
91 }
92 }
93
94 if self.keybinding_editor.is_some() {
96 let result = self.handle_keybinding_editor_input(event);
97 return Some(result);
98 }
99
100 if self.calibration_wizard.is_some() {
102 let result = self.handle_calibration_input(event);
103 return Some(result);
104 }
105
106 if self.menu_state.active_menu.is_some() {
108 let all_menus: Vec<crate::config::Menu> = self
109 .menus
110 .menus
111 .iter()
112 .chain(self.menu_state.plugin_menus.iter())
113 .cloned()
114 .collect();
115
116 let mut handler = MenuInputHandler::new(&mut self.menu_state, &all_menus);
117 let result = handler.dispatch_input(event, &mut ctx);
118 self.process_deferred_actions(ctx);
119 return Some(result);
120 }
121
122 if self.active_window().prompt.is_some() {
124 if event
128 .modifiers
129 .contains(crossterm::event::KeyModifiers::ALT)
130 {
131 if let crossterm::event::KeyCode::Char(_) = event.code {
132 let prompt_action = self.keybindings.read().unwrap().resolve_in_context_only(
133 event,
134 crate::input::keybindings::KeyContext::Prompt,
135 );
136 if let Some(action) = prompt_action {
137 if self.is_file_open_active() && self.handle_file_open_action(&action) {
139 return Some(InputResult::Consumed);
140 }
141 if let Err(e) = self.handle_action(action) {
143 tracing::warn!("Prompt action failed: {}", e);
144 }
145 return Some(InputResult::Consumed);
146 }
147 }
148 }
149
150 if self.is_file_open_active() {
152 let active_window_id = self.active_window;
153 let __win = self
154 .windows
155 .get_mut(&active_window_id)
156 .expect("active window present");
157 if let (Some(ref mut file_state), Some(ref mut prompt)) =
158 (&mut __win.file_open_state, &mut __win.prompt)
159 {
160 let mut handler = FileBrowserInputHandler::new(file_state, prompt);
161 let result = handler.dispatch_input(event, &mut ctx);
162 self.process_deferred_actions(ctx);
163 return Some(result);
164 }
165 }
166
167 use crate::view::prompt::PromptType;
169 let is_query_replace_confirm = self
170 .active_window()
171 .prompt
172 .as_ref()
173 .is_some_and(|p| p.prompt_type == PromptType::QueryReplaceConfirm);
174 if is_query_replace_confirm {
175 let mut handler = QueryReplaceConfirmInputHandler::new();
176 let result = handler.dispatch_input(event, &mut ctx);
177 self.process_deferred_actions(ctx);
178 return Some(result);
179 }
180
181 if let Some(ref mut prompt) = self.active_window_mut().prompt {
182 let result = prompt.dispatch_input(event, &mut ctx);
183 if result != InputResult::Ignored {
186 self.process_deferred_actions(ctx);
187 return Some(result);
188 }
189 }
190 }
191
192 if self.popups_capture_keys() {
198 if let Some(action) = self.resolve_completion_popup_action(event) {
203 self.process_deferred_actions(ctx);
204 if let Err(e) = self.handle_action(action) {
205 tracing::warn!("Completion popup action failed: {}", e);
206 }
207 return Some(InputResult::Consumed);
208 }
209
210 if self.global_popups.is_visible() {
214 let result = self.global_popups.dispatch_input(event, &mut ctx);
215 self.process_deferred_actions(ctx);
216 if result != InputResult::Ignored {
217 return Some(result);
218 }
219 return None;
222 }
223
224 if self.active_state().popups.is_visible() {
226 let result = self
227 .active_state_mut()
228 .popups
229 .dispatch_input(event, &mut ctx);
230 self.process_deferred_actions(ctx);
231 if result != InputResult::Ignored {
236 return Some(result);
237 }
238 }
239 }
240
241 None
242 }
243
244 pub fn process_deferred_actions(&mut self, ctx: InputContext) {
246 if let Some(msg) = ctx.status_message {
248 self.set_status_message(msg);
249 }
250
251 for action in ctx.deferred_actions {
253 if let Err(e) = self.execute_deferred_action(action) {
254 self.set_status_message(
255 t!("error.deferred_action", error = e.to_string()).to_string(),
256 );
257 }
258 }
259 }
260
261 fn execute_deferred_action(&mut self, action: DeferredAction) -> AnyhowResult<()> {
263 match action {
264 DeferredAction::CloseSettings { save } => {
266 if save {
267 self.save_settings();
268 }
269 self.close_settings(false);
270 }
271 DeferredAction::PasteToSettings => {
272 if let Some(text) = self.clipboard.paste() {
273 if !text.is_empty() {
274 if let Some(settings) = &mut self.settings_state {
275 if let Some(dialog) = settings.entry_dialog_mut() {
276 dialog.insert_str(&text);
277 }
278 }
279 }
280 }
281 }
282 DeferredAction::OpenConfigFile { layer } => {
283 self.open_config_file(layer)?;
284 }
285
286 DeferredAction::CloseMenu => {
288 self.close_menu_with_auto_hide();
289 }
290 DeferredAction::ExecuteMenuAction { action, args } => {
291 if let Some(kb_action) = self.menu_action_to_action(&action, args) {
293 self.handle_action(kb_action)?;
294 }
295 }
296
297 DeferredAction::ClosePrompt => {
299 self.cancel_prompt();
300 }
301 DeferredAction::ConfirmPrompt => {
302 self.handle_action(Action::PromptConfirm)?;
303 }
304 DeferredAction::UpdatePromptSuggestions => {
305 self.update_prompt_suggestions();
306 }
307 DeferredAction::PromptHistoryPrev => {
308 self.prompt_history_prev();
309 }
310 DeferredAction::PromptHistoryNext => {
311 self.prompt_history_next();
312 }
313 DeferredAction::PreviewThemeFromPrompt => {
314 if let Some(prompt) = &self.active_window_mut().prompt {
315 if matches!(
316 prompt.prompt_type,
317 crate::view::prompt::PromptType::SelectTheme { .. }
318 ) {
319 let theme_name = prompt.input.clone();
320 self.preview_theme(&theme_name);
321 }
322 }
323 }
324 DeferredAction::PromptSelectionChanged { selected_index } => {
325 let plugin_custom_type =
327 self.active_window()
328 .prompt
329 .as_ref()
330 .and_then(|p| match &p.prompt_type {
331 crate::view::prompt::PromptType::Plugin { custom_type } => {
332 Some(custom_type.clone())
333 }
334 _ => None,
335 });
336 if let Some(custom_type) = plugin_custom_type {
337 self.plugin_manager.read().unwrap().run_hook(
338 "prompt_selection_changed",
339 crate::services::plugins::hooks::HookArgs::PromptSelectionChanged {
340 prompt_type: custom_type.clone(),
341 selected_index,
342 },
343 );
344 }
345 }
346
347 DeferredAction::ClosePopup => {
349 self.handle_popup_cancel();
355 }
356 DeferredAction::ConfirmPopup => {
357 self.handle_action(Action::PopupConfirm)?;
358 }
359 DeferredAction::PopupTypeChar(c) => {
360 self.handle_popup_type_char(c);
361 }
362 DeferredAction::PopupBackspace => {
363 self.handle_popup_backspace();
364 }
365 DeferredAction::CopyToClipboard(text) => {
366 self.clipboard.copy(text);
367 self.set_status_message(t!("clipboard.copied").to_string());
368 }
369
370 DeferredAction::ExecuteAction(kb_action) => {
372 self.handle_action(kb_action)?;
373 }
374
375 DeferredAction::InsertCharAndUpdate(c) => {
377 if let Some(ref mut prompt) = self.active_window_mut().prompt {
378 prompt.insert_char(c);
379 }
380 self.update_prompt_suggestions();
381 }
382
383 DeferredAction::FileBrowserSelectPrev => {
385 if let Some(state) = &mut self.active_window_mut().file_open_state {
386 state.select_prev();
387 }
388 }
389 DeferredAction::FileBrowserSelectNext => {
390 if let Some(state) = &mut self.active_window_mut().file_open_state {
391 state.select_next();
392 }
393 }
394 DeferredAction::FileBrowserPageUp => {
395 if let Some(state) = &mut self.active_window_mut().file_open_state {
396 state.page_up(10);
397 }
398 }
399 DeferredAction::FileBrowserPageDown => {
400 if let Some(state) = &mut self.active_window_mut().file_open_state {
401 state.page_down(10);
402 }
403 }
404 DeferredAction::FileBrowserConfirm => {
405 self.handle_file_open_action(&Action::PromptConfirm);
408 }
409 DeferredAction::FileBrowserAcceptSuggestion => {
410 self.handle_file_open_action(&Action::PromptAcceptSuggestion);
411 }
412 DeferredAction::FileBrowserGoParent => {
413 let parent = self
415 .active_window_mut()
416 .file_open_state
417 .as_ref()
418 .and_then(|s| s.current_dir.parent())
419 .map(|p| p.to_path_buf());
420 if let Some(parent_path) = parent {
421 self.load_file_open_directory(parent_path);
422 }
423 }
424 DeferredAction::FileBrowserUpdateFilter => {
425 self.update_file_open_filter();
426 }
427 DeferredAction::FileBrowserToggleHidden => {
428 self.file_open_toggle_hidden();
429 }
430
431 DeferredAction::InteractiveReplaceKey(c) => {
433 self.handle_interactive_replace_key(c)?;
434 }
435 DeferredAction::CancelInteractiveReplace => {
436 self.cancel_prompt();
437 self.active_window_mut().interactive_replace_state = None;
438 }
439
440 DeferredAction::ToggleKeyboardCapture => {
442 self.active_window_mut().keyboard_capture =
443 !self.active_window_mut().keyboard_capture;
444 if self.active_window_mut().keyboard_capture {
445 self.set_status_message(
446 "Keyboard capture ON - all keys go to terminal (F9 to toggle)".to_string(),
447 );
448 } else {
449 self.set_status_message(
450 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
451 );
452 }
453 }
454 DeferredAction::SendTerminalKey(code, modifiers) => {
455 self.active_window_mut().send_terminal_key(code, modifiers);
456 }
457 DeferredAction::SendTerminalMouse {
458 col,
459 row,
460 kind,
461 modifiers,
462 } => {
463 self.active_window_mut()
464 .send_terminal_mouse(col, row, kind, modifiers);
465 }
466 DeferredAction::ExitTerminalMode { explicit } => {
467 self.active_window_mut().terminal_mode = false;
468 self.active_window_mut().key_context =
469 crate::input::keybindings::KeyContext::Normal;
470 if explicit {
471 let buf = self.active_buffer();
473 self.active_window_mut().terminal_mode_resume.remove(&buf);
474 {
475 let __b = self.active_buffer();
476 self.active_window_mut().sync_terminal_to_buffer(__b);
477 };
478 self.set_status_message(
479 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
480 );
481 }
482 }
483 DeferredAction::EnterScrollbackMode => {
484 self.active_window_mut().terminal_mode = false;
485 self.active_window_mut().key_context =
486 crate::input::keybindings::KeyContext::Normal;
487 {
488 let __b = self.active_buffer();
489 self.active_window_mut().sync_terminal_to_buffer(__b);
490 };
491 self.set_status_message(
492 "Scrollback mode - use PageUp/Down to scroll (Ctrl+Space to resume)"
493 .to_string(),
494 );
495 self.handle_action(Action::MovePageUp)?;
497 }
498 DeferredAction::EnterTerminalMode => {
499 self.enter_terminal_mode();
500 }
501 }
502
503 Ok(())
504 }
505
506 fn menu_action_to_action(
508 &self,
509 action_name: &str,
510 args: std::collections::HashMap<String, serde_json::Value>,
511 ) -> Option<Action> {
512 if let Some(action) = Action::from_str(action_name, &args) {
514 return Some(action);
515 }
516
517 Some(Action::PluginAction(action_name.to_string()))
519 }
520
521 fn prompt_history_prev(&mut self) {
523 let prompt_info = self
525 .active_window()
526 .prompt
527 .as_ref()
528 .map(|p| (p.prompt_type.clone(), p.input.clone()));
529
530 if let Some((prompt_type, current_input)) = prompt_info {
531 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
533 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
534 if let Some(entry) = history.navigate_prev(¤t_input) {
535 if let Some(ref mut prompt) = self.active_window_mut().prompt {
536 prompt.set_input(entry);
537 }
538 }
539 }
540 }
541 }
542 }
543
544 fn prompt_history_next(&mut self) {
546 let prompt_type = self
547 .active_window()
548 .prompt
549 .as_ref()
550 .map(|p| p.prompt_type.clone());
551
552 if let Some(prompt_type) = prompt_type {
553 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
555 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
556 if let Some(entry) = history.navigate_next() {
557 if let Some(ref mut prompt) = self.active_window_mut().prompt {
558 prompt.set_input(entry);
559 }
560 }
561 }
562 }
563 }
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570
571 #[test]
572 fn test_deferred_action_close_menu() {
573 let action = DeferredAction::CloseMenu;
576 assert!(matches!(action, DeferredAction::CloseMenu));
577 }
578
579 #[test]
580 fn test_deferred_action_execute_menu_action() {
581 let action = DeferredAction::ExecuteMenuAction {
582 action: "save".to_string(),
583 args: std::collections::HashMap::new(),
584 };
585 if let DeferredAction::ExecuteMenuAction { action: name, .. } = action {
586 assert_eq!(name, "save");
587 } else {
588 panic!("Expected ExecuteMenuAction");
589 }
590 }
591}