Skip to main content

rmux_proto/request/
session.rs

1use serde::de::{self, MapAccess, SeqAccess, Visitor};
2use serde::{Deserialize, Deserializer, Serialize};
3
4use crate::{ProcessCommand, SessionName, TerminalSize};
5
6/// Request payload for `new-session`.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct NewSessionRequest {
9    /// The exact session name to create.
10    pub session_name: SessionName,
11    /// Whether the session should remain detached after creation.
12    pub detached: bool,
13    /// The initial pane geometry, when explicitly requested.
14    pub size: Option<TerminalSize>,
15    /// Optional per-spawn environment overrides in `NAME=VALUE` form.
16    #[serde(default)]
17    pub environment: Option<Vec<String>>,
18}
19
20/// Extended request payload for `new-session`.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
22pub struct NewSessionExtRequest {
23    /// The optional exact session name to create.
24    pub session_name: Option<SessionName>,
25    /// Optional tmux format-expanded start directory for the new session.
26    #[serde(default)]
27    pub working_directory: Option<String>,
28    /// Whether the session should remain detached after creation.
29    pub detached: bool,
30    /// The initial pane geometry, when explicitly requested.
31    pub size: Option<TerminalSize>,
32    /// Optional per-spawn environment overrides in `NAME=VALUE` form.
33    #[serde(default)]
34    pub environment: Option<Vec<String>>,
35    /// The optional target session or group name for grouped-session creation.
36    #[serde(default)]
37    pub group_target: Option<SessionName>,
38    /// Whether an existing target session should be attached instead of erroring.
39    #[serde(default)]
40    pub attach_if_exists: bool,
41    /// Whether other attached clients should be detached before attaching.
42    #[serde(default)]
43    pub detach_other_clients: bool,
44    /// Whether other attached clients should be detached and terminated.
45    #[serde(default)]
46    pub kill_other_clients: bool,
47    /// Optional tmux client-flag names such as `read-only` or `active-pane`.
48    #[serde(default)]
49    pub flags: Option<Vec<String>>,
50    /// The optional initial active-window name for standalone session creation.
51    #[serde(default)]
52    pub window_name: Option<String>,
53    /// Whether the created session should print formatted session information.
54    #[serde(default)]
55    pub print_session_info: bool,
56    /// The optional format template used when printing session information.
57    #[serde(default)]
58    pub print_format: Option<String>,
59    /// Legacy optional shell command argv. A single argument is executed via
60    /// `$SHELL -c`.
61    #[serde(default)]
62    pub command: Option<Vec<String>>,
63    /// Explicit process launch mode for the initial pane.
64    #[serde(default)]
65    pub process_command: Option<ProcessCommand>,
66}
67
68impl<'de> Deserialize<'de> for NewSessionExtRequest {
69    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
70    where
71        D: Deserializer<'de>,
72    {
73        deserializer.deserialize_struct(
74            "NewSessionExtRequest",
75            &[
76                "session_name",
77                "working_directory",
78                "detached",
79                "size",
80                "environment",
81                "group_target",
82                "attach_if_exists",
83                "detach_other_clients",
84                "kill_other_clients",
85                "flags",
86                "window_name",
87                "print_session_info",
88                "print_format",
89                "command",
90                "process_command",
91            ],
92            NewSessionExtRequestVisitor,
93        )
94    }
95}
96
97struct NewSessionExtRequestVisitor;
98
99impl<'de> Visitor<'de> for NewSessionExtRequestVisitor {
100    type Value = NewSessionExtRequest;
101
102    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        formatter.write_str("a new-session extended request")
104    }
105
106    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
107    where
108        A: SeqAccess<'de>,
109    {
110        let session_name = required_next(&mut seq, 0, &self)?;
111        let working_directory = required_next(&mut seq, 1, &self)?;
112        let detached = required_next(&mut seq, 2, &self)?;
113        let size = required_next(&mut seq, 3, &self)?;
114        let environment = required_next(&mut seq, 4, &self)?;
115        let group_target = required_next(&mut seq, 5, &self)?;
116        let attach_if_exists = required_next(&mut seq, 6, &self)?;
117        let detach_other_clients = required_next(&mut seq, 7, &self)?;
118        let kill_other_clients = required_next(&mut seq, 8, &self)?;
119        let flags = required_next(&mut seq, 9, &self)?;
120        let window_name = required_next(&mut seq, 10, &self)?;
121        let print_session_info = required_next(&mut seq, 11, &self)?;
122        let print_format = required_next(&mut seq, 12, &self)?;
123        let command = required_next(&mut seq, 13, &self)?;
124        let process_command = compat_next_element(&mut seq)?;
125
126        Ok(NewSessionExtRequest {
127            session_name,
128            working_directory,
129            detached,
130            size,
131            environment,
132            group_target,
133            attach_if_exists,
134            detach_other_clients,
135            kill_other_clients,
136            flags,
137            window_name,
138            print_session_info,
139            print_format,
140            command,
141            process_command,
142        })
143    }
144
145    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
146    where
147        A: MapAccess<'de>,
148    {
149        let mut session_name = None;
150        let mut working_directory = None;
151        let mut detached = None;
152        let mut size = None;
153        let mut environment = None;
154        let mut group_target = None;
155        let mut attach_if_exists = None;
156        let mut detach_other_clients = None;
157        let mut kill_other_clients = None;
158        let mut flags = None;
159        let mut window_name = None;
160        let mut print_session_info = None;
161        let mut print_format = None;
162        let mut command = None;
163        let mut process_command = None;
164
165        while let Some(key) = map.next_key::<String>()? {
166            match key.as_str() {
167                "session_name" => session_name = Some(map.next_value()?),
168                "working_directory" => working_directory = Some(map.next_value()?),
169                "detached" => detached = Some(map.next_value()?),
170                "size" => size = Some(map.next_value()?),
171                "environment" => environment = Some(map.next_value()?),
172                "group_target" => group_target = Some(map.next_value()?),
173                "attach_if_exists" => attach_if_exists = Some(map.next_value()?),
174                "detach_other_clients" => detach_other_clients = Some(map.next_value()?),
175                "kill_other_clients" => kill_other_clients = Some(map.next_value()?),
176                "flags" => flags = Some(map.next_value()?),
177                "window_name" => window_name = Some(map.next_value()?),
178                "print_session_info" => print_session_info = Some(map.next_value()?),
179                "print_format" => print_format = Some(map.next_value()?),
180                "command" => command = Some(map.next_value()?),
181                "process_command" => process_command = Some(map.next_value()?),
182                _ => {
183                    let _: de::IgnoredAny = map.next_value()?;
184                }
185            }
186        }
187
188        Ok(NewSessionExtRequest {
189            session_name: session_name.ok_or_else(|| de::Error::missing_field("session_name"))?,
190            working_directory: working_directory
191                .ok_or_else(|| de::Error::missing_field("working_directory"))?,
192            detached: detached.ok_or_else(|| de::Error::missing_field("detached"))?,
193            size: size.ok_or_else(|| de::Error::missing_field("size"))?,
194            environment: environment.ok_or_else(|| de::Error::missing_field("environment"))?,
195            group_target: group_target.ok_or_else(|| de::Error::missing_field("group_target"))?,
196            attach_if_exists: attach_if_exists
197                .ok_or_else(|| de::Error::missing_field("attach_if_exists"))?,
198            detach_other_clients: detach_other_clients
199                .ok_or_else(|| de::Error::missing_field("detach_other_clients"))?,
200            kill_other_clients: kill_other_clients
201                .ok_or_else(|| de::Error::missing_field("kill_other_clients"))?,
202            flags: flags.ok_or_else(|| de::Error::missing_field("flags"))?,
203            window_name: window_name.ok_or_else(|| de::Error::missing_field("window_name"))?,
204            print_session_info: print_session_info
205                .ok_or_else(|| de::Error::missing_field("print_session_info"))?,
206            print_format: print_format.ok_or_else(|| de::Error::missing_field("print_format"))?,
207            command: command.ok_or_else(|| de::Error::missing_field("command"))?,
208            process_command: process_command.unwrap_or_default(),
209        })
210    }
211}
212
213fn required_next<'de, A, T, V>(seq: &mut A, index: usize, visitor: &V) -> Result<T, A::Error>
214where
215    A: SeqAccess<'de>,
216    T: Deserialize<'de>,
217    V: Visitor<'de>,
218{
219    seq.next_element()?
220        .ok_or_else(|| de::Error::invalid_length(index, visitor))
221}
222
223fn compat_next_element<'de, A, T>(seq: &mut A) -> Result<T, A::Error>
224where
225    A: SeqAccess<'de>,
226    T: Deserialize<'de> + Default,
227{
228    match seq.next_element::<T>() {
229        Ok(Some(value)) => Ok(value),
230        Ok(None) => Ok(T::default()),
231        Err(error) if is_truncated_compat_sequence(&error) => Ok(T::default()),
232        Err(error) => Err(error),
233    }
234}
235
236fn is_truncated_compat_sequence(error: &impl std::fmt::Display) -> bool {
237    let message = error.to_string();
238    message.contains("UnexpectedEof") || message.contains("unexpected end of file")
239}
240
241/// Request payload for `has-session`.
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243pub struct HasSessionRequest {
244    /// The exact target session name.
245    pub target: SessionName,
246}
247
248/// Request payload for `kill-session`.
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250pub struct KillSessionRequest {
251    /// The exact target session name.
252    pub target: SessionName,
253    /// Whether every other session should be destroyed instead of the target session.
254    #[serde(default)]
255    pub kill_all_except_target: bool,
256    /// Whether the target session's window alert flags should be cleared instead of destroying it.
257    #[serde(default)]
258    pub clear_alerts: bool,
259}
260
261/// Request payload for creating an app-owner lease for one session.
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
263pub struct CreateSessionLeaseRequest {
264    /// Session kept alive only while the owner renews this lease.
265    pub session_name: SessionName,
266    /// Requested lease time-to-live in milliseconds.
267    pub ttl_millis: u64,
268}
269
270/// Request payload for renewing an app-owner session lease.
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272pub struct RenewSessionLeaseRequest {
273    /// Leased session name.
274    pub session_name: SessionName,
275    /// Server-issued lease token.
276    pub token: u64,
277    /// Requested renewed time-to-live in milliseconds.
278    pub ttl_millis: u64,
279}
280
281/// Request payload for releasing an app-owner session lease.
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283pub struct ReleaseSessionLeaseRequest {
284    /// Leased session name.
285    pub session_name: SessionName,
286    /// Server-issued lease token.
287    pub token: u64,
288}
289
290/// Request payload for `rename-session`.
291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292pub struct RenameSessionRequest {
293    /// The exact existing session name.
294    pub target: SessionName,
295    /// The validated destination session name.
296    pub new_name: SessionName,
297}
298
299/// Request payload for `list-sessions`.
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
301pub struct ListSessionsRequest {
302    /// An optional server-side format template.
303    pub format: Option<String>,
304    /// An optional server-side filter expression.
305    #[serde(default)]
306    pub filter: Option<String>,
307    /// The optional tmux sort order name.
308    #[serde(default)]
309    pub sort_order: Option<String>,
310    /// Whether the selected sort order should be reversed.
311    #[serde(default)]
312    pub reversed: bool,
313}