Skip to main content

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::shell_error::generic::GenericError;
6use nu_protocol::{Config, ReportMode, report_shell_warning};
7use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions};
8use nu_utils::{get_ls_colors, terminal_size};
9use std::path::Path;
10
11// TODO: there are some deprecated stuff that should be removed after version
12// 0.113.0 is released. Things to do:
13// - remove the `PipelineData::Value(Value::Record { .. }, ..)` arm
14// - remove the `Type::record()` from the command signature
15// - remove the example which showcases record as input
16// - remove the `DeprecationInfo` struct and other associated code
17// - remove the `NAME_COLUMN` const
18// - merge and clean up`convert_to_list` and `convert_to_list_legacy`
19// - and finally update the tests
20
21const NAME_COLUMN: &str = "name";
22
23#[derive(Clone)]
24pub struct Griddle;
25
26impl Command for Griddle {
27    fn name(&self) -> &str {
28        "grid"
29    }
30
31    fn description(&self) -> &str {
32        "Renders the output to a textual terminal grid."
33    }
34
35    fn signature(&self) -> nu_protocol::Signature {
36        Signature::build("grid")
37            .input_output_types(vec![
38                (Type::List(Box::new(Type::Any)), Type::String),
39                (Type::record(), Type::String),
40            ])
41            .optional(
42                "column",
43                SyntaxShape::CellPath,
44                "Format this column in a grid.",
45            )
46            .named(
47                "width",
48                SyntaxShape::Int,
49                "Number of terminal columns wide (not output columns).",
50                Some('w'),
51            )
52            .switch("color", "Draw output with color.", Some('c'))
53            .switch(
54                "icons",
55                "Draw output with icons (assumes nerd font is used).",
56                Some('i'),
57            )
58            .named(
59                "separator",
60                SyntaxShape::String,
61                "Character to separate grid with.",
62                Some('s'),
63            )
64            .category(Category::Viewers)
65    }
66
67    fn extra_description(&self) -> &str {
68        "The `grid` command creates a concise gridded layout for the input. It
69prints every item of the list in a grid layout. However, for table,
70you need to provide the name of the column you want to put in the grid."
71    }
72
73    fn run(
74        &self,
75        engine_state: &EngineState,
76        stack: &mut Stack,
77        call: &Call,
78        input: PipelineData,
79    ) -> Result<PipelineData, ShellError> {
80        let cell_path: Option<CellPath> = call.opt(engine_state, stack, 0)?;
81        let width_param: Option<i64> = call.get_flag(engine_state, stack, "width")?;
82        let color_param: bool = call.has_flag(engine_state, stack, "color")?;
83        let separator_param: Option<String> = call.get_flag(engine_state, stack, "separator")?;
84        let icons_param: bool = call.has_flag(engine_state, stack, "icons")?;
85        let config = &stack.get_config(engine_state);
86        let env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
87            Some(v) => Some(env_to_string("LS_COLORS", v, engine_state, stack)?),
88            None => None,
89        };
90
91        let use_color: bool = color_param && config.use_ansi_coloring.get(engine_state);
92        let cwd = engine_state.cwd(Some(stack))?;
93
94        let deprecation_info = DeprecationInfo {
95            engine_state,
96            span: call.head,
97        };
98
99        match input {
100            PipelineData::Value(Value::List { vals, .. }, ..) => {
101                // dbg!("value::list");
102                let items = convert_to_list(vals, cell_path, config, deprecation_info)?;
103                create_grid_output(
104                    items,
105                    call,
106                    width_param,
107                    use_color,
108                    separator_param,
109                    env_str,
110                    icons_param,
111                    cwd.as_ref(),
112                )
113            }
114            PipelineData::ListStream(stream, ..) => {
115                // dbg!("value::stream");
116                let items = convert_to_list(stream, cell_path, config, deprecation_info)?;
117                create_grid_output(
118                    items,
119                    call,
120                    width_param,
121                    use_color,
122                    separator_param,
123                    env_str,
124                    icons_param,
125                    cwd.as_ref(),
126                )
127            }
128            PipelineData::Value(record @ Value::Record { .. }, ..) => {
129                // dbg!("value::record");
130
131                report_shell_warning(
132                    Some(stack),
133                    engine_state,
134                    &ShellWarning::Deprecated {
135                        dep_type: "Behavior".into(),
136                        label: "wrap the record inside a list.".into(),
137                        span: record.span(),
138                        help: Some(
139                            "Since 0.112.2, passing a record to `grid` command is deprecated. \
140                        It is expected to be removed in version 0.114.0"
141                                .into(),
142                        ),
143                        report_mode: ReportMode::FirstUse,
144                    },
145                );
146
147                let items = record
148                    .into_record()
149                    .expect("this is a record")
150                    .get(NAME_COLUMN)
151                    .map(|v| v.to_expanded_string(", ", config))
152                    .into_iter()
153                    .collect();
154
155                Ok(create_grid_output(
156                    items,
157                    call,
158                    width_param,
159                    use_color,
160                    separator_param,
161                    env_str,
162                    icons_param,
163                    cwd.as_ref(),
164                )?)
165            }
166            x => {
167                // dbg!("other value");
168                // dbg!(x.get_type());
169                Ok(x)
170            }
171        }
172    }
173
174    fn examples(&self) -> Vec<Example<'_>> {
175        vec![
176            Example {
177                description: "Render a simple list to a grid",
178                example: "[1 2 3 a b c] | grid",
179                result: Some(Value::test_string("1 │ 2 │ 3 │ a │ b │ c\n")),
180            },
181            Example {
182                description: "The above example is the same as:",
183                example: "[1 2 3 a b c] | wrap name | grid name",
184                result: Some(Value::test_string("1 │ 2 │ 3 │ a │ b │ c\n")),
185            },
186            Example {
187                description: "Render a record to a grid (deprecated)",
188                example: "{name: 'foo', b: 1, c: 2} | grid",
189                result: Some(Value::test_string("foo\n")),
190            },
191            Example {
192                description: "Render a list of records to a grid",
193                example: "[{name: 'A', v: 1} {name: 'B', v: 2} {name: 'C', v: 3}] | grid name",
194                result: Some(Value::test_string("A │ B │ C\n")),
195            },
196            Example {
197                description: "Render a table with 'name' column in it to a grid",
198                example: "[[name patch]; [0.1.0 false] [0.1.1 true] [0.2.0 false]] | grid name",
199                result: Some(Value::test_string("0.1.0 │ 0.1.1 │ 0.2.0\n")),
200            },
201            Example {
202                description: "Render a table with 'name' column in it to a grid with icons and colors",
203                example: "ls | grid --icons --color name",
204                result: None,
205            },
206        ]
207    }
208}
209
210#[allow(clippy::too_many_arguments)]
211fn create_grid_output(
212    items: Vec<String>,
213    call: &Call,
214    width_param: Option<i64>,
215    use_color: bool,
216    separator_param: Option<String>,
217    env_str: Option<String>,
218    icons_param: bool,
219    cwd: &Path,
220) -> Result<PipelineData, ShellError> {
221    let ls_colors = get_ls_colors(env_str);
222
223    let cols = if let Some(col) = width_param {
224        col as u16
225    } else if let Ok((w, _h)) = terminal_size() {
226        w
227    } else {
228        80u16
229    };
230    let sep = if let Some(separator) = separator_param {
231        separator
232    } else {
233        " │ ".to_string()
234    };
235
236    let mut grid = Grid::new(GridOptions {
237        direction: Direction::TopToBottom,
238        filling: Filling::Text(sep),
239    });
240
241    for value in items {
242        if use_color {
243            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 ls_colors_style = ls_colors.style_for_path(path);
248                let icon_style = lookup_ansi_color_style(file_icon.color);
249
250                let ansi_style = ls_colors_style
251                    .map(Style::to_nu_ansi_term_style)
252                    .unwrap_or_default();
253
254                let item = format!(
255                    "{} {}",
256                    icon_style.paint(String::from(file_icon.icon)),
257                    ansi_style.paint(value)
258                );
259
260                let mut cell = Cell::from(item);
261                cell.alignment = Alignment::Left;
262                grid.add(cell);
263            } else {
264                let no_ansi = nu_utils::strip_ansi_unlikely(&value);
265                let path = cwd.join(no_ansi.as_ref());
266                let style = ls_colors.style_for_path(path.clone());
267                let ansi_style = style.map(Style::to_nu_ansi_term_style).unwrap_or_default();
268                let mut cell = Cell::from(ansi_style.paint(value).to_string());
269                cell.alignment = Alignment::Left;
270                grid.add(cell);
271            }
272        } else if icons_param {
273            let no_ansi = nu_utils::strip_ansi_unlikely(&value);
274            let path = cwd.join(no_ansi.as_ref());
275            let file_icon = icon_for_file(&path, &None);
276            let item = format!("{} {}", String::from(file_icon.icon), value);
277            let mut cell = Cell::from(item);
278            cell.alignment = Alignment::Left;
279            grid.add(cell);
280        } else {
281            let mut cell = Cell::from(value);
282            cell.alignment = Alignment::Left;
283            grid.add(cell);
284        }
285    }
286
287    if let Some(grid_display) = grid.fit_into_width(cols as usize) {
288        Ok(Value::string(grid_display.to_string(), call.head).into_pipeline_data())
289    } else {
290        Err(ShellError::Generic(
291            GenericError::new(
292                format!("Couldn't fit grid into {cols} columns"),
293                "too few columns to fit the grid into",
294                call.head,
295            )
296            .with_help("try rerunning with a different --width"),
297        ))
298    }
299}
300
301struct DeprecationInfo<'a> {
302    engine_state: &'a EngineState,
303    span: Span,
304}
305
306fn convert_to_list(
307    iter: impl IntoIterator<Item = Value>,
308    cell_path: Option<CellPath>,
309    config: &Config,
310    deprecation_info: DeprecationInfo,
311) -> Result<Vec<String>, ShellError> {
312    let Some(cell_path) = cell_path else {
313        return convert_to_list_legacy(iter, config, deprecation_info);
314    };
315
316    iter.into_iter()
317        .map(|item| {
318            if let Value::Error { error, .. } = item {
319                return Err(*error);
320            }
321
322            let string = item
323                .follow_cell_path(&cell_path.members)?
324                .to_expanded_string(", ", config);
325
326            Ok(string)
327        })
328        .collect()
329}
330
331fn convert_to_list_legacy(
332    iter: impl IntoIterator<Item = Value>,
333    config: &Config,
334    deprecation_info: DeprecationInfo,
335) -> Result<Vec<String>, ShellError> {
336    let mut iter = iter.into_iter().peekable();
337
338    let Some(first) = iter.peek() else {
339        return Ok(vec![]);
340    };
341
342    let headers = first.columns().collect::<Vec<_>>();
343    let has_name_header = headers.iter().any(|&str| str == NAME_COLUMN);
344
345    if has_name_header {
346        report_shell_warning(
347            None,
348            deprecation_info.engine_state,
349            &ShellWarning::Deprecated {
350                dep_type: "Behavior".into(),
351                label: "add the name of the column you want to display (e.g. name)".into(),
352                span: deprecation_info.span,
353                help: Some("It is expected to be removed in version 0.114.0".into()),
354                report_mode: ReportMode::FirstUse,
355            },
356        );
357    }
358
359    if !headers.is_empty() && !has_name_header {
360        return Ok(vec![]);
361    }
362
363    iter.map(|item| {
364        if let Value::Error { error, .. } = item {
365            return Err(*error);
366        }
367
368        let string = if !has_name_header {
369            item.to_expanded_string(", ", config)
370        } else {
371            let result = match &item {
372                Value::Record { val, .. } => val.get(NAME_COLUMN),
373                item => Some(item),
374            };
375
376            match result {
377                Some(value) => {
378                    if let Value::Error { error, .. } = item {
379                        return Err(*error);
380                    }
381                    value.to_expanded_string(", ", config)
382                }
383                None => String::new(),
384            }
385        };
386
387        Ok(string)
388    })
389    .collect()
390}
391
392#[cfg(test)]
393mod test {
394    #[test]
395    fn test_examples() -> nu_test_support::Result {
396        use super::Griddle;
397        nu_test_support::test().examples(Griddle)
398    }
399}