diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/mod.rs | 16 | ||||
| -rw-r--r-- | src/api/schema.rs | 106 | ||||
| -rw-r--r-- | src/app/mod.rs | 367 | ||||
| -rw-r--r-- | src/cli.rs | 180 | ||||
| -rw-r--r-- | src/main.rs | 2 |
5 files changed, 668 insertions, 3 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs index bf95bf29..2e7692a4 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -430,6 +430,22 @@ impl ActiveSubscription { event_kind: crate::api::schema::EventKind::WorkspaceFocused, last_sequence: 0, })), + Subscription::TabCreated {} => Ok(Self::Event(ActiveEventSubscription { + event_kind: crate::api::schema::EventKind::TabCreated, + last_sequence: 0, + })), + Subscription::TabClosed {} => Ok(Self::Event(ActiveEventSubscription { + event_kind: crate::api::schema::EventKind::TabClosed, + last_sequence: 0, + })), + Subscription::TabFocused {} => Ok(Self::Event(ActiveEventSubscription { + event_kind: crate::api::schema::EventKind::TabFocused, + last_sequence: 0, + })), + Subscription::TabRenamed {} => Ok(Self::Event(ActiveEventSubscription { + event_kind: crate::api::schema::EventKind::TabRenamed, + last_sequence: 0, + })), Subscription::PaneCreated {} => Ok(Self::Event(ActiveEventSubscription { event_kind: crate::api::schema::EventKind::PaneCreated, last_sequence: 0, diff --git a/src/api/schema.rs b/src/api/schema.rs index a187a889..5a27b01b 100644 --- a/src/api/schema.rs +++ b/src/api/schema.rs @@ -24,6 +24,18 @@ pub enum Method { WorkspaceRename(WorkspaceRenameParams), #[serde(rename = "workspace.close")] WorkspaceClose(WorkspaceTarget), + #[serde(rename = "tab.create")] + TabCreate(TabCreateParams), + #[serde(rename = "tab.list")] + TabList(TabListParams), + #[serde(rename = "tab.get")] + TabGet(TabTarget), + #[serde(rename = "tab.focus")] + TabFocus(TabTarget), + #[serde(rename = "tab.rename")] + TabRename(TabRenameParams), + #[serde(rename = "tab.close")] + TabClose(TabTarget), #[serde(rename = "pane.split")] PaneSplit(PaneSplitParams), #[serde(rename = "pane.list")] @@ -69,6 +81,11 @@ pub struct PaneTarget { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TabTarget { + pub tab_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct WorkspaceCreateParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub cwd: Option<String>, @@ -83,6 +100,28 @@ pub struct WorkspaceRenameParams { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TabCreateParams { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_id: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cwd: Option<String>, + #[serde(default)] + pub focus: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct TabListParams { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_id: Option<String>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TabRenameParams { + pub tab_id: String, + pub label: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PaneSplitParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub workspace_id: Option<String>, @@ -174,6 +213,14 @@ pub enum Subscription { WorkspaceClosed {}, #[serde(rename = "workspace.focused")] WorkspaceFocused {}, + #[serde(rename = "tab.created")] + TabCreated {}, + #[serde(rename = "tab.closed")] + TabClosed {}, + #[serde(rename = "tab.focused")] + TabFocused {}, + #[serde(rename = "tab.renamed")] + TabRenamed {}, #[serde(rename = "pane.created")] PaneCreated {}, #[serde(rename = "pane.closed")] @@ -247,6 +294,23 @@ pub enum EventMatch { WorkspaceFocused { workspace_id: String, }, + TabCreated { + #[serde(default, skip_serializing_if = "Option::is_none")] + tab_id: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + workspace_id: Option<String>, + }, + TabClosed { + tab_id: String, + }, + TabRenamed { + tab_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + label: Option<String>, + }, + TabFocused { + tab_id: String, + }, PaneCreated { #[serde(default, skip_serializing_if = "Option::is_none")] pane_id: Option<String>, @@ -285,6 +349,10 @@ pub enum EventKind { WorkspaceClosed, WorkspaceRenamed, WorkspaceFocused, + TabCreated, + TabClosed, + TabRenamed, + TabFocused, PaneCreated, PaneClosed, PaneFocused, @@ -324,6 +392,12 @@ pub enum ResponseResult { WorkspaceList { workspaces: Vec<WorkspaceInfo>, }, + TabInfo { + tab: TabInfo, + }, + TabList { + tabs: Vec<TabInfo>, + }, PaneInfo { pane: PaneInfo, }, @@ -353,6 +427,19 @@ pub struct WorkspaceInfo { pub label: String, pub focused: bool, pub pane_count: usize, + pub tab_count: usize, + pub active_tab_id: String, + pub agent_state: PaneAgentState, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TabInfo { + pub tab_id: String, + pub workspace_id: String, + pub number: usize, + pub label: String, + pub focused: bool, + pub pane_count: usize, pub agent_state: PaneAgentState, } @@ -360,6 +447,7 @@ pub struct WorkspaceInfo { pub struct PaneInfo { pub pane_id: String, pub workspace_id: String, + pub tab_id: String, pub focused: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub cwd: Option<String>, @@ -373,6 +461,7 @@ pub struct PaneInfo { pub struct PaneReadResult { pub pane_id: String, pub workspace_id: String, + pub tab_id: String, pub source: ReadSource, pub text: String, pub revision: u64, @@ -438,6 +527,22 @@ pub enum EventData { WorkspaceFocused { workspace_id: String, }, + TabCreated { + tab: TabInfo, + }, + TabClosed { + tab_id: String, + workspace_id: String, + }, + TabRenamed { + tab_id: String, + workspace_id: String, + label: String, + }, + TabFocused { + tab_id: String, + workspace_id: String, + }, PaneCreated { pane: PaneInfo, }, @@ -682,6 +787,7 @@ mod tests { read: PaneReadResult { pane_id: "p_1_1".into(), workspace_id: "w_1".into(), + tab_id: "t_1_1".into(), source: ReadSource::Recent, text: "auth: received\n".into(), revision: 0, diff --git a/src/app/mod.rs b/src/app/mod.rs index 5f5d5072..311e5f82 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -90,6 +90,10 @@ fn api_request_changes_ui(request: &crate::api::schema::Request) -> bool { | Method::WorkspaceFocus(_) | Method::WorkspaceRename(_) | Method::WorkspaceClose(_) + | Method::TabCreate(_) + | Method::TabFocus(_) + | Method::TabRename(_) + | Method::TabClose(_) | Method::PaneSplit(_) | Method::PaneReportAgent(_) | Method::PaneClearAgentAuthority(_) @@ -706,6 +710,15 @@ impl App { workspace_id: self.public_workspace_id(ws_idx), }, }); + if let Some(tab_id) = self.public_tab_id(ws_idx, self.state.workspaces[ws_idx].active_tab) { + self.emit_event(crate::api::schema::EventEnvelope { + event: crate::api::schema::EventKind::TabFocused, + data: crate::api::schema::EventData::TabFocused { + tab_id, + workspace_id: self.public_workspace_id(ws_idx), + }, + }); + } if let Some(public_pane_id) = self.public_pane_id(ws_idx, pane_id) { self.emit_event(crate::api::schema::EventEnvelope { event: crate::api::schema::EventKind::PaneFocused, @@ -735,6 +748,11 @@ impl App { (ws_idx + 1).to_string() } + fn public_tab_id(&self, ws_idx: usize, tab_idx: usize) -> Option<String> { + self.state.workspaces.get(ws_idx)?.tabs.get(tab_idx)?; + Some(format!("{}:{}", ws_idx + 1, tab_idx + 1)) + } + fn public_pane_id(&self, ws_idx: usize, pane_id: crate::layout::PaneId) -> Option<String> { let ws = self.state.workspaces.get(ws_idx)?; let pane_number = ws.public_pane_number(pane_id)?; @@ -748,6 +766,22 @@ impl App { id.parse::<usize>().ok()?.checked_sub(1) } + fn parse_tab_id(&self, id: &str) -> Option<(usize, usize)> { + if let Some(rest) = id.strip_prefix("t_") { + let (ws_raw, tab_raw) = rest.split_once('_')?; + let ws_idx = ws_raw.parse::<usize>().ok()?.checked_sub(1)?; + let tab_idx = tab_raw.parse::<usize>().ok()?.checked_sub(1)?; + self.state.workspaces.get(ws_idx)?.tabs.get(tab_idx)?; + return Some((ws_idx, tab_idx)); + } + + let (ws_raw, tab_raw) = id.split_once(':')?; + let ws_idx = ws_raw.parse::<usize>().ok()?.checked_sub(1)?; + let tab_idx = tab_raw.parse::<usize>().ok()?.checked_sub(1)?; + self.state.workspaces.get(ws_idx)?.tabs.get(tab_idx)?; + Some((ws_idx, tab_idx)) + } + fn parse_pane_id(&self, id: &str) -> Option<(usize, crate::layout::PaneId)> { if let Some(rest) = id.strip_prefix("p_") { if let Some((ws_raw, pane_raw)) = rest.split_once('_') { @@ -777,7 +811,7 @@ impl App { use crate::api::schema::{ ErrorBody, ErrorResponse, Method, PaneListParams, PaneReadResult, ReadSource, - ResponseResult, SuccessResponse, + ResponseResult, SuccessResponse, TabListParams, }; let response = match request.method { @@ -836,6 +870,12 @@ impl App { workspace: workspace.clone(), }, }); + if let Some(tab) = self.tab_info(index, 0) { + self.emit_event(crate::api::schema::EventEnvelope { + event: crate::api::schema::EventKind::TabCreated, + data: crate::api::schema::EventData::TabCreated { tab }, + }); + } if let Some(pane_id) = self.state.workspaces[index] .layout .pane_ids() @@ -917,6 +957,13 @@ impl App { .unwrap(); }; ws.set_custom_name(params.label.clone()); + self.emit_event(crate::api::schema::EventEnvelope { + event: crate::api::schema::EventKind::WorkspaceRenamed, + data: crate::api::schema::EventData::WorkspaceRenamed { + workspace_id: self.public_workspace_id(index), + label: params.label, + }, + }); SuccessResponse { id: request.id, result: ResponseResult::WorkspaceInfo { @@ -958,6 +1005,278 @@ impl App { result: ResponseResult::Ok {}, } } + Method::TabList(TabListParams { workspace_id }) => { + let tabs = if let Some(workspace_id) = workspace_id { + let Some(ws_idx) = self.parse_workspace_id(&workspace_id) else { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "workspace_not_found".into(), + message: format!("workspace {} not found", workspace_id), + }, + }) + .unwrap(); + }; + let Some(ws) = self.state.workspaces.get(ws_idx) else { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "workspace_not_found".into(), + message: format!("workspace {} not found", workspace_id), + }, + }) + .unwrap(); + }; + (0..ws.tabs.len()) + .filter_map(|tab_idx| self.tab_info(ws_idx, tab_idx)) + .collect() + } else { + let mut tabs = Vec::new(); + for (ws_idx, ws) in self.state.workspaces.iter().enumerate() { + for tab_idx in 0..ws.tabs.len() { + if let Some(tab) = self.tab_info(ws_idx, tab_idx) { + tabs.push(tab); + } + } + } + tabs + }; + SuccessResponse { + id: request.id, + result: ResponseResult::TabList { tabs }, + } + } + Method::TabGet(target) => { + let Some((ws_idx, tab_idx)) = self.parse_tab_id(&target.tab_id) else { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "tab_not_found".into(), + message: format!("tab {} not found", target.tab_id), + }, + }) + .unwrap(); + }; + let Some(tab) = self.tab_info(ws_idx, tab_idx) else { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "tab_not_found".into(), + message: format!("tab {} not found", target.tab_id), + }, + }) + .unwrap(); + }; + SuccessResponse { + id: request.id, + result: ResponseResult::TabInfo { tab }, + } + } + Method::TabCreate(params) => { + let ws_idx = if let Some(workspace_id) = params.workspace_id { + let Some(ws_idx) = self.parse_workspace_id(&workspace_id) else { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "workspace_not_found".into(), + message: format!("workspace {} not found", workspace_id), + }, + }) + .unwrap(); + }; + ws_idx + } else if let Some(active) = self.state.active { + active + } else { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "workspace_not_found".into(), + message: "no active workspace".into(), + }, + }) + .unwrap(); + }; + let cwd = params + .cwd + .map(std::path::PathBuf::from) + .or_else(|| { + self.state.workspaces.get(ws_idx).and_then(|ws| { + ws.active_tab() + .and_then(|tab| tab.focused_runtime()) + .and_then(|rt| rt.cwd()) + }) + }) + .or_else(|| std::env::current_dir().ok()) + .unwrap_or_else(|| std::path::PathBuf::from("/")); + let (rows, cols) = self.state.estimate_pane_size(); + let result = self + .state + .workspaces + .get_mut(ws_idx) + .ok_or_else(|| std::io::Error::other("workspace disappeared")) + .and_then(|ws| ws.create_tab(rows, cols, cwd)); + match result { + Ok(tab_idx) => { + if params.focus { + self.state.switch_workspace(ws_idx); + if let Some(ws) = self.state.workspaces.get_mut(ws_idx) { + ws.switch_tab(tab_idx); + } + self.state.mode = Mode::Terminal; + } + let tab = self.tab_info(ws_idx, tab_idx).unwrap(); + if let Some(pane_id) = self.state.workspaces[ws_idx].tabs[tab_idx] + .layout + .pane_ids() + .first() + .copied() + { + self.emit_event(crate::api::schema::EventEnvelope { + event: crate::api::schema::EventKind::TabCreated, + data: crate::api::schema::EventData::TabCreated { + tab: tab.clone(), + }, + }); + if let Some(pane) = self.pane_info(ws_idx, pane_id) { + self.emit_event(crate::api::schema::EventEnvelope { + event: crate::api::schema::EventKind::PaneCreated, + data: crate::api::schema::EventData::PaneCreated { pane }, + }); + } + } + SuccessResponse { + id: request.id, + result: ResponseResult::TabInfo { tab }, + } + } + Err(err) => { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "tab_create_failed".into(), + message: err.to_string(), + }, + }) + .unwrap(); + } + } + } + Method::TabFocus(target) => { + let Some((ws_idx, tab_idx)) = self.parse_tab_id(&target.tab_id) else { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "tab_not_found".into(), + message: format!("tab {} not found", target.tab_id), + }, + }) + .unwrap(); + }; + self.state.switch_workspace(ws_idx); + if let Some(ws) = self.state.workspaces.get_mut(ws_idx) { + ws.switch_tab(tab_idx); + } + let tab = self.tab_info(ws_idx, tab_idx).unwrap(); + SuccessResponse { + id: request.id, + result: ResponseResult::TabInfo { tab }, + } + } + Method::TabRename(params) => { + let Some((ws_idx, tab_idx)) = self.parse_tab_id(¶ms.tab_id) else { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "tab_not_found".into(), + message: format!("tab {} not found", params.tab_id), + }, + }) + .unwrap(); + }; + let Some(tab) = self + .state + .workspaces + .get_mut(ws_idx) + .and_then(|ws| ws.tabs.get_mut(tab_idx)) + else { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "tab_not_found".into(), + message: format!("tab {} not found", params.tab_id), + }, + }) + .unwrap(); + }; + tab.set_custom_name(params.label.clone()); + self.emit_event(crate::api::schema::EventEnvelope { + event: crate::api::schema::EventKind::TabRenamed, + data: crate::api::schema::EventData::TabRenamed { + tab_id: self.public_tab_id(ws_idx, tab_idx).unwrap(), + workspace_id: self.public_workspace_id(ws_idx), + label: params.label, + }, + }); + let tab = self.tab_info(ws_idx, tab_idx).unwrap(); + SuccessResponse { + id: request.id, + result: ResponseResult::TabInfo { tab }, + } + } + Method::TabClose(target) => { + let Some((ws_idx, tab_idx)) = self.parse_tab_id(&target.tab_id) else { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "tab_not_found".into(), + message: format!("tab {} not found", target.tab_id), + }, + }) + .unwrap(); + }; + let Some(ws) = self.state.workspaces.get_mut(ws_idx) else { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "tab_not_found".into(), + message: format!("tab {} not found", target.tab_id), + }, + }) + .unwrap(); + }; + if ws.tabs.len() <= 1 { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "tab_close_failed".into(), + message: "cannot close the last tab in a workspace".into(), + }, + }) + .unwrap(); + } + if !ws.close_tab(tab_idx) { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "tab_close_failed".into(), + message: format!("tab {} could not be closed", target.tab_id), + }, + }) + .unwrap(); + } + self.emit_event(crate::api::schema::EventEnvelope { + event: crate::api::schema::EventKind::TabClosed, + data: crate::api::schema::EventData::TabClosed { + tab_id: target.tab_id, + workspace_id: self.public_workspace_id(ws_idx), + }, + }); + SuccessResponse { + id: request.id, + result: ResponseResult::Ok {}, + } + } Method::PaneSplit(params) => { let Some((ws_idx, target_pane_id)) = self.parse_pane_id(¶ms.target_pane_id) else { @@ -1083,6 +1402,16 @@ impl App { }) .unwrap(); }; + let Some(tab_idx) = self.state.workspaces.get(ws_idx).and_then(|ws| ws.find_tab_index_for_pane(pane_id)) else { + return serde_json::to_string(&ErrorResponse { + id: request.id, + error: ErrorBody { + code: "pane_not_found".into(), + message: format!("pane {} not found", params.pane_id), + }, + }) + .unwrap(); + }; let requested_lines = params.lines.unwrap_or(80).min(1000) as usize; let text = match params.source { ReadSource::Visible => pane.visible_text(), @@ -1094,6 +1423,7 @@ impl App { read: PaneReadResult { pane_id: params.pane_id, workspace_id, + tab_id: self.public_tab_id(ws_idx, tab_idx).unwrap(), source: params.source, text, revision: 0, @@ -1557,6 +1887,26 @@ impl App { } } + fn tab_info(&self, ws_idx: usize, tab_idx: usize) -> Option<crate::api::schema::TabInfo> { + let ws = self.state.workspaces.get(ws_idx)?; + let tab = ws.tabs.get(tab_idx)?; + let (agg_state, _) = tab + .panes + .values() + .map(|pane| (pane.state, pane.seen)) + .max_by_key(|(state, seen)| tab_attention_priority(*state, *seen)) + .unwrap_or((crate::detect::AgentState::Unknown, true)); + Some(crate::api::schema::TabInfo { + tab_id: self.public_tab_id(ws_idx, tab_idx)?, + workspace_id: self.public_workspace_id(ws_idx), + number: tab_idx + 1, + label: tab.display_name(), + focused: self.state.active == Some(ws_idx) && ws.active_tab == tab_idx, + pane_count: tab.panes.len(), + agent_state: pane_agent_state(agg_state), + }) + } + fn pane_info( &self, ws_idx: usize, @@ -1565,13 +1915,16 @@ impl App { 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 && ws .focused_pane_id() .is_some_and(|focused| focused == pane_id); Some(crate::api::schema::PaneInfo { pane_id: self.public_pane_id(ws_idx, pane_id)?, 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()) @@ -1614,11 +1967,23 @@ impl App { label: ws.display_name(), focused: self.state.active == Some(index), pane_count: ws.public_pane_numbers.len(), + tab_count: ws.tabs.len(), + active_tab_id: self.public_tab_id(index, ws.active_tab).unwrap_or_else(|| format!("{}:{}", index + 1, ws.active_tab + 1)), agent_state: pane_agent_state(agg_state), } } } +fn tab_attention_priority(state: crate::detect::AgentState, seen: bool) -> u8 { + match (state, seen) { + (crate::detect::AgentState::Blocked, _) => 4, + (crate::detect::AgentState::Idle, false) => 3, + (crate::detect::AgentState::Working, _) => 2, + (crate::detect::AgentState::Idle, true) => 1, + (crate::detect::AgentState::Unknown, _) => 0, + } +} + fn parse_api_key(key: &str) -> Option<crossterm::event::KeyEvent> { use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; @@ -8,8 +8,8 @@ use crate::api; use crate::api::schema::{ EmptyParams, Method, OutputMatch, PaneAgentState, PaneListParams, PaneReadParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneTarget, PaneWaitForOutputParams, - ReadSource, Request, SplitDirection, Subscription, WorkspaceCreateParams, - WorkspaceRenameParams, WorkspaceTarget, + ReadSource, Request, SplitDirection, Subscription, TabCreateParams, TabListParams, TabRenameParams, + TabTarget, WorkspaceCreateParams, WorkspaceRenameParams, WorkspaceTarget, }; pub enum CommandOutcome { @@ -24,6 +24,7 @@ pub fn maybe_run(args: &[String]) -> std::io::Result<CommandOutcome> { let exit_code = match command { "workspace" => run_workspace_command(&args[2..])?, + "tab" => run_tab_command(&args[2..])?, "pane" => run_pane_command(&args[2..])?, "wait" => run_wait_command(&args[2..])?, "integration" => run_integration_command(&args[2..])?, @@ -53,6 +54,26 @@ fn run_workspace_command(args: &[String]) -> std::io::Result<i32> { } } +fn run_tab_command(args: &[String]) -> std::io::Result<i32> { + let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { + print_tab_help(); + return Ok(2); + }; + + match subcommand { + "list" => tab_list(&args[1..]), + "create" => tab_create(&args[1..]), + "get" => tab_get(&args[1..]), + "focus" => tab_focus(&args[1..]), + "rename" => tab_rename(&args[1..]), + "close" => tab_close(&args[1..]), + _ => { + print_tab_help(); + Ok(2) + } + } +} + fn run_pane_command(args: &[String]) -> std::io::Result<i32> { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_pane_help(); @@ -220,6 +241,147 @@ fn workspace_close(args: &[String]) -> std::io::Result<i32> { })?) } +fn tab_list(args: &[String]) -> std::io::Result<i32> { + let mut workspace_id = None; + + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--workspace" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --workspace"); + return Ok(2); + }; + workspace_id = Some(normalize_workspace_id(value)); + index += 2; + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + print_response(&send_request(&Request { + id: "cli:tab:list".into(), + method: Method::TabList(TabListParams { workspace_id }), + })?) +} + +fn tab_create(args: &[String]) -> std::io::Result<i32> { + let mut workspace_id = None; + let mut cwd = None; + let mut focus = true; + + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--workspace" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --workspace"); + return Ok(2); + }; + workspace_id = Some(normalize_workspace_id(value)); + index += 2; + } + "--cwd" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --cwd"); + return Ok(2); + }; + cwd = Some(value.clone()); + index += 2; + } + "--no-focus" => { + focus = false; + index += 1; + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + print_response(&send_request(&Request { + id: "cli:tab:create".into(), + method: Method::TabCreate(TabCreateParams { + workspace_id, + cwd, + focus, + }), + })?) +} + +fn tab_get(args: &[String]) -> std::io::Result<i32> { + let Some(raw_tab_id) = args.first() else { + eprintln!("usage: herdr tab get <tab_id>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr tab get <tab_id>"); + return Ok(2); + } + + print_response(&send_request(&Request { + id: "cli:tab:get".into(), + method: Method::TabGet(TabTarget { + tab_id: normalize_tab_id(raw_tab_id), + }), + })?) +} + +fn tab_focus(args: &[String]) -> std::io::Result<i32> { + let Some(raw_tab_id) = args.first() else { + eprintln!("usage: herdr tab focus <tab_id>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr tab focus <tab_id>"); + return Ok(2); + } + + print_response(&send_request(&Request { + id: "cli:tab:focus".into(), + method: Method::TabFocus(TabTarget { + tab_id: normalize_tab_id(raw_tab_id), + }), + })?) +} + +fn tab_rename(args: &[String]) -> std::io::Result<i32> { + if args.len() < 2 { + eprintln!("usage: herdr tab rename <tab_id> <label>"); + return Ok(2); + } + + print_response(&send_request(&Request { + id: "cli:tab:rename".into(), + method: Method::TabRename(TabRenameParams { + tab_id: normalize_tab_id(&args[0]), + label: args[1..].join(" "), + }), + })?) +} + +fn tab_close(args: &[String]) -> std::io::Result<i32> { + let Some(raw_tab_id) = args.first() else { + eprintln!("usage: herdr tab close <tab_id>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr tab close <tab_id>"); + return Ok(2); + } + + print_response(&send_request(&Request { + id: "cli:tab:close".into(), + method: Method::TabClose(TabTarget { + tab_id: normalize_tab_id(raw_tab_id), + }), + })?) +} + fn pane_list(args: &[String]) -> std::io::Result<i32> { let mut workspace_id = None; @@ -832,6 +994,10 @@ fn normalize_workspace_id(value: &str) -> String { value.to_string() } +fn normalize_tab_id(value: &str) -> String { + value.to_string() +} + fn normalize_pane_id(value: &str) -> String { value.to_string() } @@ -890,6 +1056,16 @@ fn print_workspace_help() { eprintln!(" herdr workspace close <workspace_id>"); } +fn print_tab_help() { + eprintln!("herdr tab commands:"); + eprintln!(" herdr tab list [--workspace <workspace_id>]"); + eprintln!(" herdr tab create [--workspace <workspace_id>] [--cwd PATH] [--no-focus]"); + eprintln!(" herdr tab get <tab_id>"); + eprintln!(" herdr tab focus <tab_id>"); + eprintln!(" herdr tab rename <tab_id> <label>"); + eprintln!(" herdr tab close <tab_id>"); +} + fn print_pane_help() { eprintln!("herdr pane commands:"); eprintln!(" herdr pane list [--workspace <workspace_id>]"); diff --git a/src/main.rs b/src/main.rs index 4d0a9442..385f6f5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -197,6 +197,7 @@ fn main() -> io::Result<()> { println!("Usage: herdr [options]"); println!(" herdr update"); println!(" herdr workspace <subcommand> ..."); + println!(" herdr tab <subcommand> ..."); println!(" herdr pane <subcommand> ..."); println!(" herdr wait <subcommand> ..."); println!(" herdr integration <subcommand> ..."); @@ -204,6 +205,7 @@ fn main() -> io::Result<()> { println!("Commands:"); println!(" update Download and install the latest version"); println!(" workspace workspace helpers over the socket api"); + println!(" tab tab helpers over the socket api"); println!(" pane pane control helpers over the socket api"); println!(" wait blocking wait helpers over the socket api"); println!(" integration manage built-in agent integrations"); |
