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