Skip to main content

rmux_core/
pane.rs

1use std::time::{SystemTime, UNIX_EPOCH};
2
3use rmux_proto::{PaneTarget, SessionName};
4
5pub use rmux_proto::PaneId;
6
7/// A pane rectangle within terminal coordinates.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct PaneGeometry {
10    x: u16,
11    y: u16,
12    cols: u16,
13    rows: u16,
14}
15
16impl PaneGeometry {
17    /// Creates a pane rectangle at the given position and size.
18    #[must_use]
19    pub const fn new(x: u16, y: u16, cols: u16, rows: u16) -> Self {
20        Self { x, y, cols, rows }
21    }
22
23    /// Returns the left-most column of the pane.
24    #[must_use]
25    pub const fn x(&self) -> u16 {
26        self.x
27    }
28
29    /// Returns the top-most row of the pane.
30    #[must_use]
31    pub const fn y(&self) -> u16 {
32        self.y
33    }
34
35    /// Returns the pane width in columns.
36    #[must_use]
37    pub const fn cols(&self) -> u16 {
38        self.cols
39    }
40
41    /// Returns the pane height in rows.
42    #[must_use]
43    pub const fn rows(&self) -> u16 {
44        self.rows
45    }
46}
47
48/// Pure in-memory pane state.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Pane {
51    id: PaneId,
52    index: u32,
53    geometry: PaneGeometry,
54    active_point: u64,
55    created_at: i64,
56    activity_at: i64,
57}
58
59impl Pane {
60    /// Returns the pane's stable internal identity.
61    #[must_use]
62    pub const fn id(&self) -> PaneId {
63        self.id
64    }
65
66    /// Returns the stable pane index used by detached targets.
67    #[must_use]
68    pub const fn index(&self) -> u32 {
69        self.index
70    }
71
72    /// Returns the pane's current geometry.
73    #[must_use]
74    pub const fn geometry(&self) -> PaneGeometry {
75        self.geometry
76    }
77
78    /// Monotonic per-window activation counter (tmux `active_point`):
79    /// bumped each time this pane becomes the window's active pane.
80    pub const fn active_point(&self) -> u64 {
81        self.active_point
82    }
83
84    /// Returns the pane creation timestamp as Unix seconds.
85    #[must_use]
86    pub const fn created_at(&self) -> i64 {
87        self.created_at
88    }
89
90    /// Returns the last pane activity timestamp as Unix seconds.
91    #[must_use]
92    pub const fn activity_at(&self) -> i64 {
93        self.activity_at
94    }
95
96    pub(crate) fn touch_activity(&mut self) {
97        self.activity_at = current_unix_timestamp();
98    }
99
100    /// Builds an exact pane target for this pane in the given session.
101    #[must_use]
102    pub fn target(&self, session_name: &SessionName) -> PaneTarget {
103        PaneTarget::new(session_name.clone(), self.index)
104    }
105
106    #[cfg_attr(not(test), allow(dead_code))]
107    pub(crate) fn new(index: u32, geometry: PaneGeometry) -> Self {
108        Self::new_with_id(PaneId::new(index), index, geometry)
109    }
110
111    pub(crate) fn new_with_id(id: PaneId, index: u32, geometry: PaneGeometry) -> Self {
112        let now = current_unix_timestamp();
113        Self {
114            id,
115            index,
116            geometry,
117            active_point: 0,
118            created_at: now,
119            activity_at: now,
120        }
121    }
122
123    pub(crate) fn set_geometry(&mut self, geometry: PaneGeometry) {
124        self.geometry = geometry;
125    }
126
127    pub(crate) fn set_index(&mut self, index: u32) {
128        self.index = index;
129    }
130
131    pub(crate) fn set_active_point(&mut self, active_point: u64) {
132        self.active_point = active_point;
133    }
134}
135
136fn current_unix_timestamp() -> i64 {
137    SystemTime::now()
138        .duration_since(UNIX_EPOCH)
139        .map(|duration| duration.as_secs().min(i64::MAX as u64) as i64)
140        .unwrap_or(0)
141}
142
143#[cfg(test)]
144mod tests {
145    use super::{Pane, PaneGeometry, PaneId};
146    use rmux_proto::SessionName;
147
148    #[test]
149    fn geometry_accessors_match_constructor_values() {
150        let geometry = PaneGeometry::new(4, 7, 80, 24);
151
152        assert_eq!(geometry.x(), 4);
153        assert_eq!(geometry.y(), 7);
154        assert_eq!(geometry.cols(), 80);
155        assert_eq!(geometry.rows(), 24);
156    }
157
158    #[test]
159    fn pane_target_uses_session_name_and_index() {
160        let pane = Pane::new(3, PaneGeometry::new(0, 0, 10, 5));
161        let session_name = SessionName::new("alpha").expect("valid session name");
162
163        assert_eq!(pane.target(&session_name).to_string(), "alpha:0.3");
164    }
165
166    #[test]
167    fn pane_id_is_stable_and_independent_from_display_index() {
168        let pane = Pane::new_with_id(PaneId::new(9), 3, PaneGeometry::new(0, 0, 10, 5));
169
170        assert_eq!(pane.id(), PaneId::new(9));
171        assert_eq!(pane.index(), 3);
172    }
173
174    #[test]
175    fn set_geometry_replaces_the_existing_rectangle() {
176        let mut pane = Pane::new(0, PaneGeometry::new(0, 0, 10, 5));
177        let replacement = PaneGeometry::new(12, 1, 34, 50);
178
179        pane.set_geometry(replacement);
180
181        assert_eq!(pane.geometry(), replacement);
182    }
183}