diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-04 03:14:44 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-04 03:15:11 +0300 |
| commit | edd08e895fc65e07e500a1fe80e88202deae6b8d (patch) | |
| tree | 4d4c9df1e94b60a97c8a6be2ffb4f930896898a8 /src/app/input/navigate.rs | |
| parent | 797981c7e907bc6e3050373ba150fb7a8bc651fb (diff) | |
refactor: route pane layout tui actions through runtime wrappers
Diffstat (limited to 'src/app/input/navigate.rs')
| -rw-r--r-- | src/app/input/navigate.rs | 144 |
1 files changed, 104 insertions, 40 deletions
diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index 186947b6..53aa8a5d 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -6,7 +6,9 @@ use std::{ }; use bytes::Bytes; -use crossterm::event::{KeyCode, KeyEvent}; +use crossterm::event::KeyCode; +#[cfg(test)] +use crossterm::event::KeyEvent; use ratatui::layout::Direction; use crate::{ @@ -253,8 +255,9 @@ impl App { } } NavigateAction::CloseTab => { - self.close_active_tab_via_api(); - leave_navigate_mode(&mut self.state); + if !self.close_active_tab_via_api_requires_confirmation() { + leave_navigate_mode(&mut self.state); + } } NavigateAction::RenamePane => { if let Some(pane_id) = self @@ -297,8 +300,9 @@ impl App { leave_navigate_mode(&mut self.state); } NavigateAction::ClosePane => { - self.close_focused_pane_via_api(); - leave_navigate_mode(&mut self.state); + if !self.close_focused_pane_via_api_requires_confirmation() { + leave_navigate_mode(&mut self.state); + } } NavigateAction::EditScrollback => {} NavigateAction::CopyMode => self.state.enter_copy_mode(&self.terminal_runtimes), @@ -383,15 +387,28 @@ impl App { self.runtime_tab_focus("tui.tab.focus", tab_id); } - pub(crate) fn close_active_tab_via_api(&mut self) { + pub(crate) fn close_active_tab_via_api_requires_confirmation(&mut self) -> bool { let Some(ws_idx) = self.state.active else { - return; + return false; }; + if self + .state + .workspaces + .get(ws_idx) + .is_some_and(|ws| ws.tabs.len() <= 1) + { + if self.state.confirm_implicit_worktree_group_close(ws_idx) { + return true; + } + self.close_workspace_idx_via_api(ws_idx); + return false; + } let tab_idx = self.state.workspaces[ws_idx].active_tab_index(); let Some(tab_id) = self.public_tab_id(ws_idx, tab_idx) else { - return; + return false; }; self.runtime_tab_close("tui.tab.close", tab_id); + false } pub(crate) fn move_tab_via_api( @@ -428,14 +445,12 @@ impl App { self.focus_pane_internal_via_api(ws_idx, target); return; } - self.dispatch_runtime_mutation( + self.runtime_pane_focus_direction( "tui.pane.focus_direction", - crate::api::schema::Method::PaneFocusDirection( - crate::api::schema::PaneFocusDirectionParams { - pane_id: None, - direction: api_pane_direction(direction), - }, - ), + crate::api::schema::PaneFocusDirectionParams { + pane_id: None, + direction: api_pane_direction(direction), + }, ); } @@ -444,26 +459,26 @@ impl App { let source_pane_id = self.public_pane_id(ws_idx, source); let target_pane_id = self.public_pane_id(ws_idx, target); if let (Some(source_pane_id), Some(target_pane_id)) = (source_pane_id, target_pane_id) { - self.dispatch_runtime_mutation( + self.runtime_pane_swap( "tui.pane.swap_exact", - crate::api::schema::Method::PaneSwap(crate::api::schema::PaneSwapParams { + crate::api::schema::PaneSwapParams { pane_id: None, direction: None, source_pane_id: Some(source_pane_id), target_pane_id: Some(target_pane_id), - }), + }, ); return; } } - self.dispatch_runtime_mutation( + self.runtime_pane_swap( "tui.pane.swap", - crate::api::schema::Method::PaneSwap(crate::api::schema::PaneSwapParams { + crate::api::schema::PaneSwapParams { pane_id: None, direction: Some(api_pane_direction(direction)), source_pane_id: None, target_pane_id: None, - }), + }, ); } @@ -471,9 +486,9 @@ impl App { &mut self, direction: crate::api::schema::SplitDirection, ) { - self.dispatch_runtime_mutation( + self.runtime_pane_split( "tui.pane.split", - crate::api::schema::Method::PaneSplit(crate::api::schema::PaneSplitParams { + crate::api::schema::PaneSplitParams { workspace_id: None, target_pane_id: None, direction, @@ -481,41 +496,40 @@ impl App { cwd: None, focus: true, env: Default::default(), - }), + }, ); } - pub(crate) fn close_focused_pane_via_api(&mut self) { + pub(crate) fn close_focused_pane_via_api_requires_confirmation(&mut self) -> bool { let Some((ws_idx, pane_id)) = self.focused_pane_target() else { - return; + return false; }; let Some(pane_id) = self.public_pane_id(ws_idx, pane_id) else { - return; + return false; }; self.runtime_pane_close("tui.pane.close", pane_id); + self.state.mode == Mode::ConfirmClose } pub(crate) fn zoom_focused_pane_via_api(&mut self) { - self.dispatch_runtime_mutation( + self.runtime_pane_zoom( "tui.pane.zoom", - crate::api::schema::Method::PaneZoom(crate::api::schema::PaneZoomParams { + crate::api::schema::PaneZoomParams { pane_id: None, mode: crate::api::schema::PaneZoomMode::Toggle, - }), + }, ); } pub(crate) fn set_split_ratio_via_api(&mut self, path: Vec<bool>, ratio: f32) { - self.dispatch_runtime_mutation( + self.runtime_layout_set_split_ratio( "tui.layout.set_split_ratio", - crate::api::schema::Method::LayoutSetSplitRatio( - crate::api::schema::LayoutSetSplitRatioParams { - tab_id: None, - pane_id: None, - path, - ratio, - }, - ), + crate::api::schema::LayoutSetSplitRatioParams { + tab_id: None, + pane_id: None, + path, + ratio, + }, ); } @@ -1039,6 +1053,7 @@ pub(crate) fn command_for_key( .cloned() } +#[cfg(test)] pub(super) fn handle_navigate_reserved_key(state: &mut AppState, key: TerminalKey) -> bool { let (code, modifiers) = crate::config::normalize_key_combo((key.code, key.modifiers)); if modifiers.is_empty() { @@ -1171,7 +1186,7 @@ pub(super) fn api_pane_direction(direction: NavDirection) -> crate::api::schema: } } -#[allow(dead_code)] // exercised in input unit tests; production uses App::handle_navigate_key +#[cfg(test)] pub(crate) fn handle_navigate_key(state: &mut AppState, key: KeyEvent) { let mut terminal_runtimes = TerminalRuntimeRegistry::new(); state.update_dismissed = true; @@ -1381,6 +1396,7 @@ pub(super) fn execute_navigate_action(state: &mut AppState, action: NavigateActi ); } +#[cfg(test)] pub(super) fn execute_navigate_action_in_context( state: &mut AppState, terminal_runtimes: &mut TerminalRuntimeRegistry, @@ -1716,6 +1732,22 @@ mod tests { }); } + fn app_with_test_workspaces(names: &[&str]) -> App { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + app.state.workspaces = names.iter().map(|name| Workspace::test_new(name)).collect(); + app.state.ensure_test_terminals(); + app.state.active = (!app.state.workspaces.is_empty()).then_some(0); + app.state.selected = 0; + app + } + #[test] fn default_goto_key_opens_navigator() { let mut state = state_with_workspaces(&["test"]); @@ -2604,6 +2636,38 @@ last_pane = "prefix+tab" assert_eq!(state.workspaces.len(), 2); } + #[test] + fn tui_close_tab_last_parent_group_workspace_opens_confirmation_via_api() { + let mut app = app_with_test_workspaces(&["main", "issue"]); + mark_worktree_space_member(&mut app.state, 0, "repo-key"); + mark_worktree_space_member(&mut app.state, 1, "repo-key"); + app.state.active = Some(0); + app.state.selected = 1; + app.state.mode = Mode::Navigate; + + app.execute_tui_navigate_action(NavigateAction::CloseTab, ActionContext::Navigate); + + assert_eq!(app.state.selected, 0); + assert_eq!(app.state.mode, Mode::ConfirmClose); + assert_eq!(app.state.workspaces.len(), 2); + } + + #[test] + fn tui_close_pane_last_parent_group_pane_opens_confirmation_via_api() { + let mut app = app_with_test_workspaces(&["main", "issue"]); + mark_worktree_space_member(&mut app.state, 0, "repo-key"); + mark_worktree_space_member(&mut app.state, 1, "repo-key"); + app.state.active = Some(0); + app.state.selected = 1; + app.state.mode = Mode::Navigate; + + app.execute_tui_navigate_action(NavigateAction::ClosePane, ActionContext::Navigate); + + assert_eq!(app.state.selected, 0); + assert_eq!(app.state.mode, Mode::ConfirmClose); + assert_eq!(app.state.workspaces.len(), 2); + } + #[cfg(unix)] #[tokio::test] async fn custom_command_runs_from_prefix_key_in_navigate_mode() { |
