diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-04 01:50:34 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-04 01:50:34 +0300 |
| commit | 831894facfc1339f819b42c3339668154121d24f (patch) | |
| tree | 3a49bad36a65341f6296b9f3d568317e15b6c21a /src | |
| parent | e0fb8ca6fa55acaa72902a15c5468cbe2787ddf7 (diff) | |
refactor: make workspace api ids stable
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/mod.rs | 39 | ||||
| -rw-r--r-- | src/persist.rs | 13 | ||||
| -rw-r--r-- | src/workspace.rs | 18 |
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, |
