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.terminal_mode {
38 if !self.is_terminal_buffer(self.active_buffer()) {
43 self.terminal_mode = false;
44 self.key_context = crate::input::keybindings::KeyContext::Normal;
45 return None; }
47 let mut ctx = InputContext::new();
48 let keybindings = self.keybindings.read().unwrap();
49 let mut handler = TerminalModeInputHandler::new(self.keyboard_capture, &keybindings);
50 let result = handler.dispatch_input(event, &mut ctx);
51 drop(keybindings);
52 self.process_deferred_actions(ctx);
53 return Some(result);
54 }
55
56 if self.is_terminal_buffer(self.active_buffer()) && should_enter_terminal_mode(event) {
59 self.enter_terminal_mode();
60 self.send_terminal_key(event.code, event.modifiers);
62 return Some(InputResult::Consumed);
63 }
64
65 None
66 }
67
68 pub fn dispatch_modal_input(&mut self, event: &KeyEvent) -> Option<InputResult> {
73 let mut ctx = InputContext::new();
74
75 if let Some(ref mut settings) = self.settings_state {
77 if settings.visible {
78 let result = settings.dispatch_input(event, &mut ctx);
79 self.process_deferred_actions(ctx);
80 return Some(result);
81 }
82 }
83
84 if self.keybinding_editor.is_some() {
86 let result = self.handle_keybinding_editor_input(event);
87 return Some(result);
88 }
89
90 if self.calibration_wizard.is_some() {
92 let result = self.handle_calibration_input(event);
93 return Some(result);
94 }
95
96 if self.menu_state.active_menu.is_some() {
98 let all_menus: Vec<crate::config::Menu> = self
99 .menus
100 .menus
101 .iter()
102 .chain(self.menu_state.plugin_menus.iter())
103 .cloned()
104 .collect();
105
106 let mut handler = MenuInputHandler::new(&mut self.menu_state, &all_menus);
107 let result = handler.dispatch_input(event, &mut ctx);
108 self.process_deferred_actions(ctx);
109 return Some(result);
110 }
111
112 if self.prompt.is_some() {
114 if event
118 .modifiers
119 .contains(crossterm::event::KeyModifiers::ALT)
120 {
121 if let crossterm::event::KeyCode::Char(_) = event.code {
122 let prompt_action = self.keybindings.read().unwrap().resolve_in_context_only(
123 event,
124 crate::input::keybindings::KeyContext::Prompt,
125 );
126 if let Some(action) = prompt_action {
127 if self.is_file_open_active() && self.handle_file_open_action(&action) {
129 return Some(InputResult::Consumed);
130 }
131 if let Err(e) = self.handle_action(action) {
133 tracing::warn!("Prompt action failed: {}", e);
134 }
135 return Some(InputResult::Consumed);
136 }
137 }
138 }
139
140 if self.is_file_open_active() {
142 if let (Some(ref mut file_state), Some(ref mut prompt)) =
143 (&mut self.file_open_state, &mut self.prompt)
144 {
145 let mut handler = FileBrowserInputHandler::new(file_state, prompt);
146 let result = handler.dispatch_input(event, &mut ctx);
147 self.process_deferred_actions(ctx);
148 return Some(result);
149 }
150 }
151
152 use crate::view::prompt::PromptType;
154 let is_query_replace_confirm = self
155 .prompt
156 .as_ref()
157 .is_some_and(|p| p.prompt_type == PromptType::QueryReplaceConfirm);
158 if is_query_replace_confirm {
159 let mut handler = QueryReplaceConfirmInputHandler::new();
160 let result = handler.dispatch_input(event, &mut ctx);
161 self.process_deferred_actions(ctx);
162 return Some(result);
163 }
164
165 if let Some(ref mut prompt) = self.prompt {
166 let result = prompt.dispatch_input(event, &mut ctx);
167 if result != InputResult::Ignored {
170 self.process_deferred_actions(ctx);
171 return Some(result);
172 }
173 }
174 }
175
176 if self.popups_capture_keys() {
182 if let Some(action) = self.resolve_completion_popup_action(event) {
187 self.process_deferred_actions(ctx);
188 if let Err(e) = self.handle_action(action) {
189 tracing::warn!("Completion popup action failed: {}", e);
190 }
191 return Some(InputResult::Consumed);
192 }
193
194 if self.global_popups.is_visible() {
198 let result = self.global_popups.dispatch_input(event, &mut ctx);
199 self.process_deferred_actions(ctx);
200 if result != InputResult::Ignored {
201 return Some(result);
202 }
203 return None;
206 }
207
208 if self.active_state().popups.is_visible() {
210 let result = self
211 .active_state_mut()
212 .popups
213 .dispatch_input(event, &mut ctx);
214 self.process_deferred_actions(ctx);
215 if result != InputResult::Ignored {
220 return Some(result);
221 }
222 }
223 }
224
225 None
226 }
227
228 pub fn process_deferred_actions(&mut self, ctx: InputContext) {
230 if let Some(msg) = ctx.status_message {
232 self.set_status_message(msg);
233 }
234
235 for action in ctx.deferred_actions {
237 if let Err(e) = self.execute_deferred_action(action) {
238 self.set_status_message(
239 t!("error.deferred_action", error = e.to_string()).to_string(),
240 );
241 }
242 }
243 }
244
245 fn execute_deferred_action(&mut self, action: DeferredAction) -> AnyhowResult<()> {
247 match action {
248 DeferredAction::CloseSettings { save } => {
250 if save {
251 self.save_settings();
252 }
253 self.close_settings(false);
254 }
255 DeferredAction::PasteToSettings => {
256 if let Some(text) = self.clipboard.paste() {
257 if !text.is_empty() {
258 if let Some(settings) = &mut self.settings_state {
259 if let Some(dialog) = settings.entry_dialog_mut() {
260 dialog.insert_str(&text);
261 }
262 }
263 }
264 }
265 }
266 DeferredAction::OpenConfigFile { layer } => {
267 self.open_config_file(layer)?;
268 }
269
270 DeferredAction::CloseMenu => {
272 self.close_menu_with_auto_hide();
273 }
274 DeferredAction::ExecuteMenuAction { action, args } => {
275 if let Some(kb_action) = self.menu_action_to_action(&action, args) {
277 self.handle_action(kb_action)?;
278 }
279 }
280
281 DeferredAction::ClosePrompt => {
283 self.cancel_prompt();
284 }
285 DeferredAction::ConfirmPrompt => {
286 self.handle_action(Action::PromptConfirm)?;
287 }
288 DeferredAction::UpdatePromptSuggestions => {
289 self.update_prompt_suggestions();
290 }
291 DeferredAction::PromptHistoryPrev => {
292 self.prompt_history_prev();
293 }
294 DeferredAction::PromptHistoryNext => {
295 self.prompt_history_next();
296 }
297 DeferredAction::PreviewThemeFromPrompt => {
298 if let Some(prompt) = &self.prompt {
299 if matches!(
300 prompt.prompt_type,
301 crate::view::prompt::PromptType::SelectTheme { .. }
302 ) {
303 let theme_name = prompt.input.clone();
304 self.preview_theme(&theme_name);
305 }
306 }
307 }
308 DeferredAction::PromptSelectionChanged { selected_index } => {
309 if let Some(prompt) = &self.prompt {
311 if let crate::view::prompt::PromptType::Plugin { custom_type } =
312 &prompt.prompt_type
313 {
314 self.plugin_manager.run_hook(
315 "prompt_selection_changed",
316 crate::services::plugins::hooks::HookArgs::PromptSelectionChanged {
317 prompt_type: custom_type.clone(),
318 selected_index,
319 },
320 );
321 }
322 }
323 }
324
325 DeferredAction::ClosePopup => {
327 self.handle_popup_cancel();
333 }
334 DeferredAction::ConfirmPopup => {
335 self.handle_action(Action::PopupConfirm)?;
336 }
337 DeferredAction::PopupTypeChar(c) => {
338 self.handle_popup_type_char(c);
339 }
340 DeferredAction::PopupBackspace => {
341 self.handle_popup_backspace();
342 }
343 DeferredAction::CopyToClipboard(text) => {
344 self.clipboard.copy(text);
345 self.set_status_message(t!("clipboard.copied").to_string());
346 }
347
348 DeferredAction::ExecuteAction(kb_action) => {
350 self.handle_action(kb_action)?;
351 }
352
353 DeferredAction::InsertCharAndUpdate(c) => {
355 if let Some(ref mut prompt) = self.prompt {
356 prompt.insert_char(c);
357 }
358 self.update_prompt_suggestions();
359 }
360
361 DeferredAction::FileBrowserSelectPrev => {
363 if let Some(state) = &mut self.file_open_state {
364 state.select_prev();
365 }
366 }
367 DeferredAction::FileBrowserSelectNext => {
368 if let Some(state) = &mut self.file_open_state {
369 state.select_next();
370 }
371 }
372 DeferredAction::FileBrowserPageUp => {
373 if let Some(state) = &mut self.file_open_state {
374 state.page_up(10);
375 }
376 }
377 DeferredAction::FileBrowserPageDown => {
378 if let Some(state) = &mut self.file_open_state {
379 state.page_down(10);
380 }
381 }
382 DeferredAction::FileBrowserConfirm => {
383 self.handle_file_open_action(&Action::PromptConfirm);
386 }
387 DeferredAction::FileBrowserAcceptSuggestion => {
388 self.handle_file_open_action(&Action::PromptAcceptSuggestion);
389 }
390 DeferredAction::FileBrowserGoParent => {
391 let parent = self
393 .file_open_state
394 .as_ref()
395 .and_then(|s| s.current_dir.parent())
396 .map(|p| p.to_path_buf());
397 if let Some(parent_path) = parent {
398 self.load_file_open_directory(parent_path);
399 }
400 }
401 DeferredAction::FileBrowserUpdateFilter => {
402 self.update_file_open_filter();
403 }
404 DeferredAction::FileBrowserToggleHidden => {
405 self.file_open_toggle_hidden();
406 }
407
408 DeferredAction::InteractiveReplaceKey(c) => {
410 self.handle_interactive_replace_key(c)?;
411 }
412 DeferredAction::CancelInteractiveReplace => {
413 self.cancel_prompt();
414 self.interactive_replace_state = None;
415 }
416
417 DeferredAction::ToggleKeyboardCapture => {
419 self.keyboard_capture = !self.keyboard_capture;
420 if self.keyboard_capture {
421 self.set_status_message(
422 "Keyboard capture ON - all keys go to terminal (F9 to toggle)".to_string(),
423 );
424 } else {
425 self.set_status_message(
426 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
427 );
428 }
429 }
430 DeferredAction::SendTerminalKey(code, modifiers) => {
431 self.send_terminal_key(code, modifiers);
432 }
433 DeferredAction::SendTerminalMouse {
434 col,
435 row,
436 kind,
437 modifiers,
438 } => {
439 self.send_terminal_mouse(col, row, kind, modifiers);
440 }
441 DeferredAction::ExitTerminalMode { explicit } => {
442 self.terminal_mode = false;
443 self.key_context = crate::input::keybindings::KeyContext::Normal;
444 if explicit {
445 self.terminal_mode_resume.remove(&self.active_buffer());
447 self.sync_terminal_to_buffer(self.active_buffer());
448 self.set_status_message(
449 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
450 );
451 }
452 }
453 DeferredAction::EnterScrollbackMode => {
454 self.terminal_mode = false;
455 self.key_context = crate::input::keybindings::KeyContext::Normal;
456 self.sync_terminal_to_buffer(self.active_buffer());
457 self.set_status_message(
458 "Scrollback mode - use PageUp/Down to scroll (Ctrl+Space to resume)"
459 .to_string(),
460 );
461 self.handle_action(Action::MovePageUp)?;
463 }
464 DeferredAction::EnterTerminalMode => {
465 self.enter_terminal_mode();
466 }
467 }
468
469 Ok(())
470 }
471
472 fn menu_action_to_action(
474 &self,
475 action_name: &str,
476 args: std::collections::HashMap<String, serde_json::Value>,
477 ) -> Option<Action> {
478 if let Some(action) = Action::from_str(action_name, &args) {
480 return Some(action);
481 }
482
483 Some(Action::PluginAction(action_name.to_string()))
485 }
486
487 fn prompt_history_prev(&mut self) {
489 let prompt_info = self
491 .prompt
492 .as_ref()
493 .map(|p| (p.prompt_type.clone(), p.input.clone()));
494
495 if let Some((prompt_type, current_input)) = prompt_info {
496 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
498 if let Some(history) = self.prompt_histories.get_mut(&key) {
499 if let Some(entry) = history.navigate_prev(¤t_input) {
500 if let Some(ref mut prompt) = self.prompt {
501 prompt.set_input(entry);
502 }
503 }
504 }
505 }
506 }
507 }
508
509 fn prompt_history_next(&mut self) {
511 let prompt_type = self.prompt.as_ref().map(|p| p.prompt_type.clone());
512
513 if let Some(prompt_type) = prompt_type {
514 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
516 if let Some(history) = self.prompt_histories.get_mut(&key) {
517 if let Some(entry) = history.navigate_next() {
518 if let Some(ref mut prompt) = self.prompt {
519 prompt.set_input(entry);
520 }
521 }
522 }
523 }
524 }
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 #[test]
533 fn test_deferred_action_close_menu() {
534 let action = DeferredAction::CloseMenu;
537 assert!(matches!(action, DeferredAction::CloseMenu));
538 }
539
540 #[test]
541 fn test_deferred_action_execute_menu_action() {
542 let action = DeferredAction::ExecuteMenuAction {
543 action: "save".to_string(),
544 args: std::collections::HashMap::new(),
545 };
546 if let DeferredAction::ExecuteMenuAction { action: name, .. } = action {
547 assert_eq!(name, "save");
548 } else {
549 panic!("Expected ExecuteMenuAction");
550 }
551 }
552}