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
9 changed files with 667 additions and 1053 deletions

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', " ")
}