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 let window = self
193 .window_at(window_index)
194 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
195 let position = window.pane_position(pane_index).ok_or_else(|| {
196 invalid_pane_target(
197 &self.name,
198 window_index,
199 pane_index,
200 "pane index does not exist in session",
201 )
202 })?;
203 if !window.can_split_pane(pane_index, direction) {
204 return Err(RmuxError::Message("no space for new pane".to_owned()));
205 }
206 Ok(self
207 .window_at_mut(window_index)
208 .expect("addressed session window must exist")
209 .split_at_position_with_id_and_direction(position, pane_id, direction, before))
210 }
211
212 pub fn split_pane_full_size_in_window_with_id_and_direction_before(
214 &mut self,
215 window_index: u32,
216 pane_index: u32,
217 pane_id: PaneId,
218 direction: SplitDirection,
219 before: bool,
220 ) -> Result<u32, RmuxError> {
221 let window = self
222 .window_at(window_index)
223 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
224 if window.pane(pane_index).is_none() {
225 return Err(invalid_pane_target(
226 &self.name,
227 window_index,
228 pane_index,
229 "pane index does not exist in session",
230 ));
231 }
232
233 let window = self
234 .window_at_mut(window_index)
235 .expect("addressed session window must exist");
236 let previous_active_pane_id = window.active_pane().map(Pane::id);
237 let new_index = window
238 .panes()
239 .iter()
240 .map(Pane::index)
241 .max()
242 .unwrap_or(0)
243 .saturating_add(1);
244 window.insert_pane_full_size(
245 Pane::new_with_id(pane_id, new_index, PaneGeometry::new(0, 0, 0, 0)),
246 direction,
247 before,
248 )?;
249 window.renumber_panes_by_position(pane_id, previous_active_pane_id);
250 window
251 .panes()
252 .iter()
253 .find(|pane| pane.id() == pane_id)
254 .map(Pane::index)
255 .ok_or_else(|| {
256 RmuxError::Server(format!(
257 "pane id {} disappeared after full-size split",
258 pane_id.as_u32()
259 ))
260 })
261 }
262
263 pub fn kill_pane(&mut self, pane_index: u32) -> Result<KillPaneOutcome, RmuxError> {
265 self.kill_pane_in_window(self.active_window, pane_index)
266 }
267
268 pub fn kill_pane_in_window(
270 &mut self,
271 window_index: u32,
272 pane_index: u32,
273 ) -> Result<KillPaneOutcome, RmuxError> {
274 let window = self
275 .window_at(window_index)
276 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
277 let pane_id = window.pane_id(pane_index).ok_or_else(|| {
278 invalid_pane_target(
279 &self.name,
280 window_index,
281 pane_index,
282 "pane index does not exist in session",
283 )
284 })?;
285
286 if window.pane_count() == 1 {
287 let removed_window = self.remove_window(window_index)?;
288 let removed_pane_ids = removed_window.panes().iter().map(Pane::id).collect();
289 return Ok(KillPaneOutcome::new(removed_pane_ids, true));
290 }
291
292 let removed_pane = self
293 .window_at_mut(window_index)
294 .expect("addressed session window must exist")
295 .remove_pane(pane_index)
296 .expect("prevalidated pane removal must succeed");
297 debug_assert_eq!(removed_pane.id(), pane_id);
298
299 Ok(KillPaneOutcome::new(vec![removed_pane.id()], false))
300 }
301
302 pub fn kill_other_panes_in_window(
304 &mut self,
305 window_index: u32,
306 pane_index: u32,
307 ) -> Result<KillPaneOutcome, RmuxError> {
308 let window = self
309 .window_at(window_index)
310 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
311 if window.pane(pane_index).is_none() {
312 return Err(invalid_pane_target(
313 &self.name,
314 window_index,
315 pane_index,
316 "pane index does not exist in session",
317 ));
318 }
319
320 let removed_pane_ids = self
321 .window_at_mut(window_index)
322 .expect("addressed session window must exist")
323 .remove_other_panes(pane_index)
324 .expect("prevalidated pane removal must succeed");
325
326 Ok(KillPaneOutcome::new(removed_pane_ids, false))
327 }
328
329 pub fn select_pane(&mut self, pane_index: u32) -> Result<(), RmuxError> {
331 self.select_pane_in_window(self.active_window, pane_index)
332 }
333
334 pub fn select_pane_in_window(
336 &mut self,
337 window_index: u32,
338 pane_index: u32,
339 ) -> Result<(), RmuxError> {
340 self.select_pane_in_window_with_zoom(window_index, pane_index, false)
341 }
342
343 pub fn select_pane_in_window_with_zoom(
345 &mut self,
346 window_index: u32,
347 pane_index: u32,
348 preserve_zoom: bool,
349 ) -> Result<(), RmuxError> {
350 if self.window_at(window_index).is_none() {
351 return Err(invalid_window_target(&self.name, window_index));
352 }
353
354 if self
355 .window_at(window_index)
356 .expect("addressed session window must exist")
357 .pane(pane_index)
358 .is_none()
359 {
360 return Err(invalid_pane_target(
361 &self.name,
362 window_index,
363 pane_index,
364 "pane index does not exist in session",
365 ));
366 }
367
368 let window = self
369 .window_at_mut(window_index)
370 .expect("addressed session window must exist");
371 if preserve_zoom {
372 let _ = window.push_zoom(true);
373 }
374 let selected = window.select_pane(pane_index);
375 if preserve_zoom {
376 window.pop_zoom();
377 }
378 debug_assert!(selected, "validated pane target must be selectable");
379 Ok(())
380 }
381
382 pub fn select_adjacent_pane_in_window(
384 &mut self,
385 window_index: u32,
386 pane_index: u32,
387 direction: rmux_proto::SelectPaneDirection,
388 ) -> Result<u32, RmuxError> {
389 self.select_adjacent_pane_in_window_with_zoom(window_index, pane_index, direction, false)
390 }
391
392 pub fn select_adjacent_pane_in_window_with_zoom(
394 &mut self,
395 window_index: u32,
396 pane_index: u32,
397 direction: rmux_proto::SelectPaneDirection,
398 preserve_zoom: bool,
399 ) -> Result<u32, RmuxError> {
400 if self.window_at(window_index).is_none() {
401 return Err(invalid_window_target(&self.name, window_index));
402 }
403
404 if self
405 .window_at(window_index)
406 .expect("addressed session window must exist")
407 .pane(pane_index)
408 .is_none()
409 {
410 return Err(invalid_pane_target(
411 &self.name,
412 window_index,
413 pane_index,
414 "pane index does not exist in session",
415 ));
416 }
417
418 let window = self
419 .window_at_mut(window_index)
420 .expect("addressed session window must exist");
421 let was_zoomed = window.is_zoomed();
422 let original_active_pane = window.active_pane_index();
423 if was_zoomed {
424 let _ = window.push_zoom(false);
425 }
426 let selected = window
427 .select_adjacent_pane(pane_index, direction)
428 .expect("validated adjacent pane anchor must be selectable");
429 let moved = selected != original_active_pane;
430 if was_zoomed && (preserve_zoom || !moved) {
431 let _ = window.toggle_zoom(window.active_pane_index());
432 }
433 Ok(selected)
434 }
435
436 pub fn resize_terminal(&mut self, size: TerminalSize) {
438 self.terminal_size = size;
439 for window in self.windows.values_mut() {
440 window.set_size(size);
441 }
442 }
443
444 fn resolve_window_target_mut(&mut self, window_index: u32) -> Result<&mut Window, RmuxError> {
445 if !self.windows.contains_key(&window_index) {
446 return Err(invalid_window_target(&self.name, window_index));
447 }
448
449 Ok(self
450 .window_at_mut(window_index)
451 .expect("addressed session window must exist"))
452 }
453
454 pub(crate) fn lowest_available_window_index_at_or_above(
455 &self,
456 minimum_index: u32,
457 ) -> Result<u32, RmuxError> {
458 let mut next_index = minimum_index;
459
460 for window_index in self.windows.keys().copied() {
461 if window_index < next_index {
462 continue;
463 }
464 if window_index > next_index {
465 break;
466 }
467
468 if window_index == next_index {
469 next_index = next_index.checked_add(1).ok_or_else(|| {
470 RmuxError::Server(format!(
471 "window index space exhausted for session {}",
472 self.name
473 ))
474 })?;
475 }
476 }
477
478 Ok(next_index)
479 }
480
481 fn next_active_window_after_removal(&self, removed_index: u32) -> u32 {
482 if let Some(last_window) = self.last_window {
483 if last_window != removed_index && self.windows.contains_key(&last_window) {
484 return last_window;
485 }
486 }
487
488 if let Some((window_index, _)) = self.windows.range(..removed_index).next_back() {
489 return *window_index;
490 }
491
492 self.windows
493 .range((Excluded(removed_index), Unbounded))
494 .next()
495 .map(|(window_index, _)| *window_index)
496 .expect("a non-empty session must have a replacement window")
497 }
498
499 fn allocate_pane_id(&mut self) -> PaneId {
500 let mut next_pane_id = self.next_pane_id;
501
502 loop {
503 let pane_id = PaneId::new(next_pane_id);
504 if !self.contains_pane_id(pane_id) {
505 self.next_pane_id = next_pane_id.saturating_add(1);
506 return pane_id;
507 }
508
509 assert_ne!(next_pane_id, u32::MAX, "pane id space exhausted");
510 next_pane_id += 1;
511 }
512 }
513
514 fn contains_pane_id(&self, pane_id: PaneId) -> bool {
515 self.windows
516 .values()
517 .flat_map(Window::panes)
518 .any(|pane| pane.id() == pane_id)
519 }
520
521 pub fn window_index_for_pane_id(&self, pane_id: PaneId) -> Option<u32> {
523 self.windows.iter().find_map(|(window_index, window)| {
524 window
525 .panes()
526 .iter()
527 .any(|pane| pane.id() == pane_id)
528 .then_some(*window_index)
529 })
530 }
531
532 fn allocate_window_id(&self) -> WindowId {
533 self.next_window_id.allocate()
534 }
535}
536
537fn synchronized_active_window(
538 windows: &BTreeMap<u32, Window>,
539 previous_active: u32,
540 previous_last: Option<u32>,
541) -> u32 {
542 if windows.contains_key(&previous_active) {
543 return previous_active;
544 }
545
546 if let Some(last_window) = previous_last {
547 if last_window != previous_active && windows.contains_key(&last_window) {
548 return last_window;
549 }
550 }
551
552 if let Some((window_index, _)) = windows.range(..previous_active).next_back() {
553 return *window_index;
554 }
555
556 windows
557 .range((Excluded(previous_active), Unbounded))
558 .next()
559 .map(|(window_index, _)| *window_index)
560 .or_else(|| windows.keys().next().copied())
561 .expect("group synchronization requires at least one window")
562}
563
564fn current_unix_timestamp() -> i64 {
565 SystemTime::now()
566 .duration_since(UNIX_EPOCH)
567 .ok()
568 .and_then(|duration| i64::try_from(duration.as_secs()).ok())
569 .unwrap_or_default()
570}
571
572#[cfg(test)]
573mod tests;
574
575#[cfg(test)]
576#[path = "session/zoom_tests.rs"]
577mod zoom_tests;
578
579#[cfg(test)]
580#[path = "session/layout_tests.rs"]
581mod layout_tests;