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