tmux_lib/
layout.rs

1//! Parse the window layout string.
2//!
3//! Tmux reports the layout for a window, it can also use it to apply an existing layout to a
4//! window.
5//!
6//! A window layout has this format:
7//!
8//! ```text
9//! "41e9,279x71,0,0[279x40,0,0,71,279x30,0,41{147x30,0,41,72,131x30,148,41,73}]"
10//! ```
11//!
12//! The parser in this module returns the corresponding [`WindowLayout`].
13
14use nom::{
15    branch::alt,
16    character::complete::{char, digit1, hex_digit1},
17    combinator::{all_consuming, map_res},
18    multi::separated_list1,
19    sequence::delimited,
20    IResult, Parser,
21};
22
23use crate::{error::map_add_intent, Result};
24
25/// Represent a parsed window layout.
26#[derive(Debug, PartialEq, Eq)]
27pub struct WindowLayout {
28    /// 4-char hex id, such as `9f58`.
29    id: u16,
30    /// Container.
31    container: Container,
32}
33
34impl WindowLayout {
35    /// Return a flat list of pane ids.
36    #[must_use]
37    pub fn pane_ids(&self) -> Vec<u16> {
38        let mut acc: Vec<u16> = Vec::with_capacity(1);
39        self.walk(&mut acc);
40        acc
41    }
42
43    /// Walk the structure, searching for pane ids.
44    fn walk(&self, acc: &mut Vec<u16>) {
45        self.container.walk(acc);
46    }
47}
48
49#[derive(Debug, PartialEq, Eq)]
50struct Container {
51    /// Dimensions of the container.
52    dimensions: Dimensions,
53    /// Offset of the top left corner of the container.
54    coordinates: Coordinates,
55    /// Either a pane, or a horizontal or vertical split.
56    element: Element,
57}
58
59impl Container {
60    /// Walk the structure, searching for pane ids.
61    fn walk(&self, acc: &mut Vec<u16>) {
62        self.element.walk(acc);
63    }
64}
65
66#[derive(Debug, PartialEq, Eq)]
67struct Dimensions {
68    /// Width (of the window or pane).
69    width: u16,
70    /// Height (of the window or pane).
71    height: u16,
72}
73
74#[derive(Debug, PartialEq, Eq)]
75struct Coordinates {
76    /// Horizontal offset of the top left corner (of the window or pane).
77    x: u16,
78    /// Vertical offset of the top left corner (of the window or pane).
79    y: u16,
80}
81
82/// Element in a container.
83#[derive(Debug, PartialEq, Eq)]
84enum Element {
85    /// A pane.
86    Pane { pane_id: u16 },
87    /// A horizontal split.
88    Horizontal(Split),
89    /// A vertical split.
90    Vertical(Split),
91}
92
93impl Element {
94    /// Walk the structure, searching for pane ids.
95    fn walk(&self, acc: &mut Vec<u16>) {
96        match self {
97            Self::Pane { pane_id } => acc.push(*pane_id),
98            Self::Horizontal(split) | Self::Vertical(split) => {
99                split.walk(acc);
100            }
101        }
102    }
103}
104
105#[derive(Debug, PartialEq, Eq)]
106struct Split {
107    /// Embedded containers.
108    elements: Vec<Container>,
109}
110
111impl Split {
112    /// Walk the structure, searching for pane ids.
113    fn walk(&self, acc: &mut Vec<u16>) {
114        for element in &self.elements {
115            element.walk(acc);
116        }
117    }
118}
119
120/// Parse the Tmux layout string description and return the pane-ids.
121pub fn parse_window_layout(input: &str) -> Result<WindowLayout> {
122    let desc = "window-layout";
123    let intent = "window-layout";
124    let (_, win_layout) = all_consuming(window_layout)
125        .parse(input)
126        .map_err(|e| map_add_intent(desc, intent, e))?;
127
128    Ok(win_layout)
129}
130
131pub(crate) fn window_layout(input: &str) -> IResult<&str, WindowLayout> {
132    let (input, (id, _, container)) = (layout_id, char(','), container).parse(input)?;
133    Ok((input, WindowLayout { id, container }))
134}
135
136fn from_hex(input: &str) -> std::result::Result<u16, std::num::ParseIntError> {
137    u16::from_str_radix(input, 16)
138}
139
140fn layout_id(input: &str) -> IResult<&str, u16> {
141    map_res(hex_digit1, from_hex).parse(input)
142}
143
144fn parse_u16(input: &str) -> IResult<&str, u16> {
145    map_res(digit1, str::parse).parse(input)
146}
147
148fn dimensions(input: &str) -> IResult<&str, Dimensions> {
149    let (input, (width, _, height)) = (parse_u16, char('x'), parse_u16).parse(input)?;
150    Ok((input, Dimensions { width, height }))
151}
152
153fn coordinates(input: &str) -> IResult<&str, Coordinates> {
154    let (input, (x, _, y)) = (parse_u16, char(','), parse_u16).parse(input)?;
155    Ok((input, Coordinates { x, y }))
156}
157
158fn single_pane(input: &str) -> IResult<&str, Element> {
159    let (input, (_, pane_id)) = (char(','), parse_u16).parse(input)?;
160    Ok((input, Element::Pane { pane_id }))
161}
162
163fn horiz_split(input: &str) -> IResult<&str, Element> {
164    let (input, elements) =
165        delimited(char('{'), separated_list1(char(','), container), char('}')).parse(input)?;
166    Ok((input, Element::Horizontal(Split { elements })))
167}
168
169fn vert_split(input: &str) -> IResult<&str, Element> {
170    let (input, elements) =
171        delimited(char('['), separated_list1(char(','), container), char(']')).parse(input)?;
172    Ok((input, Element::Vertical(Split { elements })))
173}
174
175fn element(input: &str) -> IResult<&str, Element> {
176    alt((single_pane, horiz_split, vert_split)).parse(input)
177}
178
179fn container(input: &str) -> IResult<&str, Container> {
180    let (input, (dimensions, _, coordinates, element)) =
181        (dimensions, char(','), coordinates, element).parse(input)?;
182    Ok((
183        input,
184        Container {
185            dimensions,
186            coordinates,
187            element,
188        },
189    ))
190}
191
192#[cfg(test)]
193mod tests {
194
195    use super::{
196        coordinates, dimensions, layout_id, single_pane, vert_split, window_layout, Container,
197        Coordinates, Dimensions, Element, Split, WindowLayout,
198    };
199
200    #[test]
201    fn test_parse_layout_id() {
202        let input = "9f58";
203
204        let actual = layout_id(input);
205        let expected = Ok(("", 40792_u16));
206        assert_eq!(actual, expected);
207    }
208
209    #[test]
210    fn test_parse_dimensions() {
211        let input = "237x0";
212
213        let actual = dimensions(input);
214        let expected = Ok((
215            "",
216            Dimensions {
217                width: 237,
218                height: 0,
219            },
220        ));
221        assert_eq!(actual, expected);
222
223        let input = "7x13";
224
225        let actual = dimensions(input);
226        let expected = Ok((
227            "",
228            Dimensions {
229                width: 7,
230                height: 13,
231            },
232        ));
233        assert_eq!(actual, expected);
234    }
235
236    #[test]
237    fn test_parse_coordinates() {
238        let input = "120,0";
239
240        let actual = coordinates(input);
241        let expected = Ok(("", Coordinates { x: 120, y: 0 }));
242        assert_eq!(actual, expected);
243    }
244
245    #[test]
246    fn test_single_pane() {
247        let input = ",46";
248
249        let actual = single_pane(input);
250        let expected = Ok(("", Element::Pane { pane_id: 46 }));
251        assert_eq!(actual, expected);
252    }
253
254    #[test]
255    fn test_vertical_split() {
256        let input = "[279x47,0,0,82,279x23,0,48,83]";
257
258        let actual = vert_split(input);
259        let expected = Ok((
260            "",
261            Element::Vertical(Split {
262                elements: vec![
263                    Container {
264                        dimensions: Dimensions {
265                            width: 279,
266                            height: 47,
267                        },
268                        coordinates: Coordinates { x: 0, y: 0 },
269                        element: Element::Pane { pane_id: 82 },
270                    },
271                    Container {
272                        dimensions: Dimensions {
273                            width: 279,
274                            height: 23,
275                        },
276                        coordinates: Coordinates { x: 0, y: 48 },
277                        element: Element::Pane { pane_id: 83 },
278                    },
279                ],
280            }),
281        ));
282        assert_eq!(actual, expected);
283    }
284
285    #[test]
286    fn test_layout() {
287        let input = "41e9,279x71,0,0[279x40,0,0,71,279x30,0,41{147x30,0,41,72,131x30,148,41,73}]";
288
289        let actual = window_layout(input);
290        let expected = Ok((
291            "",
292            WindowLayout {
293                id: 0x41e9,
294                container: Container {
295                    dimensions: Dimensions {
296                        width: 279,
297                        height: 71,
298                    },
299                    coordinates: Coordinates { x: 0, y: 0 },
300                    element: Element::Vertical(Split {
301                        elements: vec![
302                            Container {
303                                dimensions: Dimensions {
304                                    width: 279,
305                                    height: 40,
306                                },
307                                coordinates: Coordinates { x: 0, y: 0 },
308                                element: Element::Pane { pane_id: 71 },
309                            },
310                            Container {
311                                dimensions: Dimensions {
312                                    width: 279,
313                                    height: 30,
314                                },
315                                coordinates: Coordinates { x: 0, y: 41 },
316                                element: Element::Horizontal(Split {
317                                    elements: vec![
318                                        Container {
319                                            dimensions: Dimensions {
320                                                width: 147,
321                                                height: 30,
322                                            },
323                                            coordinates: Coordinates { x: 0, y: 41 },
324                                            element: Element::Pane { pane_id: 72 },
325                                        },
326                                        Container {
327                                            dimensions: Dimensions {
328                                                width: 131,
329                                                height: 30,
330                                            },
331                                            coordinates: Coordinates { x: 148, y: 41 },
332                                            element: Element::Pane { pane_id: 73 },
333                                        },
334                                    ],
335                                }),
336                            },
337                        ],
338                    }),
339                },
340            },
341        ));
342        assert_eq!(actual, expected);
343    }
344
345    #[test]
346    fn test_pane_ids() {
347        let input = "41e9,279x71,0,0[279x40,0,0,71,279x30,0,41{147x30,0,41,72,131x30,148,41,73}]";
348        let (_, layout) = window_layout(input).unwrap();
349
350        let actual = layout.pane_ids();
351        let expected = vec![71, 72, 73];
352        assert_eq!(actual, expected);
353    }
354}