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, 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 windows: BTreeMap<u32, Window>,
45 winlink_alert_flags: BTreeMap<u32, AlertFlags>,
46 active_window: u32,
47 last_window: Option<u32>,
48 next_pane_id: u32,
49 next_window_id: WindowIdAllocator,
50 created_at: i64,
51 activity_at: i64,
52 last_attached_at: Option<i64>,
53 cwd: Option<PathBuf>,
54}
55
56impl Session {
57 #[must_use]
59 pub fn new(name: SessionName, size: TerminalSize) -> Self {
60 Self::new_with_initial_window(name, size, 0, PaneId::new(0), WindowId::new(0))
61 }
62
63 #[must_use]
65 pub(crate) fn new_with_initial_window(
66 name: SessionName,
67 size: TerminalSize,
68 window_index: u32,
69 pane_id: PaneId,
70 window_id: WindowId,
71 ) -> Self {
72 let now = current_unix_timestamp();
73 Self {
74 id: SessionId::new(0),
75 name,
76 group_name: None,
77 windows: BTreeMap::from([(
78 window_index,
79 Window::new_with_initial_pane(size, pane_id, window_id),
80 )]),
81 winlink_alert_flags: BTreeMap::from([(window_index, AlertFlags::empty())]),
82 active_window: window_index,
83 last_window: None,
84 next_pane_id: pane_id.as_u32().saturating_add(1),
85 next_window_id: WindowIdAllocator::new(window_id.as_u32().saturating_add(1)),
86 created_at: now,
87 activity_at: now,
88 last_attached_at: None,
89 cwd: None,
90 }
91 }
92
93 pub fn split_active_pane(&mut self) -> Result<u32, RmuxError> {
95 self.split_active_pane_with_direction(SplitDirection::Vertical)
96 }
97
98 pub fn split_active_pane_with_direction(
100 &mut self,
101 direction: SplitDirection,
102 ) -> Result<u32, RmuxError> {
103 self.split_pane_with_direction(self.active_pane_index(), direction)
104 }
105
106 pub fn split_pane(&mut self, pane_index: u32) -> Result<u32, RmuxError> {
108 self.split_pane_with_direction(pane_index, SplitDirection::Vertical)
109 }
110
111 pub fn split_pane_with_direction(
113 &mut self,
114 pane_index: u32,
115 direction: SplitDirection,
116 ) -> Result<u32, RmuxError> {
117 self.split_pane_in_window_with_direction(self.active_window, pane_index, direction)
118 }
119
120 pub fn split_pane_in_window(
122 &mut self,
123 window_index: u32,
124 pane_index: u32,
125 ) -> Result<u32, RmuxError> {
126 self.split_pane_in_window_with_direction(window_index, pane_index, SplitDirection::Vertical)
127 }
128
129 pub fn split_pane_in_window_with_direction(
131 &mut self,
132 window_index: u32,
133 pane_index: u32,
134 direction: SplitDirection,
135 ) -> Result<u32, RmuxError> {
136 self.split_pane_in_window_with_direction_before(window_index, pane_index, direction, false)
137 }
138
139 pub fn split_pane_in_window_with_direction_before(
143 &mut self,
144 window_index: u32,
145 pane_index: u32,
146 direction: SplitDirection,
147 before: bool,
148 ) -> Result<u32, RmuxError> {
149 let pane_id = self.allocate_pane_id();
150 self.split_pane_in_window_with_id_and_direction_before(
151 window_index,
152 pane_index,
153 pane_id,
154 direction,
155 before,
156 )
157 }
158
159 pub fn split_pane_in_window_with_id_and_direction(
165 &mut self,
166 window_index: u32,
167 pane_index: u32,
168 pane_id: PaneId,
169 direction: SplitDirection,
170 ) -> Result<u32, RmuxError> {
171 self.split_pane_in_window_with_id_and_direction_before(
172 window_index,
173 pane_index,
174 pane_id,
175 direction,
176 false,
177 )
178 }
179
180 pub fn split_pane_in_window_with_id_and_direction_before(
183 &mut self,
184 window_index: u32,
185 pane_index: u32,
186 pane_id: PaneId,
187 direction: SplitDirection,
188 before: bool,
189 ) -> Result<u32, RmuxError> {
190 let window = self
191 .window_at(window_index)
192 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
193 let position = window.pane_position(pane_index).ok_or_else(|| {
194 invalid_pane_target(
195 &self.name,
196 window_index,
197 pane_index,
198 "pane index does not exist in session",
199 )
200 })?;
201 if !window.can_split_pane(pane_index, direction) {
202 return Err(RmuxError::Message("no space for new pane".to_owned()));
203 }
204 Ok(self
205 .window_at_mut(window_index)
206 .expect("addressed session window must exist")
207 .split_at_position_with_id_and_direction(position, pane_id, direction, before))
208 }
209
210 pub fn kill_pane(&mut self, pane_index: u32) -> Result<KillPaneOutcome, RmuxError> {
212 self.kill_pane_in_window(self.active_window, pane_index)
213 }
214
215 pub fn kill_pane_in_window(
217 &mut self,
218 window_index: u32,
219 pane_index: u32,
220 ) -> Result<KillPaneOutcome, RmuxError> {
221 let window = self
222 .window_at(window_index)
223 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
224 let pane_id = window.pane_id(pane_index).ok_or_else(|| {
225 invalid_pane_target(
226 &self.name,
227 window_index,
228 pane_index,
229 "pane index does not exist in session",
230 )
231 })?;
232
233 if window.pane_count() == 1 {
234 let removed_window = self.remove_window(window_index)?;
235 let removed_pane_ids = removed_window.panes().iter().map(Pane::id).collect();
236 return Ok(KillPaneOutcome::new(removed_pane_ids, true));
237 }
238
239 let removed_pane = self
240 .window_at_mut(window_index)
241 .expect("addressed session window must exist")
242 .remove_pane(pane_index)
243 .expect("prevalidated pane removal must succeed");
244 debug_assert_eq!(removed_pane.id(), pane_id);
245
246 Ok(KillPaneOutcome::new(vec![removed_pane.id()], false))
247 }
248
249 pub fn kill_other_panes_in_window(
251 &mut self,
252 window_index: u32,
253 pane_index: u32,
254 ) -> Result<KillPaneOutcome, RmuxError> {
255 let window = self
256 .window_at(window_index)
257 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
258 if window.pane(pane_index).is_none() {
259 return Err(invalid_pane_target(
260 &self.name,
261 window_index,
262 pane_index,
263 "pane index does not exist in session",
264 ));
265 }
266
267 let removed_pane_ids = self
268 .window_at_mut(window_index)
269 .expect("addressed session window must exist")
270 .remove_other_panes(pane_index)
271 .expect("prevalidated pane removal must succeed");
272
273 Ok(KillPaneOutcome::new(removed_pane_ids, false))
274 }
275
276 pub fn select_pane(&mut self, pane_index: u32) -> Result<(), RmuxError> {
278 self.select_pane_in_window(self.active_window, pane_index)
279 }
280
281 pub fn select_pane_in_window(
283 &mut self,
284 window_index: u32,
285 pane_index: u32,
286 ) -> Result<(), RmuxError> {
287 if self.window_at(window_index).is_none() {
288 return Err(invalid_window_target(&self.name, window_index));
289 }
290
291 if self
292 .window_at_mut(window_index)
293 .expect("addressed session window must exist")
294 .select_pane(pane_index)
295 {
296 Ok(())
297 } else {
298 Err(invalid_pane_target(
299 &self.name,
300 window_index,
301 pane_index,
302 "pane index does not exist in session",
303 ))
304 }
305 }
306
307 pub fn select_adjacent_pane_in_window(
309 &mut self,
310 window_index: u32,
311 pane_index: u32,
312 direction: rmux_proto::SelectPaneDirection,
313 ) -> Result<u32, RmuxError> {
314 if self.window_at(window_index).is_none() {
315 return Err(invalid_window_target(&self.name, window_index));
316 }
317
318 self.window_at_mut(window_index)
319 .expect("addressed session window must exist")
320 .select_adjacent_pane(pane_index, direction)
321 .ok_or_else(|| {
322 invalid_pane_target(
323 &self.name,
324 window_index,
325 pane_index,
326 "pane index does not exist in session",
327 )
328 })
329 }
330
331 pub fn resize_terminal(&mut self, size: TerminalSize) {
333 for window in self.windows.values_mut() {
334 window.set_size(size);
335 }
336 }
337
338 fn resolve_window_target_mut(&mut self, window_index: u32) -> Result<&mut Window, RmuxError> {
339 if !self.windows.contains_key(&window_index) {
340 return Err(invalid_window_target(&self.name, window_index));
341 }
342
343 Ok(self
344 .window_at_mut(window_index)
345 .expect("addressed session window must exist"))
346 }
347
348 pub(crate) fn lowest_available_window_index_at_or_above(
349 &self,
350 minimum_index: u32,
351 ) -> Result<u32, RmuxError> {
352 let mut next_index = minimum_index;
353
354 for window_index in self.windows.keys().copied() {
355 if window_index < next_index {
356 continue;
357 }
358 if window_index > next_index {
359 break;
360 }
361
362 if window_index == next_index {
363 next_index = next_index.checked_add(1).ok_or_else(|| {
364 RmuxError::Server(format!(
365 "window index space exhausted for session {}",
366 self.name
367 ))
368 })?;
369 }
370 }
371
372 Ok(next_index)
373 }
374
375 fn next_active_window_after_removal(&self, removed_index: u32) -> u32 {
376 if let Some(last_window) = self.last_window {
377 if last_window != removed_index && self.windows.contains_key(&last_window) {
378 return last_window;
379 }
380 }
381
382 if let Some((window_index, _)) = self.windows.range(..removed_index).next_back() {
383 return *window_index;
384 }
385
386 self.windows
387 .range((Excluded(removed_index), Unbounded))
388 .next()
389 .map(|(window_index, _)| *window_index)
390 .expect("a non-empty session must have a replacement window")
391 }
392
393 fn allocate_pane_id(&mut self) -> PaneId {
394 let mut next_pane_id = self.next_pane_id;
395
396 loop {
397 let pane_id = PaneId::new(next_pane_id);
398 if !self.contains_pane_id(pane_id) {
399 self.next_pane_id = next_pane_id.saturating_add(1);
400 return pane_id;
401 }
402
403 assert_ne!(next_pane_id, u32::MAX, "pane id space exhausted");
404 next_pane_id += 1;
405 }
406 }
407
408 fn contains_pane_id(&self, pane_id: PaneId) -> bool {
409 self.windows
410 .values()
411 .flat_map(Window::panes)
412 .any(|pane| pane.id() == pane_id)
413 }
414
415 pub fn window_index_for_pane_id(&self, pane_id: PaneId) -> Option<u32> {
417 self.windows.iter().find_map(|(window_index, window)| {
418 window
419 .panes()
420 .iter()
421 .any(|pane| pane.id() == pane_id)
422 .then_some(*window_index)
423 })
424 }
425
426 fn allocate_window_id(&self) -> WindowId {
427 self.next_window_id.allocate()
428 }
429}
430
431fn synchronized_active_window(
432 windows: &BTreeMap<u32, Window>,
433 previous_active: u32,
434 previous_last: Option<u32>,
435) -> u32 {
436 if windows.contains_key(&previous_active) {
437 return previous_active;
438 }
439
440 if let Some(last_window) = previous_last {
441 if last_window != previous_active && windows.contains_key(&last_window) {
442 return last_window;
443 }
444 }
445
446 if let Some((window_index, _)) = windows.range(..previous_active).next_back() {
447 return *window_index;
448 }
449
450 windows
451 .range((Excluded(previous_active), Unbounded))
452 .next()
453 .map(|(window_index, _)| *window_index)
454 .or_else(|| windows.keys().next().copied())
455 .expect("group synchronization requires at least one window")
456}
457
458fn current_unix_timestamp() -> i64 {
459 SystemTime::now()
460 .duration_since(UNIX_EPOCH)
461 .ok()
462 .and_then(|duration| i64::try_from(duration.as_secs()).ok())
463 .unwrap_or_default()
464}
465
466#[cfg(test)]
467mod tests;
468
469#[cfg(test)]
470#[path = "session/zoom_tests.rs"]
471mod zoom_tests;
472
473#[cfg(test)]
474#[path = "session/layout_tests.rs"]
475mod layout_tests;