aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-server/src/ui/components/mod.rs
blob: f47f97c42dfd68fef5c87d198c95c1ccc5b7bdfa (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
mod component_coordinates;
mod nested_list;
mod ribbon;
mod table;
mod text;

use crate::panes::grid::Grid;
use lazy_static::lazy_static;
use regex::Regex;
use vte;
use zellij_utils::data::Style;
use zellij_utils::errors::prelude::*;

use component_coordinates::{is_too_high, is_too_wide, Coordinates};
use nested_list::{nested_list, parse_nested_list_items};
use ribbon::ribbon;
use table::table;
use text::{parse_text, parse_text_params, stringify_text, text, Text};

macro_rules! parse_next_param {
    ($next_param:expr, $type:ident, $component_name:expr, $item_name:expr) => {{
        $next_param
            .and_then(|stringified_param| stringified_param.parse::<$type>().ok())
            .with_context(|| format!("{} must have {}", $component_name, $item_name))?
    }};
}

macro_rules! parse_vte_bytes {
    ($self:expr, $encoded_component:expr) => {{
        let mut vte_parser = vte::Parser::new();
        vte_parser.advance($self.grid, &$encoded_component);
    }};
}

#[derive(Debug)]
pub struct UiComponentParser<'a> {
    grid: &'a mut Grid,
    style: Style,
    arrow_fonts: bool,
}

impl<'a> UiComponentParser<'a> {
    pub fn new(grid: &'a mut Grid, style: Style, arrow_fonts: bool) -> Self {
        UiComponentParser {
            grid,
            style,
            arrow_fonts,
        }
    }
    pub fn parse(&mut self, bytes: Vec<u8>) -> Result<()> {
        // The stages of parsing:
        // 1. We decode the bytes to utf8 and get something like (as a String): `component_name;111;222;333`
        // 2. We split this string by `;` to get at the parameters themselves
        // 3. We extract the component name, and then behave according to the component
        // 4. Some components interpret their parameters as bytes, and so have another layer of
        //    utf8 decoding, others would take them verbatim, some will act depending on their
        //    placement (eg. the `table` component treats the first two parameters as integers for
        //    the columns/rows of the table, and then treats the rest of the component as utf8
        //    encoded bytes, each one representing one cell in the table)
        // 5. Each component parses its parameters, creating a String of ANSI instructions of its
        //    own representing instructions to create the component
        // 6. Finally, we take this string, encode it back into bytes and pass it back through the ANSI
        //    parser (our `Grid`) in order to create a representation of it on screen
        let mut params: Vec<String> = String::from_utf8_lossy(&bytes)
            .to_string()
            .split(';')
            .map(|c| c.to_owned())
            .collect();
        let mut params_iter = params.iter_mut().peekable();
        let component_name = params_iter
            .next()
            .with_context(|| format!("ui component must have a name"))?;

        // parse coordinates
        let mut component_coordinates = None;
        if let Some(coordinates) = params_iter.peek() {
            component_coordinates = self.parse_coordinates(coordinates)?;
            if component_coordinates.is_some() {
                let _ = params_iter.next(); // we just peeked, let's consume the coords now
            }
        }

        if component_name == &"table" {
            let columns = parse_next_param!(params_iter.next(), usize, "table", "columns");
            let rows = parse_next_param!(params_iter.next(), usize, "table", "rows");
            let stringified_params = parse_text_params(params_iter);
            let encoded_table = table(
                columns,
                rows,
                stringified_params,
                &self.style,
                component_coordinates,
            );
            parse_vte_bytes!(self, encoded_table);
            Ok(())
        } else if component_name == &"ribbon" {
            let stringified_params = parse_text_params(params_iter)
                .into_iter()
                .next()
                .with_context(|| format!("a ribbon must have text"))?;
            let encoded_text = ribbon(
                stringified_params,
                &self.style,
                self.arrow_fonts,
                component_coordinates,
            );
            parse_vte_bytes!(self, encoded_text);
            Ok(())
        } else if component_name == &"nested_list" {
            let nested_list_items = parse_nested_list_items(params_iter);
            let encoded_nested_list =
                nested_list(nested_list_items, &self.style, component_coordinates);
            parse_vte_bytes!(self, encoded_nested_list);
            Ok(())
        } else if component_name == &"text" {
            let stringified_params = parse_text_params(params_iter)
                .into_iter()
                .next()
                .with_context(|| format!("text must have, well, text..."))?;
            let encoded_text = text(stringified_params, &self.style, component_coordinates);
            parse_vte_bytes!(self, encoded_text);
            Ok(())
        } else {
            Err(anyhow!("Unknown component: {}", component_name))
        }
    }
    fn parse_coordinates(&self, coordinates: &str) -> Result<Option<Coordinates>> {
        lazy_static! {
            static ref RE: Regex = Regex::new(r"(\d*)/(\d*)/(\d*)/(\d*)").unwrap();
        }
        if let Some(captures) = RE.captures_iter(&coordinates).next() {
            let x = captures[1].parse::<usize>().with_context(|| {
                format!(
                    "Failed to parse x coordinates for string: {:?}",
                    coordinates
                )
            })?;
            let y = captures[2].parse::<usize>().with_context(|| {
                format!(
                    "Failed to parse y coordinates for string: {:?}",
                    coordinates
                )
            })?;
            let width = captures[3].parse::<usize>().ok();
            let height = captures[4].parse::<usize>().ok();
            Ok(Some(Coordinates {
                x,
                y,
                width,
                height,
            }))
        } else {
            Ok(None)
        }
    }
}

fn parse_flag(stringified: &mut String, flag: char) -> bool {
    const FLAG_CHARS: [char; 3] = ['x', 'z', 'd'];
    let prefix_len = stringified
        .chars()
        .take_while(|c| FLAG_CHARS.contains(c))
        .count();
    if let Some(position) = stringified[..prefix_len].find(flag) {
        stringified.remove(position);
        true
    } else {
        false
    }
}

fn parse_selected(stringified: &mut String) -> bool {
    parse_flag(stringified, 'x')
}

fn parse_opaque(stringified: &mut String) -> bool {
    parse_flag(stringified, 'z')
}

fn parse_disabled(stringified: &mut String) -> bool {
    parse_flag(stringified, 'd')
}

fn parse_indices(stringified: &mut String) -> Vec<Vec<usize>> {
    stringified
        .chars()
        .collect::<Vec<_>>()
        .iter()
        .rposition(|c| c == &'$')
        .map(|last_position| stringified.drain(0..=last_position).collect::<String>())
        .map(|indices_string| {
            let mut all_indices = vec![];
            let raw_indices_for_each_variant = indices_string.split('$');
            for index_string in raw_indices_for_each_variant {
                let indices_for_variant = index_string
                    .split(',')
                    .filter_map(|s| s.parse::<usize>().ok())
                    .collect();
                all_indices.push(indices_for_variant)
            }
            all_indices
        })
        .unwrap_or_default()
}