diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/api.rs | 25 | ||||
| -rw-r--r-- | src/app/mod.rs | 121 | ||||
| -rw-r--r-- | src/cli.rs | 28 | ||||
| -rw-r--r-- | src/workspace.rs | 36 |
4 files changed, 197 insertions, 13 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(); @@ -475,7 +475,7 @@ fn workspace_list(args: &[String]) -> std::io::Result<i32> { fn workspace_create(args: &[String]) -> std::io::Result<i32> { let mut cwd = None; - let mut focus = true; + let mut focus = false; let mut label = None; let mut index = 0; @@ -497,6 +497,10 @@ fn workspace_create(args: &[String]) -> std::io::Result<i32> { label = Some(value.clone()); index += 2; } + "--focus" => { + focus = true; + index += 1; + } "--no-focus" => { focus = false; index += 1; @@ -613,7 +617,7 @@ fn tab_list(args: &[String]) -> std::io::Result<i32> { fn tab_create(args: &[String]) -> std::io::Result<i32> { let mut workspace_id = None; let mut cwd = None; - let mut focus = true; + let mut focus = false; let mut label = None; let mut index = 0; @@ -643,6 +647,10 @@ fn tab_create(args: &[String]) -> std::io::Result<i32> { label = Some(value.clone()); index += 2; } + "--focus" => { + focus = true; + index += 1; + } "--no-focus" => { focus = false; index += 1; @@ -844,7 +852,7 @@ fn pane_read(args: &[String]) -> std::io::Result<i32> { fn pane_split(args: &[String]) -> std::io::Result<i32> { let Some(raw_pane_id) = args.first() else { eprintln!( - "usage: herdr pane split <pane_id> --direction right|down [--cwd PATH] [--no-focus]" + "usage: herdr pane split <pane_id> --direction right|down [--cwd PATH] [--focus] [--no-focus]" ); return Ok(2); }; @@ -852,7 +860,7 @@ fn pane_split(args: &[String]) -> std::io::Result<i32> { let pane_id = normalize_pane_id(raw_pane_id); let mut direction = None; let mut cwd = None; - let mut focus = true; + let mut focus = false; let mut index = 1; while index < args.len() { @@ -873,6 +881,10 @@ fn pane_split(args: &[String]) -> std::io::Result<i32> { cwd = Some(value.clone()); index += 2; } + "--focus" => { + focus = true; + index += 1; + } "--no-focus" => { focus = false; index += 1; @@ -1403,7 +1415,7 @@ fn print_status_help() { fn print_workspace_help() { eprintln!("herdr workspace commands:"); eprintln!(" herdr workspace list"); - eprintln!(" herdr workspace create [--cwd PATH] [--label TEXT] [--no-focus]"); + eprintln!(" herdr workspace create [--cwd PATH] [--label TEXT] [--focus] [--no-focus]"); eprintln!(" herdr workspace get <workspace_id>"); eprintln!(" herdr workspace focus <workspace_id>"); eprintln!(" herdr workspace rename <workspace_id> <label>"); @@ -1414,7 +1426,7 @@ fn print_tab_help() { eprintln!("herdr tab commands:"); eprintln!(" herdr tab list [--workspace <workspace_id>]"); eprintln!( - " herdr tab create [--workspace <workspace_id>] [--cwd PATH] [--label TEXT] [--no-focus]" + " herdr tab create [--workspace <workspace_id>] [--cwd PATH] [--label TEXT] [--focus] [--no-focus]" ); eprintln!(" herdr tab get <tab_id>"); eprintln!(" herdr tab focus <tab_id>"); @@ -1427,7 +1439,9 @@ fn print_pane_help() { eprintln!(" herdr pane list [--workspace <workspace_id>]"); eprintln!(" herdr pane get <pane_id>"); eprintln!(" herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--raw]"); - eprintln!(" herdr pane split <pane_id> --direction right|down [--cwd PATH] [--no-focus]"); + eprintln!( + " herdr pane split <pane_id> --direction right|down [--cwd PATH] [--focus] [--no-focus]" + ); eprintln!(" herdr pane close <pane_id>"); eprintln!(" herdr pane send-text <pane_id> <text>"); eprintln!(" herdr pane send-keys <pane_id> <key> [key ...]"); diff --git a/src/workspace.rs b/src/workspace.rs index e429ec10..f9fb0493 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -244,6 +244,42 @@ impl Workspace { Ok(new_id) } + pub fn split_pane( + &mut self, + pane_id: PaneId, + direction: Direction, + rows: u16, + cols: u16, + cwd: Option<PathBuf>, + scrollback_limit_bytes: usize, + host_terminal_theme: crate::terminal_theme::TerminalTheme, + focus_new_pane: bool, + ) -> Option<std::io::Result<(usize, PaneId)>> { + let tab_idx = self.find_tab_index_for_pane(pane_id)?; + let tab = &mut self.tabs[tab_idx]; + let previous_focus = tab.layout.focused(); + tab.layout.focus_pane(pane_id); + let new_id = match tab.split_focused( + direction, + rows, + cols, + cwd, + scrollback_limit_bytes, + host_terminal_theme, + ) { + Ok(new_id) => new_id, + Err(err) => { + tab.layout.focus_pane(previous_focus); + return Some(Err(err)); + } + }; + if !focus_new_pane { + tab.layout.focus_pane(previous_focus); + } + self.register_new_pane(new_id); + Some(Ok((tab_idx, new_id))) + } + /// Close the focused pane. Returns true if the workspace should close. pub fn close_focused(&mut self) -> bool { let pane_count = self |
