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