Skip to main content

rmux_proto/
types.rs

1//! Shared protocol value types.
2//!
3//! Identity newtypes (`SessionName`, `SessionId`, `WindowId`, `PaneId`)
4//! are defined exactly once in [`crate::identity`]; this module
5//! re-exports `SessionName` so legacy import paths continue to resolve.
6
7use std::fmt;
8use std::str::FromStr;
9
10use serde::{Deserialize, Serialize};
11
12pub use crate::identity::SessionName;
13use crate::RmuxError;
14pub use rmux_types::TerminalSize;
15
16#[path = "types/hooks.rs"]
17mod hooks;
18#[path = "types/options.rs"]
19mod options;
20
21pub use hooks::{HookLifecycle, HookName};
22pub use options::{OptionName, SetOptionMode};
23
24/// Stable identifier for one pane-output subscription on a live server
25/// connection.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
27#[serde(transparent)]
28pub struct PaneOutputSubscriptionId(u64);
29
30impl PaneOutputSubscriptionId {
31    /// Wraps a raw subscription identifier.
32    #[must_use]
33    pub const fn new(value: u64) -> Self {
34        Self(value)
35    }
36
37    /// Returns the raw subscription identifier.
38    #[must_use]
39    pub const fn as_u64(self) -> u64 {
40        self.0
41    }
42}
43
44/// Opaque owner token for daemon-backed SDK waits.
45///
46/// The SDK assigns one owner token to each transport connection and then
47/// allocates [`SdkWaitId`] values within that owner. The server treats the
48/// owner as an opaque cancellation key; actual connection teardown cleanup is
49/// still keyed by the server's private connection identity.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
51#[serde(transparent)]
52pub struct SdkWaitOwnerId(u64);
53
54impl SdkWaitOwnerId {
55    /// Wraps a raw SDK wait owner identifier.
56    #[must_use]
57    pub const fn new(value: u64) -> Self {
58        Self(value)
59    }
60
61    /// Returns the raw SDK wait owner identifier.
62    #[must_use]
63    pub const fn as_u64(self) -> u64 {
64        self.0
65    }
66}
67
68/// Stable identifier for one daemon-backed SDK wait under an
69/// [`SdkWaitOwnerId`].
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
71#[serde(transparent)]
72pub struct SdkWaitId(u64);
73
74impl SdkWaitId {
75    /// Wraps a raw SDK wait identifier.
76    #[must_use]
77    pub const fn new(value: u64) -> Self {
78        Self(value)
79    }
80
81    /// Returns the raw SDK wait identifier.
82    #[must_use]
83    pub const fn as_u64(self) -> u64 {
84        self.0
85    }
86}
87
88/// A parsed exact target.
89#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
90pub enum Target {
91    /// A session target in the form `session-name`.
92    Session(SessionName),
93    /// A window target in the form `session-name:window-index`.
94    Window(WindowTarget),
95    /// A pane target in the form `session-name:window-index.pane-index`.
96    Pane(PaneTarget),
97}
98
99impl Target {
100    /// Parses the exact detached target forms supported by the detached server.
101    pub fn parse(value: &str) -> Result<Self, RmuxError> {
102        if let Some((session_name, tail)) = value.split_once(':') {
103            let session_name = SessionName::new(session_name.to_owned())?;
104
105            if !tail.is_empty() && tail.chars().all(|character| character.is_ascii_digit()) {
106                let window_index = parse_window_index(value, tail)?;
107                return Ok(Self::Window(WindowTarget::with_window(
108                    session_name,
109                    window_index,
110                )));
111            }
112
113            if let Some((window_index, pane_index)) = tail.split_once('.') {
114                let window_index = parse_window_index(value, window_index)?;
115                let pane_index = parse_pane_index(value, pane_index)?;
116                return Ok(Self::Pane(PaneTarget::with_window(
117                    session_name,
118                    window_index,
119                    pane_index,
120                )));
121            }
122
123            return Err(RmuxError::invalid_target(
124                value,
125                "targets must match 'session', 'session:window', or 'session:window.pane'",
126            ));
127        }
128
129        Ok(Self::Session(SessionName::new(value.to_owned())?))
130    }
131
132    /// Returns the session name addressed by the target.
133    #[must_use]
134    pub fn session_name(&self) -> &SessionName {
135        match self {
136            Self::Session(session_name) => session_name,
137            Self::Window(target) => target.session_name(),
138            Self::Pane(target) => target.session_name(),
139        }
140    }
141}
142
143impl fmt::Display for Target {
144    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Self::Session(session_name) => session_name.fmt(formatter),
147            Self::Window(target) => target.fmt(formatter),
148            Self::Pane(target) => target.fmt(formatter),
149        }
150    }
151}
152
153impl FromStr for Target {
154    type Err = RmuxError;
155
156    fn from_str(value: &str) -> Result<Self, Self::Err> {
157        Self::parse(value)
158    }
159}
160
161/// A validated window target.
162#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
163pub struct WindowTarget {
164    session_name: SessionName,
165    window_index: u32,
166}
167
168impl WindowTarget {
169    /// Creates a V1-compatible window target for window `0`.
170    #[must_use]
171    pub const fn new(session_name: SessionName) -> Self {
172        Self::with_window(session_name, 0)
173    }
174
175    /// Creates a window target for the provided window index.
176    #[must_use]
177    pub const fn with_window(session_name: SessionName, window_index: u32) -> Self {
178        Self {
179            session_name,
180            window_index,
181        }
182    }
183
184    /// Returns the session name component.
185    #[must_use]
186    pub const fn session_name(&self) -> &SessionName {
187        &self.session_name
188    }
189
190    /// Returns the addressed window index.
191    #[must_use]
192    pub const fn window_index(&self) -> u32 {
193        self.window_index
194    }
195}
196
197impl fmt::Display for WindowTarget {
198    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
199        write!(formatter, "{}:{}", self.session_name, self.window_index)
200    }
201}
202
203/// A validated pane target.
204#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
205pub struct PaneTarget {
206    session_name: SessionName,
207    window_index: u32,
208    pane_index: u32,
209}
210
211impl PaneTarget {
212    /// Creates a V1-compatible pane target anchored to window `0`.
213    #[must_use]
214    pub const fn new(session_name: SessionName, pane_index: u32) -> Self {
215        Self::with_window(session_name, 0, pane_index)
216    }
217
218    /// Creates a pane target for the provided window and pane indices.
219    #[must_use]
220    pub const fn with_window(
221        session_name: SessionName,
222        window_index: u32,
223        pane_index: u32,
224    ) -> Self {
225        Self {
226            session_name,
227            window_index,
228            pane_index,
229        }
230    }
231
232    /// Returns the session name component.
233    #[must_use]
234    pub const fn session_name(&self) -> &SessionName {
235        &self.session_name
236    }
237
238    /// Returns the addressed window index.
239    #[must_use]
240    pub const fn window_index(&self) -> u32 {
241        self.window_index
242    }
243
244    /// Returns the pane index component.
245    #[must_use]
246    pub const fn pane_index(&self) -> u32 {
247        self.pane_index
248    }
249}
250
251impl fmt::Display for PaneTarget {
252    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
253        write!(
254            formatter,
255            "{}:{}.{}",
256            self.session_name, self.window_index, self.pane_index
257        )
258    }
259}
260
261/// A global-or-session selector used by detached mutations.
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
263pub enum ScopeSelector {
264    /// Global scope.
265    Global,
266    /// Session-local scope.
267    Session(SessionName),
268    /// Window-local scope.
269    Window(WindowTarget),
270    /// Pane-local scope.
271    Pane(PaneTarget),
272}
273
274/// Explicit option mutation scope for the open option model.
275#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
276pub enum OptionScopeSelector {
277    /// Server-global options.
278    ServerGlobal,
279    /// Session-global options.
280    SessionGlobal,
281    /// Window-global options.
282    WindowGlobal,
283    /// Session-local options.
284    Session(SessionName),
285    /// Window-local options.
286    Window(WindowTarget),
287    /// Pane-local options.
288    Pane(PaneTarget),
289}
290
291/// The detached layout name subset.
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
293pub enum LayoutName {
294    /// The required `main-vertical` layout.
295    MainVertical,
296    /// Internal `split-window -h` geometry using a top main pane.
297    MainHorizontal,
298    /// The tmux `even-horizontal` left-to-right layout.
299    EvenHorizontal,
300    /// The tmux `even-vertical` top-to-bottom layout.
301    EvenVertical,
302    /// The tmux `tiled` grid layout.
303    Tiled,
304    /// The tmux `main-horizontal-mirrored` layout.
305    MainHorizontalMirrored,
306    /// The tmux `main-vertical-mirrored` layout.
307    MainVerticalMirrored,
308}
309
310impl fmt::Display for LayoutName {
311    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
312        match self {
313            Self::MainVertical => formatter.write_str("main-vertical"),
314            Self::MainHorizontal => formatter.write_str("main-horizontal"),
315            Self::EvenHorizontal => formatter.write_str("even-horizontal"),
316            Self::EvenVertical => formatter.write_str("even-vertical"),
317            Self::Tiled => formatter.write_str("tiled"),
318            Self::MainHorizontalMirrored => formatter.write_str("main-horizontal-mirrored"),
319            Self::MainVerticalMirrored => formatter.write_str("main-vertical-mirrored"),
320        }
321    }
322}
323
324impl FromStr for LayoutName {
325    type Err = RmuxError;
326
327    fn from_str(value: &str) -> Result<Self, Self::Err> {
328        match value {
329            "main-vertical" => Ok(Self::MainVertical),
330            "main-horizontal" => Ok(Self::MainHorizontal),
331            "even-horizontal" => Ok(Self::EvenHorizontal),
332            "even-vertical" => Ok(Self::EvenVertical),
333            "tiled" => Ok(Self::Tiled),
334            "main-horizontal-mirrored" => Ok(Self::MainHorizontalMirrored),
335            "main-vertical-mirrored" => Ok(Self::MainVerticalMirrored),
336            _ => Err(RmuxError::Server(format!("unknown layout: {value}"))),
337        }
338    }
339}
340
341/// Wire-level split orientation accepted by `split-window`.
342///
343/// The variant names follow tmux's flag convention (pane arrangement), not
344/// the divider-line convention: `Horizontal` means "panes arranged
345/// horizontally" (side by side), `Vertical` means "panes arranged
346/// vertically" (stacked). New SDK code should prefer
347/// [`rmux_sdk::SplitDirection`](https://docs.rs/rmux-sdk/latest/rmux_sdk/enum.SplitDirection.html)
348/// (`Right`/`Left`/`Up`/`Down`), which avoids this ambiguity.
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
350pub enum SplitDirection {
351    /// Stacked panes (top + bottom). Matches tmux `split-window -v`,
352    /// the tmux default when no flag is passed.
353    #[default]
354    Vertical,
355    /// Side-by-side panes (left + right). Matches tmux `split-window -h`.
356    Horizontal,
357}
358
359/// The detached resize semantics supported in V1.
360#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
361pub enum ResizePaneAdjustment {
362    /// Sets the absolute pane width in columns.
363    AbsoluteWidth {
364        /// The requested pane width in columns.
365        columns: u16,
366    },
367    /// Sets the absolute pane height in rows.
368    AbsoluteHeight {
369        /// The requested pane height in rows.
370        rows: u16,
371    },
372    /// Toggles zoom for the targeted pane's window.
373    Zoom,
374    /// Shrinks the pane height upward by a relative amount.
375    Up {
376        /// The requested row delta.
377        cells: u16,
378    },
379    /// Grows the pane height downward by a relative amount.
380    Down {
381        /// The requested row delta.
382        cells: u16,
383    },
384    /// Shrinks the pane width leftward by a relative amount.
385    Left {
386        /// The requested column delta.
387        cells: u16,
388    },
389    /// Grows the pane width rightward by a relative amount.
390    Right {
391        /// The requested column delta.
392        cells: u16,
393    },
394    /// Resolves the target and reports success without changing layout.
395    NoOp,
396}
397
398fn parse_pane_index(target: &str, pane_index: &str) -> Result<u32, RmuxError> {
399    if pane_index.is_empty() {
400        return Err(RmuxError::invalid_target(
401            target,
402            "pane index must be an unsigned integer",
403        ));
404    }
405
406    pane_index
407        .parse::<u32>()
408        .map_err(|_| RmuxError::invalid_target(target, "pane index must be an unsigned integer"))
409}
410
411fn parse_window_index(target: &str, window_index: &str) -> Result<u32, RmuxError> {
412    if window_index.is_empty() {
413        return Err(RmuxError::invalid_target(
414            target,
415            "window index must be an unsigned integer",
416        ));
417    }
418
419    window_index
420        .parse::<u32>()
421        .map_err(|_| RmuxError::invalid_target(target, "window index must be an unsigned integer"))
422}
423
424#[cfg(test)]
425mod tests;