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
11const 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 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 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 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 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}