Skip to main content

rmux_sdk/
spec.rs

1//! Inert SDK command specification DTOs.
2//!
3//! The types in this module hold caller intent and map to `rmux-proto`
4//! request payloads. They do not open IPC streams, start daemons, inspect
5//! processes, probe endpoint paths, parse tmux command text, or resolve
6//! targets.
7
8use std::fmt;
9
10use serde::{Deserialize, Serialize};
11
12use crate::types::{PaneRef, TerminalSizeSpec};
13use crate::SessionName;
14
15/// Explicit process command mode used by SDK builders and specs.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[non_exhaustive]
18pub enum ProcessCommandSpec {
19    /// Execute a program directly with structured argv.
20    Argv(Vec<String>),
21    /// Execute command text through the configured shell.
22    Shell(String),
23}
24
25impl From<ProcessCommandSpec> for rmux_proto::ProcessCommand {
26    fn from(value: ProcessCommandSpec) -> Self {
27        match value {
28            ProcessCommandSpec::Argv(argv) => Self::Argv(argv),
29            ProcessCommandSpec::Shell(command) => Self::Shell(command),
30        }
31    }
32}
33
34impl ProcessCommandSpec {
35    pub(crate) fn is_empty(&self) -> bool {
36        match self {
37            Self::Argv(argv) => argv.is_empty() || argv.first().is_some_and(String::is_empty),
38            Self::Shell(command) => command.is_empty(),
39        }
40    }
41}
42
43/// Process-spawn fields shared by SDK command specs.
44///
45/// `process_command` is the explicit launch-mode path. The older `command`
46/// field is retained for low-level tmux-compatible specs where a single item
47/// means `$SHELL -c`; new SDK surfaces should use [`ProcessSpec::argv`],
48/// [`ProcessSpec::shell`], or the matching session/pane builders rather than
49/// struct literals.
50#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct ProcessSpec {
52    /// Legacy optional command vector. A single item keeps the historical
53    /// `$SHELL -c` behavior in protocol handlers.
54    #[serde(default)]
55    pub command: Option<Vec<String>>,
56    /// Explicit process launch mode.
57    #[serde(default)]
58    pub process_command: Option<ProcessCommandSpec>,
59    /// Optional per-spawn environment overrides in `NAME=VALUE` form.
60    #[serde(default)]
61    pub environment: Option<Vec<String>>,
62}
63
64impl ProcessSpec {
65    /// Creates a process spec that executes direct argv.
66    #[must_use]
67    pub fn argv<I, S>(command: I) -> Self
68    where
69        I: IntoIterator<Item = S>,
70        S: Into<String>,
71    {
72        Self {
73            process_command: Some(ProcessCommandSpec::Argv(
74                command.into_iter().map(Into::into).collect(),
75            )),
76            ..Self::default()
77        }
78    }
79
80    /// Creates a process spec that executes command text through the shell.
81    #[must_use]
82    pub fn shell(command: impl Into<String>) -> Self {
83        Self {
84            process_command: Some(ProcessCommandSpec::Shell(command.into())),
85            ..Self::default()
86        }
87    }
88
89    pub(crate) fn into_proto_parts(
90        self,
91    ) -> (
92        Option<Vec<String>>,
93        Option<rmux_proto::ProcessCommand>,
94        Option<Vec<String>>,
95    ) {
96        (
97            self.command,
98            self.process_command.map(rmux_proto::ProcessCommand::from),
99            self.environment,
100        )
101    }
102}
103
104impl fmt::Debug for ProcessSpec {
105    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106        formatter
107            .debug_struct("ProcessSpec")
108            .field("command", &self.command)
109            .field("process_command", &self.process_command)
110            .finish_non_exhaustive()
111    }
112}
113
114/// Reuse-related flags for `new-session` specs.
115#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub struct NewSessionReuse {
117    /// Attach to an existing target session instead of treating it as an error.
118    #[serde(default)]
119    pub attach_if_exists: bool,
120    /// Detach other attached clients before attaching.
121    #[serde(default)]
122    pub detach_other_clients: bool,
123    /// Detach and terminate other attached clients before attaching.
124    #[serde(default)]
125    pub kill_other_clients: bool,
126    /// Skip client environment updates.
127    #[serde(default)]
128    pub skip_environment_update: bool,
129    /// Optional tmux client-flag names such as `read-only` or `active-pane`.
130    #[serde(default)]
131    pub flags: Option<Vec<String>>,
132}
133
134/// Reuse-related flags for `attach-session` specs.
135#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
136pub struct AttachSessionReuse {
137    /// Detach other attached clients before attaching.
138    #[serde(default)]
139    pub detach_other_clients: bool,
140    /// Detach and terminate other attached clients before attaching.
141    #[serde(default)]
142    pub kill_other_clients: bool,
143    /// Enable readonly attach mode.
144    #[serde(default)]
145    pub read_only: bool,
146    /// Skip client environment updates.
147    #[serde(default)]
148    pub skip_environment_update: bool,
149    /// Optional tmux client-flag names such as `read-only` or `active-pane`.
150    #[serde(default)]
151    pub flags: Option<Vec<String>>,
152}
153
154/// Terminal/runtime hints captured by a caller.
155#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct ClientTerminalSpec {
157    /// Explicit terminal feature names contributed by top-level client flags.
158    #[serde(default)]
159    pub terminal_features: Vec<String>,
160    /// Whether the invoking client should be treated as UTF-8 capable.
161    #[serde(default)]
162    pub utf8: bool,
163}
164
165impl From<ClientTerminalSpec> for rmux_proto::ClientTerminalContext {
166    fn from(value: ClientTerminalSpec) -> Self {
167        Self {
168            terminal_features: value.terminal_features,
169            utf8: value.utf8,
170        }
171    }
172}
173
174impl From<rmux_proto::ClientTerminalContext> for ClientTerminalSpec {
175    fn from(value: rmux_proto::ClientTerminalContext) -> Self {
176        Self {
177            terminal_features: value.terminal_features,
178            utf8: value.utf8,
179        }
180    }
181}
182
183/// SDK value object for `new-session`.
184#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
185pub struct NewSessionSpec {
186    /// Optional exact session name to create.
187    #[serde(default)]
188    pub session_name: Option<SessionName>,
189    /// Optional tmux format-expanded start directory for the new session.
190    #[serde(default)]
191    pub working_directory: Option<String>,
192    /// Whether the session should remain detached after creation.
193    #[serde(default)]
194    pub detached: bool,
195    /// Initial pane geometry, when explicitly requested.
196    #[serde(default)]
197    pub size: Option<TerminalSizeSpec>,
198    /// Process-spawn fields for the initial pane.
199    #[serde(default)]
200    pub process: ProcessSpec,
201    /// Optional target session or group name for grouped-session creation.
202    #[serde(default)]
203    pub group_target: Option<SessionName>,
204    /// Reuse and client-detach flags.
205    #[serde(default)]
206    pub reuse: NewSessionReuse,
207    /// Optional initial active-window name.
208    #[serde(default)]
209    pub window_name: Option<String>,
210    /// Whether to print formatted session information.
211    #[serde(default)]
212    pub print_session_info: bool,
213    /// Optional format template used when printing session information.
214    #[serde(default)]
215    pub print_format: Option<String>,
216}
217
218impl From<NewSessionSpec> for rmux_proto::NewSessionExtRequest {
219    fn from(value: NewSessionSpec) -> Self {
220        let (command, process_command, environment) = value.process.into_proto_parts();
221        Self {
222            session_name: value.session_name,
223            working_directory: value.working_directory,
224            detached: value.detached,
225            size: value.size.map(Into::into),
226            environment,
227            group_target: value.group_target,
228            attach_if_exists: value.reuse.attach_if_exists,
229            detach_other_clients: value.reuse.detach_other_clients,
230            kill_other_clients: value.reuse.kill_other_clients,
231            flags: value.reuse.flags,
232            window_name: value.window_name,
233            print_session_info: value.print_session_info,
234            print_format: value.print_format,
235            command,
236            process_command,
237            client_environment: None,
238            skip_environment_update: value.reuse.skip_environment_update,
239        }
240    }
241}
242
243/// SDK value object for `attach-session`.
244#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
245pub struct AttachSessionSpec {
246    /// Optional exact target session name.
247    #[serde(default)]
248    pub target: Option<SessionName>,
249    /// Optional raw tmux-style target text, including window or pane selectors.
250    #[serde(default)]
251    pub target_spec: Option<String>,
252    /// Reuse and client-detach flags.
253    #[serde(default)]
254    pub reuse: AttachSessionReuse,
255    /// Optional tmux format-expanded working directory applied to the target.
256    #[serde(default)]
257    pub working_directory: Option<String>,
258    /// Terminal/runtime hints captured from the invoking client.
259    #[serde(default)]
260    pub client_terminal: ClientTerminalSpec,
261    /// The invoking client terminal size, when known.
262    #[serde(default)]
263    pub client_size: Option<TerminalSizeSpec>,
264}
265
266impl From<AttachSessionSpec> for rmux_proto::AttachSessionExt2Request {
267    fn from(value: AttachSessionSpec) -> Self {
268        Self {
269            target: value.target,
270            target_spec: value.target_spec,
271            detach_other_clients: value.reuse.detach_other_clients,
272            kill_other_clients: value.reuse.kill_other_clients,
273            read_only: value.reuse.read_only,
274            skip_environment_update: value.reuse.skip_environment_update,
275            flags: value.reuse.flags,
276            working_directory: value.working_directory,
277            client_terminal: value.client_terminal.into(),
278            client_size: value.client_size.map(Into::into),
279        }
280    }
281}
282
283/// Control-mode subscription fields for `refresh-client`.
284#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
285pub struct SubscriptionSpec {
286    /// Control-mode subscription updates from `refresh-client -A`.
287    #[serde(default)]
288    pub subscriptions: Vec<String>,
289    /// Control-mode subscription definitions from `refresh-client -B`.
290    #[serde(default)]
291    pub subscriptions_format: Vec<String>,
292}
293
294/// SDK value object for `refresh-client`.
295#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
296pub struct RefreshClientSpec {
297    /// Optional target-client identifier or `=`.
298    #[serde(default)]
299    pub target_client: Option<String>,
300    /// Optional pan adjustment used with directional panning.
301    #[serde(default)]
302    pub adjustment: Option<u32>,
303    /// Whether client panning should be cleared.
304    #[serde(default)]
305    pub clear_pan: bool,
306    /// Whether the client view should pan left.
307    #[serde(default)]
308    pub pan_left: bool,
309    /// Whether the client view should pan right.
310    #[serde(default)]
311    pub pan_right: bool,
312    /// Whether the client view should pan up.
313    #[serde(default)]
314    pub pan_up: bool,
315    /// Whether the client view should pan down.
316    #[serde(default)]
317    pub pan_down: bool,
318    /// Whether only the status line should be redrawn.
319    #[serde(default)]
320    pub status_only: bool,
321    /// Whether the client clipboard should be queried.
322    #[serde(default)]
323    pub clipboard_query: bool,
324    /// Optional client-flag string from `-f`.
325    #[serde(default)]
326    pub flags: Option<String>,
327    /// Optional client-flag string from `-F`.
328    #[serde(default)]
329    pub flags_alias: Option<String>,
330    /// Control-mode subscription fields.
331    #[serde(default)]
332    pub subscriptions: SubscriptionSpec,
333    /// Optional control-mode size string from `-C`.
334    #[serde(default)]
335    pub control_size: Option<String>,
336    /// Optional control-mode colour report request from `-r`.
337    #[serde(default)]
338    pub colour_report: Option<String>,
339}
340
341impl From<RefreshClientSpec> for rmux_proto::RefreshClientRequest {
342    fn from(value: RefreshClientSpec) -> Self {
343        Self {
344            target_client: value.target_client,
345            adjustment: value.adjustment,
346            clear_pan: value.clear_pan,
347            pan_left: value.pan_left,
348            pan_right: value.pan_right,
349            pan_up: value.pan_up,
350            pan_down: value.pan_down,
351            status_only: value.status_only,
352            clipboard_query: value.clipboard_query,
353            flags: value.flags,
354            flags_alias: value.flags_alias,
355            subscriptions: value.subscriptions.subscriptions,
356            subscriptions_format: value.subscriptions.subscriptions_format,
357            control_size: value.control_size,
358            colour_report: value.colour_report,
359        }
360    }
361}
362
363/// Low-level split orientation used by [`SplitSpec`] and the script-level
364/// `RmuxCommand` API.
365///
366/// Variants follow tmux's flag convention (pane arrangement). Prefer the
367/// ergonomic [`SplitDirection`](crate::SplitDirection) (`Right`/`Left`/`Up`/
368/// `Down`) on [`Pane::split`](crate::Pane::split), which removes the
369/// arrangement-vs-divider-line ambiguity.
370#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
371pub enum SplitDirectionSpec {
372    /// Stacked panes (top + bottom). Matches tmux `split-window -v`,
373    /// the tmux default when no flag is passed.
374    #[default]
375    Vertical,
376    /// Side-by-side panes (left + right). Matches tmux `split-window -h`.
377    Horizontal,
378}
379
380impl From<SplitDirectionSpec> for rmux_proto::SplitDirection {
381    fn from(value: SplitDirectionSpec) -> Self {
382        match value {
383            SplitDirectionSpec::Vertical => Self::Vertical,
384            SplitDirectionSpec::Horizontal => Self::Horizontal,
385        }
386    }
387}
388
389impl From<rmux_proto::SplitDirection> for SplitDirectionSpec {
390    fn from(value: rmux_proto::SplitDirection) -> Self {
391        match value {
392            rmux_proto::SplitDirection::Vertical => Self::Vertical,
393            rmux_proto::SplitDirection::Horizontal => Self::Horizontal,
394        }
395    }
396}
397
398/// SDK split target.
399#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
400pub enum SplitTargetSpec {
401    /// Split the active pane in the addressed session.
402    Session(SessionName),
403    /// Split the addressed pane directly.
404    Pane(PaneRef),
405}
406
407impl From<SplitTargetSpec> for rmux_proto::SplitWindowTarget {
408    fn from(value: SplitTargetSpec) -> Self {
409        match value {
410            SplitTargetSpec::Session(session_name) => Self::Session(session_name),
411            SplitTargetSpec::Pane(target) => Self::Pane(target.into()),
412        }
413    }
414}
415
416impl From<rmux_proto::SplitWindowTarget> for SplitTargetSpec {
417    fn from(value: rmux_proto::SplitWindowTarget) -> Self {
418        match value {
419            rmux_proto::SplitWindowTarget::Session(session_name) => Self::Session(session_name),
420            rmux_proto::SplitWindowTarget::Pane(target) => Self::Pane(target.into()),
421        }
422    }
423}
424
425impl From<&SplitTargetSpec> for rmux_proto::SplitWindowTarget {
426    fn from(value: &SplitTargetSpec) -> Self {
427        match value {
428            SplitTargetSpec::Session(session_name) => Self::Session(session_name.clone()),
429            SplitTargetSpec::Pane(target) => Self::Pane(target.into()),
430        }
431    }
432}
433
434/// SDK value object for `split-window`.
435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436pub struct SplitSpec {
437    /// Exact split target.
438    pub target: SplitTargetSpec,
439    /// Requested split direction.
440    #[serde(default)]
441    pub direction: SplitDirectionSpec,
442    /// Whether the new pane is inserted before the target on the chosen
443    /// axis (tmux `-b`). Default `false` puts the new pane after the target.
444    #[serde(default)]
445    pub before: bool,
446    /// Process-spawn fields for the new pane.
447    #[serde(default)]
448    pub process: ProcessSpec,
449}
450
451impl From<SplitSpec> for rmux_proto::SplitWindowExtRequest {
452    fn from(value: SplitSpec) -> Self {
453        let (command, process_command, environment) = value.process.into_proto_parts();
454        Self {
455            target: value.target.into(),
456            direction: value.direction.into(),
457            before: value.before,
458            environment,
459            command,
460            process_command,
461            start_directory: None,
462            keep_alive_on_exit: None,
463            detached: false,
464            size: None,
465            preserve_zoom: false,
466            full_size: false,
467            stdin_payload: None,
468        }
469    }
470}