Skip to main content

tear_core/
registry.rs

1//! In-memory session/window/pane registry — the typed state machine
2//! every [`crate::InProcess`] op composes against.
3//!
4//! Held inside `Arc<RwLock<Registry>>` so the daemon (and tier-3
5//! mado embedders) can lock for reads/writes from any thread.
6//! parking_lot's RwLock for cheaper acquire + true reader-writer
7//! semantics — same pattern mado uses for its terminal lock (P30).
8
9use std::collections::BTreeMap;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use tear_types::{
13    LayoutNode, PaneId, PaneState, SessionId, SessionState, TearPane, TearSession, TearWindow,
14    WindowId, WindowState,
15};
16
17/// Monotonic counter used by the seed-based ID minting so IDs stay
18/// distinct even when minted in the same millisecond.
19fn next_counter() -> u64 {
20    use std::sync::atomic::{AtomicU64, Ordering};
21    static COUNTER: AtomicU64 = AtomicU64::new(1);
22    COUNTER.fetch_add(1, Ordering::Relaxed)
23}
24
25fn now_unix() -> u64 {
26    SystemTime::now()
27        .duration_since(UNIX_EPOCH)
28        .map(|d| d.as_secs())
29        .unwrap_or(0)
30}
31
32/// Mint a fresh [`SessionId`] from a name + the global counter.
33#[must_use]
34pub fn mint_session_id(name: &str) -> SessionId {
35    let seed = format!("session:{}:{}:{}", name, now_unix(), next_counter());
36    SessionId::from_seed(&seed)
37}
38#[must_use]
39pub fn mint_window_id(parent: SessionId, name: &str) -> WindowId {
40    let seed = format!("window:{}:{}:{}:{}", parent, name, now_unix(), next_counter());
41    WindowId::from_seed(&seed)
42}
43#[must_use]
44pub fn mint_pane_id(parent: WindowId, shell: &str) -> PaneId {
45    let seed = format!("pane:{}:{}:{}:{}", parent, shell, now_unix(), next_counter());
46    PaneId::from_seed(&seed)
47}
48
49/// The complete registry of sessions / windows / panes. Stored flat
50/// at the registry level — every entity is reachable in O(log N) via
51/// its id, and the [`TearSession`] struct holds child windows/panes
52/// by id for the typed-domain consumers.
53#[derive(Debug, Default)]
54pub struct Registry {
55    pub sessions: BTreeMap<SessionId, TearSession>,
56}
57
58impl Registry {
59    /// Create a new empty registry.
60    #[must_use]
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Sorted list of sessions, oldest-first.
66    pub fn sessions_in_order(&self) -> Vec<TearSession> {
67        let mut v: Vec<_> = self.sessions.values().cloned().collect();
68        v.sort_by_key(|s| s.created_at_unix);
69        v
70    }
71
72    /// Insert a freshly minted session with no windows yet — caller
73    /// is expected to add at least one window before returning.
74    pub fn create_session(&mut self, name: &str) -> SessionId {
75        let id = mint_session_id(name);
76        let s = TearSession {
77            id,
78            name: name.into(),
79            windows: BTreeMap::new(),
80            panes: BTreeMap::new(),
81            active_window: WindowId::NULL,
82            state: SessionState::Active,
83            created_at_unix: now_unix(),
84            description: String::new(),
85            source: tear_types::SessionSource::default(),
86            freio: tear_types::Freio::Released,
87        };
88        self.sessions.insert(id, s);
89        id
90    }
91
92    /// Insert a window into a session, with a single seed-pane.
93    ///
94    /// `args` / `cwd` / `env` are recorded on the seed [`TearPane`]
95    /// so the pane's typed record matches what was actually spawned.
96    /// They used to be hardcoded `vec![]` / `None` / `vec![]` here,
97    /// which is what made a captured-then-replayed session lose its
98    /// arguments: `praca`'s `from_live` read these fields faithfully,
99    /// but they had never been populated in the first place.
100    pub fn add_window(
101        &mut self,
102        session_id: SessionId,
103        name: &str,
104        shell: &str,
105        args: &[String],
106        cwd: Option<&str>,
107        env: &[(String, String)],
108        size_cells: (u16, u16),
109        // Provenance for the pane this window is born with. Threaded
110        // explicitly rather than read from `spawn_env`, which is an
111        // RwLock shared across every connection and already races: a
112        // raced cwd is a wrong directory, a raced provenance is a pane
113        // the brake misses.
114        yurai: tear_types::Yurai,
115    ) -> Option<(WindowId, PaneId)> {
116        let s = self.sessions.get_mut(&session_id)?;
117        let win_id = mint_window_id(session_id, name);
118        let pane_id = mint_pane_id(win_id, shell);
119        let pane = TearPane {
120            id: pane_id,
121            shell: shell.into(),
122            args: args.to_vec(),
123            cwd: cwd.map(Into::into),
124            env: env.to_vec(),
125            size_cells,
126            origin_cells: (0, 0),
127            state: PaneState::Running,
128            title: shell.into(),
129            input_policy: tear_types::InputPolicy::default(),
130            yurai,
131        };
132        let win = TearWindow {
133            id: win_id,
134            name: name.into(),
135            layout: LayoutNode::leaf(pane_id),
136            active_pane: pane_id,
137            size_cells,
138            state: WindowState::Active,
139        };
140        s.windows.insert(win_id, win);
141        s.panes.insert(pane_id, pane);
142        if s.active_window == WindowId::NULL {
143            s.active_window = win_id;
144        }
145        Some((win_id, pane_id))
146    }
147
148    /// Find the parent session + window for a given pane id. Returns
149    /// `None` if the pane doesn't exist in any session.
150    pub fn locate_pane(&self, pane: PaneId) -> Option<(SessionId, WindowId)> {
151        for s in self.sessions.values() {
152            if !s.panes.contains_key(&pane) {
153                continue;
154            }
155            for (wid, w) in &s.windows {
156                if w.layout.panes().contains(&pane) {
157                    return Some((s.id, *wid));
158                }
159            }
160        }
161        None
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn create_session_lands_in_map() {
171        let mut r = Registry::new();
172        let id = r.create_session("work");
173        assert!(r.sessions.contains_key(&id));
174        assert_eq!(r.sessions[&id].name, "work");
175    }
176
177    #[test]
178    fn add_window_creates_initial_pane_and_focuses_it() {
179        let mut r = Registry::new();
180        let sid = r.create_session("work");
181        let (wid, pid) = r
182            .add_window(sid, "main", "/bin/zsh", &[], None, &[], (80, 24), tear_types::Yurai::Unknown)
183            .unwrap();
184        let s = &r.sessions[&sid];
185        assert!(s.windows.contains_key(&wid));
186        assert!(s.panes.contains_key(&pid));
187        assert_eq!(s.active_window, wid);
188        assert_eq!(s.windows[&wid].active_pane, pid);
189        assert_eq!(s.windows[&wid].layout.pane_count(), 1);
190    }
191
192    /// The seed pane's typed record must carry what was actually
193    /// spawned. Before args/cwd/env were threaded, `add_window`
194    /// hardcoded `vec![]` / `None` / `vec![]` here, so `praca`'s
195    /// capture read empty fields off every pane and replay silently
196    /// dropped the arguments. This is the fail-once seal on that
197    /// class at its lowest layer: revert any of the three fields to
198    /// its old hardcode and this test goes red.
199    #[test]
200    fn add_window_records_the_spawn_args_cwd_and_env_on_the_seed_pane() {
201        let mut r = Registry::new();
202        let sid = r.create_session("work");
203        let args = vec!["-u".to_string(), "NONE".to_string()];
204        let env = vec![("EDITOR".to_string(), "nvim".to_string())];
205        let (_wid, pid) = r
206            .add_window(sid, "main", "/bin/nvim", &args, Some("/code"), &env, (80, 24), tear_types::Yurai::Unknown)
207            .unwrap();
208        let pane = &r.sessions[&sid].panes[&pid];
209        assert_eq!(pane.args, args, "seed pane must record its spawn args");
210        assert_eq!(pane.cwd.as_deref(), Some("/code"));
211        assert_eq!(pane.env, env);
212    }
213
214    #[test]
215    fn locate_pane_finds_its_parent() {
216        let mut r = Registry::new();
217        let sid = r.create_session("work");
218        let (wid, pid) = r
219            .add_window(sid, "main", "/bin/zsh", &[], None, &[], (80, 24), tear_types::Yurai::Unknown)
220            .unwrap();
221        assert_eq!(r.locate_pane(pid), Some((sid, wid)));
222    }
223}