1use std::{cmp::max, collections::HashMap};
2
3use nu_color_config::{Alignment, StyleComputer, TextStyle};
4use nu_engine::column::get_columns;
5use nu_protocol::{Config, Record, ShellError, Span, Value};
6
7use tabled::grid::config::Position;
8
9use crate::{
10 common::{
11 check_value, configure_table, error_sign, get_header_style, get_index_style, load_theme,
12 nu_value_to_string, nu_value_to_string_clean, nu_value_to_string_colored, wrap_text,
13 NuText, StringResult, TableResult, INDEX_COLUMN_NAME,
14 },
15 string_width,
16 types::has_index,
17 NuRecordsValue, NuTable, TableOpts, TableOutput,
18};
19
20#[derive(Debug, Clone)]
21pub struct ExpandedTable {
22 expand_limit: Option<usize>,
23 flatten: bool,
24 flatten_sep: String,
25}
26
27impl ExpandedTable {
28 pub fn new(expand_limit: Option<usize>, flatten: bool, flatten_sep: String) -> Self {
29 Self {
30 expand_limit,
31 flatten,
32 flatten_sep,
33 }
34 }
35
36 pub fn build_value(self, item: &Value, opts: TableOpts<'_>) -> NuText {
37 let cfg = Cfg { opts, format: self };
38 let cell = expand_entry(item, cfg);
39 (cell.text, cell.style)
40 }
41
42 pub fn build_map(self, record: &Record, opts: TableOpts<'_>) -> StringResult {
43 let cfg = Cfg { opts, format: self };
44 expanded_table_kv(record, cfg).map(|cell| cell.map(|cell| cell.text))
45 }
46
47 pub fn build_list(self, vals: &[Value], opts: TableOpts<'_>) -> StringResult {
48 let cfg = Cfg { opts, format: self };
49 let output = expand_list(vals, cfg.clone())?;
50 let mut output = match output {
51 Some(out) => out,
52 None => return Ok(None),
53 };
54
55 configure_table(
56 &mut output,
57 cfg.opts.config,
58 &cfg.opts.style_computer,
59 cfg.opts.mode,
60 );
61
62 maybe_expand_table(output, cfg.opts.width)
63 }
64}
65
66#[derive(Debug, Clone)]
67struct Cfg<'a> {
68 opts: TableOpts<'a>,
69 format: ExpandedTable,
70}
71
72#[derive(Debug, Clone)]
73struct CellOutput {
74 text: String,
75 style: TextStyle,
76 size: usize,
77 is_expanded: bool,
78}
79
80impl CellOutput {
81 fn new(text: String, style: TextStyle, size: usize, is_expanded: bool) -> Self {
82 Self {
83 text,
84 style,
85 size,
86 is_expanded,
87 }
88 }
89
90 fn clean(text: String, size: usize, is_expanded: bool) -> Self {
91 Self::new(text, Default::default(), size, is_expanded)
92 }
93
94 fn text(text: String) -> Self {
95 Self::styled((text, Default::default()))
96 }
97
98 fn styled(text: NuText) -> Self {
99 Self::new(text.0, text.1, 1, false)
100 }
101}
102
103type CellResult = Result<Option<CellOutput>, ShellError>;
104
105fn expand_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
106 const PADDING_SPACE: usize = 2;
107 const SPLIT_LINE_SPACE: usize = 1;
108 const ADDITIONAL_CELL_SPACE: usize = PADDING_SPACE + SPLIT_LINE_SPACE;
109 const MIN_CELL_CONTENT_WIDTH: usize = 1;
110 const TRUNCATE_CONTENT_WIDTH: usize = 3;
111 const TRUNCATE_CELL_WIDTH: usize = TRUNCATE_CONTENT_WIDTH + PADDING_SPACE;
112
113 if input.is_empty() {
114 return Ok(None);
115 }
116
117 let mut available_width = cfg
119 .opts
120 .width
121 .saturating_sub(SPLIT_LINE_SPACE + SPLIT_LINE_SPACE);
122 if available_width < MIN_CELL_CONTENT_WIDTH {
123 return Ok(None);
124 }
125
126 let headers = get_columns(input);
127
128 let with_index = has_index(&cfg.opts, &headers);
129 let row_offset = cfg.opts.index_offset;
130 let mut rows_count = 0usize;
131
132 let headers: Vec<_> = headers
135 .into_iter()
136 .filter(|header| header != INDEX_COLUMN_NAME)
137 .collect();
138
139 let with_header = !headers.is_empty();
140
141 let mut data = vec![vec![]; input.len() + with_header as usize];
142 let mut data_styles = HashMap::new();
143
144 if with_index {
145 if with_header {
146 data[0].push(NuRecordsValue::exact(String::from("#"), 1, vec![]));
147 }
148
149 for (row, item) in input.iter().enumerate() {
150 cfg.opts.signals.check(cfg.opts.span)?;
151 check_value(item)?;
152
153 let index = row + row_offset;
154 let text = item
155 .as_record()
156 .ok()
157 .and_then(|val| val.get(INDEX_COLUMN_NAME))
158 .map(|value| value.to_expanded_string("", cfg.opts.config))
159 .unwrap_or_else(|| index.to_string());
160
161 let row = row + with_header as usize;
162 let value = NuRecordsValue::new(text);
163 data[row].push(value);
164 }
165
166 let column_width = string_width(data[data.len() - 1][0].as_ref());
167
168 if column_width + ADDITIONAL_CELL_SPACE > available_width {
169 available_width = 0;
170 } else {
171 available_width -= column_width + ADDITIONAL_CELL_SPACE;
172 }
173 }
174
175 if !with_header {
176 if available_width > ADDITIONAL_CELL_SPACE {
177 available_width -= PADDING_SPACE;
178 } else {
179 return Ok(None);
183 }
184
185 for (row, item) in input.iter().enumerate() {
186 cfg.opts.signals.check(cfg.opts.span)?;
187 check_value(item)?;
188
189 let inner_cfg = cfg_expand_reset_table(cfg.clone(), available_width);
190 let mut cell = expand_entry(item, inner_cfg);
191
192 let value_width = string_width(&cell.text);
193 if value_width > available_width {
194 cell.text = wrap_text(&cell.text, available_width, cfg.opts.config);
200 }
201
202 let value = NuRecordsValue::new(cell.text);
203 data[row].push(value);
204 data_styles.insert((row, with_index as usize), cell.style);
205
206 rows_count = rows_count.saturating_add(cell.size);
207 }
208
209 let mut table = NuTable::from(data);
210 table.set_indent(cfg.opts.config.table.padding);
211 table.set_index_style(get_index_style(&cfg.opts.style_computer));
212 set_data_styles(&mut table, data_styles);
213
214 return Ok(Some(TableOutput::new(table, false, with_index, rows_count)));
215 }
216
217 if !headers.is_empty() {
218 let mut pad_space = PADDING_SPACE;
219 if headers.len() > 1 {
220 pad_space += SPLIT_LINE_SPACE;
221 }
222
223 if available_width < pad_space {
224 return Ok(None);
228 }
229 }
230
231 let count_columns = headers.len();
232 let mut widths = Vec::new();
233 let mut truncate = false;
234 let mut rendered_column = 0;
235 for (col, header) in headers.into_iter().enumerate() {
236 let is_last_column = col + 1 == count_columns;
237 let mut pad_space = PADDING_SPACE;
238 if !is_last_column {
239 pad_space += SPLIT_LINE_SPACE;
240 }
241
242 let mut available = available_width - pad_space;
243 let mut column_width = 0;
244
245 if !is_last_column {
246 let pad_space = PADDING_SPACE + TRUNCATE_CONTENT_WIDTH;
251
252 if available > pad_space {
253 available -= pad_space;
258 } else {
259 truncate = true;
260 break;
261 }
262
263 if available < column_width {
264 truncate = true;
265 break;
266 }
267 }
268
269 let mut column_rows = 0usize;
270
271 for (row, item) in input.iter().enumerate() {
272 cfg.opts.signals.check(cfg.opts.span)?;
273 check_value(item)?;
274
275 let inner_cfg = cfg_expand_reset_table(cfg.clone(), available);
276 let mut cell = expand_entry_with_header(item, &header, inner_cfg);
277
278 let mut value_width = string_width(&cell.text);
279 if value_width > available {
280 cell.text = wrap_text(&cell.text, available, cfg.opts.config);
284 value_width = available;
285 }
286
287 column_width = max(column_width, value_width);
288
289 let value = NuRecordsValue::new(cell.text);
290 data[row + 1].push(value);
291 data_styles.insert((row + 1, col + with_index as usize), cell.style);
292
293 column_rows = column_rows.saturating_add(cell.size);
294 }
295
296 let mut head_width = string_width(&header);
297 let mut header = header;
298 if head_width > available {
299 header = wrap_text(&header, available, cfg.opts.config);
300 head_width = available;
301 }
302
303 let head_cell = NuRecordsValue::new(header);
304 data[0].push(head_cell);
305
306 column_width = max(column_width, head_width);
307
308 if column_width > available {
309 for row in &mut data {
311 row.pop();
312 }
313
314 truncate = true;
315 break;
316 }
317
318 widths.push(column_width);
319
320 available_width -= pad_space + column_width;
321 rendered_column += 1;
322
323 rows_count = std::cmp::max(rows_count, column_rows);
324 }
325
326 if truncate && rendered_column == 0 {
327 return Ok(None);
336 }
337
338 if truncate {
339 if available_width < TRUNCATE_CELL_WIDTH {
340 while let Some(width) = widths.pop() {
344 for row in &mut data {
345 row.pop();
346 }
347
348 available_width += width + PADDING_SPACE;
349 if !widths.is_empty() {
350 available_width += SPLIT_LINE_SPACE;
351 }
352
353 if available_width > TRUNCATE_CELL_WIDTH {
354 break;
355 }
356 }
357 }
358
359 if available_width < TRUNCATE_CELL_WIDTH {
362 return Ok(None);
363 }
364
365 let is_last_column = widths.len() == count_columns;
366 if !is_last_column {
367 let shift = NuRecordsValue::exact(String::from("..."), 3, vec![]);
368 for row in &mut data {
369 row.push(shift.clone());
370 }
371
372 widths.push(3);
373 }
374 }
375
376 let mut table = NuTable::from(data);
377 table.set_index_style(get_index_style(&cfg.opts.style_computer));
378 table.set_header_style(get_header_style(&cfg.opts.style_computer));
379 table.set_indent(cfg.opts.config.table.padding);
380 set_data_styles(&mut table, data_styles);
381
382 Ok(Some(TableOutput::new(table, true, with_index, rows_count)))
383}
384
385fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
386 let theme = load_theme(cfg.opts.mode);
387 let theme = theme.as_base();
388 let key_width = record
389 .columns()
390 .map(|col| string_width(col))
391 .max()
392 .unwrap_or(0);
393 let count_borders = theme.borders_has_vertical() as usize
394 + theme.borders_has_right() as usize
395 + theme.borders_has_left() as usize;
396 let padding = 2;
397 if key_width + count_borders + padding + padding > cfg.opts.width {
398 return Ok(None);
399 }
400
401 let value_width = cfg.opts.width - key_width - count_borders - padding - padding;
402
403 let mut count_rows = 0usize;
404
405 let mut data = Vec::with_capacity(record.len());
406 for (key, value) in record {
407 cfg.opts.signals.check(cfg.opts.span)?;
408
409 let cell = match expand_value(value, value_width, &cfg)? {
410 Some(val) => val,
411 None => return Ok(None),
412 };
413
414 let mut key = key.to_owned();
418 let is_key_on_next_line = !key.is_empty() && cell.is_expanded && theme.borders_has_top();
419 if is_key_on_next_line {
420 key.insert(0, '\n');
421 }
422
423 let key = NuRecordsValue::new(key);
424 let val = NuRecordsValue::new(cell.text);
425 let row = vec![key, val];
426
427 data.push(row);
428
429 count_rows = count_rows.saturating_add(cell.size);
430 }
431
432 let mut table = NuTable::from(data);
433 table.set_index_style(get_key_style(&cfg));
434 table.set_indent(cfg.opts.config.table.padding);
435
436 let mut out = TableOutput::new(table, false, true, count_rows);
437
438 configure_table(
439 &mut out,
440 cfg.opts.config,
441 &cfg.opts.style_computer,
442 cfg.opts.mode,
443 );
444
445 maybe_expand_table(out, cfg.opts.width)
446 .map(|value| value.map(|value| CellOutput::clean(value, count_rows, false)))
447}
448
449fn expand_value(value: &Value, width: usize, cfg: &Cfg<'_>) -> CellResult {
451 if is_limit_reached(cfg) {
452 let value = value_to_string_clean(value, cfg);
453 return Ok(Some(CellOutput::clean(value, 1, false)));
454 }
455
456 let span = value.span();
457 match value {
458 Value::List { vals, .. } => {
459 let inner_cfg = cfg_expand_reset_table(cfg_expand_next_level(cfg.clone(), span), width);
460 let table = expand_list(vals, inner_cfg)?;
461
462 match table {
463 Some(mut out) => {
464 table_apply_config(&mut out, cfg);
465 let value = out.table.draw(width);
466 match value {
467 Some(value) => Ok(Some(CellOutput::clean(value, out.count_rows, true))),
468 None => Ok(None),
469 }
470 }
471 None => {
472 let value = value_to_wrapped_string(value, cfg, width);
474 Ok(Some(CellOutput::text(value)))
475 }
476 }
477 }
478 Value::Record { val: record, .. } => {
479 if record.is_empty() {
480 let value = value_to_wrapped_string(value, cfg, width);
482 return Ok(Some(CellOutput::text(value)));
483 }
484
485 let inner_cfg = cfg_expand_reset_table(cfg_expand_next_level(cfg.clone(), span), width);
486 let result = expanded_table_kv(record, inner_cfg)?;
487 match result {
488 Some(result) => Ok(Some(CellOutput::clean(result.text, result.size, true))),
489 None => {
490 let value = value_to_wrapped_string(value, cfg, width);
491 Ok(Some(CellOutput::text(value)))
492 }
493 }
494 }
495 _ => {
496 let value = value_to_wrapped_string_clean(value, cfg, width);
497 Ok(Some(CellOutput::text(value)))
498 }
499 }
500}
501
502fn get_key_style(cfg: &Cfg<'_>) -> TextStyle {
503 get_header_style(&cfg.opts.style_computer).alignment(Alignment::Left)
504}
505
506fn expand_entry_with_header(item: &Value, header: &str, cfg: Cfg<'_>) -> CellOutput {
507 match item {
508 Value::Record { val, .. } => match val.get(header) {
509 Some(val) => expand_entry(val, cfg),
510 None => CellOutput::styled(error_sign(
511 cfg.opts.config.table.missing_value_symbol.clone(),
512 &cfg.opts.style_computer,
513 )),
514 },
515 _ => expand_entry(item, cfg),
516 }
517}
518
519fn expand_entry(item: &Value, cfg: Cfg<'_>) -> CellOutput {
520 if is_limit_reached(&cfg) {
521 let value = nu_value_to_string_clean(item, cfg.opts.config, &cfg.opts.style_computer);
522 return CellOutput::styled(value);
523 }
524
525 let span = item.span();
526 match &item {
527 Value::Record { val: record, .. } => {
528 if record.is_empty() {
529 let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
530 return CellOutput::styled(value);
531 }
532
533 let inner_cfg = cfg_expand_next_level(cfg.clone(), span);
535 let table = expanded_table_kv(record, inner_cfg);
536
537 match table {
538 Ok(Some(table)) => table,
539 _ => {
540 let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
541 CellOutput::styled(value)
542 }
543 }
544 }
545 Value::List { vals, .. } => {
546 if cfg.format.flatten && is_simple_list(vals) {
547 let value = list_to_string(
548 vals,
549 cfg.opts.config,
550 &cfg.opts.style_computer,
551 &cfg.format.flatten_sep,
552 );
553 return CellOutput::text(value);
554 }
555
556 let inner_cfg = cfg_expand_next_level(cfg.clone(), span);
557 let table = expand_list(vals, inner_cfg);
558
559 let mut out = match table {
560 Ok(Some(out)) => out,
561 _ => {
562 let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
563 return CellOutput::styled(value);
564 }
565 };
566
567 table_apply_config(&mut out, &cfg);
568
569 let table = out.table.draw(usize::MAX);
570 match table {
571 Some(table) => CellOutput::clean(table, out.count_rows, false),
572 None => {
573 let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
574 CellOutput::styled(value)
575 }
576 }
577 }
578 _ => {
579 let value = nu_value_to_string_clean(item, cfg.opts.config, &cfg.opts.style_computer);
580 CellOutput::styled(value)
581 }
582 }
583}
584
585fn is_limit_reached(cfg: &Cfg<'_>) -> bool {
586 matches!(cfg.format.expand_limit, Some(0))
587}
588
589fn is_simple_list(vals: &[Value]) -> bool {
590 vals.iter()
591 .all(|v| !matches!(v, Value::Record { .. } | Value::List { .. }))
592}
593
594fn list_to_string(
595 vals: &[Value],
596 config: &Config,
597 style_computer: &StyleComputer,
598 sep: &str,
599) -> String {
600 let mut buf = String::new();
601 for (i, value) in vals.iter().enumerate() {
602 if i > 0 {
603 buf.push_str(sep);
604 }
605
606 let (text, _) = nu_value_to_string_clean(value, config, style_computer);
607 buf.push_str(&text);
608 }
609
610 buf
611}
612
613fn maybe_expand_table(mut out: TableOutput, term_width: usize) -> StringResult {
614 let total_width = out.table.total_width();
615 if total_width < term_width {
616 const EXPAND_THRESHOLD: f32 = 0.80;
617 let used_percent = total_width as f32 / term_width as f32;
618 let need_expansion = total_width < term_width && used_percent > EXPAND_THRESHOLD;
619 if need_expansion {
620 out.table.set_strategy(true);
621 }
622 }
623
624 let table = out.table.draw(term_width);
625
626 Ok(table)
627}
628
629fn set_data_styles(table: &mut NuTable, styles: HashMap<Position, TextStyle>) {
630 for (pos, style) in styles {
631 table.insert_style(pos, style);
632 }
633}
634
635fn table_apply_config(out: &mut TableOutput, cfg: &Cfg<'_>) {
636 configure_table(
637 out,
638 cfg.opts.config,
639 &cfg.opts.style_computer,
640 cfg.opts.mode,
641 )
642}
643
644fn value_to_string(value: &Value, cfg: &Cfg<'_>) -> String {
645 nu_value_to_string(value, cfg.opts.config, &cfg.opts.style_computer).0
646}
647
648fn value_to_string_clean(value: &Value, cfg: &Cfg<'_>) -> String {
649 nu_value_to_string_clean(value, cfg.opts.config, &cfg.opts.style_computer).0
650}
651
652fn value_to_wrapped_string(value: &Value, cfg: &Cfg<'_>, value_width: usize) -> String {
653 wrap_text(&value_to_string(value, cfg), value_width, cfg.opts.config)
654}
655
656fn value_to_wrapped_string_clean(value: &Value, cfg: &Cfg<'_>, value_width: usize) -> String {
657 let text = nu_value_to_string_colored(value, cfg.opts.config, &cfg.opts.style_computer);
658 wrap_text(&text, value_width, cfg.opts.config)
659}
660
661fn cfg_expand_next_level(mut cfg: Cfg<'_>, span: Span) -> Cfg<'_> {
662 cfg.opts.span = span;
663 if let Some(deep) = cfg.format.expand_limit.as_mut() {
664 *deep -= 1
665 }
666
667 cfg
668}
669
670fn cfg_expand_reset_table(mut cfg: Cfg<'_>, width: usize) -> Cfg<'_> {
671 cfg.opts.width = width;
672 cfg.opts.index_offset = 0;
673 cfg
674}