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_type(Type::List(Box::new(Type::Any)), Type::String)
26            .optional(
27                "column",
28                SyntaxShape::CellPath,
29                "Format this column in a grid.",
30            )
31            .named(
32                "width",
33                SyntaxShape::Int,
34                "Number of terminal columns wide (not output columns).",
35                Some('w'),
36            )
37            .switch("color", "Draw output with color.", Some('c'))
38            .switch(
39                "icons",
40                "Draw output with icons (assumes nerd font is used).",
41                Some('i'),
42            )
43            .named(
44                "separator",
45                SyntaxShape::String,
46                "Character to separate grid with.",
47                Some('s'),
48            )
49            .category(Category::Viewers)
50    }
51
52    fn extra_description(&self) -> &str {
53        "The `grid` command creates a concise gridded layout for the input. It
54prints every item of the list in a grid layout. For tables or list
55containing records, it will look for a 'name' column by default; if
56the 'name' column is missing, the entire record is rendered instead."
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 cell_path: Option<CellPath> = call.opt(engine_state, stack, 0)?;
67        let width_param: Option<i64> = call.get_flag(engine_state, stack, "width")?;
68        let color_param: bool = call.has_flag(engine_state, stack, "color")?;
69        let separator_param: Option<String> = call.get_flag(engine_state, stack, "separator")?;
70        let icons_param: bool = call.has_flag(engine_state, stack, "icons")?;
71        let config = &stack.get_config(engine_state);
72        let env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
73            Some(v) => Some(env_to_string("LS_COLORS", v, engine_state, stack)?),
74            None => None,
75        };
76
77        let use_color: bool = color_param && config.use_ansi_coloring.get(engine_state);
78        let cwd = engine_state.cwd(Some(stack))?;
79
80        match input {
81            PipelineData::Value(Value::List { vals, .. }, ..) => {
82                // dbg!("value::list");
83                let items = convert_to_list(vals, cell_path, config)?;
84                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            }
95            PipelineData::ListStream(stream, ..) => {
96                // dbg!("value::stream");
97                let items = convert_to_list(stream, cell_path, config)?;
98                create_grid_output(
99                    items,
100                    call,
101                    width_param,
102                    use_color,
103                    separator_param,
104                    env_str,
105                    icons_param,
106                    cwd.as_ref(),
107                )
108            }
109            x => {
110                // dbg!("other value");
111                // dbg!(x.get_type());
112                Ok(x)
113            }
114        }
115    }
116
117    fn examples(&self) -> Vec<Example<'_>> {
118        vec![
119            Example {
120                description: "Render a simple list to a grid",
121                example: "[1 2 3 a b c] | grid",
122                result: Some(Value::test_string("1 │ 2 │ 3 │ a │ b │ c\n")),
123            },
124            Example {
125                description: "The above example is the same as:",
126                example: "[1 2 3 a b c] | wrap name | grid name",
127                result: Some(Value::test_string("1 │ 2 │ 3 │ a │ b │ c\n")),
128            },
129            Example {
130                description: "Render a list of records to a grid",
131                example: "[{name: 'A', v: 1} {name: 'B', v: 2} {name: 'C', v: 3}] | grid name",
132                result: Some(Value::test_string("A │ B │ C\n")),
133            },
134            Example {
135                description: "Render a table with 'name' column in it to a grid",
136                example: "[[name patch]; [0.1.0 false] [0.1.1 true] [0.2.0 false]] | grid name",
137                result: Some(Value::test_string("0.1.0 │ 0.1.1 │ 0.2.0\n")),
138            },
139            Example {
140                description: "Render a table with 'name' column in it to a grid with icons and colors",
141                example: "ls | grid --icons --color name",
142                result: None,
143            },
144        ]
145    }
146}
147
148#[allow(clippy::too_many_arguments)]
149fn create_grid_output(
150    items: Vec<String>,
151    call: &Call,
152    width_param: Option<i64>,
153    use_color: bool,
154    separator_param: Option<String>,
155    env_str: Option<String>,
156    icons_param: bool,
157    cwd: &Path,
158) -> Result<PipelineData, ShellError> {
159    let ls_colors = get_ls_colors(env_str);
160
161    let cols = if let Some(col) = width_param {
162        col as u16
163    } else if let Ok((w, _h)) = terminal_size() {
164        w
165    } else {
166        80u16
167    };
168    let sep = if let Some(separator) = separator_param {
169        separator
170    } else {
171        " │ ".to_string()
172    };
173
174    let mut grid = Grid::new(GridOptions {
175        direction: Direction::TopToBottom,
176        filling: Filling::Text(sep),
177    });
178
179    for value in items {
180        if use_color {
181            if icons_param {
182                let no_ansi = nu_utils::strip_ansi_unlikely(&value);
183                let path = cwd.join(no_ansi.as_ref());
184                let file_icon = icon_for_file(&path, &None);
185                let ls_colors_style = ls_colors.style_for_path(path);
186                let icon_style = lookup_ansi_color_style(file_icon.color);
187
188                let ansi_style = ls_colors_style
189                    .map(Style::to_nu_ansi_term_style)
190                    .unwrap_or_default();
191
192                let item = format!(
193                    "{} {}",
194                    icon_style.paint(String::from(file_icon.icon)),
195                    ansi_style.paint(value)
196                );
197
198                let mut cell = Cell::from(item);
199                cell.alignment = Alignment::Left;
200                grid.add(cell);
201            } else {
202                let no_ansi = nu_utils::strip_ansi_unlikely(&value);
203                let path = cwd.join(no_ansi.as_ref());
204                let style = ls_colors.style_for_path(path.clone());
205                let ansi_style = style.map(Style::to_nu_ansi_term_style).unwrap_or_default();
206                let mut cell = Cell::from(ansi_style.paint(value).to_string());
207                cell.alignment = Alignment::Left;
208                grid.add(cell);
209            }
210        } else if icons_param {
211            let no_ansi = nu_utils::strip_ansi_unlikely(&value);
212            let path = cwd.join(no_ansi.as_ref());
213            let file_icon = icon_for_file(&path, &None);
214            let item = format!("{} {}", String::from(file_icon.icon), value);
215            let mut cell = Cell::from(item);
216            cell.alignment = Alignment::Left;
217            grid.add(cell);
218        } else {
219            let mut cell = Cell::from(value);
220            cell.alignment = Alignment::Left;
221            grid.add(cell);
222        }
223    }
224
225    if let Some(grid_display) = grid.fit_into_width(cols as usize) {
226        Ok(Value::string(grid_display.to_string(), call.head).into_pipeline_data())
227    } else {
228        Err(ShellError::Generic(
229            GenericError::new(
230                format!("Couldn't fit grid into {cols} columns"),
231                "too few columns to fit the grid into",
232                call.head,
233            )
234            .with_help("try rerunning with a different --width"),
235        ))
236    }
237}
238
239/// Converts an iterator of values into a list of expanded strings, suitable for grid layouts.
240///
241/// This function supports two evaluation paths depending on the presence of a cell path:
242///
243/// - **Explicit Path:** If a `cell_path` is specified (e.g., `ls | grid name`), it extracts the
244///   value at that inner path for every item.
245/// - **Implicit Fallback:** If no path is provided (e.g., `ls | grid`), it checks if the item is
246///   a `Value::Record`. If the record contains a `"name"` column, it extracts that value;
247///   otherwise, it falls back to processing the item as-is.
248///
249/// # Errors
250///
251/// Returns a `ShellError` if any item evaluates to a `Value::Error`, or if a provided
252/// `cell_path` fails to resolve against the data structure.
253fn convert_to_list(
254    iter: impl IntoIterator<Item = Value>,
255    cell_path: Option<CellPath>,
256    config: &Config,
257) -> Result<Vec<String>, ShellError> {
258    let iter = iter.into_iter();
259
260    if let Some(cell_path) = cell_path {
261        // Path A: Explicit cell path provided (e.g., `ls | grid name`)
262        iter.map(|item| {
263            if let Value::Error { error, .. } = item {
264                return Err(*error);
265            }
266
267            let string = item
268                .follow_cell_path(&cell_path.members)?
269                .to_expanded_string(", ", config);
270
271            Ok(string)
272        })
273        .collect()
274    } else {
275        // Path B: Implicit fallback (e.g., `ls | grid`). Matches the "name" column if present.
276
277        iter.map(|item| {
278            let target_value = match &item {
279                Value::Record { val, .. } => val.get("name").unwrap_or(&item),
280                item => item,
281            };
282
283            match target_value {
284                Value::Error { error, .. } => Err(*error.clone()),
285                val => Ok(val.to_expanded_string(", ", config)),
286            }
287        })
288        .collect()
289    }
290}
291#[cfg(test)]
292mod test {
293    #[test]
294    fn test_examples() -> nu_test_support::Result {
295        use super::Griddle;
296        nu_test_support::test().examples(Griddle)
297    }
298}