nu_table/types/
expanded.rs

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    // 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_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    // The header with the INDEX is removed from the table headers since
133    // it is added to the natural table index
134    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            // it means we have no space left for actual content;
180            // which means there's no point in index itself if it was even used.
181            // so we do not print it.
182            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                // it must only happen when a string is produced, so we can safely wrap it.
195                // (it might be string table representation as well) (I guess I mean default { table ...} { list ...})
196                //
197                // todo: Maybe convert_to_table2_entry could do for strings to not mess caller code?
198
199                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            // there's no space for actual data so we don't return index if it's present.
225            // (also see the comment after the loop)
226
227            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 = 0;
244
245        if !is_last_column {
246            // we need to make sure that we have a space for a next column if we use available width
247            // so we might need to decrease a bit it.
248
249            // we consider a header width be a minimum width
250            let pad_space = PADDING_SPACE + TRUNCATE_CONTENT_WIDTH;
251
252            if available > pad_space {
253                // In we have no space for a next column,
254                // We consider showing something better then nothing,
255                // So we try to decrease the width to show at least a truncution column
256
257                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                // it must only happen when a string is produced, so we can safely wrap it.
281                // (it might be string table representation as well)
282
283                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 mut head_width = string_width(&header);
297        let mut header = header;
298        if head_width > available {
299            header = wrap_text(&header, available, cfg.opts.config);
300            head_width = available;
301        }
302
303        let head_cell = NuRecordsValue::new(header);
304        data[0].push(head_cell);
305
306        column_width = max(column_width, head_width);
307
308        if column_width > available {
309            // remove the column we just inserted
310            for row in &mut data {
311                row.pop();
312            }
313
314            truncate = true;
315            break;
316        }
317
318        widths.push(column_width);
319
320        available_width -= pad_space + column_width;
321        rendered_column += 1;
322
323        rows_count = std::cmp::max(rows_count, column_rows);
324    }
325
326    if truncate && rendered_column == 0 {
327        // it means that no actual data was rendered, there might be only index present,
328        // so there's no point in rendering the table.
329        //
330        // It's actually quite important in case it's called recursively,
331        // cause we will back up to the basic table view as a string e.g. '[table 123 columns]'.
332        //
333        // But potentially if its reached as a 1st called function we might would love to see the index.
334
335        return Ok(None);
336    }
337
338    if truncate {
339        if available_width < TRUNCATE_CELL_WIDTH {
340            // back up by removing last column.
341            // it's LIKELY that removing only 1 column will leave us enough space for a shift column.
342
343            while let Some(width) = widths.pop() {
344                for row in &mut data {
345                    row.pop();
346                }
347
348                available_width += width + PADDING_SPACE;
349                if !widths.is_empty() {
350                    available_width += SPLIT_LINE_SPACE;
351                }
352
353                if available_width > TRUNCATE_CELL_WIDTH {
354                    break;
355                }
356            }
357        }
358
359        // this must be a RARE case or even NEVER happen,
360        // but we do check it just in case.
361        if available_width < TRUNCATE_CELL_WIDTH {
362            return Ok(None);
363        }
364
365        let is_last_column = widths.len() == count_columns;
366        if !is_last_column {
367            let shift = NuRecordsValue::exact(String::from("..."), 3, vec![]);
368            for row in &mut data {
369                row.push(shift.clone());
370            }
371
372            widths.push(3);
373        }
374    }
375
376    let mut table = NuTable::from(data);
377    table.set_index_style(get_index_style(&cfg.opts.style_computer));
378    table.set_header_style(get_header_style(&cfg.opts.style_computer));
379    table.set_indent(cfg.opts.config.table.padding);
380    set_data_styles(&mut table, data_styles);
381
382    Ok(Some(TableOutput::new(table, true, with_index, rows_count)))
383}
384
385fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
386    let theme = load_theme(cfg.opts.mode);
387    let theme = theme.as_base();
388    let key_width = record
389        .columns()
390        .map(|col| string_width(col))
391        .max()
392        .unwrap_or(0);
393    let count_borders = theme.borders_has_vertical() as usize
394        + theme.borders_has_right() as usize
395        + theme.borders_has_left() as usize;
396    let padding = 2;
397    if key_width + count_borders + padding + padding > cfg.opts.width {
398        return Ok(None);
399    }
400
401    let value_width = cfg.opts.width - key_width - count_borders - padding - padding;
402
403    let mut count_rows = 0usize;
404
405    let mut data = Vec::with_capacity(record.len());
406    for (key, value) in record {
407        cfg.opts.signals.check(cfg.opts.span)?;
408
409        let cell = match expand_value(value, value_width, &cfg)? {
410            Some(val) => val,
411            None => return Ok(None),
412        };
413
414        // we want to have a key being aligned to 2nd line,
415        // we could use Padding for it but,
416        // the easiest way to do so is just push a new_line char before
417        let mut key = key.to_owned();
418        let is_key_on_next_line = !key.is_empty() && cell.is_expanded && theme.borders_has_top();
419        if is_key_on_next_line {
420            key.insert(0, '\n');
421        }
422
423        let key = NuRecordsValue::new(key);
424        let val = NuRecordsValue::new(cell.text);
425        let row = vec![key, val];
426
427        data.push(row);
428
429        count_rows = count_rows.saturating_add(cell.size);
430    }
431
432    let mut table = NuTable::from(data);
433    table.set_index_style(get_key_style(&cfg));
434    table.set_indent(cfg.opts.config.table.padding);
435
436    let mut out = TableOutput::new(table, false, true, count_rows);
437
438    configure_table(
439        &mut out,
440        cfg.opts.config,
441        &cfg.opts.style_computer,
442        cfg.opts.mode,
443    );
444
445    maybe_expand_table(out, cfg.opts.width)
446        .map(|value| value.map(|value| CellOutput::clean(value, count_rows, false)))
447}
448
449// the flag is used as an optimization to not do `value.lines().count()` search.
450fn expand_value(value: &Value, width: usize, cfg: &Cfg<'_>) -> CellResult {
451    if is_limit_reached(cfg) {
452        let value = value_to_string_clean(value, cfg);
453        return Ok(Some(CellOutput::clean(value, 1, false)));
454    }
455
456    let span = value.span();
457    match value {
458        Value::List { vals, .. } => {
459            let inner_cfg = cfg_expand_reset_table(cfg_expand_next_level(cfg.clone(), span), width);
460            let table = expand_list(vals, inner_cfg)?;
461
462            match table {
463                Some(mut out) => {
464                    table_apply_config(&mut out, cfg);
465                    let value = out.table.draw(width);
466                    match value {
467                        Some(value) => Ok(Some(CellOutput::clean(value, out.count_rows, true))),
468                        None => Ok(None),
469                    }
470                }
471                None => {
472                    // it means that the list is empty
473                    let value = value_to_wrapped_string(value, cfg, width);
474                    Ok(Some(CellOutput::text(value)))
475                }
476            }
477        }
478        Value::Record { val: record, .. } => {
479            if record.is_empty() {
480                // Like list case return styled string instead of empty value
481                let value = value_to_wrapped_string(value, cfg, width);
482                return Ok(Some(CellOutput::text(value)));
483            }
484
485            let inner_cfg = cfg_expand_reset_table(cfg_expand_next_level(cfg.clone(), span), width);
486            let result = expanded_table_kv(record, inner_cfg)?;
487            match result {
488                Some(result) => Ok(Some(CellOutput::clean(result.text, result.size, true))),
489                None => {
490                    let value = value_to_wrapped_string(value, cfg, width);
491                    Ok(Some(CellOutput::text(value)))
492                }
493            }
494        }
495        _ => {
496            let value = value_to_wrapped_string_clean(value, cfg, width);
497            Ok(Some(CellOutput::text(value)))
498        }
499    }
500}
501
502fn get_key_style(cfg: &Cfg<'_>) -> TextStyle {
503    get_header_style(&cfg.opts.style_computer).alignment(Alignment::Left)
504}
505
506fn expand_entry_with_header(item: &Value, header: &str, cfg: Cfg<'_>) -> CellOutput {
507    match item {
508        Value::Record { val, .. } => match val.get(header) {
509            Some(val) => expand_entry(val, cfg),
510            None => CellOutput::styled(error_sign(
511                cfg.opts.config.table.missing_value_symbol.clone(),
512                &cfg.opts.style_computer,
513            )),
514        },
515        _ => expand_entry(item, cfg),
516    }
517}
518
519fn expand_entry(item: &Value, cfg: Cfg<'_>) -> CellOutput {
520    if is_limit_reached(&cfg) {
521        let value = nu_value_to_string_clean(item, cfg.opts.config, &cfg.opts.style_computer);
522        return CellOutput::styled(value);
523    }
524
525    let span = item.span();
526    match &item {
527        Value::Record { val: record, .. } => {
528            if record.is_empty() {
529                let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
530                return CellOutput::styled(value);
531            }
532
533            // we verify what is the structure of a Record cause it might represent
534            let inner_cfg = cfg_expand_next_level(cfg.clone(), span);
535            let table = expanded_table_kv(record, inner_cfg);
536
537            match table {
538                Ok(Some(table)) => table,
539                _ => {
540                    let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
541                    CellOutput::styled(value)
542                }
543            }
544        }
545        Value::List { vals, .. } => {
546            if cfg.format.flatten && is_simple_list(vals) {
547                let value = list_to_string(
548                    vals,
549                    cfg.opts.config,
550                    &cfg.opts.style_computer,
551                    &cfg.format.flatten_sep,
552                );
553                return CellOutput::text(value);
554            }
555
556            let inner_cfg = cfg_expand_next_level(cfg.clone(), span);
557            let table = expand_list(vals, inner_cfg);
558
559            let mut out = match table {
560                Ok(Some(out)) => out,
561                _ => {
562                    let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
563                    return CellOutput::styled(value);
564                }
565            };
566
567            table_apply_config(&mut out, &cfg);
568
569            let table = out.table.draw(usize::MAX);
570            match table {
571                Some(table) => CellOutput::clean(table, out.count_rows, false),
572                None => {
573                    let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
574                    CellOutput::styled(value)
575                }
576            }
577        }
578        _ => {
579            let value = nu_value_to_string_clean(item, cfg.opts.config, &cfg.opts.style_computer);
580            CellOutput::styled(value)
581        }
582    }
583}
584
585fn is_limit_reached(cfg: &Cfg<'_>) -> bool {
586    matches!(cfg.format.expand_limit, Some(0))
587}
588
589fn is_simple_list(vals: &[Value]) -> bool {
590    vals.iter()
591        .all(|v| !matches!(v, Value::Record { .. } | Value::List { .. }))
592}
593
594fn list_to_string(
595    vals: &[Value],
596    config: &Config,
597    style_computer: &StyleComputer,
598    sep: &str,
599) -> String {
600    let mut buf = String::new();
601    for (i, value) in vals.iter().enumerate() {
602        if i > 0 {
603            buf.push_str(sep);
604        }
605
606        let (text, _) = nu_value_to_string_clean(value, config, style_computer);
607        buf.push_str(&text);
608    }
609
610    buf
611}
612
613fn maybe_expand_table(mut out: TableOutput, term_width: usize) -> StringResult {
614    let total_width = out.table.total_width();
615    if total_width < term_width {
616        const EXPAND_THRESHOLD: f32 = 0.80;
617        let used_percent = total_width as f32 / term_width as f32;
618        let need_expansion = total_width < term_width && used_percent > EXPAND_THRESHOLD;
619        if need_expansion {
620            out.table.set_strategy(true);
621        }
622    }
623
624    let table = out.table.draw(term_width);
625
626    Ok(table)
627}
628
629fn set_data_styles(table: &mut NuTable, styles: HashMap<Position, TextStyle>) {
630    for (pos, style) in styles {
631        table.insert_style(pos, style);
632    }
633}
634
635fn table_apply_config(out: &mut TableOutput, cfg: &Cfg<'_>) {
636    configure_table(
637        out,
638        cfg.opts.config,
639        &cfg.opts.style_computer,
640        cfg.opts.mode,
641    )
642}
643
644fn value_to_string(value: &Value, cfg: &Cfg<'_>) -> String {
645    nu_value_to_string(value, cfg.opts.config, &cfg.opts.style_computer).0
646}
647
648fn value_to_string_clean(value: &Value, cfg: &Cfg<'_>) -> String {
649    nu_value_to_string_clean(value, cfg.opts.config, &cfg.opts.style_computer).0
650}
651
652fn value_to_wrapped_string(value: &Value, cfg: &Cfg<'_>, value_width: usize) -> String {
653    wrap_text(&value_to_string(value, cfg), value_width, cfg.opts.config)
654}
655
656fn value_to_wrapped_string_clean(value: &Value, cfg: &Cfg<'_>, value_width: usize) -> String {
657    let text = nu_value_to_string_colored(value, cfg.opts.config, &cfg.opts.style_computer);
658    wrap_text(&text, value_width, cfg.opts.config)
659}
660
661fn cfg_expand_next_level(mut cfg: Cfg<'_>, span: Span) -> Cfg<'_> {
662    cfg.opts.span = span;
663    if let Some(deep) = cfg.format.expand_limit.as_mut() {
664        *deep -= 1
665    }
666
667    cfg
668}
669
670fn cfg_expand_reset_table(mut cfg: Cfg<'_>, width: usize) -> Cfg<'_> {
671    cfg.opts.width = width;
672    cfg.opts.index_offset = 0;
673    cfg
674}