nu_table/
unstructured_table.rs

1use nu_color_config::StyleComputer;
2use nu_protocol::{Config, Record, Span, TableIndent, Value};
3
4use tabled::{
5    grid::{
6        ansi::ANSIStr,
7        config::{Borders, CompactMultilineConfig},
8        dimension::{DimensionPriority, PoolTableDimension},
9    },
10    settings::{Alignment, Color, Padding, TableOption},
11    tables::{PoolTable, TableValue},
12};
13
14use crate::{TableTheme, is_color_empty, string_width, string_wrap};
15
16/// UnstructuredTable has a recursive table representation of nu_protocol::Value.
17///
18/// It doesn't support alignment and a proper width control (although it's possible to achieve).
19pub struct UnstructuredTable {
20    value: TableValue,
21}
22
23impl UnstructuredTable {
24    pub fn new(value: Value, config: &Config) -> Self {
25        let value = convert_nu_value_to_table_value(value, config);
26        Self { value }
27    }
28
29    pub fn truncate(&mut self, theme: &TableTheme, width: usize) -> bool {
30        let mut available = width;
31        let has_vertical = theme.as_full().borders_has_left();
32        if has_vertical {
33            available = available.saturating_sub(2);
34        }
35
36        truncate_table_value(&mut self.value, has_vertical, available).is_none()
37    }
38
39    pub fn draw(self, theme: &TableTheme, indent: TableIndent, style: &StyleComputer) -> String {
40        build_table(self.value, style, theme, indent)
41    }
42}
43
44fn build_table(
45    val: TableValue,
46    style: &StyleComputer,
47    theme: &TableTheme,
48    indent: TableIndent,
49) -> String {
50    let mut table = PoolTable::from(val);
51
52    let mut theme = theme.as_full().clone();
53    theme.set_horizontal_lines(Default::default());
54
55    table.with(Padding::new(indent.left, indent.right, 0, 0));
56    table.with(*theme.get_borders());
57    table.with(Alignment::left());
58    table.with(PoolTableDimension::new(
59        DimensionPriority::Last,
60        DimensionPriority::Last,
61    ));
62
63    if let Some(color) = get_border_color(style)
64        && !is_color_empty(&color)
65    {
66        return build_table_with_border_color(table, color);
67    }
68
69    table.to_string()
70}
71
72fn convert_nu_value_to_table_value(value: Value, config: &Config) -> TableValue {
73    match value {
74        Value::Record { val, .. } => build_vertical_map(val.into_owned(), config),
75        Value::List { vals, .. } => {
76            let rebuild_array_as_map = is_valid_record(&vals) && count_columns_in_record(&vals) > 0;
77            if rebuild_array_as_map {
78                build_map_from_record(vals, config)
79            } else {
80                build_vertical_array(vals, config)
81            }
82        }
83        value => build_string_value(value, config),
84    }
85}
86
87fn build_string_value(value: Value, config: &Config) -> TableValue {
88    const MAX_STRING_WIDTH: usize = 50;
89    const WRAP_STRING_WIDTH: usize = 30;
90
91    let mut text = value.to_abbreviated_string(config);
92    if string_width(&text) > MAX_STRING_WIDTH {
93        text = string_wrap(&text, WRAP_STRING_WIDTH, false);
94    }
95
96    TableValue::Cell(text)
97}
98
99fn build_vertical_map(record: Record, config: &Config) -> TableValue {
100    let max_key_width = record
101        .iter()
102        .map(|(k, _)| string_width(k))
103        .max()
104        .unwrap_or(0);
105
106    let mut rows = Vec::with_capacity(record.len());
107    for (mut key, value) in record {
108        string_append_to_width(&mut key, max_key_width);
109
110        let value = convert_nu_value_to_table_value(value, config);
111
112        let row = TableValue::Row(vec![TableValue::Cell(key), value]);
113        rows.push(row);
114    }
115
116    TableValue::Column(rows)
117}
118
119fn string_append_to_width(key: &mut String, max: usize) {
120    let width = string_width(key);
121    let rest = max - width;
122    key.extend(std::iter::repeat_n(' ', rest));
123}
124
125fn build_vertical_array(vals: Vec<Value>, config: &Config) -> TableValue {
126    let map = vals
127        .into_iter()
128        .map(|val| convert_nu_value_to_table_value(val, config))
129        .collect();
130
131    TableValue::Column(map)
132}
133
134fn is_valid_record(vals: &[Value]) -> bool {
135    if vals.is_empty() {
136        return true;
137    }
138
139    let first_value = match &vals[0] {
140        Value::Record { val, .. } => val,
141        _ => return false,
142    };
143
144    for val in &vals[1..] {
145        match val {
146            Value::Record { val, .. } => {
147                let equal = val.columns().eq(first_value.columns());
148                if !equal {
149                    return false;
150                }
151            }
152            _ => return false,
153        }
154    }
155
156    true
157}
158
159fn count_columns_in_record(vals: &[Value]) -> usize {
160    match vals.iter().next() {
161        Some(Value::Record { val, .. }) => val.len(),
162        _ => 0,
163    }
164}
165
166fn build_map_from_record(vals: Vec<Value>, config: &Config) -> TableValue {
167    // assumes that we have a valid record structure (checked by is_valid_record)
168
169    let head = get_columns_in_record(&vals);
170    let mut list = Vec::with_capacity(head.len());
171    for col in head {
172        list.push(TableValue::Column(vec![TableValue::Cell(col)]));
173    }
174
175    for val in vals {
176        let val = get_as_record(val);
177        for (i, (_, val)) in val.into_owned().into_iter().enumerate() {
178            let value = convert_nu_value_to_table_value(val, config);
179            let list = get_table_value_column_mut(&mut list[i]);
180
181            list.push(value);
182        }
183    }
184
185    TableValue::Row(list)
186}
187
188fn get_table_value_column_mut(val: &mut TableValue) -> &mut Vec<TableValue> {
189    match val {
190        TableValue::Column(row) => row,
191        _ => {
192            unreachable!();
193        }
194    }
195}
196
197fn get_as_record(val: Value) -> nu_utils::SharedCow<Record> {
198    match val {
199        Value::Record { val, .. } => val,
200        _ => unreachable!(),
201    }
202}
203
204fn get_columns_in_record(vals: &[Value]) -> Vec<String> {
205    match vals.iter().next() {
206        Some(Value::Record { val, .. }) => val.columns().cloned().collect(),
207        _ => vec![],
208    }
209}
210
211fn truncate_table_value(
212    value: &mut TableValue,
213    has_vertical: bool,
214    available: usize,
215) -> Option<usize> {
216    const MIN_CONTENT_WIDTH: usize = 10;
217    const TRUNCATE_CELL_WIDTH: usize = 3;
218    const PAD: usize = 2;
219
220    match value {
221        TableValue::Row(row) => {
222            if row.is_empty() {
223                return Some(PAD);
224            }
225
226            if row.len() == 1 {
227                return truncate_table_value(&mut row[0], has_vertical, available);
228            }
229
230            let count_cells = row.len();
231            let mut row_width = 0;
232            let mut i = 0;
233            let mut last_used_width = 0;
234            for cell in row.iter_mut() {
235                let vertical = (has_vertical && i + 1 != count_cells) as usize;
236                if available < row_width + vertical {
237                    break;
238                }
239
240                let available = available - row_width - vertical;
241                let width = match truncate_table_value(cell, has_vertical, available) {
242                    Some(width) => width,
243                    None => break,
244                };
245
246                row_width += width + vertical;
247                last_used_width = row_width;
248                i += 1;
249            }
250
251            if i == row.len() {
252                return Some(row_width);
253            }
254
255            if i == 0 {
256                if available >= PAD + TRUNCATE_CELL_WIDTH {
257                    *value = TableValue::Cell(String::from("..."));
258                    return Some(PAD + TRUNCATE_CELL_WIDTH);
259                } else {
260                    return None;
261                }
262            }
263
264            let available = available - row_width;
265            let has_space_empty_cell = available >= PAD + TRUNCATE_CELL_WIDTH;
266            if has_space_empty_cell {
267                row[i] = TableValue::Cell(String::from("..."));
268                row.truncate(i + 1);
269                row_width += PAD + TRUNCATE_CELL_WIDTH;
270            } else if i == 0 {
271                return None;
272            } else {
273                row[i - 1] = TableValue::Cell(String::from("..."));
274                row.truncate(i);
275                row_width -= last_used_width;
276                row_width += PAD + TRUNCATE_CELL_WIDTH;
277            }
278
279            Some(row_width)
280        }
281        TableValue::Column(column) => {
282            let mut max_width = PAD;
283            for cell in column.iter_mut() {
284                let width = truncate_table_value(cell, has_vertical, available)?;
285                max_width = std::cmp::max(max_width, width);
286            }
287
288            Some(max_width)
289        }
290        TableValue::Cell(text) => {
291            if available <= PAD {
292                return None;
293            }
294
295            let available = available - PAD;
296            let width = string_width(text);
297
298            if width > available {
299                if available > MIN_CONTENT_WIDTH {
300                    *text = string_wrap(text, available, false);
301                    Some(available + PAD)
302                } else if available >= 3 {
303                    *text = String::from("...");
304                    Some(3 + PAD)
305                } else {
306                    // situation where we have too little space
307                    None
308                }
309            } else {
310                Some(width + PAD)
311            }
312        }
313    }
314}
315
316fn build_table_with_border_color(mut table: PoolTable, color: Color) -> String {
317    // NOTE: We have this function presizely because of color_into_ansistr internals
318    // color must be alive  why we build table
319
320    let color = color_into_ansistr(&color);
321    table.with(SetBorderColor(color));
322    table.to_string()
323}
324
325fn color_into_ansistr(color: &Color) -> ANSIStr<'static> {
326    // # SAFETY
327    //
328    // It's perfectly save to do cause table does not store the reference internally.
329    // We just need this unsafe section to cope with some limitations of [`PoolTable`].
330    // Mitigation of this is definitely on a todo list.
331
332    let prefix = color.get_prefix();
333    let suffix = color.get_suffix();
334    let prefix: &'static str = unsafe { std::mem::transmute(prefix) };
335    let suffix: &'static str = unsafe { std::mem::transmute(suffix) };
336
337    ANSIStr::new(prefix, suffix)
338}
339
340struct SetBorderColor(ANSIStr<'static>);
341
342impl<R, D> TableOption<R, CompactMultilineConfig, D> for SetBorderColor {
343    fn change(self, _: &mut R, cfg: &mut CompactMultilineConfig, _: &mut D) {
344        let borders = Borders::filled(self.0);
345        cfg.set_borders_color(borders);
346    }
347}
348
349fn get_border_color(style: &StyleComputer<'_>) -> Option<Color> {
350    // color_config closures for "separator" are just given a null.
351    let color = style.compute("separator", &Value::nothing(Span::unknown()));
352    let color = color.paint(" ").to_string();
353    let color = Color::try_from(color);
354    color.ok()
355}