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 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 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 Ok(PipelineData::empty())
115 }
116 }
117 PipelineData::Value(Value::Record { val, .. }, ..) => {
118 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 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 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 if h.is_empty() {
325 h.push("#".to_string());
327 h.push("name".to_string());
328 }
329
330 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 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}