nu_command/viewers/
griddle.rs

1use devicons::icon_for_file;
2use lscolors::Style;
3use nu_color_config::lookup_ansi_color_style;
4use nu_engine::{command_prelude::*, env_to_string};
5use nu_protocol::Config;
6use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions};
7use nu_utils::{get_ls_colors, terminal_size};
8use std::path::Path;
9
10#[derive(Clone)]
11pub struct Griddle;
12
13impl Command for Griddle {
14    fn name(&self) -> &str {
15        "grid"
16    }
17
18    fn description(&self) -> &str {
19        "Renders the output to a textual terminal grid."
20    }
21
22    fn signature(&self) -> nu_protocol::Signature {
23        Signature::build("grid")
24            .input_output_types(vec![
25                (Type::List(Box::new(Type::Any)), Type::String),
26                (Type::record(), Type::String),
27            ])
28            .named(
29                "width",
30                SyntaxShape::Int,
31                "number of terminal columns wide (not output columns)",
32                Some('w'),
33            )
34            .switch("color", "draw output with color", Some('c'))
35            .switch(
36                "icons",
37                "draw output with icons (assumes nerd font is used)",
38                Some('i'),
39            )
40            .named(
41                "separator",
42                SyntaxShape::String,
43                "character to separate grid with",
44                Some('s'),
45            )
46            .category(Category::Viewers)
47    }
48
49    fn extra_description(&self) -> &str {
50        r#"grid was built to give a concise gridded layout for ls. however,
51it determines what to put in the grid by looking for a column named
52'name'. this works great for tables and records but for lists we
53need to do something different. such as with '[one two three] | grid'
54it creates a fake column called 'name' for these values so that it
55prints out the list properly."#
56    }
57
58    fn run(
59        &self,
60        engine_state: &EngineState,
61        stack: &mut Stack,
62        call: &Call,
63        input: PipelineData,
64    ) -> Result<PipelineData, ShellError> {
65        let width_param: Option<i64> = call.get_flag(engine_state, stack, "width")?;
66        let color_param: bool = call.has_flag(engine_state, stack, "color")?;
67        let separator_param: Option<String> = call.get_flag(engine_state, stack, "separator")?;
68        let icons_param: bool = call.has_flag(engine_state, stack, "icons")?;
69        let config = &stack.get_config(engine_state);
70        let env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
71            Some(v) => Some(env_to_string("LS_COLORS", v, engine_state, stack)?),
72            None => None,
73        };
74
75        let use_color: bool = color_param && config.use_ansi_coloring.get(engine_state);
76        let cwd = engine_state.cwd(Some(stack))?;
77
78        match input {
79            PipelineData::Value(Value::List { vals, .. }, ..) => {
80                // dbg!("value::list");
81                let data = convert_to_list(vals, config)?;
82                if let Some(items) = data {
83                    Ok(create_grid_output(
84                        items,
85                        call,
86                        width_param,
87                        use_color,
88                        separator_param,
89                        env_str,
90                        icons_param,
91                        cwd.as_ref(),
92                    )?)
93                } else {
94                    Ok(PipelineData::empty())
95                }
96            }
97            PipelineData::ListStream(stream, ..) => {
98                // dbg!("value::stream");
99                let data = convert_to_list(stream, config)?;
100                if let Some(items) = data {
101                    Ok(create_grid_output(
102                        items,
103                        call,
104                        width_param,
105                        use_color,
106                        separator_param,
107                        env_str,
108                        icons_param,
109                        cwd.as_ref(),
110                    )?)
111                } else {
112                    // dbg!(data);
113                    Ok(PipelineData::empty())
114                }
115            }
116            PipelineData::Value(Value::Record { val, .. }, ..) => {
117                // dbg!("value::record");
118                let mut items = vec![];
119
120                for (i, (c, v)) in val.into_owned().into_iter().enumerate() {
121                    items.push((i, c, v.to_expanded_string(", ", config)))
122                }
123
124                Ok(create_grid_output(
125                    items,
126                    call,
127                    width_param,
128                    use_color,
129                    separator_param,
130                    env_str,
131                    icons_param,
132                    cwd.as_ref(),
133                )?)
134            }
135            x => {
136                // dbg!("other value");
137                // dbg!(x.get_type());
138                Ok(x)
139            }
140        }
141    }
142
143    fn examples(&self) -> Vec<Example> {
144        vec![
145            Example {
146                description: "Render a simple list to a grid",
147                example: "[1 2 3 a b c] | grid",
148                result: Some(Value::test_string("1 │ 2 │ 3 │ a │ b │ c\n")),
149            },
150            Example {
151                description: "The above example is the same as:",
152                example: "[1 2 3 a b c] | wrap name | grid",
153                result: Some(Value::test_string("1 │ 2 │ 3 │ a │ b │ c\n")),
154            },
155            Example {
156                description: "Render a record to a grid",
157                example: "{name: 'foo', b: 1, c: 2} | grid",
158                result: Some(Value::test_string("foo\n")),
159            },
160            Example {
161                description: "Render a list of records to a grid",
162                example: "[{name: 'A', v: 1} {name: 'B', v: 2} {name: 'C', v: 3}] | grid",
163                result: Some(Value::test_string("A │ B │ C\n")),
164            },
165            Example {
166                description: "Render a table with 'name' column in it to a grid",
167                example: "[[name patch]; [0.1.0 false] [0.1.1 true] [0.2.0 false]] | grid",
168                result: Some(Value::test_string("0.1.0 │ 0.1.1 │ 0.2.0\n")),
169            },
170            Example {
171                description: "Render a table with 'name' column in it to a grid with icons and colors",
172                example: "[[name patch]; [Cargo.toml false] [README.md true] [SECURITY.md false]] | grid --icons --color",
173                result: None,
174            },
175        ]
176    }
177}
178
179#[allow(clippy::too_many_arguments)]
180fn create_grid_output(
181    items: Vec<(usize, String, String)>,
182    call: &Call,
183    width_param: Option<i64>,
184    use_color: bool,
185    separator_param: Option<String>,
186    env_str: Option<String>,
187    icons_param: bool,
188    cwd: &Path,
189) -> Result<PipelineData, ShellError> {
190    let ls_colors = get_ls_colors(env_str);
191
192    let cols = if let Some(col) = width_param {
193        col as u16
194    } else if let Ok((w, _h)) = terminal_size() {
195        w
196    } else {
197        80u16
198    };
199    let sep = if let Some(separator) = separator_param {
200        separator
201    } else {
202        " │ ".to_string()
203    };
204
205    let mut grid = Grid::new(GridOptions {
206        direction: Direction::TopToBottom,
207        filling: Filling::Text(sep),
208    });
209
210    for (_row_index, header, value) in items {
211        // only output value if the header name is 'name'
212        if header == "name" {
213            if use_color {
214                if icons_param {
215                    let no_ansi = nu_utils::strip_ansi_unlikely(&value);
216                    let path = cwd.join(no_ansi.as_ref());
217                    let file_icon = icon_for_file(&path, &None);
218                    let ls_colors_style = ls_colors.style_for_path(path);
219                    let icon_style = lookup_ansi_color_style(file_icon.color);
220
221                    let ansi_style = ls_colors_style
222                        .map(Style::to_nu_ansi_term_style)
223                        .unwrap_or_default();
224
225                    let item = format!(
226                        "{} {}",
227                        icon_style.paint(String::from(file_icon.icon)),
228                        ansi_style.paint(value)
229                    );
230
231                    let mut cell = Cell::from(item);
232                    cell.alignment = Alignment::Left;
233                    grid.add(cell);
234                } else {
235                    let no_ansi = nu_utils::strip_ansi_unlikely(&value);
236                    let path = cwd.join(no_ansi.as_ref());
237                    let style = ls_colors.style_for_path(path.clone());
238                    let ansi_style = style.map(Style::to_nu_ansi_term_style).unwrap_or_default();
239                    let mut cell = Cell::from(ansi_style.paint(value).to_string());
240                    cell.alignment = Alignment::Left;
241                    grid.add(cell);
242                }
243            } else if icons_param {
244                let no_ansi = nu_utils::strip_ansi_unlikely(&value);
245                let path = cwd.join(no_ansi.as_ref());
246                let file_icon = icon_for_file(&path, &None);
247                let item = format!("{} {}", String::from(file_icon.icon), value);
248                let mut cell = Cell::from(item);
249                cell.alignment = Alignment::Left;
250                grid.add(cell);
251            } else {
252                let mut cell = Cell::from(value);
253                cell.alignment = Alignment::Left;
254                grid.add(cell);
255            }
256        }
257    }
258
259    if let Some(grid_display) = grid.fit_into_width(cols as usize) {
260        Ok(Value::string(grid_display.to_string(), call.head).into_pipeline_data())
261    } else {
262        Err(ShellError::GenericError {
263            error: format!("Couldn't fit grid into {cols} columns"),
264            msg: "too few columns to fit the grid into".into(),
265            span: Some(call.head),
266            help: Some("try rerunning with a different --width".into()),
267            inner: Vec::new(),
268        })
269    }
270}
271
272#[allow(clippy::type_complexity)]
273fn convert_to_list(
274    iter: impl IntoIterator<Item = Value>,
275    config: &Config,
276) -> Result<Option<Vec<(usize, String, String)>>, ShellError> {
277    let mut iter = iter.into_iter().peekable();
278
279    if let Some(first) = iter.peek() {
280        let mut headers: Vec<String> = first.columns().cloned().collect();
281
282        if !headers.is_empty() {
283            headers.insert(0, "#".into());
284        }
285
286        let mut data = vec![];
287
288        for (row_num, item) in iter.enumerate() {
289            if let Value::Error { error, .. } = item {
290                return Err(*error);
291            }
292
293            let mut row = vec![row_num.to_string()];
294
295            if headers.is_empty() {
296                row.push(item.to_expanded_string(", ", config))
297            } else {
298                for header in headers.iter().skip(1) {
299                    let result = match &item {
300                        Value::Record { val, .. } => val.get(header),
301                        item => Some(item),
302                    };
303
304                    match result {
305                        Some(value) => {
306                            if let Value::Error { error, .. } = item {
307                                return Err(*error);
308                            }
309                            row.push(value.to_expanded_string(", ", config));
310                        }
311                        None => row.push(String::new()),
312                    }
313                }
314            }
315
316            data.push(row);
317        }
318
319        let mut h: Vec<String> = headers.into_iter().collect();
320
321        // This is just a list
322        if h.is_empty() {
323            // let's fake the header
324            h.push("#".to_string());
325            h.push("name".to_string());
326        }
327
328        // this tuple is (row_index, header_name, value)
329        let mut interleaved = vec![];
330        for (i, v) in data.into_iter().enumerate() {
331            for (n, s) in v.into_iter().enumerate() {
332                if h.len() == 1 {
333                    // always get the 1th element since this is a simple list
334                    // and we hacked the header above because it was empty
335                    // 0th element is an index, 1th element is the value
336                    interleaved.push((i, h[1].clone(), s))
337                } else {
338                    interleaved.push((i, h[n].clone(), s))
339                }
340            }
341        }
342
343        Ok(Some(interleaved))
344    } else {
345        Ok(None)
346    }
347}
348
349#[cfg(test)]
350mod test {
351    #[test]
352    fn test_examples() {
353        use super::Griddle;
354        use crate::test_examples;
355        test_examples(Griddle {})
356    }
357}