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