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
16pub 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 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 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 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 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 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}