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, tuple},
20    IResult,
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) =
125        all_consuming(window_layout)(input).map_err(|e| map_add_intent(desc, intent, e))?;
126
127    Ok(win_layout)
128}
129
130pub(crate) fn window_layout(input: &str) -> IResult<&str, WindowLayout> {
131    let (input, (id, _, container)) = tuple((layout_id, char(','), container))(input)?;
132    Ok((input, WindowLayout { id, container }))
133}
134
135fn from_hex(input: &str) -> std::result::Result<u16, std::num::ParseIntError> {
136    u16::from_str_radix(input, 16)
137}
138
139fn layout_id(input: &str) -> IResult<&str, u16> {
140    map_res(hex_digit1, from_hex)(input)
141}
142
143fn parse_u16(input: &str) -> IResult<&str, u16> {
144    map_res(digit1, str::parse)(input)
145}
146
147fn dimensions(input: &str) -> IResult<&str, Dimensions> {
148    let (input, (width, _, height)) = tuple((parse_u16, char('x'), parse_u16))(input)?;
149    Ok((input, Dimensions { width, height }))
150}
151
152fn coordinates(input: &str) -> IResult<&str, Coordinates> {
153    let (input, (x, _, y)) = tuple((parse_u16, char(','), parse_u16))(input)?;
154    Ok((input, Coordinates { x, y }))
155}
156
157fn single_pane(input: &str) -> IResult<&str, Element> {
158    let (input, (_, pane_id)) = tuple((char(','), parse_u16))(input)?;
159    Ok((input, Element::Pane { pane_id }))
160}
161
162fn horiz_split(input: &str) -> IResult<&str, Element> {
163    let (input, elements) =
164        delimited(char('{'), separated_list1(char(','), container), char('}'))(input)?;
165    Ok((input, Element::Horizontal(Split { elements })))
166}
167
168fn vert_split(input: &str) -> IResult<&str, Element> {
169    let (input, elements) =
170        delimited(char('['), separated_list1(char(','), container), char(']'))(input)?;
171    Ok((input, Element::Vertical(Split { elements })))
172}
173
174fn element(input: &str) -> IResult<&str, Element> {
175    alt((single_pane, horiz_split, vert_split))(input)
176}
177
178fn container(input: &str) -> IResult<&str, Container> {
179    let (input, (dimensions, _, coordinates, element)) =
180        tuple((dimensions, char(','), coordinates, element))(input)?;
181    Ok((
182        input,
183        Container {
184            dimensions,
185            coordinates,
186            element,
187        },
188    ))
189}
190
191#[cfg(test)]
192mod tests {
193
194    use super::{
195        coordinates, dimensions, layout_id, single_pane, vert_split, window_layout, Container,
196        Coordinates, Dimensions, Element, Split, WindowLayout,
197    };
198
199    #[test]
200    fn test_parse_layout_id() {
201        let input = "9f58";
202
203        let actual = layout_id(input);
204        let expected = Ok(("", 40792_u16));
205        assert_eq!(actual, expected);
206    }
207
208    #[test]
209    fn test_parse_dimensions() {
210        let input = "237x0";
211
212        let actual = dimensions(input);
213        let expected = Ok((
214            "",
215            Dimensions {
216                width: 237,
217                height: 0,
218            },
219        ));
220        assert_eq!(actual, expected);
221
222        let input = "7x13";
223
224        let actual = dimensions(input);
225        let expected = Ok((
226            "",
227            Dimensions {
228                width: 7,
229                height: 13,
230            },
231        ));
232        assert_eq!(actual, expected);
233    }
234
235    #[test]
236    fn test_parse_coordinates() {
237        let input = "120,0";
238
239        let actual = coordinates(input);
240        let expected = Ok(("", Coordinates { x: 120, y: 0 }));
241        assert_eq!(actual, expected);
242    }
243
244    #[test]
245    fn test_single_pane() {
246        let input = ",46";
247
248        let actual = single_pane(input);
249        let expected = Ok(("", Element::Pane { pane_id: 46 }));
250        assert_eq!(actual, expected);
251    }
252
253    #[test]
254    fn test_vertical_split() {
255        let input = "[279x47,0,0,82,279x23,0,48,83]";
256
257        let actual = vert_split(input);
258        let expected = Ok((
259            "",
260            Element::Vertical(Split {
261                elements: vec![
262                    Container {
263                        dimensions: Dimensions {
264                            width: 279,
265                            height: 47,
266                        },
267                        coordinates: Coordinates { x: 0, y: 0 },
268                        element: Element::Pane { pane_id: 82 },
269                    },
270                    Container {
271                        dimensions: Dimensions {
272                            width: 279,
273                            height: 23,
274                        },
275                        coordinates: Coordinates { x: 0, y: 48 },
276                        element: Element::Pane { pane_id: 83 },
277                    },
278                ],
279            }),
280        ));
281        assert_eq!(actual, expected);
282    }
283
284    #[test]
285    fn test_layout() {
286        let input = "41e9,279x71,0,0[279x40,0,0,71,279x30,0,41{147x30,0,41,72,131x30,148,41,73}]";
287
288        let actual = window_layout(input);
289        let expected = Ok((
290            "",
291            WindowLayout {
292                id: 0x41e9,
293                container: Container {
294                    dimensions: Dimensions {
295                        width: 279,
296                        height: 71,
297                    },
298                    coordinates: Coordinates { x: 0, y: 0 },
299                    element: Element::Vertical(Split {
300                        elements: vec![
301                            Container {
302                                dimensions: Dimensions {
303                                    width: 279,
304                                    height: 40,
305                                },
306                                coordinates: Coordinates { x: 0, y: 0 },
307                                element: Element::Pane { pane_id: 71 },
308                            },
309                            Container {
310                                dimensions: Dimensions {
311                                    width: 279,
312                                    height: 30,
313                                },
314                                coordinates: Coordinates { x: 0, y: 41 },
315                                element: Element::Horizontal(Split {
316                                    elements: vec![
317                                        Container {
318                                            dimensions: Dimensions {
319                                                width: 147,
320                                                height: 30,
321                                            },
322                                            coordinates: Coordinates { x: 0, y: 41 },
323                                            element: Element::Pane { pane_id: 72 },
324                                        },
325                                        Container {
326                                            dimensions: Dimensions {
327                                                width: 131,
328                                                height: 30,
329                                            },
330                                            coordinates: Coordinates { x: 148, y: 41 },
331                                            element: Element::Pane { pane_id: 73 },
332                                        },
333                                    ],
334                                }),
335                            },
336                        ],
337                    }),
338                },
339            },
340        ));
341        assert_eq!(actual, expected);
342    }
343
344    #[test]
345    fn test_pane_ids() {
346        let input = "41e9,279x71,0,0[279x40,0,0,71,279x30,0,41{147x30,0,41,72,131x30,148,41,73}]";
347        let (_, layout) = window_layout(input).unwrap();
348
349        let actual = layout.pane_ids();
350        let expected = vec![71, 72, 73];
351        assert_eq!(actual, expected);
352    }
353}