1use std::collections::BTreeMap;
2use std::ops::Bound::{Excluded, Unbounded};
3use std::path::PathBuf;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use rmux_proto::{ResizePaneAdjustment, RmuxError, SessionName, SplitDirection, TerminalSize};
7
8use crate::{AlertFlags, Pane, 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/store.rs"]
21mod store;
22#[path = "session/target_error.rs"]
23mod target_error;
24#[path = "session/types.rs"]
25mod types;
26#[path = "session/window_ops.rs"]
27mod window_ops;
28
29pub use store::SessionStore;
30use target_error::{invalid_pane_target, invalid_window_target};
31pub(crate) use types::WindowIdAllocator;
32pub use types::{
33 BreakPaneOptions, KillPaneOutcome, PaneJoinOptions, PaneSwapOptions, SessionPaneTarget,
34};
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Session {
39 id: SessionId,
40 name: SessionName,
41 group_name: Option<SessionName>,
42 windows: BTreeMap<u32, Window>,
43 winlink_alert_flags: BTreeMap<u32, AlertFlags>,
44 active_window: u32,
45 last_window: Option<u32>,
46 next_pane_id: u32,
47 next_window_id: WindowIdAllocator,
48 created_at: i64,
49 activity_at: i64,
50 last_attached_at: Option<i64>,
51 cwd: Option<PathBuf>,
52}
53
54impl Session {
55 #[must_use]
57 pub fn new(name: SessionName, size: TerminalSize) -> Self {
58 Self::new_with_initial_window(name, size, 0, PaneId::new(0), WindowId::new(0))
59 }
60
61 #[must_use]
63 pub(crate) fn new_with_initial_window(
64 name: SessionName,
65 size: TerminalSize,
66 window_index: u32,
67 pane_id: PaneId,
68 window_id: WindowId,
69 ) -> Self {
70 let now = current_unix_timestamp();
71 Self {
72 id: SessionId::new(0),
73 name,
74 group_name: None,
75 windows: BTreeMap::from([(
76 window_index,
77 Window::new_with_initial_pane(size, pane_id, window_id),
78 )]),
79 winlink_alert_flags: BTreeMap::from([(window_index, AlertFlags::empty())]),
80 active_window: window_index,
81 last_window: None,
82 next_pane_id: pane_id.as_u32().saturating_add(1),
83 next_window_id: WindowIdAllocator::new(window_id.as_u32().saturating_add(1)),
84 created_at: now,
85 activity_at: now,
86 last_attached_at: None,
87 cwd: None,
88 }
89 }
90
91 pub fn split_active_pane(&mut self) -> Result<u32, RmuxError> {
93 self.split_active_pane_with_direction(SplitDirection::Vertical)
94 }
95
96 pub fn split_active_pane_with_direction(
98 &mut self,
99 direction: SplitDirection,
100 ) -> Result<u32, RmuxError> {
101 self.split_pane_with_direction(self.active_pane_index(), direction)
102 }
103
104 pub fn split_pane(&mut self, pane_index: u32) -> Result<u32, RmuxError> {
106 self.split_pane_with_direction(pane_index, SplitDirection::Vertical)
107 }
108
109 pub fn split_pane_with_direction(
111 &mut self,
112 pane_index: u32,
113 direction: SplitDirection,
114 ) -> Result<u32, RmuxError> {
115 self.split_pane_in_window_with_direction(self.active_window, pane_index, direction)
116 }
117
118 pub fn split_pane_in_window(
120 &mut self,
121 window_index: u32,
122 pane_index: u32,
123 ) -> Result<u32, RmuxError> {
124 self.split_pane_in_window_with_direction(window_index, pane_index, SplitDirection::Vertical)
125 }
126
127 pub fn split_pane_in_window_with_direction(
129 &mut self,
130 window_index: u32,
131 pane_index: u32,
132 direction: SplitDirection,
133 ) -> Result<u32, RmuxError> {
134 self.split_pane_in_window_with_direction_before(window_index, pane_index, direction, false)
135 }
136
137 pub fn split_pane_in_window_with_direction_before(
141 &mut self,
142 window_index: u32,
143 pane_index: u32,
144 direction: SplitDirection,
145 before: bool,
146 ) -> Result<u32, RmuxError> {
147 let pane_id = self.allocate_pane_id();
148 self.split_pane_in_window_with_id_and_direction_before(
149 window_index,
150 pane_index,
151 pane_id,
152 direction,
153 before,
154 )
155 }
156
157 pub fn split_pane_in_window_with_id_and_direction(
163 &mut self,
164 window_index: u32,
165 pane_index: u32,
166 pane_id: PaneId,
167 direction: SplitDirection,
168 ) -> Result<u32, RmuxError> {
169 self.split_pane_in_window_with_id_and_direction_before(
170 window_index,
171 pane_index,
172 pane_id,
173 direction,
174 false,
175 )
176 }
177
178 pub fn split_pane_in_window_with_id_and_direction_before(
181 &mut self,
182 window_index: u32,
183 pane_index: u32,
184 pane_id: PaneId,
185 direction: SplitDirection,
186 before: bool,
187 ) -> Result<u32, RmuxError> {
188 let window = self
189 .window_at(window_index)
190 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
191 let position = window.pane_position(pane_index).ok_or_else(|| {
192 invalid_pane_target(
193 &self.name,
194 window_index,
195 pane_index,
196 "pane index does not exist in session",
197 )
198 })?;
199 if !window.can_split_pane(pane_index, direction) {
200 return Err(RmuxError::Message("no space for new pane".to_owned()));
201 }
202 Ok(self
203 .window_at_mut(window_index)
204 .expect("addressed session window must exist")
205 .split_at_position_with_id_and_direction(position, pane_id, direction, before))
206 }
207
208 pub fn kill_pane(&mut self, pane_index: u32) -> Result<KillPaneOutcome, RmuxError> {
210 self.kill_pane_in_window(self.active_window, pane_index)
211 }
212
213 pub fn kill_pane_in_window(
215 &mut self,
216 window_index: u32,
217 pane_index: u32,
218 ) -> Result<KillPaneOutcome, RmuxError> {
219 let window = self
220 .window_at(window_index)
221 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
222 let pane_id = window.pane_id(pane_index).ok_or_else(|| {
223 invalid_pane_target(
224 &self.name,
225 window_index,
226 pane_index,
227 "pane index does not exist in session",
228 )
229 })?;
230
231 if window.pane_count() == 1 {
232 let removed_window = self.remove_window(window_index)?;
233 let removed_pane_ids = removed_window.panes().iter().map(Pane::id).collect();
234 return Ok(KillPaneOutcome::new(removed_pane_ids, true));
235 }
236
237 let removed_pane = self
238 .window_at_mut(window_index)
239 .expect("addressed session window must exist")
240 .remove_pane(pane_index)
241 .expect("prevalidated pane removal must succeed");
242 debug_assert_eq!(removed_pane.id(), pane_id);
243
244 Ok(KillPaneOutcome::new(vec![removed_pane.id()], false))
245 }
246
247 pub fn kill_other_panes_in_window(
249 &mut self,
250 window_index: u32,
251 pane_index: u32,
252 ) -> Result<KillPaneOutcome, RmuxError> {
253 let window = self
254 .window_at(window_index)
255 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
256 if window.pane(pane_index).is_none() {
257 return Err(invalid_pane_target(
258 &self.name,
259 window_index,
260 pane_index,
261 "pane index does not exist in session",
262 ));
263 }
264
265 let removed_pane_ids = self
266 .window_at_mut(window_index)
267 .expect("addressed session window must exist")
268 .remove_other_panes(pane_index)
269 .expect("prevalidated pane removal must succeed");
270
271 Ok(KillPaneOutcome::new(removed_pane_ids, false))
272 }
273
274 pub fn select_pane(&mut self, pane_index: u32) -> Result<(), RmuxError> {
276 self.select_pane_in_window(self.active_window, pane_index)
277 }
278
279 pub fn select_pane_in_window(
281 &mut self,
282 window_index: u32,
283 pane_index: u32,
284 ) -> Result<(), RmuxError> {
285 if self.window_at(window_index).is_none() {
286 return Err(invalid_window_target(&self.name, window_index));
287 }
288
289 if self
290 .window_at_mut(window_index)
291 .expect("addressed session window must exist")
292 .select_pane(pane_index)
293 {
294 Ok(())
295 } else {
296 Err(invalid_pane_target(
297 &self.name,
298 window_index,
299 pane_index,
300 "pane index does not exist in session",
301 ))
302 }
303 }
304
305 pub fn select_adjacent_pane_in_window(
307 &mut self,
308 window_index: u32,
309 pane_index: u32,
310 direction: rmux_proto::SelectPaneDirection,
311 ) -> Result<u32, RmuxError> {
312 if self.window_at(window_index).is_none() {
313 return Err(invalid_window_target(&self.name, window_index));
314 }
315
316 self.window_at_mut(window_index)
317 .expect("addressed session window must exist")
318 .select_adjacent_pane(pane_index, direction)
319 .ok_or_else(|| {
320 invalid_pane_target(
321 &self.name,
322 window_index,
323 pane_index,
324 "pane index does not exist in session",
325 )
326 })
327 }
328
329 pub fn resize_pane(
331 &mut self,
332 pane_index: u32,
333 adjustment: ResizePaneAdjustment,
334 ) -> Result<(), RmuxError> {
335 self.resize_pane_in_window(self.active_window, pane_index, adjustment)
336 }
337
338 pub fn resize_pane_in_window(
340 &mut self,
341 window_index: u32,
342 pane_index: u32,
343 adjustment: ResizePaneAdjustment,
344 ) -> Result<(), RmuxError> {
345 if adjustment == ResizePaneAdjustment::Zoom {
346 return self.toggle_zoom_in_window(window_index, pane_index);
347 }
348
349 if self.window_at(window_index).is_none() {
350 return Err(invalid_window_target(&self.name, window_index));
351 }
352
353 if self
354 .window_at(window_index)
355 .and_then(|window| window.pane(pane_index))
356 .is_none()
357 {
358 return Err(invalid_pane_target(
359 &self.name,
360 window_index,
361 pane_index,
362 "pane index does not exist in session",
363 ));
364 }
365
366 let window = self
367 .window_at_mut(window_index)
368 .expect("addressed session window must exist");
369
370 match adjustment {
371 ResizePaneAdjustment::NoOp => {}
372 ResizePaneAdjustment::AbsoluteWidth { columns } => {
373 let _ = window.resize_pane_width(pane_index, columns);
374 }
375 ResizePaneAdjustment::AbsoluteHeight { rows } => {
376 let _ = window.resize_pane_height(pane_index, rows);
377 }
378 ResizePaneAdjustment::AbsoluteSize { columns, rows } => {
379 let _ = window.resize_pane_width(pane_index, columns);
380 let _ = window.resize_pane_height(pane_index, rows);
381 }
382 ResizePaneAdjustment::Up { cells } => {
383 let _ = window.resize_pane_by(pane_index, ResizePaneAdjustment::Up { cells });
384 }
385 ResizePaneAdjustment::Down { cells } => {
386 let _ = window.resize_pane_by(pane_index, ResizePaneAdjustment::Down { cells });
387 }
388 ResizePaneAdjustment::Left { cells } => {
389 let _ = window.resize_pane_by(pane_index, ResizePaneAdjustment::Left { cells });
390 }
391 ResizePaneAdjustment::Right { cells } => {
392 let _ = window.resize_pane_by(pane_index, ResizePaneAdjustment::Right { cells });
393 }
394 ResizePaneAdjustment::Zoom => unreachable!("zoom returned early"),
395 }
396 Ok(())
397 }
398
399 pub fn toggle_zoom_in_window(
401 &mut self,
402 window_index: u32,
403 pane_index: u32,
404 ) -> Result<(), RmuxError> {
405 if self.window_at(window_index).is_none() {
406 return Err(invalid_window_target(&self.name, window_index));
407 }
408
409 if self
410 .window_at(window_index)
411 .and_then(|window| window.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 self.window_at_mut(window_index)
423 .expect("addressed session window must exist")
424 .toggle_zoom(pane_index);
425 Ok(())
426 }
427
428 pub fn resize_terminal(&mut self, size: TerminalSize) {
430 for window in self.windows.values_mut() {
431 window.set_size(size);
432 }
433 }
434
435 fn resolve_window_target_mut(&mut self, window_index: u32) -> Result<&mut Window, RmuxError> {
436 if !self.windows.contains_key(&window_index) {
437 return Err(invalid_window_target(&self.name, window_index));
438 }
439
440 Ok(self
441 .window_at_mut(window_index)
442 .expect("addressed session window must exist"))
443 }
444
445 pub(crate) fn lowest_available_window_index_at_or_above(
446 &self,
447 minimum_index: u32,
448 ) -> Result<u32, RmuxError> {
449 let mut next_index = minimum_index;
450
451 for window_index in self.windows.keys().copied() {
452 if window_index < next_index {
453 continue;
454 }
455 if window_index > next_index {
456 break;
457 }
458
459 if window_index == next_index {
460 next_index = next_index.checked_add(1).ok_or_else(|| {
461 RmuxError::Server(format!(
462 "window index space exhausted for session {}",
463 self.name
464 ))
465 })?;
466 }
467 }
468
469 Ok(next_index)
470 }
471
472 fn next_active_window_after_removal(&self, removed_index: u32) -> u32 {
473 if let Some(last_window) = self.last_window {
474 if last_window != removed_index && self.windows.contains_key(&last_window) {
475 return last_window;
476 }
477 }
478
479 if let Some((window_index, _)) = self.windows.range(..removed_index).next_back() {
480 return *window_index;
481 }
482
483 self.windows
484 .range((Excluded(removed_index), Unbounded))
485 .next()
486 .map(|(window_index, _)| *window_index)
487 .expect("a non-empty session must have a replacement window")
488 }
489
490 fn allocate_pane_id(&mut self) -> PaneId {
491 let mut next_pane_id = self.next_pane_id;
492
493 loop {
494 let pane_id = PaneId::new(next_pane_id);
495 if !self.contains_pane_id(pane_id) {
496 self.next_pane_id = next_pane_id.saturating_add(1);
497 return pane_id;
498 }
499
500 assert_ne!(next_pane_id, u32::MAX, "pane id space exhausted");
501 next_pane_id += 1;
502 }
503 }
504
505 fn contains_pane_id(&self, pane_id: PaneId) -> bool {
506 self.windows
507 .values()
508 .flat_map(Window::panes)
509 .any(|pane| pane.id() == pane_id)
510 }
511
512 pub fn window_index_for_pane_id(&self, pane_id: PaneId) -> Option<u32> {
514 self.windows.iter().find_map(|(window_index, window)| {
515 window
516 .panes()
517 .iter()
518 .any(|pane| pane.id() == pane_id)
519 .then_some(*window_index)
520 })
521 }
522
523 fn allocate_window_id(&self) -> WindowId {
524 self.next_window_id.allocate()
525 }
526}
527
528fn synchronized_active_window(
529 windows: &BTreeMap<u32, Window>,
530 previous_active: u32,
531 previous_last: Option<u32>,
532) -> u32 {
533 if windows.contains_key(&previous_active) {
534 return previous_active;
535 }
536
537 if let Some(last_window) = previous_last {
538 if last_window != previous_active && windows.contains_key(&last_window) {
539 return last_window;
540 }
541 }
542
543 if let Some((window_index, _)) = windows.range(..previous_active).next_back() {
544 return *window_index;
545 }
546
547 windows
548 .range((Excluded(previous_active), Unbounded))
549 .next()
550 .map(|(window_index, _)| *window_index)
551 .or_else(|| windows.keys().next().copied())
552 .expect("group synchronization requires at least one window")
553}
554
555fn current_unix_timestamp() -> i64 {
556 SystemTime::now()
557 .duration_since(UNIX_EPOCH)
558 .ok()
559 .and_then(|duration| i64::try_from(duration.as_secs()).ok())
560 .unwrap_or_default()
561}
562
563#[cfg(test)]
564mod tests;
565
566#[cfg(test)]
567#[path = "session/zoom_tests.rs"]
568mod zoom_tests;
569
570#[cfg(test)]
571#[path = "session/layout_tests.rs"]
572mod layout_tests;