1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Path, PathBuf};
3
4use rmux_proto::{SessionName, TerminalSize};
5
6use super::{current_unix_timestamp, synchronized_active_window, Session, WindowIdAllocator};
7use crate::{AlertFlags, Pane, PaneId, SessionId, Window, WindowId, WINLINK_ALERTFLAGS};
8
9fn window_index_for_id(
10 windows: &BTreeMap<u32, Window>,
11 window_id: WindowId,
12 preferred_index: u32,
13) -> Option<u32> {
14 if windows
15 .get(&preferred_index)
16 .is_some_and(|window| window.id() == window_id)
17 {
18 return Some(preferred_index);
19 }
20 let mut matches = windows
21 .iter()
22 .filter_map(|(window_index, window)| (window.id() == window_id).then_some(*window_index));
23 let only_match = matches.next()?;
24 matches.next().is_none().then_some(only_match)
25}
26
27fn remap_local_winlink_alert_flags(
28 previous_windows: &BTreeMap<u32, Window>,
29 previous_flags: &BTreeMap<u32, AlertFlags>,
30 synchronized_windows: &BTreeMap<u32, Window>,
31 source_flags: &BTreeMap<u32, AlertFlags>,
32 winlink_alert_map: Option<&BTreeMap<u32, u32>>,
33) -> BTreeMap<u32, AlertFlags> {
34 let mut synchronized_indices = BTreeMap::new();
35 let mut claimed = BTreeSet::new();
36
37 if let Some(winlink_alert_map) = winlink_alert_map {
38 for (&previous_index, previous_window) in previous_windows {
39 let Some(&next_index) = winlink_alert_map.get(&previous_index) else {
40 continue;
41 };
42 if synchronized_windows
43 .get(&next_index)
44 .is_some_and(|window| window.id() == previous_window.id())
45 && claimed.insert(next_index)
46 {
47 synchronized_indices.insert(previous_index, next_index);
48 }
49 }
50 }
51
52 for (&previous_index, previous_window) in previous_windows {
56 if synchronized_indices.contains_key(&previous_index) {
57 continue;
58 }
59 if synchronized_windows
60 .get(&previous_index)
61 .is_some_and(|window| window.id() == previous_window.id())
62 && claimed.insert(previous_index)
63 {
64 synchronized_indices.insert(previous_index, previous_index);
65 }
66 }
67
68 for (&previous_index, previous_window) in previous_windows {
69 if synchronized_indices.contains_key(&previous_index) {
70 continue;
71 }
72 let next_index = synchronized_windows.iter().find_map(|(&index, window)| {
73 (window.id() == previous_window.id() && !claimed.contains(&index)).then_some(index)
74 });
75 if let Some(next_index) = next_index {
76 claimed.insert(next_index);
77 synchronized_indices.insert(previous_index, next_index);
78 }
79 }
80
81 let mut remapped = previous_flags
82 .iter()
83 .filter_map(|(&previous_index, &flags)| {
84 synchronized_indices
85 .get(&previous_index)
86 .copied()
87 .map(|next_index| (next_index, flags))
88 })
89 .collect::<BTreeMap<_, _>>();
90 for &window_index in synchronized_windows.keys() {
91 remapped.entry(window_index).or_insert_with(|| {
92 source_flags
93 .get(&window_index)
94 .copied()
95 .unwrap_or_else(AlertFlags::empty)
96 });
97 }
98 remapped
99}
100
101impl Session {
102 #[must_use]
104 pub const fn name(&self) -> &SessionName {
105 &self.name
106 }
107
108 #[must_use]
110 pub const fn group_name(&self) -> Option<&SessionName> {
111 self.group_name.as_ref()
112 }
113
114 #[must_use]
116 pub const fn id(&self) -> SessionId {
117 self.id
118 }
119
120 #[must_use]
122 pub const fn created_at(&self) -> i64 {
123 self.created_at
124 }
125
126 #[must_use]
128 pub const fn activity_at(&self) -> i64 {
129 self.activity_at
130 }
131
132 #[must_use]
134 pub const fn last_attached_at(&self) -> Option<i64> {
135 self.last_attached_at
136 }
137
138 #[doc(hidden)]
144 #[must_use]
145 pub const fn recency(&self) -> super::SessionRecency {
146 self.recency
147 }
148
149 #[must_use]
151 pub fn cwd(&self) -> Option<&Path> {
152 self.cwd.as_deref()
153 }
154
155 #[must_use]
157 pub const fn terminal_size(&self) -> TerminalSize {
158 self.terminal_size
159 }
160
161 pub(crate) fn set_id(&mut self, id: SessionId) {
162 self.id = id;
163 }
164
165 pub(crate) fn rebind_window_id_allocator(&mut self, allocator: WindowIdAllocator) {
166 let next_after_windows = self
167 .windows
168 .values()
169 .map(|window| window.id().as_u32().saturating_add(1))
170 .max()
171 .unwrap_or_else(|| allocator.peek());
172 self.next_window_id = allocator;
173 self.next_window_id.bump_to(next_after_windows);
174 }
175
176 pub fn rename(&mut self, new_name: SessionName) {
178 self.name = new_name;
179 }
180
181 pub fn set_group_name(&mut self, group_name: Option<SessionName>) {
183 self.group_name = group_name;
184 }
185
186 pub fn set_cwd(&mut self, cwd: Option<PathBuf>) {
188 self.cwd = cwd;
189 }
190
191 pub fn touch_attached(&mut self) {
193 let now = current_unix_timestamp();
194 self.activity_at = now;
195 self.last_attached_at = Some(now);
196 self.recency = super::SessionRecency::next();
197 }
198
199 #[cfg(any(test, feature = "test-seams"))]
214 #[doc(hidden)]
215 pub fn pin_public_times_for_tests(&mut self, seconds: i64) {
216 self.created_at = seconds;
217 self.activity_at = seconds;
218 self.last_attached_at = self.last_attached_at.map(|_| seconds);
219 }
220
221 pub fn touch_activity(&mut self) {
234 self.activity_at = current_unix_timestamp();
235 self.recency = super::SessionRecency::next();
236 }
237
238 pub(super) fn renew_recency(&mut self) {
246 self.recency = super::SessionRecency::next();
247 }
248
249 #[must_use]
251 pub fn clone_as_group_member(
252 &self,
253 name: SessionName,
254 group_name: SessionName,
255 session_id: SessionId,
256 ) -> Self {
257 let now = current_unix_timestamp();
258 let mut cloned = self.clone();
259 cloned.id = session_id;
260 cloned.name = name;
261 cloned.group_name = Some(group_name);
262 cloned.group_initial_window_id = self
263 .windows
264 .first_key_value()
265 .map(|(_, window)| window.id());
266 cloned.created_at = now;
267 cloned.activity_at = now;
268 cloned.last_attached_at = None;
269 cloned.recency = super::SessionRecency::next();
270 cloned
271 }
272
273 pub fn synchronize_group_from(&mut self, source: &Session) {
275 self.synchronize_group_from_with_optional_winlink_alert_map(source, None);
276 }
277
278 pub fn synchronize_group_from_with_winlink_alert_map(
281 &mut self,
282 source: &Session,
283 winlink_alert_map: &BTreeMap<u32, u32>,
284 ) {
285 self.synchronize_group_from_with_optional_winlink_alert_map(
286 source,
287 Some(winlink_alert_map),
288 );
289 }
290
291 fn synchronize_group_from_with_optional_winlink_alert_map(
292 &mut self,
293 source: &Session,
294 winlink_alert_map: Option<&BTreeMap<u32, u32>>,
295 ) {
296 debug_assert_ne!(self.name, source.name);
297 let previous_active = self.active_window;
298 let previous_last = self.last_window;
299 let previous_active_window_id = self.window_at(previous_active).map(Window::id);
300 let previous_last_window_id = previous_last
301 .and_then(|window_index| self.window_at(window_index))
302 .map(Window::id);
303
304 let synchronized_active = previous_active_window_id
305 .and_then(|window_id| window_index_for_id(&source.windows, window_id, previous_active))
306 .unwrap_or_else(|| {
307 synchronized_active_window(&source.windows, previous_active, previous_last)
308 });
309 let synchronized_last = previous_last_window_id
310 .and_then(|window_id| {
311 window_index_for_id(
312 &source.windows,
313 window_id,
314 previous_last.expect("last window id requires an index"),
315 )
316 })
317 .filter(|window_index| *window_index != synchronized_active)
318 .or_else(|| {
319 (previous_active != synchronized_active
320 && source.windows.contains_key(&previous_active))
321 .then_some(previous_active)
322 });
323 self.synchronize_group_windows(
324 source,
325 synchronized_active,
326 synchronized_last,
327 winlink_alert_map,
328 );
329 }
330
331 pub fn synchronize_group_from_with_window_selection_map(
333 &mut self,
334 source: &Session,
335 index_map: &BTreeMap<u32, u32>,
336 ) {
337 self.synchronize_group_from_with_window_selection_and_winlink_alert_maps(
338 source, index_map, index_map,
339 );
340 }
341
342 pub fn synchronize_group_from_with_window_selection_and_winlink_alert_maps(
345 &mut self,
346 source: &Session,
347 window_selection_map: &BTreeMap<u32, u32>,
348 winlink_alert_map: &BTreeMap<u32, u32>,
349 ) {
350 debug_assert_ne!(self.name, source.name);
351 let previous_active = self.active_window;
352 let previous_last = self.last_window;
353 let synchronized_active = window_selection_map
354 .get(&previous_active)
355 .copied()
356 .unwrap_or(previous_active);
357 let synchronized_active = if source.windows.contains_key(&synchronized_active) {
358 synchronized_active
359 } else {
360 synchronized_active_window(&source.windows, previous_active, previous_last)
361 };
362 let synchronized_last = previous_last
363 .map(|window_index| {
364 window_selection_map
365 .get(&window_index)
366 .copied()
367 .unwrap_or(window_index)
368 })
369 .filter(|window_index| {
370 *window_index != synchronized_active && source.windows.contains_key(window_index)
371 });
372 self.synchronize_group_windows(
373 source,
374 synchronized_active,
375 synchronized_last,
376 Some(winlink_alert_map),
377 );
378 }
379
380 fn synchronize_group_windows(
381 &mut self,
382 source: &Session,
383 active_window: u32,
384 last_window: Option<u32>,
385 winlink_alert_map: Option<&BTreeMap<u32, u32>>,
386 ) {
387 let synchronized_alert_flags = remap_local_winlink_alert_flags(
388 &self.windows,
389 &self.winlink_alert_flags,
390 &source.windows,
391 &source.winlink_alert_flags,
392 winlink_alert_map,
393 );
394 self.windows = source.windows.clone();
395 self.winlink_alert_flags = synchronized_alert_flags;
396 self.next_pane_id = source.next_pane_id;
397 self.cwd = source.cwd.clone();
398 self.active_window = active_window;
399 self.last_window = last_window;
400 }
401
402 #[must_use]
404 pub fn window(&self) -> &Window {
405 self.window_at(self.active_window)
406 .expect("active session window must exist")
407 }
408
409 #[must_use]
411 pub fn window_at(&self, window_index: u32) -> Option<&Window> {
412 self.windows.get(&window_index)
413 }
414
415 #[must_use]
417 pub const fn windows(&self) -> &BTreeMap<u32, Window> {
418 &self.windows
419 }
420
421 pub(crate) fn window_mut(&mut self) -> &mut Window {
422 self.window_at_mut(self.active_window)
423 .expect("active session window must exist")
424 }
425
426 pub fn window_at_mut(&mut self, window_index: u32) -> Option<&mut Window> {
428 self.windows.get_mut(&window_index)
429 }
430
431 #[must_use]
433 pub fn winlink_alert_flags(&self, window_index: u32) -> AlertFlags {
434 self.winlink_alert_flags
435 .get(&window_index)
436 .copied()
437 .unwrap_or_else(AlertFlags::empty)
438 }
439
440 #[must_use]
442 pub fn session_alert_flags(&self) -> AlertFlags {
443 self.winlink_alert_flags
444 .values()
445 .copied()
446 .fold(AlertFlags::empty(), |flags, winlink_flags| {
447 flags.union(winlink_flags)
448 })
449 }
450
451 #[must_use]
453 pub fn alerted_window_indexes(&self) -> Vec<u32> {
454 self.winlink_alert_flags
455 .iter()
456 .filter_map(|(window_index, flags)| {
457 flags
458 .intersects(WINLINK_ALERTFLAGS)
459 .then_some(*window_index)
460 })
461 .collect()
462 }
463
464 #[must_use]
466 pub fn has_alerts(&self) -> bool {
467 self.winlink_alert_flags
468 .values()
469 .any(|flags| flags.intersects(WINLINK_ALERTFLAGS))
470 }
471
472 pub fn add_winlink_alert_flags(&mut self, window_index: u32, flags: AlertFlags) -> bool {
474 if !self.windows.contains_key(&window_index) {
475 return false;
476 }
477
478 let entry = self
479 .winlink_alert_flags
480 .entry(window_index)
481 .or_insert_with(AlertFlags::empty);
482 let changed = !entry.contains(flags);
483 entry.insert(flags);
484 changed
485 }
486
487 pub fn clear_winlink_alert_flags(&mut self, window_index: u32, flags: AlertFlags) -> bool {
489 let Some(entry) = self.winlink_alert_flags.get_mut(&window_index) else {
490 return false;
491 };
492 if !entry.intersects(flags) {
493 return false;
494 }
495
496 entry.remove(flags);
497 true
498 }
499
500 pub fn clear_all_winlink_alert_flags(&mut self, window_index: u32) -> bool {
502 self.clear_winlink_alert_flags(window_index, WINLINK_ALERTFLAGS)
503 }
504
505 #[must_use]
507 pub const fn active_window_index(&self) -> u32 {
508 self.active_window
509 }
510
511 #[must_use]
513 pub const fn last_window_index(&self) -> Option<u32> {
514 self.last_window
515 }
516
517 #[must_use]
519 pub fn active_pane_index(&self) -> u32 {
520 self.window().active_pane_index()
521 }
522
523 #[must_use]
525 pub fn active_pane_id(&self) -> Option<PaneId> {
526 self.active_pane().map(Pane::id)
527 }
528
529 #[must_use]
531 pub fn active_pane(&self) -> Option<&Pane> {
532 self.window().active_pane()
533 }
534
535 #[must_use]
537 pub fn pane_id(&self, pane_index: u32) -> Option<PaneId> {
538 self.pane_id_in_window(self.active_window, pane_index)
539 }
540
541 #[must_use]
543 pub fn pane_id_in_window(&self, window_index: u32, pane_index: u32) -> Option<PaneId> {
544 self.window_at(window_index)
545 .and_then(|window| window.pane_id(pane_index))
546 }
547}