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 = string_width(&header);
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 head_cell = NuRecordsValue::new(header);
297 data[0].push(head_cell);
298
299 if column_width > available {
300 for row in &mut data {
302 row.pop();
303 }
304
305 truncate = true;
306 break;
307 }
308
309 widths.push(column_width);
310
311 available_width -= pad_space + column_width;
312 rendered_column += 1;
313
314 rows_count = std::cmp::max(rows_count, column_rows);
315 }
316
317 if truncate && rendered_column == 0 {
318 return Ok(None);
327 }
328
329 if truncate {
330 if available_width < TRUNCATE_CELL_WIDTH {
331 while let Some(width) = widths.pop() {
335 for row in &mut data {
336 row.pop();
337 }
338
339 available_width += width + PADDING_SPACE;
340 if !widths.is_empty() {
341 available_width += SPLIT_LINE_SPACE;
342 }
343
344 if available_width > TRUNCATE_CELL_WIDTH {
345 break;
346 }
347 }
348 }
349
350 if available_width < TRUNCATE_CELL_WIDTH {
353 return Ok(None);
354 }
355
356 let is_last_column = widths.len() == count_columns;
357 if !is_last_column {
358 let shift = NuRecordsValue::exact(String::from("..."), 3, vec![]);
359 for row in &mut data {
360 row.push(shift.clone());
361 }
362
363 widths.push(3);
364 }
365 }
366
367 let mut table = NuTable::from(data);
368 table.set_index_style(get_index_style(&cfg.opts.style_computer));
369 table.set_header_style(get_header_style(&cfg.opts.style_computer));
370 table.set_indent(cfg.opts.config.table.padding);
371 set_data_styles(&mut table, data_styles);
372
373 Ok(Some(TableOutput::new(table, true, with_index, rows_count)))
374}
375
376fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
377 let theme = load_theme(cfg.opts.mode);
378 let theme = theme.as_base();
379 let key_width = record
380 .columns()
381 .map(|col| string_width(col))
382 .max()
383 .unwrap_or(0);
384 let count_borders = theme.borders_has_vertical() as usize
385 + theme.borders_has_right() as usize
386 + theme.borders_has_left() as usize;
387 let padding = 2;
388 if key_width + count_borders + padding + padding > cfg.opts.width {
389 return Ok(None);
390 }
391
392 let value_width = cfg.opts.width - key_width - count_borders - padding - padding;
393
394 let mut count_rows = 0usize;
395
396 let mut data = Vec::with_capacity(record.len());
397 for (key, value) in record {
398 cfg.opts.signals.check(cfg.opts.span)?;
399
400 let cell = match expand_value(value, value_width, &cfg)? {
401 Some(val) => val,
402 None => return Ok(None),
403 };
404
405 let mut key = key.to_owned();
409 let is_key_on_next_line = !key.is_empty() && cell.is_expanded && theme.borders_has_top();
410 if is_key_on_next_line {
411 key.insert(0, '\n');
412 }
413
414 let key = NuRecordsValue::new(key);
415 let val = NuRecordsValue::new(cell.text);
416 let row = vec![key, val];
417
418 data.push(row);
419
420 count_rows = count_rows.saturating_add(cell.size);
421 }
422
423 let mut table = NuTable::from(data);
424 table.set_index_style(get_key_style(&cfg));
425 table.set_indent(cfg.opts.config.table.padding);
426
427 let mut out = TableOutput::new(table, false, true, count_rows);
428
429 configure_table(
430 &mut out,
431 cfg.opts.config,
432 &cfg.opts.style_computer,
433 cfg.opts.mode,
434 );
435
436 maybe_expand_table(out, cfg.opts.width)
437 .map(|value| value.map(|value| CellOutput::clean(value, count_rows, false)))
438}
439
440fn expand_value(value: &Value, width: usize, cfg: &Cfg<'_>) -> CellResult {
442 if is_limit_reached(cfg) {
443 let value = value_to_string_clean(value, cfg);
444 return Ok(Some(CellOutput::clean(value, 1, false)));
445 }
446
447 let span = value.span();
448 match value {
449 Value::List { vals, .. } => {
450 let inner_cfg = cfg_expand_reset_table(cfg_expand_next_level(cfg.clone(), span), width);
451 let table = expand_list(vals, inner_cfg)?;
452
453 match table {
454 Some(mut out) => {
455 table_apply_config(&mut out, cfg);
456 let value = out.table.draw(width);
457 match value {
458 Some(value) => Ok(Some(CellOutput::clean(value, out.count_rows, true))),
459 None => Ok(None),
460 }
461 }
462 None => {
463 let value = value_to_wrapped_string(value, cfg, width);
465 Ok(Some(CellOutput::text(value)))
466 }
467 }
468 }
469 Value::Record { val: record, .. } => {
470 if record.is_empty() {
471 let value = value_to_wrapped_string(value, cfg, width);
473 return Ok(Some(CellOutput::text(value)));
474 }
475
476 let inner_cfg = cfg_expand_reset_table(cfg_expand_next_level(cfg.clone(), span), width);
477 let result = expanded_table_kv(record, inner_cfg)?;
478 match result {
479 Some(result) => Ok(Some(CellOutput::clean(result.text, result.size, true))),
480 None => {
481 let value = value_to_wrapped_string(value, cfg, width);
482 Ok(Some(CellOutput::text(value)))
483 }
484 }
485 }
486 _ => {
487 let value = value_to_wrapped_string_clean(value, cfg, width);
488 Ok(Some(CellOutput::text(value)))
489 }
490 }
491}
492
493fn get_key_style(cfg: &Cfg<'_>) -> TextStyle {
494 get_header_style(&cfg.opts.style_computer).alignment(Alignment::Left)
495}
496
497fn expand_entry_with_header(item: &Value, header: &str, cfg: Cfg<'_>) -> CellOutput {
498 match item {
499 Value::Record { val, .. } => match val.get(header) {
500 Some(val) => expand_entry(val, cfg),
501 None => CellOutput::styled(error_sign(&cfg.opts.style_computer)),
502 },
503 _ => expand_entry(item, cfg),
504 }
505}
506
507fn expand_entry(item: &Value, cfg: Cfg<'_>) -> CellOutput {
508 if is_limit_reached(&cfg) {
509 let value = nu_value_to_string_clean(item, cfg.opts.config, &cfg.opts.style_computer);
510 return CellOutput::styled(value);
511 }
512
513 let span = item.span();
514 match &item {
515 Value::Record { val: record, .. } => {
516 if record.is_empty() {
517 let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
518 return CellOutput::styled(value);
519 }
520
521 let inner_cfg = cfg_expand_next_level(cfg.clone(), span);
523 let table = expanded_table_kv(record, inner_cfg);
524
525 match table {
526 Ok(Some(table)) => table,
527 _ => {
528 let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
529 CellOutput::styled(value)
530 }
531 }
532 }
533 Value::List { vals, .. } => {
534 if cfg.format.flatten && is_simple_list(vals) {
535 let value = list_to_string(
536 vals,
537 cfg.opts.config,
538 &cfg.opts.style_computer,
539 &cfg.format.flatten_sep,
540 );
541 return CellOutput::text(value);
542 }
543
544 let inner_cfg = cfg_expand_next_level(cfg.clone(), span);
545 let table = expand_list(vals, inner_cfg);
546
547 let mut out = match table {
548 Ok(Some(out)) => out,
549 _ => {
550 let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
551 return CellOutput::styled(value);
552 }
553 };
554
555 table_apply_config(&mut out, &cfg);
556
557 let table = out.table.draw(usize::MAX);
558 match table {
559 Some(table) => CellOutput::clean(table, out.count_rows, false),
560 None => {
561 let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
562 CellOutput::styled(value)
563 }
564 }
565 }
566 _ => {
567 let value = nu_value_to_string_clean(item, cfg.opts.config, &cfg.opts.style_computer);
568 CellOutput::styled(value)
569 }
570 }
571}
572
573fn is_limit_reached(cfg: &Cfg<'_>) -> bool {
574 matches!(cfg.format.expand_limit, Some(0))
575}
576
577fn is_simple_list(vals: &[Value]) -> bool {
578 vals.iter()
579 .all(|v| !matches!(v, Value::Record { .. } | Value::List { .. }))
580}
581
582fn list_to_string(
583 vals: &[Value],
584 config: &Config,
585 style_computer: &StyleComputer,
586 sep: &str,
587) -> String {
588 let mut buf = String::new();
589 for (i, value) in vals.iter().enumerate() {
590 if i > 0 {
591 buf.push_str(sep);
592 }
593
594 let (text, _) = nu_value_to_string_clean(value, config, style_computer);
595 buf.push_str(&text);
596 }
597
598 buf
599}
600
601fn maybe_expand_table(mut out: TableOutput, term_width: usize) -> StringResult {
602 let total_width = out.table.total_width();
603 if total_width < term_width {
604 const EXPAND_THRESHOLD: f32 = 0.80;
605 let used_percent = total_width as f32 / term_width as f32;
606 let need_expansion = total_width < term_width && used_percent > EXPAND_THRESHOLD;
607 if need_expansion {
608 out.table.set_strategy(true);
609 }
610 }
611
612 let table = out.table.draw(term_width);
613
614 Ok(table)
615}
616
617fn set_data_styles(table: &mut NuTable, styles: HashMap<Position, TextStyle>) {
618 for (pos, style) in styles {
619 table.insert_style(pos, style);
620 }
621}
622
623fn table_apply_config(out: &mut TableOutput, cfg: &Cfg<'_>) {
624 configure_table(
625 out,
626 cfg.opts.config,
627 &cfg.opts.style_computer,
628 cfg.opts.mode,
629 )
630}
631
632fn value_to_string(value: &Value, cfg: &Cfg<'_>) -> String {
633 nu_value_to_string(value, cfg.opts.config, &cfg.opts.style_computer).0
634}
635
636fn value_to_string_clean(value: &Value, cfg: &Cfg<'_>) -> String {
637 nu_value_to_string_clean(value, cfg.opts.config, &cfg.opts.style_computer).0
638}
639
640fn value_to_wrapped_string(value: &Value, cfg: &Cfg<'_>, value_width: usize) -> String {
641 wrap_text(&value_to_string(value, cfg), value_width, cfg.opts.config)
642}
643
644fn value_to_wrapped_string_clean(value: &Value, cfg: &Cfg<'_>, value_width: usize) -> String {
645 let text = nu_value_to_string_colored(value, cfg.opts.config, &cfg.opts.style_computer);
646 wrap_text(&text, value_width, cfg.opts.config)
647}
648
649fn cfg_expand_next_level(mut cfg: Cfg<'_>, span: Span) -> Cfg<'_> {
650 cfg.opts.span = span;
651 if let Some(deep) = cfg.format.expand_limit.as_mut() {
652 *deep -= 1
653 }
654
655 cfg
656}
657
658fn cfg_expand_reset_table(mut cfg: Cfg<'_>, width: usize) -> Cfg<'_> {
659 cfg.opts.width = width;
660 cfg.opts.index_offset = 0;
661 cfg
662}