diff --git a/src/format/table.rs b/src/format/table.rs index fc4eab8e62..687417a932 100644 --- a/src/format/table.rs +++ b/src/format/table.rs @@ -9,13 +9,15 @@ use textwrap::fill; use prettytable::format::{FormatBuilder, LinePosition, LineSeparator}; use prettytable::{color, Attr, Cell, Row, Table}; +type Entries = Vec>; + #[derive(Debug, new)] pub struct TableView { // List of header cell values: headers: Vec, // List of rows of cells, each containing value and prettytable style-string: - entries: Vec>, + entries: Entries, } enum TableMode { @@ -50,110 +52,169 @@ impl TableView { return None; } - let mut headers = TableView::merge_descriptors(values); - - if headers.is_empty() { - headers.push("".to_string()); - } - - let mut entries = vec![]; - - for (idx, value) in values.iter().enumerate() { - let mut row: Vec<(String, &'static str)> = headers - .iter() - .map(|d| { - if d == "" { - match value { - Value { - value: UntaggedValue::Row(..), - .. - } => ( - format_leaf(&UntaggedValue::nothing()).plain_string(100_000), - style_leaf(&UntaggedValue::nothing()), - ), - _ => (format_leaf(value).plain_string(100_000), style_leaf(value)), - } - } else { - match value { - Value { - value: UntaggedValue::Row(..), - .. - } => { - let data = value.get_data(d); - ( - format_leaf(data.borrow()).plain_string(100_000), - style_leaf(data.borrow()), - ) - } - _ => ( - format_leaf(&UntaggedValue::nothing()).plain_string(100_000), - style_leaf(&UntaggedValue::nothing()), - ), - } - } - }) - .collect(); - - if values.len() > 1 { - // Indices are black, bold, right-aligned: - row.insert(0, ((starting_idx + idx).to_string(), "Fdbr")); - } - - entries.push(row); - } - - let mut max_per_column = vec![]; - - if values.len() > 1 { - headers.insert(0, "#".to_owned()); - } - - for i in 0..headers.len() { - let mut current_col_max = 0; - let iter = entries.iter().take(values.len()); - - for entry in iter { - let value_length = entry[i].0.chars().count(); - if value_length > current_col_max { - current_col_max = value_length; - } - } - - max_per_column.push(std::cmp::max(current_col_max, headers[i].chars().count())); - } - // Different platforms want different amounts of buffer, not sure why let termwidth = std::cmp::max(textwrap::termwidth(), 20); - // Make sure we have enough space for the columns we have - let max_num_of_columns = termwidth / 10; + let mut headers = TableView::merge_descriptors(values); + let mut entries = values_to_entries(values, &mut headers, starting_idx); + let max_per_column = max_per_column(&headers, &entries, values.len()); - // If we have too many columns, truncate the table - if max_num_of_columns < headers.len() { - headers.truncate(max_num_of_columns); + maybe_truncate_columns(&mut headers, &mut entries, termwidth); + let headers_len = headers.len(); - for entry in &mut entries { - entry.truncate(max_num_of_columns); - } + // Measure how big our columns need to be (accounting for separators also) + let max_naive_column_width = (termwidth - 3 * (headers_len - 1)) / headers_len; - headers.push("...".to_owned()); - for entry in &mut entries { - entry.push(("...".to_owned(), "c")); // ellipsis is centred + let column_space = + ColumnSpace::measure(&max_per_column, max_naive_column_width, headers_len); + + // This gives us the max column width + let max_column_width = column_space.max_width(termwidth); + + // This width isn't quite right, as we're rounding off some of our space + let column_space = column_space.fix_almost_column_width( + &max_per_column, + max_naive_column_width, + max_column_width, + headers_len, + ); + + // This should give us the final max column width + let max_column_width = column_space.max_width(termwidth); + + // Wrap cells as needed + let table_view = wrap_cells( + headers, + entries, + max_per_column, + max_naive_column_width, + max_column_width, + ); + Some(table_view) + } +} + +fn values_to_entries(values: &[Value], headers: &mut Vec, starting_idx: usize) -> Entries { + let mut entries = vec![]; + let values_len = values.len(); + + if headers.is_empty() { + headers.push("".to_string()); + } + + for (idx, value) in values.iter().enumerate() { + let mut row: Vec<(String, &'static str)> = headers + .iter() + .map(|d: &String| { + if d == "" { + match value { + Value { + value: UntaggedValue::Row(..), + .. + } => ( + format_leaf(&UntaggedValue::nothing()).plain_string(100_000), + style_leaf(&UntaggedValue::nothing()), + ), + _ => (format_leaf(value).plain_string(100_000), style_leaf(value)), + } + } else { + match value { + Value { + value: UntaggedValue::Row(..), + .. + } => { + let data = value.get_data(d); + ( + format_leaf(data.borrow()).plain_string(100_000), + style_leaf(data.borrow()), + ) + } + _ => ( + format_leaf(&UntaggedValue::nothing()).plain_string(100_000), + style_leaf(&UntaggedValue::nothing()), + ), + } + } + }) + .collect(); + + if values_len > 1 { + // Indices are black, bold, right-aligned: + row.insert(0, ((starting_idx + idx).to_string(), "Fdbr")); + } + + entries.push(row); + } + + if values_len > 1 { + headers.insert(0, "#".to_owned()); + } + + entries +} + +fn max_per_column(headers: &Vec, entries: &Entries, values_len: usize) -> Vec { + let mut max_per_column = vec![]; + + for i in 0..headers.len() { + let mut current_col_max = 0; + let iter = entries.iter().take(values_len); + + for entry in iter { + let value_length = entry[i].0.chars().count(); + if value_length > current_col_max { + current_col_max = value_length; } } - // Measure how big our columns need to be (accounting for separators also) - let max_naive_column_width = (termwidth - 3 * (headers.len() - 1)) / headers.len(); + max_per_column.push(std::cmp::max(current_col_max, headers[i].chars().count())); + } - // Measure how much space we have once we subtract off the columns who are small enough + max_per_column +} + +fn maybe_truncate_columns(headers: &mut Vec, entries: &mut Entries, termwidth: usize) { + // Make sure we have enough space for the columns we have + let max_num_of_columns = termwidth / 10; + + // If we have too many columns, truncate the table + if max_num_of_columns < headers.len() { + headers.truncate(max_num_of_columns); + + for entry in &mut entries.into_iter() { + entry.truncate(max_num_of_columns); + } + + headers.push("...".to_owned()); + + for entry in &mut entries.into_iter() { + entry.push(("...".to_owned(), "c")); // ellipsis is centred + } + } +} + +struct ColumnSpace { + num_overages: usize, + underage_sum: usize, + overage_separator_sum: usize, +} + +impl ColumnSpace { + /// Measure how much space we have once we subtract off the columns who are small enough + fn measure( + max_per_column: &[usize], + max_naive_column_width: usize, + headers_len: usize, + ) -> ColumnSpace { let mut num_overages = 0; let mut underage_sum = 0; let mut overage_separator_sum = 0; - let iter = max_per_column.iter().enumerate().take(headers.len()); + let iter = max_per_column.iter().enumerate().take(headers_len); + for (i, &column_max) in iter { if column_max > max_naive_column_width { num_overages += 1; - if i != (headers.len() - 1) { + if i != (headers_len - 1) { overage_separator_sum += 3; } if i == 0 { @@ -162,7 +223,7 @@ impl TableView { } else { underage_sum += column_max; // if column isn't last, add 3 for its separator - if i != (headers.len() - 1) { + if i != (headers_len - 1) { underage_sum += 3; } if i == 0 { @@ -171,23 +232,31 @@ impl TableView { } } - // This gives us the max column width - let max_column_width = if num_overages > 0 { - (termwidth - 1 - underage_sum - overage_separator_sum) / num_overages - } else { - 99999 - }; + ColumnSpace { + num_overages, + underage_sum, + overage_separator_sum, + } + } + + fn fix_almost_column_width( + self, + max_per_column: &[usize], + max_naive_column_width: usize, + max_column_width: usize, + headers_len: usize, + ) -> ColumnSpace { + let mut num_overages = 0; + let mut overage_separator_sum = 0; + let mut underage_sum = self.underage_sum; + let iter = max_per_column.iter().enumerate().take(headers_len); - // This width isn't quite right, as we're rounding off some of our space - num_overages = 0; - overage_separator_sum = 0; - let iter = max_per_column.iter().enumerate().take(headers.len()); for (i, &column_max) in iter { if column_max > max_naive_column_width { if column_max <= max_column_width { underage_sum += column_max; // if column isn't last, add 3 for its separator - if i != (headers.len() - 1) { + if i != (headers_len - 1) { underage_sum += 3; } if i == 0 { @@ -196,7 +265,7 @@ impl TableView { } else { // Column is still too large, so let's count it num_overages += 1; - if i != (headers.len() - 1) { + if i != (headers_len - 1) { overage_separator_sum += 3; } if i == 0 { @@ -205,25 +274,53 @@ impl TableView { } } } - // This should give us the final max column width - let max_column_width = if num_overages > 0 { - (termwidth - 1 - underage_sum - overage_separator_sum) / num_overages + + ColumnSpace { + num_overages, + underage_sum, + overage_separator_sum, + } + } + + fn max_width(&self, termwidth: usize) -> usize { + let ColumnSpace { + num_overages, + underage_sum, + overage_separator_sum, + } = self; + + if *num_overages > 0 { + (termwidth - 1 - *underage_sum - *overage_separator_sum) / *num_overages } else { 99999 - }; + } + } +} + +fn wrap_cells( + mut headers: Vec, + mut entries: Entries, + max_per_column: Vec, + max_naive_column_width: usize, + max_column_width: usize, +) -> TableView { + { + let entries = &mut entries; - // Wrap cells as needed for head in 0..headers.len() { if max_per_column[head] > max_naive_column_width { headers[head] = fill(&headers[head], max_column_width); - for entry in &mut entries { + for entry in &mut entries.into_iter() { entry[head].0 = fill(&entry[head].0, max_column_width); } } } + } - Some(TableView { headers, entries }) + TableView { + headers: headers, + entries: entries, } }