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::{PaneId, 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/// Explicit process launch mode for daemon-owned pane processes.
25///
26/// This is distinct from the legacy `command: Option<Vec<String>>` fields on
27/// some request DTOs. Legacy command fields preserve tmux-compatible behavior
28/// where a single string runs through `$SHELL -c`; this enum records caller
29/// intent directly so SDK `spawn(argv)` can remain argv-based even for a
30/// single program name.
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[non_exhaustive]
33pub enum ProcessCommand {
34    /// Execute the program directly with the supplied argv vector.
35    Argv(Vec<String>),
36    /// Execute command text through the configured shell.
37    Shell(String),
38}
39
40impl ProcessCommand {
41    /// Converts a legacy command vector into the historical tmux-compatible
42    /// launch mode.
43    #[must_use]
44    pub fn from_legacy_command(command: Option<&[String]>) -> Option<Self> {
45        match command {
46            Some([single]) => Some(Self::Shell(single.clone())),
47            Some(argv) if !argv.is_empty() => Some(Self::Argv(argv.to_vec())),
48            _ => None,
49        }
50    }
51
52    /// Returns a redaction/display-friendly command vector.
53    ///
54    /// Shell commands are represented as a one-element vector to preserve the
55    /// existing `pane_start_command` encoding shape.
56    #[must_use]
57    pub fn display_command(&self) -> Vec<String> {
58        match self {
59            Self::Argv(argv) => argv.clone(),
60            Self::Shell(command) => vec![command.clone()],
61        }
62    }
63
64    /// Returns true when the command contains no executable work.
65    #[must_use]
66    pub fn is_empty(&self) -> bool {
67        match self {
68            Self::Argv(argv) => argv.is_empty() || argv.first().is_some_and(String::is_empty),
69            Self::Shell(command) => command.is_empty(),
70        }
71    }
72}
73
74/// Stable identifier for one pane-output subscription on a live server
75/// connection.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
77#[serde(transparent)]
78pub struct PaneOutputSubscriptionId(u64);
79
80impl PaneOutputSubscriptionId {
81    /// Wraps a raw subscription identifier.
82    #[must_use]
83    pub const fn new(value: u64) -> Self {
84        Self(value)
85    }
86
87    /// Returns the raw subscription identifier.
88    #[must_use]
89    pub const fn as_u64(self) -> u64 {
90        self.0
91    }
92}
93
94/// Opaque owner token for daemon-backed SDK waits.
95///
96/// The SDK assigns one owner token to each transport connection and then
97/// allocates [`SdkWaitId`] values within that owner. The server treats the
98/// owner as an opaque cancellation key; actual connection teardown cleanup is
99/// still keyed by the server's private connection identity.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
101#[serde(transparent)]
102pub struct SdkWaitOwnerId(u64);
103
104impl SdkWaitOwnerId {
105    /// Wraps a raw SDK wait owner identifier.
106    #[must_use]
107    pub const fn new(value: u64) -> Self {
108        Self(value)
109    }
110
111    /// Returns the raw SDK wait owner identifier.
112    #[must_use]
113    pub const fn as_u64(self) -> u64 {
114        self.0
115    }
116}
117
118/// Stable identifier for one daemon-backed SDK wait under an
119/// [`SdkWaitOwnerId`].
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
121#[serde(transparent)]
122pub struct SdkWaitId(u64);
123
124impl SdkWaitId {
125    /// Wraps a raw SDK wait identifier.
126    #[must_use]
127    pub const fn new(value: u64) -> Self {
128        Self(value)
129    }
130
131    /// Returns the raw SDK wait identifier.
132    #[must_use]
133    pub const fn as_u64(self) -> u64 {
134        self.0
135    }
136}
137
138/// A parsed exact target.
139#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
140pub enum Target {
141    /// A session target in the form `session-name`.
142    Session(SessionName),
143    /// A window target in the form `session-name:window-index`.
144    Window(WindowTarget),
145    /// A pane target in the form `session-name:window-index.pane-index`.
146    Pane(PaneTarget),
147}
148
149impl Target {
150    /// Parses the exact detached target forms supported by the detached server.
151    pub fn parse(value: &str) -> Result<Self, RmuxError> {
152        if let Some((session_name, tail)) = value.split_once(':') {
153            let session_name = SessionName::new(session_name.to_owned())?;
154
155            if !tail.is_empty() && tail.chars().all(|character| character.is_ascii_digit()) {
156                let window_index = parse_window_index(value, tail)?;
157                return Ok(Self::Window(WindowTarget::with_window(
158                    session_name,
159                    window_index,
160                )));
161            }
162
163            if let Some((window_index, pane_index)) = tail.split_once('.') {
164                let window_index = parse_window_index(value, window_index)?;
165                let pane_index = parse_pane_index(value, pane_index)?;
166                return Ok(Self::Pane(PaneTarget::with_window(
167                    session_name,
168                    window_index,
169                    pane_index,
170                )));
171            }
172
173            return Err(RmuxError::invalid_target(
174                value,
175                "targets must match 'session', 'session:window', or 'session:window.pane'",
176            ));
177        }
178
179        Ok(Self::Session(SessionName::new(value.to_owned())?))
180    }
181
182    /// Returns the session name addressed by the target.
183    #[must_use]
184    pub fn session_name(&self) -> &SessionName {
185        match self {
186            Self::Session(session_name) => session_name,
187            Self::Window(target) => target.session_name(),
188            Self::Pane(target) => target.session_name(),
189        }
190    }
191}
192
193impl fmt::Display for Target {
194    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
195        match self {
196            Self::Session(session_name) => session_name.fmt(formatter),
197            Self::Window(target) => target.fmt(formatter),
198            Self::Pane(target) => target.fmt(formatter),
199        }
200    }
201}
202
203impl FromStr for Target {
204    type Err = RmuxError;
205
206    fn from_str(value: &str) -> Result<Self, Self::Err> {
207        Self::parse(value)
208    }
209}
210
211/// A validated window target.
212#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
213pub struct WindowTarget {
214    session_name: SessionName,
215    window_index: u32,
216}
217
218impl WindowTarget {
219    /// Creates a V1-compatible window target for window `0`.
220    #[must_use]
221    pub const fn new(session_name: SessionName) -> Self {
222        Self::with_window(session_name, 0)
223    }
224
225    /// Creates a window target for the provided window index.
226    #[must_use]
227    pub const fn with_window(session_name: SessionName, window_index: u32) -> Self {
228        Self {
229            session_name,
230            window_index,
231        }
232    }
233
234    /// Returns the session name component.
235    #[must_use]
236    pub const fn session_name(&self) -> &SessionName {
237        &self.session_name
238    }
239
240    /// Returns the addressed window index.
241    #[must_use]
242    pub const fn window_index(&self) -> u32 {
243        self.window_index
244    }
245}
246
247impl fmt::Display for WindowTarget {
248    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
249        write!(formatter, "{}:{}", self.session_name, self.window_index)
250    }
251}
252
253/// A validated pane target.
254#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
255pub struct PaneTarget {
256    session_name: SessionName,
257    window_index: u32,
258    pane_index: u32,
259}
260
261impl PaneTarget {
262    /// Creates a V1-compatible pane target anchored to window `0`.
263    #[must_use]
264    pub const fn new(session_name: SessionName, pane_index: u32) -> Self {
265        Self::with_window(session_name, 0, pane_index)
266    }
267
268    /// Creates a pane target for the provided window and pane indices.
269    #[must_use]
270    pub const fn with_window(
271        session_name: SessionName,
272        window_index: u32,
273        pane_index: u32,
274    ) -> Self {
275        Self {
276            session_name,
277            window_index,
278            pane_index,
279        }
280    }
281
282    /// Returns the session name component.
283    #[must_use]
284    pub const fn session_name(&self) -> &SessionName {
285        &self.session_name
286    }
287
288    /// Returns the addressed window index.
289    #[must_use]
290    pub const fn window_index(&self) -> u32 {
291        self.window_index
292    }
293
294    /// Returns the pane index component.
295    #[must_use]
296    pub const fn pane_index(&self) -> u32 {
297        self.pane_index
298    }
299}
300
301impl fmt::Display for PaneTarget {
302    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
303        write!(
304            formatter,
305            "{}:{}.{}",
306            self.session_name, self.window_index, self.pane_index
307        )
308    }
309}
310
311/// Pane selector for SDK operations that can address either a display slot
312/// or a stable pane identity.
313#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
314pub enum PaneTargetRef {
315    /// Existing slot-based selector.
316    Slot(PaneTarget),
317    /// Stable pane id scoped by session name.
318    Id {
319        /// Exact session name component.
320        session_name: SessionName,
321        /// Stable pane identity within one daemon lifetime.
322        pane_id: PaneId,
323    },
324}
325
326impl PaneTargetRef {
327    /// Creates a selector for an existing slot target.
328    #[must_use]
329    pub const fn slot(target: PaneTarget) -> Self {
330        Self::Slot(target)
331    }
332
333    /// Creates a selector for a stable pane id in a session.
334    #[must_use]
335    pub const fn by_id(session_name: SessionName, pane_id: PaneId) -> Self {
336        Self::Id {
337            session_name,
338            pane_id,
339        }
340    }
341
342    /// Returns the session name component.
343    #[must_use]
344    pub const fn session_name(&self) -> &SessionName {
345        match self {
346            Self::Slot(target) => target.session_name(),
347            Self::Id { session_name, .. } => session_name,
348        }
349    }
350}
351
352impl From<PaneTarget> for PaneTargetRef {
353    fn from(value: PaneTarget) -> Self {
354        Self::Slot(value)
355    }
356}
357
358impl fmt::Display for PaneTargetRef {
359    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
360        match self {
361            Self::Slot(target) => target.fmt(formatter),
362            Self::Id {
363                session_name,
364                pane_id,
365            } => write!(formatter, "{session_name}:{pane_id}"),
366        }
367    }
368}
369
370/// A global-or-session selector used by detached mutations.
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372pub enum ScopeSelector {
373    /// Global scope.
374    Global,
375    /// Session-local scope.
376    Session(SessionName),
377    /// Window-local scope.
378    Window(WindowTarget),
379    /// Pane-local scope.
380    Pane(PaneTarget),
381}
382
383/// Explicit option mutation scope for the open option model.
384#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
385pub enum OptionScopeSelector {
386    /// Server-global options.
387    ServerGlobal,
388    /// Session-global options.
389    SessionGlobal,
390    /// Window-global options.
391    WindowGlobal,
392    /// Session-local options.
393    Session(SessionName),
394    /// Window-local options.
395    Window(WindowTarget),
396    /// Pane-local options.
397    Pane(PaneTarget),
398}
399
400/// The detached layout name subset.
401#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
402pub enum LayoutName {
403    /// The required `main-vertical` layout.
404    MainVertical,
405    /// Internal `split-window -h` geometry using a top main pane.
406    MainHorizontal,
407    /// The tmux `even-horizontal` left-to-right layout.
408    EvenHorizontal,
409    /// The tmux `even-vertical` top-to-bottom layout.
410    EvenVertical,
411    /// The tmux `tiled` grid layout.
412    Tiled,
413    /// The tmux `main-horizontal-mirrored` layout.
414    MainHorizontalMirrored,
415    /// The tmux `main-vertical-mirrored` layout.
416    MainVerticalMirrored,
417}
418
419impl fmt::Display for LayoutName {
420    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
421        match self {
422            Self::MainVertical => formatter.write_str("main-vertical"),
423            Self::MainHorizontal => formatter.write_str("main-horizontal"),
424            Self::EvenHorizontal => formatter.write_str("even-horizontal"),
425            Self::EvenVertical => formatter.write_str("even-vertical"),
426            Self::Tiled => formatter.write_str("tiled"),
427            Self::MainHorizontalMirrored => formatter.write_str("main-horizontal-mirrored"),
428            Self::MainVerticalMirrored => formatter.write_str("main-vertical-mirrored"),
429        }
430    }
431}
432
433impl FromStr for LayoutName {
434    type Err = RmuxError;
435
436    fn from_str(value: &str) -> Result<Self, Self::Err> {
437        match value {
438            "main-vertical" => Ok(Self::MainVertical),
439            "main-horizontal" => Ok(Self::MainHorizontal),
440            "even-horizontal" => Ok(Self::EvenHorizontal),
441            "even-vertical" => Ok(Self::EvenVertical),
442            "tiled" => Ok(Self::Tiled),
443            "main-horizontal-mirrored" => Ok(Self::MainHorizontalMirrored),
444            "main-vertical-mirrored" => Ok(Self::MainVerticalMirrored),
445            _ => Err(RmuxError::Server(format!("unknown layout: {value}"))),
446        }
447    }
448}
449
450/// Wire-level split orientation accepted by `split-window`.
451///
452/// The variant names follow tmux's flag convention (pane arrangement), not
453/// the divider-line convention: `Horizontal` means "panes arranged
454/// horizontally" (side by side), `Vertical` means "panes arranged
455/// vertically" (stacked). New SDK code should prefer
456/// [`rmux_sdk::SplitDirection`](https://docs.rs/rmux-sdk/latest/rmux_sdk/enum.SplitDirection.html)
457/// (`Right`/`Left`/`Up`/`Down`), which avoids this ambiguity.
458#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
459pub enum SplitDirection {
460    /// Stacked panes (top + bottom). Matches tmux `split-window -v`,
461    /// the tmux default when no flag is passed.
462    #[default]
463    Vertical,
464    /// Side-by-side panes (left + right). Matches tmux `split-window -h`.
465    Horizontal,
466}
467
468/// The detached resize semantics supported in V1.
469#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
470pub enum ResizePaneAdjustment {
471    /// Sets the absolute pane width in columns.
472    AbsoluteWidth {
473        /// The requested pane width in columns.
474        columns: u16,
475    },
476    /// Sets the absolute pane height in rows.
477    AbsoluteHeight {
478        /// The requested pane height in rows.
479        rows: u16,
480    },
481    /// Toggles zoom for the targeted pane's window.
482    Zoom,
483    /// Shrinks the pane height upward by a relative amount.
484    Up {
485        /// The requested row delta.
486        cells: u16,
487    },
488    /// Grows the pane height downward by a relative amount.
489    Down {
490        /// The requested row delta.
491        cells: u16,
492    },
493    /// Shrinks the pane width leftward by a relative amount.
494    Left {
495        /// The requested column delta.
496        cells: u16,
497    },
498    /// Grows the pane width rightward by a relative amount.
499    Right {
500        /// The requested column delta.
501        cells: u16,
502    },
503    /// Resolves the target and reports success without changing layout.
504    NoOp,
505}
506
507fn parse_pane_index(target: &str, pane_index: &str) -> Result<u32, RmuxError> {
508    if pane_index.is_empty() {
509        return Err(RmuxError::invalid_target(
510            target,
511            "pane index must be an unsigned integer",
512        ));
513    }
514
515    pane_index
516        .parse::<u32>()
517        .map_err(|_| RmuxError::invalid_target(target, "pane index must be an unsigned integer"))
518}
519
520fn parse_window_index(target: &str, window_index: &str) -> Result<u32, RmuxError> {
521    if window_index.is_empty() {
522        return Err(RmuxError::invalid_target(
523            target,
524            "window index must be an unsigned integer",
525        ));
526    }
527
528    window_index
529        .parse::<u32>()
530        .map_err(|_| RmuxError::invalid_target(target, "window index must be an unsigned integer"))
531}
532
533#[cfg(test)]
534mod tests;