Skip to main content

rmux_sdk/
info.rs

1//! Inert session/window/pane info-snapshot DTOs for SDK consumers.
2//!
3//! The types in this module describe the *sticky* v1 metadata and process
4//! state the daemon retains for every session, window, and pane. They are
5//! pure DTOs: the SDK does not call into `rmux-core`, `rmux-server`, or
6//! `rmux-pty` from this module, and it does not poll, subscribe, or
7//! reconcile state. Consumers receive an [`InfoSnapshot`] from a
8//! daemon-backed handle and read it as captured.
9//!
10//! Identity newtypes (`SessionName`, `SessionId`, `WindowId`, `PaneId`) are
11//! re-exported from `rmux-proto` via [`crate::types`] so SDK users never
12//! depend on `rmux-core`, `rmux-server`, `rmux-client`, or `rmux-pty` to
13//! describe an info snapshot.
14//!
15//! Pane metadata in this module deliberately omits any `env` /
16//! `environment` field: per-pane process environment is not part of the
17//! sticky info surface and is never exposed to public SDK consumers. The
18//! [`PaneInfo`] vocabulary therefore covers `command`, `working_directory`,
19//! `tags`, `size`, `process` state, `generation`, `revision`,
20//! `output_sequence`, and `exit_state` — but not `env`.
21//!
22//! ## Lag recovery via `info()`
23//!
24//! The daemon-backed SDK handle exposes a synchronous `info()` accessor (or
25//! its async equivalent on the asynchronous handle) that re-reads the
26//! sticky metadata and returns a fresh [`InfoSnapshot`]. This call is the
27//! canonical *lag-recovery* path after one of the following:
28//!
29//! * a [`PaneEvent::Lag`](crate::PaneEvent::Lag) signal indicating the
30//!   per-pane broadcast channel skipped frames; or
31//! * a [`PaneEvent::Disconnect`](crate::PaneEvent::Disconnect) carrying
32//!   [`PaneDisconnectReason::TooFarBehind`](crate::PaneDisconnectReason::TooFarBehind); or
33//! * any other transport recovery that re-establishes a control-mode
34//!   subscription after frames were dropped.
35//!
36//! `info()` refreshes:
37//!
38//! * the sticky session/window/pane metadata (names, working directory,
39//!   tags, dimensions, generations, and revisions);
40//! * the sticky pane process state, including the recorded
41//!   [`PaneProcessState`] and any captured [`PaneExitState`] for panes that
42//!   have already exited;
43//! * the latest output-sequence cursor the daemon has assigned to each
44//!   pane, so a subscriber can re-anchor to the live stream.
45//!
46//! `info()` does **not** reconstruct raw pane output bytes. Pane output is
47//! retained only inside the daemon's bounded scrollback ring; bytes that
48//! were dropped past the retained ring before `info()` was called are gone
49//! from the daemon's perspective and cannot be recovered. Consumers that
50//! must observe an exact byte-for-byte transcript should treat `info()` as
51//! a re-anchor for *future* output rather than a backfill of dropped bytes.
52//!
53//! ## Sparse / default decoding
54//!
55//! Every metadata or state field on these DTOs uses `#[serde(default)]`,
56//! and [`InfoSnapshot`] itself defaults to an empty bundle. This makes the
57//! DTOs forward-compatible: a producer that elides optional fields, or a
58//! consumer that decodes a snapshot written by a newer daemon, still
59//! produces a usable value with deterministic zero-valued defaults rather
60//! than a hard parse error. The required fields are limited to the
61//! identity newtypes (`id`, `name`, `session_id`, `window_id`), which carry
62//! no sensible default and must be supplied by every producer.
63
64use serde::{Deserialize, Serialize};
65
66use crate::types::{PaneId, SessionId, SessionName, TerminalSizeSpec, WindowId};
67
68/// Sticky metadata and counters captured for one daemon session.
69///
70/// `attached_clients` is the count of currently attached detached-RPC
71/// clients at the moment the snapshot was assembled — it is *not* a
72/// monotonic counter and may decrease as clients disconnect.
73#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
74pub struct SessionInfo {
75    /// Stable per-server session identity (`$N`).
76    pub id: SessionId,
77    /// Validated session name in canonical sanitized form.
78    pub name: SessionName,
79    /// Tmux format-expanded working directory at session-start time, when
80    /// the daemon recorded one.
81    #[serde(default)]
82    pub working_directory: Option<String>,
83    /// Smallest attached-client geometry the session has agreed on.
84    #[serde(default)]
85    pub size: TerminalSizeSpec,
86    /// Sticky session-scoped tag labels.
87    #[serde(default)]
88    pub tags: Vec<String>,
89    /// Monotonic session-state generation counter incremented on every
90    /// observed mutation.
91    #[serde(default)]
92    pub generation: u64,
93    /// Coarser revision counter incremented on layout-affecting mutations
94    /// such as window list or active-window changes.
95    #[serde(default)]
96    pub revision: u64,
97    /// Number of currently attached detached-RPC clients.
98    #[serde(default)]
99    pub attached_clients: u32,
100}
101
102impl SessionInfo {
103    /// Creates a sticky session info snapshot with default optional fields.
104    #[must_use]
105    pub fn new(id: SessionId, name: SessionName) -> Self {
106        Self {
107            id,
108            name,
109            working_directory: None,
110            size: TerminalSizeSpec::default(),
111            tags: Vec::new(),
112            generation: 0,
113            revision: 0,
114            attached_clients: 0,
115        }
116    }
117}
118
119/// Sticky metadata and counters captured for one daemon window.
120#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
121pub struct WindowInfo {
122    /// Stable per-server window identity (`@N`).
123    pub id: WindowId,
124    /// Owning session identity (`$N`).
125    pub session_id: SessionId,
126    /// Window index inside its session.
127    #[serde(default)]
128    pub index: u32,
129    /// Window name, when the user or a `rename-window` invocation set one.
130    #[serde(default)]
131    pub name: Option<String>,
132    /// Window geometry as last reported by the daemon.
133    #[serde(default)]
134    pub size: TerminalSizeSpec,
135    /// Sticky window-scoped tag labels.
136    #[serde(default)]
137    pub tags: Vec<String>,
138    /// Monotonic window-state generation counter.
139    #[serde(default)]
140    pub generation: u64,
141    /// Coarser revision counter incremented on layout-affecting mutations
142    /// such as pane list or active-pane changes.
143    #[serde(default)]
144    pub revision: u64,
145}
146
147impl WindowInfo {
148    /// Creates a sticky window info snapshot with default optional fields.
149    #[must_use]
150    pub fn new(id: WindowId, session_id: SessionId) -> Self {
151        Self {
152            id,
153            session_id,
154            index: 0,
155            name: None,
156            size: TerminalSizeSpec::default(),
157            tags: Vec::new(),
158            generation: 0,
159            revision: 0,
160        }
161    }
162}
163
164/// Sticky metadata, process state, and counters captured for one pane.
165///
166/// `PaneInfo` deliberately has no `env` or `environment` field. The
167/// daemon-backed SDK never exposes the spawned process environment via
168/// info snapshots; the omission is part of the public SDK contract.
169#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
170pub struct PaneInfo {
171    /// Stable per-server pane identity (`%N`).
172    pub id: PaneId,
173    /// Owning window identity (`@N`).
174    pub window_id: WindowId,
175    /// Owning session identity (`$N`).
176    pub session_id: SessionId,
177    /// Pane index inside its window.
178    #[serde(default)]
179    pub index: u32,
180    /// Spawned process argv, when the daemon recorded it. Stored exactly as
181    /// supplied at spawn time — the SDK does not split shell text or
182    /// rewrite argv on its way through the wire.
183    #[serde(default)]
184    pub command: Option<Vec<String>>,
185    /// Process working directory at the moment of the snapshot, when the
186    /// daemon could resolve one.
187    #[serde(default)]
188    pub working_directory: Option<String>,
189    /// Sticky pane-scoped tag labels.
190    #[serde(default)]
191    pub tags: Vec<String>,
192    /// Pane geometry as last reported by the daemon.
193    #[serde(default)]
194    pub size: TerminalSizeSpec,
195    /// Sticky pane process state.
196    #[serde(default)]
197    pub process: PaneProcessState,
198    /// Monotonic pane-state generation counter.
199    #[serde(default)]
200    pub generation: u64,
201    /// Coarser revision counter incremented on visible-state mutations such
202    /// as resizes or grid clears.
203    #[serde(default)]
204    pub revision: u64,
205    /// Latest pane-output sequence number assigned by the daemon. Consumers
206    /// re-anchor to this value when subscribing again after a lag recovery.
207    #[serde(default)]
208    pub output_sequence: u64,
209    /// Captured exit details for panes whose process has already exited.
210    #[serde(default)]
211    pub exit_state: Option<PaneExitState>,
212}
213
214impl PaneInfo {
215    /// Creates a sticky pane info snapshot with default optional fields.
216    #[must_use]
217    pub fn new(id: PaneId, window_id: WindowId, session_id: SessionId) -> Self {
218        Self {
219            id,
220            window_id,
221            session_id,
222            index: 0,
223            command: None,
224            working_directory: None,
225            tags: Vec::new(),
226            size: TerminalSizeSpec::default(),
227            process: PaneProcessState::default(),
228            generation: 0,
229            revision: 0,
230            output_sequence: 0,
231            exit_state: None,
232        }
233    }
234}
235
236/// Sticky process-state vocabulary for a captured pane.
237///
238/// Marked `#[non_exhaustive]` because more granular states (such as a
239/// dedicated *paused* or *zombie* indicator) may be added without breaking
240/// downstream pattern matches. Externally tagged for serde, so the encoded
241/// form round-trips through both `serde_json` and `bincode`.
242#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
243#[serde(rename_all = "kebab-case")]
244#[non_exhaustive]
245pub enum PaneProcessState {
246    /// State has not yet been observed for this pane (mid-recovery
247    /// snapshots default to this value).
248    #[default]
249    Unknown,
250    /// PTY child is still running. `pid` is set when the daemon could
251    /// surface the OS process identifier for the child; for platforms or
252    /// configurations where the pid is unavailable the field stays `None`
253    /// rather than an arbitrary sentinel.
254    Running {
255        /// OS process identifier for the running child, when known.
256        #[serde(default)]
257        pid: Option<u32>,
258    },
259    /// PTY child has exited. Detailed exit information is recorded in
260    /// [`PaneInfo::exit_state`].
261    Exited,
262}
263
264/// Captured exit details for an already-terminated pane process.
265///
266/// All fields are optional: a clean exit reports `code` only, a
267/// signal-driven exit reports `signal`, and a daemon-supplied human
268/// message can be carried in `message` for surfaces such as
269/// `remain-on-exit` overlays.
270#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
271pub struct PaneExitState {
272    /// Numeric exit code, when the process exited normally.
273    #[serde(default)]
274    pub code: Option<i32>,
275    /// Numeric signal value, when the process was terminated by a signal.
276    #[serde(default)]
277    pub signal: Option<i32>,
278    /// Optional daemon-supplied human-readable exit message.
279    #[serde(default)]
280    pub message: Option<String>,
281}
282
283impl PaneExitState {
284    /// Creates an exit state describing a clean normal exit.
285    #[must_use]
286    pub fn from_code(code: i32) -> Self {
287        Self {
288            code: Some(code),
289            signal: None,
290            message: None,
291        }
292    }
293
294    /// Creates an exit state describing a signal-driven termination.
295    #[must_use]
296    pub fn from_signal(signal: i32) -> Self {
297        Self {
298            code: None,
299            signal: Some(signal),
300            message: None,
301        }
302    }
303}
304
305/// Aggregate sticky info snapshot returned by the daemon-backed handle's
306/// `info()` accessor.
307///
308/// Producers populate the three vectors with the daemon's currently
309/// retained sessions, windows, and panes. The vectors are not
310/// guaranteed to be sorted by identity, but they preserve the daemon's
311/// insertion order so consumers that compare consecutive snapshots see a
312/// stable ordering for unchanged entries.
313///
314/// Consumers should treat [`InfoSnapshot`] as the *re-anchor point* after
315/// lag recovery: refresh local sticky caches, re-bind subscriptions on the
316/// returned `output_sequence` cursors, and accept that any pane bytes
317/// dropped from the retained ring before this snapshot was taken cannot be
318/// reconstructed.
319#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
320pub struct InfoSnapshot {
321    /// Sticky sessions known to the daemon at snapshot time.
322    #[serde(default)]
323    pub sessions: Vec<SessionInfo>,
324    /// Sticky windows known to the daemon at snapshot time.
325    #[serde(default)]
326    pub windows: Vec<WindowInfo>,
327    /// Sticky panes known to the daemon at snapshot time.
328    #[serde(default)]
329    pub panes: Vec<PaneInfo>,
330}
331
332impl InfoSnapshot {
333    /// Creates an info snapshot from explicit session, window, and pane
334    /// vectors.
335    #[must_use]
336    pub fn new(sessions: Vec<SessionInfo>, windows: Vec<WindowInfo>, panes: Vec<PaneInfo>) -> Self {
337        Self {
338            sessions,
339            windows,
340            panes,
341        }
342    }
343
344    /// Returns the recorded info entry for `session_id`, when present.
345    #[must_use]
346    pub fn session(&self, session_id: SessionId) -> Option<&SessionInfo> {
347        self.sessions.iter().find(|info| info.id == session_id)
348    }
349
350    /// Returns the recorded info entry for `window_id`, when present.
351    #[must_use]
352    pub fn window(&self, window_id: WindowId) -> Option<&WindowInfo> {
353        self.windows.iter().find(|info| info.id == window_id)
354    }
355
356    /// Returns the recorded info entry for `pane_id`, when present.
357    #[must_use]
358    pub fn pane(&self, pane_id: PaneId) -> Option<&PaneInfo> {
359        self.panes.iter().find(|info| info.id == pane_id)
360    }
361}