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}