1use super::*;
2use anyhow::Result as AnyhowResult;
3use rust_i18n::t;
4
5fn key_event_to_payload(ev: &crossterm::event::KeyEvent) -> fresh_core::api::KeyEventPayload {
14 use crossterm::event::{KeyCode, KeyModifiers};
15 let key = match ev.code {
16 KeyCode::Char(c) => c.to_string(),
17 KeyCode::Esc => "escape".to_string(),
18 KeyCode::Enter => "enter".to_string(),
19 KeyCode::Tab => "tab".to_string(),
20 KeyCode::BackTab => "backtab".to_string(),
21 KeyCode::Backspace => "backspace".to_string(),
22 KeyCode::Delete => "delete".to_string(),
23 KeyCode::Left => "left".to_string(),
24 KeyCode::Right => "right".to_string(),
25 KeyCode::Up => "up".to_string(),
26 KeyCode::Down => "down".to_string(),
27 KeyCode::Home => "home".to_string(),
28 KeyCode::End => "end".to_string(),
29 KeyCode::PageUp => "pageup".to_string(),
30 KeyCode::PageDown => "pagedown".to_string(),
31 KeyCode::Insert => "insert".to_string(),
32 KeyCode::F(n) => format!("f{}", n),
33 _ => String::new(),
34 };
35 fresh_core::api::KeyEventPayload {
36 key,
37 ctrl: ev.modifiers.contains(KeyModifiers::CONTROL),
38 alt: ev.modifiers.contains(KeyModifiers::ALT),
39 shift: ev.modifiers.contains(KeyModifiers::SHIFT),
40 meta: ev.modifiers.contains(KeyModifiers::SUPER),
41 }
42}
43
44impl Editor {
45 fn try_resolve_next_key_callback(&mut self, key_event: &crossterm::event::KeyEvent) -> bool {
58 let payload = key_event_to_payload(key_event);
59 if let Some(callback_id) = self.pending_next_key_callbacks.pop_front() {
60 let json = serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string());
61 self.plugin_manager.resolve_callback(callback_id, json);
62 return true;
63 }
64 if self.key_capture_active {
65 self.pending_key_capture_buffer.push_back(payload);
66 return true;
67 }
68 false
69 }
70}
71
72impl Editor {
73 pub(crate) fn popups_capture_keys(&self) -> bool {
92 use crate::input::keybindings::KeyContext;
93 if matches!(self.key_context, KeyContext::FileExplorer) {
94 return false;
95 }
96 self.topmost_popup_focused()
97 }
98
99 pub(crate) fn topmost_popup_focused(&self) -> bool {
104 if let Some(popup) = self.global_popups.top() {
105 return popup.focused;
106 }
107 if let Some(popup) = self.active_state().popups.top() {
108 return popup.focused;
109 }
110 false
113 }
114
115 pub(crate) fn resolve_unfocused_popup_action(
125 &self,
126 event: &crossterm::event::KeyEvent,
127 ) -> Option<crate::input::keybindings::Action> {
128 use crate::input::keybindings::{Action, KeyContext};
129
130 let popup_visible =
131 self.global_popups.is_visible() || self.active_state().popups.is_visible();
132 if !popup_visible || self.topmost_popup_focused() {
133 return None;
134 }
135
136 if self.settings_state.as_ref().is_some_and(|s| s.visible)
142 || self.menu_state.active_menu.is_some()
143 || self.is_prompting()
144 {
145 return None;
146 }
147
148 let kb = self.keybindings.read().ok()?;
149
150 let popup_focus_match = matches!(
159 kb.resolve_in_context_only(event, self.key_context.clone()),
160 Some(Action::PopupFocus),
161 );
162 if popup_focus_match {
163 return Some(Action::PopupFocus);
164 }
165
166 let resolved_popup = kb.resolve_in_context_only(event, KeyContext::Popup);
172 match resolved_popup {
173 Some(action @ (Action::PopupCancel | Action::PopupFocus)) => Some(action),
174 _ => None,
175 }
176 }
177
178 pub(crate) fn resolve_completion_popup_action(
184 &self,
185 event: &crossterm::event::KeyEvent,
186 ) -> Option<crate::input::keybindings::Action> {
187 use crate::input::keybindings::{Action, KeyContext};
188 use crate::view::popup::PopupKind;
189
190 let topmost_kind = if self.global_popups.is_visible() {
191 self.global_popups.top().map(|p| p.kind)
192 } else if self.active_state().popups.is_visible() {
193 self.active_state().popups.top().map(|p| p.kind)
194 } else {
195 None
196 };
197
198 if topmost_kind != Some(PopupKind::Completion) {
199 return None;
200 }
201
202 match self
203 .keybindings
204 .read()
205 .unwrap()
206 .resolve_in_context_only(event, KeyContext::Completion)
207 {
208 Some(action @ (Action::CompletionAccept | Action::CompletionDismiss)) => Some(action),
209 _ => None,
210 }
211 }
212
213 pub fn get_key_context(&self) -> crate::input::keybindings::KeyContext {
215 use crate::input::keybindings::KeyContext;
216
217 if self.settings_state.as_ref().is_some_and(|s| s.visible) {
221 KeyContext::Settings
222 } else if self.menu_state.active_menu.is_some() {
223 KeyContext::Menu
224 } else if self.is_prompting() {
225 KeyContext::Prompt
226 } else if self.popups_capture_keys()
227 && (self.global_popups.is_visible() || self.active_state().popups.is_visible())
228 {
229 KeyContext::Popup
230 } else if self.is_composite_buffer(self.active_buffer()) {
231 KeyContext::CompositeBuffer
232 } else {
233 self.key_context.clone()
235 }
236 }
237
238 pub fn handle_key(
241 &mut self,
242 code: crossterm::event::KeyCode,
243 modifiers: crossterm::event::KeyModifiers,
244 ) -> AnyhowResult<()> {
245 use crate::input::keybindings::Action;
246
247 let _t_total = std::time::Instant::now();
248
249 tracing::trace!(
250 "Editor.handle_key: code={:?}, modifiers={:?}",
251 code,
252 modifiers
253 );
254
255 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
257
258 if self.is_event_debug_active() {
262 self.handle_event_debug_input(&key_event);
263 return Ok(());
264 }
265
266 if self.dispatch_terminal_input(&key_event).is_some() {
268 return Ok(());
269 }
270
271 if self.try_resolve_next_key_callback(&key_event) {
278 return Ok(());
279 }
280
281 let active_split = self.effective_active_split();
290 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
291 view_state.viewport.clear_skip_ensure_visible();
292 }
293
294 if self.theme_info_popup.is_some() {
296 self.theme_info_popup = None;
297 }
298
299 if self.file_explorer_context_menu.is_some() {
300 if let Some(result) = self.handle_file_explorer_context_menu_key(code, modifiers) {
301 return result;
302 }
303 }
304
305 let mut context = self.get_key_context();
307
308 let popup_visible_on_screen =
318 self.global_popups.is_visible() || self.active_state().popups.is_visible();
319 if popup_visible_on_screen {
320 let (is_transient_popup, has_selection) = {
324 let popup = self
325 .global_popups
326 .top()
327 .or_else(|| self.active_state().popups.top());
328 (
329 popup.is_some_and(|p| p.transient),
330 popup.is_some_and(|p| p.has_selection()),
331 )
332 };
333
334 let is_copy_key = key_event.code == crossterm::event::KeyCode::Char('c')
336 && key_event
337 .modifiers
338 .contains(crossterm::event::KeyModifiers::CONTROL);
339
340 let resolved_action = self
345 .keybindings
346 .read()
347 .ok()
348 .map(|kb| kb.resolve(&key_event, context.clone()));
349 let is_focus_popup_key = matches!(
350 resolved_action,
351 Some(crate::input::keybindings::Action::PopupFocus)
352 );
353
354 if is_transient_popup && !(has_selection && is_copy_key) && !is_focus_popup_key {
355 self.hide_popup();
357 tracing::debug!("Dismissed transient popup on key press");
358 context = self.get_key_context();
360 }
361 }
362
363 if let Some(action) = self.resolve_unfocused_popup_action(&key_event) {
369 self.handle_action(action)?;
370 return Ok(());
371 }
372
373 if self.dispatch_modal_input(&key_event).is_some() {
375 return Ok(());
376 }
377
378 if context != self.get_key_context() {
381 context = self.get_key_context();
382 }
383
384 let should_check_mode_bindings =
388 matches!(context, crate::input::keybindings::KeyContext::Normal);
389
390 if should_check_mode_bindings {
391 let effective_mode = self.effective_mode().map(|s| s.to_owned());
394
395 if let Some(ref mode_name) = effective_mode {
396 let mode_ctx = crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
397 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
398
399 let (chord_result, resolved_action) = {
401 let keybindings = self.keybindings.read().unwrap();
402 let chord_result =
403 keybindings.resolve_chord(&self.chord_state, &key_event, mode_ctx.clone());
404 let resolved = keybindings.resolve(&key_event, mode_ctx);
405 (chord_result, resolved)
406 };
407 match chord_result {
408 crate::input::keybindings::ChordResolution::Complete(action) => {
409 tracing::debug!("Mode chord resolved to action: {:?}", action);
410 self.chord_state.clear();
411 return self.handle_action(action);
412 }
413 crate::input::keybindings::ChordResolution::Partial => {
414 tracing::debug!("Potential chord prefix in mode '{}'", mode_name);
415 self.chord_state.push((code, modifiers));
416 return Ok(());
417 }
418 crate::input::keybindings::ChordResolution::NoMatch => {
419 if !self.chord_state.is_empty() {
420 tracing::debug!("Chord sequence abandoned in mode, clearing state");
421 self.chord_state.clear();
422 }
423 }
424 }
425
426 if resolved_action != Action::None {
428 return self.handle_action(resolved_action);
429 }
430 }
431
432 if let Some(ref mode_name) = effective_mode {
444 if self.mode_registry.allows_text_input(mode_name) {
445 if let KeyCode::Char(c) = code {
446 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
447 c.to_uppercase().next().unwrap_or(c)
448 } else {
449 c
450 };
451 if !modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
452 let action_name = format!("mode_text_input:{}", ch);
453 return self.handle_action(Action::PluginAction(action_name));
454 }
455 }
456 tracing::debug!("Blocking unbound key in text-input mode '{}'", mode_name);
457 return Ok(());
458 }
459 }
460 if let Some(ref mode_name) = self.editor_mode {
461 if self.mode_registry.is_read_only(mode_name) {
462 tracing::debug!("Ignoring unbound key in read-only mode '{}'", mode_name);
463 return Ok(());
464 }
465 tracing::debug!(
466 "Mode '{}' is not read-only, allowing key through",
467 mode_name
468 );
469 }
470 }
471
472 {
479 let active_buf = self.active_buffer();
480 let active_split = self.effective_active_split();
481 if self.is_composite_buffer(active_buf) {
482 if let Some(handled) =
483 self.try_route_composite_key(active_split, active_buf, &key_event)
484 {
485 return handled;
486 }
487 }
488 }
489
490 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
492 let (chord_result, action) = {
493 let keybindings = self.keybindings.read().unwrap();
494 let chord_result =
495 keybindings.resolve_chord(&self.chord_state, &key_event, context.clone());
496 let action = keybindings.resolve(&key_event, context.clone());
497 (chord_result, action)
498 };
499
500 match chord_result {
501 crate::input::keybindings::ChordResolution::Complete(action) => {
502 tracing::debug!("Complete chord match -> Action: {:?}", action);
504 self.chord_state.clear();
505 return self.handle_action(action);
506 }
507 crate::input::keybindings::ChordResolution::Partial => {
508 tracing::debug!("Partial chord match - waiting for next key");
510 self.chord_state.push((code, modifiers));
511 return Ok(());
512 }
513 crate::input::keybindings::ChordResolution::NoMatch => {
514 if !self.chord_state.is_empty() {
516 tracing::debug!("Chord sequence abandoned, clearing state");
517 self.chord_state.clear();
518 }
519 }
520 }
521
522 tracing::trace!("Context: {:?} -> Action: {:?}", context, action);
524
525 match action {
528 Action::LspCompletion
529 | Action::LspGotoDefinition
530 | Action::LspReferences
531 | Action::LspHover
532 | Action::None => {
533 }
535 _ => {
536 self.cancel_pending_lsp_requests();
538 }
539 }
540
541 self.handle_action(action)
545 }
546
547 pub(crate) fn handle_action(&mut self, action: Action) -> AnyhowResult<()> {
550 use crate::input::keybindings::Action;
551
552 self.record_macro_action(&action);
554
555 if !matches!(action, Action::DabbrevExpand) {
557 self.reset_dabbrev_state();
558 }
559
560 match action {
561 Action::Quit => self.quit(),
562 Action::ForceQuit => {
563 self.should_quit = true;
564 }
565 Action::Detach => {
566 self.should_detach = true;
567 }
568 Action::Save => {
569 if self.active_state().buffer.file_path().is_none() {
571 self.start_prompt_with_initial_text(
572 t!("file.save_as_prompt").to_string(),
573 PromptType::SaveFileAs,
574 String::new(),
575 );
576 self.init_file_open_state();
577 } else if self.check_save_conflict().is_some() {
578 self.start_prompt(
580 t!("file.file_changed_prompt").to_string(),
581 PromptType::ConfirmSaveConflict,
582 );
583 } else if let Err(e) = self.save() {
584 let msg = format!("{}", e);
585 self.status_message = Some(t!("file.save_failed", error = &msg).to_string());
586 }
587 }
588 Action::SaveAs => {
589 let current_path = self
591 .active_state()
592 .buffer
593 .file_path()
594 .map(|p| {
595 p.strip_prefix(&self.working_dir)
597 .unwrap_or(p)
598 .to_string_lossy()
599 .to_string()
600 })
601 .unwrap_or_default();
602 self.start_prompt_with_initial_text(
603 t!("file.save_as_prompt").to_string(),
604 PromptType::SaveFileAs,
605 current_path,
606 );
607 self.init_file_open_state();
608 }
609 Action::Open => {
610 self.start_prompt(t!("file.open_prompt").to_string(), PromptType::OpenFile);
611 self.prefill_open_file_prompt();
612 self.init_file_open_state();
613 }
614 Action::SwitchProject => {
615 self.start_prompt(
616 t!("file.switch_project_prompt").to_string(),
617 PromptType::SwitchProject,
618 );
619 self.init_folder_open_state();
620 }
621 Action::GotoLine => {
622 let has_line_index = self
623 .buffers
624 .get(&self.active_buffer())
625 .is_none_or(|s| s.buffer.line_count().is_some());
626 if has_line_index {
627 self.start_prompt(
628 t!("file.goto_line_prompt").to_string(),
629 PromptType::GotoLine,
630 );
631 } else {
632 self.start_prompt(
633 t!("goto.scan_confirm_prompt", yes = "y", no = "N").to_string(),
634 PromptType::GotoLineScanConfirm,
635 );
636 }
637 }
638 Action::ScanLineIndex => {
639 self.start_incremental_line_scan(false);
640 }
641 Action::New => {
642 self.new_buffer();
643 }
644 Action::Close | Action::CloseTab => {
645 self.close_tab();
650 }
651 Action::Revert => {
652 if self.active_state().buffer.is_modified() {
654 let revert_key = t!("prompt.key.revert").to_string();
655 let cancel_key = t!("prompt.key.cancel").to_string();
656 self.start_prompt(
657 t!(
658 "prompt.revert_confirm",
659 revert_key = revert_key,
660 cancel_key = cancel_key
661 )
662 .to_string(),
663 PromptType::ConfirmRevert,
664 );
665 } else {
666 if let Err(e) = self.revert_file() {
668 self.set_status_message(
669 t!("error.failed_to_revert", error = e.to_string()).to_string(),
670 );
671 }
672 }
673 }
674 Action::ToggleAutoRevert => {
675 self.toggle_auto_revert();
676 }
677 Action::FormatBuffer => {
678 if let Err(e) = self.format_buffer() {
679 self.set_status_message(
680 t!("error.format_failed", error = e.to_string()).to_string(),
681 );
682 }
683 }
684 Action::TrimTrailingWhitespace => match self.trim_trailing_whitespace() {
685 Ok(true) => {
686 self.set_status_message(t!("whitespace.trimmed").to_string());
687 }
688 Ok(false) => {
689 self.set_status_message(t!("whitespace.no_trailing").to_string());
690 }
691 Err(e) => {
692 self.set_status_message(
693 t!("error.trim_whitespace_failed", error = e).to_string(),
694 );
695 }
696 },
697 Action::EnsureFinalNewline => match self.ensure_final_newline() {
698 Ok(true) => {
699 self.set_status_message(t!("whitespace.newline_added").to_string());
700 }
701 Ok(false) => {
702 self.set_status_message(t!("whitespace.already_has_newline").to_string());
703 }
704 Err(e) => {
705 self.set_status_message(
706 t!("error.ensure_newline_failed", error = e).to_string(),
707 );
708 }
709 },
710 Action::Copy => {
711 let popup = self
713 .global_popups
714 .top()
715 .or_else(|| self.active_state().popups.top());
716 if let Some(popup) = popup {
717 if popup.has_selection() {
718 if let Some(text) = popup.get_selected_text() {
719 self.clipboard.copy(text);
720 self.set_status_message(t!("clipboard.copied").to_string());
721 return Ok(());
722 }
723 }
724 }
725 if self.key_context == crate::input::keybindings::KeyContext::FileExplorer {
726 self.file_explorer_copy();
727 return Ok(());
728 }
729 let buffer_id = self.active_buffer();
731 if self.is_composite_buffer(buffer_id) {
732 if let Some(_handled) = self.handle_composite_action(buffer_id, &Action::Copy) {
733 return Ok(());
734 }
735 }
736 self.copy_selection()
737 }
738 Action::CopyWithTheme(theme) => self.copy_selection_with_theme(&theme),
739 Action::CopyFilePath => self.copy_active_buffer_path(false),
740 Action::CopyRelativeFilePath => self.copy_active_buffer_path(true),
741 Action::Cut => {
742 if self.key_context == crate::input::keybindings::KeyContext::FileExplorer {
743 self.file_explorer_cut();
744 return Ok(());
745 }
746 if self.is_editing_disabled() {
747 self.set_status_message(t!("buffer.editing_disabled").to_string());
748 return Ok(());
749 }
750 self.cut_selection()
751 }
752 Action::Paste => {
753 if self.key_context == crate::input::keybindings::KeyContext::FileExplorer {
754 self.file_explorer_paste();
755 return Ok(());
756 }
757 if self.is_editing_disabled() {
758 self.set_status_message(t!("buffer.editing_disabled").to_string());
759 return Ok(());
760 }
761 self.paste()
762 }
763 Action::YankWordForward => self.yank_word_forward(),
764 Action::YankWordBackward => self.yank_word_backward(),
765 Action::YankToLineEnd => self.yank_to_line_end(),
766 Action::YankToLineStart => self.yank_to_line_start(),
767 Action::YankViWordEnd => self.yank_vi_word_end(),
768 Action::Undo => {
769 self.handle_undo();
770 }
771 Action::Redo => {
772 self.handle_redo();
773 }
774 Action::ShowHelp => {
775 self.open_help_manual();
776 }
777 Action::ShowKeyboardShortcuts => {
778 self.open_keyboard_shortcuts();
779 }
780 Action::ShowWarnings => {
781 self.show_warnings_popup();
782 }
783 Action::ShowStatusLog => {
784 self.open_status_log();
785 }
786 Action::ShowLspStatus => {
787 self.show_lsp_status_popup();
788 }
789 Action::ShowRemoteIndicatorMenu => {
790 self.show_remote_indicator_popup();
791 }
792 Action::ClearWarnings => {
793 self.clear_warnings();
794 }
795 Action::CommandPalette => {
796 if let Some(prompt) = &self.prompt {
799 if prompt.prompt_type == PromptType::QuickOpen {
800 self.cancel_prompt();
801 return Ok(());
802 }
803 }
804 self.start_quick_open();
805 }
806 Action::QuickOpen => {
807 if let Some(prompt) = &self.prompt {
809 if prompt.prompt_type == PromptType::QuickOpen {
810 self.cancel_prompt();
811 return Ok(());
812 }
813 }
814
815 self.start_quick_open();
817 }
818 Action::QuickOpenBuffers => {
819 if let Some(prompt) = &self.prompt {
820 if prompt.prompt_type == PromptType::QuickOpen {
821 self.cancel_prompt();
822 return Ok(());
823 }
824 }
825 self.start_quick_open_with_prefix("#");
826 }
827 Action::QuickOpenFiles => {
828 if let Some(prompt) = &self.prompt {
829 if prompt.prompt_type == PromptType::QuickOpen {
830 self.cancel_prompt();
831 return Ok(());
832 }
833 }
834 self.start_quick_open_with_prefix("");
835 }
836 Action::ToggleLineWrap => {
837 let new_value = !self.config.editor.line_wrap;
838 self.config_mut().editor.line_wrap = new_value;
839
840 let leaf_ids: Vec<_> = self.split_view_states.keys().copied().collect();
843 for leaf_id in leaf_ids {
844 let buffer_id = self
845 .split_manager
846 .get_buffer_id(leaf_id.into())
847 .unwrap_or(BufferId(0));
848 let effective_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
849 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
850 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
851 view_state.viewport.line_wrap_enabled = effective_wrap;
852 view_state.viewport.wrap_indent = self.config.editor.wrap_indent;
853 view_state.viewport.wrap_column = wrap_column;
854 }
855 }
856
857 let state = if self.config.editor.line_wrap {
858 t!("view.state_enabled").to_string()
859 } else {
860 t!("view.state_disabled").to_string()
861 };
862 self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
863 }
864 Action::ToggleCurrentLineHighlight => {
865 let new_value = !self.config.editor.highlight_current_line;
866 self.config_mut().editor.highlight_current_line = new_value;
867
868 let leaf_ids: Vec<_> = self.split_view_states.keys().copied().collect();
870 for leaf_id in leaf_ids {
871 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
872 view_state.highlight_current_line =
873 self.config.editor.highlight_current_line;
874 }
875 }
876
877 let state = if self.config.editor.highlight_current_line {
878 t!("view.state_enabled").to_string()
879 } else {
880 t!("view.state_disabled").to_string()
881 };
882 self.set_status_message(
883 t!("view.current_line_highlight_state", state = state).to_string(),
884 );
885 }
886 Action::ToggleReadOnly => {
887 let buffer_id = self.active_buffer();
888 let is_now_read_only = self
889 .buffer_metadata
890 .get(&buffer_id)
891 .map(|m| !m.read_only)
892 .unwrap_or(false);
893 self.mark_buffer_read_only(buffer_id, is_now_read_only);
894
895 let state_str = if is_now_read_only {
896 t!("view.state_enabled").to_string()
897 } else {
898 t!("view.state_disabled").to_string()
899 };
900 self.set_status_message(t!("view.read_only_state", state = state_str).to_string());
901 }
902 Action::TogglePageView => {
903 self.handle_toggle_page_view();
904 }
905 Action::SetPageWidth => {
906 let active_split = self.split_manager.active_split();
907 let current = self
908 .split_view_states
909 .get(&active_split)
910 .and_then(|v| v.compose_width.map(|w| w.to_string()))
911 .unwrap_or_default();
912 self.start_prompt_with_initial_text(
913 "Page width (empty = viewport): ".to_string(),
914 PromptType::SetPageWidth,
915 current,
916 );
917 }
918 Action::SetBackground => {
919 let default_path = self
920 .ansi_background_path
921 .as_ref()
922 .and_then(|p| {
923 p.strip_prefix(&self.working_dir)
924 .ok()
925 .map(|rel| rel.to_string_lossy().to_string())
926 })
927 .unwrap_or_else(|| DEFAULT_BACKGROUND_FILE.to_string());
928
929 self.start_prompt_with_initial_text(
930 "Background file: ".to_string(),
931 PromptType::SetBackgroundFile,
932 default_path,
933 );
934 }
935 Action::SetBackgroundBlend => {
936 let default_amount = format!("{:.2}", self.background_fade);
937 self.start_prompt_with_initial_text(
938 "Background blend (0-1): ".to_string(),
939 PromptType::SetBackgroundBlend,
940 default_amount,
941 );
942 }
943 Action::LspCompletion => {
944 self.request_completion();
945 }
946 Action::DabbrevExpand => {
947 self.dabbrev_expand();
948 }
949 Action::LspGotoDefinition => {
950 self.request_goto_definition()?;
951 }
952 Action::LspRename => {
953 self.start_rename()?;
954 }
955 Action::LspHover => {
956 self.request_hover()?;
957 }
958 Action::LspReferences => {
959 self.request_references()?;
960 }
961 Action::LspSignatureHelp => {
962 self.request_signature_help();
963 }
964 Action::LspCodeActions => {
965 self.request_code_actions()?;
966 }
967 Action::LspRestart => {
968 self.handle_lsp_restart();
969 }
970 Action::LspStop => {
971 self.handle_lsp_stop();
972 }
973 Action::LspToggleForBuffer => {
974 self.handle_lsp_toggle_for_buffer();
975 }
976 Action::ToggleInlayHints => {
977 self.toggle_inlay_hints();
978 }
979 Action::DumpConfig => {
980 self.dump_config();
981 }
982 Action::RedrawScreen => {
983 self.request_full_redraw();
984 }
985 Action::SelectTheme => {
986 self.start_select_theme_prompt();
987 }
988 Action::InspectThemeAtCursor => {
989 self.inspect_theme_at_cursor();
990 }
991 Action::SelectKeybindingMap => {
992 self.start_select_keybinding_map_prompt();
993 }
994 Action::SelectCursorStyle => {
995 self.start_select_cursor_style_prompt();
996 }
997 Action::SelectLocale => {
998 self.start_select_locale_prompt();
999 }
1000 Action::Search => {
1001 let is_search_prompt = self.prompt.as_ref().is_some_and(|p| {
1003 matches!(
1004 p.prompt_type,
1005 PromptType::Search
1006 | PromptType::ReplaceSearch
1007 | PromptType::QueryReplaceSearch
1008 )
1009 });
1010
1011 if is_search_prompt {
1012 self.confirm_prompt();
1013 } else {
1014 self.start_search_prompt(
1015 t!("file.search_prompt").to_string(),
1016 PromptType::Search,
1017 false,
1018 );
1019 }
1020 }
1021 Action::Replace => {
1022 self.start_search_prompt(
1024 t!("file.replace_prompt").to_string(),
1025 PromptType::ReplaceSearch,
1026 false,
1027 );
1028 }
1029 Action::QueryReplace => {
1030 self.search_confirm_each = true;
1032 self.start_search_prompt(
1033 "Query replace: ".to_string(),
1034 PromptType::QueryReplaceSearch,
1035 false,
1036 );
1037 }
1038 Action::FindInSelection => {
1039 self.start_search_prompt(
1040 t!("file.search_prompt").to_string(),
1041 PromptType::Search,
1042 true,
1043 );
1044 }
1045 Action::FindNext => {
1046 self.find_next();
1047 }
1048 Action::FindPrevious => {
1049 self.find_previous();
1050 }
1051 Action::FindSelectionNext => {
1052 self.find_selection_next();
1053 }
1054 Action::FindSelectionPrevious => {
1055 self.find_selection_previous();
1056 }
1057 Action::AddCursorNextMatch => self.add_cursor_at_next_match(),
1058 Action::AddCursorAbove => self.add_cursor_above(),
1059 Action::AddCursorBelow => self.add_cursor_below(),
1060 Action::NextBuffer => self.next_buffer(),
1061 Action::PrevBuffer => self.prev_buffer(),
1062 Action::SwitchToPreviousTab => self.switch_to_previous_tab(),
1063 Action::SwitchToTabByName => self.start_switch_to_tab_prompt(),
1064
1065 Action::ScrollTabsLeft => {
1067 let active_split_id = self.split_manager.active_split();
1068 if let Some(view_state) = self.split_view_states.get_mut(&active_split_id) {
1069 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_sub(5);
1070 self.set_status_message(t!("status.scrolled_tabs_left").to_string());
1071 }
1072 }
1073 Action::ScrollTabsRight => {
1074 let active_split_id = self.split_manager.active_split();
1075 if let Some(view_state) = self.split_view_states.get_mut(&active_split_id) {
1076 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_add(5);
1077 self.set_status_message(t!("status.scrolled_tabs_right").to_string());
1078 }
1079 }
1080 Action::NavigateBack => self.navigate_back(),
1081 Action::NavigateForward => self.navigate_forward(),
1082 Action::SplitHorizontal => self.split_pane_horizontal(),
1083 Action::SplitVertical => self.split_pane_vertical(),
1084 Action::CloseSplit => self.close_active_split(),
1085 Action::NextSplit => self.next_split(),
1086 Action::PrevSplit => self.prev_split(),
1087 Action::IncreaseSplitSize => self.adjust_split_size(0.05),
1088 Action::DecreaseSplitSize => self.adjust_split_size(-0.05),
1089 Action::ToggleMaximizeSplit => self.toggle_maximize_split(),
1090 Action::ToggleFileExplorer => self.toggle_file_explorer(),
1091 Action::ToggleMenuBar => self.toggle_menu_bar(),
1092 Action::ToggleTabBar => self.toggle_tab_bar(),
1093 Action::ToggleStatusBar => self.toggle_status_bar(),
1094 Action::TogglePromptLine => self.toggle_prompt_line(),
1095 Action::ToggleVerticalScrollbar => self.toggle_vertical_scrollbar(),
1096 Action::ToggleHorizontalScrollbar => self.toggle_horizontal_scrollbar(),
1097 Action::ToggleLineNumbers => self.toggle_line_numbers(),
1098 Action::ToggleScrollSync => self.toggle_scroll_sync(),
1099 Action::ToggleMouseCapture => self.toggle_mouse_capture(),
1100 Action::ToggleMouseHover => self.toggle_mouse_hover(),
1101 Action::ToggleDebugHighlights => self.toggle_debug_highlights(),
1102 Action::AddRuler => {
1104 self.start_prompt(t!("rulers.add_prompt").to_string(), PromptType::AddRuler);
1105 }
1106 Action::RemoveRuler => {
1107 self.start_remove_ruler_prompt();
1108 }
1109 Action::SetTabSize => {
1111 let current = self
1112 .buffers
1113 .get(&self.active_buffer())
1114 .map(|s| s.buffer_settings.tab_size.to_string())
1115 .unwrap_or_else(|| "4".to_string());
1116 self.start_prompt_with_initial_text(
1117 "Tab size: ".to_string(),
1118 PromptType::SetTabSize,
1119 current,
1120 );
1121 }
1122 Action::SetLineEnding => {
1123 self.start_set_line_ending_prompt();
1124 }
1125 Action::SetEncoding => {
1126 self.start_set_encoding_prompt();
1127 }
1128 Action::ReloadWithEncoding => {
1129 self.start_reload_with_encoding_prompt();
1130 }
1131 Action::SetLanguage => {
1132 self.start_set_language_prompt();
1133 }
1134 Action::ToggleIndentationStyle => {
1135 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1136 state.buffer_settings.use_tabs = !state.buffer_settings.use_tabs;
1137 let status = if state.buffer_settings.use_tabs {
1138 "Indentation: Tabs"
1139 } else {
1140 "Indentation: Spaces"
1141 };
1142 self.set_status_message(status.to_string());
1143 }
1144 }
1145 Action::ToggleTabIndicators | Action::ToggleWhitespaceIndicators => {
1146 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1147 state.buffer_settings.whitespace.toggle_all();
1148 let status = if state.buffer_settings.whitespace.any_visible() {
1149 t!("toggle.whitespace_indicators_shown")
1150 } else {
1151 t!("toggle.whitespace_indicators_hidden")
1152 };
1153 self.set_status_message(status.to_string());
1154 }
1155 }
1156 Action::ResetBufferSettings => self.reset_buffer_settings(),
1157 Action::FocusFileExplorer => self.focus_file_explorer(),
1158 Action::FocusEditor => self.focus_editor(),
1159 Action::FileExplorerUp => self.file_explorer_navigate_up(),
1160 Action::FileExplorerDown => self.file_explorer_navigate_down(),
1161 Action::FileExplorerPageUp => self.file_explorer_page_up(),
1162 Action::FileExplorerPageDown => self.file_explorer_page_down(),
1163 Action::FileExplorerExpand => self.file_explorer_toggle_expand(),
1164 Action::FileExplorerCollapse => self.file_explorer_collapse(),
1165 Action::FileExplorerOpen => self.file_explorer_open_file()?,
1166 Action::FileExplorerRefresh => self.file_explorer_refresh(),
1167 Action::FileExplorerNewFile => self.file_explorer_new_file(),
1168 Action::FileExplorerNewDirectory => self.file_explorer_new_directory(),
1169 Action::FileExplorerDelete => self.file_explorer_delete(),
1170 Action::FileExplorerRename => self.file_explorer_rename(),
1171 Action::FileExplorerToggleHidden => self.file_explorer_toggle_hidden(),
1172 Action::FileExplorerToggleGitignored => self.file_explorer_toggle_gitignored(),
1173 Action::FileExplorerSearchClear => self.file_explorer_search_clear(),
1174 Action::FileExplorerSearchBackspace => self.file_explorer_search_pop_char(),
1175 Action::FileExplorerCopy => self.file_explorer_copy(),
1176 Action::FileExplorerCut => self.file_explorer_cut(),
1177 Action::FileExplorerPaste => self.file_explorer_paste(),
1178 Action::FileExplorerExtendSelectionUp => self.file_explorer_extend_selection_up(),
1179 Action::FileExplorerExtendSelectionDown => self.file_explorer_extend_selection_down(),
1180 Action::FileExplorerToggleSelect => self.file_explorer_toggle_select(),
1181 Action::FileExplorerSelectAll => self.file_explorer_select_all(),
1182 Action::RemoveSecondaryCursors => {
1183 if let Some(events) = self.action_to_events(Action::RemoveSecondaryCursors) {
1185 let batch = Event::Batch {
1187 events: events.clone(),
1188 description: "Remove secondary cursors".to_string(),
1189 };
1190 self.active_event_log_mut().append(batch.clone());
1191 self.apply_event_to_active_buffer(&batch);
1192
1193 let active_split = self.split_manager.active_split();
1195 let active_buffer = self.active_buffer();
1196 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1197 let state = self.buffers.get_mut(&active_buffer).unwrap();
1198 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
1199 }
1200 }
1201 }
1202
1203 Action::MenuActivate => {
1205 self.handle_menu_activate();
1206 }
1207 Action::MenuClose => {
1208 self.handle_menu_close();
1209 }
1210 Action::MenuLeft => {
1211 self.handle_menu_left();
1212 }
1213 Action::MenuRight => {
1214 self.handle_menu_right();
1215 }
1216 Action::MenuUp => {
1217 self.handle_menu_up();
1218 }
1219 Action::MenuDown => {
1220 self.handle_menu_down();
1221 }
1222 Action::MenuExecute => {
1223 if let Some(action) = self.handle_menu_execute() {
1224 return self.handle_action(action);
1225 }
1226 }
1227 Action::MenuOpen(menu_name) => {
1228 if self.config.editor.menu_bar_mnemonics {
1229 self.handle_menu_open(&menu_name);
1230 }
1231 }
1232
1233 Action::SwitchKeybindingMap(map_name) => {
1234 let is_builtin =
1236 matches!(map_name.as_str(), "default" | "emacs" | "vscode" | "macos");
1237 let is_user_defined = self.config.keybinding_maps.contains_key(&map_name);
1238
1239 if is_builtin || is_user_defined {
1240 self.config_mut().active_keybinding_map = map_name.clone().into();
1242
1243 *self.keybindings.write().unwrap() =
1245 crate::input::keybindings::KeybindingResolver::new(&self.config);
1246
1247 self.set_status_message(
1248 t!("view.keybindings_switched", map = map_name).to_string(),
1249 );
1250 } else {
1251 self.set_status_message(
1252 t!("view.keybindings_unknown", map = map_name).to_string(),
1253 );
1254 }
1255 }
1256
1257 Action::SmartHome => {
1258 let buffer_id = self.active_buffer();
1260 if self.is_composite_buffer(buffer_id) {
1261 if let Some(_handled) =
1262 self.handle_composite_action(buffer_id, &Action::SmartHome)
1263 {
1264 return Ok(());
1265 }
1266 }
1267 self.smart_home();
1268 }
1269 Action::ToggleComment => {
1270 self.toggle_comment();
1271 }
1272 Action::ToggleFold => {
1273 self.toggle_fold_at_cursor();
1274 }
1275 Action::GoToMatchingBracket => {
1276 self.goto_matching_bracket();
1277 }
1278 Action::JumpToNextError => {
1279 self.jump_to_next_error();
1280 }
1281 Action::JumpToPreviousError => {
1282 self.jump_to_previous_error();
1283 }
1284 Action::SetBookmark(key) => {
1285 self.set_bookmark(key);
1286 }
1287 Action::JumpToBookmark(key) => {
1288 self.jump_to_bookmark(key);
1289 }
1290 Action::ClearBookmark(key) => {
1291 self.clear_bookmark(key);
1292 }
1293 Action::ListBookmarks => {
1294 self.list_bookmarks();
1295 }
1296 Action::ToggleSearchCaseSensitive => {
1297 self.search_case_sensitive = !self.search_case_sensitive;
1298 let state = if self.search_case_sensitive {
1299 "enabled"
1300 } else {
1301 "disabled"
1302 };
1303 self.set_status_message(
1304 t!("search.case_sensitive_state", state = state).to_string(),
1305 );
1306 if let Some(prompt) = &self.prompt {
1309 if matches!(
1310 prompt.prompt_type,
1311 PromptType::Search
1312 | PromptType::ReplaceSearch
1313 | PromptType::QueryReplaceSearch
1314 ) {
1315 let query = prompt.input.clone();
1316 self.update_search_highlights(&query);
1317 }
1318 } else if let Some(search_state) = &self.search_state {
1319 let query = search_state.query.clone();
1320 self.perform_search(&query);
1321 }
1322 }
1323 Action::ToggleSearchWholeWord => {
1324 self.search_whole_word = !self.search_whole_word;
1325 let state = if self.search_whole_word {
1326 "enabled"
1327 } else {
1328 "disabled"
1329 };
1330 self.set_status_message(t!("search.whole_word_state", state = state).to_string());
1331 if let Some(prompt) = &self.prompt {
1334 if matches!(
1335 prompt.prompt_type,
1336 PromptType::Search
1337 | PromptType::ReplaceSearch
1338 | PromptType::QueryReplaceSearch
1339 ) {
1340 let query = prompt.input.clone();
1341 self.update_search_highlights(&query);
1342 }
1343 } else if let Some(search_state) = &self.search_state {
1344 let query = search_state.query.clone();
1345 self.perform_search(&query);
1346 }
1347 }
1348 Action::ToggleSearchRegex => {
1349 self.search_use_regex = !self.search_use_regex;
1350 let state = if self.search_use_regex {
1351 "enabled"
1352 } else {
1353 "disabled"
1354 };
1355 self.set_status_message(t!("search.regex_state", state = state).to_string());
1356 if let Some(prompt) = &self.prompt {
1359 if matches!(
1360 prompt.prompt_type,
1361 PromptType::Search
1362 | PromptType::ReplaceSearch
1363 | PromptType::QueryReplaceSearch
1364 ) {
1365 let query = prompt.input.clone();
1366 self.update_search_highlights(&query);
1367 }
1368 } else if let Some(search_state) = &self.search_state {
1369 let query = search_state.query.clone();
1370 self.perform_search(&query);
1371 }
1372 }
1373 Action::ToggleSearchConfirmEach => {
1374 self.search_confirm_each = !self.search_confirm_each;
1375 let state = if self.search_confirm_each {
1376 "enabled"
1377 } else {
1378 "disabled"
1379 };
1380 self.set_status_message(t!("search.confirm_each_state", state = state).to_string());
1381 }
1382 Action::FileBrowserToggleHidden => {
1383 self.file_open_toggle_hidden();
1385 }
1386 Action::StartMacroRecording => {
1387 self.set_status_message(
1389 "Use Ctrl+Shift+R to start recording (will prompt for register)".to_string(),
1390 );
1391 }
1392 Action::StopMacroRecording => {
1393 self.stop_macro_recording();
1394 }
1395 Action::PlayMacro(key) => {
1396 self.play_macro(key);
1397 }
1398 Action::ToggleMacroRecording(key) => {
1399 self.toggle_macro_recording(key);
1400 }
1401 Action::ShowMacro(key) => {
1402 self.show_macro_in_buffer(key);
1403 }
1404 Action::ListMacros => {
1405 self.list_macros_in_buffer();
1406 }
1407 Action::PromptRecordMacro => {
1408 self.start_prompt("Record macro (0-9): ".to_string(), PromptType::RecordMacro);
1409 }
1410 Action::PromptPlayMacro => {
1411 self.start_prompt("Play macro (0-9): ".to_string(), PromptType::PlayMacro);
1412 }
1413 Action::PlayLastMacro => {
1414 if let Some(key) = self.macros.last_register() {
1415 self.play_macro(key);
1416 } else {
1417 self.set_status_message(t!("status.no_macro_recorded").to_string());
1418 }
1419 }
1420 Action::PromptSetBookmark => {
1421 self.start_prompt("Set bookmark (0-9): ".to_string(), PromptType::SetBookmark);
1422 }
1423 Action::PromptJumpToBookmark => {
1424 self.start_prompt(
1425 "Jump to bookmark (0-9): ".to_string(),
1426 PromptType::JumpToBookmark,
1427 );
1428 }
1429 Action::CompositeNextHunk => {
1430 let buf = self.active_buffer();
1431 self.composite_next_hunk_active(buf);
1432 }
1433 Action::CompositePrevHunk => {
1434 let buf = self.active_buffer();
1435 self.composite_prev_hunk_active(buf);
1436 }
1437 Action::None => {}
1438 Action::DeleteBackward => {
1439 if self.is_editing_disabled() {
1440 self.set_status_message(t!("buffer.editing_disabled").to_string());
1441 return Ok(());
1442 }
1443 if let Some(events) = self.action_to_events(Action::DeleteBackward) {
1445 if events.len() > 1 {
1446 let description = "Delete backward".to_string();
1448 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description)
1449 {
1450 self.active_event_log_mut().append(bulk_edit);
1451 }
1452 } else {
1453 for event in events {
1454 self.active_event_log_mut().append(event.clone());
1455 self.apply_event_to_active_buffer(&event);
1456 }
1457 }
1458 }
1459 }
1460 Action::PluginAction(action_name) => {
1461 tracing::debug!("handle_action: PluginAction('{}')", action_name);
1462 #[cfg(feature = "plugins")]
1465 if let Some(result) = self.plugin_manager.execute_action_async(&action_name) {
1466 match result {
1467 Ok(receiver) => {
1468 self.pending_plugin_actions
1470 .push((action_name.clone(), receiver));
1471 }
1472 Err(e) => {
1473 self.set_status_message(
1474 t!("view.plugin_error", error = e.to_string()).to_string(),
1475 );
1476 tracing::error!("Plugin action error: {}", e);
1477 }
1478 }
1479 } else {
1480 self.set_status_message(t!("status.plugin_manager_unavailable").to_string());
1481 }
1482 #[cfg(not(feature = "plugins"))]
1483 {
1484 let _ = action_name;
1485 self.set_status_message(
1486 "Plugins not available (compiled without plugin support)".to_string(),
1487 );
1488 }
1489 }
1490 Action::LoadPluginFromBuffer => {
1491 #[cfg(feature = "plugins")]
1492 {
1493 let buffer_id = self.active_buffer();
1494 let state = self.active_state();
1495 let buffer = &state.buffer;
1496 let total = buffer.total_bytes();
1497 let content =
1498 String::from_utf8_lossy(&buffer.slice_bytes(0..total)).to_string();
1499
1500 let is_ts = buffer
1502 .file_path()
1503 .and_then(|p| p.extension())
1504 .and_then(|e| e.to_str())
1505 .map(|e| e == "ts" || e == "tsx")
1506 .unwrap_or(true);
1507
1508 let name = buffer
1510 .file_path()
1511 .and_then(|p| p.file_name())
1512 .and_then(|s| s.to_str())
1513 .map(|s| s.to_string())
1514 .unwrap_or_else(|| "buffer-plugin".to_string());
1515
1516 match self
1517 .plugin_manager
1518 .load_plugin_from_source(&content, &name, is_ts)
1519 {
1520 Ok(()) => {
1521 self.set_status_message(format!(
1522 "Plugin '{}' loaded from buffer",
1523 name
1524 ));
1525 }
1526 Err(e) => {
1527 self.set_status_message(format!("Failed to load plugin: {}", e));
1528 tracing::error!("LoadPluginFromBuffer error: {}", e);
1529 }
1530 }
1531
1532 self.setup_plugin_dev_lsp(buffer_id, &content);
1534 }
1535 #[cfg(not(feature = "plugins"))]
1536 {
1537 self.set_status_message(
1538 "Plugins not available (compiled without plugin support)".to_string(),
1539 );
1540 }
1541 }
1542 Action::InitReload => {
1543 self.load_init_script(true);
1548 self.fire_plugins_loaded_hook();
1551 }
1552 Action::InitEdit => {
1553 let config_dir = self.dir_context.config_dir.clone();
1556 match crate::init_script::ensure_starter(&config_dir) {
1557 Ok(path) => {
1558 let declarations = self.plugin_manager.plugin_declarations();
1568 crate::init_script::write_plugin_declarations(&config_dir, &declarations);
1569 match self.open_file(&path) {
1570 Ok(_) => {
1571 self.set_status_message(format!("init.ts: {}", path.display()));
1572 }
1573 Err(e) => {
1574 self.set_status_message(format!("init.ts: open failed: {e}"));
1575 }
1576 }
1577 }
1578 Err(e) => {
1579 self.set_status_message(format!("init.ts: create failed: {e}"));
1580 }
1581 }
1582 }
1583 Action::InitCheck => {
1584 let report = crate::init_script::check(&self.dir_context.config_dir);
1587 if report.ok && report.diagnostics.is_empty() {
1588 self.set_status_message("init.ts: ok".into());
1589 } else if !report.ok {
1590 let first = report
1591 .diagnostics
1592 .first()
1593 .map(|d| format!("{}:{}: {}", d.line, d.column, d.message))
1594 .unwrap_or_else(|| "unknown error".into());
1595 self.set_status_message(format!(
1596 "init.ts: {} error(s) — first: {first}",
1597 report.diagnostics.len()
1598 ));
1599 } else {
1600 self.set_status_message(format!(
1601 "init.ts: {} warning(s)",
1602 report.diagnostics.len()
1603 ));
1604 }
1605 }
1606 Action::OpenTerminal => {
1607 self.open_terminal();
1608 }
1609 Action::CloseTerminal => {
1610 self.close_terminal();
1611 }
1612 Action::FocusTerminal => {
1613 if self.is_terminal_buffer(self.active_buffer()) {
1615 self.terminal_mode = true;
1616 self.key_context = KeyContext::Terminal;
1617 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
1618 }
1619 }
1620 Action::TerminalEscape => {
1621 if self.terminal_mode {
1623 self.terminal_mode = false;
1624 self.key_context = KeyContext::Normal;
1625 self.set_status_message(t!("status.terminal_mode_disabled").to_string());
1626 }
1627 }
1628 Action::ToggleKeyboardCapture => {
1629 if self.terminal_mode {
1631 self.keyboard_capture = !self.keyboard_capture;
1632 if self.keyboard_capture {
1633 self.set_status_message(
1634 "Keyboard capture ON - all keys go to terminal (F9 to toggle)"
1635 .to_string(),
1636 );
1637 } else {
1638 self.set_status_message(
1639 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
1640 );
1641 }
1642 }
1643 }
1644 Action::TerminalPaste => {
1645 if self.terminal_mode {
1647 if let Some(text) = self.clipboard.paste() {
1648 self.send_terminal_input(text.as_bytes());
1649 }
1650 }
1651 }
1652 Action::ShellCommand => {
1653 self.start_shell_command_prompt(false);
1655 }
1656 Action::ShellCommandReplace => {
1657 self.start_shell_command_prompt(true);
1659 }
1660 Action::OpenSettings => {
1661 self.open_settings();
1662 }
1663 Action::CloseSettings => {
1664 let has_changes = self
1666 .settings_state
1667 .as_ref()
1668 .is_some_and(|s| s.has_changes());
1669 if has_changes {
1670 if let Some(ref mut state) = self.settings_state {
1672 state.show_confirm_dialog();
1673 }
1674 } else {
1675 self.close_settings(false);
1676 }
1677 }
1678 Action::SettingsSave => {
1679 self.save_settings();
1680 }
1681 Action::SettingsReset => {
1682 if let Some(ref mut state) = self.settings_state {
1683 state.reset_current_to_default();
1684 }
1685 }
1686 Action::SettingsInherit => {
1687 if let Some(ref mut state) = self.settings_state {
1688 state.set_current_to_null();
1689 }
1690 }
1691 Action::SettingsToggleFocus => {
1692 if let Some(ref mut state) = self.settings_state {
1693 state.toggle_focus();
1694 }
1695 }
1696 Action::SettingsActivate => {
1697 self.settings_activate_current();
1698 }
1699 Action::SettingsSearch => {
1700 if let Some(ref mut state) = self.settings_state {
1701 state.start_search();
1702 }
1703 }
1704 Action::SettingsHelp => {
1705 if let Some(ref mut state) = self.settings_state {
1706 state.toggle_help();
1707 }
1708 }
1709 Action::SettingsIncrement => {
1710 self.settings_increment_current();
1711 }
1712 Action::SettingsDecrement => {
1713 self.settings_decrement_current();
1714 }
1715 Action::CalibrateInput => {
1716 self.open_calibration_wizard();
1717 }
1718 Action::EventDebug => {
1719 self.open_event_debug();
1720 }
1721 Action::SuspendProcess => {
1722 self.request_suspend();
1723 }
1724 Action::OpenKeybindingEditor => {
1725 self.open_keybinding_editor();
1726 }
1727 Action::PromptConfirm => {
1728 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
1729 use super::prompt_actions::PromptResult;
1730 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
1731 PromptResult::ExecuteAction(action) => {
1732 return self.handle_action(action);
1733 }
1734 PromptResult::EarlyReturn => {
1735 return Ok(());
1736 }
1737 PromptResult::Done => {}
1738 }
1739 }
1740 }
1741 Action::PromptConfirmWithText(ref text) => {
1742 if let Some(ref mut prompt) = self.prompt {
1744 prompt.set_input(text.clone());
1745 self.update_prompt_suggestions();
1746 }
1747 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
1748 use super::prompt_actions::PromptResult;
1749 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
1750 PromptResult::ExecuteAction(action) => {
1751 return self.handle_action(action);
1752 }
1753 PromptResult::EarlyReturn => {
1754 return Ok(());
1755 }
1756 PromptResult::Done => {}
1757 }
1758 }
1759 }
1760 Action::PopupConfirm => {
1761 use super::popup_actions::PopupConfirmResult;
1762 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
1763 return Ok(());
1764 }
1765 }
1766 Action::PopupCancel => {
1767 self.handle_popup_cancel();
1768 }
1769 Action::PopupFocus => {
1770 self.handle_popup_focus();
1771 }
1772 Action::CompletionAccept => {
1773 use super::popup_actions::PopupConfirmResult;
1774 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
1775 return Ok(());
1776 }
1777 }
1778 Action::CompletionDismiss => {
1779 self.handle_popup_cancel();
1780 }
1781 Action::InsertChar(c) => {
1782 if self.is_prompting() {
1783 return self.handle_insert_char_prompt(c);
1784 } else if self.key_context == KeyContext::FileExplorer {
1785 self.file_explorer_search_push_char(c);
1786 } else {
1787 self.handle_insert_char_editor(c)?;
1788 }
1789 }
1790 Action::PromptCopy => {
1792 if let Some(prompt) = &self.prompt {
1793 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
1794 if !text.is_empty() {
1795 self.clipboard.copy(text);
1796 self.set_status_message(t!("clipboard.copied").to_string());
1797 }
1798 }
1799 }
1800 Action::PromptCut => {
1801 if let Some(prompt) = &self.prompt {
1802 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
1803 if !text.is_empty() {
1804 self.clipboard.copy(text);
1805 }
1806 }
1807 if let Some(prompt) = self.prompt.as_mut() {
1808 if prompt.has_selection() {
1809 prompt.delete_selection();
1810 } else {
1811 prompt.clear();
1812 }
1813 }
1814 self.set_status_message(t!("clipboard.cut").to_string());
1815 self.update_prompt_suggestions();
1816 }
1817 Action::PromptPaste => {
1818 if let Some(text) = self.clipboard.paste() {
1819 if let Some(prompt) = self.prompt.as_mut() {
1820 prompt.insert_str(&text);
1821 }
1822 self.update_prompt_suggestions();
1823 }
1824 }
1825 _ => {
1826 self.apply_action_as_events(action)?;
1832 }
1833 }
1834
1835 Ok(())
1836 }
1837}