nu_table/types/
expanded.rs

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    // 2 - split lines
118    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    // The header with the INDEX is removed from the table headers since
130    // it is added to the natural table index
131    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            // it means we have no space left for actual content;
143            // which means there's no point in index itself if it was even used.
144            // so we do not print it.
145            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                // NOTE: we don't wanna wrap index; so we return
194                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    // NOTE: redefine to not break above logic (fixme)
221    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            // NOTE: we don't wanna wrap index; so we return
255            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        // We want to reserver some space for next column
276        // If we can't fit it in it will be popped anyhow.
277        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            // TODO: optimize cause when we expand we alrready know the width (most of the time or all)
301            let mut value = NuTable::create(cell.text);
302            let mut value_width = value.width();
303            if value_width > available {
304                // NOTE:
305                // most likely it was emojie which we are not sure about what to do
306                // so we truncate it just in case
307                //
308                // most likely width is 1
309
310                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            // it means that no actual data was rendered, there might be only index present,
345            // so there's no point in rendering the table.
346            //
347            // It's actually quite important in case it's called recursively,
348            // cause we will back up to the basic table view as a string e.g. '[table 123 columns]'.
349            //
350            // But potentially if its reached as a 1st called function we might would love to see the index.
351
352            return Ok(None);
353        }
354
355        if available_width < truncate_column_width {
356            // back up by removing last column.
357            // it's LIKELY that removing only 1 column will leave us enough space for a shift column.
358            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        // this must be a RARE case or even NEVER happen,
373        // but we do check it just in case.
374        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        // we want to have a key being aligned to 2nd line,
424        // we could use Padding for it but,
425        // the easiest way to do so is just push a new_line char before
426        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
450// the flag is used as an optimization to not do `value.lines().count()` search.
451fn 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                    // it means that the list is empty
474                    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                // Like list case return styled string instead of empty value
482                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            // we verify what is the structure of a Record cause it might represent
537            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}