Skip to main content

rmux_core/session/
accessors.rs

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    // Reserve every surviving exact slot before falling back by WindowId. In
53    // particular, a removed duplicate alias must not steal the surviving
54    // alias's index merely because both aliases share one WindowId.
55    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    /// Returns the stable validated session name.
103    #[must_use]
104    pub const fn name(&self) -> &SessionName {
105        &self.name
106    }
107
108    /// Returns the named session group when the session is grouped.
109    #[must_use]
110    pub const fn group_name(&self) -> Option<&SessionName> {
111        self.group_name.as_ref()
112    }
113
114    /// Returns the store-assigned session identity used by `$N` targets.
115    #[must_use]
116    pub const fn id(&self) -> SessionId {
117        self.id
118    }
119
120    /// Returns the session creation timestamp as Unix seconds.
121    #[must_use]
122    pub const fn created_at(&self) -> i64 {
123        self.created_at
124    }
125
126    /// Returns the last session activity timestamp as Unix seconds.
127    #[must_use]
128    pub const fn activity_at(&self) -> i64 {
129        self.activity_at
130    }
131
132    /// Returns the last attached timestamp as Unix seconds.
133    #[must_use]
134    pub const fn last_attached_at(&self) -> Option<i64> {
135        self.last_attached_at
136    }
137
138    /// Returns the session working directory when one has been assigned.
139    #[must_use]
140    pub fn cwd(&self) -> Option<&Path> {
141        self.cwd.as_deref()
142    }
143
144    /// Returns the terminal size last applied to the session as a whole.
145    #[must_use]
146    pub const fn terminal_size(&self) -> TerminalSize {
147        self.terminal_size
148    }
149
150    pub(crate) fn set_id(&mut self, id: SessionId) {
151        self.id = id;
152    }
153
154    pub(crate) fn rebind_window_id_allocator(&mut self, allocator: WindowIdAllocator) {
155        let next_after_windows = self
156            .windows
157            .values()
158            .map(|window| window.id().as_u32().saturating_add(1))
159            .max()
160            .unwrap_or_else(|| allocator.peek());
161        self.next_window_id = allocator;
162        self.next_window_id.bump_to(next_after_windows);
163    }
164
165    /// Renames the session without rewriting any other session state.
166    pub fn rename(&mut self, new_name: SessionName) {
167        self.name = new_name;
168    }
169
170    /// Assigns or clears the session group name without mutating any other session state.
171    pub fn set_group_name(&mut self, group_name: Option<SessionName>) {
172        self.group_name = group_name;
173    }
174
175    /// Updates the session working directory.
176    pub fn set_cwd(&mut self, cwd: Option<PathBuf>) {
177        self.cwd = cwd;
178    }
179
180    /// Records that a client attached to the session at the current time.
181    pub fn touch_attached(&mut self) {
182        let now = current_unix_timestamp();
183        self.activity_at = now;
184        self.last_attached_at = Some(now);
185    }
186
187    /// Clones the session as a grouped peer with a fresh identity and timestamps.
188    #[must_use]
189    pub fn clone_as_group_member(
190        &self,
191        name: SessionName,
192        group_name: SessionName,
193        session_id: SessionId,
194    ) -> Self {
195        let now = current_unix_timestamp();
196        let mut cloned = self.clone();
197        cloned.id = session_id;
198        cloned.name = name;
199        cloned.group_name = Some(group_name);
200        cloned.created_at = now;
201        cloned.activity_at = now;
202        cloned.last_attached_at = None;
203        cloned
204    }
205
206    /// Synchronizes shared grouped-session window state from the source session while preserving the local current window when possible.
207    pub fn synchronize_group_from(&mut self, source: &Session) {
208        self.synchronize_group_from_with_optional_winlink_alert_map(source, None);
209    }
210
211    /// Synchronizes grouped windows by identity while applying an authoritative
212    /// permutation to peer-local winlink alert flags.
213    pub fn synchronize_group_from_with_winlink_alert_map(
214        &mut self,
215        source: &Session,
216        winlink_alert_map: &BTreeMap<u32, u32>,
217    ) {
218        self.synchronize_group_from_with_optional_winlink_alert_map(
219            source,
220            Some(winlink_alert_map),
221        );
222    }
223
224    fn synchronize_group_from_with_optional_winlink_alert_map(
225        &mut self,
226        source: &Session,
227        winlink_alert_map: Option<&BTreeMap<u32, u32>>,
228    ) {
229        debug_assert_ne!(self.name, source.name);
230        let previous_active = self.active_window;
231        let previous_last = self.last_window;
232        let previous_active_window_id = self.window_at(previous_active).map(Window::id);
233        let previous_last_window_id = previous_last
234            .and_then(|window_index| self.window_at(window_index))
235            .map(Window::id);
236
237        let synchronized_active = previous_active_window_id
238            .and_then(|window_id| window_index_for_id(&source.windows, window_id, previous_active))
239            .unwrap_or_else(|| {
240                synchronized_active_window(&source.windows, previous_active, previous_last)
241            });
242        let synchronized_last = previous_last_window_id
243            .and_then(|window_id| {
244                window_index_for_id(
245                    &source.windows,
246                    window_id,
247                    previous_last.expect("last window id requires an index"),
248                )
249            })
250            .filter(|window_index| *window_index != synchronized_active)
251            .or_else(|| {
252                (previous_active != synchronized_active
253                    && source.windows.contains_key(&previous_active))
254                .then_some(previous_active)
255            });
256        self.synchronize_group_windows(
257            source,
258            synchronized_active,
259            synchronized_last,
260            winlink_alert_map,
261        );
262    }
263
264    /// Synchronizes grouped window state while remapping selected source slots explicitly.
265    pub fn synchronize_group_from_with_window_selection_map(
266        &mut self,
267        source: &Session,
268        index_map: &BTreeMap<u32, u32>,
269    ) {
270        self.synchronize_group_from_with_window_selection_and_winlink_alert_maps(
271            source, index_map, index_map,
272        );
273    }
274
275    /// Synchronizes grouped windows with independent maps for peer selection
276    /// and peer-local winlink alert flags.
277    pub fn synchronize_group_from_with_window_selection_and_winlink_alert_maps(
278        &mut self,
279        source: &Session,
280        window_selection_map: &BTreeMap<u32, u32>,
281        winlink_alert_map: &BTreeMap<u32, u32>,
282    ) {
283        debug_assert_ne!(self.name, source.name);
284        let previous_active = self.active_window;
285        let previous_last = self.last_window;
286        let synchronized_active = window_selection_map
287            .get(&previous_active)
288            .copied()
289            .unwrap_or(previous_active);
290        let synchronized_active = if source.windows.contains_key(&synchronized_active) {
291            synchronized_active
292        } else {
293            synchronized_active_window(&source.windows, previous_active, previous_last)
294        };
295        let synchronized_last = previous_last
296            .map(|window_index| {
297                window_selection_map
298                    .get(&window_index)
299                    .copied()
300                    .unwrap_or(window_index)
301            })
302            .filter(|window_index| {
303                *window_index != synchronized_active && source.windows.contains_key(window_index)
304            });
305        self.synchronize_group_windows(
306            source,
307            synchronized_active,
308            synchronized_last,
309            Some(winlink_alert_map),
310        );
311    }
312
313    fn synchronize_group_windows(
314        &mut self,
315        source: &Session,
316        active_window: u32,
317        last_window: Option<u32>,
318        winlink_alert_map: Option<&BTreeMap<u32, u32>>,
319    ) {
320        let synchronized_alert_flags = remap_local_winlink_alert_flags(
321            &self.windows,
322            &self.winlink_alert_flags,
323            &source.windows,
324            &source.winlink_alert_flags,
325            winlink_alert_map,
326        );
327        self.windows = source.windows.clone();
328        self.winlink_alert_flags = synchronized_alert_flags;
329        self.next_pane_id = source.next_pane_id;
330        self.cwd = source.cwd.clone();
331        self.active_window = active_window;
332        self.last_window = last_window;
333    }
334
335    /// Returns the session's active window.
336    #[must_use]
337    pub fn window(&self) -> &Window {
338        self.window_at(self.active_window)
339            .expect("active session window must exist")
340    }
341
342    /// Returns the explicitly addressed window when it exists.
343    #[must_use]
344    pub fn window_at(&self, window_index: u32) -> Option<&Window> {
345        self.windows.get(&window_index)
346    }
347
348    /// Returns all windows keyed by window index.
349    #[must_use]
350    pub const fn windows(&self) -> &BTreeMap<u32, Window> {
351        &self.windows
352    }
353
354    pub(crate) fn window_mut(&mut self) -> &mut Window {
355        self.window_at_mut(self.active_window)
356            .expect("active session window must exist")
357    }
358
359    /// Returns the addressed window as a mutable reference when it exists.
360    pub fn window_at_mut(&mut self, window_index: u32) -> Option<&mut Window> {
361        self.windows.get_mut(&window_index)
362    }
363
364    /// Returns the persistent alert flags for the addressed window slot.
365    #[must_use]
366    pub fn winlink_alert_flags(&self, window_index: u32) -> AlertFlags {
367        self.winlink_alert_flags
368            .get(&window_index)
369            .copied()
370            .unwrap_or_else(AlertFlags::empty)
371    }
372
373    /// Returns the combined session alert flags across all alerted windows.
374    #[must_use]
375    pub fn session_alert_flags(&self) -> AlertFlags {
376        self.winlink_alert_flags
377            .values()
378            .copied()
379            .fold(AlertFlags::empty(), |flags, winlink_flags| {
380                flags.union(winlink_flags)
381            })
382    }
383
384    /// Returns the alerted window indexes in display order.
385    #[must_use]
386    pub fn alerted_window_indexes(&self) -> Vec<u32> {
387        self.winlink_alert_flags
388            .iter()
389            .filter_map(|(window_index, flags)| {
390                flags
391                    .intersects(WINLINK_ALERTFLAGS)
392                    .then_some(*window_index)
393            })
394            .collect()
395    }
396
397    /// Returns whether any window in the session currently carries an alert.
398    #[must_use]
399    pub fn has_alerts(&self) -> bool {
400        self.winlink_alert_flags
401            .values()
402            .any(|flags| flags.intersects(WINLINK_ALERTFLAGS))
403    }
404
405    /// Adds persistent alert flags to the addressed session winlink.
406    pub fn add_winlink_alert_flags(&mut self, window_index: u32, flags: AlertFlags) -> bool {
407        if !self.windows.contains_key(&window_index) {
408            return false;
409        }
410
411        let entry = self
412            .winlink_alert_flags
413            .entry(window_index)
414            .or_insert_with(AlertFlags::empty);
415        let changed = !entry.contains(flags);
416        entry.insert(flags);
417        changed
418    }
419
420    /// Clears the selected persistent alert flags from the addressed session winlink.
421    pub fn clear_winlink_alert_flags(&mut self, window_index: u32, flags: AlertFlags) -> bool {
422        let Some(entry) = self.winlink_alert_flags.get_mut(&window_index) else {
423            return false;
424        };
425        if !entry.intersects(flags) {
426            return false;
427        }
428
429        entry.remove(flags);
430        true
431    }
432
433    /// Clears all persistent alert flags from the addressed session winlink.
434    pub fn clear_all_winlink_alert_flags(&mut self, window_index: u32) -> bool {
435        self.clear_winlink_alert_flags(window_index, WINLINK_ALERTFLAGS)
436    }
437
438    /// Returns the active window index.
439    #[must_use]
440    pub const fn active_window_index(&self) -> u32 {
441        self.active_window
442    }
443
444    /// Returns the previously active window index when one exists.
445    #[must_use]
446    pub const fn last_window_index(&self) -> Option<u32> {
447        self.last_window
448    }
449
450    /// Returns the active pane index owned by the session.
451    #[must_use]
452    pub fn active_pane_index(&self) -> u32 {
453        self.window().active_pane_index()
454    }
455
456    /// Returns the stable internal identity for the active pane.
457    #[must_use]
458    pub fn active_pane_id(&self) -> Option<PaneId> {
459        self.active_pane().map(Pane::id)
460    }
461
462    /// Returns the active pane when the session invariant is satisfied.
463    #[must_use]
464    pub fn active_pane(&self) -> Option<&Pane> {
465        self.window().active_pane()
466    }
467
468    /// Returns the stable internal identity for a pane in the active window.
469    #[must_use]
470    pub fn pane_id(&self, pane_index: u32) -> Option<PaneId> {
471        self.pane_id_in_window(self.active_window, pane_index)
472    }
473
474    /// Returns the stable internal identity for a pane in the addressed window.
475    #[must_use]
476    pub fn pane_id_in_window(&self, window_index: u32, pane_index: u32) -> Option<PaneId> {
477        self.window_at(window_index)
478            .and_then(|window| window.pane_id(pane_index))
479    }
480}