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