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 internal total-order token behind `activity_at`.
139    ///
140    /// Targetless resolution ranks by this token so two sessions whose public
141    /// whole-second timestamps collide still have a defined order. It is not a
142    /// timestamp and never leaves the process.
143    #[doc(hidden)]
144    #[must_use]
145    pub const fn recency(&self) -> super::SessionRecency {
146        self.recency
147    }
148
149    /// Returns the session working directory when one has been assigned.
150    #[must_use]
151    pub fn cwd(&self) -> Option<&Path> {
152        self.cwd.as_deref()
153    }
154
155    /// Returns the terminal size last applied to the session as a whole.
156    #[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    /// Renames the session without rewriting any other session state.
177    pub fn rename(&mut self, new_name: SessionName) {
178        self.name = new_name;
179    }
180
181    /// Assigns or clears the session group name without mutating any other session state.
182    pub fn set_group_name(&mut self, group_name: Option<SessionName>) {
183        self.group_name = group_name;
184    }
185
186    /// Updates the session working directory.
187    pub fn set_cwd(&mut self, cwd: Option<PathBuf>) {
188        self.cwd = cwd;
189    }
190
191    /// Records that a client attached to the session at the current time.
192    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    /// Pins the public whole-second timestamps without disturbing the internal
200    /// recency order.
201    ///
202    /// Session times come from the wall clock, so no test can otherwise
203    /// guarantee that two sessions land in the same second — which is exactly
204    /// the condition targetless ranking has to survive. Regressions for it
205    /// would have to race the clock or retry; this makes them deterministic
206    /// instead.
207    ///
208    /// It is a test seam, never a runtime operation. `cfg(test)` alone cannot
209    /// reach the server crate's tests, so the `test-seams` feature carries it
210    /// there and nowhere else: it is off by default and only the dev-dependency
211    /// edge turns it on, so the published API never gains a way to rewrite a
212    /// session's public timestamps.
213    #[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    /// Records that an attached client interacted with the session, leaving
222    /// attach history untouched.
223    ///
224    /// Callers must invoke this only once per accepted interaction, at the
225    /// boundary where an attached client's input is admitted — not once per
226    /// pane write, and not for input the server rejects.
227    ///
228    /// This advances the *public* `activity_at`, not only the internal recency
229    /// token: `#{session_activity}` and the `list-sessions` payload report the
230    /// new second. tmux 3.7b does the same — it calls `session_update_activity`
231    /// while admitting a key, before deciding what the key means — so a client
232    /// typing into a session is expected to change that output.
233    pub fn touch_activity(&mut self) {
234        self.activity_at = current_unix_timestamp();
235        self.recency = super::SessionRecency::next();
236    }
237
238    /// Moves the session to its own position at the head of the recency order,
239    /// leaving every public timestamp untouched.
240    ///
241    /// `Session` is publicly cloneable, so a session handed back to a store can
242    /// carry a token a live session already holds. The order the readers rank by
243    /// has to stay total, so the store re-mints on collision rather than letting
244    /// two sessions share a position.
245    pub(super) fn renew_recency(&mut self) {
246        self.recency = super::SessionRecency::next();
247    }
248
249    /// Clones the session as a grouped peer with a fresh identity and timestamps.
250    #[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    /// Synchronizes shared grouped-session window state from the source session while preserving the local current window when possible.
274    pub fn synchronize_group_from(&mut self, source: &Session) {
275        self.synchronize_group_from_with_optional_winlink_alert_map(source, None);
276    }
277
278    /// Synchronizes grouped windows by identity while applying an authoritative
279    /// permutation to peer-local winlink alert flags.
280    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    /// Synchronizes grouped window state while remapping selected source slots explicitly.
332    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    /// Synchronizes grouped windows with independent maps for peer selection
343    /// and peer-local winlink alert flags.
344    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    /// Returns the session's active window.
403    #[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    /// Returns the explicitly addressed window when it exists.
410    #[must_use]
411    pub fn window_at(&self, window_index: u32) -> Option<&Window> {
412        self.windows.get(&window_index)
413    }
414
415    /// Returns all windows keyed by window index.
416    #[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    /// Returns the addressed window as a mutable reference when it exists.
427    pub fn window_at_mut(&mut self, window_index: u32) -> Option<&mut Window> {
428        self.windows.get_mut(&window_index)
429    }
430
431    /// Returns the persistent alert flags for the addressed window slot.
432    #[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    /// Returns the combined session alert flags across all alerted windows.
441    #[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    /// Returns the alerted window indexes in display order.
452    #[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    /// Returns whether any window in the session currently carries an alert.
465    #[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    /// Adds persistent alert flags to the addressed session winlink.
473    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    /// Clears the selected persistent alert flags from the addressed session winlink.
488    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    /// Clears all persistent alert flags from the addressed session winlink.
501    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    /// Returns the active window index.
506    #[must_use]
507    pub const fn active_window_index(&self) -> u32 {
508        self.active_window
509    }
510
511    /// Returns the previously active window index when one exists.
512    #[must_use]
513    pub const fn last_window_index(&self) -> Option<u32> {
514        self.last_window
515    }
516
517    /// Returns the active pane index owned by the session.
518    #[must_use]
519    pub fn active_pane_index(&self) -> u32 {
520        self.window().active_pane_index()
521    }
522
523    /// Returns the stable internal identity for the active pane.
524    #[must_use]
525    pub fn active_pane_id(&self) -> Option<PaneId> {
526        self.active_pane().map(Pane::id)
527    }
528
529    /// Returns the active pane when the session invariant is satisfied.
530    #[must_use]
531    pub fn active_pane(&self) -> Option<&Pane> {
532        self.window().active_pane()
533    }
534
535    /// Returns the stable internal identity for a pane in the active window.
536    #[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    /// Returns the stable internal identity for a pane in the addressed window.
542    #[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}