Skip to main content

rmux_proto/request/
window.rs

1use serde::de::{self, MapAccess, SeqAccess, Visitor};
2use serde::{Deserialize, Deserializer, Serialize};
3use std::path::PathBuf;
4
5use crate::{SessionName, WindowTarget};
6
7/// Request payload for `new-window`.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9pub struct NewWindowRequest {
10    /// The exact target session name.
11    pub target: SessionName,
12    /// The optional explicit window name.
13    pub name: Option<String>,
14    /// Whether the newly created window should remain inactive.
15    pub detached: bool,
16    /// Optional per-spawn environment overrides in `NAME=VALUE` form.
17    #[serde(default)]
18    pub environment: Option<Vec<String>>,
19    /// Optional shell command argv. A single argument is executed via `$SHELL -c`.
20    #[serde(default)]
21    pub command: Option<Vec<String>>,
22    /// Optional working-directory override.
23    #[serde(default)]
24    pub start_directory: Option<PathBuf>,
25    /// Optional destination window index from `new-window -t session:index`.
26    #[serde(default)]
27    pub target_window_index: Option<u32>,
28    /// Whether an occupied destination index should be opened by shifting windows upward.
29    #[serde(default)]
30    pub insert_at_target: bool,
31}
32
33/// Request payload for `kill-window`.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct KillWindowRequest {
36    /// The exact target window.
37    pub target: WindowTarget,
38    /// Whether all other windows in the session should be removed instead.
39    pub kill_all_others: bool,
40}
41
42/// Request payload for `select-window`.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct SelectWindowRequest {
45    /// The exact target window.
46    pub target: WindowTarget,
47}
48
49/// Request payload for `rename-window`.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct RenameWindowRequest {
52    /// The exact target window.
53    pub target: WindowTarget,
54    /// The new window name.
55    pub name: String,
56}
57
58/// Request payload for `next-window`.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct NextWindowRequest {
61    /// The exact target session name.
62    pub target: SessionName,
63    /// Whether only alerted windows should be considered.
64    #[serde(default)]
65    pub alerts_only: bool,
66}
67
68/// Request payload for `previous-window`.
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct PreviousWindowRequest {
71    /// The exact target session name.
72    pub target: SessionName,
73    /// Whether only alerted windows should be considered.
74    #[serde(default)]
75    pub alerts_only: bool,
76}
77
78/// Request payload for `last-window`.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct LastWindowRequest {
81    /// The exact target session name.
82    pub target: SessionName,
83}
84
85/// Request payload for `list-windows`.
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct ListWindowsRequest {
88    /// The exact target session name.
89    pub target: SessionName,
90    /// An optional server-side compatibility format template.
91    pub format: Option<String>,
92}
93
94/// Request payload for `link-window`.
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub struct LinkWindowRequest {
97    /// The source window slot.
98    pub source: WindowTarget,
99    /// The destination window slot.
100    pub target: WindowTarget,
101    /// Whether to insert after the target slot (`-a`).
102    #[serde(default)]
103    pub after: bool,
104    /// Whether to insert before the target slot (`-b`).
105    #[serde(default)]
106    pub before: bool,
107    /// Whether an occupied destination should be replaced (`-k`).
108    #[serde(default)]
109    pub kill_destination: bool,
110    /// Whether the destination session should keep its current active window (`-d`).
111    #[serde(default)]
112    pub detached: bool,
113}
114
115/// Target forms accepted by `move-window`.
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub enum MoveWindowTarget {
118    /// Applies to the addressed session during `move-window -r`.
119    Session(SessionName),
120    /// Applies to the addressed destination window slot.
121    Window(WindowTarget),
122}
123
124/// Request payload for `move-window`.
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126pub struct MoveWindowRequest {
127    /// The optional source window being moved when not reindexing.
128    pub source: Option<WindowTarget>,
129    /// The destination window slot or reindex target session.
130    pub target: MoveWindowTarget,
131    /// Whether the session should be reindexed instead of moving one window.
132    pub renumber: bool,
133    /// Whether an occupied destination should be replaced.
134    pub kill_destination: bool,
135    /// Whether the destination session should keep its current active window.
136    pub detached: bool,
137}
138
139/// Request payload for `swap-window`.
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141pub struct SwapWindowRequest {
142    /// The source window slot.
143    pub source: WindowTarget,
144    /// The destination window slot.
145    pub target: WindowTarget,
146    /// Whether the swapped destination slots should become active after the swap.
147    pub detached: bool,
148}
149
150/// The supported pane rotation directions for `rotate-window`.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152pub enum RotateWindowDirection {
153    /// Move the last pane to the head.
154    Down,
155    /// Move the first pane to the tail.
156    Up,
157}
158
159/// Request payload for `rotate-window`.
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161pub struct RotateWindowRequest {
162    /// The addressed window.
163    pub target: WindowTarget,
164    /// The requested rotation direction.
165    pub direction: RotateWindowDirection,
166    /// Whether to save and restore zoom state around the rotation (`-Z`).
167    #[serde(default)]
168    pub restore_zoom: bool,
169}
170
171/// Request payload for `resize-window`.
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct ResizeWindowRequest {
174    /// The addressed window.
175    pub target: WindowTarget,
176    /// Optional explicit width (`-x`).
177    pub width: Option<u16>,
178    /// Optional explicit height (`-y`).
179    pub height: Option<u16>,
180    /// Relative adjustment (from `-D`, `-U`, `-L`, `-R`).
181    #[serde(default)]
182    pub adjustment: Option<ResizeWindowAdjustment>,
183}
184
185/// Directional relative-size adjustment for `resize-window`.
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
187pub enum ResizeWindowAdjustment {
188    /// Shrink height (`-U`).
189    Up(u16),
190    /// Grow height (`-D`).
191    Down(u16),
192    /// Shrink width (`-L`).
193    Left(u16),
194    /// Grow width (`-R`).
195    Right(u16),
196}
197
198/// Request payload for `respawn-window`.
199#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
200pub struct RespawnWindowRequest {
201    /// The addressed window.
202    pub target: WindowTarget,
203    /// Whether to kill existing panes even when they are still running (`-k`).
204    #[serde(default)]
205    pub kill: bool,
206    /// Optional per-spawn environment overrides in `NAME=VALUE` form.
207    #[serde(default)]
208    pub environment: Option<Vec<String>>,
209    /// Optional shell command argv. A single argument is executed via `$SHELL -c`.
210    #[serde(default)]
211    pub command: Option<Vec<String>>,
212    /// Optional working-directory override.
213    #[serde(default)]
214    pub start_directory: Option<PathBuf>,
215}
216
217impl<'de> Deserialize<'de> for NewWindowRequest {
218    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
219    where
220        D: Deserializer<'de>,
221    {
222        deserializer.deserialize_struct(
223            "NewWindowRequest",
224            &[
225                "target",
226                "name",
227                "detached",
228                "environment",
229                "command",
230                "start_directory",
231                "target_window_index",
232                "insert_at_target",
233            ],
234            NewWindowRequestVisitor,
235        )
236    }
237}
238
239impl<'de> Deserialize<'de> for RespawnWindowRequest {
240    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
241    where
242        D: Deserializer<'de>,
243    {
244        deserializer.deserialize_struct(
245            "RespawnWindowRequest",
246            &[
247                "target",
248                "kill",
249                "environment",
250                "command",
251                "start_directory",
252            ],
253            RespawnWindowRequestVisitor,
254        )
255    }
256}
257
258struct NewWindowRequestVisitor;
259
260impl<'de> Visitor<'de> for NewWindowRequestVisitor {
261    type Value = NewWindowRequest;
262
263    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        formatter.write_str("a new-window request")
265    }
266
267    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
268    where
269        A: SeqAccess<'de>,
270    {
271        let target = seq
272            .next_element()?
273            .ok_or_else(|| de::Error::invalid_length(0, &self))?;
274        let name = seq
275            .next_element()?
276            .ok_or_else(|| de::Error::invalid_length(1, &self))?;
277        let detached = seq
278            .next_element()?
279            .ok_or_else(|| de::Error::invalid_length(2, &self))?;
280        let environment = seq
281            .next_element()?
282            .ok_or_else(|| de::Error::invalid_length(3, &self))?;
283        let command = compat_next_element(&mut seq)?;
284        let start_directory = compat_next_element(&mut seq)?;
285        let target_window_index = compat_next_element(&mut seq)?;
286        let insert_at_target = compat_next_element(&mut seq)?;
287
288        Ok(NewWindowRequest {
289            target,
290            name,
291            detached,
292            environment,
293            command,
294            start_directory,
295            target_window_index,
296            insert_at_target,
297        })
298    }
299
300    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
301    where
302        A: MapAccess<'de>,
303    {
304        let mut target = None;
305        let mut name = None;
306        let mut detached = None;
307        let mut environment = None;
308        let mut command = None;
309        let mut start_directory = None;
310        let mut target_window_index = None;
311        let mut insert_at_target = None;
312
313        while let Some(key) = map.next_key::<String>()? {
314            match key.as_str() {
315                "target" => target = Some(map.next_value()?),
316                "name" => name = Some(map.next_value()?),
317                "detached" => detached = Some(map.next_value()?),
318                "environment" => environment = Some(map.next_value()?),
319                "command" => command = Some(map.next_value()?),
320                "start_directory" => start_directory = Some(map.next_value()?),
321                "target_window_index" => target_window_index = Some(map.next_value()?),
322                "insert_at_target" => insert_at_target = Some(map.next_value()?),
323                _ => {
324                    let _: de::IgnoredAny = map.next_value()?;
325                }
326            }
327        }
328
329        Ok(NewWindowRequest {
330            target: target.ok_or_else(|| de::Error::missing_field("target"))?,
331            name: name.ok_or_else(|| de::Error::missing_field("name"))?,
332            detached: detached.ok_or_else(|| de::Error::missing_field("detached"))?,
333            environment: environment.ok_or_else(|| de::Error::missing_field("environment"))?,
334            command: command.unwrap_or_default(),
335            start_directory: start_directory.unwrap_or_default(),
336            target_window_index: target_window_index.unwrap_or_default(),
337            insert_at_target: insert_at_target.unwrap_or_default(),
338        })
339    }
340}
341
342struct RespawnWindowRequestVisitor;
343
344impl<'de> Visitor<'de> for RespawnWindowRequestVisitor {
345    type Value = RespawnWindowRequest;
346
347    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348        formatter.write_str("a respawn-window request")
349    }
350
351    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
352    where
353        A: SeqAccess<'de>,
354    {
355        let target = seq
356            .next_element()?
357            .ok_or_else(|| de::Error::invalid_length(0, &self))?;
358        let kill = seq
359            .next_element()?
360            .ok_or_else(|| de::Error::invalid_length(1, &self))?;
361        let environment = seq
362            .next_element()?
363            .ok_or_else(|| de::Error::invalid_length(2, &self))?;
364        let command = compat_next_element(&mut seq)?;
365        let start_directory = compat_next_element(&mut seq)?;
366
367        Ok(RespawnWindowRequest {
368            target,
369            kill,
370            environment,
371            command,
372            start_directory,
373        })
374    }
375
376    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
377    where
378        A: MapAccess<'de>,
379    {
380        let mut target = None;
381        let mut kill = None;
382        let mut environment = None;
383        let mut command = None;
384        let mut start_directory = None;
385
386        while let Some(key) = map.next_key::<String>()? {
387            match key.as_str() {
388                "target" => target = Some(map.next_value()?),
389                "kill" => kill = Some(map.next_value()?),
390                "environment" => environment = Some(map.next_value()?),
391                "command" => command = Some(map.next_value()?),
392                "start_directory" => start_directory = Some(map.next_value()?),
393                _ => {
394                    let _: de::IgnoredAny = map.next_value()?;
395                }
396            }
397        }
398
399        Ok(RespawnWindowRequest {
400            target: target.ok_or_else(|| de::Error::missing_field("target"))?,
401            kill: kill.ok_or_else(|| de::Error::missing_field("kill"))?,
402            environment: environment.ok_or_else(|| de::Error::missing_field("environment"))?,
403            command: command.unwrap_or_default(),
404            start_directory: start_directory.unwrap_or_default(),
405        })
406    }
407}
408
409fn compat_next_element<'de, A, T>(seq: &mut A) -> Result<T, A::Error>
410where
411    A: SeqAccess<'de>,
412    T: Deserialize<'de> + Default,
413{
414    match seq.next_element::<T>() {
415        Ok(Some(value)) => Ok(value),
416        Ok(None) => Ok(T::default()),
417        Err(error) if is_truncated_compat_sequence(&error) => Ok(T::default()),
418        Err(error) => Err(error),
419    }
420}
421
422fn is_truncated_compat_sequence(error: &impl std::fmt::Display) -> bool {
423    let message = error.to_string();
424    message.contains("UnexpectedEof") || message.contains("unexpected end of file")
425}