diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-03-29 19:54:52 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-03-29 19:54:52 +0300 |
| commit | bf6b6d779a63d8952d8db0a7de5daa3454984620 (patch) | |
| tree | 06776548463ce0b70177f85ace997d6e33f07533 /src | |
| parent | a5847a8039d2754dd2908a13e4625b31f7404d9a (diff) | |
feat(api): use compact public workspace and pane ids
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/mod.rs | 190 | ||||
| -rw-r--r-- | src/cli.rs | 14 | ||||
| -rw-r--r-- | src/persist.rs | 9 | ||||
| -rw-r--r-- | src/workspace.rs | 30 |
4 files changed, 149 insertions, 94 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs index d5fa0ef2..ef424ed8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -211,13 +211,15 @@ impl App { match &ev { AppEvent::PaneDied { pane_id } => { if let Some((ws_idx, _)) = self.find_pane(*pane_id) { - self.emit_event(crate::api::schema::EventEnvelope { - event: crate::api::schema::EventKind::PaneExited, - data: crate::api::schema::EventData::PaneExited { - pane_id: format!("p_{}_{}", ws_idx + 1, pane_id.raw()), - workspace_id: format!("w_{}", ws_idx + 1), - }, - }); + 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::PaneExited, + data: crate::api::schema::EventData::PaneExited { + pane_id: public_pane_id, + workspace_id: self.public_workspace_id(ws_idx), + }, + }); + } } } AppEvent::StateChanged { @@ -226,27 +228,28 @@ impl App { state, } => { if let Some((ws_idx, pane)) = self.find_pane(*pane_id) { - let pane_id = format!("p_{}_{}", ws_idx + 1, pane_id.raw()); - let workspace_id = format!("w_{}", ws_idx + 1); - if pane.detected_agent != *agent { - self.emit_event(crate::api::schema::EventEnvelope { - event: crate::api::schema::EventKind::PaneAgentDetected, - data: crate::api::schema::EventData::PaneAgentDetected { - pane_id: pane_id.clone(), - workspace_id: workspace_id.clone(), - agent: agent.map(agent_name), - }, - }); - } - if pane.state != *state { - self.emit_event(crate::api::schema::EventEnvelope { - event: crate::api::schema::EventKind::PaneAgentStateChanged, - data: crate::api::schema::EventData::PaneAgentStateChanged { - pane_id, - workspace_id, - state: pane_agent_state(*state), - }, - }); + if let Some(pane_id) = self.public_pane_id(ws_idx, *pane_id) { + let workspace_id = self.public_workspace_id(ws_idx); + if pane.detected_agent != *agent { + self.emit_event(crate::api::schema::EventEnvelope { + event: crate::api::schema::EventKind::PaneAgentDetected, + data: crate::api::schema::EventData::PaneAgentDetected { + pane_id: pane_id.clone(), + workspace_id: workspace_id.clone(), + agent: agent.map(agent_name), + }, + }); + } + if pane.state != *state { + self.emit_event(crate::api::schema::EventEnvelope { + event: crate::api::schema::EventKind::PaneAgentStateChanged, + data: crate::api::schema::EventData::PaneAgentStateChanged { + pane_id, + workspace_id, + state: pane_agent_state(*state), + }, + }); + } } } } @@ -286,16 +289,18 @@ impl App { self.emit_event(crate::api::schema::EventEnvelope { event: crate::api::schema::EventKind::WorkspaceFocused, data: crate::api::schema::EventData::WorkspaceFocused { - workspace_id: format!("w_{}", ws_idx + 1), - }, - }); - self.emit_event(crate::api::schema::EventEnvelope { - event: crate::api::schema::EventKind::PaneFocused, - data: crate::api::schema::EventData::PaneFocused { - pane_id: format!("p_{}_{}", ws_idx + 1, pane_id.raw()), - workspace_id: format!("w_{}", ws_idx + 1), + 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, + data: crate::api::schema::EventData::PaneFocused { + pane_id: public_pane_id, + workspace_id: self.public_workspace_id(ws_idx), + }, + }); + } } self.last_focus = current_focus; @@ -312,6 +317,42 @@ impl App { .find_map(|(ws_idx, ws)| ws.panes.get(&pane_id).map(|pane| (ws_idx, pane))) } + fn public_workspace_id(&self, ws_idx: usize) -> String { + (ws_idx + 1).to_string() + } + + 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)?; + Some(format!("{}-{pane_number}", ws_idx + 1)) + } + + fn parse_workspace_id(&self, id: &str) -> Option<usize> { + if let Some(raw) = id.strip_prefix("w_") { + return raw.parse::<usize>().ok()?.checked_sub(1); + } + id.parse::<usize>().ok()?.checked_sub(1) + } + + fn parse_pane_id(&self, id: &str) -> Option<(usize, crate::layout::PaneId)> { + if let Some(rest) = id.strip_prefix("p_") { + let (ws_raw, pane_raw) = rest.split_once('_')?; + let ws_idx = ws_raw.parse::<usize>().ok()?.checked_sub(1)?; + let pane_id = crate::layout::PaneId::from_raw(pane_raw.parse::<u32>().ok()?); + return Some((ws_idx, pane_id)); + } + + let (ws_raw, pane_number_raw) = id.split_once('-')?; + let ws_idx = ws_raw.parse::<usize>().ok()?.checked_sub(1)?; + let pane_number = pane_number_raw.parse::<usize>().ok()?; + let ws = self.state.workspaces.get(ws_idx)?; + let pane_id = ws + .public_pane_numbers + .iter() + .find_map(|(pane_id, number)| (*number == pane_number).then_some(*pane_id))?; + Some((ws_idx, pane_id)) + } + fn handle_api_request(&mut self, request: crate::api::schema::Request) -> String { self.drain_internal_events(); use bytes::Bytes; @@ -330,12 +371,12 @@ impl App { .workspaces .iter() .enumerate() - .map(|(idx, _)| workspace_info(&self.state, idx)) + .map(|(idx, _)| self.workspace_info(idx)) .collect(), }, }, Method::WorkspaceGet(target) => { - let Some(index) = parse_workspace_id(&target.workspace_id) else { + let Some(index) = self.parse_workspace_id(&target.workspace_id) else { return serde_json::to_string(&ErrorResponse { id: request.id, error: ErrorBody { @@ -358,7 +399,7 @@ impl App { SuccessResponse { id: request.id, result: ResponseResult::WorkspaceInfo { - workspace: workspace_info(&self.state, index), + workspace: self.workspace_info(index), }, } } @@ -370,7 +411,7 @@ impl App { .unwrap_or_else(|| std::path::PathBuf::from("/")); match self.create_workspace_with_options(cwd, params.focus) { Ok(index) => { - let workspace = workspace_info(&self.state, index); + let workspace = self.workspace_info(index); self.emit_event(crate::api::schema::EventEnvelope { event: crate::api::schema::EventKind::WorkspaceCreated, data: crate::api::schema::EventData::WorkspaceCreated { @@ -408,7 +449,7 @@ impl App { } } Method::WorkspaceFocus(target) => { - let Some(index) = parse_workspace_id(&target.workspace_id) else { + let Some(index) = self.parse_workspace_id(&target.workspace_id) else { return serde_json::to_string(&ErrorResponse { id: request.id, error: ErrorBody { @@ -432,12 +473,12 @@ impl App { SuccessResponse { id: request.id, result: ResponseResult::WorkspaceInfo { - workspace: workspace_info(&self.state, index), + workspace: self.workspace_info(index), }, } } Method::WorkspaceRename(params) => { - let Some(index) = parse_workspace_id(¶ms.workspace_id) else { + let Some(index) = self.parse_workspace_id(¶ms.workspace_id) else { return serde_json::to_string(&ErrorResponse { id: request.id, error: ErrorBody { @@ -461,12 +502,12 @@ impl App { SuccessResponse { id: request.id, result: ResponseResult::WorkspaceInfo { - workspace: workspace_info(&self.state, index), + workspace: self.workspace_info(index), }, } } Method::WorkspaceClose(target) => { - let Some(index) = parse_workspace_id(&target.workspace_id) else { + let Some(index) = self.parse_workspace_id(&target.workspace_id) else { return serde_json::to_string(&ErrorResponse { id: request.id, error: ErrorBody { @@ -500,7 +541,8 @@ impl App { } } Method::PaneSplit(params) => { - let Some((ws_idx, target_pane_id)) = parse_pane_id(¶ms.target_pane_id) else { + let Some((ws_idx, target_pane_id)) = self.parse_pane_id(¶ms.target_pane_id) + else { return serde_json::to_string(&ErrorResponse { id: request.id, error: ErrorBody { @@ -577,7 +619,7 @@ impl App { } } Method::PaneGet(target) => { - let Some((ws_idx, pane_id)) = parse_pane_id(&target.pane_id) else { + let Some((ws_idx, pane_id)) = self.parse_pane_id(&target.pane_id) else { return serde_json::to_string(&ErrorResponse { id: request.id, error: ErrorBody { @@ -603,7 +645,7 @@ impl App { } } Method::PaneRead(params) => { - let Some((ws_idx, pane_id)) = parse_pane_id(¶ms.pane_id) else { + let Some((ws_idx, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { return serde_json::to_string(&ErrorResponse { id: request.id, error: ErrorBody { @@ -643,7 +685,7 @@ impl App { } } Method::PaneSendText(params) => { - let Some((ws_idx, pane_id)) = parse_pane_id(¶ms.pane_id) else { + let Some((ws_idx, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { return serde_json::to_string(&ErrorResponse { id: request.id, error: ErrorBody { @@ -679,7 +721,7 @@ impl App { } } Method::PaneClose(target) => { - let Some((ws_idx, pane_id)) = parse_pane_id(&target.pane_id) else { + let Some((ws_idx, pane_id)) = self.parse_pane_id(&target.pane_id) else { return serde_json::to_string(&ErrorResponse { id: request.id, error: ErrorBody { @@ -731,7 +773,7 @@ impl App { } } Method::PaneSendKeys(params) => { - let Some((ws_idx, pane_id)) = parse_pane_id(¶ms.pane_id) else { + let Some((ws_idx, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { return serde_json::to_string(&ErrorResponse { id: request.id, error: ErrorBody { @@ -861,7 +903,7 @@ impl App { workspace_id: Option<&str>, ) -> Result<Vec<crate::api::schema::PaneInfo>, (String, String)> { if let Some(workspace_id) = workspace_id { - let Some(ws_idx) = parse_workspace_id(workspace_id) else { + let Some(ws_idx) = self.parse_workspace_id(workspace_id) else { return Err(( "workspace_not_found".into(), format!("workspace {workspace_id} not found"), @@ -904,8 +946,8 @@ impl App { let pane = ws.panes.get(&pane_id)?; let runtime = ws.runtimes.get(&pane_id); Some(crate::api::schema::PaneInfo { - pane_id: format!("p_{}_{}", ws_idx + 1, pane_id.raw()), - workspace_id: format!("w_{}", ws_idx + 1), + pane_id: self.public_pane_id(ws_idx, pane_id)?, + workspace_id: self.public_workspace_id(ws_idx), focused: self.state.active == Some(ws_idx) && ws.layout.focused() == pane_id, cwd: runtime .and_then(|rt| rt.cwd()) @@ -923,7 +965,7 @@ impl App { ) -> Option<(&crate::pane::PaneRuntime, String)> { let ws = self.state.workspaces.get(ws_idx)?; let runtime = ws.runtimes.get(&pane_id)?; - Some((runtime, format!("w_{}", ws_idx + 1))) + Some((runtime, self.public_workspace_id(ws_idx))) } fn lookup_runtime_sender( @@ -938,6 +980,19 @@ impl App { let runtime = ws.runtimes.get(&pane_id)?; Some((&runtime.sender, runtime)) } + + fn workspace_info(&self, index: usize) -> crate::api::schema::WorkspaceInfo { + let ws = &self.state.workspaces[index]; + let (agg_state, _) = ws.aggregate_state(); + crate::api::schema::WorkspaceInfo { + workspace_id: self.public_workspace_id(index), + number: index + 1, + label: ws.display_name(), + focused: self.state.active == Some(index), + pane_count: ws.panes.len(), + agent_state: pane_agent_state(agg_state), + } + } } fn parse_api_key(key: &str) -> Option<crossterm::event::KeyEvent> { @@ -962,18 +1017,6 @@ fn parse_api_key(key: &str) -> Option<crossterm::event::KeyEvent> { } } -fn parse_workspace_id(id: &str) -> Option<usize> { - id.strip_prefix("w_")?.parse::<usize>().ok()?.checked_sub(1) -} - -fn parse_pane_id(id: &str) -> Option<(usize, crate::layout::PaneId)> { - let rest = id.strip_prefix("p_")?; - let (ws_raw, pane_raw) = rest.split_once('_')?; - let ws_idx = ws_raw.parse::<usize>().ok()?.checked_sub(1)?; - let pane_id = crate::layout::PaneId::from_raw(pane_raw.parse::<u32>().ok()?); - Some((ws_idx, pane_id)) -} - fn pane_agent_state(state: crate::detect::AgentState) -> crate::api::schema::PaneAgentState { match state { crate::detect::AgentState::Idle => crate::api::schema::PaneAgentState::Idle, @@ -983,19 +1026,6 @@ fn pane_agent_state(state: crate::detect::AgentState) -> crate::api::schema::Pan } } -fn workspace_info(state: &AppState, index: usize) -> crate::api::schema::WorkspaceInfo { - let ws = &state.workspaces[index]; - let (agg_state, _) = ws.aggregate_state(); - crate::api::schema::WorkspaceInfo { - workspace_id: format!("w_{}", index + 1), - number: index + 1, - label: ws.display_name(), - focused: state.active == Some(index), - pane_count: ws.panes.len(), - agent_state: pane_agent_state(agg_state), - } -} - fn agent_name(agent: crate::detect::Agent) -> String { match agent { crate::detect::Agent::Pi => "pi", @@ -662,24 +662,10 @@ fn send_request(request: &Request) -> std::io::Result<serde_json::Value> { } fn normalize_workspace_id(value: &str) -> String { - if value.starts_with("w_") { - return value.to_string(); - } - if let Ok(number) = value.parse::<usize>() { - return format!("w_{number}"); - } value.to_string() } fn normalize_pane_id(value: &str) -> String { - if value.starts_with("p_") { - return value.to_string(); - } - if let Some((workspace, pane)) = value.split_once('-') { - if workspace.parse::<usize>().is_ok() && pane.parse::<u32>().is_ok() { - return format!("p_{workspace}_{pane}"); - } - } value.to_string() } diff --git a/src/persist.rs b/src/persist.rs index 9e211730..4f6de57b 100644 --- a/src/persist.rs +++ b/src/persist.rs @@ -187,10 +187,19 @@ fn restore_workspace( .or_else(|| pane_ids.first().copied()) .unwrap_or(PaneId::from_raw(0)); + let public_pane_numbers = layout + .pane_ids() + .into_iter() + .enumerate() + .map(|(index, pane_id)| (pane_id, index + 1)) + .collect(); + Some(Workspace { custom_name: snap.custom_name.clone(), root_pane, layout, + public_pane_numbers, + next_public_pane_number: panes.len() + 1, panes, runtimes, zoomed: snap.zoomed, diff --git a/src/workspace.rs b/src/workspace.rs index 78992a73..ff8e302f 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -17,6 +17,10 @@ pub struct Workspace { /// Identity source for this workspace. pub root_pane: PaneId, pub layout: TileLayout, + /// Stable-ish public pane numbers within this workspace. + /// New panes append at the end; closing a pane compacts higher numbers down. + pub public_pane_numbers: HashMap<PaneId, usize>, + pub(crate) next_public_pane_number: usize, /// Pane state — always present, testable without PTYs. pub panes: HashMap<PaneId, PaneState>, /// Pane runtimes — only present in production (empty in tests). @@ -37,6 +41,8 @@ impl Workspace { let mut panes = HashMap::new(); panes.insert(root_id, PaneState::new()); + let mut public_pane_numbers = HashMap::new(); + public_pane_numbers.insert(root_id, 1); let mut runtimes = HashMap::new(); runtimes.insert(root_id, runtime); @@ -45,6 +51,8 @@ impl Workspace { custom_name: None, root_pane: root_id, layout, + public_pane_numbers, + next_public_pane_number: 2, panes, runtimes, zoomed: false, @@ -65,6 +73,9 @@ impl Workspace { cwd.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| "/".into())); let runtime = PaneRuntime::spawn(new_id, rows, cols, actual_cwd, self.events.clone())?; self.panes.insert(new_id, PaneState::new()); + self.public_pane_numbers + .insert(new_id, self.next_public_pane_number); + self.next_public_pane_number += 1; self.runtimes.insert(new_id, runtime); self.zoomed = false; Ok(new_id) @@ -94,6 +105,14 @@ impl Workspace { self.layout.focus_pane(prev_focus); } + if let Some(removed_number) = self.public_pane_numbers.remove(&pane_id) { + for number in self.public_pane_numbers.values_mut() { + if *number > removed_number { + *number -= 1; + } + } + self.next_public_pane_number = self.public_pane_numbers.len() + 1; + } self.panes.remove(&pane_id); self.runtimes.remove(&pane_id); self.zoomed = false; @@ -110,6 +129,10 @@ impl Workspace { self.layout.pane_ids().into_iter().find(|id| *id != closing) } + pub fn public_pane_number(&self, pane_id: PaneId) -> Option<usize> { + self.public_pane_numbers.get(&pane_id).copied() + } + /// Get the runtime for the focused pane. pub fn focused_runtime(&self) -> Option<&PaneRuntime> { self.runtimes.get(&self.layout.focused()) @@ -251,10 +274,14 @@ impl Workspace { let (layout, root_id) = TileLayout::new(); let mut panes = HashMap::new(); panes.insert(root_id, PaneState::new()); + let mut public_pane_numbers = HashMap::new(); + public_pane_numbers.insert(root_id, 1); Self { custom_name: Some(name.to_string()), root_pane: root_id, layout, + public_pane_numbers, + next_public_pane_number: 2, panes, runtimes: HashMap::new(), zoomed: false, @@ -266,6 +293,9 @@ impl Workspace { pub fn test_split(&mut self, direction: Direction) -> PaneId { let new_id = self.layout.split_focused(direction); self.panes.insert(new_id, PaneState::new()); + self.public_pane_numbers + .insert(new_id, self.next_public_pane_number); + self.next_public_pane_number += 1; new_id } } |
