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, 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 insert_existing_session(&mut self, session: Session) -> Result<(), RmuxError> {
442 let mut session = session;
443 let session_name = session.name().clone();
444 if self.sessions.contains_key(&session_name) {
445 return Err(RmuxError::DuplicateSession(session_name.to_string()));
446 }
447
448 if self
449 .sessions
450 .values()
451 .any(|existing| existing.id() == session.id())
452 {
453 session.set_id(self.allocate_session_id());
454 }
455 session.rebind_window_id_allocator(self.next_window_id.clone());
456 self.next_session_id = self
457 .next_session_id
458 .max(session.id().as_u32().saturating_add(1));
459 self.bump_next_pane_id_from_session(&session);
460 if let Some(group_name) = session.group_name().cloned() {
461 match self.group_runtime_owners.get(&group_name) {
462 Some(owner) if self.sessions.contains_key(owner) || owner == &session_name => {}
463 _ => {
464 self.group_runtime_owners
465 .insert(group_name, session_name.clone());
466 }
467 }
468 }
469
470 let replaced = self.sessions.insert(session_name, session);
471 debug_assert!(replaced.is_none());
472 Ok(())
473 }
474
475 #[must_use]
477 pub fn session_mut(&mut self, session_name: &SessionName) -> Option<&mut Session> {
478 self.sessions.get_mut(session_name)
479 }
480
481 #[must_use]
483 pub const fn next_session_id(&self) -> SessionId {
484 SessionId::new(self.next_session_id)
485 }
486
487 #[must_use]
489 pub const fn next_pane_id(&self) -> crate::PaneId {
490 crate::PaneId::new(self.next_pane_id)
491 }
492
493 pub fn rename_session(
495 &mut self,
496 session_name: &SessionName,
497 new_name: SessionName,
498 ) -> Result<(), RmuxError> {
499 if !self.sessions.contains_key(session_name) {
500 return Err(RmuxError::SessionNotFound(session_name.to_string()));
501 }
502 if self.sessions.contains_key(&new_name) {
503 return Err(RmuxError::DuplicateSession(new_name.to_string()));
504 }
505
506 let mut sessions = std::mem::take(&mut self.sessions);
507 let mut session = sessions
508 .remove(session_name)
509 .expect("prevalidated session must exist");
510 let previous_group_name = session.group_name().cloned();
511 session.rename(new_name.clone());
512 if let Some(group_name) = previous_group_name {
513 if self.group_runtime_owners.get(&group_name) == Some(session_name) {
514 self.group_runtime_owners
515 .insert(group_name, new_name.clone());
516 }
517 }
518 let replaced = sessions.insert(new_name, session);
519 debug_assert!(replaced.is_none());
520 self.sessions = sessions;
521 Ok(())
522 }
523
524 fn allocate_session_id(&mut self) -> SessionId {
525 let mut next_session_id = self.next_session_id;
526
527 loop {
528 let session_id = SessionId::new(next_session_id);
529 if self.session_by_id(session_id).is_none() {
530 self.next_session_id = next_session_id.saturating_add(1);
531 return session_id;
532 }
533
534 assert_ne!(next_session_id, u32::MAX, "session id space exhausted");
535 next_session_id += 1;
536 }
537 }
538
539 pub fn allocate_pane_id(&mut self) -> crate::PaneId {
541 let pane_id = crate::PaneId::new(self.next_pane_id);
542 self.next_pane_id = self.next_pane_id.saturating_add(1);
543 pane_id
544 }
545
546 fn allocate_window_id(&mut self) -> crate::WindowId {
547 self.next_window_id.allocate()
548 }
549
550 fn bump_next_pane_id_from_session(&mut self, session: &Session) {
551 let next_after_session = session
552 .windows()
553 .values()
554 .flat_map(|window| window.panes().iter())
555 .map(|pane| pane.id().as_u32().saturating_add(1))
556 .max()
557 .unwrap_or(self.next_pane_id);
558 self.next_pane_id = self.next_pane_id.max(next_after_session);
559 }
560
561 fn repair_group_runtime_owner(&mut self, group_name: Option<SessionName>) {
562 let Some(group_name) = group_name else {
563 return;
564 };
565 let mut sessions = self.sessions_in_group(&group_name);
566 if sessions.is_empty() {
567 self.group_runtime_owners.remove(&group_name);
568 return;
569 }
570 let owner = self
571 .group_runtime_owners
572 .get(&group_name)
573 .cloned()
574 .filter(|owner| sessions.contains(owner))
575 .unwrap_or_else(|| {
576 sessions
577 .drain(..)
578 .next()
579 .expect("non-empty grouped session list")
580 });
581 self.group_runtime_owners.insert(group_name, owner);
582 }
583}