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 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 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 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
239fn 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 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 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}