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