diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-17 01:41:16 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-17 01:41:16 +0300 |
| commit | e4de58b37f58d59d146158239ed218a8accd5c3c (patch) | |
| tree | 6bb35b8c9aaec969110ff51a58814dd8099617c0 /src | |
| parent | 86adf88351e17c6a11cac253714b169eac0db233 (diff) | |
feat: add agent start for server-owned terminals
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/mod.rs | 2 | ||||
| -rw-r--r-- | src/api/schema.rs | 22 | ||||
| -rw-r--r-- | src/app/agents.rs | 240 | ||||
| -rw-r--r-- | src/app/api.rs | 16 | ||||
| -rw-r--r-- | src/app/creation.rs | 5 | ||||
| -rw-r--r-- | src/cli.rs | 100 | ||||
| -rw-r--r-- | src/pane.rs | 40 | ||||
| -rw-r--r-- | src/workspace.rs | 204 | ||||
| -rw-r--r-- | src/workspace/tab.rs | 142 |
9 files changed, 725 insertions, 46 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs index 9a4134ab..4893c295 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -41,6 +41,7 @@ pub(crate) fn request_changes_ui(request: &Request) -> bool { | Method::TabClose(_) | Method::AgentRename(_) | Method::AgentFocus(_) + | Method::AgentStart(_) | Method::PaneSplit(_) | Method::PaneRename(_) | Method::PaneReportAgent(_) @@ -407,6 +408,7 @@ fn api_method_name(method: &Method) -> &'static str { Method::AgentSend(_) => "agent.send", Method::AgentRename(_) => "agent.rename", Method::AgentFocus(_) => "agent.focus", + Method::AgentStart(_) => "agent.start", Method::PaneSplit(_) => "pane.split", Method::PaneList(_) => "pane.list", Method::PaneGet(_) => "pane.get", diff --git a/src/api/schema.rs b/src/api/schema.rs index e6d24aaf..a82070e0 100644 --- a/src/api/schema.rs +++ b/src/api/schema.rs @@ -52,6 +52,8 @@ pub enum Method { AgentRename(AgentRenameParams), #[serde(rename = "agent.focus")] AgentFocus(AgentTarget), + #[serde(rename = "agent.start")] + AgentStart(AgentStartParams), #[serde(rename = "pane.split")] PaneSplit(PaneSplitParams), #[serde(rename = "pane.list")] @@ -180,6 +182,22 @@ pub struct AgentRenameParams { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AgentStartParams { + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cwd: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_id: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tab_id: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub split: Option<SplitDirection>, + #[serde(default)] + pub focus: bool, + pub argv: Vec<String>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PaneSplitParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub workspace_id: Option<String>, @@ -523,6 +541,10 @@ pub enum ResponseResult { AgentInfo { agent: AgentInfo, }, + AgentStarted { + agent: AgentInfo, + argv: Vec<String>, + }, AgentList { agents: Vec<AgentInfo>, }, diff --git a/src/app/agents.rs b/src/app/agents.rs index 88f7ec97..35e26c42 100644 --- a/src/app/agents.rs +++ b/src/app/agents.rs @@ -1,4 +1,7 @@ +use std::path::PathBuf; + use super::{terminal_targets::TerminalTargetError, App, Mode}; +use crate::api::schema::{AgentStartParams, SplitDirection}; impl App { pub(super) fn collect_agent_infos(&self) -> Vec<crate::api::schema::AgentInfo> { @@ -97,6 +100,154 @@ impl App { }) } + pub(super) fn start_agent( + &mut self, + params: AgentStartParams, + ) -> Result<(crate::api::schema::AgentInfo, Vec<String>), AgentStartError> { + let name = params.name.trim().to_string(); + if name.is_empty() { + return Err(AgentStartError::InvalidName); + } + if params.argv.is_empty() { + return Err(AgentStartError::EmptyArgv); + } + let conflicts = self.agent_name_conflicts(&name, ""); + if !conflicts.is_empty() { + return Err(AgentStartError::DuplicateName { + name, + candidates: conflicts, + }); + } + + let cwd = params + .cwd + .map(PathBuf::from) + .or_else(|| std::env::current_dir().ok()) + .unwrap_or_else(|| PathBuf::from("/")); + let argv = params.argv; + let focus = params.focus; + let (rows, cols) = self.state.estimate_pane_size(); + + let (ws_idx, tab_idx, pane_id) = if let Some(tab_id) = params.tab_id { + let (ws_idx, tab_idx) = + self.parse_tab_id(&tab_id) + .ok_or_else(|| AgentStartError::TargetNotFound { + target: tab_id.clone(), + })?; + if let Some(workspace_id) = params.workspace_id.as_deref() { + let requested_ws_idx = self.parse_workspace_id(workspace_id).ok_or_else(|| { + AgentStartError::TargetNotFound { + target: workspace_id.to_string(), + } + })?; + if requested_ws_idx != ws_idx { + return Err(AgentStartError::PlacementConflict); + } + } + let target_pane = self.state.workspaces[ws_idx].tabs[tab_idx].layout.focused(); + self.spawn_agent_split( + ws_idx, + target_pane, + params.split.unwrap_or(SplitDirection::Right), + cwd, + &argv, + focus, + )? + } else if let Some(workspace_id) = params.workspace_id { + let ws_idx = self.parse_workspace_id(&workspace_id).ok_or_else(|| { + AgentStartError::TargetNotFound { + target: workspace_id.clone(), + } + })?; + let tab_idx = self.state.workspaces[ws_idx].active_tab; + let target_pane = self.state.workspaces[ws_idx].tabs[tab_idx].layout.focused(); + self.spawn_agent_split( + ws_idx, + target_pane, + params.split.unwrap_or(SplitDirection::Right), + cwd, + &argv, + focus, + )? + } else if self.state.workspaces.is_empty() { + self.spawn_agent_workspace(cwd, rows, cols, &argv, focus)? + } else { + let ws_idx = self.state.active.unwrap_or(0); + let tab_idx = self.state.workspaces[ws_idx].active_tab; + let target_pane = self.state.workspaces[ws_idx].tabs[tab_idx].layout.focused(); + self.spawn_agent_split( + ws_idx, + target_pane, + params.split.unwrap_or(SplitDirection::Right), + cwd, + &argv, + focus, + )? + }; + + let Some(ws) = self.state.workspaces.get_mut(ws_idx) else { + return Err(AgentStartError::SpawnFailed("workspace disappeared".into())); + }; + let Some(pane_state) = ws.pane_state_mut(pane_id) else { + return Err(AgentStartError::SpawnFailed("pane disappeared".into())); + }; + pane_state.set_manual_label(name); + self.state.mark_session_dirty(); + + let agent = self + .agent_info(ws_idx, pane_id) + .ok_or_else(|| AgentStartError::SpawnFailed("agent disappeared".into()))?; + debug_assert_eq!(agent.tab_id, self.public_tab_id(ws_idx, tab_idx).unwrap()); + Ok((agent, argv)) + } + + pub(super) fn agent_start_error_body( + &self, + err: AgentStartError, + ) -> crate::api::schema::ErrorBody { + match err { + AgentStartError::InvalidName => crate::api::schema::ErrorBody { + code: "invalid_agent_name".into(), + message: "agent name must not be empty".into(), + }, + AgentStartError::EmptyArgv => crate::api::schema::ErrorBody { + code: "invalid_agent_argv".into(), + message: "agent start argv must not be empty".into(), + }, + AgentStartError::TargetNotFound { target } => crate::api::schema::ErrorBody { + code: "agent_placement_not_found".into(), + message: format!("agent placement target {target} not found"), + }, + AgentStartError::PlacementConflict => crate::api::schema::ErrorBody { + code: "agent_placement_conflict".into(), + message: "--tab must belong to --workspace".into(), + }, + AgentStartError::SpawnFailed(message) => crate::api::schema::ErrorBody { + code: "agent_start_failed".into(), + message, + }, + AgentStartError::DuplicateName { name, candidates } => crate::api::schema::ErrorBody { + code: "agent_name_taken".into(), + message: format!( + "agent name {name} is already used; candidates: {}", + candidates + .into_iter() + .map(|candidate| format!( + "terminal_id={} pane_id={} workspace_id={} tab_id={} cwd={} status={:?}", + candidate.terminal_id, + candidate.pane_id, + candidate.workspace_id, + candidate.tab_id, + candidate.cwd.unwrap_or_else(|| "unknown".into()), + candidate.agent_status, + )) + .collect::<Vec<_>>() + .join("; ") + ), + }, + } + } + pub(super) fn agent_target_error_body( &self, err: TerminalTargetError, @@ -158,6 +309,81 @@ impl App { } } + fn spawn_agent_workspace( + &mut self, + cwd: PathBuf, + rows: u16, + cols: u16, + argv: &[String], + focus: bool, + ) -> Result<(usize, usize, crate::layout::PaneId), AgentStartError> { + let ws = crate::workspace::Workspace::new_argv_command( + cwd, + rows, + cols, + argv, + self.state.pane_scrollback_limit_bytes, + self.state.host_terminal_theme, + self.event_tx.clone(), + self.render_notify.clone(), + self.render_dirty.clone(), + ) + .map_err(|err| AgentStartError::SpawnFailed(err.to_string()))?; + self.state.workspaces.push(ws); + let ws_idx = self.state.workspaces.len() - 1; + if focus || self.state.active.is_none() { + self.state.switch_workspace(ws_idx); + self.state.mode = Mode::Terminal; + } + self.schedule_session_save(); + let pane_id = self.state.workspaces[ws_idx].tabs[0].root_pane; + Ok((ws_idx, 0, pane_id)) + } + + fn spawn_agent_split( + &mut self, + ws_idx: usize, + target_pane: crate::layout::PaneId, + split: SplitDirection, + cwd: PathBuf, + argv: &[String], + focus: bool, + ) -> Result<(usize, usize, crate::layout::PaneId), AgentStartError> { + let (rows, cols) = self.state.estimate_pane_size(); + let direction = match split { + SplitDirection::Right => ratatui::layout::Direction::Horizontal, + SplitDirection::Down => ratatui::layout::Direction::Vertical, + }; + let result = self + .state + .workspaces + .get_mut(ws_idx) + .and_then(|ws| { + ws.split_pane_argv_command( + target_pane, + direction, + rows, + cols, + Some(cwd), + argv, + self.state.pane_scrollback_limit_bytes, + self.state.host_terminal_theme, + focus, + ) + }) + .ok_or_else(|| AgentStartError::TargetNotFound { + target: target_pane.raw().to_string(), + })? + .map_err(|err| AgentStartError::SpawnFailed(err.to_string()))?; + if focus { + self.state.switch_workspace(ws_idx); + self.state.switch_tab(result.0); + self.state.mode = Mode::Terminal; + } + self.schedule_session_save(); + Ok((ws_idx, result.0, result.1)) + } + fn agent_info( &self, ws_idx: usize, @@ -204,6 +430,20 @@ impl App { } } +pub(super) enum AgentStartError { + InvalidName, + EmptyArgv, + TargetNotFound { + target: String, + }, + PlacementConflict, + SpawnFailed(String), + DuplicateName { + name: String, + candidates: Vec<crate::api::schema::AgentInfo>, + }, +} + pub(super) enum AgentRenameError { Target(TerminalTargetError), DuplicateName { diff --git a/src/app/api.rs b/src/app/api.rs index 666e8a20..c6fa88fa 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -882,6 +882,22 @@ impl App { result: ResponseResult::AgentInfo { agent }, } } + Method::AgentStart(params) => { + let (agent, argv) = match self.start_agent(params) { + Ok(started) => started, + Err(err) => { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: self.agent_start_error_body(err), + }) + .unwrap(); + } + }; + SuccessResponse { + id: request.id, + result: ResponseResult::AgentStarted { agent, argv }, + } + } Method::AgentRead(params) => { let resolved = match self.resolve_terminal_target(¶ms.target) { Ok(resolved) => resolved, diff --git a/src/app/creation.rs b/src/app/creation.rs index f1ee41d8..6e557066 100644 --- a/src/app/creation.rs +++ b/src/app/creation.rs @@ -230,7 +230,6 @@ impl App { ) -> Option<crate::api::schema::PaneInfo> { let ws = self.state.workspaces.get(ws_idx)?; let pane = ws.pane_state(pane_id)?; - let runtime = ws.runtime(pane_id); let tab_idx = ws.find_tab_index_for_pane(pane_id)?; let focused = self.state.active == Some(ws_idx) && ws.active_tab == tab_idx @@ -243,8 +242,8 @@ impl App { workspace_id: self.public_workspace_id(ws_idx), tab_id: self.public_tab_id(ws_idx, tab_idx)?, focused, - cwd: runtime - .and_then(|rt| rt.cwd()) + cwd: ws.tabs[tab_idx] + .cwd_for_pane(pane_id) .map(|cwd| cwd.display().to_string()), label: pane.manual_label.clone(), agent: pane.effective_agent_label().map(str::to_string), @@ -6,12 +6,13 @@ use serde::Serialize; use crate::api; use crate::api::schema::{ - AgentReadParams, AgentRenameParams, AgentSendParams, AgentStatus, AgentTarget, EmptyParams, - IntegrationTarget, Method, OutputMatch, PaneAgentState, PaneListParams, PaneReadParams, - PaneRenameParams, PaneReportAgentParams, PaneSendInputParams, PaneSendKeysParams, - PaneSendTextParams, PaneSplitParams, PaneTarget, PaneWaitForOutputParams, PingParams, - ReadFormat, ReadSource, Request, SplitDirection, Subscription, TabCreateParams, TabListParams, - TabRenameParams, TabTarget, WorkspaceCreateParams, WorkspaceRenameParams, WorkspaceTarget, + AgentReadParams, AgentRenameParams, AgentSendParams, AgentStartParams, AgentStatus, + AgentTarget, EmptyParams, IntegrationTarget, Method, OutputMatch, PaneAgentState, + PaneListParams, PaneReadParams, PaneRenameParams, PaneReportAgentParams, PaneSendInputParams, + PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneTarget, PaneWaitForOutputParams, + PingParams, ReadFormat, ReadSource, Request, SplitDirection, Subscription, TabCreateParams, + TabListParams, TabRenameParams, TabTarget, WorkspaceCreateParams, WorkspaceRenameParams, + WorkspaceTarget, }; pub enum CommandOutcome { @@ -279,6 +280,7 @@ fn run_agent_command(args: &[String]) -> std::io::Result<i32> { "send" => agent_send(&args[1..]), "rename" => agent_rename(&args[1..]), "focus" => agent_focus(&args[1..]), + "start" => agent_start(&args[1..]), "help" | "--help" | "-h" => { print_agent_help(); Ok(0) @@ -771,6 +773,91 @@ fn tab_close(args: &[String]) -> std::io::Result<i32> { })?) } +fn agent_start(args: &[String]) -> std::io::Result<i32> { + let Some(name) = args.first() else { + eprintln!("usage: herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>"); + return Ok(2); + }; + + let Some(separator) = args.iter().position(|arg| arg == "--") else { + eprintln!("usage: herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>"); + return Ok(2); + }; + if separator == args.len() - 1 { + eprintln!("agent start requires argv after --"); + return Ok(2); + } + + let mut cwd = None; + let mut workspace_id = None; + let mut tab_id = None; + let mut split = None; + let mut focus = false; + + let mut index = 1; + while index < separator { + match args[index].as_str() { + "--cwd" => { + let Some(value) = args.get(index + 1).filter(|_| index + 1 < separator) else { + eprintln!("missing value for --cwd"); + return Ok(2); + }; + cwd = Some(value.clone()); + index += 2; + } + "--workspace" => { + let Some(value) = args.get(index + 1).filter(|_| index + 1 < separator) else { + eprintln!("missing value for --workspace"); + return Ok(2); + }; + workspace_id = Some(normalize_workspace_id(value)); + index += 2; + } + "--tab" => { + let Some(value) = args.get(index + 1).filter(|_| index + 1 < separator) else { + eprintln!("missing value for --tab"); + return Ok(2); + }; + tab_id = Some(normalize_tab_id(value)); + index += 2; + } + "--split" => { + let Some(value) = args.get(index + 1).filter(|_| index + 1 < separator) else { + eprintln!("missing value for --split"); + return Ok(2); + }; + split = Some(parse_split_direction(value)?); + index += 2; + } + "--focus" => { + focus = true; + index += 1; + } + "--no-focus" => { + focus = false; + index += 1; + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + print_response(&send_request(&Request { + id: "cli:agent:start".into(), + method: Method::AgentStart(AgentStartParams { + name: name.clone(), + cwd, + workspace_id, + tab_id, + split, + focus, + argv: args[separator + 1..].to_vec(), + }), + })?) +} + fn agent_list(args: &[String]) -> std::io::Result<i32> { if !args.is_empty() { eprintln!("usage: herdr agent list"); @@ -1811,6 +1898,7 @@ fn print_agent_help() { eprintln!(" herdr agent send <target> <text>"); eprintln!(" herdr agent rename <target> <name>|--clear"); eprintln!(" herdr agent focus <target>"); + eprintln!(" herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>"); eprintln!(" targets accept terminal ids, unique agent names, and legacy pane ids"); } diff --git a/src/pane.rs b/src/pane.rs index 272229fa..b5886933 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -328,6 +328,46 @@ impl PaneRuntime { ) } + pub fn spawn_argv_command( + pane_id: PaneId, + rows: u16, + cols: u16, + cwd: std::path::PathBuf, + argv: &[String], + scrollback_limit_bytes: usize, + host_terminal_theme: crate::terminal_theme::TerminalTheme, + events: mpsc::Sender<AppEvent>, + render_notify: Arc<Notify>, + render_dirty: Arc<AtomicBool>, + ) -> std::io::Result<Self> { + let Some((program, args)) = argv.split_first() else { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "argv must not be empty", + )); + }; + let mut cmd = CommandBuilder::new(program); + for arg in args { + cmd.arg(arg); + } + cmd.cwd(cwd); + cmd.env(crate::HERDR_ENV_VAR, crate::HERDR_ENV_VALUE); + apply_pane_terminal_env(&mut cmd); + crate::integration::apply_pane_env(&mut cmd, pane_id); + Self::spawn_command_builder( + pane_id, + rows, + cols, + scrollback_limit_bytes, + host_terminal_theme, + events, + render_notify, + render_dirty, + cmd, + "failed to spawn argv command pane", + ) + } + fn spawn_command_builder( pane_id: PaneId, rows: u16, diff --git a/src/workspace.rs b/src/workspace.rs index 46ad14fa..59b3e35d 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -91,9 +91,8 @@ impl Workspace { render_notify: Arc<Notify>, render_dirty: Arc<AtomicBool>, ) -> std::io::Result<Self> { - let tab = Tab::new( - 1, - initial_cwd.clone(), + Self::new_with_tab( + initial_cwd, rows, cols, scrollback_limit_bytes, @@ -101,7 +100,72 @@ impl Workspace { events, render_notify, render_dirty, - )?; + None, + ) + } + + pub fn new_argv_command( + initial_cwd: PathBuf, + rows: u16, + cols: u16, + argv: &[String], + scrollback_limit_bytes: usize, + host_terminal_theme: crate::terminal_theme::TerminalTheme, + events: mpsc::Sender<AppEvent>, + render_notify: Arc<Notify>, + render_dirty: Arc<AtomicBool>, + ) -> std::io::Result<Self> { + Self::new_with_tab( + initial_cwd, + rows, + cols, + scrollback_limit_bytes, + host_terminal_theme, + events, + render_notify, + render_dirty, + Some(argv), + ) + } + + #[allow(clippy::too_many_arguments)] + fn new_with_tab( + initial_cwd: PathBuf, + rows: u16, + cols: u16, + scrollback_limit_bytes: usize, + host_terminal_theme: crate::terminal_theme::TerminalTheme, + events: mpsc::Sender<AppEvent>, + render_notify: Arc<Notify>, + render_dirty: Arc<AtomicBool>, + argv: Option<&[String]>, + ) -> std::io::Result<Self> { + let tab = if let Some(argv) = argv { + Tab::new_argv_command( + 1, + initial_cwd.clone(), + rows, + cols, + argv, + scrollback_limit_bytes, + host_terminal_theme, + events, + render_notify, + render_dirty, + )? + } else { + Tab::new( + 1, + initial_cwd.clone(), + rows, + cols, + scrollback_limit_bytes, + host_terminal_theme, + events, + render_notify, + render_dirty, + )? + }; let mut public_pane_numbers = HashMap::new(); public_pane_numbers.insert(tab.root_pane, 1); Ok(Self { @@ -152,6 +216,25 @@ impl Workspace { scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, ) -> std::io::Result<usize> { + self.create_tab_with_runtime( + rows, + cols, + cwd, + scrollback_limit_bytes, + host_terminal_theme, + None, + ) + } + + fn create_tab_with_runtime( + &mut self, + rows: u16, + cols: u16, + cwd: PathBuf, + scrollback_limit_bytes: usize, + host_terminal_theme: crate::terminal_theme::TerminalTheme, + argv: Option<&[String]>, + ) -> std::io::Result<usize> { let number = self.tabs.len() + 1; let events = self .active_tab() @@ -166,17 +249,32 @@ impl Workspace { .map(|tab| tab.render_dirty.clone()) .expect("workspace must always have at least one tab"); - let tab = Tab::new( - number, - cwd, - rows, - cols, - scrollback_limit_bytes, - host_terminal_theme, - events, - render_notify, - render_dirty, - )?; + let tab = if let Some(argv) = argv { + Tab::new_argv_command( + number, + cwd, + rows, + cols, + argv, + scrollback_limit_bytes, + host_terminal_theme, + events, + render_notify, + render_dirty, + )? + } else { + Tab::new( + number, + cwd, + rows, + cols, + scrollback_limit_bytes, + host_terminal_theme, + events, + render_notify, + render_dirty, + )? + }; self.register_new_pane(tab.root_pane); self.tabs.push(tab); Ok(self.tabs.len() - 1) @@ -264,18 +362,82 @@ impl Workspace { 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( + self.split_pane_with_runtime( + pane_id, + direction, + rows, + cols, + cwd, + scrollback_limit_bytes, + host_terminal_theme, + focus_new_pane, + None, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn split_pane_argv_command( + &mut self, + pane_id: PaneId, + direction: Direction, + rows: u16, + cols: u16, + cwd: Option<PathBuf>, + argv: &[String], + scrollback_limit_bytes: usize, + host_terminal_theme: crate::terminal_theme::TerminalTheme, + focus_new_pane: bool, + ) -> Option<std::io::Result<(usize, PaneId)>> { + self.split_pane_with_runtime( + pane_id, direction, rows, cols, cwd, scrollback_limit_bytes, host_terminal_theme, - ) { + focus_new_pane, + Some(argv), + ) + } + + #[allow(clippy::too_many_arguments)] + fn split_pane_with_runtime( + &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, + argv: Option<&[String]>, + ) -> 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 if let Some(argv) = argv { + tab.split_focused_argv_command( + direction, + rows, + cols, + cwd, + argv, + scrollback_limit_bytes, + host_terminal_theme, + ) + } else { + 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); diff --git a/src/workspace/tab.rs b/src/workspace/tab.rs index 6f3177be..d809f944 100644 --- a/src/workspace/tab.rs +++ b/src/workspace/tab.rs @@ -11,6 +11,16 @@ use crate::layout::{PaneId, TileLayout}; use crate::pane::{PaneRuntime, PaneState}; use crate::terminal::TerminalId; +enum SplitCommand<'a> { + Shell { + command: &'a str, + extra_env: &'a [(String, String)], + }, + Argv { + argv: &'a [String], + }, +} + pub struct Tab { pub custom_name: Option<String>, pub number: usize, @@ -52,18 +62,86 @@ impl Tab { render_notify: Arc<Notify>, render_dirty: Arc<AtomicBool>, ) -> std::io::Result<Self> { - let (layout, root_id) = TileLayout::new(); - let runtime = PaneRuntime::spawn( - root_id, + Self::new_with_runtime( + number, + initial_cwd, + rows, + cols, + scrollback_limit_bytes, + host_terminal_theme, + events, + render_notify, + render_dirty, + None, + ) + } + + pub fn new_argv_command( + number: usize, + initial_cwd: PathBuf, + rows: u16, + cols: u16, + argv: &[String], + scrollback_limit_bytes: usize, + host_terminal_theme: crate::terminal_theme::TerminalTheme, + events: mpsc::Sender<AppEvent>, + render_notify: Arc<Notify>, + render_dirty: Arc<AtomicBool>, + ) -> std::io::Result<Self> { + Self::new_with_runtime( + number, + initial_cwd, rows, cols, - initial_cwd.clone(), scrollback_limit_bytes, host_terminal_theme, - events.clone(), - render_notify.clone(), - render_dirty.clone(), - )?; + events, + render_notify, + render_dirty, + Some(argv), + ) + } + + #[allow(clippy::too_many_arguments)] + fn new_with_runtime( + number: usize, + initial_cwd: PathBuf, + rows: u16, + cols: u16, + scrollback_limit_bytes: usize, + host_terminal_theme: crate::terminal_theme::TerminalTheme, + events: mpsc::Sender<AppEvent>, + render_notify: Arc<Notify>, + render_dirty: Arc<AtomicBool>, + argv: Option<&[String]>, + ) -> std::io::Result<Self> { + let (layout, root_id) = TileLayout::new(); + let runtime = if let Some(argv) = argv { + PaneRuntime::spawn_argv_command( + root_id, + rows, + cols, + initial_cwd.clone(), + argv, + scrollback_limit_bytes, + host_terminal_theme, + events.clone(), + render_notify.clone(), + render_dirty.clone(), + )? + } else { + PaneRuntime::spawn( + root_id, + rows, + cols, + initial_cwd.clone(), + scrollback_limit_bytes, + host_terminal_theme, + events.clone(), + render_notify.clone(), + render_dirty.clone(), + )? + }; let mut panes = HashMap::new(); panes.insert(root_id, PaneState::new()); @@ -142,7 +220,28 @@ impl Tab { cwd, scrollback_limit_bytes, host_terminal_theme, - Some((command, extra_env)), + Some(SplitCommand::Shell { command, extra_env }), + ) + } + + pub fn split_focused_argv_command( + &mut self, + direction: Direction, + rows: u16, + cols: u16, + cwd: Option<PathBuf>, + argv: &[String], + scrollback_limit_bytes: usize, + host_terminal_theme: crate::terminal_theme::TerminalTheme, + ) -> std::io::Result<PaneId> { + self.split_focused_with_runtime( + direction, + rows, + cols, + cwd, + scrollback_limit_bytes, + host_terminal_theme, + Some(SplitCommand::Argv { argv }), ) } @@ -154,13 +253,13 @@ impl Tab { cwd: Option<PathBuf>, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, - command: Option<(&str, &[(String, String)])>, + command: Option<SplitCommand<'_>>, ) -> std::io::Result<PaneId> { let new_id = self.layout.split_focused(direction); let actual_cwd = cwd.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| "/".into())); - let runtime = if let Some((command, extra_env)) = command { - PaneRuntime::spawn_shell_command( + let runtime = match command { + Some(SplitCommand::Shell { command, extra_env }) => PaneRuntime::spawn_shell_command( new_id, rows, cols, @@ -172,19 +271,30 @@ impl Tab { self.events.clone(), self.render_notify.clone(), self.render_dirty.clone(), - )? - } else { - PaneRuntime::spawn( + )?, + Some(SplitCommand::Argv { argv }) => PaneRuntime::spawn_argv_command( new_id, rows, cols, actual_cwd.clone(), + argv, scrollback_limit_bytes, host_terminal_theme, self.events.clone(), self.render_notify.clone(), self.render_dirty.clone(), - )? + )?, + None => PaneRuntime::spawn( + new_id, + rows, + cols, + actual_cwd.clone(), + scrollback_limit_bytes, + host_terminal_theme, + self.events.clone(), + self.render_notify.clone(), + self.render_dirty.clone(), + )?, }; self.panes.insert(new_id, PaneState::new()); self.pane_cwds.insert(new_id, actual_cwd); |
