1use self::global_horizontal_char::SetHorizontalChar;
8use nu_protocol::Value;
9use nu_protocol::engine::EngineState;
10use nu_table::{string_width, string_wrap};
11use tabled::{
12 Table,
13 grid::config::ColoredConfig,
14 settings::{Style, peaker::Priority, width::Wrap},
15};
16
17pub fn build_table(
18 engine_state: &EngineState,
19 value: Value,
20 description: String,
21 termsize: usize,
22) -> String {
23 let (head, mut data) = util::collect_input(engine_state, value);
24 let count_columns = head.len();
25 data.insert(0, head);
26
27 let mut desc = description;
28 let mut desc_width = string_width(&desc);
29 let mut desc_table_width = get_total_width_2_column_table(11, desc_width);
30
31 let cfg = Table::default().with(Style::modern()).get_config().clone();
32 let mut widths = get_data_widths(&data, count_columns);
33 truncate_data(&mut data, &mut widths, &cfg, termsize);
34
35 let val_table_width = get_total_width2(&widths, &cfg);
36 if val_table_width < desc_table_width {
37 increase_widths(&mut widths, desc_table_width - val_table_width);
38 increase_data_width(&mut data, &widths);
39 }
40
41 if val_table_width > desc_table_width {
42 desc_width += val_table_width - desc_table_width;
43 increase_string_width(&mut desc, desc_width);
44 }
45
46 if desc_table_width > termsize {
47 let delete_width = desc_table_width - termsize;
48 if delete_width >= desc_width {
49 return String::new();
51 }
52
53 desc_width -= delete_width;
54 desc = string_wrap(&desc, desc_width, false);
55 desc_table_width = termsize;
56 }
57
58 add_padding_to_widths(&mut widths);
59
60 let width = val_table_width.max(desc_table_width).min(termsize);
61
62 let mut desc_table = Table::from_iter([[String::from("description"), desc]]);
63 desc_table.with(Style::rounded().remove_bottom().remove_horizontals());
64
65 let mut val_table = Table::from_iter(data);
66 val_table.get_dimension_mut().set_widths(widths);
67 val_table.with(Style::rounded().corner_top_left('├').corner_top_right('┤'));
68 val_table.with((
69 Wrap::new(width).priority(Priority::max(true)),
70 SetHorizontalChar::new('┼', '┴', 11 + 2 + 1),
71 ));
72
73 format!("{desc_table}\n{val_table}")
74}
75
76fn get_data_widths(data: &[Vec<String>], count_columns: usize) -> Vec<usize> {
77 let mut widths = vec![0; count_columns];
78 for row in data {
79 for col in 0..count_columns {
80 let text = &row[col];
81 let width = string_width(text);
82 widths[col] = std::cmp::max(widths[col], width);
83 }
84 }
85
86 widths
87}
88
89fn add_padding_to_widths(widths: &mut [usize]) {
90 for width in widths {
91 *width += 2;
92 }
93}
94
95fn increase_widths(widths: &mut [usize], need: usize) {
96 let all = need / widths.len();
97 let mut rest = need - all * widths.len();
98
99 for width in widths {
100 *width += all;
101
102 if rest > 0 {
103 *width += 1;
104 rest -= 1;
105 }
106 }
107}
108
109fn increase_data_width(data: &mut Vec<Vec<String>>, widths: &[usize]) {
110 for row in data {
111 for (col, max_width) in widths.iter().enumerate() {
112 let text = &mut row[col];
113 increase_string_width(text, *max_width);
114 }
115 }
116}
117
118fn increase_string_width(text: &mut String, total: usize) {
119 let width = string_width(text);
120 let rest = total - width;
121
122 if rest > 0 {
123 text.extend(std::iter::repeat_n(' ', rest));
124 }
125}
126
127fn get_total_width_2_column_table(col1: usize, col2: usize) -> usize {
128 const PAD: usize = 1;
129 const SPLIT_LINE: usize = 1;
130 SPLIT_LINE + PAD + col1 + PAD + SPLIT_LINE + PAD + col2 + PAD + SPLIT_LINE
131}
132
133fn truncate_data(
134 data: &mut Vec<Vec<String>>,
135 widths: &mut Vec<usize>,
136 cfg: &ColoredConfig,
137 expected_width: usize,
138) {
139 const SPLIT_LINE_WIDTH: usize = 1;
140 const PAD: usize = 2;
141
142 let total_width = get_total_width2(widths, cfg);
143 if total_width <= expected_width {
144 return;
145 }
146
147 let mut width = 0;
148 let mut peak_count = 0;
149 for column_width in widths.iter() {
150 let next_width = width + *column_width + SPLIT_LINE_WIDTH + PAD;
151 if next_width >= expected_width {
152 break;
153 }
154
155 width = next_width;
156 peak_count += 1;
157 }
158
159 debug_assert!(peak_count < widths.len());
160
161 let left_space = expected_width - width;
162 let has_space_for_truncation_column = left_space > PAD;
163 if !has_space_for_truncation_column {
164 peak_count = peak_count.saturating_sub(1);
165 }
166
167 remove_columns(data, peak_count);
168 widths.drain(peak_count..);
169 push_empty_column(data);
170 widths.push(1);
171}
172
173fn remove_columns(data: &mut Vec<Vec<String>>, peak_count: usize) {
174 if peak_count == 0 {
175 for row in data {
176 row.clear();
177 }
178 } else {
179 for row in data {
180 row.drain(peak_count..);
181 }
182 }
183}
184
185fn get_total_width2(widths: &[usize], cfg: &ColoredConfig) -> usize {
186 let pad = 2;
187 let total = widths.iter().sum::<usize>() + pad * widths.len();
188 let countv = cfg.count_vertical(widths.len());
189 let margin = cfg.get_margin();
190
191 total + countv + margin.left.size + margin.right.size
192}
193
194fn push_empty_column(data: &mut Vec<Vec<String>>) {
195 let empty_cell = String::from("‥");
196 for row in data {
197 row.push(empty_cell.clone());
198 }
199}
200
201mod util {
202 use crate::debug::explain::debug_string_without_formatting;
203 use nu_engine::get_columns;
204 use nu_protocol::Value;
205 use nu_protocol::engine::EngineState;
206
207 pub fn collect_input(
209 engine_state: &EngineState,
210 value: Value,
211 ) -> (Vec<String>, Vec<Vec<String>>) {
212 let span = value.span();
213 match value {
214 Value::Record { val: record, .. } => {
215 let (cols, vals): (Vec<_>, Vec<_>) = record.into_owned().into_iter().unzip();
216 (
217 match cols.is_empty() {
218 true => vec![String::from("")],
219 false => cols,
220 },
221 match vals
222 .into_iter()
223 .map(|s| debug_string_without_formatting(engine_state, &s))
224 .collect::<Vec<String>>()
225 {
226 vals if vals.is_empty() => vec![],
227 vals => vec![vals],
228 },
229 )
230 }
231 Value::List { vals, .. } => {
232 let mut columns = get_columns(&vals);
233 let data = convert_records_to_dataset(engine_state, &columns, vals);
234
235 if columns.is_empty() {
236 columns = vec![String::from("")];
237 }
238
239 (columns, data)
240 }
241 Value::String { val, .. } => {
242 let lines = val
243 .lines()
244 .map(|line| Value::string(line.to_string(), span))
245 .map(|val| vec![debug_string_without_formatting(engine_state, &val)])
246 .collect();
247
248 (vec![String::from("")], lines)
249 }
250 Value::Nothing { .. } => (vec![], vec![]),
251 value => (
252 vec![String::from("")],
253 vec![vec![debug_string_without_formatting(engine_state, &value)]],
254 ),
255 }
256 }
257
258 fn convert_records_to_dataset(
259 engine_state: &EngineState,
260 cols: &[String],
261 records: Vec<Value>,
262 ) -> Vec<Vec<String>> {
263 if !cols.is_empty() {
264 create_table_for_record(engine_state, cols, &records)
265 } else if cols.is_empty() && records.is_empty() {
266 vec![]
267 } else if cols.len() == records.len() {
268 vec![
269 records
270 .into_iter()
271 .map(|s| debug_string_without_formatting(engine_state, &s))
272 .collect(),
273 ]
274 } else {
275 records
276 .into_iter()
277 .map(|record| vec![debug_string_without_formatting(engine_state, &record)])
278 .collect()
279 }
280 }
281
282 fn create_table_for_record(
283 engine_state: &EngineState,
284 headers: &[String],
285 items: &[Value],
286 ) -> Vec<Vec<String>> {
287 let mut data = vec![Vec::new(); items.len()];
288
289 for (i, item) in items.iter().enumerate() {
290 let row = record_create_row(engine_state, headers, item);
291 data[i] = row;
292 }
293
294 data
295 }
296
297 fn record_create_row(
298 engine_state: &EngineState,
299 headers: &[String],
300 item: &Value,
301 ) -> Vec<String> {
302 if let Value::Record { val, .. } = item {
303 headers
304 .iter()
305 .map(|col| {
306 val.get(col)
307 .map(|v| debug_string_without_formatting(engine_state, v))
308 .unwrap_or_else(String::new)
309 })
310 .collect()
311 } else {
312 vec![String::new(); headers.len()]
315 }
316 }
317}
318
319mod global_horizontal_char {
320 use nu_table::NuRecords;
321 use tabled::{
322 grid::{
323 config::{ColoredConfig, Offset, Position},
324 dimension::{CompleteDimension, Dimension},
325 records::{ExactRecords, Records},
326 },
327 settings::TableOption,
328 };
329
330 pub struct SetHorizontalChar {
331 intersection: char,
332 split: char,
333 index: usize,
334 }
335
336 impl SetHorizontalChar {
337 pub fn new(intersection: char, split: char, index: usize) -> Self {
338 Self {
339 intersection,
340 split,
341 index,
342 }
343 }
344 }
345
346 impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for SetHorizontalChar {
347 fn change(
348 self,
349 records: &mut NuRecords,
350 cfg: &mut ColoredConfig,
351 dimension: &mut CompleteDimension,
352 ) {
353 let count_columns = records.count_columns();
354 let count_rows = records.count_rows();
355
356 if count_columns == 0 || count_rows == 0 {
357 return;
358 }
359
360 let widths = get_widths(dimension, records.count_columns());
361
362 let has_vertical = cfg.has_vertical(0, count_columns);
363 if has_vertical && self.index == 0 {
364 let mut border = cfg.get_border(Position::new(0, 0), (count_rows, count_columns));
365 border.left_top_corner = Some(self.intersection);
366 cfg.set_border(Position::new(0, 0), border);
367 return;
368 }
369
370 let mut i = 1;
371 for (col, width) in widths.into_iter().enumerate() {
372 if self.index < i + width {
373 let o = self.index - i;
374 cfg.set_horizontal_char(Position::new(0, col), Offset::Start(o), self.split);
375 return;
376 }
377
378 i += width;
379
380 let has_vertical = cfg.has_vertical(col, count_columns);
381 if has_vertical {
382 if self.index == i {
383 let mut border =
384 cfg.get_border(Position::new(0, col), (count_rows, count_columns));
385 border.right_top_corner = Some(self.intersection);
386 cfg.set_border(Position::new(0, col), border);
387 return;
388 }
389
390 i += 1;
391 }
392 }
393 }
394 }
395
396 fn get_widths(dims: &CompleteDimension, count_columns: usize) -> Vec<usize> {
397 let mut widths = vec![0; count_columns];
398 for (col, width) in widths.iter_mut().enumerate() {
399 *width = dims.get_width(col);
400 }
401
402 widths
403 }
404}