1use std::collections::BTreeMap;
2use std::ops::Bound::{Excluded, Unbounded};
3use std::path::PathBuf;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use rmux_proto::{RmuxError, SessionName, SplitDirection, TerminalSize};
7
8use crate::{AlertFlags, Pane, PaneGeometry, PaneId, SessionId, Window, WindowId};
9
10#[path = "session/accessors.rs"]
11mod accessors;
12#[path = "session/layout_cycle.rs"]
13mod layout_cycle;
14#[path = "session/pane_transfer.rs"]
15mod pane_transfer;
16#[path = "session/pane_transfer_cross.rs"]
17mod pane_transfer_cross;
18#[path = "session/pane_transfer_shared.rs"]
19mod pane_transfer_shared;
20#[path = "session/recency.rs"]
21mod recency;
22#[path = "session/resize.rs"]
23mod resize;
24#[path = "session/store.rs"]
25mod store;
26#[path = "session/target_error.rs"]
27mod target_error;
28#[path = "session/types.rs"]
29mod types;
30#[path = "session/window_ops.rs"]
31mod window_ops;
32
33pub use recency::SessionRecency;
34pub use store::SessionStore;
35use target_error::{invalid_pane_target, invalid_window_target};
36pub(crate) use types::WindowIdAllocator;
37pub use types::{
38 BreakPaneOptions, KillPaneOutcome, PaneJoinOptions, PaneSwapOptions, SessionPaneTarget,
39};
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct Session {
44 id: SessionId,
45 name: SessionName,
46 group_name: Option<SessionName>,
47 terminal_size: TerminalSize,
48 windows: BTreeMap<u32, Window>,
49 winlink_alert_flags: BTreeMap<u32, AlertFlags>,
50 active_window: u32,
51 last_window: Option<u32>,
52 group_initial_window_id: Option<WindowId>,
54 next_pane_id: u32,
55 next_window_id: WindowIdAllocator,
56 created_at: i64,
57 activity_at: i64,
58 last_attached_at: Option<i64>,
59 recency: SessionRecency,
62 cwd: Option<PathBuf>,
63}
64
65impl Session {
66 #[must_use]
68 pub fn new(name: SessionName, size: TerminalSize) -> Self {
69 Self::new_with_initial_window(name, size, 0, PaneId::new(0), WindowId::new(0))
70 }
71
72 #[must_use]
74 pub(crate) fn new_with_initial_window(
75 name: SessionName,
76 size: TerminalSize,
77 window_index: u32,
78 pane_id: PaneId,
79 window_id: WindowId,
80 ) -> Self {
81 let now = current_unix_timestamp();
82 Self {
83 id: SessionId::new(0),
84 name,
85 group_name: None,
86 terminal_size: size,
87 windows: BTreeMap::from([(
88 window_index,
89 Window::new_with_initial_pane(size, pane_id, window_id),
90 )]),
91 winlink_alert_flags: BTreeMap::from([(window_index, AlertFlags::empty())]),
92 active_window: window_index,
93 last_window: None,
94 group_initial_window_id: None,
95 next_pane_id: pane_id.as_u32().saturating_add(1),
96 next_window_id: WindowIdAllocator::new(window_id.as_u32().saturating_add(1)),
97 created_at: now,
98 activity_at: now,
99 last_attached_at: None,
100 recency: SessionRecency::next(),
101 cwd: None,
102 }
103 }
104
105 pub fn split_active_pane(&mut self) -> Result<u32, RmuxError> {
107 self.split_active_pane_with_direction(SplitDirection::Vertical)
108 }
109
110 pub fn split_active_pane_with_direction(
112 &mut self,
113 direction: SplitDirection,
114 ) -> Result<u32, RmuxError> {
115 self.split_pane_with_direction(self.active_pane_index(), direction)
116 }
117
118 pub fn split_pane(&mut self, pane_index: u32) -> Result<u32, RmuxError> {
120 self.split_pane_with_direction(pane_index, SplitDirection::Vertical)
121 }
122
123 pub fn split_pane_with_direction(
125 &mut self,
126 pane_index: u32,
127 direction: SplitDirection,
128 ) -> Result<u32, RmuxError> {
129 self.split_pane_in_window_with_direction(self.active_window, pane_index, direction)
130 }
131
132 pub fn split_pane_in_window(
134 &mut self,
135 window_index: u32,
136 pane_index: u32,
137 ) -> Result<u32, RmuxError> {
138 self.split_pane_in_window_with_direction(window_index, pane_index, SplitDirection::Vertical)
139 }
140
141 pub fn split_pane_in_window_with_direction(
143 &mut self,
144 window_index: u32,
145 pane_index: u32,
146 direction: SplitDirection,
147 ) -> Result<u32, RmuxError> {
148 self.split_pane_in_window_with_direction_before(window_index, pane_index, direction, false)
149 }
150
151 pub fn split_pane_in_window_with_direction_before(
155 &mut self,
156 window_index: u32,
157 pane_index: u32,
158 direction: SplitDirection,
159 before: bool,
160 ) -> Result<u32, RmuxError> {
161 let pane_id = self.allocate_pane_id();
162 self.split_pane_in_window_with_id_and_direction_before(
163 window_index,
164 pane_index,
165 pane_id,
166 direction,
167 before,
168 )
169 }
170
171 pub fn split_pane_in_window_with_id_and_direction(
177 &mut self,
178 window_index: u32,
179 pane_index: u32,
180 pane_id: PaneId,
181 direction: SplitDirection,
182 ) -> Result<u32, RmuxError> {
183 self.split_pane_in_window_with_id_and_direction_before(
184 window_index,
185 pane_index,
186 pane_id,
187 direction,
188 false,
189 )
190 }
191
192 pub fn split_pane_in_window_with_id_and_direction_before(
195 &mut self,
196 window_index: u32,
197 pane_index: u32,
198 pane_id: PaneId,
199 direction: SplitDirection,
200 before: bool,
201 ) -> Result<u32, RmuxError> {
202 self.split_pane_in_window_with_id_and_direction_before_detached(
203 window_index,
204 pane_index,
205 pane_id,
206 direction,
207 before,
208 false,
209 )
210 }
211
212 pub fn split_pane_in_window_with_id_and_direction_before_detached(
216 &mut self,
217 window_index: u32,
218 pane_index: u32,
219 pane_id: PaneId,
220 direction: SplitDirection,
221 before: bool,
222 detached: bool,
223 ) -> Result<u32, RmuxError> {
224 let window = self
225 .window_at(window_index)
226 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
227 let position = window.pane_position(pane_index).ok_or_else(|| {
228 invalid_pane_target(
229 &self.name,
230 window_index,
231 pane_index,
232 "pane index does not exist in session",
233 )
234 })?;
235 if !window.can_split_pane(pane_index, direction) {
236 return Err(RmuxError::Message("no space for new pane".to_owned()));
237 }
238 Ok(self
239 .window_at_mut(window_index)
240 .expect("addressed session window must exist")
241 .split_at_position_with_id_and_direction_detached(
242 position, pane_id, direction, before, detached,
243 ))
244 }
245
246 pub fn split_pane_full_size_in_window_with_id_and_direction_before(
248 &mut self,
249 window_index: u32,
250 pane_index: u32,
251 pane_id: PaneId,
252 direction: SplitDirection,
253 before: bool,
254 ) -> Result<u32, RmuxError> {
255 self.split_pane_full_size_in_window_with_id_and_direction_before_detached(
256 window_index,
257 pane_index,
258 pane_id,
259 direction,
260 before,
261 false,
262 )
263 }
264
265 pub fn split_pane_full_size_in_window_with_id_and_direction_before_detached(
268 &mut self,
269 window_index: u32,
270 pane_index: u32,
271 pane_id: PaneId,
272 direction: SplitDirection,
273 before: bool,
274 detached: bool,
275 ) -> Result<u32, RmuxError> {
276 let window = self
277 .window_at(window_index)
278 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
279 if window.pane(pane_index).is_none() {
280 return Err(invalid_pane_target(
281 &self.name,
282 window_index,
283 pane_index,
284 "pane index does not exist in session",
285 ));
286 }
287
288 let window = self
289 .window_at_mut(window_index)
290 .expect("addressed session window must exist");
291 let previous_active_pane_id = window.active_pane().map(Pane::id);
292 let previous_last_pane_id = window
293 .last_pane_index()
294 .and_then(|pane_index| window.pane(pane_index))
295 .map(Pane::id);
296 let new_index = window
297 .panes()
298 .iter()
299 .map(Pane::index)
300 .max()
301 .unwrap_or(0)
302 .saturating_add(1);
303 window.insert_pane_full_size(
304 Pane::new_with_id(pane_id, new_index, PaneGeometry::new(0, 0, 0, 0)),
305 direction,
306 before,
307 )?;
308 window.commit_split_renumber(
309 pane_id,
310 previous_active_pane_id,
311 previous_last_pane_id,
312 detached,
313 );
314 window
315 .panes()
316 .iter()
317 .find(|pane| pane.id() == pane_id)
318 .map(Pane::index)
319 .ok_or_else(|| {
320 RmuxError::Server(format!(
321 "pane id {} disappeared after full-size split",
322 pane_id.as_u32()
323 ))
324 })
325 }
326
327 pub fn kill_pane(&mut self, pane_index: u32) -> Result<KillPaneOutcome, RmuxError> {
329 self.kill_pane_in_window(self.active_window, pane_index)
330 }
331
332 pub fn kill_pane_in_window(
334 &mut self,
335 window_index: u32,
336 pane_index: u32,
337 ) -> Result<KillPaneOutcome, RmuxError> {
338 let window = self
339 .window_at(window_index)
340 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
341 let pane_id = window.pane_id(pane_index).ok_or_else(|| {
342 invalid_pane_target(
343 &self.name,
344 window_index,
345 pane_index,
346 "pane index does not exist in session",
347 )
348 })?;
349
350 if window.pane_count() == 1 {
351 let removed_window = self.remove_window(window_index)?;
352 let removed_pane_ids = removed_window.panes().iter().map(Pane::id).collect();
353 return Ok(KillPaneOutcome::new(removed_pane_ids, true));
354 }
355
356 let removed_pane = self
357 .window_at_mut(window_index)
358 .expect("addressed session window must exist")
359 .remove_pane(pane_index)
360 .expect("prevalidated pane removal must succeed");
361 debug_assert_eq!(removed_pane.id(), pane_id);
362
363 Ok(KillPaneOutcome::new(vec![removed_pane.id()], false))
364 }
365
366 pub fn kill_other_panes_in_window(
368 &mut self,
369 window_index: u32,
370 pane_index: u32,
371 ) -> Result<KillPaneOutcome, RmuxError> {
372 let window = self
373 .window_at(window_index)
374 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
375 if window.pane(pane_index).is_none() {
376 return Err(invalid_pane_target(
377 &self.name,
378 window_index,
379 pane_index,
380 "pane index does not exist in session",
381 ));
382 }
383
384 let removed_pane_ids = self
385 .window_at_mut(window_index)
386 .expect("addressed session window must exist")
387 .remove_other_panes(pane_index)
388 .expect("prevalidated pane removal must succeed");
389
390 Ok(KillPaneOutcome::new(removed_pane_ids, false))
391 }
392
393 pub fn select_pane(&mut self, pane_index: u32) -> Result<(), RmuxError> {
395 self.select_pane_in_window(self.active_window, pane_index)
396 }
397
398 pub fn select_pane_in_window(
400 &mut self,
401 window_index: u32,
402 pane_index: u32,
403 ) -> Result<(), RmuxError> {
404 self.select_pane_in_window_with_zoom(window_index, pane_index, false)
405 }
406
407 pub fn select_pane_in_window_with_zoom(
409 &mut self,
410 window_index: u32,
411 pane_index: u32,
412 preserve_zoom: bool,
413 ) -> Result<(), RmuxError> {
414 if self.window_at(window_index).is_none() {
415 return Err(invalid_window_target(&self.name, window_index));
416 }
417
418 if self
419 .window_at(window_index)
420 .expect("addressed session window must exist")
421 .pane(pane_index)
422 .is_none()
423 {
424 return Err(invalid_pane_target(
425 &self.name,
426 window_index,
427 pane_index,
428 "pane index does not exist in session",
429 ));
430 }
431
432 let window = self
433 .window_at_mut(window_index)
434 .expect("addressed session window must exist");
435 if preserve_zoom {
436 let _ = window.push_zoom(true);
437 }
438 let selected = window.select_pane(pane_index);
439 if preserve_zoom {
440 window.pop_zoom();
441 }
442 debug_assert!(selected, "validated pane target must be selectable");
443 Ok(())
444 }
445
446 pub fn select_adjacent_pane_in_window(
448 &mut self,
449 window_index: u32,
450 pane_index: u32,
451 direction: rmux_proto::SelectPaneDirection,
452 ) -> Result<u32, RmuxError> {
453 self.select_adjacent_pane_in_window_with_zoom(window_index, pane_index, direction, false)
454 }
455
456 pub fn select_adjacent_pane_in_window_with_zoom(
458 &mut self,
459 window_index: u32,
460 pane_index: u32,
461 direction: rmux_proto::SelectPaneDirection,
462 preserve_zoom: bool,
463 ) -> Result<u32, RmuxError> {
464 if self.window_at(window_index).is_none() {
465 return Err(invalid_window_target(&self.name, window_index));
466 }
467
468 if self
469 .window_at(window_index)
470 .expect("addressed session window must exist")
471 .pane(pane_index)
472 .is_none()
473 {
474 return Err(invalid_pane_target(
475 &self.name,
476 window_index,
477 pane_index,
478 "pane index does not exist in session",
479 ));
480 }
481
482 let window = self
483 .window_at_mut(window_index)
484 .expect("addressed session window must exist");
485 let was_zoomed = window.is_zoomed();
486 let original_active_pane = window.active_pane_index();
487 if was_zoomed {
488 let _ = window.push_zoom(false);
489 }
490 let selected = window
491 .select_adjacent_pane(pane_index, direction)
492 .expect("validated adjacent pane anchor must be selectable");
493 let moved = selected != original_active_pane;
494 if was_zoomed && (preserve_zoom || !moved) {
495 let _ = window.toggle_zoom(window.active_pane_index());
496 }
497 Ok(selected)
498 }
499
500 pub fn resize_terminal(&mut self, size: TerminalSize) {
506 self.terminal_size = size;
507 for window in self.windows.values_mut() {
508 window.set_size(size);
509 }
510 }
511
512 pub fn set_terminal_size(&mut self, size: TerminalSize) {
514 self.terminal_size = size;
515 }
516
517 pub fn resize_active_window_geometry(
523 &mut self,
524 terminal_size: TerminalSize,
525 content_size: TerminalSize,
526 ) {
527 self.terminal_size = terminal_size;
528 self.window_mut().set_size(content_size);
529 }
530
531 fn resolve_window_target_mut(&mut self, window_index: u32) -> Result<&mut Window, RmuxError> {
532 if !self.windows.contains_key(&window_index) {
533 return Err(invalid_window_target(&self.name, window_index));
534 }
535
536 Ok(self
537 .window_at_mut(window_index)
538 .expect("addressed session window must exist"))
539 }
540
541 pub(crate) fn lowest_available_window_index_at_or_above(
542 &self,
543 minimum_index: u32,
544 ) -> Result<u32, RmuxError> {
545 let mut next_index = minimum_index;
546
547 for window_index in self.windows.keys().copied() {
548 if window_index < next_index {
549 continue;
550 }
551 if window_index > next_index {
552 break;
553 }
554
555 if window_index == next_index {
556 next_index = next_index.checked_add(1).ok_or_else(|| {
557 RmuxError::Server(format!(
558 "window index space exhausted for session {}",
559 self.name
560 ))
561 })?;
562 }
563 }
564
565 Ok(next_index)
566 }
567
568 fn next_active_window_after_removal(&self, removed_index: u32) -> u32 {
569 if let Some(last_window) = self.last_window {
570 if last_window != removed_index && self.windows.contains_key(&last_window) {
571 return last_window;
572 }
573 }
574
575 if let Some(group_initial_window_id) = self.group_initial_window_id {
576 if let Some((window_index, _)) = self.windows.iter().find(|(window_index, window)| {
577 **window_index != removed_index && window.id() == group_initial_window_id
578 }) {
579 return *window_index;
580 }
581 }
582
583 cyclic_previous_window_index(&self.windows, removed_index)
584 .expect("a non-empty session must have a replacement window")
585 }
586
587 fn allocate_pane_id(&mut self) -> PaneId {
588 let mut next_pane_id = self.next_pane_id;
589
590 loop {
591 let pane_id = PaneId::new(next_pane_id);
592 if !self.contains_pane_id(pane_id) {
593 self.next_pane_id = next_pane_id.saturating_add(1);
594 return pane_id;
595 }
596
597 assert_ne!(next_pane_id, u32::MAX, "pane id space exhausted");
598 next_pane_id += 1;
599 }
600 }
601
602 fn contains_pane_id(&self, pane_id: PaneId) -> bool {
603 self.windows
604 .values()
605 .flat_map(Window::panes)
606 .any(|pane| pane.id() == pane_id)
607 }
608
609 pub fn window_index_for_pane_id(&self, pane_id: PaneId) -> Option<u32> {
611 self.windows.iter().find_map(|(window_index, window)| {
612 window
613 .panes()
614 .iter()
615 .any(|pane| pane.id() == pane_id)
616 .then_some(*window_index)
617 })
618 }
619
620 fn allocate_window_id(&self) -> WindowId {
621 self.next_window_id.allocate()
622 }
623}
624
625fn synchronized_active_window(
626 windows: &BTreeMap<u32, Window>,
627 previous_active: u32,
628 previous_last: Option<u32>,
629) -> u32 {
630 if windows.contains_key(&previous_active) {
631 return previous_active;
632 }
633
634 if let Some(last_window) = previous_last {
635 if last_window != previous_active && windows.contains_key(&last_window) {
636 return last_window;
637 }
638 }
639
640 cyclic_previous_window_index(windows, previous_active)
641 .expect("group synchronization requires at least one window")
642}
643
644fn cyclic_previous_window_index(
645 windows: &BTreeMap<u32, Window>,
646 removed_index: u32,
647) -> Option<u32> {
648 windows
649 .range(..removed_index)
650 .next_back()
651 .or_else(|| {
652 windows
653 .range((Excluded(removed_index), Unbounded))
654 .next_back()
655 })
656 .map(|(window_index, _)| *window_index)
657}
658
659fn current_unix_timestamp() -> i64 {
660 SystemTime::now()
661 .duration_since(UNIX_EPOCH)
662 .ok()
663 .and_then(|duration| i64::try_from(duration.as_secs()).ok())
664 .unwrap_or_default()
665}
666
667#[cfg(test)]
668mod tests;
669
670#[cfg(test)]
671#[path = "session/zoom_tests.rs"]
672mod zoom_tests;
673
674#[cfg(test)]
675#[path = "session/layout_tests.rs"]
676mod layout_tests;