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.active_state().popups.is_visible()
26 || self.menu_state.active_menu.is_some()
27 || self.settings_state.as_ref().is_some_and(|s| s.visible)
28 || self.calibration_wizard.is_some()
29 || self.keybinding_editor.is_some();
30
31 if in_modal {
32 return None;
33 }
34
35 if self.terminal_mode {
37 let mut ctx = InputContext::new();
38 let mut handler =
39 TerminalModeInputHandler::new(self.keyboard_capture, &self.keybindings);
40 let result = handler.dispatch_input(event, &mut ctx);
41 self.process_deferred_actions(ctx);
42 return Some(result);
43 }
44
45 if self.is_terminal_buffer(self.active_buffer()) && should_enter_terminal_mode(event) {
48 self.enter_terminal_mode();
49 self.send_terminal_key(event.code, event.modifiers);
51 return Some(InputResult::Consumed);
52 }
53
54 None
55 }
56
57 pub fn dispatch_modal_input(&mut self, event: &KeyEvent) -> Option<InputResult> {
62 let mut ctx = InputContext::new();
63
64 if let Some(ref mut settings) = self.settings_state {
66 if settings.visible {
67 let result = settings.dispatch_input(event, &mut ctx);
68 self.process_deferred_actions(ctx);
69 return Some(result);
70 }
71 }
72
73 if self.keybinding_editor.is_some() {
75 let result = self.handle_keybinding_editor_input(event);
76 return Some(result);
77 }
78
79 if self.calibration_wizard.is_some() {
81 let result = self.handle_calibration_input(event);
82 return Some(result);
83 }
84
85 if self.menu_state.active_menu.is_some() {
87 let all_menus: Vec<crate::config::Menu> = self
88 .menus
89 .menus
90 .iter()
91 .chain(self.menu_state.plugin_menus.iter())
92 .cloned()
93 .collect();
94
95 let mut handler = MenuInputHandler::new(&mut self.menu_state, &all_menus);
96 let result = handler.dispatch_input(event, &mut ctx);
97 self.process_deferred_actions(ctx);
98 return Some(result);
99 }
100
101 if self.prompt.is_some() {
103 if event
107 .modifiers
108 .contains(crossterm::event::KeyModifiers::ALT)
109 {
110 if let crossterm::event::KeyCode::Char(_) = event.code {
111 if let Some(action) = self.keybindings.resolve_in_context_only(
112 event,
113 crate::input::keybindings::KeyContext::Prompt,
114 ) {
115 if self.is_file_open_active() && self.handle_file_open_action(&action) {
117 return Some(InputResult::Consumed);
118 }
119 if let Err(e) = self.handle_action(action) {
121 tracing::warn!("Prompt action failed: {}", e);
122 }
123 return Some(InputResult::Consumed);
124 }
125 }
126 }
127
128 if self.is_file_open_active() {
130 if let (Some(ref mut file_state), Some(ref mut prompt)) =
131 (&mut self.file_open_state, &mut self.prompt)
132 {
133 let mut handler = FileBrowserInputHandler::new(file_state, prompt);
134 let result = handler.dispatch_input(event, &mut ctx);
135 self.process_deferred_actions(ctx);
136 return Some(result);
137 }
138 }
139
140 use crate::view::prompt::PromptType;
142 let is_query_replace_confirm = self
143 .prompt
144 .as_ref()
145 .is_some_and(|p| p.prompt_type == PromptType::QueryReplaceConfirm);
146 if is_query_replace_confirm {
147 let mut handler = QueryReplaceConfirmInputHandler::new();
148 let result = handler.dispatch_input(event, &mut ctx);
149 self.process_deferred_actions(ctx);
150 return Some(result);
151 }
152
153 if let Some(ref mut prompt) = self.prompt {
154 let result = prompt.dispatch_input(event, &mut ctx);
155 if result != InputResult::Ignored {
158 self.process_deferred_actions(ctx);
159 return Some(result);
160 }
161 }
162 }
163
164 if self.active_state().popups.is_visible() {
166 let result = self
167 .active_state_mut()
168 .popups
169 .dispatch_input(event, &mut ctx);
170 self.process_deferred_actions(ctx);
171 if result != InputResult::Ignored {
175 return Some(result);
176 }
177 }
178
179 None
180 }
181
182 pub fn process_deferred_actions(&mut self, ctx: InputContext) {
184 if let Some(msg) = ctx.status_message {
186 self.set_status_message(msg);
187 }
188
189 for action in ctx.deferred_actions {
191 if let Err(e) = self.execute_deferred_action(action) {
192 self.set_status_message(
193 t!("error.deferred_action", error = e.to_string()).to_string(),
194 );
195 }
196 }
197 }
198
199 fn execute_deferred_action(&mut self, action: DeferredAction) -> AnyhowResult<()> {
201 match action {
202 DeferredAction::CloseSettings { save } => {
204 if save {
205 self.save_settings();
206 }
207 self.close_settings(false);
208 }
209 DeferredAction::PasteToSettings => {
210 if let Some(text) = self.clipboard.paste() {
211 if !text.is_empty() {
212 if let Some(settings) = &mut self.settings_state {
213 if let Some(dialog) = settings.entry_dialog_mut() {
214 dialog.insert_str(&text);
215 }
216 }
217 }
218 }
219 }
220 DeferredAction::OpenConfigFile { layer } => {
221 self.open_config_file(layer)?;
222 }
223
224 DeferredAction::CloseMenu => {
226 self.close_menu_with_auto_hide();
227 }
228 DeferredAction::ExecuteMenuAction { action, args } => {
229 if let Some(kb_action) = self.menu_action_to_action(&action, args) {
231 self.handle_action(kb_action)?;
232 }
233 }
234
235 DeferredAction::ClosePrompt => {
237 self.cancel_prompt();
238 }
239 DeferredAction::ConfirmPrompt => {
240 self.handle_action(Action::PromptConfirm)?;
241 }
242 DeferredAction::UpdatePromptSuggestions => {
243 self.update_prompt_suggestions();
244 }
245 DeferredAction::PromptHistoryPrev => {
246 self.prompt_history_prev();
247 }
248 DeferredAction::PromptHistoryNext => {
249 self.prompt_history_next();
250 }
251 DeferredAction::PreviewThemeFromPrompt => {
252 if let Some(prompt) = &self.prompt {
253 if matches!(
254 prompt.prompt_type,
255 crate::view::prompt::PromptType::SelectTheme { .. }
256 ) {
257 let theme_name = prompt.input.clone();
258 self.preview_theme(&theme_name);
259 }
260 }
261 }
262 DeferredAction::PromptSelectionChanged { selected_index } => {
263 if let Some(prompt) = &self.prompt {
265 if let crate::view::prompt::PromptType::Plugin { custom_type } =
266 &prompt.prompt_type
267 {
268 self.plugin_manager.run_hook(
269 "prompt_selection_changed",
270 crate::services::plugins::hooks::HookArgs::PromptSelectionChanged {
271 prompt_type: custom_type.clone(),
272 selected_index,
273 },
274 );
275 }
276 }
277 }
278
279 DeferredAction::ClosePopup => {
281 self.hide_popup();
282 }
283 DeferredAction::ConfirmPopup => {
284 self.handle_action(Action::PopupConfirm)?;
285 }
286 DeferredAction::CompletionEnterKey => {
287 use crate::config::AcceptSuggestionOnEnter;
288 match self.config.editor.accept_suggestion_on_enter {
289 AcceptSuggestionOnEnter::On => {
290 self.handle_action(Action::PopupConfirm)?;
292 }
293 AcceptSuggestionOnEnter::Off => {
294 self.hide_popup();
296 self.handle_action(Action::InsertNewline)?;
297 }
298 AcceptSuggestionOnEnter::Smart => {
299 let should_accept = self
303 .active_state()
304 .popups
305 .top()
306 .and_then(|p| p.selected_item())
307 .map(|item| {
308 item.data.is_some()
310 })
311 .unwrap_or(false);
312
313 if should_accept {
314 self.handle_action(Action::PopupConfirm)?;
315 } else {
316 self.hide_popup();
317 self.handle_action(Action::InsertNewline)?;
318 }
319 }
320 }
321 }
322 DeferredAction::PopupTypeChar(c) => {
323 self.handle_popup_type_char(c);
324 }
325 DeferredAction::PopupBackspace => {
326 self.handle_popup_backspace();
327 }
328 DeferredAction::CopyToClipboard(text) => {
329 self.clipboard.copy(text);
330 self.set_status_message(t!("clipboard.copied").to_string());
331 }
332
333 DeferredAction::ExecuteAction(kb_action) => {
335 self.handle_action(kb_action)?;
336 }
337
338 DeferredAction::InsertCharAndUpdate(c) => {
340 if let Some(ref mut prompt) = self.prompt {
341 prompt.insert_char(c);
342 }
343 self.update_prompt_suggestions();
344 }
345
346 DeferredAction::FileBrowserSelectPrev => {
348 if let Some(state) = &mut self.file_open_state {
349 state.select_prev();
350 }
351 }
352 DeferredAction::FileBrowserSelectNext => {
353 if let Some(state) = &mut self.file_open_state {
354 state.select_next();
355 }
356 }
357 DeferredAction::FileBrowserPageUp => {
358 if let Some(state) = &mut self.file_open_state {
359 state.page_up(10);
360 }
361 }
362 DeferredAction::FileBrowserPageDown => {
363 if let Some(state) = &mut self.file_open_state {
364 state.page_down(10);
365 }
366 }
367 DeferredAction::FileBrowserConfirm => {
368 self.handle_file_open_action(&Action::PromptConfirm);
371 }
372 DeferredAction::FileBrowserAcceptSuggestion => {
373 self.handle_file_open_action(&Action::PromptAcceptSuggestion);
374 }
375 DeferredAction::FileBrowserGoParent => {
376 let parent = self
378 .file_open_state
379 .as_ref()
380 .and_then(|s| s.current_dir.parent())
381 .map(|p| p.to_path_buf());
382 if let Some(parent_path) = parent {
383 self.load_file_open_directory(parent_path);
384 }
385 }
386 DeferredAction::FileBrowserUpdateFilter => {
387 self.update_file_open_filter();
388 }
389 DeferredAction::FileBrowserToggleHidden => {
390 self.file_open_toggle_hidden();
391 }
392
393 DeferredAction::InteractiveReplaceKey(c) => {
395 self.handle_interactive_replace_key(c)?;
396 }
397 DeferredAction::CancelInteractiveReplace => {
398 self.cancel_prompt();
399 self.interactive_replace_state = None;
400 }
401
402 DeferredAction::ToggleKeyboardCapture => {
404 self.keyboard_capture = !self.keyboard_capture;
405 if self.keyboard_capture {
406 self.set_status_message(
407 "Keyboard capture ON - all keys go to terminal (F9 to toggle)".to_string(),
408 );
409 } else {
410 self.set_status_message(
411 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
412 );
413 }
414 }
415 DeferredAction::SendTerminalKey(code, modifiers) => {
416 self.send_terminal_key(code, modifiers);
417 }
418 DeferredAction::SendTerminalMouse {
419 col,
420 row,
421 kind,
422 modifiers,
423 } => {
424 self.send_terminal_mouse(col, row, kind, modifiers);
425 }
426 DeferredAction::ExitTerminalMode { explicit } => {
427 self.terminal_mode = false;
428 self.key_context = crate::input::keybindings::KeyContext::Normal;
429 if explicit {
430 self.terminal_mode_resume.remove(&self.active_buffer());
432 self.sync_terminal_to_buffer(self.active_buffer());
433 self.set_status_message(
434 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
435 );
436 }
437 }
438 DeferredAction::EnterScrollbackMode => {
439 self.terminal_mode = false;
440 self.key_context = crate::input::keybindings::KeyContext::Normal;
441 self.sync_terminal_to_buffer(self.active_buffer());
442 self.set_status_message(
443 "Scrollback mode - use PageUp/Down to scroll (Ctrl+Space to resume)"
444 .to_string(),
445 );
446 self.handle_action(Action::MovePageUp)?;
448 }
449 DeferredAction::EnterTerminalMode => {
450 self.enter_terminal_mode();
451 }
452 }
453
454 Ok(())
455 }
456
457 fn menu_action_to_action(
459 &self,
460 action_name: &str,
461 args: std::collections::HashMap<String, serde_json::Value>,
462 ) -> Option<Action> {
463 if let Some(action) = Action::from_str(action_name, &args) {
465 return Some(action);
466 }
467
468 Some(Action::PluginAction(action_name.to_string()))
470 }
471
472 fn prompt_history_prev(&mut self) {
474 let prompt_info = self
476 .prompt
477 .as_ref()
478 .map(|p| (p.prompt_type.clone(), p.input.clone()));
479
480 if let Some((prompt_type, current_input)) = prompt_info {
481 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
483 if let Some(history) = self.prompt_histories.get_mut(&key) {
484 if let Some(entry) = history.navigate_prev(¤t_input) {
485 if let Some(ref mut prompt) = self.prompt {
486 prompt.set_input(entry);
487 }
488 }
489 }
490 }
491 }
492 }
493
494 fn prompt_history_next(&mut self) {
496 let prompt_type = self.prompt.as_ref().map(|p| p.prompt_type.clone());
497
498 if let Some(prompt_type) = prompt_type {
499 if let Some(key) = Self::prompt_type_to_history_key(&prompt_type) {
501 if let Some(history) = self.prompt_histories.get_mut(&key) {
502 if let Some(entry) = history.navigate_next() {
503 if let Some(ref mut prompt) = self.prompt {
504 prompt.set_input(entry);
505 }
506 }
507 }
508 }
509 }
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516
517 #[test]
518 fn test_deferred_action_close_menu() {
519 let action = DeferredAction::CloseMenu;
522 assert!(matches!(action, DeferredAction::CloseMenu));
523 }
524
525 #[test]
526 fn test_deferred_action_execute_menu_action() {
527 let action = DeferredAction::ExecuteMenuAction {
528 action: "save".to_string(),
529 args: std::collections::HashMap::new(),
530 };
531 if let DeferredAction::ExecuteMenuAction { action: name, .. } = action {
532 assert_eq!(name, "save");
533 } else {
534 panic!("Expected ExecuteMenuAction");
535 }
536 }
537}