Skip to main content

rmux_core/session/
store.rs

1use std::collections::HashMap;
2
3use rmux_proto::{RmuxError, SessionId, SessionName, TerminalSize};
4
5use super::{Session, WindowIdAllocator};
6
7/// Summary for a grouped session creation request.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct GroupedSessionCreation {
10    /// The newly-created session name.
11    pub session_name: SessionName,
12    /// The stable group name attached to the new session.
13    pub group_name: SessionName,
14    /// The session used as the template for shared window state, when one existed.
15    pub template_session: Option<SessionName>,
16    /// The runtime-owning session for the group after creation.
17    pub runtime_owner: SessionName,
18}
19
20/// In-memory storage for all sessions owned by the server.
21#[derive(Debug, Clone, Default)]
22pub struct SessionStore {
23    sessions: HashMap<SessionName, Session>,
24    next_session_id: u32,
25    next_pane_id: u32,
26    next_window_id: WindowIdAllocator,
27    group_runtime_owners: HashMap<SessionName, SessionName>,
28}
29
30impl SessionStore {
31    /// Creates an empty session store.
32    #[must_use]
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Returns the number of sessions currently present.
38    #[must_use]
39    pub fn len(&self) -> usize {
40        self.sessions.len()
41    }
42
43    /// Returns whether the store contains no sessions.
44    #[must_use]
45    pub fn is_empty(&self) -> bool {
46        self.sessions.is_empty()
47    }
48
49    /// Returns whether the addressed session exists.
50    #[must_use]
51    pub fn contains_session(&self, session_name: &SessionName) -> bool {
52        self.sessions.contains_key(session_name)
53    }
54
55    /// Returns an immutable reference to the named session.
56    #[must_use]
57    pub fn session(&self, session_name: &SessionName) -> Option<&Session> {
58        self.sessions.get(session_name)
59    }
60
61    /// Returns the session with the store-assigned `$N` identity.
62    #[must_use]
63    pub fn session_by_id(&self, session_id: impl Into<SessionId>) -> Option<&Session> {
64        let session_id = session_id.into();
65        self.sessions
66            .values()
67            .find(|session| session.id() == session_id)
68    }
69
70    /// Returns an iterator over all sessions in the store.
71    pub fn iter(&self) -> impl Iterator<Item = (&SessionName, &Session)> {
72        self.sessions.iter()
73    }
74
75    /// Returns the stable session group name for the addressed session.
76    #[must_use]
77    pub fn session_group_name(&self, session_name: &SessionName) -> Option<&SessionName> {
78        self.session(session_name).and_then(Session::group_name)
79    }
80
81    /// Returns the runtime-owning session for the addressed session.
82    #[must_use]
83    pub fn runtime_owner(&self, session_name: &SessionName) -> Option<SessionName> {
84        let session = self.session(session_name)?;
85        match session.group_name() {
86            Some(group_name) => self.group_runtime_owners.get(group_name).cloned(),
87            None => Some(session_name.clone()),
88        }
89    }
90
91    /// Returns the next runtime-owning session when the current runtime owner is removed.
92    #[must_use]
93    pub fn runtime_owner_transfer_target(&self, session_name: &SessionName) -> Option<SessionName> {
94        if self.runtime_owner(session_name).as_ref() != Some(session_name) {
95            return None;
96        }
97
98        let group_name = self.session_group_name(session_name)?;
99        self.sessions_in_group(group_name)
100            .into_iter()
101            .find(|candidate| candidate != session_name)
102    }
103
104    /// Returns the grouped peer sessions for the addressed session in stable name order.
105    #[must_use]
106    pub fn session_group_members(&self, session_name: &SessionName) -> Vec<SessionName> {
107        let Some(group_name) = self.session_group_name(session_name).cloned() else {
108            return vec![session_name.clone()];
109        };
110        self.sessions_in_group(&group_name)
111    }
112
113    /// Returns the number of sessions sharing the addressed session's group.
114    #[must_use]
115    pub fn session_group_size(&self, session_name: &SessionName) -> usize {
116        self.session_group_members(session_name).len()
117    }
118
119    /// Returns whether a named session group exists.
120    #[must_use]
121    pub fn contains_group(&self, group_name: &SessionName) -> bool {
122        self.group_runtime_owners.contains_key(group_name)
123            || self
124                .sessions
125                .values()
126                .any(|session| session.group_name() == Some(group_name))
127    }
128
129    /// Returns grouped sessions for the named group in stable name order.
130    #[must_use]
131    pub fn sessions_in_group(&self, group_name: &SessionName) -> Vec<SessionName> {
132        let mut sessions = self
133            .sessions
134            .iter()
135            .filter_map(|(session_name, session)| {
136                (session.group_name() == Some(group_name)).then_some(session_name.clone())
137            })
138            .collect::<Vec<_>>();
139        sessions.sort_by(|left, right| left.as_str().cmp(right.as_str()));
140        sessions
141    }
142
143    /// Creates a session or returns `DuplicateSession` when the name exists.
144    pub fn create_session(
145        &mut self,
146        session_name: SessionName,
147        size: TerminalSize,
148    ) -> Result<(), RmuxError> {
149        self.create_session_with_base_index(session_name, size, 0)
150    }
151
152    /// Creates a session using the requested first window index.
153    pub fn create_session_with_base_index(
154        &mut self,
155        session_name: SessionName,
156        size: TerminalSize,
157        base_index: u32,
158    ) -> Result<(), RmuxError> {
159        let session_id = self.allocate_session_id();
160        self.create_session_with_base_index_and_id(session_name, size, base_index, session_id)
161    }
162
163    /// Creates an auto-named tmux-compatible session using the next global
164    /// session id for both the `$id` and the visible session name.
165    pub fn create_auto_named_session_with_base_index(
166        &mut self,
167        size: TerminalSize,
168        base_index: u32,
169    ) -> Result<SessionName, RmuxError> {
170        let (session_name, session_id) = self.next_automatic_session_identity(None);
171        self.create_session_with_base_index_and_id(
172            session_name.clone(),
173            size,
174            base_index,
175            session_id,
176        )?;
177        Ok(session_name)
178    }
179
180    fn create_session_with_base_index_and_id(
181        &mut self,
182        session_name: SessionName,
183        size: TerminalSize,
184        base_index: u32,
185        session_id: SessionId,
186    ) -> Result<(), RmuxError> {
187        if self.sessions.contains_key(&session_name) {
188            return Err(RmuxError::DuplicateSession(session_name.to_string()));
189        }
190        if self.session_by_id(session_id).is_some() {
191            return Err(RmuxError::Server(format!(
192                "session id {} already exists",
193                session_id.as_u32()
194            )));
195        }
196
197        let pane_id = self.allocate_pane_id();
198        let window_id = self.allocate_window_id();
199        let mut session = Session::new_with_initial_window(
200            session_name.clone(),
201            size,
202            base_index,
203            pane_id,
204            window_id,
205        );
206        session.rebind_window_id_allocator(self.next_window_id.clone());
207        session.set_id(session_id);
208        self.next_session_id = self
209            .next_session_id
210            .max(session_id.as_u32().saturating_add(1));
211        self.sessions.insert(session_name, session);
212        Ok(())
213    }
214
215    /// Creates a session in an existing or newly-declared session group.
216    pub fn create_grouped_session_with_base_index(
217        &mut self,
218        session_name: SessionName,
219        size: TerminalSize,
220        base_index: u32,
221        group_target: SessionName,
222    ) -> Result<GroupedSessionCreation, RmuxError> {
223        let session_id = self.allocate_session_id();
224        self.create_grouped_session_with_base_index_and_id(
225            session_name,
226            size,
227            base_index,
228            group_target,
229            session_id,
230        )
231    }
232
233    /// Creates an auto-named grouped session using tmux's global session-id
234    /// suffix rule (`group-$id`).
235    pub fn create_auto_grouped_session_with_base_index(
236        &mut self,
237        size: TerminalSize,
238        base_index: u32,
239        group_target: SessionName,
240    ) -> Result<GroupedSessionCreation, RmuxError> {
241        let group_name = self
242            .sessions
243            .get(&group_target)
244            .and_then(Session::group_name)
245            .cloned()
246            .unwrap_or_else(|| group_target.clone());
247        let (session_name, session_id) = self.next_automatic_session_identity(Some(&group_name));
248        self.create_grouped_session_with_base_index_and_id(
249            session_name,
250            size,
251            base_index,
252            group_target,
253            session_id,
254        )
255    }
256
257    fn create_grouped_session_with_base_index_and_id(
258        &mut self,
259        session_name: SessionName,
260        size: TerminalSize,
261        base_index: u32,
262        group_target: SessionName,
263        session_id: SessionId,
264    ) -> Result<GroupedSessionCreation, RmuxError> {
265        if self.sessions.contains_key(&session_name) {
266            return Err(RmuxError::DuplicateSession(session_name.to_string()));
267        }
268        if self.session_by_id(session_id).is_some() {
269            return Err(RmuxError::Server(format!(
270                "session id {} already exists",
271                session_id.as_u32()
272            )));
273        }
274
275        enum GroupTemplate {
276            Existing {
277                group_name: SessionName,
278                template_session: SessionName,
279                runtime_owner: SessionName,
280            },
281            Standalone {
282                group_name: SessionName,
283            },
284        }
285
286        let template = if let Some(source_session) = self.sessions.get(&group_target) {
287            let group_name = source_session
288                .group_name()
289                .cloned()
290                .unwrap_or_else(|| group_target.clone());
291            let runtime_owner = self
292                .group_runtime_owners
293                .get(&group_name)
294                .cloned()
295                .unwrap_or_else(|| group_target.clone());
296            GroupTemplate::Existing {
297                group_name,
298                template_session: group_target.clone(),
299                runtime_owner,
300            }
301        } else if let Some(runtime_owner) = self.group_runtime_owners.get(&group_target).cloned() {
302            let template_session = if self.sessions.contains_key(&runtime_owner) {
303                runtime_owner.clone()
304            } else {
305                self.sessions_in_group(&group_target)
306                    .into_iter()
307                    .next()
308                    .ok_or_else(|| {
309                        RmuxError::Server(format!(
310                            "session group {group_target} has no template session"
311                        ))
312                    })?
313            };
314            GroupTemplate::Existing {
315                group_name: group_target.clone(),
316                template_session,
317                runtime_owner,
318            }
319        } else {
320            GroupTemplate::Standalone {
321                group_name: group_target.clone(),
322            }
323        };
324
325        match template {
326            GroupTemplate::Existing {
327                group_name,
328                template_session,
329                runtime_owner,
330            } => {
331                if self
332                    .sessions
333                    .get(&template_session)
334                    .and_then(Session::group_name)
335                    .is_none()
336                {
337                    let source = self
338                        .sessions
339                        .get_mut(&template_session)
340                        .expect("template session must exist");
341                    source.set_group_name(Some(group_name.clone()));
342                    self.group_runtime_owners
343                        .entry(group_name.clone())
344                        .or_insert(template_session.clone());
345                }
346
347                let grouped = self
348                    .sessions
349                    .get(&template_session)
350                    .expect("template session must exist")
351                    .clone_as_group_member(session_name.clone(), group_name.clone(), session_id);
352                let replaced = self.sessions.insert(session_name.clone(), grouped);
353                debug_assert!(replaced.is_none());
354                self.next_session_id = self
355                    .next_session_id
356                    .max(session_id.as_u32().saturating_add(1));
357                Ok(GroupedSessionCreation {
358                    session_name,
359                    group_name,
360                    template_session: Some(template_session),
361                    runtime_owner,
362                })
363            }
364            GroupTemplate::Standalone { group_name } => {
365                let pane_id = self.allocate_pane_id();
366                let window_id = self.allocate_window_id();
367                let mut session = Session::new_with_initial_window(
368                    session_name.clone(),
369                    size,
370                    base_index,
371                    pane_id,
372                    window_id,
373                );
374                session.rebind_window_id_allocator(self.next_window_id.clone());
375                session.set_id(session_id);
376                self.next_session_id = self
377                    .next_session_id
378                    .max(session_id.as_u32().saturating_add(1));
379                session.set_group_name(Some(group_name.clone()));
380                let replaced = self.sessions.insert(session_name.clone(), session);
381                debug_assert!(replaced.is_none());
382                self.group_runtime_owners
383                    .insert(group_name.clone(), session_name.clone());
384                Ok(GroupedSessionCreation {
385                    session_name: session_name.clone(),
386                    group_name,
387                    template_session: None,
388                    runtime_owner: session_name,
389                })
390            }
391        }
392    }
393
394    /// Returns the next available grouped-session name using tmux's `group-N` prefix shape.
395    #[must_use]
396    pub fn next_grouped_session_name(&self, group_name: &SessionName) -> SessionName {
397        for suffix in 1_u32.. {
398            let candidate = SessionName::new(format!("{group_name}-{suffix}"))
399                .expect("generated grouped session name must be valid");
400            if !self.contains_session(&candidate) {
401                return candidate;
402            }
403        }
404
405        unreachable!("u32 loop must eventually yield an unused grouped session name")
406    }
407
408    fn next_automatic_session_identity(
409        &self,
410        prefix: Option<&SessionName>,
411    ) -> (SessionName, SessionId) {
412        let mut session_id = self.next_session_id;
413
414        loop {
415            let candidate = match prefix {
416                Some(prefix) => SessionName::new(format!("{prefix}-{session_id}"))
417                    .expect("generated grouped session name must be valid"),
418                None => SessionName::new(session_id.to_string())
419                    .expect("generated default session name must be valid"),
420            };
421            if !self.contains_session(&candidate) {
422                return (candidate, SessionId::new(session_id));
423            }
424            session_id = session_id
425                .checked_add(1)
426                .expect("u32 loop must eventually yield an unused session id");
427        }
428    }
429
430    /// Removes a session or returns `SessionNotFound` when it is absent.
431    pub fn remove_session(&mut self, session_name: &SessionName) -> Result<Session, RmuxError> {
432        let removed = self
433            .sessions
434            .remove(session_name)
435            .ok_or_else(|| RmuxError::SessionNotFound(session_name.to_string()))?;
436        self.repair_group_runtime_owner(removed.group_name().cloned());
437        Ok(removed)
438    }
439
440    /// Temporarily extracts two distinct sessions for a paired mutation.
441    ///
442    /// Unlike [`Self::remove_session`], this leaves group runtime ownership
443    /// untouched and reinserts both sessions before returning the closure's
444    /// result. The closure cannot observe the store while the pair is absent.
445    pub fn with_extracted_session_pair<T>(
446        &mut self,
447        first_session_name: &SessionName,
448        second_session_name: &SessionName,
449        mutate: impl FnOnce(&mut Session, &mut Session) -> T,
450    ) -> Result<T, RmuxError> {
451        if first_session_name == second_session_name {
452            return Err(RmuxError::Server(
453                "paired session mutation requires distinct sessions".to_owned(),
454            ));
455        }
456        for session_name in [first_session_name, second_session_name] {
457            if !self.sessions.contains_key(session_name) {
458                return Err(RmuxError::SessionNotFound(session_name.to_string()));
459            }
460        }
461
462        let mut first_session = self
463            .sessions
464            .remove(first_session_name)
465            .expect("prevalidated first session must exist");
466        let mut second_session = self
467            .sessions
468            .remove(second_session_name)
469            .expect("prevalidated second session must exist");
470        let result = mutate(&mut first_session, &mut second_session);
471
472        debug_assert_eq!(first_session.name(), first_session_name);
473        debug_assert_eq!(second_session.name(), second_session_name);
474        let replaced_first = self
475            .sessions
476            .insert(first_session_name.clone(), first_session);
477        let replaced_second = self
478            .sessions
479            .insert(second_session_name.clone(), second_session);
480        debug_assert!(replaced_first.is_none());
481        debug_assert!(replaced_second.is_none());
482        Ok(result)
483    }
484
485    /// Inserts an existing session back into the store, preserving its state.
486    ///
487    /// Extracting a session and handing it back leaves everything untouched.
488    /// Only the two keys that have to stay unique across stored sessions — the
489    /// session id and the internal recency position — are re-issued, and only
490    /// when the incoming session actually collides with a stored one.
491    pub fn insert_existing_session(&mut self, session: Session) -> Result<(), RmuxError> {
492        let mut session = session;
493        let session_name = session.name().clone();
494        if self.sessions.contains_key(&session_name) {
495            return Err(RmuxError::DuplicateSession(session_name.to_string()));
496        }
497
498        if self
499            .sessions
500            .values()
501            .any(|existing| existing.id() == session.id())
502        {
503            session.set_id(self.allocate_session_id());
504        }
505        // A caller may hand back a clone of a session that is still stored, so
506        // the incoming token can already belong to a live session. Targetless
507        // readers rank by that token and disagree on how to break a tie, so a
508        // shared position would put them back on the creation id or the name
509        // this order exists to replace. Re-mint instead: a session entering the
510        // store alongside the one it was copied from is a separate lifetime,
511        // exactly like the grouped peer that `clone_as_group_member` produces.
512        if self
513            .sessions
514            .values()
515            .any(|existing| existing.recency() == session.recency())
516        {
517            session.renew_recency();
518        }
519        session.rebind_window_id_allocator(self.next_window_id.clone());
520        self.next_session_id = self
521            .next_session_id
522            .max(session.id().as_u32().saturating_add(1));
523        self.bump_next_pane_id_from_session(&session);
524        if let Some(group_name) = session.group_name().cloned() {
525            match self.group_runtime_owners.get(&group_name) {
526                Some(owner) if self.sessions.contains_key(owner) || owner == &session_name => {}
527                _ => {
528                    self.group_runtime_owners
529                        .insert(group_name, session_name.clone());
530                }
531            }
532        }
533
534        let replaced = self.sessions.insert(session_name, session);
535        debug_assert!(replaced.is_none());
536        Ok(())
537    }
538
539    /// Returns a mutable reference to the named session.
540    #[must_use]
541    pub fn session_mut(&mut self, session_name: &SessionName) -> Option<&mut Session> {
542        self.sessions.get_mut(session_name)
543    }
544
545    /// Returns the next session id that will be allocated.
546    #[must_use]
547    pub const fn next_session_id(&self) -> SessionId {
548        SessionId::new(self.next_session_id)
549    }
550
551    /// Returns the next globally visible pane id that will be allocated.
552    #[must_use]
553    pub const fn next_pane_id(&self) -> crate::PaneId {
554        crate::PaneId::new(self.next_pane_id)
555    }
556
557    /// Renames the addressed session by updating both the stored key and the session's internal name.
558    pub fn rename_session(
559        &mut self,
560        session_name: &SessionName,
561        new_name: SessionName,
562    ) -> Result<(), RmuxError> {
563        if !self.sessions.contains_key(session_name) {
564            return Err(RmuxError::SessionNotFound(session_name.to_string()));
565        }
566        if self.sessions.contains_key(&new_name) {
567            return Err(RmuxError::DuplicateSession(new_name.to_string()));
568        }
569
570        let mut sessions = std::mem::take(&mut self.sessions);
571        let mut session = sessions
572            .remove(session_name)
573            .expect("prevalidated session must exist");
574        let previous_group_name = session.group_name().cloned();
575        session.rename(new_name.clone());
576        if let Some(group_name) = previous_group_name {
577            if self.group_runtime_owners.get(&group_name) == Some(session_name) {
578                self.group_runtime_owners
579                    .insert(group_name, new_name.clone());
580            }
581        }
582        let replaced = sessions.insert(new_name, session);
583        debug_assert!(replaced.is_none());
584        self.sessions = sessions;
585        Ok(())
586    }
587
588    fn allocate_session_id(&mut self) -> SessionId {
589        let mut next_session_id = self.next_session_id;
590
591        loop {
592            let session_id = SessionId::new(next_session_id);
593            if self.session_by_id(session_id).is_none() {
594                self.next_session_id = next_session_id.saturating_add(1);
595                return session_id;
596            }
597
598            assert_ne!(next_session_id, u32::MAX, "session id space exhausted");
599            next_session_id += 1;
600        }
601    }
602
603    /// Allocates the next globally visible pane id for runtime-created panes.
604    pub fn allocate_pane_id(&mut self) -> crate::PaneId {
605        let pane_id = crate::PaneId::new(self.next_pane_id);
606        self.next_pane_id = self.next_pane_id.saturating_add(1);
607        pane_id
608    }
609
610    fn allocate_window_id(&mut self) -> crate::WindowId {
611        self.next_window_id.allocate()
612    }
613
614    fn bump_next_pane_id_from_session(&mut self, session: &Session) {
615        let next_after_session = session
616            .windows()
617            .values()
618            .flat_map(|window| window.panes().iter())
619            .map(|pane| pane.id().as_u32().saturating_add(1))
620            .max()
621            .unwrap_or(self.next_pane_id);
622        self.next_pane_id = self.next_pane_id.max(next_after_session);
623    }
624
625    fn repair_group_runtime_owner(&mut self, group_name: Option<SessionName>) {
626        let Some(group_name) = group_name else {
627            return;
628        };
629        let mut sessions = self.sessions_in_group(&group_name);
630        if sessions.is_empty() {
631            self.group_runtime_owners.remove(&group_name);
632            return;
633        }
634        let owner = self
635            .group_runtime_owners
636            .get(&group_name)
637            .cloned()
638            .filter(|owner| sessions.contains(owner))
639            .unwrap_or_else(|| {
640                sessions
641                    .drain(..)
642                    .next()
643                    .expect("non-empty grouped session list")
644            });
645        self.group_runtime_owners.insert(group_name, owner);
646    }
647}