Skip to main content

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