aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-08 15:37:01 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-08 15:37:01 +0300
commitbb0dad86f81768c8b7d5894acf7cd53bd05d302b (patch)
treebf132301605fcfc40c992848b85da9d453647aaf /src/app
parentac32c1a19bff6373a3b24618f8e553834e960bdc (diff)
fix: split panes in target tab without stealing focus
fixes #75
Diffstat (limited to 'src/app')
-rw-r--r--src/app/api.rs25
-rw-r--r--src/app/mod.rs121
2 files changed, 140 insertions, 6 deletions
diff --git a/src/app/api.rs b/src/app/api.rs
index e689d89f..ab8058cf 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -819,7 +819,6 @@ impl App {
})
.unwrap();
};
- ws.layout.focus_pane(target_pane_id);
let direction = match params.direction {
crate::api::schema::SplitDirection::Right => {
ratatui::layout::Direction::Horizontal
@@ -828,16 +827,18 @@ impl App {
ratatui::layout::Direction::Vertical
}
};
- let new_pane_id = match ws.split_focused(
+ let (target_tab_idx, new_pane_id) = match ws.split_pane(
+ target_pane_id,
direction,
rows,
cols,
params.cwd.map(std::path::PathBuf::from),
self.state.pane_scrollback_limit_bytes,
self.state.host_terminal_theme,
+ params.focus,
) {
- Ok(new_pane_id) => new_pane_id,
- Err(err) => {
+ Some(Ok(result)) => result,
+ Some(Err(err)) => {
return serde_json::to_string(&ErrorResponse {
id: request.id,
error: ErrorBody {
@@ -847,9 +848,21 @@ impl App {
})
.unwrap();
}
+ None => {
+ return serde_json::to_string(&ErrorResponse {
+ id: request.id,
+ error: ErrorBody {
+ code: "pane_not_found".into(),
+ message: format!("pane {} not found", params.target_pane_id),
+ },
+ })
+ .unwrap();
+ }
};
- if !params.focus {
- ws.layout.focus_pane(target_pane_id);
+ if params.focus {
+ self.state.switch_workspace(ws_idx);
+ self.state.switch_tab(target_tab_idx);
+ self.state.mode = Mode::Terminal;
}
self.schedule_session_save();
let pane = self.pane_info(ws_idx, new_pane_id).unwrap();
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 32e89f0e..1d09230a 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -1436,6 +1436,127 @@ mod tests {
assert!(app.state.should_quit);
}
+ #[tokio::test]
+ async fn pane_split_request_targets_pane_in_background_tab() {
+ let _guard = config_env_lock().lock().unwrap();
+ let original_shell = std::env::var_os("SHELL");
+ std::env::set_var("SHELL", "/usr/bin/true");
+
+ let mut app = test_app();
+ let mut workspace = Workspace::test_new("api-pane-split-background-tab");
+ let active_pane = workspace.tabs[0].root_pane;
+ let background_tab = workspace.test_add_tab(Some("worker"));
+ let target_pane = workspace.tabs[background_tab].root_pane;
+ workspace.switch_tab(background_tab);
+ let background_previous_focus =
+ workspace.test_split(ratatui::layout::Direction::Horizontal);
+ workspace.switch_tab(0);
+ app.state.workspaces = vec![workspace];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+
+ let target_pane_id = app.pane_info(0, target_pane).unwrap().pane_id;
+ let target_tab_id = app.public_tab_id(0, background_tab).unwrap();
+
+ let response = app.handle_api_request(crate::api::schema::Request {
+ id: "req_pane_split_background_tab".into(),
+ method: crate::api::schema::Method::PaneSplit(crate::api::schema::PaneSplitParams {
+ workspace_id: None,
+ target_pane_id,
+ direction: crate::api::schema::SplitDirection::Right,
+ cwd: None,
+ focus: false,
+ }),
+ });
+ let response: serde_json::Value = serde_json::from_str(&response).unwrap();
+
+ assert_eq!(response["result"]["type"], "pane_info");
+ assert_eq!(response["result"]["pane"]["tab_id"], target_tab_id);
+ assert_eq!(response["result"]["pane"]["focused"], false);
+ assert_eq!(app.state.active, Some(0));
+ assert_eq!(app.state.workspaces[0].active_tab, 0);
+ assert_eq!(
+ app.state.workspaces[0].tabs[0].layout.focused(),
+ active_pane
+ );
+ assert_eq!(app.state.workspaces[0].tabs[0].layout.pane_count(), 1);
+ assert_eq!(
+ app.state.workspaces[0].tabs[background_tab]
+ .layout
+ .focused(),
+ background_previous_focus
+ );
+ assert_eq!(
+ app.state.workspaces[0].tabs[background_tab]
+ .layout
+ .pane_count(),
+ 3
+ );
+
+ let runtimes: Vec<_> = app.state.workspaces[0]
+ .tabs
+ .iter_mut()
+ .flat_map(|tab| tab.runtimes.drain())
+ .collect();
+ for (pane_id, runtime) in runtimes {
+ runtime.shutdown(pane_id);
+ }
+ match original_shell {
+ Some(value) => std::env::set_var("SHELL", value),
+ None => std::env::remove_var("SHELL"),
+ }
+ }
+
+ #[tokio::test]
+ async fn pane_split_request_focuses_new_pane_when_requested() {
+ let _guard = config_env_lock().lock().unwrap();
+ let original_shell = std::env::var_os("SHELL");
+ std::env::set_var("SHELL", "/usr/bin/true");
+
+ let mut app = test_app();
+ let mut workspace = Workspace::test_new("api-pane-split-focus-background-tab");
+ let background_tab = workspace.test_add_tab(Some("worker"));
+ workspace.switch_tab(0);
+ app.state.workspaces = vec![workspace];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+
+ let target_pane = app.state.workspaces[0].tabs[background_tab].root_pane;
+ let target_pane_id = app.pane_info(0, target_pane).unwrap().pane_id;
+ let target_tab_id = app.public_tab_id(0, background_tab).unwrap();
+
+ let response = app.handle_api_request(crate::api::schema::Request {
+ id: "req_pane_split_focus_background_tab".into(),
+ method: crate::api::schema::Method::PaneSplit(crate::api::schema::PaneSplitParams {
+ workspace_id: None,
+ target_pane_id,
+ direction: crate::api::schema::SplitDirection::Right,
+ cwd: None,
+ focus: true,
+ }),
+ });
+ let response: serde_json::Value = serde_json::from_str(&response).unwrap();
+
+ assert_eq!(response["result"]["type"], "pane_info");
+ assert_eq!(response["result"]["pane"]["tab_id"], target_tab_id);
+ assert_eq!(response["result"]["pane"]["focused"], true);
+ assert_eq!(app.state.active, Some(0));
+ assert_eq!(app.state.workspaces[0].active_tab, background_tab);
+
+ let runtimes: Vec<_> = app.state.workspaces[0]
+ .tabs
+ .iter_mut()
+ .flat_map(|tab| tab.runtimes.drain())
+ .collect();
+ for (pane_id, runtime) in runtimes {
+ runtime.shutdown(pane_id);
+ }
+ match original_shell {
+ Some(value) => std::env::set_var("SHELL", value),
+ None => std::env::remove_var("SHELL"),
+ }
+ }
+
#[test]
fn pane_close_request_closes_only_the_target_tab_when_other_tabs_exist() {
let mut app = test_app();