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