diff --git a/crates/spaceshd/src/main.rs b/crates/spaceshd/src/main.rs index e12c513..2715dc4 100644 --- a/crates/spaceshd/src/main.rs +++ b/crates/spaceshd/src/main.rs @@ -1,4 +1,5 @@ mod lifecycle; +mod registry; mod surface; fn main() { diff --git a/crates/spaceshd/src/registry.rs b/crates/spaceshd/src/registry.rs new file mode 100644 index 0000000..872696b --- /dev/null +++ b/crates/spaceshd/src/registry.rs @@ -0,0 +1,105 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::atomic::{AtomicU64, Ordering}; +use spacesh_proto::{SurfaceId, WorkspaceId}; +use crate::surface::SurfaceHandle; + +#[derive(Clone)] +pub struct WorkspaceMeta { + pub id: WorkspaceId, + pub path: PathBuf, +} + +/// Single-threaded owner of all live surfaces and workspaces. +/// Lives inside the server task; not shared across threads. +#[derive(Default)] +pub struct Registry { + counter: AtomicU64, + workspaces: HashMap, + /// path → workspace, so `open` is idempotent. + by_path: HashMap, + surfaces: HashMap, +} + +impl Registry { + pub fn new() -> Self { + Self::default() + } + + fn next_id(&self, prefix: &str) -> String { + let n = self.counter.fetch_add(1, Ordering::Relaxed); + format!("{prefix}_{n:x}") + } + + /// Idempotent: opening the same canonicalized path returns the existing workspace. + pub fn open_workspace(&mut self, path: PathBuf) -> WorkspaceMeta { + let canonical = path.canonicalize().unwrap_or(path); + if let Some(id) = self.by_path.get(&canonical) { + return self.workspaces[id].clone(); + } + let id = WorkspaceId(self.next_id("w")); + let meta = WorkspaceMeta { id: id.clone(), path: canonical.clone() }; + self.workspaces.insert(id.clone(), meta.clone()); + self.by_path.insert(canonical, id); + meta + } + + pub fn workspace(&self, id: &WorkspaceId) -> Option<&WorkspaceMeta> { + self.workspaces.get(id) + } + + pub fn new_surface_id(&self) -> SurfaceId { + SurfaceId(self.next_id("s")) + } + + pub fn insert_surface(&mut self, handle: SurfaceHandle) { + self.surfaces.insert(handle.id.clone(), handle); + } + + pub fn surface(&self, id: &SurfaceId) -> Option<&SurfaceHandle> { + self.surfaces.get(id) + } + + pub fn remove_surface(&mut self, id: &SurfaceId) -> Option { + self.surfaces.remove(id) + } + + /// Snapshot for the `status` command: (workspace, its surface ids). + pub fn status(&self) -> Vec<(WorkspaceMeta, Vec)> { + self.workspaces + .values() + .map(|w| { + let sids = self + .surfaces + .values() + .filter(|s| s.workspace_id == w.id) + .map(|s| s.id.clone()) + .collect(); + (w.clone(), sids) + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn open_is_idempotent_per_path() { + let mut reg = Registry::new(); + let dir = std::env::temp_dir(); + let a = reg.open_workspace(dir.clone()); + let b = reg.open_workspace(dir.clone()); + assert_eq!(a.id, b.id); + } + + #[test] + fn ids_are_unique_and_prefixed() { + let reg = Registry::new(); + let s1 = reg.new_surface_id(); + let s2 = reg.new_surface_id(); + assert!(s1.0.starts_with("s_")); + assert_ne!(s1, s2); + } +}