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