aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-03-29 19:54:52 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-03-29 19:54:52 +0300
commitbf6b6d779a63d8952d8db0a7de5daa3454984620 (patch)
tree06776548463ce0b70177f85ace997d6e33f07533 /src
parenta5847a8039d2754dd2908a13e4625b31f7404d9a (diff)
feat(api): use compact public workspace and pane ids
Diffstat (limited to 'src')
-rw-r--r--src/app/mod.rs190
-rw-r--r--src/cli.rs14
-rw-r--r--src/persist.rs9
-rw-r--r--src/workspace.rs30
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(&params.workspace_id) else {
+ let Some(index) = self.parse_workspace_id(&params.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(&params.target_pane_id) else {
+ let Some((ws_idx, target_pane_id)) = self.parse_pane_id(&params.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(&params.pane_id) else {
+ let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.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(&params.pane_id) else {
+ let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.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(&params.pane_id) else {
+ let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.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",
diff --git a/src/cli.rs b/src/cli.rs
index 724a8f57..6ce6853e 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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
}
}