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::Up { cells } => {
379 let _ = window.resize_pane_by(pane_index, ResizePaneAdjustment::Up { cells });
380 }
381 ResizePaneAdjustment::Down { cells } => {
382 let _ = window.resize_pane_by(pane_index, ResizePaneAdjustment::Down { cells });
383 }
384 ResizePaneAdjustment::Left { cells } => {
385 let _ = window.resize_pane_by(pane_index, ResizePaneAdjustment::Left { cells });
386 }
387 ResizePaneAdjustment::Right { cells } => {
388 let _ = window.resize_pane_by(pane_index, ResizePaneAdjustment::Right { cells });
389 }
390 ResizePaneAdjustment::Zoom => unreachable!("zoom returned early"),
391 }
392 Ok(())
393 }
394
395 pub fn toggle_zoom_in_window(
397 &mut self,
398 window_index: u32,
399 pane_index: u32,
400 ) -> Result<(), RmuxError> {
401 if self.window_at(window_index).is_none() {
402 return Err(invalid_window_target(&self.name, window_index));
403 }
404
405 if self
406 .window_at(window_index)
407 .and_then(|window| window.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 self.window_at_mut(window_index)
419 .expect("addressed session window must exist")
420 .toggle_zoom(pane_index);
421 Ok(())
422 }
423
424 pub fn resize_terminal(&mut self, size: TerminalSize) {
426 for window in self.windows.values_mut() {
427 window.set_size(size);
428 }
429 }
430
431 fn resolve_window_target_mut(&mut self, window_index: u32) -> Result<&mut Window, RmuxError> {
432 if !self.windows.contains_key(&window_index) {
433 return Err(invalid_window_target(&self.name, window_index));
434 }
435
436 Ok(self
437 .window_at_mut(window_index)
438 .expect("addressed session window must exist"))
439 }
440
441 pub(crate) fn lowest_available_window_index_at_or_above(
442 &self,
443 minimum_index: u32,
444 ) -> Result<u32, RmuxError> {
445 let mut next_index = minimum_index;
446
447 for window_index in self.windows.keys().copied() {
448 if window_index < next_index {
449 continue;
450 }
451 if window_index > next_index {
452 break;
453 }
454
455 if window_index == next_index {
456 next_index = next_index.checked_add(1).ok_or_else(|| {
457 RmuxError::Server(format!(
458 "window index space exhausted for session {}",
459 self.name
460 ))
461 })?;
462 }
463 }
464
465 Ok(next_index)
466 }
467
468 fn next_active_window_after_removal(&self, removed_index: u32) -> u32 {
469 if let Some(last_window) = self.last_window {
470 if last_window != removed_index && self.windows.contains_key(&last_window) {
471 return last_window;
472 }
473 }
474
475 if let Some((window_index, _)) = self.windows.range(..removed_index).next_back() {
476 return *window_index;
477 }
478
479 self.windows
480 .range((Excluded(removed_index), Unbounded))
481 .next()
482 .map(|(window_index, _)| *window_index)
483 .expect("a non-empty session must have a replacement window")
484 }
485
486 fn allocate_pane_id(&mut self) -> PaneId {
487 let mut next_pane_id = self.next_pane_id;
488
489 loop {
490 let pane_id = PaneId::new(next_pane_id);
491 if !self.contains_pane_id(pane_id) {
492 self.next_pane_id = next_pane_id.saturating_add(1);
493 return pane_id;
494 }
495
496 assert_ne!(next_pane_id, u32::MAX, "pane id space exhausted");
497 next_pane_id += 1;
498 }
499 }
500
501 fn contains_pane_id(&self, pane_id: PaneId) -> bool {
502 self.windows
503 .values()
504 .flat_map(Window::panes)
505 .any(|pane| pane.id() == pane_id)
506 }
507
508 pub fn window_index_for_pane_id(&self, pane_id: PaneId) -> Option<u32> {
510 self.windows.iter().find_map(|(window_index, window)| {
511 window
512 .panes()
513 .iter()
514 .any(|pane| pane.id() == pane_id)
515 .then_some(*window_index)
516 })
517 }
518
519 fn allocate_window_id(&self) -> WindowId {
520 self.next_window_id.allocate()
521 }
522}
523
524fn synchronized_active_window(
525 windows: &BTreeMap<u32, Window>,
526 previous_active: u32,
527 previous_last: Option<u32>,
528) -> u32 {
529 if windows.contains_key(&previous_active) {
530 return previous_active;
531 }
532
533 if let Some(last_window) = previous_last {
534 if last_window != previous_active && windows.contains_key(&last_window) {
535 return last_window;
536 }
537 }
538
539 if let Some((window_index, _)) = windows.range(..previous_active).next_back() {
540 return *window_index;
541 }
542
543 windows
544 .range((Excluded(previous_active), Unbounded))
545 .next()
546 .map(|(window_index, _)| *window_index)
547 .or_else(|| windows.keys().next().copied())
548 .expect("group synchronization requires at least one window")
549}
550
551fn current_unix_timestamp() -> i64 {
552 SystemTime::now()
553 .duration_since(UNIX_EPOCH)
554 .ok()
555 .and_then(|duration| i64::try_from(duration.as_secs()).ok())
556 .unwrap_or_default()
557}
558
559#[cfg(test)]
560mod tests;
561
562#[cfg(test)]
563#[path = "session/zoom_tests.rs"]
564mod zoom_tests;
565
566#[cfg(test)]
567#[path = "session/layout_tests.rs"]
568mod layout_tests;