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
60 .active_window_mut()
61 .pending_next_key_callbacks
62 .pop_front()
63 {
64 let json = serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string());
65 self.plugin_manager
66 .read()
67 .unwrap()
68 .resolve_callback(callback_id, json);
69 return true;
70 }
71 if self.active_window_mut().key_capture_active {
72 self.active_window_mut()
73 .pending_key_capture_buffer
74 .push_back(payload);
75 return true;
76 }
77 false
78 }
79}
80
81impl Editor {
82 pub(crate) fn popups_capture_keys(&self) -> bool {
101 use crate::input::keybindings::KeyContext;
102 use crate::view::popup::PopupResolver;
103 let trust_prompt_up = self
109 .global_popups
110 .top()
111 .is_some_and(|p| p.focused && matches!(p.resolver, PopupResolver::WorkspaceTrust));
112 if trust_prompt_up {
113 return true;
114 }
115 if matches!(self.active_window().key_context, KeyContext::FileExplorer) {
116 return false;
117 }
118 self.topmost_popup_focused()
119 }
120
121 pub(crate) fn topmost_popup_focused(&self) -> bool {
126 if let Some(popup) = self.global_popups.top() {
127 return popup.focused;
128 }
129 if let Some(popup) = self.active_state().popups.top() {
130 return popup.focused;
131 }
132 false
135 }
136
137 pub(crate) fn resolve_unfocused_popup_action(
147 &self,
148 event: &crossterm::event::KeyEvent,
149 ) -> Option<crate::input::keybindings::Action> {
150 use crate::input::keybindings::{Action, KeyContext};
151
152 let popup_visible =
153 self.global_popups.is_visible() || self.active_state().popups.is_visible();
154 if !popup_visible || self.topmost_popup_focused() {
155 return None;
156 }
157
158 let blocked_by_higher_modal = self
170 .overlay_layers()
171 .iter()
172 .take_while(|l| l.kind != crate::app::overlay::LayerKind::Popup)
173 .any(|l| l.owns_keyboard);
174 if blocked_by_higher_modal {
175 return None;
176 }
177
178 let kb = self.keybindings.read().ok()?;
179
180 let popup_focus_match = matches!(
189 kb.resolve_in_context_only(event, self.active_window().key_context.clone()),
190 Some(Action::PopupFocus),
191 );
192 if popup_focus_match {
193 return Some(Action::PopupFocus);
194 }
195
196 let resolved_popup = kb.resolve_in_context_only(event, KeyContext::Popup);
202 match resolved_popup {
203 Some(action @ (Action::PopupCancel | Action::PopupFocus)) => Some(action),
204 _ => None,
205 }
206 }
207
208 pub(crate) fn resolve_completion_popup_action(
214 &self,
215 event: &crossterm::event::KeyEvent,
216 ) -> Option<crate::input::keybindings::Action> {
217 use crate::input::keybindings::{Action, KeyContext};
218 use crate::view::popup::PopupKind;
219
220 let topmost_kind = if self.global_popups.is_visible() {
221 self.global_popups.top().map(|p| p.kind)
222 } else if self.active_state().popups.is_visible() {
223 self.active_state().popups.top().map(|p| p.kind)
224 } else {
225 None
226 };
227
228 if topmost_kind != Some(PopupKind::Completion) {
229 return None;
230 }
231
232 match self
233 .keybindings
234 .read()
235 .unwrap()
236 .resolve_in_context_only(event, KeyContext::Completion)
237 {
238 Some(action @ (Action::CompletionAccept | Action::CompletionDismiss)) => Some(action),
239 _ => None,
240 }
241 }
242
243 pub(crate) fn overlay_layers(&self) -> Vec<crate::app::overlay::Layer> {
254 use crate::app::overlay::{Layer, LayerKind};
255 use crate::input::keybindings::KeyContext;
256
257 let mut layers = Vec::new();
258
259 if self.active_window().is_event_debug_active() {
263 layers.push(Layer {
264 kind: LayerKind::EventDebug,
265 owns_keyboard: true,
266 key_context: None,
267 blocks_terminal_input: true,
268 });
269 }
270 if self.settings_state.as_ref().is_some_and(|s| s.visible) {
272 layers.push(Layer {
273 kind: LayerKind::Settings,
274 owns_keyboard: true,
275 key_context: Some(KeyContext::Settings),
276 blocks_terminal_input: true,
277 });
278 }
279 if self.keybinding_editor.is_some() {
285 layers.push(Layer {
286 kind: LayerKind::KeybindingEditor,
287 owns_keyboard: true,
288 key_context: None,
289 blocks_terminal_input: true,
290 });
291 }
292 if self.calibration_wizard.is_some() {
293 layers.push(Layer {
294 kind: LayerKind::CalibrationWizard,
295 owns_keyboard: true,
296 key_context: None,
297 blocks_terminal_input: true,
298 });
299 }
300 let trust_on_top = self.global_popups.top().is_some_and(|p| {
306 matches!(
307 p.resolver,
308 crate::view::popup::PopupResolver::WorkspaceTrust
309 )
310 });
311 if trust_on_top {
312 layers.push(Layer {
313 kind: LayerKind::WorkspaceTrust,
314 owns_keyboard: self.popups_capture_keys(),
315 key_context: Some(KeyContext::Popup),
316 blocks_terminal_input: true,
317 });
318 }
319 if self.menu_state.active_menu.is_some() {
320 layers.push(Layer {
321 kind: LayerKind::Menu,
322 owns_keyboard: true,
323 key_context: Some(KeyContext::Menu),
324 blocks_terminal_input: true,
325 });
326 }
327 if self.is_prompting() {
328 layers.push(Layer {
329 kind: LayerKind::Prompt,
330 owns_keyboard: true,
331 key_context: Some(KeyContext::Prompt),
332 blocks_terminal_input: true,
333 });
334 }
335 if !trust_on_top
340 && (self.global_popups.is_visible() || self.active_state().popups.is_visible())
341 {
342 layers.push(Layer {
343 kind: LayerKind::Popup,
344 owns_keyboard: self.popups_capture_keys(),
345 key_context: Some(KeyContext::Popup),
346 blocks_terminal_input: true,
347 });
348 }
349 if let Some(f) = self.floating_widget_panel.as_ref() {
356 layers.push(Layer {
357 kind: LayerKind::FloatingModal,
358 owns_keyboard: f.focused,
359 key_context: Some(KeyContext::Normal),
360 blocks_terminal_input: true,
361 });
362 }
363 if let Some(d) = self.dock.as_ref() {
368 layers.push(Layer {
369 kind: LayerKind::Dock,
370 owns_keyboard: d.focused,
371 key_context: Some(KeyContext::Dock),
372 blocks_terminal_input: d.focused,
373 });
374 }
375 let base_context = if self
377 .active_window()
378 .is_composite_buffer(self.active_buffer())
379 {
380 KeyContext::CompositeBuffer
381 } else {
382 self.active_window().key_context.clone()
383 };
384 layers.push(Layer {
385 kind: LayerKind::Editor,
386 owns_keyboard: true,
387 key_context: Some(base_context),
388 blocks_terminal_input: false,
389 });
390
391 layers
392 }
393
394 pub(crate) fn presents_blocking_overlay(&self) -> bool {
398 crate::app::overlay::any_layer_blocks_terminal_input(&self.overlay_layers())
399 }
400
401 pub fn get_key_context(&self) -> crate::input::keybindings::KeyContext {
406 crate::app::overlay::resolve_focus_context(&self.overlay_layers())
407 .expect("editor base layer always owns the keyboard")
408 }
409
410 pub fn handle_key(
413 &mut self,
414 code: crossterm::event::KeyCode,
415 modifiers: crossterm::event::KeyModifiers,
416 ) -> AnyhowResult<()> {
417 use crate::input::keybindings::Action;
418
419 let _t_total = std::time::Instant::now();
420
421 tracing::trace!(
422 "Editor.handle_key: code={:?}, modifiers={:?}",
423 code,
424 modifiers
425 );
426
427 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
429
430 if let Some(focused) = self.dock.as_ref().map(|d| d.focused) {
438 tracing::debug!(
439 target: "fresh::dock",
440 ?code,
441 dock_focused = focused,
442 key_context = ?self.active_window().key_context,
443 active_window = ?self.active_window_id(),
444 "handle_key: dock mounted (routing diagnostic)"
445 );
446 }
447
448 if self.active_window().is_event_debug_active() {
452 self.active_window_mut()
453 .handle_event_debug_input(&key_event);
454 return Ok(());
455 }
456
457 if self.dispatch_terminal_input(&key_event).is_some() {
462 return Ok(());
463 }
464
465 if self.try_resolve_next_key_callback(&key_event) {
472 return Ok(());
473 }
474
475 if self
484 .floating_widget_panel
485 .as_ref()
486 .is_some_and(|f| f.focused)
487 && self.dispatch_floating_widget_key(super::PanelSlot::Floating, code, modifiers)
488 {
489 return Ok(());
490 }
491 if self.dock.as_ref().is_some_and(|f| f.focused) {
499 let ctx = self.get_key_context();
500 let resolved = self
501 .keybindings
502 .read()
503 .ok()
504 .map(|kb| kb.resolve(&key_event, ctx));
505 if matches!(resolved, Some(Action::ToggleDockFocus)) {
506 self.handle_action(Action::ToggleDockFocus)?;
507 return Ok(());
508 }
509 }
510 if self.dock.as_ref().is_some_and(|f| f.focused)
511 && self.dispatch_floating_widget_key(super::PanelSlot::Dock, code, modifiers)
512 {
513 return Ok(());
514 }
515
516 let active_split = self.effective_active_split();
525 if let Some(view_state) = self
526 .windows
527 .get_mut(&self.active_window)
528 .and_then(|w| w.split_view_states_mut())
529 .expect("active window must have a populated split layout")
530 .get_mut(&active_split)
531 {
532 view_state.viewport.clear_skip_ensure_visible();
533 }
534
535 if self.active_window_mut().theme_info_popup.is_some() {
537 self.active_window_mut().theme_info_popup = None;
538 }
539
540 if self
541 .active_window_mut()
542 .file_explorer_context_menu
543 .is_some()
544 {
545 if let Some(result) = self.handle_file_explorer_context_menu_key(code, modifiers) {
546 return result;
547 }
548 }
549
550 let mut context = self.get_key_context();
552
553 let popup_visible_on_screen =
563 self.global_popups.is_visible() || self.active_state().popups.is_visible();
564 if popup_visible_on_screen {
565 let (is_transient_popup, has_selection) = {
569 let popup = self
570 .global_popups
571 .top()
572 .or_else(|| self.active_state().popups.top());
573 (
574 popup.is_some_and(|p| p.transient),
575 popup.is_some_and(|p| p.has_selection()),
576 )
577 };
578
579 let is_copy_key = key_event.code == crossterm::event::KeyCode::Char('c')
581 && key_event
582 .modifiers
583 .contains(crossterm::event::KeyModifiers::CONTROL);
584
585 let resolved_action = self
590 .keybindings
591 .read()
592 .ok()
593 .map(|kb| kb.resolve(&key_event, context.clone()));
594 let is_focus_popup_key = matches!(
595 resolved_action,
596 Some(crate::input::keybindings::Action::PopupFocus)
597 );
598
599 if is_transient_popup && !(has_selection && is_copy_key) && !is_focus_popup_key {
600 self.hide_popup();
602 tracing::debug!("Dismissed transient popup on key press");
603 context = self.get_key_context();
605 }
606 }
607
608 if let Some(action) = self.resolve_unfocused_popup_action(&key_event) {
614 self.handle_action(action)?;
615 return Ok(());
616 }
617
618 if self.dispatch_modal_input(&key_event).is_some() {
620 return Ok(());
621 }
622
623 if context != self.get_key_context() {
626 context = self.get_key_context();
627 }
628
629 let should_check_mode_bindings = matches!(
640 context,
641 crate::input::keybindings::KeyContext::Normal
642 | crate::input::keybindings::KeyContext::CompositeBuffer
643 );
644
645 if should_check_mode_bindings {
646 let effective_mode = self.effective_mode().map(|s| s.to_owned());
649
650 if let Some(ref mode_name) = effective_mode {
651 let mode_ctx = crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
652 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
653
654 let (chord_result, resolved_action) = {
656 let keybindings = self.keybindings.read().unwrap();
657 let chord_result = keybindings.resolve_chord(
658 &self.active_window().chord_state,
659 &key_event,
660 mode_ctx.clone(),
661 );
662 let resolved = keybindings.resolve(&key_event, mode_ctx);
663 (chord_result, resolved)
664 };
665 match chord_result {
666 crate::input::keybindings::ChordResolution::Complete(action) => {
667 tracing::debug!("Mode chord resolved to action: {:?}", action);
668 self.active_window_mut().chord_state.clear();
669 return self.handle_action(action);
670 }
671 crate::input::keybindings::ChordResolution::Partial => {
672 tracing::debug!("Potential chord prefix in mode '{}'", mode_name);
673 self.active_window_mut().chord_state.push((code, modifiers));
674 return Ok(());
675 }
676 crate::input::keybindings::ChordResolution::NoMatch => {
677 if !self.active_window_mut().chord_state.is_empty() {
678 tracing::debug!("Chord sequence abandoned in mode, clearing state");
679 self.active_window_mut().chord_state.clear();
680 }
681 }
682 }
683
684 if resolved_action != Action::None {
686 return self.handle_action(resolved_action);
687 }
688 }
689
690 if let Some(ref mode_name) = effective_mode {
702 if self.mode_registry.allows_text_input(mode_name) {
703 if let KeyCode::Char(c) = code {
704 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
705 c.to_uppercase().next().unwrap_or(c)
706 } else {
707 c
708 };
709 if !modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
710 let action_name = format!("mode_text_input:{}", ch);
711 return self.handle_action(Action::PluginAction(action_name));
712 }
713 }
714 let normal_ctx = crate::input::keybindings::KeyContext::Normal;
723 let resolved = {
724 let keybindings = self.keybindings.read().unwrap();
725 keybindings.resolve(&key_event, normal_ctx)
726 };
727 match resolved {
728 Action::Paste | Action::Copy | Action::Cut | Action::SelectAll => {
729 return self.handle_action(resolved);
730 }
731 _ => {}
732 }
733 if modifiers.contains(KeyModifiers::SHIFT) {
742 let buffer_id = self.active_buffer();
743 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id)
744 {
745 let ctrl = modifiers.contains(KeyModifiers::CONTROL);
746 let handled = match code {
747 KeyCode::Left if ctrl => self
748 .with_focused_text_editor(panel_id, |e| {
749 e.move_word_left_selecting()
750 }),
751 KeyCode::Right if ctrl => self
752 .with_focused_text_editor(panel_id, |e| {
753 e.move_word_right_selecting()
754 }),
755 KeyCode::Left => self.with_focused_text_editor(panel_id, |e| {
756 e.move_left_selecting()
757 }),
758 KeyCode::Right => self.with_focused_text_editor(panel_id, |e| {
759 e.move_right_selecting()
760 }),
761 KeyCode::Up => self
762 .with_focused_text_editor(panel_id, |e| e.move_up_selecting()),
763 KeyCode::Down => self.with_focused_text_editor(panel_id, |e| {
764 e.move_down_selecting()
765 }),
766 KeyCode::Home => self.with_focused_text_editor(panel_id, |e| {
767 e.move_home_selecting()
768 }),
769 KeyCode::End => self
770 .with_focused_text_editor(panel_id, |e| e.move_end_selecting()),
771 _ => false,
772 };
773 if matches!(
779 code,
780 KeyCode::Left
781 | KeyCode::Right
782 | KeyCode::Up
783 | KeyCode::Down
784 | KeyCode::Home
785 | KeyCode::End
786 ) {
787 let _ = handled;
788 return Ok(());
789 }
790 }
791 }
792 tracing::debug!("Blocking unbound key in text-input mode '{}'", mode_name);
793 return Ok(());
794 }
795 }
796 if let Some(ref mode_name) = self.active_window().editor_mode {
797 if self.mode_registry.is_read_only(mode_name) {
798 tracing::debug!("Ignoring unbound key in read-only mode '{}'", mode_name);
799 return Ok(());
800 }
801 tracing::debug!(
802 "Mode '{}' is not read-only, allowing key through",
803 mode_name
804 );
805 }
806 }
807
808 {
815 let active_buf = self.active_buffer();
816 let active_split = self.effective_active_split();
817 if self.active_window().is_composite_buffer(active_buf) {
818 if let Some(handled) =
819 self.try_route_composite_key(active_split, active_buf, &key_event)
820 {
821 return handled;
822 }
823 }
824 }
825
826 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
828 let (chord_result, action) = {
829 let keybindings = self.keybindings.read().unwrap();
830 let chord_result = keybindings.resolve_chord(
831 &self.active_window().chord_state,
832 &key_event,
833 context.clone(),
834 );
835 let action = keybindings.resolve(&key_event, context.clone());
836 (chord_result, action)
837 };
838
839 match chord_result {
840 crate::input::keybindings::ChordResolution::Complete(action) => {
841 tracing::debug!("Complete chord match -> Action: {:?}", action);
843 self.active_window_mut().chord_state.clear();
844 return self.handle_action(action);
845 }
846 crate::input::keybindings::ChordResolution::Partial => {
847 tracing::debug!("Partial chord match - waiting for next key");
849 self.active_window_mut().chord_state.push((code, modifiers));
850 return Ok(());
851 }
852 crate::input::keybindings::ChordResolution::NoMatch => {
853 if !self.active_window_mut().chord_state.is_empty() {
855 tracing::debug!("Chord sequence abandoned, clearing state");
856 self.active_window_mut().chord_state.clear();
857 }
858 }
859 }
860
861 tracing::trace!("Context: {:?} -> Action: {:?}", context, action);
863
864 match action {
867 Action::LspCompletion
868 | Action::LspGotoDefinition
869 | Action::LspReferences
870 | Action::LspHover
871 | Action::None => {
872 }
874 _ => {
875 self.active_window_mut().cancel_pending_lsp_requests();
877 }
878 }
879
880 self.handle_action(action)
884 }
885
886 pub(crate) fn set_workspace_trust_level(
894 &mut self,
895 level: crate::services::workspace_trust::TrustLevel,
896 ) {
897 use crate::services::workspace_trust::TrustLevel;
898 let trust = &self.authority.workspace_trust;
899 let changed = trust.level() != level;
900 trust.set_level(level);
901 let msg = match level {
902 TrustLevel::Trusted => t!("trust.now_trusted"),
903 TrustLevel::Restricted => t!("trust.now_restricted"),
904 TrustLevel::Blocked => t!("trust.now_blocked"),
905 }
906 .to_string();
907 self.active_window_mut().status_message = Some(msg);
908 if changed {
911 self.request_restart(self.working_dir().to_path_buf());
912 }
913 }
914
915 pub(crate) fn handle_action(&mut self, action: Action) -> AnyhowResult<()> {
916 use crate::input::keybindings::Action;
917
918 self.record_macro_action(&action);
920
921 if !matches!(action, Action::DabbrevExpand) {
923 self.reset_dabbrev_state();
924 }
925
926 match action {
927 Action::Quit => self.quit(),
928 Action::ForceQuit => {
929 self.should_quit = true;
930 }
931 Action::Detach => {
932 self.should_detach = true;
933 }
934 Action::WorkspaceTrustTrust => {
935 self.set_workspace_trust_level(
936 crate::services::workspace_trust::TrustLevel::Trusted,
937 );
938 }
939 Action::WorkspaceTrustRestrict => {
940 self.set_workspace_trust_level(
941 crate::services::workspace_trust::TrustLevel::Restricted,
942 );
943 }
944 Action::WorkspaceTrustBlock => {
945 self.set_workspace_trust_level(
946 crate::services::workspace_trust::TrustLevel::Blocked,
947 );
948 }
949 Action::WorkspaceTrustPrompt => {
950 self.show_workspace_trust_popup(true);
952 }
953 Action::Save => {
954 if self.active_state().buffer.file_path().is_none() {
956 self.start_prompt_with_initial_text(
957 t!("file.save_as_prompt").to_string(),
958 PromptType::SaveFileAs,
959 String::new(),
960 );
961 self.init_file_open_state();
962 } else if self.check_save_conflict().is_some() {
963 self.start_prompt(
965 t!("file.file_changed_prompt").to_string(),
966 PromptType::ConfirmSaveConflict,
967 );
968 } else if let Err(e) = self.save() {
969 let msg = format!("{}", e);
970 self.active_window_mut().status_message =
971 Some(t!("file.save_failed", error = &msg).to_string());
972 }
973 }
974 Action::SaveAs => {
975 let current_path = self
977 .active_state()
978 .buffer
979 .file_path()
980 .map(|p| {
981 p.strip_prefix(self.working_dir())
983 .unwrap_or(p)
984 .to_string_lossy()
985 .to_string()
986 })
987 .unwrap_or_default();
988 self.start_prompt_with_initial_text(
989 t!("file.save_as_prompt").to_string(),
990 PromptType::SaveFileAs,
991 current_path,
992 );
993 self.init_file_open_state();
994 }
995 Action::Open => {
996 self.start_prompt(t!("file.open_prompt").to_string(), PromptType::OpenFile);
997 self.prefill_open_file_prompt();
998 self.init_file_open_state();
999 }
1000 Action::SwitchProject => {
1001 self.start_prompt(
1002 t!("file.switch_project_prompt").to_string(),
1003 PromptType::SwitchProject,
1004 );
1005 self.init_folder_open_state();
1006 }
1007 Action::GotoLine => {
1008 let has_line_index = self
1009 .buffers()
1010 .get(&self.active_buffer())
1011 .is_none_or(|s| s.buffer.line_count().is_some());
1012 if has_line_index {
1013 self.start_prompt(
1014 t!("file.goto_line_prompt").to_string(),
1015 PromptType::GotoLine,
1016 );
1017 } else {
1018 self.start_prompt(
1019 t!("goto.scan_confirm_prompt", yes = "y", no = "N").to_string(),
1020 PromptType::GotoLineScanConfirm,
1021 );
1022 }
1023 }
1024 Action::ScanLineIndex => {
1025 self.start_incremental_line_scan(false);
1026 }
1027 Action::New => {
1028 self.new_buffer();
1029 }
1030 Action::Close | Action::CloseTab => {
1031 self.close_tab();
1036 }
1037 Action::Revert => {
1038 if self.active_state().buffer.is_modified() {
1040 let revert_key = t!("prompt.key.revert").to_string();
1041 let cancel_key = t!("prompt.key.cancel").to_string();
1042 self.start_prompt(
1043 t!(
1044 "prompt.revert_confirm",
1045 revert_key = revert_key,
1046 cancel_key = cancel_key
1047 )
1048 .to_string(),
1049 PromptType::ConfirmRevert,
1050 );
1051 } else {
1052 if let Err(e) = self.revert_file() {
1054 self.set_status_message(
1055 t!("error.failed_to_revert", error = e.to_string()).to_string(),
1056 );
1057 }
1058 }
1059 }
1060 Action::ToggleAutoRevert => {
1061 self.toggle_auto_revert();
1062 }
1063 Action::FormatBuffer => {
1064 if let Err(e) = self.format_buffer() {
1065 self.set_status_message(
1066 t!("error.format_failed", error = e.to_string()).to_string(),
1067 );
1068 }
1069 }
1070 Action::TrimTrailingWhitespace => match self.trim_trailing_whitespace() {
1071 Ok(true) => {
1072 self.set_status_message(t!("whitespace.trimmed").to_string());
1073 }
1074 Ok(false) => {
1075 self.set_status_message(t!("whitespace.no_trailing").to_string());
1076 }
1077 Err(e) => {
1078 self.set_status_message(
1079 t!("error.trim_whitespace_failed", error = e).to_string(),
1080 );
1081 }
1082 },
1083 Action::EnsureFinalNewline => match self.ensure_final_newline() {
1084 Ok(true) => {
1085 self.set_status_message(t!("whitespace.newline_added").to_string());
1086 }
1087 Ok(false) => {
1088 self.set_status_message(t!("whitespace.already_has_newline").to_string());
1089 }
1090 Err(e) => {
1091 self.set_status_message(
1092 t!("error.ensure_newline_failed", error = e).to_string(),
1093 );
1094 }
1095 },
1096 Action::Copy => {
1097 let popup = self
1099 .global_popups
1100 .top()
1101 .or_else(|| self.active_state().popups.top());
1102 if let Some(popup) = popup {
1103 if popup.has_selection() {
1104 if let Some(text) = popup.get_selected_text() {
1105 self.clipboard.copy(text);
1106 self.set_status_message(t!("clipboard.copied").to_string());
1107 return Ok(());
1108 }
1109 }
1110 }
1111 if self.active_window_mut().key_context
1112 == crate::input::keybindings::KeyContext::FileExplorer
1113 {
1114 self.active_window_mut().file_explorer_copy();
1115 return Ok(());
1116 }
1117 let buffer_id = self.active_buffer();
1124 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1125 if self.handle_widget_copy(panel_id) {
1126 self.set_status_message(t!("clipboard.copied").to_string());
1127 return Ok(());
1128 }
1129 }
1130 if self.active_window().is_composite_buffer(buffer_id) {
1132 if let Some(_handled) = self.handle_composite_action(buffer_id, &Action::Copy) {
1133 return Ok(());
1134 }
1135 }
1136 self.copy_selection()
1137 }
1138 Action::CopyWithTheme(theme) => self.copy_selection_with_theme(&theme),
1139 Action::CopyFilePath => self.copy_active_buffer_path(false),
1140 Action::CopyRelativeFilePath => self.copy_active_buffer_path(true),
1141 Action::Cut => {
1142 if self.active_window_mut().key_context
1143 == crate::input::keybindings::KeyContext::FileExplorer
1144 {
1145 self.active_window_mut().file_explorer_cut();
1146 return Ok(());
1147 }
1148 let buffer_id = self.active_buffer();
1152 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1153 if self.handle_widget_cut(panel_id) {
1154 return Ok(());
1155 }
1156 }
1157 if self.active_window().is_editing_disabled() {
1158 self.set_status_message(t!("buffer.editing_disabled").to_string());
1159 return Ok(());
1160 }
1161 self.cut_selection()
1162 }
1163 Action::Paste => {
1164 if self.active_window_mut().key_context
1165 == crate::input::keybindings::KeyContext::FileExplorer
1166 {
1167 self.file_explorer_paste();
1168 return Ok(());
1169 }
1170 let buffer_id = self.active_buffer();
1176 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1177 if let Some(text) = self.clipboard.paste() {
1178 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
1179 self.handle_widget_insert_str(panel_id, &normalized);
1180 self.set_status_message(t!("clipboard.pasted").to_string());
1181 }
1182 return Ok(());
1183 }
1184 if self.active_window().is_editing_disabled() {
1185 self.set_status_message(t!("buffer.editing_disabled").to_string());
1186 return Ok(());
1187 }
1188 self.paste()
1189 }
1190 Action::SelectAll => {
1191 let buffer_id = self.active_buffer();
1196 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1197 self.handle_widget_select_all(panel_id);
1198 return Ok(());
1199 }
1200 self.apply_action_as_events(Action::SelectAll)?;
1201 }
1202 Action::YankWordForward => self.yank_word_forward(),
1203 Action::YankWordBackward => self.yank_word_backward(),
1204 Action::YankToLineEnd => self.yank_to_line_end(),
1205 Action::YankToLineStart => self.yank_to_line_start(),
1206 Action::YankViWordEnd => self.yank_vi_word_end(),
1207 Action::Undo => {
1208 self.handle_undo();
1209 }
1210 Action::Redo => {
1211 self.handle_redo();
1212 }
1213 Action::ShowHelp => {
1214 self.active_window_mut().open_help_manual();
1215 }
1216 Action::ShowKeyboardShortcuts => {
1217 self.active_window_mut().open_keyboard_shortcuts();
1218 }
1219 Action::ShowWarnings => {
1220 self.show_warnings_popup();
1221 }
1222 Action::ShowStatusLog => {
1223 self.open_status_log();
1224 }
1225 Action::ShowLspStatus => {
1226 self.show_lsp_status_popup();
1227 }
1228 Action::ShowRemoteIndicatorMenu => {
1229 self.show_remote_indicator_popup();
1230 }
1231 Action::ClearWarnings => {
1232 self.active_window_mut().clear_warnings();
1233 }
1234 Action::CommandPalette => {
1235 if self.close_quick_open_if_open() {
1238 return Ok(());
1239 }
1240 self.start_quick_open();
1241 }
1242 Action::QuickOpen => {
1243 if self.close_quick_open_if_open() {
1244 return Ok(());
1245 }
1246 self.start_quick_open();
1247 }
1248 Action::QuickOpenBuffers => {
1249 if self.close_quick_open_if_open() {
1250 return Ok(());
1251 }
1252 self.start_quick_open_with_prefix("#");
1253 }
1254 Action::QuickOpenFiles => {
1255 if self.close_quick_open_if_open() {
1256 return Ok(());
1257 }
1258 self.start_quick_open_with_prefix("");
1259 }
1260 Action::OpenLiveGrep => {
1261 self.handle_action(Action::PluginAction("start_live_grep".to_string()))?;
1262 }
1263 Action::ResumeLiveGrep => {
1264 self.handle_action(Action::PluginAction("resume_live_grep".to_string()))?;
1265 }
1266 Action::ToggleUtilityDock => {
1267 use crate::view::split::SplitRole;
1268 if let Some(dock_leaf) = self
1269 .windows
1270 .get(&self.active_window)
1271 .and_then(|w| w.buffers.splits())
1272 .map(|(mgr, _)| mgr)
1273 .expect("active window must have a populated split layout")
1274 .find_leaf_by_role(SplitRole::UtilityDock)
1275 {
1276 let active = self
1277 .windows
1278 .get(&self.active_window)
1279 .and_then(|w| w.buffers.splits())
1280 .map(|(mgr, _)| mgr)
1281 .expect("active window must have a populated split layout")
1282 .active_split();
1283 if active == dock_leaf {
1284 self.next_split();
1289 } else {
1290 self.windows
1291 .get_mut(&self.active_window)
1292 .and_then(|w| w.split_manager_mut())
1293 .expect("active window must have a populated split layout")
1294 .set_active_split(dock_leaf);
1295 }
1296 } else {
1297 self.set_status_message(
1298 "No Utility Dock open — invoke a dock-aware utility (Diagnostics, Search/Replace, …)"
1299 .to_string(),
1300 );
1301 }
1302 }
1303 Action::CycleLiveGrepProvider => {
1304 let in_live_grep = self
1308 .active_window()
1309 .prompt
1310 .as_ref()
1311 .map(|p| match &p.prompt_type {
1312 PromptType::LiveGrep => true,
1313 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1314 _ => false,
1315 })
1316 .unwrap_or(false);
1317 if !in_live_grep {
1318 self.set_status_message(
1319 "Cycle Live Grep provider only works inside Live Grep".to_string(),
1320 );
1321 return Ok(());
1322 }
1323 self.handle_action(Action::PluginAction("live_grep_cycle_provider".to_string()))?;
1324 }
1325 Action::OpenTerminalInDock => {
1326 self.handle_open_terminal_in_dock()?;
1327 }
1328 Action::ToggleLineWrap => {
1329 let new_value = !self.config.editor.line_wrap;
1330 self.config_mut().editor.line_wrap = new_value;
1331 self.sync_windows_config();
1339
1340 let leaf_ids: Vec<_> = self
1343 .windows
1344 .get(&self.active_window)
1345 .and_then(|w| w.buffers.splits())
1346 .map(|(_, vs)| vs)
1347 .expect("active window must have a populated split layout")
1348 .keys()
1349 .copied()
1350 .collect();
1351 for leaf_id in leaf_ids {
1352 let buffer_id = self
1353 .split_manager_mut()
1354 .get_buffer_id(leaf_id.into())
1355 .unwrap_or(BufferId(0));
1356 let effective_wrap =
1357 self.active_window().resolve_line_wrap_for_buffer(buffer_id);
1358 let wrap_column = self
1359 .active_window()
1360 .resolve_wrap_column_for_buffer(buffer_id);
1361 if let Some(view_state) = self
1362 .windows
1363 .get_mut(&self.active_window)
1364 .and_then(|w| w.split_view_states_mut())
1365 .expect("active window must have a populated split layout")
1366 .get_mut(&leaf_id)
1367 {
1368 view_state.viewport.line_wrap_enabled = effective_wrap;
1369 view_state.viewport.wrap_indent = self.config.editor.wrap_indent;
1370 view_state.viewport.wrap_column = wrap_column;
1371 }
1372 }
1373
1374 let state = if self.config.editor.line_wrap {
1375 t!("view.state_enabled").to_string()
1376 } else {
1377 t!("view.state_disabled").to_string()
1378 };
1379 self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
1380 }
1381 Action::ToggleCurrentLineHighlight => {
1382 let new_value = !self.config.editor.highlight_current_line;
1383 self.config_mut().editor.highlight_current_line = new_value;
1384
1385 let leaf_ids: Vec<_> = self
1387 .windows
1388 .get(&self.active_window)
1389 .and_then(|w| w.buffers.splits())
1390 .map(|(_, vs)| vs)
1391 .expect("active window must have a populated split layout")
1392 .keys()
1393 .copied()
1394 .collect();
1395 for leaf_id in leaf_ids {
1396 if let Some(view_state) = self
1397 .windows
1398 .get_mut(&self.active_window)
1399 .and_then(|w| w.split_view_states_mut())
1400 .expect("active window must have a populated split layout")
1401 .get_mut(&leaf_id)
1402 {
1403 view_state.highlight_current_line =
1404 self.config.editor.highlight_current_line;
1405 }
1406 }
1407
1408 let state = if self.config.editor.highlight_current_line {
1409 t!("view.state_enabled").to_string()
1410 } else {
1411 t!("view.state_disabled").to_string()
1412 };
1413 self.set_status_message(
1414 t!("view.current_line_highlight_state", state = state).to_string(),
1415 );
1416 }
1417 Action::ToggleReadOnly => {
1418 let buffer_id = self.active_buffer();
1419 let is_now_read_only = self
1420 .active_window()
1421 .buffer_metadata
1422 .get(&buffer_id)
1423 .map(|m| !m.read_only)
1424 .unwrap_or(false);
1425 self.active_window_mut()
1426 .mark_buffer_read_only(buffer_id, is_now_read_only);
1427
1428 let state_str = if is_now_read_only {
1429 t!("view.state_enabled").to_string()
1430 } else {
1431 t!("view.state_disabled").to_string()
1432 };
1433 self.set_status_message(t!("view.read_only_state", state = state_str).to_string());
1434 }
1435 Action::TogglePageView => {
1436 self.active_window_mut().handle_toggle_page_view();
1437 }
1438 Action::SetPageWidth => {
1439 let active_split = self
1440 .windows
1441 .get(&self.active_window)
1442 .and_then(|w| w.buffers.splits())
1443 .map(|(mgr, _)| mgr)
1444 .expect("active window must have a populated split layout")
1445 .active_split();
1446 let current = self
1447 .windows
1448 .get(&self.active_window)
1449 .and_then(|w| w.buffers.splits())
1450 .map(|(_, vs)| vs)
1451 .expect("active window must have a populated split layout")
1452 .get(&active_split)
1453 .and_then(|v| v.compose_width.map(|w| w.to_string()))
1454 .unwrap_or_default();
1455 self.start_prompt_with_initial_text(
1456 "Page width (empty = viewport): ".to_string(),
1457 PromptType::SetPageWidth,
1458 current,
1459 );
1460 }
1461 Action::SetBackground => {
1462 let default_path = self
1463 .ansi_background_path
1464 .as_ref()
1465 .and_then(|p| {
1466 p.strip_prefix(self.working_dir())
1467 .ok()
1468 .map(|rel| rel.to_string_lossy().to_string())
1469 })
1470 .unwrap_or_else(|| DEFAULT_BACKGROUND_FILE.to_string());
1471
1472 self.start_prompt_with_initial_text(
1473 "Background file: ".to_string(),
1474 PromptType::SetBackgroundFile,
1475 default_path,
1476 );
1477 }
1478 Action::SetBackgroundBlend => {
1479 let default_amount = format!("{:.2}", self.background_fade);
1480 self.start_prompt_with_initial_text(
1481 "Background blend (0-1): ".to_string(),
1482 PromptType::SetBackgroundBlend,
1483 default_amount,
1484 );
1485 }
1486 Action::LspCompletion => {
1487 self.request_completion();
1488 }
1489 Action::DabbrevExpand => {
1490 self.dabbrev_expand();
1491 }
1492 Action::LspGotoDefinition => {
1493 self.request_goto_definition()?;
1494 }
1495 Action::LspRename => {
1496 self.start_rename()?;
1497 }
1498 Action::LspHover => {
1499 self.request_hover()?;
1500 }
1501 Action::LspReferences => {
1502 self.request_references()?;
1503 }
1504 Action::LspSignatureHelp => {
1505 self.request_signature_help();
1506 }
1507 Action::LspCodeActions => {
1508 self.request_code_actions()?;
1509 }
1510 Action::LspRestart => {
1511 self.handle_lsp_restart();
1512 }
1513 Action::LspStop => {
1514 self.handle_lsp_stop();
1515 }
1516 Action::LspToggleForBuffer => {
1517 self.handle_lsp_toggle_for_buffer();
1518 }
1519 Action::ToggleInlayHints => {
1520 self.toggle_inlay_hints();
1521 }
1522 Action::DumpConfig => {
1523 self.dump_config();
1524 }
1525 Action::RedrawScreen => {
1526 self.request_full_redraw();
1527 }
1528 Action::SelectTheme => {
1529 self.start_select_theme_prompt();
1530 }
1531 Action::InspectThemeAtCursor => {
1532 self.inspect_theme_at_cursor();
1533 }
1534 Action::SelectKeybindingMap => {
1535 self.start_select_keybinding_map_prompt();
1536 }
1537 Action::SelectCursorStyle => {
1538 self.start_select_cursor_style_prompt();
1539 }
1540 Action::SelectLocale => {
1541 self.start_select_locale_prompt();
1542 }
1543 Action::Search => {
1544 let is_search_prompt = self.active_window().prompt.as_ref().is_some_and(|p| {
1546 matches!(
1547 p.prompt_type,
1548 PromptType::Search
1549 | PromptType::ReplaceSearch
1550 | PromptType::QueryReplaceSearch
1551 )
1552 });
1553
1554 if is_search_prompt {
1555 self.confirm_prompt();
1556 } else {
1557 self.start_search_prompt(
1558 t!("file.search_prompt").to_string(),
1559 PromptType::Search,
1560 false,
1561 );
1562 }
1563 }
1564 Action::Replace => {
1565 self.start_search_prompt(
1567 t!("file.replace_prompt").to_string(),
1568 PromptType::ReplaceSearch,
1569 false,
1570 );
1571 }
1572 Action::QueryReplace => {
1573 self.active_window_mut().search_confirm_each = true;
1575 self.start_search_prompt(
1576 "Query replace: ".to_string(),
1577 PromptType::QueryReplaceSearch,
1578 false,
1579 );
1580 }
1581 Action::FindInSelection => {
1582 self.start_search_prompt(
1583 t!("file.search_prompt").to_string(),
1584 PromptType::Search,
1585 true,
1586 );
1587 }
1588 Action::FindNext => {
1589 self.find_next();
1590 }
1591 Action::FindPrevious => {
1592 self.find_previous();
1593 }
1594 Action::FindSelectionNext => {
1595 self.find_selection_next();
1596 }
1597 Action::FindSelectionPrevious => {
1598 self.find_selection_previous();
1599 }
1600 Action::AddCursorNextMatch => self.add_cursor_at_next_match(),
1601 Action::AddCursorAbove => self.add_cursor_above(),
1602 Action::AddCursorBelow => self.add_cursor_below(),
1603 Action::AddCursorsToLineEnds => self.add_cursors_to_line_ends(),
1604 Action::NextBuffer => self.next_buffer(),
1605 Action::PrevBuffer => self.prev_buffer(),
1606 Action::SwitchToPreviousTab => self.switch_to_previous_tab(),
1607 Action::SwitchToTabByName => self.start_switch_to_tab_prompt(),
1608
1609 Action::ScrollTabsLeft => {
1611 let active_split_id = self
1612 .windows
1613 .get(&self.active_window)
1614 .and_then(|w| w.buffers.splits())
1615 .map(|(mgr, _)| mgr)
1616 .expect("active window must have a populated split layout")
1617 .active_split();
1618 if let Some(view_state) = self
1619 .windows
1620 .get_mut(&self.active_window)
1621 .and_then(|w| w.split_view_states_mut())
1622 .expect("active window must have a populated split layout")
1623 .get_mut(&active_split_id)
1624 {
1625 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_sub(5);
1626 self.set_status_message(t!("status.scrolled_tabs_left").to_string());
1627 }
1628 }
1629 Action::ScrollTabsRight => {
1630 let active_split_id = self
1631 .windows
1632 .get(&self.active_window)
1633 .and_then(|w| w.buffers.splits())
1634 .map(|(mgr, _)| mgr)
1635 .expect("active window must have a populated split layout")
1636 .active_split();
1637 if let Some(view_state) = self
1638 .windows
1639 .get_mut(&self.active_window)
1640 .and_then(|w| w.split_view_states_mut())
1641 .expect("active window must have a populated split layout")
1642 .get_mut(&active_split_id)
1643 {
1644 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_add(5);
1645 self.set_status_message(t!("status.scrolled_tabs_right").to_string());
1646 }
1647 }
1648 Action::NavigateBack => self.navigate_back(),
1649 Action::NavigateForward => self.navigate_forward(),
1650 Action::SplitHorizontal => self.split_pane_horizontal(),
1651 Action::SplitVertical => self.split_pane_vertical(),
1652 Action::CloseSplit => self.close_active_split(),
1653 Action::NextSplit => self.next_split(),
1654 Action::PrevSplit => self.prev_split(),
1655 Action::NextWindow => self.next_window(),
1656 Action::PrevWindow => self.prev_window(),
1657 Action::IncreaseSplitSize => self.adjust_split_size(0.05),
1658 Action::DecreaseSplitSize => self.adjust_split_size(-0.05),
1659 Action::ToggleMaximizeSplit => self.toggle_maximize_split(),
1660 Action::ToggleFileExplorer => self.toggle_file_explorer(),
1661 Action::ToggleFileExplorerSide => self.toggle_file_explorer_side(),
1662 Action::ToggleMenuBar => self.toggle_menu_bar(),
1663 Action::ToggleTabBar => self.active_window_mut().toggle_tab_bar(),
1664 Action::ToggleStatusBar => self.active_window_mut().toggle_status_bar(),
1665 Action::TogglePromptLine => self.active_window_mut().toggle_prompt_line(),
1666 Action::ToggleVerticalScrollbar => self.toggle_vertical_scrollbar(),
1667 Action::ToggleHorizontalScrollbar => self.toggle_horizontal_scrollbar(),
1668 Action::ToggleLineNumbers => self.toggle_line_numbers(),
1669 Action::ToggleScrollSync => self.active_window_mut().toggle_scroll_sync(),
1670 Action::ToggleMouseCapture => self.toggle_mouse_capture(),
1671 Action::ToggleMouseHover => self.toggle_mouse_hover(),
1672 Action::ToggleDebugHighlights => self.active_window_mut().toggle_debug_highlights(),
1673 Action::AddRuler => {
1675 self.start_prompt(t!("rulers.add_prompt").to_string(), PromptType::AddRuler);
1676 }
1677 Action::RemoveRuler => {
1678 self.start_remove_ruler_prompt();
1679 }
1680 Action::SetTabSize => {
1682 let current = self
1683 .buffers()
1684 .get(&self.active_buffer())
1685 .map(|s| s.buffer_settings.tab_size.to_string())
1686 .unwrap_or_else(|| "4".to_string());
1687 self.start_prompt_with_initial_text(
1688 "Tab size: ".to_string(),
1689 PromptType::SetTabSize,
1690 current,
1691 );
1692 }
1693 Action::SetLineEnding => {
1694 self.start_set_line_ending_prompt();
1695 }
1696 Action::SetEncoding => {
1697 self.start_set_encoding_prompt();
1698 }
1699 Action::ReloadWithEncoding => {
1700 self.start_reload_with_encoding_prompt();
1701 }
1702 Action::SetLanguage => {
1703 self.start_set_language_prompt();
1704 }
1705 Action::ToggleIndentationStyle => {
1706 let __buffer_id = self.active_buffer();
1707 if let Some(state) = self
1708 .windows
1709 .get_mut(&self.active_window)
1710 .map(|w| &mut w.buffers)
1711 .expect("active window present")
1712 .get_mut(&__buffer_id)
1713 {
1714 state.buffer_settings.use_tabs = !state.buffer_settings.use_tabs;
1715 let status = if state.buffer_settings.use_tabs {
1716 "Indentation: Tabs"
1717 } else {
1718 "Indentation: Spaces"
1719 };
1720 self.set_status_message(status.to_string());
1721 }
1722 }
1723 Action::ToggleTabIndicators | Action::ToggleWhitespaceIndicators => {
1724 let __buffer_id = self.active_buffer();
1725 if let Some(state) = self
1726 .windows
1727 .get_mut(&self.active_window)
1728 .map(|w| &mut w.buffers)
1729 .expect("active window present")
1730 .get_mut(&__buffer_id)
1731 {
1732 state.buffer_settings.whitespace.toggle_all();
1733 let status = if state.buffer_settings.whitespace.any_visible() {
1734 t!("toggle.whitespace_indicators_shown")
1735 } else {
1736 t!("toggle.whitespace_indicators_hidden")
1737 };
1738 self.set_status_message(status.to_string());
1739 }
1740 }
1741 Action::ResetBufferSettings => self.reset_buffer_settings(),
1742 Action::FocusFileExplorer => self.focus_file_explorer(),
1743 Action::FocusEditor => self.active_window_mut().focus_editor(),
1744 Action::ToggleDockFocus => {
1745 match self.dock.as_ref().map(|d| d.focused) {
1751 Some(true) => self.blur_floating_panel(super::PanelSlot::Dock),
1752 Some(false) => self.refocus_floating_panel(super::PanelSlot::Dock),
1753 None => {
1756 return self.handle_action(Action::PluginAction(
1757 "orchestrator_dock_toggle".to_string(),
1758 ));
1759 }
1760 }
1761 }
1762 Action::FileExplorerUp => self.file_explorer_navigate_up(),
1763 Action::FileExplorerDown => self.file_explorer_navigate_down(),
1764 Action::FileExplorerPageUp => self.file_explorer_page_up(),
1765 Action::FileExplorerPageDown => self.file_explorer_page_down(),
1766 Action::FileExplorerExpand => self.file_explorer_toggle_expand(),
1767 Action::FileExplorerCollapse => self.file_explorer_collapse(),
1768 Action::FileExplorerOpen => self.file_explorer_open_file()?,
1769 Action::FileExplorerRefresh => self.file_explorer_refresh(),
1770 Action::FileExplorerNewFile => self.file_explorer_new_file(),
1771 Action::FileExplorerNewDirectory => self.file_explorer_new_directory(),
1772 Action::FileExplorerDelete => self.file_explorer_delete(),
1773 Action::FileExplorerRename => self.file_explorer_rename(),
1774 Action::FileExplorerToggleHidden => self.file_explorer_toggle_hidden(),
1775 Action::FileExplorerToggleGitignored => self.file_explorer_toggle_gitignored(),
1776 Action::FileExplorerSearchClear => {
1777 self.active_window_mut().file_explorer_search_clear()
1778 }
1779 Action::FileExplorerSearchBackspace => {
1780 self.active_window_mut().file_explorer_search_pop_char()
1781 }
1782 Action::FileExplorerCopy => self.active_window_mut().file_explorer_copy(),
1783 Action::FileExplorerCut => self.active_window_mut().file_explorer_cut(),
1784 Action::FileExplorerPaste => self.file_explorer_paste(),
1785 Action::FileExplorerDuplicate => self.file_explorer_duplicate(),
1786 Action::FileExplorerCopyFullPath => self.file_explorer_copy_path(false),
1787 Action::FileExplorerCopyRelativePath => self.file_explorer_copy_path(true),
1788 Action::FileExplorerExtendSelectionUp => {
1789 self.active_window_mut().file_explorer_extend_selection_up()
1790 }
1791 Action::FileExplorerExtendSelectionDown => self
1792 .active_window_mut()
1793 .file_explorer_extend_selection_down(),
1794 Action::FileExplorerToggleSelect => {
1795 self.active_window_mut().file_explorer_toggle_select()
1796 }
1797 Action::FileExplorerSelectAll => self.active_window_mut().file_explorer_select_all(),
1798 Action::RemoveSecondaryCursors => {
1799 if let Some(events) = self
1801 .active_window_mut()
1802 .action_to_events(Action::RemoveSecondaryCursors)
1803 {
1804 let batch = Event::Batch {
1806 events: events.clone(),
1807 description: "Remove secondary cursors".to_string(),
1808 };
1809 self.active_event_log_mut().append(batch.clone());
1810 self.apply_event_to_active_buffer(&batch);
1811
1812 let active_split = self
1814 .windows
1815 .get(&self.active_window)
1816 .and_then(|w| w.buffers.splits())
1817 .map(|(mgr, _)| mgr)
1818 .expect("active window must have a populated split layout")
1819 .active_split();
1820 let active_buffer = self.active_buffer();
1821 self.active_window_mut()
1822 .ensure_cursor_visible_for_split(active_buffer, active_split);
1823 }
1824 }
1825
1826 Action::MenuActivate => {
1828 self.handle_menu_activate();
1829 }
1830 Action::MenuClose => {
1831 self.handle_menu_close();
1832 }
1833 Action::MenuLeft => {
1834 self.handle_menu_left();
1835 }
1836 Action::MenuRight => {
1837 self.handle_menu_right();
1838 }
1839 Action::MenuUp => {
1840 self.handle_menu_up();
1841 }
1842 Action::MenuDown => {
1843 self.handle_menu_down();
1844 }
1845 Action::MenuExecute => {
1846 if let Some(action) = self.handle_menu_execute() {
1847 return self.handle_action(action);
1848 }
1849 }
1850 Action::MenuOpen(menu_name) => {
1851 if self.config.editor.menu_bar_mnemonics {
1852 self.handle_menu_open(&menu_name);
1853 }
1854 }
1855
1856 Action::SwitchKeybindingMap(map_name) => {
1857 let is_builtin =
1859 matches!(map_name.as_str(), "default" | "emacs" | "vscode" | "macos");
1860 let is_user_defined = self.config.keybinding_maps.contains_key(&map_name);
1861
1862 if is_builtin || is_user_defined {
1863 self.config_mut().active_keybinding_map = map_name.clone().into();
1865
1866 *self.keybindings.write().unwrap() =
1868 crate::input::keybindings::KeybindingResolver::new(&self.config);
1869
1870 self.set_status_message(
1871 t!("view.keybindings_switched", map = map_name).to_string(),
1872 );
1873 } else {
1874 self.set_status_message(
1875 t!("view.keybindings_unknown", map = map_name).to_string(),
1876 );
1877 }
1878 }
1879
1880 Action::SmartHome => {
1881 let buffer_id = self.active_buffer();
1883 if self.active_window().is_composite_buffer(buffer_id) {
1884 if let Some(_handled) =
1885 self.handle_composite_action(buffer_id, &Action::SmartHome)
1886 {
1887 return Ok(());
1888 }
1889 }
1890 self.smart_home();
1891 }
1892 Action::ToggleComment => {
1893 self.toggle_comment();
1894 }
1895 Action::ToggleFold => {
1896 self.active_window_mut().toggle_fold_at_cursor();
1897 }
1898 Action::GoToMatchingBracket => {
1899 self.goto_matching_bracket();
1900 }
1901 Action::JumpToNextError => {
1902 self.jump_to_next_error();
1903 }
1904 Action::JumpToPreviousError => {
1905 self.jump_to_previous_error();
1906 }
1907 Action::SetBookmark(key) => {
1908 self.active_window_mut().set_bookmark(key);
1909 }
1910 Action::JumpToBookmark(key) => {
1911 self.jump_to_bookmark(key);
1912 }
1913 Action::ClearBookmark(key) => {
1914 self.active_window_mut().clear_bookmark(key);
1915 }
1916 Action::ListBookmarks => {
1917 self.active_window_mut().list_bookmarks();
1918 }
1919 Action::ToggleSearchCaseSensitive => {
1920 self.active_window_mut().search_case_sensitive =
1921 !self.active_window().search_case_sensitive;
1922 let state = if self.active_window().search_case_sensitive {
1923 "enabled"
1924 } else {
1925 "disabled"
1926 };
1927 self.set_status_message(
1928 t!("search.case_sensitive_state", state = state).to_string(),
1929 );
1930 self.refresh_active_search();
1931 }
1932 Action::ToggleSearchWholeWord => {
1933 self.active_window_mut().search_whole_word =
1934 !self.active_window().search_whole_word;
1935 let state = if self.active_window().search_whole_word {
1936 "enabled"
1937 } else {
1938 "disabled"
1939 };
1940 self.set_status_message(t!("search.whole_word_state", state = state).to_string());
1941 self.refresh_active_search();
1942 }
1943 Action::ToggleSearchRegex => {
1944 self.active_window_mut().search_use_regex = !self.active_window().search_use_regex;
1945 let state = if self.active_window().search_use_regex {
1946 "enabled"
1947 } else {
1948 "disabled"
1949 };
1950 self.set_status_message(t!("search.regex_state", state = state).to_string());
1951 self.refresh_active_search();
1952 }
1953 Action::ToggleSearchConfirmEach => {
1954 self.active_window_mut().search_confirm_each =
1955 !self.active_window().search_confirm_each;
1956 let state = if self.active_window().search_confirm_each {
1957 "enabled"
1958 } else {
1959 "disabled"
1960 };
1961 self.set_status_message(t!("search.confirm_each_state", state = state).to_string());
1962 }
1963 Action::FileBrowserToggleHidden => {
1964 self.file_open_toggle_hidden();
1966 }
1967 Action::StartMacroRecording => {
1968 self.set_status_message(
1970 "Use Ctrl+Shift+R to start recording (will prompt for register)".to_string(),
1971 );
1972 }
1973 Action::StopMacroRecording => {
1974 self.stop_macro_recording();
1975 }
1976 Action::PlayMacro(key) => {
1977 self.play_macro(key);
1978 }
1979 Action::ToggleMacroRecording(key) => {
1980 self.toggle_macro_recording(key);
1981 }
1982 Action::ShowMacro(key) => {
1983 self.show_macro_in_buffer(key);
1984 }
1985 Action::ListMacros => {
1986 self.list_macros_in_buffer();
1987 }
1988 Action::PromptRecordMacro => {
1989 self.start_prompt("Record macro (0-9): ".to_string(), PromptType::RecordMacro);
1990 }
1991 Action::PromptPlayMacro => {
1992 self.start_prompt("Play macro (0-9): ".to_string(), PromptType::PlayMacro);
1993 }
1994 Action::PlayLastMacro => {
1995 if let Some(key) = self.active_window_mut().macros.last_register() {
1996 self.play_macro(key);
1997 } else {
1998 self.set_status_message(t!("status.no_macro_recorded").to_string());
1999 }
2000 }
2001 Action::PromptSetBookmark => {
2002 self.start_prompt("Set bookmark (0-9): ".to_string(), PromptType::SetBookmark);
2003 }
2004 Action::PromptJumpToBookmark => {
2005 self.start_prompt(
2006 "Jump to bookmark (0-9): ".to_string(),
2007 PromptType::JumpToBookmark,
2008 );
2009 }
2010 Action::CompositeNextHunk => {
2011 let buf = self.active_buffer();
2012 self.active_window_mut().composite_next_hunk_active(buf);
2013 }
2014 Action::CompositePrevHunk => {
2015 let buf = self.active_buffer();
2016 self.active_window_mut().composite_prev_hunk_active(buf);
2017 }
2018 Action::None => {}
2019 Action::DeleteBackward => {
2020 if self.active_window().is_editing_disabled() {
2021 self.set_status_message(t!("buffer.editing_disabled").to_string());
2022 return Ok(());
2023 }
2024 if let Some(events) = self
2026 .active_window_mut()
2027 .action_to_events(Action::DeleteBackward)
2028 {
2029 if events.len() > 1 {
2030 let description = "Delete backward".to_string();
2032 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description)
2033 {
2034 self.active_event_log_mut().append(bulk_edit);
2035 }
2036 } else {
2037 for event in events {
2038 self.active_event_log_mut().append(event.clone());
2039 self.apply_event_to_active_buffer(&event);
2040 }
2041 }
2042 }
2043 }
2044 Action::PluginAction(action_name) => {
2045 tracing::debug!("handle_action: PluginAction('{}')", action_name);
2046 #[cfg(feature = "plugins")]
2049 {
2050 let result = self
2051 .plugin_manager
2052 .read()
2053 .unwrap()
2054 .execute_action_async(&action_name);
2055 if let Some(result) = result {
2056 match result {
2057 Ok(receiver) => {
2058 self.pending_plugin_actions
2060 .push((action_name.clone(), receiver));
2061 }
2062 Err(e) => {
2063 self.set_status_message(
2064 t!("view.plugin_error", error = e.to_string()).to_string(),
2065 );
2066 tracing::error!("Plugin action error: {}", e);
2067 }
2068 }
2069 } else {
2070 self.set_status_message(
2071 t!("status.plugin_manager_unavailable").to_string(),
2072 );
2073 }
2074 }
2075 #[cfg(not(feature = "plugins"))]
2076 {
2077 let _ = action_name;
2078 self.set_status_message(
2079 "Plugins not available (compiled without plugin support)".to_string(),
2080 );
2081 }
2082 }
2083 Action::LoadPluginFromBuffer => {
2084 #[cfg(feature = "plugins")]
2085 {
2086 let buffer_id = self.active_buffer();
2087 let state = self.active_state();
2088 let buffer = &state.buffer;
2089 let total = buffer.total_bytes();
2090 let content =
2091 String::from_utf8_lossy(&buffer.slice_bytes(0..total)).to_string();
2092
2093 let is_ts = buffer
2095 .file_path()
2096 .and_then(|p| p.extension())
2097 .and_then(|e| e.to_str())
2098 .map(|e| e == "ts" || e == "tsx")
2099 .unwrap_or(true);
2100
2101 let name = buffer
2103 .file_path()
2104 .and_then(|p| p.file_name())
2105 .and_then(|s| s.to_str())
2106 .map(|s| s.to_string())
2107 .unwrap_or_else(|| "buffer-plugin".to_string());
2108
2109 let load_result = self
2110 .plugin_manager
2111 .read()
2112 .unwrap()
2113 .load_plugin_from_source(&content, &name, is_ts);
2114 match load_result {
2115 Ok(()) => {
2116 self.set_status_message(format!(
2117 "Plugin '{}' loaded from buffer",
2118 name
2119 ));
2120 }
2121 Err(e) => {
2122 self.set_status_message(format!("Failed to load plugin: {}", e));
2123 tracing::error!("LoadPluginFromBuffer error: {}", e);
2124 }
2125 }
2126
2127 self.setup_plugin_dev_lsp(buffer_id, &content);
2129 }
2130 #[cfg(not(feature = "plugins"))]
2131 {
2132 self.set_status_message(
2133 "Plugins not available (compiled without plugin support)".to_string(),
2134 );
2135 }
2136 }
2137 Action::InitReload => {
2138 self.load_init_script(true);
2143 self.fire_plugins_loaded_hook();
2146 }
2147 Action::InitEdit => {
2148 let config_dir = self.dir_context.config_dir.clone();
2151 match crate::init_script::ensure_starter(&config_dir) {
2152 Ok(path) => {
2153 let declarations =
2163 self.plugin_manager.read().unwrap().plugin_declarations();
2164 crate::init_script::write_plugin_declarations(&config_dir, &declarations);
2165 match self.open_file(&path) {
2166 Ok(_) => {
2167 self.set_status_message(format!("init.ts: {}", path.display()));
2168 }
2169 Err(e) => {
2170 self.set_status_message(format!("init.ts: open failed: {e}"));
2171 }
2172 }
2173 }
2174 Err(e) => {
2175 self.set_status_message(format!("init.ts: create failed: {e}"));
2176 }
2177 }
2178 }
2179 Action::InitCheck => {
2180 let report = crate::init_script::check(&self.dir_context.config_dir);
2183 if report.ok && report.diagnostics.is_empty() {
2184 self.set_status_message("init.ts: ok".into());
2185 } else if !report.ok {
2186 let first = report
2187 .diagnostics
2188 .first()
2189 .map(|d| format!("{}:{}: {}", d.line, d.column, d.message))
2190 .unwrap_or_else(|| "unknown error".into());
2191 self.set_status_message(format!(
2192 "init.ts: {} error(s) — first: {first}",
2193 report.diagnostics.len()
2194 ));
2195 } else {
2196 self.set_status_message(format!(
2197 "init.ts: {} warning(s)",
2198 report.diagnostics.len()
2199 ));
2200 }
2201 }
2202 Action::OpenTerminal => {
2203 self.open_terminal();
2204 }
2205 Action::CloseTerminal => {
2206 self.close_terminal();
2207 }
2208 Action::FocusTerminal => {
2209 if self
2211 .active_window()
2212 .is_terminal_buffer(self.active_buffer())
2213 {
2214 self.active_window_mut().terminal_mode = true;
2215 self.active_window_mut().key_context = KeyContext::Terminal;
2216 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
2217 }
2218 }
2219 Action::TerminalEscape => {
2220 if self.active_window().terminal_mode {
2222 self.active_window_mut().terminal_mode = false;
2223 self.active_window_mut().key_context = KeyContext::Normal;
2224 self.set_status_message(t!("status.terminal_mode_disabled").to_string());
2225 }
2226 }
2227 Action::ToggleKeyboardCapture => {
2228 if self.active_window().terminal_mode {
2230 self.active_window_mut().keyboard_capture =
2231 !self.active_window_mut().keyboard_capture;
2232 if self.active_window_mut().keyboard_capture {
2233 self.set_status_message(
2234 "Keyboard capture ON - all keys go to terminal (F9 to toggle)"
2235 .to_string(),
2236 );
2237 } else {
2238 self.set_status_message(
2239 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
2240 );
2241 }
2242 }
2243 }
2244 Action::TerminalPaste => {
2245 if self.active_window().terminal_mode {
2247 if let Some(text) = self.clipboard.paste() {
2248 self.active_window_mut()
2249 .send_terminal_input(text.as_bytes());
2250 }
2251 }
2252 }
2253 Action::ShellCommand => {
2254 self.start_shell_command_prompt(false);
2256 }
2257 Action::ShellCommandReplace => {
2258 self.start_shell_command_prompt(true);
2260 }
2261 Action::OpenSettings => {
2262 self.open_settings();
2263 }
2264 Action::CloseSettings => {
2265 let has_changes = self
2267 .settings_state
2268 .as_ref()
2269 .is_some_and(|s| s.has_changes());
2270 if has_changes {
2271 if let Some(ref mut state) = self.settings_state {
2273 state.show_confirm_dialog();
2274 }
2275 } else {
2276 self.close_settings(false);
2277 }
2278 }
2279 Action::SettingsSave => {
2280 self.save_settings();
2281 }
2282 Action::SettingsReset => {
2283 if let Some(ref mut state) = self.settings_state {
2284 state.reset_current_to_default();
2285 }
2286 }
2287 Action::SettingsInherit => {
2288 if let Some(ref mut state) = self.settings_state {
2289 state.set_current_to_null();
2290 }
2291 }
2292 Action::SettingsToggleFocus => {
2293 if let Some(ref mut state) = self.settings_state {
2294 state.toggle_focus();
2295 }
2296 }
2297 Action::SettingsActivate => {
2298 self.settings_activate_current();
2299 }
2300 Action::SettingsSearch => {
2301 if let Some(ref mut state) = self.settings_state {
2302 state.start_search();
2303 }
2304 }
2305 Action::SettingsHelp => {
2306 if let Some(ref mut state) = self.settings_state {
2307 state.toggle_help();
2308 }
2309 }
2310 Action::SettingsIncrement => {
2311 self.settings_increment_current();
2312 }
2313 Action::SettingsDecrement => {
2314 self.settings_decrement_current();
2315 }
2316 Action::CalibrateInput => {
2317 self.open_calibration_wizard();
2318 }
2319 Action::EventDebug => {
2320 self.active_window_mut().open_event_debug();
2321 }
2322 Action::SuspendProcess => {
2323 self.request_suspend();
2324 }
2325 Action::OpenKeybindingEditor => {
2326 self.open_keybinding_editor();
2327 }
2328 Action::PromptConfirm => {
2329 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2330 use super::prompt_actions::PromptResult;
2331 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2332 PromptResult::ExecuteAction(action) => {
2333 return self.handle_action(action);
2334 }
2335 PromptResult::EarlyReturn => {
2336 return Ok(());
2337 }
2338 PromptResult::Done => {}
2339 }
2340 }
2341 }
2342 Action::PromptConfirmWithText(ref text) => {
2343 if let Some(ref mut prompt) = self.active_window_mut().prompt {
2345 prompt.set_input(text.clone());
2346 self.update_prompt_suggestions();
2347 }
2348 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2349 use super::prompt_actions::PromptResult;
2350 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2351 PromptResult::ExecuteAction(action) => {
2352 return self.handle_action(action);
2353 }
2354 PromptResult::EarlyReturn => {
2355 return Ok(());
2356 }
2357 PromptResult::Done => {}
2358 }
2359 }
2360 }
2361 Action::PopupConfirm => {
2362 use super::popup_actions::PopupConfirmResult;
2363 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2364 return Ok(());
2365 }
2366 }
2367 Action::PopupCancel => {
2368 self.handle_popup_cancel();
2369 }
2370 Action::PopupFocus => {
2371 self.handle_popup_focus();
2372 }
2373 Action::CompletionAccept => {
2374 use super::popup_actions::PopupConfirmResult;
2375 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2376 return Ok(());
2377 }
2378 }
2379 Action::CompletionDismiss => {
2380 self.handle_popup_cancel();
2381 }
2382 Action::InsertChar(c) => {
2383 if self.is_prompting() {
2384 return self.handle_insert_char_prompt(c);
2385 } else if self.active_window_mut().key_context == KeyContext::FileExplorer {
2386 self.active_window_mut().file_explorer_search_push_char(c);
2387 } else {
2388 self.handle_insert_char_editor(c)?;
2389 }
2390 }
2391 Action::PromptCopy => {
2393 if let Some(prompt) = &self.active_window_mut().prompt {
2394 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2395 if !text.is_empty() {
2396 self.clipboard.copy(text);
2397 self.set_status_message(t!("clipboard.copied").to_string());
2398 }
2399 }
2400 }
2401 Action::PromptCut => {
2402 if let Some(prompt) = &self.active_window_mut().prompt {
2403 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2404 if !text.is_empty() {
2405 self.clipboard.copy(text);
2406 }
2407 }
2408 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2409 if prompt.has_selection() {
2410 prompt.delete_selection();
2411 } else {
2412 prompt.clear();
2413 }
2414 }
2415 self.set_status_message(t!("clipboard.cut").to_string());
2416 self.update_prompt_suggestions();
2417 }
2418 Action::PromptPaste => {
2419 if let Some(text) = self.clipboard.paste() {
2420 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2421 prompt.insert_str(&text);
2422 }
2423 self.update_prompt_suggestions();
2424 }
2425 }
2426 _ => {
2427 self.apply_action_as_events(action)?;
2433 }
2434 }
2435
2436 Ok(())
2437 }
2438
2439 fn fire_dock_widget_event(&self, panel_id: u64, event_type: &str) {
2444 if self
2445 .plugin_manager
2446 .read()
2447 .unwrap()
2448 .has_hook_handlers("widget_event")
2449 {
2450 self.plugin_manager.read().unwrap().run_hook(
2451 "widget_event",
2452 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2453 panel_id,
2454 widget_key: "sessions".to_string(),
2455 event_type: event_type.to_string(),
2456 payload: serde_json::json!({}),
2457 },
2458 );
2459 }
2460 }
2461
2462 fn dispatch_floating_widget_key(
2473 &mut self,
2474 slot: super::PanelSlot,
2475 code: crossterm::event::KeyCode,
2476 modifiers: crossterm::event::KeyModifiers,
2477 ) -> bool {
2478 use crossterm::event::{KeyCode, KeyModifiers};
2479 let panel_id = match self.panel(slot) {
2480 Some(fwp) => fwp.panel_id,
2481 None => {
2482 tracing::debug!(
2483 target: "fresh::dock",
2484 ?slot,
2485 ?code,
2486 "dispatch_floating_widget_key: no panel mounted in slot — returning false"
2487 );
2488 return false;
2489 }
2490 };
2491 tracing::debug!(
2492 target: "fresh::dock",
2493 panel_id,
2494 ?slot,
2495 ?code,
2496 modifiers = ?modifiers,
2497 placement = ?self.panel(slot).map(|f| f.placement),
2498 focused = ?self.panel(slot).map(|f| f.focused),
2499 "dispatch_floating_widget_key: entry"
2500 );
2501 if matches!(
2509 self.panel(slot).map(|f| f.placement),
2510 Some(super::PanelPlacement::LeftDock { .. })
2511 ) {
2512 let on_filter = self
2513 .widget_registry
2514 .focus_key(panel_id)
2515 .map(|k| k == "filter")
2516 .unwrap_or(false);
2517 match code {
2518 KeyCode::Esc => {
2519 if on_filter {
2520 self.set_panel_focus_and_notify(panel_id, "sessions".to_string());
2522 } else {
2523 self.blur_floating_panel(slot);
2525 }
2526 return true;
2527 }
2528 KeyCode::Enter => {
2529 if on_filter {
2530 self.set_panel_focus_and_notify(panel_id, "sessions".to_string());
2532 } else if self
2533 .widget_registry
2534 .focus_key(panel_id)
2535 .map(|k| k == "sessions" || k.is_empty())
2536 .unwrap_or(true)
2537 {
2538 self.fire_dock_widget_event(panel_id, "dock_activate");
2548 } else {
2549 self.handle_widget_command(
2559 panel_id,
2560 fresh_core::api::WidgetAction::Key {
2561 key: "Enter".to_string(),
2562 },
2563 );
2564 }
2565 return true;
2566 }
2567 KeyCode::Char('/') if modifiers.is_empty() => {
2568 self.set_panel_focus_and_notify(panel_id, "filter".to_string());
2569 return true;
2570 }
2571 KeyCode::Char('t' | 'T') if modifiers.contains(KeyModifiers::ALT) => {
2572 self.fire_dock_widget_event(panel_id, "dock_toggle_worktrees");
2579 return true;
2580 }
2581 KeyCode::Char('i' | 'I') if modifiers.contains(KeyModifiers::ALT) => {
2582 self.fire_dock_widget_event(panel_id, "dock_toggle_trivial");
2585 return true;
2586 }
2587 KeyCode::Char('p' | 'P') if modifiers.contains(KeyModifiers::ALT) => {
2588 self.fire_dock_widget_event(panel_id, "dock_toggle_scope");
2591 return true;
2592 }
2593 KeyCode::Char('n' | 'N') if modifiers.contains(KeyModifiers::ALT) => {
2594 if self
2600 .plugin_manager
2601 .read()
2602 .unwrap()
2603 .has_hook_handlers("widget_event")
2604 {
2605 self.plugin_manager.read().unwrap().run_hook(
2606 "widget_event",
2607 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2608 panel_id,
2609 widget_key: "sessions".to_string(),
2610 event_type: "dock_new".to_string(),
2611 payload: serde_json::json!({}),
2612 },
2613 );
2614 }
2615 return true;
2616 }
2617 KeyCode::Char(' ') => {
2618 let has_handler = self
2621 .plugin_manager
2622 .read()
2623 .unwrap()
2624 .has_hook_handlers("widget_event");
2625 tracing::debug!(
2626 target: "fresh::dock",
2627 panel_id,
2628 has_handler,
2629 focus_key = ?self.widget_registry.focus_key(panel_id),
2630 "dispatch_floating_widget_key: Space on LeftDock — firing dock_space widget_event"
2631 );
2632 if has_handler {
2633 self.plugin_manager.read().unwrap().run_hook(
2634 "widget_event",
2635 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2636 panel_id,
2637 widget_key: "sessions".to_string(),
2638 event_type: "dock_space".to_string(),
2639 payload: serde_json::json!({}),
2640 },
2641 );
2642 }
2643 return true;
2644 }
2645 _ => {}
2646 }
2647 }
2648 let key_name: Option<&str> = match code {
2649 KeyCode::Esc => {
2650 let mode_has_binding = self
2659 .active_window()
2660 .editor_mode
2661 .as_ref()
2662 .map(|mode_name| {
2663 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2664 let mode_ctx =
2665 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2666 let keybindings = self.keybindings.read().unwrap();
2667 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2668 })
2669 .unwrap_or(false);
2670 if mode_has_binding {
2671 return false;
2672 }
2673 let widget_key = self
2674 .widget_registry
2675 .get(panel_id)
2676 .map(|p| p.focus_key.clone())
2677 .unwrap_or_default();
2678 if self
2679 .plugin_manager
2680 .read()
2681 .unwrap()
2682 .has_hook_handlers("widget_event")
2683 {
2684 self.plugin_manager.read().unwrap().run_hook(
2685 "widget_event",
2686 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2687 panel_id,
2688 widget_key,
2689 event_type: "cancel".to_string(),
2690 payload: serde_json::json!({}),
2691 },
2692 );
2693 }
2694 *self.panel_opt_mut(slot) = None;
2695 let _ = self.widget_registry.unmount(panel_id);
2696 return true;
2697 }
2698 KeyCode::Tab => Some(if modifiers.contains(KeyModifiers::SHIFT) {
2699 "Shift+Tab"
2700 } else {
2701 "Tab"
2702 }),
2703 KeyCode::BackTab => Some("Shift+Tab"),
2704 KeyCode::Enter => Some("Enter"),
2705 KeyCode::Backspace => Some("Backspace"),
2706 KeyCode::Delete => Some("Delete"),
2707 KeyCode::Home => Some("Home"),
2708 KeyCode::End => Some("End"),
2709 KeyCode::Left => Some("Left"),
2710 KeyCode::Right => Some("Right"),
2711 KeyCode::Up => Some("Up"),
2712 KeyCode::Down => Some("Down"),
2713 KeyCode::PageUp => Some("PageUp"),
2714 KeyCode::PageDown => Some("PageDown"),
2715 _ => None,
2716 };
2717 if let Some(name) = key_name {
2718 let mode_has_binding = self
2737 .active_window()
2738 .editor_mode
2739 .as_ref()
2740 .map(|mode_name| {
2741 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2742 let mode_ctx =
2743 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2744 let keybindings = self.keybindings.read().unwrap();
2745 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2746 })
2747 .unwrap_or(false);
2748 if mode_has_binding {
2749 return false;
2750 }
2751 self.handle_widget_command(
2752 panel_id,
2753 fresh_core::api::WidgetAction::Key {
2754 key: name.to_string(),
2755 },
2756 );
2757 return true;
2758 }
2759 if let KeyCode::Char(c) = code {
2760 {
2771 let mode_has_binding = self
2772 .active_window()
2773 .editor_mode
2774 .as_ref()
2775 .map(|mode_name| {
2776 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2777 let mode_ctx =
2778 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2779 let keybindings = self.keybindings.read().unwrap();
2780 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2781 })
2782 .unwrap_or(false);
2783 if mode_has_binding {
2784 return false;
2785 }
2786 }
2787 if modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
2794 if matches!(
2795 self.panel(slot).map(|f| f.placement),
2796 Some(super::PanelPlacement::LeftDock { .. })
2797 ) {
2798 self.blur_floating_panel(slot);
2799 return false;
2800 }
2801 return true;
2802 }
2803 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
2804 c.to_uppercase().next().unwrap_or(c)
2805 } else {
2806 c
2807 };
2808 if ch == ' ' {
2819 self.handle_widget_command(
2820 panel_id,
2821 fresh_core::api::WidgetAction::Key {
2822 key: "Space".to_string(),
2823 },
2824 );
2825 return true;
2826 }
2827 self.handle_widget_command(
2828 panel_id,
2829 fresh_core::api::WidgetAction::TextInputChar {
2830 text: ch.to_string(),
2831 },
2832 );
2833 return true;
2834 }
2835 true
2840 }
2841
2842 fn close_quick_open_if_open(&mut self) -> bool {
2846 if let Some(prompt) = &self.active_window_mut().prompt {
2847 if prompt.prompt_type == PromptType::QuickOpen {
2848 self.cancel_prompt();
2849 return true;
2850 }
2851 }
2852 false
2853 }
2854
2855 fn refresh_active_search(&mut self) {
2859 if let Some(prompt) = &self.active_window_mut().prompt {
2860 if matches!(
2861 prompt.prompt_type,
2862 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch
2863 ) {
2864 let query = prompt.input.clone();
2865 self.update_search_highlights(&query);
2866 }
2867 } else if let Some(search_state) = &self.active_window().search_state {
2868 let query = search_state.query.clone();
2869 self.perform_search(&query);
2870 }
2871 }
2872
2873 fn handle_open_terminal_in_dock(&mut self) -> AnyhowResult<()> {
2875 use crate::model::event::SplitDirection;
2876 use crate::view::split::SplitRole;
2877
2878 if let Some(dock_leaf) = self
2879 .windows
2880 .get(&self.active_window)
2881 .and_then(|w| w.buffers.splits())
2882 .map(|(mgr, _)| mgr)
2883 .expect("active window must have a populated split layout")
2884 .find_leaf_by_role(SplitRole::UtilityDock)
2885 {
2886 self.windows
2888 .get_mut(&self.active_window)
2889 .and_then(|w| w.split_manager_mut())
2890 .expect("active window must have a populated split layout")
2891 .set_active_split(dock_leaf);
2892 self.open_terminal();
2893 return Ok(());
2894 }
2895
2896 let Some(terminal_id) = self.spawn_terminal_session() else {
2900 return Ok(());
2901 };
2902 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
2903
2904 let new_leaf = self
2906 .windows
2907 .get_mut(&self.active_window)
2908 .and_then(|w| w.split_manager_mut())
2909 .expect("active window must have a populated split layout")
2910 .split_root_positioned(SplitDirection::Horizontal, buffer_id, 0.7, false)
2911 .map_err(|e| {
2912 self.set_status_message(format!("Failed to create dock for terminal: {}", e));
2913 });
2914 let Ok(new_leaf) = new_leaf else {
2915 return Ok(());
2916 };
2917
2918 let mut view_state = crate::view::split::SplitViewState::with_buffer(
2919 self.terminal_width,
2920 self.terminal_height,
2921 buffer_id,
2922 );
2923 view_state.apply_config_defaults(
2926 false,
2927 false,
2928 self.active_window().resolve_line_wrap_for_buffer(buffer_id),
2929 self.config.editor.wrap_indent,
2930 self.active_window()
2931 .resolve_wrap_column_for_buffer(buffer_id),
2932 self.config.editor.rulers.clone(),
2933 0,
2934 );
2935 view_state.viewport.line_wrap_enabled = false;
2937
2938 self.windows
2939 .get_mut(&self.active_window)
2940 .and_then(|w| w.split_view_states_mut())
2941 .expect("active window must have a populated split layout")
2942 .insert(new_leaf, view_state);
2943 self.windows
2944 .get_mut(&self.active_window)
2945 .and_then(|w| w.split_manager_mut())
2946 .expect("active window must have a populated split layout")
2947 .set_leaf_role(new_leaf, Some(SplitRole::UtilityDock));
2948 self.windows
2949 .get_mut(&self.active_window)
2950 .and_then(|w| w.split_manager_mut())
2951 .expect("active window must have a populated split layout")
2952 .set_active_split(new_leaf);
2953
2954 self.active_window_mut().terminal_mode = true;
2956 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Terminal;
2957 self.active_window_mut().resize_visible_terminals();
2958
2959 let exit_key = self
2960 .keybindings
2961 .read()
2962 .unwrap()
2963 .find_keybinding_for_action(
2964 "terminal_escape",
2965 crate::input::keybindings::KeyContext::Terminal,
2966 )
2967 .unwrap_or_else(|| "Ctrl+Space".to_string());
2968 self.set_status_message(
2969 rust_i18n::t!("terminal.opened", id = terminal_id.0, exit_key = exit_key).to_string(),
2970 );
2971 tracing::info!(
2972 "Opened terminal {:?} into new dock leaf {:?} (buffer {:?})",
2973 terminal_id,
2974 new_leaf,
2975 buffer_id
2976 );
2977 Ok(())
2978 }
2979}