aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-04 01:50:34 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-04 01:50:34 +0300
commit831894facfc1339f819b42c3339668154121d24f (patch)
tree3a49bad36a65341f6296b9f3d568317e15b6c21a /src
parente0fb8ca6fa55acaa72902a15c5468cbe2787ddf7 (diff)
refactor: make workspace api ids stable
Diffstat (limited to 'src')
-rw-r--r--src/app/mod.rs39
-rw-r--r--src/persist.rs13
-rw-r--r--src/workspace.rs18
3 files changed, 51 insertions, 19 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs
index a6f18177..c78d1c74 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -780,38 +780,41 @@ impl App {
}
fn public_workspace_id(&self, ws_idx: usize) -> String {
- (ws_idx + 1).to_string()
+ self.state.workspaces[ws_idx].id.clone()
}
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))
+ let ws = self.state.workspaces.get(ws_idx)?;
+ ws.tabs.get(tab_idx)?;
+ Some(format!("{}:{}", ws.id, 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)?;
- Some(format!("{}-{pane_number}", ws_idx + 1))
+ Some(format!("{}-{pane_number}", ws.id))
}
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)
+ self.state
+ .workspaces
+ .iter()
+ .position(|workspace| workspace.id == id)
+ .or_else(|| id.strip_prefix("w_")?.parse::<usize>().ok()?.checked_sub(1))
+ .or_else(|| 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 (ws_raw, tab_raw) = rest.rsplit_once('_')?;
+ let ws_idx = self.parse_workspace_id(ws_raw)?;
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 (ws_raw, tab_raw) = id.rsplit_once(':')?;
+ let ws_idx = self.parse_workspace_id(ws_raw)?;
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))
@@ -819,8 +822,8 @@ impl App {
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('_') {
- let ws_idx = ws_raw.parse::<usize>().ok()?.checked_sub(1)?;
+ if let Some((ws_raw, pane_raw)) = rest.rsplit_once('_') {
+ let ws_idx = self.parse_workspace_id(ws_raw)?;
let pane_id = crate::layout::PaneId::from_raw(pane_raw.parse::<u32>().ok()?);
return Some((ws_idx, pane_id));
}
@@ -829,8 +832,8 @@ impl App {
return self.find_pane(pane_id).map(|(ws_idx, _)| (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 (ws_raw, pane_number_raw) = id.rsplit_once('-')?;
+ let ws_idx = self.parse_workspace_id(ws_raw)?;
let pane_number = pane_number_raw.parse::<usize>().ok()?;
let ws = self.state.workspaces.get(ws_idx)?;
let pane_id = ws
@@ -1603,6 +1606,7 @@ impl App {
})
.unwrap();
};
+ let workspace_id = self.state.workspaces[ws_idx].id.clone();
let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
return serde_json::to_string(&ErrorResponse {
id: request.id,
@@ -1613,7 +1617,6 @@ impl App {
})
.unwrap();
};
- let workspace_id = format!("w_{}", ws_idx + 1);
let pane_count = ws.layout.pane_count();
if pane_count <= 1 {
self.state.selected = ws_idx;
@@ -2039,7 +2042,7 @@ impl App {
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)),
+ .unwrap_or_else(|| format!("{}:{}", ws.id, ws.active_tab + 1)),
agent_state: pane_agent_state(agg_state),
}
}
diff --git a/src/persist.rs b/src/persist.rs
index fb8ab2d8..3b181b06 100644
--- a/src/persist.rs
+++ b/src/persist.rs
@@ -35,6 +35,8 @@ pub struct SessionSnapshot {
#[derive(Serialize)]
pub struct WorkspaceSnapshot {
#[serde(default)]
+ pub id: Option<String>,
+ #[serde(default)]
pub custom_name: Option<String>,
pub identity_cwd: PathBuf,
pub tabs: Vec<TabSnapshot>,
@@ -45,6 +47,8 @@ pub struct WorkspaceSnapshot {
#[derive(Deserialize)]
struct WorkspaceSnapshotWire {
#[serde(default)]
+ id: Option<String>,
+ #[serde(default)]
custom_name: Option<String>,
#[serde(default)]
identity_cwd: Option<PathBuf>,
@@ -126,6 +130,7 @@ impl From<LegacyWorkspaceSnapshot> for WorkspaceSnapshot {
};
Self {
+ id: None,
custom_name: snap.custom_name,
identity_cwd,
tabs: vec![tab],
@@ -143,6 +148,7 @@ impl<'de> Deserialize<'de> for WorkspaceSnapshot {
if let Some(identity_cwd) = snap.identity_cwd {
return Ok(Self {
+ id: snap.id,
custom_name: snap.custom_name,
identity_cwd,
tabs: snap.tabs,
@@ -220,6 +226,7 @@ pub fn capture(
fn capture_workspace(ws: &Workspace) -> WorkspaceSnapshot {
WorkspaceSnapshot {
+ id: Some(ws.id.clone()),
custom_name: ws.custom_name.clone(),
identity_cwd: ws
.resolved_identity_cwd()
@@ -328,6 +335,10 @@ fn restore_workspace(
}
Some(Workspace {
+ id: snap
+ .id
+ .clone()
+ .unwrap_or_else(crate::workspace::generate_workspace_id),
custom_name: snap.custom_name.clone(),
identity_cwd: snap.identity_cwd.clone(),
cached_git_ahead_behind: None,
@@ -592,6 +603,7 @@ mod tests {
let snap = SessionSnapshot {
workspaces: vec![WorkspaceSnapshot {
+ id: Some("wproj".to_string()),
custom_name: Some("pi-mono".to_string()),
identity_cwd: PathBuf::from("/home/can/Projects/herdr"),
tabs: vec![TabSnapshot {
@@ -618,6 +630,7 @@ mod tests {
let restored: SessionSnapshot = serde_json::from_str(&json).unwrap();
assert_eq!(restored.workspaces.len(), 1);
+ assert_eq!(restored.workspaces[0].id.as_deref(), Some("wproj"));
assert_eq!(
restored.workspaces[0].custom_name.as_deref(),
Some("pi-mono")
diff --git a/src/workspace.rs b/src/workspace.rs
index 4090593c..18825cb9 100644
--- a/src/workspace.rs
+++ b/src/workspace.rs
@@ -1,8 +1,9 @@
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
-use std::sync::atomic::AtomicBool;
+use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
+use std::time::{SystemTime, UNIX_EPOCH};
use ratatui::layout::Direction;
use tokio::sync::{mpsc, Notify};
@@ -13,6 +14,17 @@ use crate::events::AppEvent;
use crate::layout::{PaneId, TileLayout};
use crate::pane::{PaneRuntime, PaneState};
+static NEXT_WORKSPACE_ID: AtomicU64 = AtomicU64::new(1);
+
+pub(crate) fn generate_workspace_id() -> String {
+ let micros = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|duration| duration.as_micros())
+ .unwrap_or(0);
+ let counter = NEXT_WORKSPACE_ID.fetch_add(1, Ordering::Relaxed);
+ format!("w{micros:x}{counter:x}")
+}
+
pub struct Tab {
pub custom_name: Option<String>,
pub number: usize,
@@ -228,6 +240,8 @@ impl Tab {
/// A named workspace containing tabs.
pub struct Workspace {
+ /// Stable public workspace identity, independent of display order.
+ pub id: String,
/// User-provided override. If set, auto-derived identity stops updating.
pub custom_name: Option<String>,
/// Fallback workspace identity source for tests, old snapshots, or missing runtimes.
@@ -280,6 +294,7 @@ impl Workspace {
public_pane_numbers.insert(tab.root_pane, 1);
info!(root_pane = tab.root_pane.raw(), "workspace created");
Ok(Self {
+ id: generate_workspace_id(),
custom_name: None,
identity_cwd: initial_cwd,
cached_git_ahead_behind: None,
@@ -712,6 +727,7 @@ impl Workspace {
let mut public_pane_numbers = HashMap::new();
public_pane_numbers.insert(tab.root_pane, 1);
Self {
+ id: generate_workspace_id(),
custom_name: Some(name.to_string()),
identity_cwd,
cached_git_ahead_behind: None,