Give tabled a try (#5969)

* Drop in replacement from nu-table to tabled.

Must act the same way as original nu-table.

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

Fix some issues

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* Bump ansi-str version

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* Update to latest

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* Fix footer issue

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* Fix header alignment

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* Fix header style

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* Use latest tabled/ansi-str

* Refactorings

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* Fix clippy warnings

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>
This commit is contained in:
Maxim Zhiburt 2022-07-06 22:57:40 +03:00 committed by GitHub
parent e77219a59f
commit d1687df067
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 667 additions and 1053 deletions

149
Cargo.lock generated
View file

@ -35,7 +35,7 @@ checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom 0.2.6",
"once_cell",
"version_check 0.9.4",
"version_check",
]
[[package]]
@ -69,22 +69,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e9c9abb82613923ec78d7a461595d52491ba7240f3c64c0bbe0e6d98e0fce0"
[[package]]
name = "ansi-parser"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcb2392079bf27198570d6af79ecbd9ec7d8f16d3ec6b60933922fdb66287127"
name = "ansi-str"
version = "0.2.0"
source = "git+https://github.com/zhiburt/ansi-str?rev=655cd8125a032286082794690c2cc6dc835345b4#655cd8125a032286082794690c2cc6dc835345b4"
dependencies = [
"heapless 0.5.6",
"nom 4.2.3",
"ansi_rs",
]
[[package]]
name = "ansi-str"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d04a41a1463228d6eed971b9cdadd86b5577c69c09fd2379c57fe5e159071"
name = "ansi_rs"
version = "0.1.0"
source = "git+https://gitlab.com/zhiburt/ansi_rs#f45b8378e5ac7bafc0ca2d2748505fe92e2af6d6"
dependencies = [
"ansi-parser",
"nom 7.1.1",
]
[[package]]
@ -164,18 +161,6 @@ dependencies = [
"strength_reduce",
]
[[package]]
name = "as-slice"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0"
dependencies = [
"generic-array 0.12.4",
"generic-array 0.13.3",
"generic-array 0.14.5",
"stable_deref_trait",
]
[[package]]
name = "assert_cmd"
version = "2.0.4"
@ -317,7 +302,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array 0.14.5",
"generic-array",
]
[[package]]
@ -326,7 +311,7 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
"generic-array 0.14.5",
"generic-array",
]
[[package]]
@ -386,6 +371,12 @@ dependencies = [
"utf8-width",
]
[[package]]
name = "bytecount"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
[[package]]
name = "bytemuck"
version = "1.9.1"
@ -724,7 +715,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
dependencies = [
"generic-array 0.14.5",
"generic-array",
"typenum",
]
@ -856,7 +847,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array 0.14.5",
"generic-array",
]
[[package]]
@ -1329,24 +1320,6 @@ dependencies = [
"byteorder",
]
[[package]]
name = "generic-array"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
dependencies = [
"typenum",
]
[[package]]
name = "generic-array"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309"
dependencies = [
"typenum",
]
[[package]]
name = "generic-array"
version = "0.14.5"
@ -1354,7 +1327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [
"typenum",
"version_check 0.9.4",
"version_check",
]
[[package]]
@ -1483,15 +1456,6 @@ dependencies = [
"regex",
]
[[package]]
name = "hash32"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc"
dependencies = [
"byteorder",
]
[[package]]
name = "hash32"
version = "0.2.1"
@ -1535,18 +1499,6 @@ dependencies = [
"hashbrown 0.11.2",
]
[[package]]
name = "heapless"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74911a68a1658cfcfb61bc0ccfbd536e3b6e906f8c2f7883ee50157e3e2184f1"
dependencies = [
"as-slice",
"generic-array 0.13.3",
"hash32 0.1.1",
"stable_deref_trait",
]
[[package]]
name = "heapless"
version = "0.7.13"
@ -1554,7 +1506,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a08e755adbc0ad283725b29f4a4883deee15336f372d5f61fae59efec40f983"
dependencies = [
"atomic-polyfill",
"hash32 0.2.1",
"hash32",
"rustc_version 0.4.0",
"spin",
"stable_deref_trait",
@ -2412,16 +2364,6 @@ version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
[[package]]
name = "nom"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
dependencies = [
"memchr",
"version_check 0.1.5",
]
[[package]]
name = "nom"
version = "7.1.1"
@ -2722,7 +2664,7 @@ dependencies = [
name = "nu-pretty-hex"
version = "0.65.1"
dependencies = [
"heapless 0.7.13",
"heapless",
"nu-ansi-term",
"rand 0.8.5",
]
@ -2771,6 +2713,7 @@ dependencies = [
"nu-protocol",
"regex",
"strip-ansi-escapes",
"tabled",
"unicode-width",
]
@ -3092,6 +3035,17 @@ version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b"
[[package]]
name = "papergrid"
version = "0.4.0"
source = "git+https://github.com/zhiburt/tabled?rev=e3cbdea5b81edda8b3cc7b9f64f98fecae7db423#e3cbdea5b81edda8b3cc7b9f64f98fecae7db423"
dependencies = [
"ansi-str",
"bytecount",
"strip-ansi-escapes",
"unicode-width",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
@ -3544,7 +3498,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn",
"version_check 0.9.4",
"version_check",
]
[[package]]
@ -3555,7 +3509,7 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check 0.9.4",
"version_check",
]
[[package]]
@ -4658,6 +4612,29 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "tabled"
version = "0.7.0"
source = "git+https://github.com/zhiburt/tabled?rev=e3cbdea5b81edda8b3cc7b9f64f98fecae7db423#e3cbdea5b81edda8b3cc7b9f64f98fecae7db423"
dependencies = [
"ansi-str",
"papergrid",
"tabled_derive",
"unicode-width",
]
[[package]]
name = "tabled_derive"
version = "0.3.0"
source = "git+https://github.com/zhiburt/tabled?rev=e3cbdea5b81edda8b3cc7b9f64f98fecae7db423#e3cbdea5b81edda8b3cc7b9f64f98fecae7db423"
dependencies = [
"heck 0.4.0",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tempdir"
version = "0.3.7"
@ -4952,7 +4929,7 @@ version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622"
dependencies = [
"version_check 0.9.4",
"version_check",
]
[[package]]
@ -5080,12 +5057,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc1631c774f0f9570797191e01247cbefde789eebfbf128074cb934115a6133"
[[package]]
name = "version_check"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
[[package]]
name = "version_check"
version = "0.9.4"

View file

@ -173,7 +173,8 @@ impl Command for Table {
theme: load_theme_from_config(config),
};
let result = nu_table::draw_table(&table, term_width, &color_hm, config);
let result = nu_table::draw_table(&table, term_width, &color_hm, config)
.unwrap_or_else(|| format!("Couldn't fit table into {} columns!", term_width));
Ok(Value::String {
val: result,
@ -556,7 +557,8 @@ impl Iterator for PagingTableCreator {
match table {
Ok(Some(table)) => {
let result = nu_table::draw_table(&table, term_width, &color_hm, &self.config);
let result = nu_table::draw_table(&table, term_width, &color_hm, &self.config)
.unwrap_or_else(|| format!("Couldn't fit table into {} columns!", term_width));
Some(Ok(result.as_bytes().to_vec()))
}

View file

@ -17,5 +17,8 @@ nu-protocol = { path = "../nu-protocol", version = "0.65.1" }
regex = "1.4"
unicode-width = "0.1.8"
strip-ansi-escapes = "0.1.1"
ansi-str = "0.2.0"
ansi-str = { git = "https://github.com/zhiburt/ansi-str", rev = "655cd8125a032286082794690c2cc6dc835345b4" }
atty = "0.2.14"
tabled = { git = "https://github.com/zhiburt/tabled", rev = "e3cbdea5b81edda8b3cc7b9f64f98fecae7db423", features = [
"color",
] }

View file

@ -5,5 +5,4 @@ mod wrap;
pub use table::{draw_table, Table};
pub use table_theme::TableTheme;
pub use textstyle::{StyledString, TextStyle};
pub use wrap::Alignment;
pub use textstyle::{Alignment, StyledString, TextStyle};

View file

@ -29,7 +29,8 @@ fn main() {
// get the default config
let config = Config::default();
// Capture the table as a string
let output_table = draw_table(&table, width, &color_hm, &config);
let output_table = draw_table(&table, width, &color_hm, &config)
.unwrap_or_else(|| format!("Couldn't fit table into {} columns!", width));
// Draw the table
println!("{}", output_table)
}

View file

@ -1,16 +1,16 @@
use crate::table_theme::TableTheme;
use crate::wrap::{column_width, split_sublines, wrap, Alignment, Subline, WrappedCell};
use crate::{StyledString, TextStyle};
use nu_ansi_term::{AnsiString, AnsiStrings, Style};
use crate::StyledString;
use nu_ansi_term::Style;
use nu_protocol::{Config, FooterMode};
use std::collections::HashMap;
use std::fmt::Write;
enum SeparatorPosition {
Top,
Middle,
Bottom,
}
use tabled::{
builder::Builder,
formatting_settings::AlignmentStrategy,
object::{Cell, Columns, Rows},
papergrid,
style::BorderColor,
Alignment, Modify, TableOption,
};
#[derive(Debug)]
pub struct Table {
@ -33,669 +33,176 @@ impl Table {
}
}
#[derive(Debug)]
pub struct ProcessedTable {
pub headers: Vec<ProcessedCell>,
pub data: Vec<Vec<ProcessedCell>>,
pub theme: TableTheme,
}
#[derive(Debug)]
pub struct ProcessedCell {
pub contents: Vec<Vec<Subline>>,
pub style: TextStyle,
}
#[derive(Debug)]
pub struct WrappedTable {
pub column_widths: Vec<usize>,
pub headers: Vec<WrappedCell>,
pub data: Vec<Vec<WrappedCell>>,
pub theme: TableTheme,
pub footer: Vec<WrappedCell>,
}
impl WrappedTable {
fn print_separator(
&self,
separator_position: SeparatorPosition,
color_hm: &HashMap<String, Style>,
) -> String {
let column_count = self.column_widths.len();
let mut output: Vec<AnsiString> = Vec::new();
let sep_color = color_hm
.get("separator")
.unwrap_or(&Style::default())
.to_owned();
match separator_position {
SeparatorPosition::Top => {
for column in self.column_widths.iter().enumerate() {
if column.0 == 0 && self.theme.print_left_border {
output.push(sep_color.paint(self.theme.top_left.to_string()));
}
for _ in 0..*column.1 {
output.push(sep_color.paint(self.theme.top_horizontal.to_string()));
}
output.push(sep_color.paint(self.theme.top_horizontal.to_string()));
output.push(sep_color.paint(self.theme.top_horizontal.to_string()));
if column.0 == column_count - 1 {
if self.theme.print_right_border {
output.push(sep_color.paint(self.theme.top_right.to_string()));
}
} else {
output.push(sep_color.paint(self.theme.top_center.to_string()));
}
}
output.push(AnsiString::from("\n".to_string()));
}
SeparatorPosition::Middle => {
for column in self.column_widths.iter().enumerate() {
if column.0 == 0 && self.theme.print_left_border {
output.push(sep_color.paint(self.theme.middle_left.to_string()));
}
for _ in 0..*column.1 {
output.push(sep_color.paint(self.theme.middle_horizontal.to_string()));
}
output.push(sep_color.paint(self.theme.middle_horizontal.to_string()));
output.push(sep_color.paint(self.theme.middle_horizontal.to_string()));
if column.0 == column_count - 1 {
if self.theme.print_right_border {
output.push(sep_color.paint(self.theme.middle_right.to_string()));
}
} else {
output.push(sep_color.paint(self.theme.center.to_string()));
}
}
output.push(AnsiString::from("\n".to_string()));
}
SeparatorPosition::Bottom => {
for column in self.column_widths.iter().enumerate() {
if column.0 == 0 && self.theme.print_left_border {
output.push(sep_color.paint(self.theme.bottom_left.to_string()));
}
for _ in 0..*column.1 {
output.push(sep_color.paint(self.theme.bottom_horizontal.to_string()));
}
output.push(sep_color.paint(self.theme.bottom_horizontal.to_string()));
output.push(sep_color.paint(self.theme.bottom_horizontal.to_string()));
if column.0 == column_count - 1 {
if self.theme.print_right_border {
output.push(sep_color.paint(self.theme.bottom_right.to_string()));
}
} else {
output.push(sep_color.paint(self.theme.bottom_center.to_string()));
}
}
}
}
AnsiStrings(&output[..]).to_string()
}
fn print_cell_contents(
&self,
cells: &[WrappedCell],
color_hm: &HashMap<String, Style>,
) -> String {
let sep_color = color_hm
.get("separator")
.unwrap_or(&Style::default())
.to_owned();
let mut total_output = String::new();
for current_line in 0.. {
let mut lines_printed = 0;
let mut output = String::new();
if self.theme.print_left_border {
output.push_str(
&sep_color
.paint(&self.theme.left_vertical.to_string())
.to_string(),
);
}
for column in cells.iter().enumerate() {
if let Some(line) = (column.1).lines.get(current_line) {
let remainder = self.column_widths[column.0] - line.width;
output.push(' ');
match column.1.style.alignment {
Alignment::Left => {
if let Some(color) = column.1.style.color_style {
output.push_str(&color.paint(&line.line).to_string());
} else {
output.push_str(&line.line);
}
for _ in 0..remainder {
output.push(' ');
}
}
Alignment::Center => {
for _ in 0..remainder / 2 {
output.push(' ');
}
if let Some(color) = column.1.style.color_style {
output.push_str(&color.paint(&line.line).to_string());
} else {
output.push_str(&line.line);
}
for _ in 0..(remainder / 2 + remainder % 2) {
output.push(' ');
}
}
Alignment::Right => {
for _ in 0..remainder {
output.push(' ');
}
if let Some(color) = column.1.style.color_style {
output.push_str(&color.paint(&line.line).to_string());
} else {
output.push_str(&line.line);
}
}
}
output.push(' ');
lines_printed += 1;
} else {
for _ in 0..self.column_widths[column.0] + 2 {
output.push(' ');
}
}
if column.0 < cells.len() - 1 {
output.push_str(
&sep_color
.paint(&self.theme.center_vertical.to_string())
.to_string(),
);
} else if self.theme.print_right_border {
output.push_str(
&sep_color
.paint(&self.theme.right_vertical.to_string())
.to_string(),
);
}
}
if lines_printed == 0 {
break;
}
writeln!(&mut total_output, "{}", output).expect("writing should be done to buffer");
}
total_output
}
fn print_table(&self, color_hm: &HashMap<String, Style>, config: &Config) -> String {
let mut output = String::new();
// TODO: This may be unnecessary after JTs changes. Let's remove it and see.
// #[cfg(windows)]
// {
// let _ = nu_ansi_term::enable_ansi_support();
// }
if self.data.is_empty() {
return output;
}
// The top border
if self.theme.print_top_border {
output.push_str(&self.print_separator(SeparatorPosition::Top, color_hm));
}
// The header
let skip_headers = (self.headers.len() == 2 && self.headers[1].max_width == 0)
|| (self.headers.len() == 1 && self.headers[0].max_width == 0);
if !self.headers.is_empty() && !skip_headers {
output.push_str(&self.print_cell_contents(&self.headers, color_hm));
}
// The middle section
let mut first_row = true;
for row in &self.data {
if !first_row {
if self.theme.separate_rows {
output.push_str(&self.print_separator(SeparatorPosition::Middle, color_hm));
}
} else {
first_row = false;
if self.theme.separate_header && !self.headers.is_empty() && !skip_headers {
output.push_str(&self.print_separator(SeparatorPosition::Middle, color_hm));
}
}
output.push_str(&self.print_cell_contents(row, color_hm));
}
match config.footer_mode {
FooterMode::Always => {
if self.theme.separate_header && !self.headers.is_empty() && !skip_headers {
output.push_str(&self.print_separator(SeparatorPosition::Middle, color_hm));
}
if !self.headers.is_empty() && !skip_headers {
output.push_str(&self.print_cell_contents(&self.footer, color_hm));
}
}
FooterMode::RowCount(r) => {
if self.data.len() as u64 > r {
if self.theme.separate_header && !self.headers.is_empty() && !skip_headers {
output.push_str(&self.print_separator(SeparatorPosition::Middle, color_hm));
}
if !self.headers.is_empty() && !skip_headers {
output.push_str(&self.print_cell_contents(&self.footer, color_hm));
}
}
}
_ => {} // Never and Auto aka auto get eaten and nothing happens
}
// The table finish
if self.theme.print_bottom_border {
output.push_str(&self.print_separator(SeparatorPosition::Bottom, color_hm));
}
// the atty is for when people do ls from vim, there should be no coloring there
if !config.use_ansi_coloring || !atty::is(atty::Stream::Stdout) {
// Draw the table without ansi colors
if let Ok(bytes) = strip_ansi_escapes::strip(&output) {
String::from_utf8_lossy(&bytes).to_string()
} else {
output
}
} else {
// Draw the table with ansi colors
output
}
}
}
fn process_table(table: &Table) -> ProcessedTable {
let mut processed_data = vec![];
for row in &table.data {
let mut out_row = vec![];
for column in row {
let cleaned = clean(&column.contents);
out_row.push(ProcessedCell {
contents: split_sublines(&cleaned),
style: column.style,
});
}
processed_data.push(out_row);
}
let mut processed_headers = vec![];
for header in &table.headers {
let cleaned = clean(&header.contents);
processed_headers.push(ProcessedCell {
contents: split_sublines(&cleaned),
style: header.style,
});
}
ProcessedTable {
headers: processed_headers,
data: processed_data,
theme: table.theme.clone(),
}
}
fn clean(input: &str) -> String {
let input = input.replace('\r', "");
input.replace('\t', " ")
}
fn get_max_column_widths(processed_table: &ProcessedTable) -> Vec<usize> {
use std::cmp::max;
let mut max_num_columns = 0;
max_num_columns = max(max_num_columns, processed_table.headers.len());
for row in &processed_table.data {
max_num_columns = max(max_num_columns, row.len());
}
let mut output = vec![0; max_num_columns];
for column in processed_table.headers.iter().enumerate() {
output[column.0] = max(output[column.0], column_width(&column.1.contents));
}
for row in &processed_table.data {
for column in row.iter().enumerate() {
output[column.0] = max(output[column.0], column_width(&column.1.contents));
}
}
output
}
pub fn maybe_truncate_columns(termwidth: usize, processed_table: &mut ProcessedTable) {
// 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 < processed_table.headers.len() {
processed_table.headers.truncate(max_num_of_columns);
for entry in processed_table.data.iter_mut() {
entry.truncate(max_num_of_columns);
}
processed_table.headers.push(ProcessedCell {
contents: vec![vec![Subline {
subline: "...".to_string(),
width: 3,
}]],
style: TextStyle::basic_center(),
});
for entry in processed_table.data.iter_mut() {
entry.push(ProcessedCell {
contents: vec![vec![Subline {
subline: "...".to_string(),
width: 3,
}]],
style: TextStyle::basic_center(),
}); // ellipsis is centred
}
}
}
pub fn draw_table(
table: &Table,
termwidth: usize,
color_hm: &HashMap<String, Style>,
config: &Config,
) -> String {
) -> Option<String> {
// Remove the edges, if used
let edges_width = if table.theme.print_left_border && table.theme.print_right_border {
3
} else if table.theme.print_left_border || table.theme.print_right_border {
1
let (headers, data) = crate::wrap::wrap(&table.headers, &table.data, termwidth, &table.theme)?;
let headers = if headers.is_empty() {
None
} else {
0
Some(headers)
};
if termwidth < edges_width {
return format!("Couldn't fit table into {} columns!", termwidth);
}
let alignments = build_alignment_map(&table.data);
let raw_termwidth = termwidth;
let termwidth = termwidth - edges_width;
let theme = &table.theme;
let mut processed_table = process_table(table);
let with_header = headers.is_some();
let with_footer = with_header && need_footer(config, data.len() as u64);
let max_per_column = get_max_column_widths(&processed_table);
let table = build_table(data, headers, Some(alignments), config, with_footer);
let table = load_theme(table, color_hm, theme, with_footer, with_header);
maybe_truncate_columns(termwidth, &mut processed_table);
let headers_len = processed_table.headers.len();
// fix the length of the table if there are no headers:
let headers_len = if headers_len == 0 {
if !table.data.is_empty() && !table.data[0].is_empty() {
table.data[0].len()
} else {
return String::new();
}
} else {
headers_len
};
// Measure how big our columns need to be (accounting for separators also)
let max_naive_column_width = (termwidth - 3 * (headers_len - 1)) / headers_len;
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 = match max_column_width {
None => return format!("Couldn't fit table into {} columns!", raw_termwidth),
Some(max_column_width) => 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);
let re_leading =
regex::Regex::new(r"(?P<beginsp>^\s+)").expect("error with leading space regex");
let re_trailing =
regex::Regex::new(r"(?P<endsp>\s+$)").expect("error with trailing space regex");
let wrapped_table = match max_column_width {
None => return format!("Couldn't fit table into {} columns!", raw_termwidth),
Some(max_column_width) => wrap_cells(
processed_table,
max_column_width,
color_hm,
&re_leading,
&re_trailing,
),
};
wrapped_table.print_table(color_hm, config)
print_table(table, termwidth)
}
fn wrap_cells(
processed_table: ProcessedTable,
max_column_width: usize,
fn print_table(table: tabled::Table, term_width: usize) -> Option<String> {
let s = table.to_string();
let width = s.lines().next().map(papergrid::string_width).unwrap_or(0);
if width > term_width {
return None;
}
Some(s)
}
fn build_alignment_map(data: &[Vec<StyledString>]) -> Vec<Vec<Alignment>> {
let mut v = vec![Vec::new(); data.len()];
for (i, row) in data.iter().enumerate() {
let mut row_alignments = Vec::with_capacity(row.len());
for col in row {
row_alignments.push(Alignment::Horizontal(col.style.alignment));
}
v[i] = row_alignments;
}
v
}
fn build_table(
data: Vec<Vec<String>>,
headers: Option<Vec<String>>,
alignment_map: Option<Vec<Vec<Alignment>>>,
config: &Config,
need_footer: bool,
) -> tabled::Table {
let header_present = headers.is_some();
let mut builder = Builder::from(data);
if let Some(headers) = headers {
builder = builder.set_columns(headers.clone());
if need_footer {
builder = builder.add_record(headers);
}
}
let mut table = builder.build();
table = table.with(
Modify::new(Rows::new(1..))
.with(Alignment::left())
.with(AlignmentStrategy::PerLine),
);
if !config.disable_table_indexes {
table = table.with(Modify::new(Columns::first()).with(Alignment::right()));
}
if header_present {
table = table.with(Modify::new(Rows::first()).with(Alignment::center()));
}
if let Some(alignments) = alignment_map {
table = apply_alignments(table, alignments, header_present);
}
table
}
fn apply_alignments(
mut table: tabled::Table,
alignment: Vec<Vec<Alignment>>,
header_present: bool,
) -> tabled::Table {
let offset = if header_present { 1 } else { 0 };
for (row, alignments) in alignment.into_iter().enumerate() {
for (col, alignment) in alignments.into_iter().enumerate() {
table = table.with(Modify::new(Cell(row + offset, col)).with(alignment));
}
}
table
}
fn load_theme(
mut table: tabled::Table,
color_hm: &HashMap<String, Style>,
re_leading: &regex::Regex,
re_trailing: &regex::Regex,
) -> WrappedTable {
let mut column_widths = vec![
0;
std::cmp::max(
processed_table.headers.len(),
if !processed_table.data.is_empty() {
processed_table.data[0].len()
} else {
0
}
)
];
let mut output_headers = vec![];
for header in processed_table.headers.into_iter().enumerate() {
let mut wrapped = WrappedCell {
lines: vec![],
max_width: 0,
style: header.1.style,
};
theme: &TableTheme,
with_footer: bool,
with_header: bool,
) -> tabled::Table {
table = table.with(theme.theme.clone());
for contents in header.1.contents.into_iter() {
let (mut lines, inner_max_width) = wrap(
max_column_width,
contents.into_iter(),
color_hm,
re_leading,
re_trailing,
);
wrapped.lines.append(&mut lines);
if inner_max_width > wrapped.max_width {
wrapped.max_width = inner_max_width;
}
if let Some(color) = color_hm.get("separator") {
let color = color.paint(" ").to_string();
if let Ok(color) = BorderColor::try_from(color) {
table = table.with(color);
}
if column_widths[header.0] < wrapped.max_width {
column_widths[header.0] = wrapped.max_width;
}
output_headers.push(wrapped);
}
let mut output_data = vec![];
for row in processed_table.data.into_iter() {
let mut output_row = vec![];
for column in row.into_iter().enumerate() {
let mut wrapped = WrappedCell {
lines: vec![],
max_width: 0,
style: column.1.style,
};
for contents in column.1.contents.into_iter() {
let (mut lines, inner_max_width) = wrap(
max_column_width,
contents.into_iter(),
color_hm,
re_leading,
re_trailing,
);
wrapped.lines.append(&mut lines);
if inner_max_width > wrapped.max_width {
wrapped.max_width = inner_max_width;
}
}
if column_widths[column.0] < wrapped.max_width {
column_widths[column.0] = wrapped.max_width;
}
output_row.push(wrapped);
}
output_data.push(output_row);
if with_footer {
table = table.with(FooterStyle).with(
Modify::new(Rows::last())
.with(Alignment::center())
.with(AlignmentStrategy::PerCell),
);
}
let mut footer = vec![
WrappedCell {
lines: vec![],
max_width: 0,
style: TextStyle {
..Default::default()
},
};
output_headers.len()
];
footer.clone_from_slice(&output_headers[..]);
if !with_header {
table = table.with(RemoveHeaderLine);
}
WrappedTable {
column_widths,
headers: output_headers,
data: output_data,
theme: processed_table.theme,
footer,
table
}
fn need_footer(config: &Config, count_records: u64) -> bool {
matches!(config.footer_mode, FooterMode::RowCount(limit) if count_records > limit)
|| matches!(config.footer_mode, FooterMode::Always)
}
struct FooterStyle;
impl TableOption for FooterStyle {
fn change(&mut self, grid: &mut papergrid::Grid) {
if grid.count_columns() == 0 || grid.count_rows() == 0 {
return;
}
let mut line = papergrid::Line::default();
let border = grid.get_border((0, 0));
line.left = border.left_bottom_corner;
line.intersection = border.right_bottom_corner;
line.horizontal = border.bottom;
let border = grid.get_border((0, grid.count_columns() - 1));
line.right = border.right_bottom_corner;
grid.set_split_line(grid.count_rows() - 1, line);
}
}
struct ColumnSpace {
num_overages: usize,
underage_sum: usize,
overage_separator_sum: usize,
}
struct RemoveHeaderLine;
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);
for (i, &column_max) in iter {
if column_max > max_naive_column_width {
num_overages += 1;
if i != (headers_len - 1) {
overage_separator_sum += 3;
}
if i == 0 {
overage_separator_sum += 1;
}
} else {
underage_sum += column_max;
// if column isn't last, add 3 for its separator
if i != (headers_len - 1) {
underage_sum += 3;
}
if i == 0 {
underage_sum += 1;
}
}
}
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);
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) {
underage_sum += 3;
}
if i == 0 {
underage_sum += 1;
}
} else {
// Column is still too large, so let's count it
num_overages += 1;
if i != (headers_len - 1) {
overage_separator_sum += 3;
}
if i == 0 {
overage_separator_sum += 1;
}
}
}
}
ColumnSpace {
num_overages,
underage_sum,
overage_separator_sum,
}
}
fn max_width(&self, termwidth: usize) -> Option<usize> {
let ColumnSpace {
num_overages,
underage_sum,
overage_separator_sum,
} = self;
if *num_overages > 0 {
termwidth
.checked_sub(1)?
.checked_sub(*underage_sum)?
.checked_sub(*overage_separator_sum)?
.checked_div(*num_overages)
} else {
Some(99999)
}
impl TableOption for RemoveHeaderLine {
fn change(&mut self, grid: &mut papergrid::Grid) {
grid.set_split_line(1, papergrid::Line::default());
}
}

View file

@ -1,329 +1,130 @@
use tabled::{style::StyleConfig, Style};
#[derive(Debug, Clone)]
pub struct TableTheme {
pub top_left: char,
pub middle_left: char,
pub bottom_left: char,
pub top_center: char,
pub center: char,
pub bottom_center: char,
pub top_right: char,
pub middle_right: char,
pub bottom_right: char,
pub top_horizontal: char,
pub middle_horizontal: char,
pub bottom_horizontal: char,
pub left_vertical: char,
pub center_vertical: char,
pub right_vertical: char,
pub separate_header: bool,
pub separate_rows: bool,
pub print_left_border: bool,
pub print_right_border: bool,
pub print_top_border: bool,
pub print_bottom_border: bool,
pub(crate) theme: StyleConfig,
pub(crate) is_left_set: bool,
pub(crate) is_right_set: bool,
}
impl TableTheme {
#[allow(unused)]
pub fn basic() -> TableTheme {
TableTheme {
top_left: '+',
middle_left: '+',
bottom_left: '+',
top_center: '+',
center: '+',
bottom_center: '+',
top_right: '+',
middle_right: '+',
bottom_right: '+',
top_horizontal: '-',
middle_horizontal: '-',
bottom_horizontal: '-',
left_vertical: '|',
center_vertical: '|',
right_vertical: '|',
separate_header: true,
separate_rows: true,
print_left_border: true,
print_right_border: true,
print_top_border: true,
print_bottom_border: true,
Self {
theme: Style::ascii().into(),
is_left_set: true,
is_right_set: true,
}
}
#[allow(unused)]
pub fn thin() -> TableTheme {
TableTheme {
top_left: '',
middle_left: '',
bottom_left: '',
top_center: '',
center: '',
bottom_center: '',
top_right: '',
middle_right: '',
bottom_right: '',
top_horizontal: '',
middle_horizontal: '',
bottom_horizontal: '',
left_vertical: '',
center_vertical: '',
right_vertical: '',
separate_header: true,
separate_rows: true,
print_left_border: true,
print_right_border: true,
print_top_border: true,
print_bottom_border: true,
Self {
theme: Style::modern().into(),
is_left_set: true,
is_right_set: true,
}
}
#[allow(unused)]
pub fn light() -> TableTheme {
TableTheme {
top_left: ' ',
middle_left: '',
bottom_left: ' ',
top_center: ' ',
center: '',
bottom_center: ' ',
top_right: ' ',
middle_right: '',
bottom_right: ' ',
top_horizontal: ' ',
middle_horizontal: '',
bottom_horizontal: ' ',
left_vertical: ' ',
center_vertical: ' ',
right_vertical: ' ',
separate_header: true,
separate_rows: false,
print_left_border: true,
print_right_border: true,
print_top_border: false,
print_bottom_border: true,
Self {
theme: Style::blank().header('─').into(),
is_left_set: false,
is_right_set: false,
}
}
#[allow(unused)]
pub fn compact() -> TableTheme {
TableTheme {
top_left: '',
middle_left: '',
bottom_left: '',
top_center: '',
center: '',
bottom_center: '',
top_right: '',
middle_right: '',
bottom_right: '',
top_horizontal: '',
middle_horizontal: '',
bottom_horizontal: '',
left_vertical: ' ',
center_vertical: '',
right_vertical: ' ',
separate_header: true,
separate_rows: false,
print_left_border: false,
print_right_border: false,
print_top_border: true,
print_bottom_border: true,
Self {
theme: Style::modern()
.left_off()
.right_off()
.horizontal_off()
.into(),
is_left_set: false,
is_right_set: false,
}
}
#[allow(unused)]
pub fn with_love() -> TableTheme {
TableTheme {
top_left: '',
middle_left: '',
bottom_left: '',
top_center: '',
center: '',
bottom_center: '',
top_right: '',
middle_right: '',
bottom_right: '',
top_horizontal: '',
middle_horizontal: '',
bottom_horizontal: '',
left_vertical: ' ',
center_vertical: '',
right_vertical: ' ',
separate_header: true,
separate_rows: false,
print_left_border: false,
print_right_border: false,
print_top_border: true,
print_bottom_border: true,
Self {
theme: Style::psql()
.header('❤')
.top('❤')
.bottom('❤')
.vertical('❤')
.into(),
is_left_set: false,
is_right_set: false,
}
}
#[allow(unused)]
pub fn compact_double() -> TableTheme {
TableTheme {
top_left: '',
middle_left: '',
bottom_left: '',
top_center: '',
center: '',
bottom_center: '',
top_right: '',
middle_right: '',
bottom_right: '',
top_horizontal: '',
middle_horizontal: '',
bottom_horizontal: '',
left_vertical: ' ',
center_vertical: '',
right_vertical: ' ',
separate_header: true,
separate_rows: false,
print_left_border: false,
print_right_border: false,
print_top_border: true,
print_bottom_border: true,
Self {
theme: Style::psql()
.header('═')
.top('═')
.bottom('═')
.vertical('║')
.top_intersection('╦')
.bottom_intersection('╩')
.header_intersection('╬')
.into(),
is_left_set: false,
is_right_set: false,
}
}
#[allow(unused)]
pub fn rounded() -> TableTheme {
TableTheme {
top_left: '',
middle_left: '',
bottom_left: '',
top_center: '',
center: '',
bottom_center: '',
top_right: '',
middle_right: '',
bottom_right: '',
top_horizontal: '',
middle_horizontal: '',
bottom_horizontal: '',
left_vertical: '',
center_vertical: '',
right_vertical: '',
separate_header: true,
separate_rows: false,
print_left_border: true,
print_right_border: true,
print_top_border: true,
print_bottom_border: true,
Self {
theme: Style::rounded().into(),
is_left_set: true,
is_right_set: true,
}
}
#[allow(unused)]
pub fn reinforced() -> TableTheme {
TableTheme {
top_left: '',
middle_left: '',
bottom_left: '',
top_center: '',
center: '',
bottom_center: '',
top_right: '',
middle_right: '',
bottom_right: '',
top_horizontal: '',
middle_horizontal: '',
bottom_horizontal: '',
left_vertical: '',
center_vertical: '',
right_vertical: '',
separate_header: true,
separate_rows: false,
print_left_border: true,
print_right_border: true,
print_top_border: true,
print_bottom_border: true,
Self {
theme: Style::modern()
.top_left_corner('┏')
.top_right_corner('┓')
.bottom_left_corner('┗')
.bottom_right_corner('┛')
.horizontal_off()
.into(),
is_left_set: true,
is_right_set: true,
}
}
#[allow(unused)]
pub fn heavy() -> TableTheme {
TableTheme {
top_left: '',
middle_left: '',
bottom_left: '',
top_center: '',
center: '',
bottom_center: '',
top_right: '',
middle_right: '',
bottom_right: '',
top_horizontal: '',
middle_horizontal: '',
bottom_horizontal: '',
left_vertical: '',
center_vertical: '',
right_vertical: '',
separate_header: true,
separate_rows: false,
print_left_border: true,
print_right_border: true,
print_top_border: true,
print_bottom_border: true,
Self {
theme: Style::modern()
.header('━')
.top('━')
.bottom('━')
.vertical('┃')
.left('┃')
.right('┃')
.left_intersection('┣')
.right_intersection('┫')
.bottom_intersection('┻')
.top_intersection('┳')
.top_left_corner('┏')
.top_right_corner('┓')
.bottom_left_corner('┗')
.bottom_right_corner('┛')
.header_intersection('╋')
.horizontal_off()
.into(),
is_left_set: true,
is_right_set: true,
}
}
#[allow(unused)]
pub fn none() -> TableTheme {
TableTheme {
top_left: ' ',
middle_left: ' ',
bottom_left: ' ',
top_center: ' ',
center: ' ',
bottom_center: ' ',
top_right: ' ',
middle_right: ' ',
bottom_right: ' ',
top_horizontal: ' ',
middle_horizontal: ' ',
bottom_horizontal: ' ',
left_vertical: ' ',
center_vertical: ' ',
right_vertical: ' ',
separate_header: false,
separate_rows: false,
print_left_border: false,
print_right_border: false,
print_top_border: false,
print_bottom_border: false,
Self {
theme: Style::blank().into(),
is_left_set: false,
is_right_set: false,
}
}
}

View file

@ -1,6 +1,7 @@
use crate::wrap::Alignment;
use nu_ansi_term::{Color, Style};
pub type Alignment = tabled::AlignmentHorizontal;
#[derive(Debug, Clone, Copy)]
pub struct TextStyle {
pub alignment: Alignment,
@ -239,7 +240,7 @@ impl Default for TextStyle {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct StyledString {
pub contents: String,
pub style: TextStyle,

View file

@ -1,18 +1,12 @@
use crate::textstyle::TextStyle;
use crate::{StyledString, TableTheme};
use ansi_str::AnsiStr;
use nu_ansi_term::Style;
use std::borrow::Cow;
use std::collections::HashMap;
use std::{fmt::Display, iter::Iterator};
use std::iter::Iterator;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
#[derive(Debug, Clone, Copy)]
pub enum Alignment {
Left,
Center,
Right,
}
#[derive(Debug)]
pub struct Subline {
pub subline: String,
@ -39,21 +33,6 @@ pub struct WrappedCell {
pub style: TextStyle,
}
impl Display for Line {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut first = true;
for subline in &self.sublines {
if !first {
write!(f, " ")?;
} else {
first = false;
}
write!(f, "{}", subline.subline)?;
}
Ok(())
}
}
/// Removes ANSI escape codes and some ASCII control characters
///
/// Keeps `\n` removes `\r`, `\t` etc.
@ -181,7 +160,7 @@ fn split_word(cell_width: usize, word: &str) -> Vec<Subline> {
output
}
pub fn wrap(
pub fn wrap_content(
cell_width: usize,
mut input: impl Iterator<Item = Subline>,
color_hm: &HashMap<String, Style>,
@ -318,3 +297,353 @@ pub fn wrap(
(output, current_max)
}
pub fn wrap(
headers: &[StyledString],
data: &[Vec<StyledString>],
termwidth: usize,
theme: &TableTheme,
) -> Option<(Vec<String>, Vec<Vec<String>>)> {
// Remove the edges, if used
let edges_width = if theme.is_left_set && theme.is_right_set {
3
} else if theme.is_left_set || theme.is_right_set {
1
} else {
0
};
if termwidth < edges_width {
return None;
}
let termwidth = termwidth - edges_width;
let (mut headers_splited, mut data_splited) = split_lines(headers, data);
let max_per_column = get_max_column_widths(&headers_splited, &data_splited);
maybe_truncate_columns(termwidth, &mut headers_splited, &mut data_splited);
let mut headers_len = headers_splited.len();
if headers_len == 0 {
if !data.is_empty() && !data[0].is_empty() {
headers_len = data_splited[0].len();
} else {
return Some((Vec::new(), Vec::new()));
}
}
// Measure how big our columns need to be (accounting for separators also)
let max_naive_column_width = (termwidth - 3 * (headers_len - 1)) / headers_len;
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)?;
let re_leading =
regex::Regex::new(r"(?P<beginsp>^\s+)").expect("error with leading space regex");
let re_trailing =
regex::Regex::new(r"(?P<endsp>\s+$)").expect("error with trailing space regex");
let result = wrap_cells(
headers_splited,
data_splited,
max_column_width,
&re_leading,
&re_trailing,
);
Some(result)
}
struct ContentLines {
pub lines: Vec<Vec<Subline>>,
pub style: TextStyle,
}
fn split_lines(
headers: &[StyledString],
data: &[Vec<StyledString>],
) -> (Vec<ContentLines>, Vec<Vec<ContentLines>>) {
let mut splited_headers = Vec::with_capacity(headers.len());
for column in headers {
let content = clean(&column.contents);
let lines = split_sublines(&content);
splited_headers.push(ContentLines {
lines,
style: column.style,
});
}
let mut splited_data = Vec::with_capacity(data.len());
for row in data {
let mut splited_row = Vec::with_capacity(row.len());
for column in row {
let content = clean(&column.contents);
let lines = split_sublines(&content);
splited_row.push(ContentLines {
lines,
style: column.style,
});
}
splited_data.push(splited_row);
}
(splited_headers, splited_data)
}
fn get_max_column_widths(headers: &[ContentLines], data: &[Vec<ContentLines>]) -> Vec<usize> {
use std::cmp::max;
let mut max_num_columns = 0;
max_num_columns = max(max_num_columns, headers.len());
for row in data {
max_num_columns = max(max_num_columns, row.len());
}
let mut output = vec![0; max_num_columns];
for (col, content) in headers.iter().enumerate() {
output[col] = max(output[col], column_width(&content.lines));
}
for row in data {
for (col, content) in row.iter().enumerate() {
output[col] = max(output[col], column_width(&content.lines));
}
}
output
}
fn wrap_cells(
headers_splited: Vec<ContentLines>,
data_splited: Vec<Vec<ContentLines>>,
max_column_width: usize,
re_leading: &regex::Regex,
re_trailing: &regex::Regex,
) -> (Vec<String>, Vec<Vec<String>>) {
let mut header = vec![String::new(); headers_splited.len()];
for (col, splited) in headers_splited.into_iter().enumerate() {
let mut wrapped = vec![];
for contents in splited.lines {
let (mut lines, _) = wrap_content(
max_column_width,
contents.into_iter(),
&HashMap::new(),
re_leading,
re_trailing,
);
wrapped.append(&mut lines);
}
let content = wrapped
.into_iter()
.map(|l| l.line)
.collect::<Vec<_>>()
.join("\n");
let content = splited
.style
.color_style
.map(|color| color.paint(&content).to_string())
.unwrap_or(content);
header[col] = content;
}
let mut data = vec![Vec::new(); data_splited.len()];
for (row, splited) in data_splited.into_iter().enumerate() {
for splited in splited.into_iter() {
let mut wrapped = vec![];
for contents in splited.lines {
let (mut lines, _) = wrap_content(
max_column_width,
contents.into_iter(),
&HashMap::new(),
re_leading,
re_trailing,
);
wrapped.append(&mut lines);
}
let content = wrapped
.into_iter()
.map(|l| l.line)
.collect::<Vec<_>>()
.join("\n");
let content = splited
.style
.color_style
.map(|color| color.paint(&content).to_string())
.unwrap_or(content);
data[row].push(content);
}
}
(header, data)
}
fn maybe_truncate_columns(
termwidth: usize,
headers: &mut Vec<ContentLines>,
data: &mut [Vec<ContentLines>],
) {
// 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);
headers.push(ContentLines {
lines: vec![vec![Subline {
subline: String::from("..."),
width: 3,
}]],
style: TextStyle::basic_center(),
});
}
if max_num_of_columns < headers.len() {
for entry in data.iter_mut() {
entry.truncate(max_num_of_columns);
entry.push(ContentLines {
lines: vec![vec![Subline {
subline: String::from("..."),
width: 3,
}]],
style: TextStyle::basic_center(),
});
}
}
}
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);
for (i, &column_max) in iter {
if column_max > max_naive_column_width {
num_overages += 1;
if i != (headers_len - 1) {
overage_separator_sum += 3;
}
if i == 0 {
overage_separator_sum += 1;
}
} else {
underage_sum += column_max;
// if column isn't last, add 3 for its separator
if i != (headers_len - 1) {
underage_sum += 3;
}
if i == 0 {
underage_sum += 1;
}
}
}
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);
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) {
underage_sum += 3;
}
if i == 0 {
underage_sum += 1;
}
} else {
// Column is still too large, so let's count it
num_overages += 1;
if i != (headers_len - 1) {
overage_separator_sum += 3;
}
if i == 0 {
overage_separator_sum += 1;
}
}
}
}
ColumnSpace {
num_overages,
underage_sum,
overage_separator_sum,
}
}
fn max_width(&self, termwidth: usize) -> Option<usize> {
let ColumnSpace {
num_overages,
underage_sum,
overage_separator_sum,
} = self;
if *num_overages > 0 {
termwidth
.checked_sub(1)?
.checked_sub(*underage_sum)?
.checked_sub(*overage_separator_sum)?
.checked_div(*num_overages)
} else {
Some(99999)
}
}
}
fn clean(input: &str) -> String {
let input = input.replace('\r', "");
input.replace('\t', " ")
}