1use std::collections::HashMap;
2
3use rmux_proto::{RmuxError, SessionId, SessionName, TerminalSize};
4
5use super::{Session, WindowIdAllocator};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct GroupedSessionCreation {
10 pub session_name: SessionName,
12 pub group_name: SessionName,
14 pub template_session: Option<SessionName>,
16 pub runtime_owner: SessionName,
18}
19
20#[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 #[must_use]
33 pub fn new() -> Self {
34 Self::default()
35 }
36
37 #[must_use]
39 pub fn len(&self) -> usize {
40 self.sessions.len()
41 }
42
43 #[must_use]
45 pub fn is_empty(&self) -> bool {
46 self.sessions.is_empty()
47 }
48
49 #[must_use]
51 pub fn contains_session(&self, session_name: &SessionName) -> bool {
52 self.sessions.contains_key(session_name)
53 }
54
55 #[must_use]
57 pub fn session(&self, session_name: &SessionName) -> Option<&Session> {
58 self.sessions.get(session_name)
59 }
60
61 #[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 pub fn iter(&self) -> impl Iterator<Item = (&SessionName, &Session)> {
72 self.sessions.iter()
73 }
74
75 #[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 #[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 #[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 #[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 #[must_use]
115 pub fn session_group_size(&self, session_name: &SessionName) -> usize {
116 self.session_group_members(session_name).len()
117 }
118
119 #[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 #[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 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 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 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 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 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 #[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 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 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 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 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 #[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 #[must_use]
547 pub const fn next_session_id(&self) -> SessionId {
548 SessionId::new(self.next_session_id)
549 }
550
551 #[must_use]
553 pub const fn next_pane_id(&self) -> crate::PaneId {
554 crate::PaneId::new(self.next_pane_id)
555 }
556
557 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 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}