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