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