Skip to main content

nu_table/types/
general.rs

1use nu_color_config::TextStyle;
2use nu_engine::column::get_columns;
3use nu_protocol::{Config, Record, ShellError, Value};
4
5use crate::{
6    NuRecordsValue, NuTable, StringResult, TableOpts, TableOutput, TableResult, clean_charset,
7    colorize_space,
8    common::{
9        INDEX_COLUMN_NAME, NuText, check_value, configure_table, error_sign, get_header_style,
10        get_index_style, get_value_style, nu_value_to_string_colored,
11    },
12    types::has_index,
13};
14
15pub struct JustTable;
16
17impl JustTable {
18    pub fn table(input: Vec<Value>, opts: TableOpts<'_>) -> StringResult {
19        list_table(input, opts)
20    }
21
22    pub fn kv_table(record: Record, opts: TableOpts<'_>) -> StringResult {
23        kv_table(record, opts)
24    }
25}
26
27fn list_table(input: Vec<Value>, opts: TableOpts<'_>) -> Result<Option<String>, ShellError> {
28    let output = create_table(input, &opts)?;
29    let mut out = match output {
30        Some(out) => out,
31        None => return Ok(None),
32    };
33
34    // TODO: It would be WAY more effitient to do right away instead of second pass over the data.
35    colorize_space(out.table.get_records_mut(), &opts.style_computer);
36
37    configure_table(&mut out, opts.config, &opts.style_computer, opts.mode);
38    let table = out.table.draw(opts.width);
39
40    Ok(table)
41}
42
43fn get_key_style(topts: &TableOpts<'_>) -> TextStyle {
44    get_header_style(&topts.style_computer).alignment(nu_color_config::Alignment::Left)
45}
46
47fn kv_table(record: Record, opts: TableOpts<'_>) -> StringResult {
48    let mut table = NuTable::new(record.len(), 2);
49    table.set_index_style(get_key_style(&opts));
50    table.set_indent(opts.config.table.padding);
51
52    for (i, (key, value)) in record.into_iter().enumerate() {
53        opts.signals.check(&opts.span)?;
54
55        let value = nu_value_to_string_colored(&value, opts.config, &opts.style_computer);
56
57        table.insert((i, 0), key);
58        table.insert((i, 1), value);
59    }
60
61    let mut out = TableOutput::from_table(table, false, true);
62    configure_table(&mut out, opts.config, &opts.style_computer, opts.mode);
63    let table = out.table.draw(opts.width);
64
65    Ok(table)
66}
67
68fn create_table(input: Vec<Value>, opts: &TableOpts<'_>) -> TableResult {
69    if input.is_empty() {
70        return Ok(None);
71    }
72
73    let headers = get_columns(&input);
74    let with_index = has_index(opts, &headers);
75    let with_header = !headers.is_empty();
76    let row_offset = opts.index_offset;
77
78    let table = match (with_header, with_index) {
79        (true, true) => create_table_with_header_and_index(input, headers, row_offset, opts)?,
80        (true, false) => create_table_with_header(input, headers, opts)?,
81        (false, true) => create_table_with_no_header_and_index(input, row_offset, opts)?,
82        (false, false) => create_table_with_no_header(input, opts)?,
83    };
84
85    let table = table.map(|table| TableOutput::from_table(table, with_header, with_index));
86
87    Ok(table)
88}
89
90fn create_table_with_header(
91    input: Vec<Value>,
92    headers: Vec<String>,
93    opts: &TableOpts<'_>,
94) -> Result<Option<NuTable>, ShellError> {
95    let count_rows = input.len() + 1;
96    let count_columns = headers.len();
97    let mut table = NuTable::new(count_rows, count_columns);
98    table.set_header_style(get_header_style(&opts.style_computer));
99    table.set_index_style(get_index_style(&opts.style_computer));
100    table.set_indent(opts.config.table.padding);
101    let priority_columns = resolve_priority_columns(&headers, &opts.width_priority_columns, false);
102    table.set_width_priority_columns(&priority_columns);
103
104    for (row, item) in input.into_iter().enumerate() {
105        opts.signals.check(&opts.span)?;
106        check_value(&item)?;
107
108        for (col, header) in headers.iter().enumerate() {
109            let (text, style) = get_string_value_with_header(&item, header, opts);
110
111            let pos = (row + 1, col);
112            table.insert(pos, text);
113            table.insert_style(pos, style);
114        }
115    }
116
117    let headers = collect_headers(headers, false);
118    table.set_row(0, headers);
119
120    Ok(Some(table))
121}
122
123fn create_table_with_header_and_index(
124    input: Vec<Value>,
125    headers: Vec<String>,
126    row_offset: usize,
127    opts: &TableOpts<'_>,
128) -> Result<Option<NuTable>, ShellError> {
129    let priority_columns = resolve_priority_columns(&headers, &opts.width_priority_columns, true);
130    let head = collect_headers(headers, true);
131
132    let count_rows = input.len() + 1;
133    let count_columns = head.len();
134
135    let mut table = NuTable::new(count_rows, count_columns);
136    table.set_header_style(get_header_style(&opts.style_computer));
137    table.set_index_style(get_index_style(&opts.style_computer));
138    table.set_indent(opts.config.table.padding);
139    table.set_width_priority_columns(&priority_columns);
140
141    table.set_row(0, head.clone());
142
143    for (row, item) in input.into_iter().enumerate() {
144        opts.signals.check(&opts.span)?;
145        check_value(&item)?;
146
147        let text = get_table_row_index(&item, opts.config, row, row_offset);
148        table.insert((row + 1, 0), text);
149
150        for (col, head) in head.iter().enumerate().skip(1) {
151            let (text, style) = get_string_value_with_header(&item, head.as_ref(), opts);
152
153            let pos = (row + 1, col);
154            table.insert(pos, text);
155            table.insert_style(pos, style);
156        }
157    }
158
159    Ok(Some(table))
160}
161
162fn create_table_with_no_header(
163    input: Vec<Value>,
164    opts: &TableOpts<'_>,
165) -> Result<Option<NuTable>, ShellError> {
166    let mut table = NuTable::new(input.len(), 1);
167    table.set_index_style(get_index_style(&opts.style_computer));
168    table.set_indent(opts.config.table.padding);
169
170    for (row, item) in input.into_iter().enumerate() {
171        opts.signals.check(&opts.span)?;
172        check_value(&item)?;
173
174        let (text, style) = get_string_value(&item, opts);
175
176        table.insert((row, 0), text);
177        table.insert_style((row, 0), style);
178    }
179
180    Ok(Some(table))
181}
182
183fn create_table_with_no_header_and_index(
184    input: Vec<Value>,
185    row_offset: usize,
186    opts: &TableOpts<'_>,
187) -> Result<Option<NuTable>, ShellError> {
188    let mut table = NuTable::new(input.len(), 1 + 1);
189    table.set_index_style(get_index_style(&opts.style_computer));
190    table.set_indent(opts.config.table.padding);
191
192    for (row, item) in input.into_iter().enumerate() {
193        opts.signals.check(&opts.span)?;
194        check_value(&item)?;
195
196        let index = get_table_row_index(&item, opts.config, row, row_offset);
197        let (value, style) = get_string_value(&item, opts);
198
199        table.insert((row, 0), index);
200        table.insert((row, 1), value);
201        table.insert_style((row, 1), style);
202    }
203
204    Ok(Some(table))
205}
206
207fn get_string_value_with_header(item: &Value, header: &str, opts: &TableOpts) -> NuText {
208    match item {
209        Value::Record { val, .. } => match val.get(header) {
210            Some(value) => get_string_value(value, opts),
211            None => error_sign(
212                opts.config.table.missing_value_symbol.clone(),
213                &opts.style_computer,
214            ),
215        },
216        value => get_string_value(value, opts),
217    }
218}
219
220fn get_string_value(item: &Value, opts: &TableOpts) -> NuText {
221    let (mut text, style) = get_value_style(item, opts.config, &opts.style_computer);
222
223    let is_string = matches!(item, Value::String { .. });
224    if is_string {
225        text = clean_charset(&text);
226    }
227
228    (text, style)
229}
230
231fn get_table_row_index(item: &Value, config: &Config, row: usize, offset: usize) -> String {
232    match item {
233        Value::Record { val, .. } => val
234            .get(INDEX_COLUMN_NAME)
235            .map(|value| value.to_expanded_string("", config))
236            .unwrap_or_else(|| (row + offset).to_string()),
237        _ => (row + offset).to_string(),
238    }
239}
240
241fn collect_headers(headers: Vec<String>, index: bool) -> Vec<NuRecordsValue> {
242    // The header with the INDEX is removed from the table headers since
243    // it is added to the natural table index
244    let length = if index {
245        headers.len() + 1
246    } else {
247        headers.len()
248    };
249
250    let mut v = Vec::with_capacity(length);
251
252    if index {
253        v.insert(0, NuRecordsValue::new("#".into()));
254    }
255
256    for text in headers {
257        // Only filter out the INDEX column when we're adding our own index column
258        if index && text == INDEX_COLUMN_NAME {
259            continue;
260        }
261
262        v.push(NuRecordsValue::new(text));
263    }
264
265    v
266}
267
268/// Resolves configured priority column names into concrete table column indexes.
269///
270/// The returned indexes preserve caller order and contain no duplicates.
271fn resolve_priority_columns(
272    headers: &[String],
273    width_priority_columns: &[String],
274    with_index: bool,
275) -> Vec<usize> {
276    let mut resolved = Vec::new();
277
278    for priority_column in width_priority_columns {
279        let mut resolved_index = None;
280
281        if with_index && priority_column == INDEX_COLUMN_NAME {
282            resolved_index = Some(0);
283        } else {
284            let mut table_index = usize::from(with_index);
285            for header in headers {
286                if header == INDEX_COLUMN_NAME && with_index {
287                    continue;
288                }
289
290                if header == priority_column {
291                    resolved_index = Some(table_index);
292                    break;
293                }
294
295                table_index += 1;
296            }
297        }
298
299        if let Some(index) = resolved_index
300            && !resolved.contains(&index)
301        {
302            resolved.push(index);
303        }
304    }
305
306    resolved
307}