Bump tabled dependency to 0.11 (#8922)

close? #8060

Quite a bit of refactoring took place.
I believe a few improvements to collapse/expand were made.

I've tried to track any performance regressions and seems like it is
fine.

I've noticed something different now with default configuration path or
something in this regard?
So I might missed something while testing because of this.

Requires some oversight.

---------

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>
This commit is contained in:
Maxim Zhiburt 2023-04-26 21:56:10 +03:00 committed by GitHub
parent 07c9f681c7
commit 8d8b011702
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 3041 additions and 3230 deletions

41
Cargo.lock generated
View file

@ -87,15 +87,6 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "ansi-str"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84252a7e1a0df81706ce70bbad85ed1e4916448a4093ccd52dd98c6a44a477cd"
dependencies = [
"ansitok",
]
[[package]] [[package]]
name = "ansi-str" name = "ansi-str"
version = "0.7.2" version = "0.7.2"
@ -2087,16 +2078,6 @@ dependencies = [
"indexmap", "indexmap",
] ]
[[package]]
name = "json_to_table"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0be33515faeb3773f550c80fd7a889148164e58f7e3cf36467718c8ce71ee55"
dependencies = [
"serde_json",
"tabled",
]
[[package]] [[package]]
name = "kernel32-sys" name = "kernel32-sys"
version = "0.2.2" version = "0.2.2"
@ -2823,7 +2804,6 @@ dependencies = [
"nu-test-support", "nu-test-support",
"nu-utils", "nu-utils",
"serde", "serde",
"tabled",
] ]
[[package]] [[package]]
@ -2947,7 +2927,7 @@ dependencies = [
name = "nu-explore" name = "nu-explore"
version = "0.79.1" version = "0.79.1"
dependencies = [ dependencies = [
"ansi-str 0.7.2", "ansi-str",
"crossterm 0.26.1", "crossterm 0.26.1",
"lscolors", "lscolors",
"nu-ansi-term", "nu-ansi-term",
@ -3082,13 +3062,11 @@ dependencies = [
name = "nu-table" name = "nu-table"
version = "0.79.1" version = "0.79.1"
dependencies = [ dependencies = [
"json_to_table",
"nu-ansi-term", "nu-ansi-term",
"nu-color-config", "nu-color-config",
"nu-engine", "nu-engine",
"nu-protocol", "nu-protocol",
"nu-utils", "nu-utils",
"serde_json",
"tabled", "tabled",
] ]
@ -3472,11 +3450,11 @@ checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]] [[package]]
name = "papergrid" name = "papergrid"
version = "0.7.1" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1526bb6aa9f10ec339fb10360f22c57edf81d5678d0278e93bc12a47ffbe4b01" checksum = "1fdfe703c51ddc52887ad78fc69cd2ea78d895ffcd6e955c9d03566db8ab5bb1"
dependencies = [ dependencies = [
"ansi-str 0.5.0", "ansi-str",
"ansitok", "ansitok",
"bytecount", "bytecount",
"fnv", "fnv",
@ -5128,11 +5106,12 @@ dependencies = [
[[package]] [[package]]
name = "tabled" name = "tabled"
version = "0.10.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c3ee73732ffceaea7b8f6b719ce3bb17f253fa27461ffeaf568ebd0cdb4b85" checksum = "da1a2e56bbf7bfdd08aaa7592157a742205459eff774b73bc01809ae2d99dc2a"
dependencies = [ dependencies = [
"ansi-str 0.5.0", "ansi-str",
"ansitok",
"papergrid", "papergrid",
"tabled_derive", "tabled_derive",
"unicode-width", "unicode-width",
@ -5140,9 +5119,9 @@ dependencies = [
[[package]] [[package]]
name = "tabled_derive" name = "tabled_derive"
version = "0.5.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beca1b4eaceb4f2755df858b88d9b9315b7ccfd1ffd0d7a48a52602301f01a57" checksum = "99f688a08b54f4f02f0a3c382aefdb7884d3d69609f785bd253dc033243e3fe4"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-error", "proc-macro-error",

View file

@ -12,8 +12,6 @@ bench = false
[dependencies] [dependencies]
serde = { version="1.0.123", features=["derive"] } serde = { version="1.0.123", features=["derive"] }
# used only for text_style Alignments
tabled = { version = "0.10.0", features = ["color"], default-features = false }
nu-protocol = { path = "../nu-protocol", version = "0.79.1" } nu-protocol = { path = "../nu-protocol", version = "0.79.1" }
nu-ansi-term = "0.47.0" nu-ansi-term = "0.47.0"

View file

@ -1,3 +1,4 @@
use crate::text_style::Alignment;
use crate::{color_record_to_nustyle, lookup_ansi_color_style, TextStyle}; use crate::{color_record_to_nustyle, lookup_ansi_color_style, TextStyle};
use nu_ansi_term::{Color, Style}; use nu_ansi_term::{Color, Style};
use nu_engine::eval_block; use nu_engine::eval_block;
@ -5,7 +6,6 @@ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
CliError, IntoPipelineData, Value, CliError, IntoPipelineData, Value,
}; };
use tabled::alignment::AlignmentHorizontal;
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -111,34 +111,28 @@ impl<'a> StyleComputer<'a> {
// Used only by the `table` command. // Used only by the `table` command.
pub fn style_primitive(&self, value: &Value) -> TextStyle { pub fn style_primitive(&self, value: &Value) -> TextStyle {
use Alignment::*;
let s = self.compute(&value.get_type().get_non_specified_string(), value); let s = self.compute(&value.get_type().get_non_specified_string(), value);
match *value { match *value {
Value::Bool { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s), Value::Bool { .. } => TextStyle::with_style(Left, s),
Value::Int { .. } => TextStyle::with_style(Right, s),
Value::Int { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s), Value::Filesize { .. } => TextStyle::with_style(Right, s),
Value::Duration { .. } => TextStyle::with_style(Right, s),
Value::Filesize { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s), Value::Date { .. } => TextStyle::with_style(Left, s),
Value::Range { .. } => TextStyle::with_style(Left, s),
Value::Duration { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s), Value::Float { .. } => TextStyle::with_style(Right, s),
Value::String { .. } => TextStyle::with_style(Left, s),
Value::Date { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s), Value::Nothing { .. } => TextStyle::with_style(Left, s),
Value::Binary { .. } => TextStyle::with_style(Left, s),
Value::Range { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s), Value::CellPath { .. } => TextStyle::with_style(Left, s),
Value::Float { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s),
Value::String { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
Value::Nothing { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
Value::Binary { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
Value::CellPath { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
Value::Record { .. } | Value::List { .. } | Value::Block { .. } => { Value::Record { .. } | Value::List { .. } | Value::Block { .. } => {
TextStyle::with_style(AlignmentHorizontal::Left, s) TextStyle::with_style(Left, s)
} }
_ => TextStyle::basic_left(), Value::Closure { .. }
| Value::CustomValue { .. }
| Value::Error { .. }
| Value::LazyRecord { .. }
| Value::MatchPattern { .. } => TextStyle::basic_left(),
} }
} }

View file

@ -1,7 +1,11 @@
use nu_ansi_term::{Color, Style}; use nu_ansi_term::{Color, Style};
use std::fmt::Display;
pub type Alignment = tabled::alignment::AlignmentHorizontal; #[derive(Debug, Clone, Copy)]
pub enum Alignment {
Center,
Left,
Right,
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct TextStyle { pub struct TextStyle {
@ -240,23 +244,3 @@ impl Default for TextStyle {
Self::new() Self::new()
} }
} }
impl tabled::papergrid::Color for TextStyle {
fn fmt_prefix(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(color) = &self.color_style {
color.prefix().fmt(f)?;
}
Ok(())
}
fn fmt_suffix(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(color) = &self.color_style {
if !color.is_plain() {
f.write_str("\u{1b}[0m")?;
}
}
Ok(())
}
}

View file

@ -28,7 +28,6 @@ nu-table = { path = "../nu-table", version = "0.79.1" }
nu-term-grid = { path = "../nu-term-grid", version = "0.79.1" } nu-term-grid = { path = "../nu-term-grid", version = "0.79.1" }
nu-utils = { path = "../nu-utils", version = "0.79.1" } nu-utils = { path = "../nu-utils", version = "0.79.1" }
num-format = { version = "0.4.3" } num-format = { version = "0.4.3" }
nu-ansi-term = "0.47.0" nu-ansi-term = "0.47.0"
# Potential dependencies for extras # Potential dependencies for extras
@ -89,7 +88,7 @@ percent-encoding = "2.2.0"
rusqlite = { version = "0.28.0", features = ["bundled"], optional = true } rusqlite = { version = "0.28.0", features = ["bundled"], optional = true }
sqlparser = { version = "0.32.0", features = ["serde"], optional = true } sqlparser = { version = "0.32.0", features = ["serde"], optional = true }
sysinfo = "0.28.2" sysinfo = "0.28.2"
tabled = "0.10.0" tabled = "0.12.0"
terminal_size = "0.2.1" terminal_size = "0.2.1"
thiserror = "1.0.31" thiserror = "1.0.31"
titlecase = "2.0.0" titlecase = "2.0.0"

View file

@ -1,123 +1,193 @@
use nu_protocol::Value; use nu_protocol::Value;
use nu_table::{string_width, string_wrap};
use tabled::{ use tabled::{
builder::Builder, grid::config::ColoredConfig,
peaker::PriorityMax, settings::{peaker::PriorityMax, width::Wrap, Settings, Style},
width::{MinWidth, Wrap}, Table,
Style,
}; };
use self::{ use crate::debug::inspect_table::{
global_horizontal_char::SetHorizontalCharOnFirstRow, peak2::Peak2, global_horizontal_char::SetHorizontalChar, set_widths::SetWidths,
table_column_width::get_first_cell_width, truncate_table::TruncateTable,
width_increase::IncWidth,
}; };
pub fn build_table(value: Value, description: String, termsize: usize) -> String { pub fn build_table(value: Value, description: String, termsize: usize) -> String {
let (head, mut data) = util::collect_input(value); let (head, mut data) = util::collect_input(value);
let count_columns = head.len();
data.insert(0, head); data.insert(0, head);
let mut val_table = Builder::from(data).build(); let mut desc = description;
let val_table_width = val_table.total_width(); let mut desc_width = string_width(&desc);
let mut desc_table_width = get_total_width_2_column_table(11, desc_width);
let desc = vec![vec![String::from("description"), description]]; let cfg = Table::default().with(Style::modern()).get_config().clone();
let mut widths = get_data_widths(&data, count_columns);
truncate_data(&mut data, &mut widths, &cfg, termsize);
let mut desc_table = Builder::from(desc).build(); let val_table_width = get_total_width2(&widths, &cfg);
let desc_table_width = desc_table.total_width(); if val_table_width < desc_table_width {
increase_widths(&mut widths, desc_table_width - val_table_width);
increase_data_width(&mut data, &widths);
}
if val_table_width > desc_table_width {
increase_string_width(&mut desc, val_table_width);
}
if desc_table_width > termsize {
let delete_width = desc_table_width - termsize;
if delete_width >= desc_width {
// we can't fit in a description; we consider it's no point in showing then?
return String::new();
}
desc_width -= delete_width;
desc = string_wrap(&desc, desc_width, false);
desc_table_width = termsize;
}
add_padding_to_widths(&mut widths);
#[allow(clippy::manual_clamp)] #[allow(clippy::manual_clamp)]
let width = val_table_width.max(desc_table_width).min(termsize); let width = val_table_width.max(desc_table_width).min(termsize);
desc_table let mut desc_table = Table::from_iter([[String::from("description"), desc]]);
.with(Style::rounded().off_bottom()) desc_table.with(Style::rounded().remove_bottom().remove_horizontals());
.with(Wrap::new(width).priority::<PriorityMax>())
.with(MinWidth::new(width).priority::<Peak2>());
val_table let mut val_table = Table::from_iter(data);
.with(Style::rounded().top_left_corner('├').top_right_corner('┤')) val_table.with(
.with(TruncateTable(width)) Settings::default()
.with(Wrap::new(width).priority::<PriorityMax>()) .with(Style::rounded().corner_top_left('├').corner_top_right('┤'))
.with(IncWidth(width)); .with(SetWidths(widths))
.with(Wrap::new(width).priority::<PriorityMax>())
// we use only 1, cause left border considered 0 position .with(SetHorizontalChar::new('┼', '┴', 11 + 2 + 1)),
let count_split_lines = 1; );
let desc_width = get_first_cell_width(&mut desc_table) + count_split_lines;
val_table.with(SetHorizontalCharOnFirstRow::new('┼', '┴', desc_width));
format!("{desc_table}\n{val_table}") format!("{desc_table}\n{val_table}")
} }
mod truncate_table { fn get_data_widths(data: &[Vec<String>], count_columns: usize) -> Vec<usize> {
use tabled::{ let mut widths = vec![0; count_columns];
papergrid::{ for row in data {
records::{Records, RecordsMut, Resizable}, for col in 0..count_columns {
width::{CfgWidthFunction, WidthEstimator}, let text = &row[col];
Estimate, let width = string_width(text);
}, widths[col] = std::cmp::max(widths[col], width);
TableOption,
};
pub struct TruncateTable(pub usize);
impl<R> TableOption<R> for TruncateTable
where
R: Records + RecordsMut<String> + Resizable,
{
fn change(&mut self, table: &mut tabled::Table<R>) {
let width = table.total_width();
if width <= self.0 {
return;
}
let count_columns = table.get_records().count_columns();
if count_columns < 1 {
return;
}
let mut evaluator = WidthEstimator::default();
evaluator.estimate(table.get_records(), table.get_config());
let columns_width: Vec<_> = evaluator.into();
const SPLIT_LINE_WIDTH: usize = 1;
let mut width = 0;
let mut i = 0;
for w in columns_width {
width += w + SPLIT_LINE_WIDTH;
if width >= self.0 {
break;
}
i += 1;
}
if i == 0 && count_columns > 0 {
i = 1;
} else if i + 1 == count_columns {
// we want to left at least 1 column
i -= 1;
}
let count_columns = table.get_records().count_columns();
let y = count_columns - i;
let mut column = count_columns;
for _ in 0..y {
column -= 1;
table.get_records_mut().remove_column(column);
}
table.get_records_mut().push_column();
let width_ctrl = CfgWidthFunction::from_cfg(table.get_config());
let last_column = table.get_records().count_columns() - 1;
for row in 0..table.get_records().count_rows() {
table
.get_records_mut()
.set((row, last_column), String::from(""), &width_ctrl)
}
} }
} }
widths
}
fn add_padding_to_widths(widths: &mut [usize]) {
for width in widths {
*width += 2;
}
}
fn increase_widths(widths: &mut [usize], need: usize) {
let all = need / widths.len();
let mut rest = need - all * widths.len();
for width in widths {
*width += all;
if rest > 0 {
*width += 1;
rest -= 1;
}
}
}
fn increase_data_width(data: &mut Vec<Vec<String>>, widths: &[usize]) {
for row in data {
for (col, max_width) in widths.iter().enumerate() {
let text = &mut row[col];
increase_string_width(text, *max_width);
}
}
}
fn increase_string_width(text: &mut String, total: usize) {
let width = string_width(text);
let rest = total - width;
if rest > 0 {
text.extend(std::iter::repeat(' ').take(rest));
}
}
fn get_total_width_2_column_table(col1: usize, col2: usize) -> usize {
const PAD: usize = 1;
const SPLIT_LINE: usize = 1;
SPLIT_LINE + PAD + col1 + PAD + SPLIT_LINE + PAD + col2 + PAD + SPLIT_LINE
}
fn truncate_data(
data: &mut Vec<Vec<String>>,
widths: &mut Vec<usize>,
cfg: &ColoredConfig,
expected_width: usize,
) {
const SPLIT_LINE_WIDTH: usize = 1;
const PAD: usize = 2;
let total_width = get_total_width2(widths, cfg);
if total_width <= expected_width {
return;
}
let mut width = 0;
let mut peak_count = 0;
for column_width in widths.iter() {
let next_width = width + *column_width + SPLIT_LINE_WIDTH + PAD;
if next_width >= expected_width {
break;
}
width = next_width;
peak_count += 1;
}
debug_assert!(peak_count < widths.len());
let left_space = expected_width - width;
let has_space_for_truncation_column = left_space > PAD;
if !has_space_for_truncation_column {
peak_count -= 1;
}
remove_columns(data, peak_count);
widths.drain(peak_count..);
push_empty_column(data);
widths.push(1);
}
fn remove_columns(data: &mut Vec<Vec<String>>, peak_count: usize) {
if peak_count == 0 {
for row in data {
row.clear();
}
} else {
for row in data {
row.drain(peak_count..);
}
}
}
fn get_total_width2(widths: &[usize], cfg: &ColoredConfig) -> usize {
let pad = 2;
let total = widths.iter().sum::<usize>() + pad * widths.len();
let countv = cfg.count_vertical(widths.len());
let margin = cfg.get_margin();
total + countv + margin.left.size + margin.right.size
}
fn push_empty_column(data: &mut Vec<Vec<String>>) {
let empty_cell = String::from("");
for row in data {
row.push(empty_cell.clone());
}
} }
mod util { mod util {
@ -223,135 +293,74 @@ mod util {
} }
} }
mod style_no_left_right_1st {
use tabled::{papergrid::records::Records, Table, TableOption};
struct StyleOffLeftRightFirstLine;
impl<R> TableOption<R> for StyleOffLeftRightFirstLine
where
R: Records,
{
fn change(&mut self, table: &mut Table<R>) {
let shape = table.shape();
let cfg = table.get_config_mut();
let mut b = cfg.get_border((0, 0), shape);
b.left = Some(' ');
cfg.set_border((0, 0), b);
let mut b = cfg.get_border((0, shape.1 - 1), shape);
b.right = Some(' ');
cfg.set_border((0, 0), b);
}
}
}
mod peak2 {
use tabled::peaker::Peaker;
pub struct Peak2;
impl Peaker for Peak2 {
fn create() -> Self {
Self
}
fn peak(&mut self, _: &[usize], _: &[usize]) -> Option<usize> {
Some(1)
}
}
}
mod table_column_width {
use tabled::{
papergrid::{records::Records, width::CfgWidthFunction},
Table,
};
pub fn get_first_cell_width<R: Records>(table: &mut Table<R>) -> usize {
let mut opt = GetFirstCellWidth(0);
table.with(&mut opt);
opt.0
}
struct GetFirstCellWidth(pub usize);
impl<R: Records> tabled::TableOption<R> for GetFirstCellWidth {
fn change(&mut self, table: &mut tabled::Table<R>) {
let w = table
.get_records()
.get_width((0, 0), CfgWidthFunction::default());
let pad = table
.get_config()
.get_padding(tabled::papergrid::Entity::Cell(0, 0));
let pad = pad.left.size + pad.right.size;
self.0 = w + pad;
}
}
}
mod global_horizontal_char { mod global_horizontal_char {
use tabled::{ use tabled::{
papergrid::{records::Records, width::WidthEstimator, Estimate, Offset::Begin}, grid::{
Table, TableOption, config::{ColoredConfig, Offset},
dimension::{CompleteDimensionVecRecords, Dimension},
records::{ExactRecords, Records},
},
settings::TableOption,
}; };
pub struct SetHorizontalCharOnFirstRow { pub struct SetHorizontalChar {
c1: char, intersection: char,
c2: char, split: char,
pos: usize, index: usize,
} }
impl SetHorizontalCharOnFirstRow { impl SetHorizontalChar {
pub fn new(c1: char, c2: char, pos: usize) -> Self { pub fn new(intersection: char, split: char, index: usize) -> Self {
Self { c1, c2, pos } Self {
intersection,
split,
index,
}
} }
} }
impl<R> TableOption<R> for SetHorizontalCharOnFirstRow impl<R: Records + ExactRecords> TableOption<R, CompleteDimensionVecRecords<'_>, ColoredConfig>
where for SetHorizontalChar
R: Records,
{ {
fn change(&mut self, table: &mut Table<R>) { fn change(
if table.is_empty() { self,
records: &mut R,
cfg: &mut ColoredConfig,
dimension: &mut CompleteDimensionVecRecords<'_>,
) {
let count_columns = records.count_columns();
let count_rows = records.count_rows();
if count_columns == 0 || count_rows == 0 {
return; return;
} }
let shape = table.shape(); let widths = get_widths(dimension, records.count_columns());
let mut evaluator = WidthEstimator::default(); let has_vertical = cfg.has_vertical(0, count_columns);
evaluator.estimate(table.get_records(), table.get_config()); if has_vertical && self.index == 0 {
let widths: Vec<_> = evaluator.into(); let mut border = cfg.get_border((0, 0), (count_rows, count_columns));
border.left_top_corner = Some(self.intersection);
let has_vertical = table.get_config().has_vertical(0, shape.1); cfg.set_border((0, 0), border);
if has_vertical && self.pos == 0 {
let mut border = table.get_config().get_border((0, 0), shape);
border.left_top_corner = Some(self.c1);
table.get_config_mut().set_border((0, 0), border);
return; return;
} }
let mut i = 1; let mut i = 1;
#[allow(clippy::needless_range_loop)]
for (col, width) in widths.into_iter().enumerate() { for (col, width) in widths.into_iter().enumerate() {
if self.pos < i + width { if self.index < i + width {
let o = self.pos - i; let o = self.index - i;
table cfg.set_horizontal_char((0, col), self.split, Offset::Begin(o));
.get_config_mut()
.override_horizontal_border((0, col), self.c2, Begin(o));
return; return;
} }
i += width; i += width;
let has_vertical = table.get_config().has_vertical(col, shape.1); let has_vertical = cfg.has_vertical(col, count_columns);
if has_vertical { if has_vertical {
if self.pos == i { if self.index == i {
let mut border = table.get_config().get_border((0, col), shape); let mut border = cfg.get_border((0, col), (count_rows, count_columns));
border.right_top_corner = Some(self.c1); border.right_top_corner = Some(self.intersection);
table.get_config_mut().set_border((0, col), border); cfg.set_border((0, col), border);
return; return;
} }
@ -360,96 +369,33 @@ mod global_horizontal_char {
} }
} }
} }
}
mod width_increase { fn get_widths(dims: &CompleteDimensionVecRecords<'_>, count_columns: usize) -> Vec<usize> {
use tabled::{ let mut widths = vec![0; count_columns];
object::Cell, for (col, width) in widths.iter_mut().enumerate() {
papergrid::{ *width = dims.get_width(col);
records::{Records, RecordsMut},
width::WidthEstimator,
Entity, Estimate, GridConfig,
},
peaker::PriorityNone,
Modify, Width,
};
use tabled::{peaker::Peaker, Table, TableOption};
#[derive(Debug)]
pub struct IncWidth(pub usize);
impl<R> TableOption<R> for IncWidth
where
R: Records + RecordsMut<String>,
{
fn change(&mut self, table: &mut Table<R>) {
if table.is_empty() {
return;
}
let (widths, total_width) =
get_table_widths_with_total(table.get_records(), table.get_config());
if total_width >= self.0 {
return;
}
let increase_list =
get_increase_list(widths, self.0, total_width, PriorityNone::default());
for (col, width) in increase_list.into_iter().enumerate() {
for row in 0..table.get_records().count_rows() {
let pad = table.get_config().get_padding(Entity::Cell(row, col));
let width = width - pad.left.size - pad.right.size;
table.with(Modify::new(Cell(row, col)).with(Width::increase(width)));
}
}
}
}
fn get_increase_list<F>(
mut widths: Vec<usize>,
total_width: usize,
mut width: usize,
mut peaker: F,
) -> Vec<usize>
where
F: Peaker,
{
while width != total_width {
let col = match peaker.peak(&[], &widths) {
Some(col) => col,
None => break,
};
widths[col] += 1;
width += 1;
} }
widths widths
} }
}
fn get_table_widths_with_total<R>(records: R, cfg: &GridConfig) -> (Vec<usize>, usize) mod set_widths {
where use tabled::{
R: Records, grid::{config::ColoredConfig, dimension::CompleteDimensionVecRecords},
{ settings::TableOption,
let mut evaluator = WidthEstimator::default(); };
evaluator.estimate(&records, cfg);
let total_width = get_table_total_width(&records, cfg, &evaluator);
let widths = evaluator.into();
(widths, total_width) pub struct SetWidths(pub Vec<usize>);
}
pub(crate) fn get_table_total_width<W, R>(records: R, cfg: &GridConfig, ctrl: &W) -> usize impl<R> TableOption<R, CompleteDimensionVecRecords<'_>, ColoredConfig> for SetWidths {
where fn change(
W: Estimate<R>, self,
R: Records, _: &mut R,
{ _: &mut ColoredConfig,
ctrl.total() dims: &mut CompleteDimensionVecRecords<'_>,
+ cfg.count_vertical(records.count_columns()) ) {
+ cfg.get_margin().left.size dims.set_widths(self.0);
+ cfg.get_margin().right.size }
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -154,11 +154,11 @@ fn table_collapse_none() {
assert_eq!( assert_eq!(
actual.out, actual.out,
concat!( concat!(
" a b c ", " a b c ",
" 1 2 3 ", " 1 2 3 ",
" 4 5 1 ", " 4 5 1 ",
" 2 ", " 2 ",
" 3 ", " 3 ",
) )
); );
} }
@ -232,11 +232,20 @@ fn table_collapse_hearts() {
} }
#[test] #[test]
fn table_collapse_doesnot_support_width_control() { fn table_collapse_does_wrapping_for_long_strings() {
let actual = nu!( let actual = nu!(
r#"[[a]; [11111111111111111111111111111111111111111111111111111111111111111111111111111111]] | table --collapse"# r#"[[a]; [11111111111111111111111111111111111111111111111111111111111111111111111111111111]] | table --collapse"#
); );
assert_eq!(actual.out, "Couldn't fit table into 80 columns!"); assert_eq!(
actual.out,
"╭────────────────────────────────╮\
a \
\
111111111111111109312339230430 \
179149313814687359833671239329 \
01313323321729744896.0000 \
"
);
} }
#[test] #[test]
@ -1795,6 +1804,526 @@ fn table_expande_with_no_header_internally_1() {
); );
} }
#[test]
fn test_collapse_big_0() {
Playground::setup("test_expand_big_0", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContent(
"sample.toml",
r#"
[package]
authors = ["The Nushell Project Developers"]
default-run = "nu"
description = "A new type of shell"
documentation = "https://www.nushell.sh/book/"
edition = "2021"
exclude = ["images"]
homepage = "https://www.nushell.sh"
license = "MIT"
name = "nu"
repository = "https://github.com/nushell/nushell"
rust-version = "1.60"
version = "0.74.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[package.metadata.binstall]
pkg-url = "{ repo }/releases/download/{ version }/{ name }-{ version }-{ target }.{ archive-format }"
pkg-fmt = "tgz"
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
pkg-fmt = "zip"
[workspace]
members = [
"crates/nu-cli",
"crates/nu-engine",
"crates/nu-parser",
"crates/nu-system",
"crates/nu-command",
"crates/nu-protocol",
"crates/nu-plugin",
"crates/nu_plugin_inc",
"crates/nu_plugin_gstat",
"crates/nu_plugin_example",
"crates/nu_plugin_query",
"crates/nu_plugin_custom_values",
"crates/nu-utils",
]
[dependencies]
chrono = { version = "0.4.23", features = ["serde"] }
crossterm = "0.24.0"
ctrlc = "3.2.1"
log = "0.4"
miette = { version = "5.5.0", features = ["fancy-no-backtrace"] }
nu-ansi-term = "0.46.0"
nu-cli = { path = "./crates/nu-cli", version = "0.74.1" }
nu-engine = { path = "./crates/nu-engine", version = "0.74.1" }
reedline = { version = "0.14.0", features = ["bashisms", "sqlite"] }
rayon = "1.6.1"
is_executable = "1.0.1"
simplelog = "0.12.0"
time = "0.3.12"
[target.'cfg(not(target_os = "windows"))'.dependencies]
# Our dependencies don't use OpenSSL on Windows
openssl = { version = "0.10.38", features = ["vendored"], optional = true }
signal-hook = { version = "0.3.14", default-features = false }
[target.'cfg(windows)'.build-dependencies]
winres = "0.1"
[target.'cfg(target_family = "unix")'.dependencies]
nix = { version = "0.25", default-features = false, features = ["signal", "process", "fs", "term"] }
atty = "0.2"
[dev-dependencies]
nu-test-support = { path = "./crates/nu-test-support", version = "0.74.1" }
tempfile = "3.2.0"
assert_cmd = "2.0.2"
criterion = "0.4"
pretty_assertions = "1.0.0"
serial_test = "0.10.0"
hamcrest2 = "0.3.0"
rstest = { version = "0.15.0", default-features = false }
itertools = "0.10.3"
[features]
plugin = [
"nu-plugin",
"nu-cli/plugin",
"nu-parser/plugin",
"nu-command/plugin",
"nu-protocol/plugin",
"nu-engine/plugin",
]
# extra used to be more useful but now it's the same as default. Leaving it in for backcompat with existing build scripts
extra = ["default"]
default = ["plugin", "which-support", "trash-support", "sqlite"]
stable = ["default"]
wasi = []
# Enable to statically link OpenSSL; otherwise the system version will be used. Not enabled by default because it takes a while to build
static-link-openssl = ["dep:openssl"]
# Stable (Default)
which-support = ["nu-command/which-support"]
trash-support = ["nu-command/trash-support"]
# Main nu binary
[[bin]]
name = "nu"
path = "src/main.rs"
# To use a development version of a dependency please use a global override here
# changing versions in each sub-crate of the workspace is tedious
[patch.crates-io]
reedline = { git = "https://github.com/nushell/reedline.git", branch = "main" }
# Criterion benchmarking setup
# Run all benchmarks with `cargo bench`
# Run individual benchmarks like `cargo bench -- <regex>` e.g. `cargo bench -- parse`
[[bench]]
name = "benchmarks"
harness = false
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
"open sample.toml | table --collapse"
));
_print_lines(&actual.out, 80);
let expected = join_lines([
"╭──────────────────┬─────────┬─────────────────────────────────────────────────╮",
"│ bench │ harness │ name │",
"│ ├─────────┼─────────────────────────────────────────────────┤",
"│ │ false │ benchmarks │",
"├──────────────────┼──────┬──┴─────────────────────────────────────────────────┤",
"│ bin │ name │ path │",
"│ ├──────┼────────────────────────────────────────────────────┤",
"│ │ nu │ src/main.rs │",
"├──────────────────┼──────┴────────┬──────────┬────────────────────────────────┤",
"│ dependencies │ chrono │ features │ serde │",
"│ │ ├──────────┼────────────────────────────────┤",
"│ │ │ version │ 0.4.23 │",
"│ ├───────────────┼──────────┴────────────────────────────────┤",
"│ │ crossterm │ 0.24.0 │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ ctrlc │ 3.2.1 │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ is_executable │ 1.0.1 │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ log │ 0.4 │",
"│ ├───────────────┼──────────┬────────────────────────────────┤",
"│ │ miette │ features │ fancy-no-backtrace │",
"│ │ ├──────────┼────────────────────────────────┤",
"│ │ │ version │ 5.5.0 │",
"│ ├───────────────┼──────────┴────────────────────────────────┤",
"│ │ nu-ansi-term │ 0.46.0 │",
"│ ├───────────────┼─────────┬─────────────────────────────────┤",
"│ │ nu-cli │ path │ ./crates/nu-cli │",
"│ │ ├─────────┼─────────────────────────────────┤",
"│ │ │ version │ 0.74.1 │",
"│ ├───────────────┼─────────┼─────────────────────────────────┤",
"│ │ nu-engine │ path │ ./crates/nu-engine │",
"│ │ ├─────────┼─────────────────────────────────┤",
"│ │ │ version │ 0.74.1 │",
"│ ├───────────────┼─────────┴─────────────────────────────────┤",
"│ │ rayon │ 1.6.1 │",
"│ ├───────────────┼──────────┬────────────────────────────────┤",
"│ │ reedline │ features │ bashisms │",
"│ │ │ ├────────────────────────────────┤",
"│ │ │ │ sqlite │",
"│ │ ├──────────┼────────────────────────────────┤",
"│ │ │ version │ 0.14.0 │",
"│ ├───────────────┼──────────┴────────────────────────────────┤",
"│ │ simplelog │ 0.12.0 │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ time │ 0.3.12 │",
"├──────────────────┼───────────────┴───┬───────────────────────────────────────┤",
"│ dev-dependencies │ assert_cmd │ 2.0.2 │",
"│ ├───────────────────┼───────────────────────────────────────┤",
"│ │ criterion │ 0.4 │",
"│ ├───────────────────┼───────────────────────────────────────┤",
"│ │ hamcrest2 │ 0.3.0 │",
"│ ├───────────────────┼───────────────────────────────────────┤",
"│ │ itertools │ 0.10.3 │",
"│ ├───────────────────┼─────────┬─────────────────────────────┤",
"│ │ nu-test-support │ path │ ./crates/nu-test-support │",
"│ │ ├─────────┼─────────────────────────────┤",
"│ │ │ version │ 0.74.1 │",
"│ ├───────────────────┼─────────┴─────────────────────────────┤",
"│ │ pretty_assertions │ 1.0.0 │",
"│ ├───────────────────┼──────────────────┬────────────────────┤",
"│ │ rstest │ default-features │ false │",
"│ │ ├──────────────────┼────────────────────┤",
"│ │ │ version │ 0.15.0 │",
"│ ├───────────────────┼──────────────────┴────────────────────┤",
"│ │ serial_test │ 0.10.0 │",
"│ ├───────────────────┼───────────────────────────────────────┤",
"│ │ tempfile │ 3.2.0 │",
"├──────────────────┼───────────────────┴─┬─────────────────────────────────────┤",
"│ features │ default │ plugin │",
"│ │ ├─────────────────────────────────────┤",
"│ │ │ which-support │",
"│ │ ├─────────────────────────────────────┤",
"│ │ │ trash-support │",
"│ │ ├─────────────────────────────────────┤",
"│ │ │ sqlite │",
"│ ├─────────────────────┼─────────────────────────────────────┤",
"│ │ extra │ default │",
"│ ├─────────────────────┼─────────────────────────────────────┤",
"│ │ plugin │ nu-plugin │",
"│ │ ├─────────────────────────────────────┤",
"│ │ │ nu-cli/plugin │",
"│ │ ├─────────────────────────────────────┤",
"│ │ │ nu-parser/plugin │",
"│ │ ├─────────────────────────────────────┤",
"│ │ │ nu-command/plugin │",
"│ │ ├─────────────────────────────────────┤",
"│ │ │ nu-protocol/plugin │",
"│ │ ├─────────────────────────────────────┤",
"│ │ │ nu-engine/plugin │",
"│ ├─────────────────────┼─────────────────────────────────────┤",
"│ │ stable │ default │",
"│ ├─────────────────────┼─────────────────────────────────────┤",
"│ │ static-link-openssl │ dep:openssl │",
"│ ├─────────────────────┼─────────────────────────────────────┤",
"│ │ trash-support │ nu-command/trash-support │",
"│ ├─────────────────────┼─────────────────────────────────────┤",
"│ │ wasi │ │",
"│ ├─────────────────────┼─────────────────────────────────────┤",
"│ │ which-support │ nu-command/which-support │",
"├──────────────────┼───────────────┬─────┴─────────────────────────────────────┤",
"│ package │ authors │ The Nushell Project Developers │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ default-run │ nu │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ description │ A new type of shell │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ documentation │ https://www.nushell.sh/book/ │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ edition │ 2021 │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ exclude │ images │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ homepage │ https://www.nushell.sh │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ license │ MIT │",
"│ ├───────────────┼──────────┬───────────┬────────────────────┤",
"│ │ metadata │ binstall │ overrides │ ... │",
"│ │ │ ├───────────┼────────────────────┤",
"│ │ │ │ pkg-fmt │ tgz │",
"│ │ │ ├───────────┼────────────────────┤",
"│ │ │ │ pkg-url │ { repo }/releases/ │",
"│ │ │ │ │ download/{ v │",
"│ │ │ │ │ ersion │",
"│ │ │ │ │ }/{ name }-{ vers │",
"│ │ │ │ │ ion }- │",
"│ │ │ │ │ { target }.{ │",
"│ │ │ │ │ archive-format } │",
"│ ├───────────────┼──────────┴───────────┴────────────────────┤",
"│ │ name │ nu │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ repository │ https://github.com/nushell/nushell │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ rust-version │ 1.60 │",
"│ ├───────────────┼───────────────────────────────────────────┤",
"│ │ version │ 0.74.1 │",
"├──────────────────┼───────────┬───┴──────┬────────┬───────────────────────────┤",
"│ patch │ crates-io │ reedline │ branch │ main │",
"│ │ │ ├────────┼───────────────────────────┤",
"│ │ │ │ git │ https://github.com/nushel │",
"│ │ │ │ │ l/reedline.git │",
"├──────────────────┼───────────┴──────────┴────────┴─┬──────────────┬──────────┤",
"│ target │ cfg(not(target_os = \"windows\")) │ dependencies │ ... │",
"│ │ │ ├──────────┤",
"│ │ │ │ ... │",
"│ ├─────────────────────────────────┼──────────────┼──────────┤",
"│ │ cfg(target_family = \"unix\") │ dependencies │ ... │",
"│ │ │ ├──────────┤",
"│ │ │ │ ... │",
"│ ├─────────────────────────────────┼──────────────┴──────────┤",
"│ │ cfg(windows) │ ... │",
"├──────────────────┼─────────┬───────────────────────┴─────────────────────────┤",
"│ workspace │ members │ crates/nu-cli │",
"│ │ ├─────────────────────────────────────────────────┤",
"│ │ │ crates/nu-engine │",
"│ │ ├─────────────────────────────────────────────────┤",
"│ │ │ crates/nu-parser │",
"│ │ ├─────────────────────────────────────────────────┤",
"│ │ │ crates/nu-system │",
"│ │ ├─────────────────────────────────────────────────┤",
"│ │ │ crates/nu-command │",
"│ │ ├─────────────────────────────────────────────────┤",
"│ │ │ crates/nu-protocol │",
"│ │ ├─────────────────────────────────────────────────┤",
"│ │ │ crates/nu-plugin │",
"│ │ ├─────────────────────────────────────────────────┤",
"│ │ │ crates/nu_plugin_inc │",
"│ │ ├─────────────────────────────────────────────────┤",
"│ │ │ crates/nu_plugin_gstat │",
"│ │ ├─────────────────────────────────────────────────┤",
"│ │ │ crates/nu_plugin_example │",
"│ │ ├─────────────────────────────────────────────────┤",
"│ │ │ crates/nu_plugin_query │",
"│ │ ├─────────────────────────────────────────────────┤",
"│ │ │ crates/nu_plugin_custom_values │",
"│ │ ├─────────────────────────────────────────────────┤",
"│ │ │ crates/nu-utils │",
"╰──────────────────┴─────────┴─────────────────────────────────────────────────╯",
]);
assert_eq!(actual.out, expected);
let actual = nu!(
cwd: dirs.test(), pipeline(
"open sample.toml | table --collapse --width=160"
));
_print_lines(&actual.out, 111);
let expected = join_lines([
"╭──────────────────┬─────────┬────────────────────────────────────────────────────────────────────────────────╮",
"│ bench │ harness │ name │",
"│ ├─────────┼────────────────────────────────────────────────────────────────────────────────┤",
"│ │ false │ benchmarks │",
"├──────────────────┼──────┬──┴────────────────────────────────────────────────────────────────────────────────┤",
"│ bin │ name │ path │",
"│ ├──────┼───────────────────────────────────────────────────────────────────────────────────┤",
"│ │ nu │ src/main.rs │",
"├──────────────────┼──────┴────────┬──────────┬───────────────────────────────────────────────────────────────┤",
"│ dependencies │ chrono │ features │ serde │",
"│ │ ├──────────┼───────────────────────────────────────────────────────────────┤",
"│ │ │ version │ 0.4.23 │",
"│ ├───────────────┼──────────┴───────────────────────────────────────────────────────────────┤",
"│ │ crossterm │ 0.24.0 │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ ctrlc │ 3.2.1 │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ is_executable │ 1.0.1 │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ log │ 0.4 │",
"│ ├───────────────┼──────────┬───────────────────────────────────────────────────────────────┤",
"│ │ miette │ features │ fancy-no-backtrace │",
"│ │ ├──────────┼───────────────────────────────────────────────────────────────┤",
"│ │ │ version │ 5.5.0 │",
"│ ├───────────────┼──────────┴───────────────────────────────────────────────────────────────┤",
"│ │ nu-ansi-term │ 0.46.0 │",
"│ ├───────────────┼─────────┬────────────────────────────────────────────────────────────────┤",
"│ │ nu-cli │ path │ ./crates/nu-cli │",
"│ │ ├─────────┼────────────────────────────────────────────────────────────────┤",
"│ │ │ version │ 0.74.1 │",
"│ ├───────────────┼─────────┼────────────────────────────────────────────────────────────────┤",
"│ │ nu-engine │ path │ ./crates/nu-engine │",
"│ │ ├─────────┼────────────────────────────────────────────────────────────────┤",
"│ │ │ version │ 0.74.1 │",
"│ ├───────────────┼─────────┴────────────────────────────────────────────────────────────────┤",
"│ │ rayon │ 1.6.1 │",
"│ ├───────────────┼──────────┬───────────────────────────────────────────────────────────────┤",
"│ │ reedline │ features │ bashisms │",
"│ │ │ ├───────────────────────────────────────────────────────────────┤",
"│ │ │ │ sqlite │",
"│ │ ├──────────┼───────────────────────────────────────────────────────────────┤",
"│ │ │ version │ 0.14.0 │",
"│ ├───────────────┼──────────┴───────────────────────────────────────────────────────────────┤",
"│ │ simplelog │ 0.12.0 │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ time │ 0.3.12 │",
"├──────────────────┼───────────────┴───┬──────────────────────────────────────────────────────────────────────┤",
"│ dev-dependencies │ assert_cmd │ 2.0.2 │",
"│ ├───────────────────┼──────────────────────────────────────────────────────────────────────┤",
"│ │ criterion │ 0.4 │",
"│ ├───────────────────┼──────────────────────────────────────────────────────────────────────┤",
"│ │ hamcrest2 │ 0.3.0 │",
"│ ├───────────────────┼──────────────────────────────────────────────────────────────────────┤",
"│ │ itertools │ 0.10.3 │",
"│ ├───────────────────┼─────────┬────────────────────────────────────────────────────────────┤",
"│ │ nu-test-support │ path │ ./crates/nu-test-support │",
"│ │ ├─────────┼────────────────────────────────────────────────────────────┤",
"│ │ │ version │ 0.74.1 │",
"│ ├───────────────────┼─────────┴────────────────────────────────────────────────────────────┤",
"│ │ pretty_assertions │ 1.0.0 │",
"│ ├───────────────────┼──────────────────┬───────────────────────────────────────────────────┤",
"│ │ rstest │ default-features │ false │",
"│ │ ├──────────────────┼───────────────────────────────────────────────────┤",
"│ │ │ version │ 0.15.0 │",
"│ ├───────────────────┼──────────────────┴───────────────────────────────────────────────────┤",
"│ │ serial_test │ 0.10.0 │",
"│ ├───────────────────┼──────────────────────────────────────────────────────────────────────┤",
"│ │ tempfile │ 3.2.0 │",
"├──────────────────┼───────────────────┴─┬────────────────────────────────────────────────────────────────────┤",
"│ features │ default │ plugin │",
"│ │ ├────────────────────────────────────────────────────────────────────┤",
"│ │ │ which-support │",
"│ │ ├────────────────────────────────────────────────────────────────────┤",
"│ │ │ trash-support │",
"│ │ ├────────────────────────────────────────────────────────────────────┤",
"│ │ │ sqlite │",
"│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤",
"│ │ extra │ default │",
"│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤",
"│ │ plugin │ nu-plugin │",
"│ │ ├────────────────────────────────────────────────────────────────────┤",
"│ │ │ nu-cli/plugin │",
"│ │ ├────────────────────────────────────────────────────────────────────┤",
"│ │ │ nu-parser/plugin │",
"│ │ ├────────────────────────────────────────────────────────────────────┤",
"│ │ │ nu-command/plugin │",
"│ │ ├────────────────────────────────────────────────────────────────────┤",
"│ │ │ nu-protocol/plugin │",
"│ │ ├────────────────────────────────────────────────────────────────────┤",
"│ │ │ nu-engine/plugin │",
"│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤",
"│ │ stable │ default │",
"│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤",
"│ │ static-link-openssl │ dep:openssl │",
"│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤",
"│ │ trash-support │ nu-command/trash-support │",
"│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤",
"│ │ wasi │ │",
"│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤",
"│ │ which-support │ nu-command/which-support │",
"├──────────────────┼───────────────┬─────┴────────────────────────────────────────────────────────────────────┤",
"│ package │ authors │ The Nushell Project Developers │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ default-run │ nu │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ description │ A new type of shell │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ documentation │ https://www.nushell.sh/book/ │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ edition │ 2021 │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ exclude │ images │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ homepage │ https://www.nushell.sh │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ license │ MIT │",
"│ ├───────────────┼──────────┬───────────┬────────────────────────┬─────────┬────────────────┤",
"│ │ metadata │ binstall │ overrides │ x86_64-pc-windows-msvc │ pkg-fmt │ zip │",
"│ │ │ ├───────────┼────────────────────────┴─────────┴────────────────┤",
"│ │ │ │ pkg-fmt │ tgz │",
"│ │ │ ├───────────┼───────────────────────────────────────────────────┤",
"│ │ │ │ pkg-url │ { repo }/releases/download/{ v │",
"│ │ │ │ │ ersion }/{ name }-{ version }- │",
"│ │ │ │ │ { target }.{ archive-format } │",
"│ ├───────────────┼──────────┴───────────┴───────────────────────────────────────────────────┤",
"│ │ name │ nu │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ repository │ https://github.com/nushell/nushell │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ rust-version │ 1.60 │",
"│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤",
"│ │ version │ 0.74.1 │",
"├──────────────────┼───────────┬───┴──────┬────────┬──────────────────────────────────────────────────────────┤",
"│ patch │ crates-io │ reedline │ branch │ main │",
"│ │ │ ├────────┼──────────────────────────────────────────────────────────┤",
"│ │ │ │ git │ https://github.com/nushell/reedline.git │",
"├──────────────────┼───────────┴──────────┴────────┴─┬──────────────┬─────────────┬──────────┬────────────────┤",
"│ target │ cfg(not(target_os = \"windows\")) │ dependencies │ openssl │ features │ vendored │",
"│ │ │ │ ├──────────┼────────────────┤",
"│ │ │ │ │ optional │ true │",
"│ │ │ │ ├──────────┼────────────────┤",
"│ │ │ │ │ version │ 0.10.38 │",
"│ │ │ ├─────────────┼──────────┴───────┬────────┤",
"│ │ │ │ signal-hook │ default-features │ false │",
"│ │ │ │ ├──────────────────┼────────┤",
"│ │ │ │ │ version │ 0.3.14 │",
"│ ├─────────────────────────────────┼──────────────┼──────┬──────┴──────────────────┴────────┤",
"│ │ cfg(target_family = \"unix\") │ dependencies │ atty │ 0.2 │",
"│ │ │ ├──────┼──────────────────┬───────────────┤",
"│ │ │ │ nix │ default-features │ false │",
"│ │ │ │ ├──────────────────┼───────────────┤",
"│ │ │ │ │ features │ signal │",
"│ │ │ │ │ ├───────────────┤",
"│ │ │ │ │ │ process │",
"│ │ │ │ │ ├───────────────┤",
"│ │ │ │ │ │ fs │",
"│ │ │ │ │ ├───────────────┤",
"│ │ │ │ │ │ term │",
"│ │ │ │ ├──────────────────┼───────────────┤",
"│ │ │ │ │ version │ 0.25 │",
"│ ├─────────────────────────────────┼──────────────┴─────┬┴───────┬──────────┴───────────────┤",
"│ │ cfg(windows) │ build-dependencies │ winres │ 0.1 │",
"├──────────────────┼─────────┬───────────────────────┴────────────────────┴────────┴──────────────────────────┤",
"│ workspace │ members │ crates/nu-cli │",
"│ │ ├────────────────────────────────────────────────────────────────────────────────┤",
"│ │ │ crates/nu-engine │",
"│ │ ├────────────────────────────────────────────────────────────────────────────────┤",
"│ │ │ crates/nu-parser │",
"│ │ ├────────────────────────────────────────────────────────────────────────────────┤",
"│ │ │ crates/nu-system │",
"│ │ ├────────────────────────────────────────────────────────────────────────────────┤",
"│ │ │ crates/nu-command │",
"│ │ ├────────────────────────────────────────────────────────────────────────────────┤",
"│ │ │ crates/nu-protocol │",
"│ │ ├────────────────────────────────────────────────────────────────────────────────┤",
"│ │ │ crates/nu-plugin │",
"│ │ ├────────────────────────────────────────────────────────────────────────────────┤",
"│ │ │ crates/nu_plugin_inc │",
"│ │ ├────────────────────────────────────────────────────────────────────────────────┤",
"│ │ │ crates/nu_plugin_gstat │",
"│ │ ├────────────────────────────────────────────────────────────────────────────────┤",
"│ │ │ crates/nu_plugin_example │",
"│ │ ├────────────────────────────────────────────────────────────────────────────────┤",
"│ │ │ crates/nu_plugin_query │",
"│ │ ├────────────────────────────────────────────────────────────────────────────────┤",
"│ │ │ crates/nu_plugin_custom_values │",
"│ │ ├────────────────────────────────────────────────────────────────────────────────┤",
"│ │ │ crates/nu-utils │",
"╰──────────────────┴─────────┴────────────────────────────────────────────────────────────────────────────────╯",
]);
assert_eq!(actual.out, expected);
})
}
fn join_lines(lines: impl IntoIterator<Item = impl AsRef<str>>) -> String { fn join_lines(lines: impl IntoIterator<Item = impl AsRef<str>>) -> String {
lines lines
.into_iter() .into_iter()

View file

@ -1,9 +1,8 @@
use nu_protocol::Value; use nu_protocol::Value;
use std::collections::HashSet; use std::collections::HashSet;
pub fn get_columns<'a>(input: impl IntoIterator<Item = &'a Value>) -> Vec<String> { pub fn get_columns(input: &[Value]) -> Vec<String> {
let mut columns = vec![]; let mut columns = vec![];
for item in input { for item in input {
let Value::Record { cols, .. } = item else { let Value::Record { cols, .. } = item else {
return vec![]; return vec![];

View file

@ -1,17 +1,9 @@
use nu_color_config::{Alignment, StyleComputer, TextStyle}; use nu_color_config::StyleComputer;
use nu_engine::column::get_columns; use nu_protocol::{Span, Value};
use nu_protocol::{ast::PathMember, Config, ShellError, Span, TableIndexMode, Value}; use nu_table::{value_to_clean_styled_string, value_to_styled_string, BuildConfig, ExpandedTable};
use nu_protocol::{FooterMode, TrimStrategy}; use std::sync::atomic::AtomicBool;
use nu_table::{string_width, Table as NuTable, TableConfig, TableTheme};
use std::sync::Arc; use std::sync::Arc;
use std::{
cmp::max,
sync::atomic::{AtomicBool, Ordering},
};
const INDEX_COLUMN_NAME: &str = "index";
type NuText = (String, TextStyle);
use crate::nu_common::NuConfig; use crate::nu_common::NuConfig;
pub fn try_build_table( pub fn try_build_table(
@ -21,10 +13,13 @@ pub fn try_build_table(
value: Value, value: Value,
) -> String { ) -> String {
match value { match value {
Value::List { vals, span } => try_build_list(vals, &ctrlc, config, span, style_computer), Value::List { vals, span } => try_build_list(vals, ctrlc, config, span, style_computer),
Value::Record { cols, vals, span } => { Value::Record { cols, vals, span } => {
try_build_map(cols, vals, span, style_computer, ctrlc, config) try_build_map(cols, vals, span, style_computer, ctrlc, config)
} }
val if matches!(val, Value::String { .. }) => {
value_to_clean_styled_string(&val, config, style_computer).0
}
val => value_to_styled_string(&val, config, style_computer).0, val => value_to_styled_string(&val, config, style_computer).0,
} }
} }
@ -37,18 +32,8 @@ fn try_build_map(
ctrlc: Option<Arc<AtomicBool>>, ctrlc: Option<Arc<AtomicBool>>,
config: &NuConfig, config: &NuConfig,
) -> String { ) -> String {
let result = build_expanded_table( let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX);
cols.clone(), let result = ExpandedTable::new(None, false, String::new()).build_map(&cols, &vals, opts);
vals.clone(),
span,
ctrlc,
config,
style_computer,
usize::MAX,
None,
false,
"",
);
match result { match result {
Ok(Some(result)) => result, Ok(Some(result)) => result,
Ok(None) | Err(_) => { Ok(None) | Err(_) => {
@ -59,896 +44,18 @@ fn try_build_map(
fn try_build_list( fn try_build_list(
vals: Vec<Value>, vals: Vec<Value>,
ctrlc: &Option<Arc<AtomicBool>>, ctrlc: Option<Arc<AtomicBool>>,
config: &NuConfig, config: &NuConfig,
span: Span, span: Span,
style_computer: &StyleComputer, style_computer: &StyleComputer,
) -> String { ) -> String {
let table = convert_to_table2( let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX);
0, let result = ExpandedTable::new(None, false, String::new()).build_list(&vals, opts);
vals.iter(), match result {
ctrlc.clone(), Ok(Some(out)) => out,
config,
span,
style_computer,
None,
false,
"",
usize::MAX,
);
match table {
Ok(Some((table, with_header, with_index))) => {
let table_config = create_table_config(
config,
style_computer,
table.count_rows(),
with_header,
with_index,
false,
);
table.draw(table_config, usize::MAX).unwrap_or_else(|| {
value_to_styled_string(&Value::List { vals, span }, config, style_computer).0
})
}
Ok(None) | Err(_) => { Ok(None) | Err(_) => {
// it means that the list is empty // it means that the list is empty
value_to_styled_string(&Value::List { vals, span }, config, style_computer).0 value_to_styled_string(&Value::List { vals, span }, config, style_computer).0
} }
} }
} }
#[allow(clippy::too_many_arguments)]
fn build_expanded_table(
cols: Vec<String>,
vals: Vec<Value>,
span: Span,
ctrlc: Option<Arc<AtomicBool>>,
config: &Config,
style_computer: &StyleComputer,
term_width: usize,
expand_limit: Option<usize>,
flatten: bool,
flatten_sep: &str,
) -> Result<Option<String>, ShellError> {
let theme = load_theme_from_config(config);
// calculate the width of a key part + the rest of table so we know the rest of the table width available for value.
let key_width = cols.iter().map(|col| string_width(col)).max().unwrap_or(0);
let key = NuTable::create_cell(" ".repeat(key_width), TextStyle::default());
let key_table = NuTable::new(vec![vec![key]], (1, 2));
let key_width = key_table
.draw(
create_table_config(config, style_computer, 1, false, false, false),
usize::MAX,
)
.map(|table| string_width(&table))
.unwrap_or(0);
// 3 - count borders (left, center, right)
// 2 - padding
if key_width + 3 + 2 > term_width {
return Ok(None);
}
let remaining_width = term_width - key_width - 3 - 2;
let mut data = Vec::with_capacity(cols.len());
for (key, value) in cols.into_iter().zip(vals) {
// handle CTRLC event
if let Some(ctrlc) = &ctrlc {
if ctrlc.load(Ordering::SeqCst) {
return Ok(None);
}
}
let is_limited = matches!(expand_limit, Some(0));
let mut is_expanded = false;
let value = if is_limited {
value_to_styled_string(&value, config, style_computer).0
} else {
let deep = expand_limit.map(|i| i - 1);
match value {
Value::List { vals, .. } => {
let table = convert_to_table2(
0,
vals.iter(),
ctrlc.clone(),
config,
span,
style_computer,
deep,
flatten,
flatten_sep,
remaining_width,
)?;
match table {
Some((mut table, with_header, with_index)) => {
// control width via removing table columns.
let theme = load_theme_from_config(config);
table.truncate(remaining_width, &theme);
is_expanded = true;
let table_config = create_table_config(
config,
style_computer,
table.count_rows(),
with_header,
with_index,
false,
);
let val = table.draw(table_config, remaining_width);
match val {
Some(result) => result,
None => return Ok(None),
}
}
None => {
// it means that the list is empty
let value = Value::List { vals, span };
value_to_styled_string(&value, config, style_computer).0
}
}
}
Value::Record { cols, vals, span } => {
let result = build_expanded_table(
cols.clone(),
vals.clone(),
span,
ctrlc.clone(),
config,
style_computer,
remaining_width,
deep,
flatten,
flatten_sep,
)?;
match result {
Some(result) => {
is_expanded = true;
result
}
None => {
let failed_value = value_to_styled_string(
&Value::Record { cols, vals, span },
config,
style_computer,
);
wrap_nu_text(failed_value, remaining_width, config).0
}
}
}
val => {
let text = value_to_styled_string(&val, config, style_computer).0;
wrap_nu_text((text, TextStyle::default()), remaining_width, config).0
}
}
};
// we want to have a key being aligned to 2nd line,
// we could use Padding for it but,
// the easiest way to do so is just push a new_line char before
let mut key = key;
if !key.is_empty() && is_expanded && theme.has_top_line() {
key.insert(0, '\n');
}
let key = NuTable::create_cell(key, TextStyle::default_field());
let val = NuTable::create_cell(value, TextStyle::default());
let row = vec![key, val];
data.push(row);
}
let table_config = create_table_config(config, style_computer, data.len(), false, false, false);
let data_len = data.len();
let table = NuTable::new(data, (data_len, 2));
let table_s = table.clone().draw(table_config.clone(), term_width);
let table = match table_s {
Some(s) => {
// check whether we need to expand table or not,
// todo: we can make it more effitient
const EXPAND_THRESHOLD: f32 = 0.80;
let width = string_width(&s);
let used_percent = width as f32 / term_width as f32;
if width < term_width && used_percent > EXPAND_THRESHOLD {
let table_config = table_config.expand();
table.draw(table_config, term_width)
} else {
Some(s)
}
}
None => None,
};
Ok(table)
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::into_iter_on_ref)]
fn convert_to_table2<'a>(
row_offset: usize,
input: impl Iterator<Item = &'a Value> + ExactSizeIterator + Clone,
ctrlc: Option<Arc<AtomicBool>>,
config: &Config,
head: Span,
style_computer: &StyleComputer,
deep: Option<usize>,
flatten: bool,
flatten_sep: &str,
available_width: usize,
) -> Result<Option<(NuTable, bool, bool)>, ShellError> {
const PADDING_SPACE: usize = 2;
const SPLIT_LINE_SPACE: usize = 1;
const ADDITIONAL_CELL_SPACE: usize = PADDING_SPACE + SPLIT_LINE_SPACE;
const TRUNCATE_CELL_WIDTH: usize = 3;
const MIN_CELL_CONTENT_WIDTH: usize = 1;
const OK_CELL_CONTENT_WIDTH: usize = 25;
if input.len() == 0 {
return Ok(None);
}
// 2 - split lines
let mut available_width = available_width.saturating_sub(SPLIT_LINE_SPACE + SPLIT_LINE_SPACE);
if available_width < MIN_CELL_CONTENT_WIDTH {
return Ok(None);
}
let headers = get_columns(input.clone());
let with_index = match config.table_index_mode {
TableIndexMode::Always => true,
TableIndexMode::Never => false,
TableIndexMode::Auto => headers.iter().any(|header| header == INDEX_COLUMN_NAME),
};
// The header with the INDEX is removed from the table headers since
// it is added to the natural table index
let headers: Vec<_> = headers
.into_iter()
.filter(|header| header != INDEX_COLUMN_NAME)
.collect();
let with_header = !headers.is_empty();
let mut data = vec![vec![]; input.len()];
if !headers.is_empty() {
data.push(vec![]);
};
if with_index {
let mut column_width = 0;
if with_header {
data[0].push(NuTable::create_cell(
"#",
header_style(style_computer, String::from("#")),
));
}
for (row, item) in input.clone().enumerate() {
if let Some(ctrlc) = &ctrlc {
if ctrlc.load(Ordering::SeqCst) {
return Ok(None);
}
}
if let Value::Error { error } = item {
return Err(*error.clone());
}
let index = row + row_offset;
let text = matches!(item, Value::Record { .. })
.then(|| lookup_index_value(item, config).unwrap_or_else(|| index.to_string()))
.unwrap_or_else(|| index.to_string());
let value = make_index_string(text, style_computer);
let width = string_width(&value.0);
column_width = max(column_width, width);
let value = NuTable::create_cell(value.0, value.1);
let row = if with_header { row + 1 } else { row };
data[row].push(value);
}
if column_width + ADDITIONAL_CELL_SPACE > available_width {
available_width = 0;
} else {
available_width -= column_width + ADDITIONAL_CELL_SPACE;
}
}
if !with_header {
for (row, item) in input.into_iter().enumerate() {
if let Some(ctrlc) = &ctrlc {
if ctrlc.load(Ordering::SeqCst) {
return Ok(None);
}
}
if let Value::Error { error } = item {
return Err(*error.clone());
}
let value = convert_to_table2_entry(
item,
config,
&ctrlc,
style_computer,
deep,
flatten,
flatten_sep,
available_width,
);
let value = NuTable::create_cell(value.0, value.1);
data[row].push(value);
}
let count_columns = if with_index { 2 } else { 1 };
let size = (data.len(), count_columns);
let table = NuTable::new(data, size);
return Ok(Some((table, with_header, with_index)));
}
let mut widths = Vec::new();
let mut truncate = false;
let count_columns = headers.len();
for (col, header) in headers.into_iter().enumerate() {
let is_last_col = col + 1 == count_columns;
let mut necessary_space = PADDING_SPACE;
if !is_last_col {
necessary_space += SPLIT_LINE_SPACE;
}
if available_width == 0 || available_width <= necessary_space {
// MUST NEVER HAPPEN (ideally)
// but it does...
truncate = true;
break;
}
available_width -= necessary_space;
let mut column_width = string_width(&header);
data[0].push(NuTable::create_cell(
&header,
header_style(style_computer, header.clone()),
));
for (row, item) in input.clone().enumerate() {
if let Some(ctrlc) = &ctrlc {
if ctrlc.load(Ordering::SeqCst) {
return Ok(None);
}
}
if let Value::Error { error } = item {
return Err(*error.clone());
}
let value = create_table2_entry(
item,
&header,
head,
config,
&ctrlc,
style_computer,
deep,
flatten,
flatten_sep,
available_width,
);
let value_width = string_width(&value.0);
column_width = max(column_width, value_width);
let value = NuTable::create_cell(value.0, value.1);
data[row + 1].push(value);
}
if column_width >= available_width
|| (!is_last_col && column_width + necessary_space >= available_width)
{
// so we try to do soft landing
// by doing a truncating in case there will be enough space for it.
column_width = string_width(&header);
for (row, item) in input.clone().enumerate() {
if let Some(ctrlc) = &ctrlc {
if ctrlc.load(Ordering::SeqCst) {
return Ok(None);
}
}
let value = create_table2_entry_basic(item, &header, head, config, style_computer);
let value = wrap_nu_text(value, available_width, config);
let value_width = string_width(&value.0);
column_width = max(column_width, value_width);
let value = NuTable::create_cell(value.0, value.1);
*data[row + 1].last_mut().expect("unwrap") = value;
}
}
let is_suitable_for_wrap =
available_width >= string_width(&header) && available_width >= OK_CELL_CONTENT_WIDTH;
if column_width >= available_width && is_suitable_for_wrap {
// so we try to do soft landing ONCE AGAIN
// but including a wrap
column_width = string_width(&header);
for (row, item) in input.clone().enumerate() {
if let Some(ctrlc) = &ctrlc {
if ctrlc.load(Ordering::SeqCst) {
return Ok(None);
}
}
let value = create_table2_entry_basic(item, &header, head, config, style_computer);
let value = wrap_nu_text(value, OK_CELL_CONTENT_WIDTH, config);
let value = NuTable::create_cell(value.0, value.1);
*data[row + 1].last_mut().expect("unwrap") = value;
}
}
if column_width > available_width {
// remove just added column
for row in &mut data {
row.pop();
}
available_width += necessary_space;
truncate = true;
break;
}
available_width -= column_width;
widths.push(column_width);
}
if truncate {
if available_width <= TRUNCATE_CELL_WIDTH + PADDING_SPACE {
// back up by removing last column.
// it's ALWAYS MUST has us enough space for a shift column
while let Some(width) = widths.pop() {
for row in &mut data {
row.pop();
}
available_width += width + PADDING_SPACE + SPLIT_LINE_SPACE;
if available_width > TRUNCATE_CELL_WIDTH + PADDING_SPACE {
break;
}
}
}
// this must be a RARE case or even NEVER happen,
// but we do check it just in case.
if widths.is_empty() {
return Ok(None);
}
let shift = NuTable::create_cell(String::from("..."), TextStyle::default());
for row in &mut data {
row.push(shift.clone());
}
widths.push(3);
}
let count_columns = widths.len() + with_index as usize;
let count_rows = data.len();
let size = (count_rows, count_columns);
let table = NuTable::new(data, size);
Ok(Some((table, with_header, with_index)))
}
fn lookup_index_value(item: &Value, config: &Config) -> Option<String> {
item.get_data_by_key(INDEX_COLUMN_NAME)
.map(|value| value.into_string("", config))
}
fn header_style(style_computer: &StyleComputer, header: String) -> TextStyle {
let style = style_computer.compute("header", &Value::string(header.as_str(), Span::unknown()));
TextStyle {
alignment: Alignment::Center,
color_style: Some(style),
}
}
#[allow(clippy::too_many_arguments)]
fn create_table2_entry_basic(
item: &Value,
header: &str,
head: Span,
config: &Config,
style_computer: &StyleComputer,
) -> NuText {
match item {
Value::Record { .. } => {
let val = header.to_owned();
let path = PathMember::String {
val,
span: head,
optional: false,
};
let val = item.clone().follow_cell_path(&[path], false);
match val {
Ok(val) => value_to_styled_string(&val, config, style_computer),
Err(_) => error_sign(style_computer),
}
}
_ => value_to_styled_string(item, config, style_computer),
}
}
#[allow(clippy::too_many_arguments)]
fn create_table2_entry(
item: &Value,
header: &str,
head: Span,
config: &Config,
ctrlc: &Option<Arc<AtomicBool>>,
style_computer: &StyleComputer,
deep: Option<usize>,
flatten: bool,
flatten_sep: &str,
width: usize,
) -> NuText {
match item {
Value::Record { .. } => {
let val = header.to_owned();
let path = PathMember::String {
val,
span: head,
optional: false,
};
let val = item.clone().follow_cell_path(&[path], false);
match val {
Ok(val) => convert_to_table2_entry(
&val,
config,
ctrlc,
style_computer,
deep,
flatten,
flatten_sep,
width,
),
Err(_) => wrap_nu_text(error_sign(style_computer), width, config),
}
}
_ => convert_to_table2_entry(
item,
config,
ctrlc,
style_computer,
deep,
flatten,
flatten_sep,
width,
),
}
}
fn error_sign(style_computer: &StyleComputer) -> (String, TextStyle) {
make_styled_string(style_computer, String::from(""), None, 0)
}
fn wrap_nu_text(mut text: NuText, width: usize, config: &Config) -> NuText {
text.0 = nu_table::string_wrap(&text.0, width, is_cfg_trim_keep_words(config));
text
}
#[allow(clippy::too_many_arguments)]
fn convert_to_table2_entry(
item: &Value,
config: &Config,
ctrlc: &Option<Arc<AtomicBool>>,
// This is passed in, even though it could be retrieved from config,
// to save reallocation (because it's presumably being used upstream).
style_computer: &StyleComputer,
deep: Option<usize>,
flatten: bool,
flatten_sep: &str,
width: usize,
) -> NuText {
let is_limit_reached = matches!(deep, Some(0));
if is_limit_reached {
return wrap_nu_text(
value_to_styled_string(item, config, style_computer),
width,
config,
);
}
match &item {
Value::Record { span, cols, vals } => {
if cols.is_empty() && vals.is_empty() {
wrap_nu_text(
value_to_styled_string(item, config, style_computer),
width,
config,
)
} else {
let table = convert_to_table2(
0,
std::iter::once(item),
ctrlc.clone(),
config,
*span,
style_computer,
deep.map(|i| i - 1),
flatten,
flatten_sep,
width,
);
let inner_table = table.map(|table| {
table.and_then(|(table, with_header, with_index)| {
let table_config = create_table_config(
config,
style_computer,
table.count_rows(),
with_header,
with_index,
false,
);
table.draw(table_config, usize::MAX)
})
});
if let Ok(Some(table)) = inner_table {
(table, TextStyle::default())
} else {
// error so back down to the default
wrap_nu_text(
value_to_styled_string(item, config, style_computer),
width,
config,
)
}
}
}
Value::List { vals, span } => {
let is_simple_list = vals
.iter()
.all(|v| !matches!(v, Value::Record { .. } | Value::List { .. }));
if flatten && is_simple_list {
wrap_nu_text(
convert_value_list_to_string(vals, config, style_computer, flatten_sep),
width,
config,
)
} else {
let table = convert_to_table2(
0,
vals.iter(),
ctrlc.clone(),
config,
*span,
style_computer,
deep.map(|i| i - 1),
flatten,
flatten_sep,
width,
);
let inner_table = table.map(|table| {
table.and_then(|(table, with_header, with_index)| {
let table_config = create_table_config(
config,
style_computer,
table.count_rows(),
with_header,
with_index,
false,
);
table.draw(table_config, usize::MAX)
})
});
if let Ok(Some(table)) = inner_table {
(table, TextStyle::default())
} else {
// error so back down to the default
wrap_nu_text(
value_to_styled_string(item, config, style_computer),
width,
config,
)
}
}
}
_ => wrap_nu_text(
value_to_styled_string(item, config, style_computer),
width,
config,
), // unknown type.
}
}
fn convert_value_list_to_string(
vals: &[Value],
config: &Config,
// This is passed in, even though it could be retrieved from config,
// to save reallocation (because it's presumably being used upstream).
style_computer: &StyleComputer,
flatten_sep: &str,
) -> NuText {
let mut buf = Vec::new();
for value in vals {
let (text, _) = value_to_styled_string(value, config, style_computer);
buf.push(text);
}
let text = buf.join(flatten_sep);
(text, TextStyle::default())
}
fn value_to_styled_string(
value: &Value,
config: &Config,
// This is passed in, even though it could be retrieved from config,
// to save reallocation (because it's presumably being used upstream).
style_computer: &StyleComputer,
) -> NuText {
let float_precision = config.float_precision as usize;
make_styled_string(
style_computer,
value.into_abbreviated_string(config),
Some(value),
float_precision,
)
}
fn make_styled_string(
style_computer: &StyleComputer,
text: String,
value: Option<&Value>, // None represents table holes.
float_precision: usize,
) -> NuText {
match value {
Some(value) => {
match value {
Value::Float { .. } => {
// set dynamic precision from config
let precise_number = convert_with_precision(&text, float_precision)
.unwrap_or_else(|e| e.to_string());
(precise_number, style_computer.style_primitive(value))
}
_ => (text, style_computer.style_primitive(value)),
}
}
None => {
// Though holes are not the same as null, the closure for "empty" is passed a null anyway.
(
text,
TextStyle::with_style(
Alignment::Center,
style_computer.compute("empty", &Value::nothing(Span::unknown())),
),
)
}
}
}
fn make_index_string(text: String, style_computer: &StyleComputer) -> NuText {
let style = style_computer.compute("row_index", &Value::string(text.as_str(), Span::unknown()));
(text, TextStyle::with_style(Alignment::Right, style))
}
fn convert_with_precision(val: &str, precision: usize) -> Result<String, ShellError> {
// vall will always be a f64 so convert it with precision formatting
let val_float = match val.trim().parse::<f64>() {
Ok(f) => f,
Err(e) => {
return Err(ShellError::GenericError(
format!("error converting string [{}] to f64", &val),
"".to_string(),
None,
Some(e.to_string()),
Vec::new(),
));
}
};
Ok(format!("{val_float:.precision$}"))
}
fn load_theme_from_config(config: &Config) -> TableTheme {
match config.table_mode.as_str() {
"basic" => nu_table::TableTheme::basic(),
"thin" => nu_table::TableTheme::thin(),
"light" => nu_table::TableTheme::light(),
"compact" => nu_table::TableTheme::compact(),
"with_love" => nu_table::TableTheme::with_love(),
"compact_double" => nu_table::TableTheme::compact_double(),
"rounded" => nu_table::TableTheme::rounded(),
"reinforced" => nu_table::TableTheme::reinforced(),
"heavy" => nu_table::TableTheme::heavy(),
"none" => nu_table::TableTheme::none(),
_ => nu_table::TableTheme::rounded(),
}
}
fn create_table_config(
config: &Config,
style_computer: &StyleComputer,
count_records: usize,
with_header: bool,
with_index: bool,
expand: bool,
) -> TableConfig {
let theme = load_theme_from_config(config);
let append_footer = with_footer(config, with_header, count_records);
let mut table_cfg = TableConfig::new(theme, with_header, with_index, append_footer);
table_cfg = table_cfg.splitline_style(lookup_separator_color(style_computer));
if expand {
table_cfg = table_cfg.expand();
}
table_cfg.trim(config.trim_strategy.clone())
}
fn lookup_separator_color(style_computer: &StyleComputer) -> nu_ansi_term::Style {
style_computer.compute("separator", &Value::nothing(Span::unknown()))
}
fn with_footer(config: &Config, with_header: bool, count_records: usize) -> bool {
with_header && need_footer(config, count_records as u64)
}
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)
}
fn is_cfg_trim_keep_words(config: &Config) -> bool {
matches!(
config.trim_strategy,
TrimStrategy::Wrap {
try_to_keep_words: true
}
)
}

View file

@ -18,9 +18,7 @@ nu-color-config = { path = "../nu-color-config", version = "0.79.1" }
nu-ansi-term = "0.47.0" nu-ansi-term = "0.47.0"
tabled = { version = "0.10.0", features = ["color"], default-features = false } tabled = { version = "0.12.0", features = ["color"], default-features = false }
json_to_table = { version = "0.3.1", features = ["color"] }
serde_json = "1"
[dev-dependencies] [dev-dependencies]
# nu-test-support = { path="../nu-test-support", version = "0.79.1" } # nu-test-support = { path="../nu-test-support", version = "0.79.1" }

View file

@ -1,13 +1,13 @@
use nu_ansi_term::{Color, Style};
use nu_color_config::TextStyle; use nu_color_config::TextStyle;
use nu_table::{Table, TableConfig, TableTheme}; use nu_table::{NuTable, TableConfig, TableTheme};
use tabled::papergrid::records::{cell_info::CellInfo, tcell::TCell}; use tabled::grid::records::vec_records::CellInfo;
fn main() { fn main() {
let args: Vec<_> = std::env::args().collect(); let args: Vec<_> = std::env::args().collect();
let mut width = 0; let mut width = 0;
if args.len() > 1 { if args.len() > 1 {
// Width in terminal characters
width = args[1].parse::<usize>().expect("Need a width in columns"); width = args[1].parse::<usize>().expect("Need a width in columns");
} }
@ -16,31 +16,26 @@ fn main() {
width = 80; width = 80;
} }
// The mocked up table data
let (table_headers, row_data) = make_table_data(); let (table_headers, row_data) = make_table_data();
// The table headers let headers = to_cell_info_vec(&table_headers);
let headers = vec_of_str_to_vec_of_styledstr(&table_headers, true); let rows = to_cell_info_vec(&row_data);
// The table rows
let rows = vec_of_str_to_vec_of_styledstr(&row_data, false);
// The table itself
let count_cols = std::cmp::max(rows.len(), headers.len());
let mut rows = vec![rows; 3]; let mut rows = vec![rows; 3];
rows.insert(0, headers); rows.insert(0, headers);
let mut table = NuTable::from(rows);
table.set_data_style(TextStyle::basic_left());
table.set_header_style(TextStyle::basic_center().style(Style::new().on(Color::Blue)));
let theme = TableTheme::rounded(); let theme = TableTheme::rounded();
let table_cfg = TableConfig::new(theme, true, false, false); let table_cfg = TableConfig::new().theme(theme).with_header(true);
let table = Table::new(rows, (3, count_cols));
// Capture the table as a string
let output_table = table let output_table = table
.draw(table_cfg, width) .draw(table_cfg, width)
.unwrap_or_else(|| format!("Couldn't fit table into {width} columns!")); .unwrap_or_else(|| format!("Couldn't fit table into {width} columns!"));
// Draw the table
println!("{output_table}") println!("{output_table}")
} }
@ -82,24 +77,11 @@ fn make_table_data() -> (Vec<&'static str>, Vec<&'static str>) {
(table_headers, row_data) (table_headers, row_data)
} }
fn vec_of_str_to_vec_of_styledstr( fn to_cell_info_vec(data: &[&str]) -> Vec<CellInfo<String>> {
data: &[&str],
is_header: bool,
) -> Vec<TCell<CellInfo<'static>, TextStyle>> {
let mut v = vec![]; let mut v = vec![];
for x in data { for x in data {
if is_header { v.push(CellInfo::new(String::from(*x)));
v.push(Table::create_cell(
String::from(*x),
TextStyle::default_header(),
))
} else {
v.push(Table::create_cell(
String::from(*x),
TextStyle::basic_left(),
))
}
} }
v v
} }

View file

@ -1,10 +1,15 @@
mod nu_protocol_table;
mod table; mod table;
mod table_theme; mod table_theme;
mod types;
mod unstructured_table;
mod util; mod util;
pub use nu_color_config::TextStyle; pub use nu_color_config::TextStyle;
pub use nu_protocol_table::NuTable; pub use table::{Alignments, Cell, NuTable, TableConfig};
pub use table::{Alignments, Table, TableConfig};
pub use table_theme::TableTheme; pub use table_theme::TableTheme;
pub use types::{
clean_charset, value_to_clean_styled_string, value_to_styled_string, BuildConfig,
CollapsedTable, ExpandedTable, JustTable, NuText, StringResult, TableOutput, TableResult,
};
pub use unstructured_table::UnstructuredTable;
pub use util::*; pub use util::*;

View file

@ -1,227 +0,0 @@
use std::collections::HashMap;
use crate::{string_width, Alignments, TableTheme};
use nu_color_config::StyleComputer;
use nu_protocol::{Config, Span, Value};
use tabled::{
color::Color,
formatting::AlignmentStrategy,
object::Segment,
papergrid::{records::Records, GridConfig},
Alignment, Modify,
};
use serde_json::Value as Json;
/// NuTable has a recursive table representation of nu_protocol::Value.
///
/// It doesn't support alignment and a proper width control.
pub struct NuTable {
inner: String,
}
impl NuTable {
pub fn new(
value: Value,
collapse: bool,
config: &Config,
style_computer: &StyleComputer,
theme: &TableTheme,
with_footer: bool,
) -> Self {
let mut table = tabled::Table::new([""]);
load_theme(&mut table, style_computer, theme);
let cfg = table.get_config().clone();
let val = nu_protocol_value_to_json(value, config, with_footer);
let table = build_table(val, cfg, collapse);
Self { inner: table }
}
pub fn draw(&self, termwidth: usize) -> Option<String> {
let table_width = string_width(&self.inner);
if table_width > termwidth {
None
} else {
Some(self.inner.clone())
}
}
}
fn build_table(val: Json, cfg: GridConfig, collapse: bool) -> String {
let mut table = json_to_table::json_to_table(&val);
table.set_config(cfg);
if collapse {
table.collapse();
}
table.to_string()
}
fn nu_protocol_value_to_json(value: Value, config: &Config, with_footer: bool) -> Json {
match value {
Value::Record { cols, vals, .. } => {
let mut map = serde_json::Map::new();
for (key, value) in cols.into_iter().zip(vals) {
let val = nu_protocol_value_to_json(value, config, false);
map.insert(key, val);
}
Json::Object(map)
}
Value::List { vals, .. } => {
let mut used_cols: Option<&[String]> = None;
for val in &vals {
match val {
Value::Record { cols, .. } => {
if let Some(_cols) = &used_cols {
if _cols != cols {
used_cols = None;
break;
}
} else {
used_cols = Some(cols)
}
}
_ => {
used_cols = None;
break;
}
}
}
if let Some(cols) = used_cols {
// rebuild array as a map
if cols.len() > 1 {
let mut arr = vec![];
let head = cols.iter().map(|s| Value::String {
val: s.to_owned(),
span: Span::new(0, 0),
});
let head = build_map(head, config);
arr.push(Json::Object(head.clone()));
for value in &vals {
if let Ok((_, vals)) = value.as_record() {
let vals = build_map(vals.iter().cloned(), config);
let mut map = serde_json::Map::new();
connect_maps(&mut map, Json::Object(vals));
arr.push(Json::Object(map));
}
}
if with_footer {
arr.push(Json::Object(head));
}
return Json::Array(arr);
} else {
let mut map = vec![];
let head = Json::Array(vec![Json::String(cols[0].to_owned())]);
map.push(head.clone());
for value in vals {
if let Value::Record { vals, .. } = value {
let list = Value::List {
vals,
span: Span::new(0, 0),
};
let val = nu_protocol_value_to_json(list, config, false); // rebuild array as a map
map.push(val);
}
}
if with_footer {
map.push(head);
}
return Json::Array(map);
};
}
let mut map = Vec::new();
for value in vals {
let val = nu_protocol_value_to_json(value, config, false);
map.push(val);
}
Json::Array(map)
}
val => Json::String(val.into_abbreviated_string(config)),
}
}
fn build_map(
values: impl Iterator<Item = Value> + DoubleEndedIterator,
config: &Config,
) -> serde_json::Map<String, Json> {
let mut map = serde_json::Map::new();
let mut last_val: Option<Value> = None;
for val in values.rev() {
if map.is_empty() {
match last_val.take() {
Some(prev_val) => {
let col = val.into_abbreviated_string(&Config::default());
let prev = nu_protocol_value_to_json(prev_val, config, false);
map.insert(col, prev);
}
None => {
last_val = Some(val);
}
}
} else {
let mut new_m = serde_json::Map::new();
let col = val.into_abbreviated_string(&Config::default());
new_m.insert(col, Json::Object(map));
map = new_m;
}
}
map
}
fn connect_maps(map: &mut serde_json::Map<String, Json>, value: Json) {
if let Json::Object(m) = value {
for (key, value) in m {
if value.is_object() {
let mut new_m = serde_json::Map::new();
connect_maps(&mut new_m, value);
map.insert(key, Json::Object(new_m));
} else {
map.insert(key, value);
}
}
}
}
//
fn load_theme<R>(table: &mut tabled::Table<R>, style_computer: &StyleComputer, theme: &TableTheme)
where
R: Records,
{
let mut theme = theme.into_full().unwrap_or_else(|| theme.theme.clone());
theme.set_horizontals(HashMap::default());
table.with(theme);
// color_config closures for "separator" are just given a null.
let color = style_computer.compute("separator", &Value::nothing(Span::unknown()));
let color = color.paint(" ").to_string();
if let Ok(color) = Color::try_from(color) {
table.with(color);
}
table.with(
Modify::new(Segment::all())
.with(Alignment::Horizontal(Alignments::default().data))
.with(AlignmentStrategy::PerLine),
);
}

View file

@ -4,108 +4,145 @@ use nu_color_config::TextStyle;
use nu_protocol::TrimStrategy; use nu_protocol::TrimStrategy;
use std::{cmp::min, collections::HashMap}; use std::{cmp::min, collections::HashMap};
use tabled::{ use tabled::{
alignment::AlignmentHorizontal,
builder::Builder, builder::Builder,
color::Color, grid::{
formatting::AlignmentStrategy, color::AnsiColor,
object::{Cell, Columns, Rows, Segment}, config::{AlignmentHorizontal, ColoredConfig, Entity, EntityMap, Position},
papergrid::{ dimension::CompleteDimensionVecRecords,
records::{ records::{
cell_info::CellInfo, tcell::TCell, vec_records::VecRecords, Records, RecordsMut, vec_records::{CellInfo, VecRecords},
ExactRecords, Records,
}, },
util::string_width_multiline,
width::{CfgWidthFunction, WidthEstimator},
Estimate,
}, },
peaker::Peaker, settings::{
Alignment, Modify, ModifyObject, TableOption, Width, formatting::AlignmentStrategy, object::Segment, peaker::Peaker, Color, Modify, Settings,
TableOption, Width,
},
Table,
}; };
/// Table represent a table view. /// Table represent a table view.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Table { pub struct NuTable {
data: Data, data: Data,
styles: Styles,
alignments: Alignments,
size: (usize, usize),
} }
type Data = VecRecords<TCell<CellInfo<'static>, TextStyle>>; #[derive(Debug, Default, Clone)]
struct Styles {
index: AnsiColor<'static>,
header: AnsiColor<'static>,
data: EntityMap<AnsiColor<'static>>,
data_is_set: bool,
}
impl Table { type Data = VecRecords<Cell>;
/// Creates a [Table] instance. pub type Cell = CellInfo<String>;
///
/// If `headers.is_empty` then no headers will be rendered.
pub fn new(data: Vec<Vec<TCell<CellInfo<'static>, TextStyle>>>, size: (usize, usize)) -> Table {
// it's not guaranteed that data will have all rows with the same number of columns.
// but VecRecords::with_hint require this constrain.
//
// so we do a check to make it certainly true
let mut data = data; impl NuTable {
make_data_consistent(&mut data, size); /// Creates an empty [Table] instance.
pub fn new(count_rows: usize, count_columns: usize) -> Self {
let data = VecRecords::with_hint(data, size.1); let data = VecRecords::new(vec![vec![CellInfo::default(); count_columns]; count_rows]);
Self {
Table { data } data,
size: (count_rows, count_columns),
styles: Styles::default(),
alignments: Alignments::default(),
}
} }
pub fn count_rows(&self) -> usize { pub fn count_rows(&self) -> usize {
self.data.count_rows() self.size.0
} }
pub fn create_cell( pub fn count_columns(&self) -> usize {
text: impl Into<String>, self.size.1
style: TextStyle,
) -> TCell<CellInfo<'static>, TextStyle> {
TCell::new(CellInfo::new(text.into(), CfgWidthFunction::new(4)), style)
} }
pub fn truncate(&mut self, width: usize, theme: &TableTheme) -> bool { pub fn insert(&mut self, pos: Position, text: String) {
let mut truncated = false; self.data[pos.0][pos.1] = CellInfo::new(text);
while self.data.count_rows() > 0 && self.data.count_columns() > 0 { }
let total;
{
let mut table = Builder::custom(self.data.clone()).build();
load_theme(&mut table, theme, false, false, None);
total = table.total_width();
}
if total > width { pub fn set_column_style(&mut self, column: usize, style: TextStyle) {
truncated = true; if let Some(style) = style.color_style {
self.data.truncate(self.data.count_columns() - 1); let style = AnsiColor::from(convert_style(style));
} else { self.styles.data.insert(Entity::Column(column), style);
break; self.styles.data_is_set = true;
}
} }
let is_empty = self.data.count_rows() == 0 || self.data.count_columns() == 0; let alignment = convert_alignment(style.alignment);
if is_empty { if alignment != self.alignments.data {
return true; self.alignments.columns.insert(column, alignment);
}
}
pub fn set_cell_style(&mut self, pos: Position, style: TextStyle) {
if let Some(style) = style.color_style {
let style = AnsiColor::from(convert_style(style));
self.styles.data.insert(Entity::Cell(pos.0, pos.1), style);
self.styles.data_is_set = true;
} }
if truncated { let alignment = convert_alignment(style.alignment);
self.data.push(Table::create_cell( if alignment != self.alignments.data {
String::from("..."), self.alignments.cells.insert(pos, alignment);
TextStyle::default(), }
)); }
pub fn set_header_style(&mut self, style: TextStyle) {
if let Some(style) = style.color_style {
let style = AnsiColor::from(convert_style(style));
self.styles.header = style;
} }
false self.alignments.header = convert_alignment(style.alignment);
}
pub fn set_index_style(&mut self, style: TextStyle) {
if let Some(style) = style.color_style {
let style = AnsiColor::from(convert_style(style));
self.styles.index = style;
}
self.alignments.index = convert_alignment(style.alignment);
}
pub fn set_data_style(&mut self, style: TextStyle) {
if let Some(style) = style.color_style {
let style = AnsiColor::from(convert_style(style));
self.styles.data.insert(Entity::Global, style);
self.styles.data_is_set = true;
}
self.alignments.data = convert_alignment(style.alignment);
} }
/// Converts a table to a String. /// Converts a table to a String.
/// ///
/// It returns None in case where table cannot be fit to a terminal width. /// It returns None in case where table cannot be fit to a terminal width.
pub fn draw(self, config: TableConfig, termwidth: usize) -> Option<String> { pub fn draw(self, config: TableConfig, termwidth: usize) -> Option<String> {
build_table(self.data, config, termwidth) build_table(self.data, config, self.alignments, self.styles, termwidth)
}
/// Return a total table width.
pub fn total_width(&self, config: &TableConfig) -> usize {
let config = get_config(&config.theme, false, None);
let widths = build_width(&self.data);
get_total_width2(&widths, &config)
} }
} }
fn make_data_consistent(data: &mut Vec<Vec<TCell<CellInfo, TextStyle>>>, size: (usize, usize)) { impl From<Vec<Vec<CellInfo<String>>>> for NuTable {
for row in data { fn from(value: Vec<Vec<CellInfo<String>>>) -> Self {
if row.len() < size.1 { let data = VecRecords::new(value);
row.extend( let size = (data.count_rows(), data.count_columns());
std::iter::repeat(Table::create_cell(String::default(), TextStyle::default())) Self {
.take(size.1 - row.len()), data,
); size,
alignments: Alignments::default(),
styles: Styles::default(),
} }
} }
} }
@ -113,7 +150,6 @@ fn make_data_consistent(data: &mut Vec<Vec<TCell<CellInfo, TextStyle>>>, size: (
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TableConfig { pub struct TableConfig {
theme: TableTheme, theme: TableTheme,
alignments: Alignments,
trim: TrimStrategy, trim: TrimStrategy,
split_color: Option<Style>, split_color: Option<Style>,
expand: bool, expand: bool,
@ -123,26 +159,20 @@ pub struct TableConfig {
} }
impl TableConfig { impl TableConfig {
pub fn new( pub fn new() -> Self {
theme: TableTheme,
with_header: bool,
with_index: bool,
append_footer: bool,
) -> Self {
Self { Self {
theme, theme: TableTheme::basic(),
with_header, with_header: false,
with_index, with_index: false,
with_footer: append_footer, with_footer: false,
expand: false, expand: false,
alignments: Alignments::default(),
trim: TrimStrategy::truncate(None), trim: TrimStrategy::truncate(None),
split_color: None, split_color: None,
} }
} }
pub fn expand(mut self) -> Self { pub fn expand(mut self, on: bool) -> Self {
self.expand = true; self.expand = on;
self self
} }
@ -151,248 +181,256 @@ impl TableConfig {
self self
} }
pub fn splitline_style(mut self, color: Style) -> Self { pub fn line_style(mut self, color: Style) -> Self {
self.split_color = Some(color); self.split_color = Some(color);
self self
} }
pub fn with_header(mut self, on: bool) -> Self {
self.with_header = on;
self
}
pub fn with_footer(mut self, on: bool) -> Self {
self.with_footer = on;
self
}
pub fn with_index(mut self, on: bool) -> Self {
self.with_index = on;
self
}
pub fn theme(mut self, theme: TableTheme) -> Self {
self.theme = theme;
self
}
} }
#[derive(Debug, Clone, Copy)] impl Default for TableConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct Alignments { pub struct Alignments {
pub(crate) data: AlignmentHorizontal, data: AlignmentHorizontal,
pub(crate) index: AlignmentHorizontal, index: AlignmentHorizontal,
pub(crate) header: AlignmentHorizontal, header: AlignmentHorizontal,
columns: HashMap<usize, AlignmentHorizontal>,
cells: HashMap<Position, AlignmentHorizontal>,
} }
impl Default for Alignments { impl Default for Alignments {
fn default() -> Self { fn default() -> Self {
Self { Self {
data: AlignmentHorizontal::Center, data: AlignmentHorizontal::Left,
index: AlignmentHorizontal::Right, index: AlignmentHorizontal::Right,
header: AlignmentHorizontal::Center, header: AlignmentHorizontal::Center,
columns: HashMap::default(),
cells: HashMap::default(),
} }
} }
} }
fn build_table(mut data: Data, cfg: TableConfig, termwidth: usize) -> Option<String> { fn build_table(
let is_empty = maybe_truncate_columns(&mut data, &cfg.theme, termwidth); mut data: Data,
if is_empty { cfg: TableConfig,
alignments: Alignments,
styles: Styles,
termwidth: usize,
) -> Option<String> {
if data.count_columns() == 0 || data.count_rows() == 0 {
return Some(String::new());
}
let widths = maybe_truncate_columns(&mut data, &cfg.theme, termwidth);
if widths.is_empty() {
return None; return None;
} }
if cfg.with_footer { if cfg.with_header && cfg.with_footer {
data.duplicate_row(0); duplicate_row(&mut data, 0);
} }
draw_table( draw_table(data, alignments, styles, widths, cfg, termwidth)
data,
&cfg.theme,
cfg.alignments,
cfg.with_index,
cfg.with_header,
cfg.with_footer,
cfg.expand,
cfg.split_color,
&cfg.trim,
termwidth,
)
} }
#[allow(clippy::too_many_arguments)]
fn draw_table( fn draw_table(
data: Data, data: Data,
theme: &TableTheme,
alignments: Alignments, alignments: Alignments,
with_index: bool, styles: Styles,
with_header: bool, widths: Vec<usize>,
with_footer: bool, cfg: TableConfig,
expand: bool,
split_color: Option<Style>,
trim_strategy: &TrimStrategy,
termwidth: usize, termwidth: usize,
) -> Option<String> { ) -> Option<String> {
let mut table = Builder::custom(data).build(); let data: Vec<Vec<_>> = data.into();
load_theme(&mut table, theme, with_footer, with_header, split_color); let mut table = Builder::from(data).build();
let with_footer = cfg.with_footer;
let with_index = cfg.with_index;
let with_header = cfg.with_header && table.count_rows() > 1;
let sep_color = cfg.split_color;
load_theme(&mut table, &cfg.theme, with_footer, with_header, sep_color);
align_table(&mut table, alignments, with_index, with_header, with_footer); align_table(&mut table, alignments, with_index, with_header, with_footer);
colorize_table(&mut table, styles, with_index, with_header, with_footer);
if expand { let total_width = get_total_width2(&widths, table.get_config());
table.with(Width::increase(termwidth)); let total_width = if total_width > termwidth {
} table_trim_columns(&mut table, widths, termwidth, &cfg.trim);
table.total_width()
} else if cfg.expand && termwidth > total_width {
table.with(Settings::new(
SetDimensions(widths),
Width::increase(termwidth),
));
table_trim_columns(&mut table, termwidth, trim_strategy); termwidth
} else {
total_width
};
let text = table.to_string(); if total_width > termwidth {
if string_width_multiline(&text) > termwidth {
None None
} else { } else {
Some(text) let content = table.to_string();
Some(content)
} }
} }
fn align_table( fn align_table(
table: &mut tabled::Table<Data>, table: &mut Table,
alignments: Alignments, alignments: Alignments,
with_index: bool, with_index: bool,
with_header: bool, with_header: bool,
with_footer: bool, with_footer: bool,
) { ) {
table.with( table
Modify::new(Segment::all()) .with(Modify::new(Segment::all()).with(AlignmentStrategy::PerLine))
.with(Alignment::Horizontal(alignments.data)) .with(SetAlignment(alignments.data, Entity::Global));
.with(AlignmentStrategy::PerLine),
); for (column, alignment) in alignments.columns {
table.with(SetAlignment(alignment, Entity::Column(column)));
}
for (pos, alignment) in alignments.cells {
table.with(SetAlignment(alignment, Entity::Cell(pos.0, pos.1)));
}
if with_header { if with_header {
let alignment = Alignment::Horizontal(alignments.header); table.with(SetAlignment(alignments.header, Entity::Row(0)));
if with_footer {
table.with(Modify::new(Rows::last()).with(alignment.clone()));
}
table.with(Modify::new(Rows::first()).with(alignment)); if with_footer {
table.with(SetAlignment(
alignments.header,
Entity::Row(table.count_rows() - 1),
));
}
} }
if with_index { if with_index {
table.with(Modify::new(Columns::first()).with(Alignment::Horizontal(alignments.index))); table.with(SetAlignment(alignments.index, Entity::Column(0)));
} }
override_alignments(table, with_header, with_index, alignments);
} }
fn override_alignments( fn colorize_table(
table: &mut tabled::Table<Data>, table: &mut Table,
header_present: bool, mut styles: Styles,
index_present: bool, with_index: bool,
alignments: Alignments, with_header: bool,
with_footer: bool,
) { ) {
let offset = usize::from(header_present); if with_index {
let (count_rows, count_columns) = table.shape(); styles.data.insert(Entity::Column(0), styles.index);
for row in offset..count_rows { styles.data_is_set = true;
for col in 0..count_columns { }
let alignment = table.get_records()[(row, col)].get_data().alignment;
if index_present && col == 0 && alignment == alignments.index {
continue;
}
if alignment == alignments.data { if with_header {
continue; styles.data.insert(Entity::Row(0), styles.header.clone());
} styles.data_is_set = true;
table.with( if with_footer {
Cell(row, col) let count_rows = table.count_rows();
.modify() if count_rows > 1 {
.with(Alignment::Horizontal(alignment)), let last_row = count_rows - 1;
); styles.data.insert(Entity::Row(last_row), styles.header);
}
} }
} }
if styles.data_is_set {
table.get_config_mut().set_colors(styles.data);
}
} }
fn load_theme<R>( fn load_theme(
table: &mut tabled::Table<R>, table: &mut Table,
theme: &TableTheme, theme: &TableTheme,
with_footer: bool, with_footer: bool,
with_header: bool, with_header: bool,
separator_color: Option<Style>, sep_color: Option<Style>,
) where ) {
R: Records, let mut theme = theme.get_theme();
{
let mut theme = theme.theme.clone();
if !with_header { if !with_header {
theme.set_horizontals(HashMap::default()); theme.set_horizontals(HashMap::new());
} else if with_footer && table.count_rows() > 2 {
if let Some(line) = theme.get_horizontal(1) {
theme.insert_horizontal(table.count_rows() - 1, line);
}
} }
table.with(theme); table.with(theme);
if let Some(color) = separator_color { if let Some(style) = sep_color {
let color = color.paint(" ").to_string(); let color = convert_style(style);
if let Ok(color) = Color::try_from(color) { let color = AnsiColor::from(color);
table.with(color); table.get_config_mut().set_border_color_global(color);
}
}
if with_footer {
table.with(FooterStyle).with(
Modify::new(Rows::last())
.with(Alignment::center())
.with(AlignmentStrategy::PerCell),
);
} }
} }
struct FooterStyle; struct FooterStyle;
impl<R> TableOption<R> for FooterStyle impl<R: ExactRecords, D> TableOption<R, D, ColoredConfig> for FooterStyle {
where fn change(self, records: &mut R, cfg: &mut ColoredConfig, _: &mut D) {
R: Records, if let Some(line) = cfg.get_horizontal_line(1).cloned() {
{ let count_rows = records.count_rows();
fn change(&mut self, table: &mut tabled::Table<R>) { cfg.insert_horizontal_line(count_rows - 1, line);
if table.is_empty() {
return;
}
if let Some(line) = table.get_config().get_horizontal_line(1).cloned() {
let count_rows = table.shape().0;
table
.get_config_mut()
.set_horizontal_line(count_rows - 1, line);
} }
} }
} }
fn table_trim_columns( fn table_trim_columns(
table: &mut tabled::Table<Data>, table: &mut Table,
widths: Vec<usize>,
termwidth: usize, termwidth: usize,
trim_strategy: &TrimStrategy, trim_strategy: &TrimStrategy,
) { ) {
table.with(TrimStrategyModifier::new(termwidth, trim_strategy)); match trim_strategy {
} TrimStrategy::Wrap { try_to_keep_words } => {
let mut wrap = Width::wrap(termwidth).priority::<PriorityMax>();
if *try_to_keep_words {
wrap = wrap.keep_words();
}
pub struct TrimStrategyModifier<'a> { table.with(Settings::new(SetDimensions(widths), wrap));
termwidth: usize, }
trim_strategy: &'a TrimStrategy, TrimStrategy::Truncate { suffix } => {
} let mut truncate = Width::truncate(termwidth).priority::<PriorityMax>();
if let Some(suffix) = suffix {
truncate = truncate.suffix(suffix).suffix_try_color(true);
}
impl<'a> TrimStrategyModifier<'a> { table.with(Settings::new(SetDimensions(widths), truncate));
pub fn new(termwidth: usize, trim_strategy: &'a TrimStrategy) -> Self {
Self {
termwidth,
trim_strategy,
} }
} }
} }
impl<R> tabled::TableOption<R> for TrimStrategyModifier<'_> fn maybe_truncate_columns(data: &mut Data, theme: &TableTheme, termwidth: usize) -> Vec<usize> {
where
R: Records + RecordsMut<String>,
{
fn change(&mut self, table: &mut tabled::Table<R>) {
match self.trim_strategy {
TrimStrategy::Wrap { try_to_keep_words } => {
let mut w = Width::wrap(self.termwidth).priority::<PriorityMax>();
if *try_to_keep_words {
w = w.keep_words();
}
w.change(table)
}
TrimStrategy::Truncate { suffix } => {
let mut w = Width::truncate(self.termwidth).priority::<PriorityMax>();
if let Some(suffix) = suffix {
w = w.suffix(suffix).suffix_try_color(true);
}
w.change(table);
}
};
}
}
fn maybe_truncate_columns(data: &mut Data, theme: &TableTheme, termwidth: usize) -> bool {
const TERMWIDTH_THRESHOLD: usize = 120; const TERMWIDTH_THRESHOLD: usize = 120;
if data.count_columns() == 0 {
return true;
}
let truncate = if termwidth > TERMWIDTH_THRESHOLD { let truncate = if termwidth > TERMWIDTH_THRESHOLD {
truncate_columns_by_columns truncate_columns_by_columns
} else { } else {
@ -403,34 +441,27 @@ fn maybe_truncate_columns(data: &mut Data, theme: &TableTheme, termwidth: usize)
} }
// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE. // VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE.
fn truncate_columns_by_content(data: &mut Data, theme: &TableTheme, termwidth: usize) -> bool { fn truncate_columns_by_content(
data: &mut Data,
theme: &TableTheme,
termwidth: usize,
) -> Vec<usize> {
const MIN_ACCEPTABLE_WIDTH: usize = 3; const MIN_ACCEPTABLE_WIDTH: usize = 3;
const TRAILING_COLUMN_WIDTH: usize = 5; const TRAILING_COLUMN_WIDTH: usize = 5;
const TRAILING_COLUMN_STR: &str = "...";
let config; let config = get_config(theme, false, None);
let total; let mut widths = build_width(&*data);
{ let total_width = get_total_width2(&widths, &config);
let mut table = Builder::custom(&*data).build(); if total_width <= termwidth {
load_theme(&mut table, theme, false, false, None); return widths;
total = table.total_width();
config = table.get_config().clone();
} }
if total <= termwidth {
return false;
}
let mut width_ctrl = WidthEstimator::default();
width_ctrl.estimate(&*data, &config);
let widths = Vec::from(width_ctrl);
let borders = config.get_borders(); let borders = config.get_borders();
let vertical_border_i = borders.has_vertical() as usize; let vertical_border_i = borders.has_vertical() as usize;
let mut width = borders.has_left() as usize + borders.has_right() as usize; let mut width = borders.has_left() as usize + borders.has_right() as usize;
let mut truncate_pos = 0; let mut truncate_pos = 0;
for column_width in widths { for column_width in &widths {
width += column_width; width += column_width;
width += vertical_border_i; width += vertical_border_i;
@ -451,14 +482,15 @@ fn truncate_columns_by_content(data: &mut Data, theme: &TableTheme, termwidth: u
// we don't need any truncation then (is it possible?) // we don't need any truncation then (is it possible?)
if truncate_pos == data.count_columns() { if truncate_pos == data.count_columns() {
return false; return widths;
} }
if truncate_pos == 0 { if truncate_pos == 0 {
return true; return vec![];
} }
data.truncate(truncate_pos); truncate_columns(data, truncate_pos);
widths.truncate(truncate_pos);
// Append columns with a trailing column // Append columns with a trailing column
@ -471,56 +503,50 @@ fn truncate_columns_by_content(data: &mut Data, theme: &TableTheme, termwidth: u
let can_be_squeezed = diff > TRAILING_COLUMN_WIDTH + vertical_border_i; let can_be_squeezed = diff > TRAILING_COLUMN_WIDTH + vertical_border_i;
if can_be_squeezed { if can_be_squeezed {
let cell = Table::create_cell(String::from(TRAILING_COLUMN_STR), TextStyle::default()); push_empty_column(data);
data.push(cell); widths.push(3 + 2);
} else { } else {
if data.count_columns() == 1 { if data.count_columns() == 1 {
return true; return vec![];
} }
data.truncate(data.count_columns() - 1); truncate_columns(data, data.count_columns() - 1);
push_empty_column(data);
let cell = Table::create_cell(String::from(TRAILING_COLUMN_STR), TextStyle::default()); widths.pop();
data.push(cell); widths.push(3 + 2);
} }
false widths
} }
// VERSION where we are showing AS MANY COLUMNS AS POSSIBLE but as a side affect they MIGHT CONTAIN AS LITTLE CONTENT AS POSSIBLE // VERSION where we are showing AS MANY COLUMNS AS POSSIBLE but as a side affect they MIGHT CONTAIN AS LITTLE CONTENT AS POSSIBLE
fn truncate_columns_by_columns(data: &mut Data, theme: &TableTheme, termwidth: usize) -> bool { fn truncate_columns_by_columns(
data: &mut Data,
theme: &TableTheme,
termwidth: usize,
) -> Vec<usize> {
const ACCEPTABLE_WIDTH: usize = 10 + 2; const ACCEPTABLE_WIDTH: usize = 10 + 2;
const TRAILING_COLUMN_WIDTH: usize = 3 + 2; const TRAILING_COLUMN_WIDTH: usize = 3 + 2;
const TRAILING_COLUMN_STR: &str = "...";
let config; let config = get_config(theme, false, None);
let total; let mut widths = build_width(&*data);
{ let total_width = get_total_width2(&widths, &config);
let mut table = Builder::custom(&*data).build(); if total_width <= termwidth {
load_theme(&mut table, theme, false, false, None); return widths;
total = table.total_width();
config = table.get_config().clone();
} }
if total <= termwidth {
return false;
}
let mut width_ctrl = WidthEstimator::default();
width_ctrl.estimate(&*data, &config);
let widths = Vec::from(width_ctrl);
let widths_total = widths.iter().sum::<usize>(); let widths_total = widths.iter().sum::<usize>();
let min_widths = widths let min_widths = widths
.iter() .iter()
.map(|w| min(*w, ACCEPTABLE_WIDTH)) .map(|w| min(*w, ACCEPTABLE_WIDTH))
.sum::<usize>(); .sum::<usize>();
let mut min_total = total - widths_total + min_widths; let mut min_total = total_width - widths_total + min_widths;
if min_total <= termwidth { if min_total <= termwidth {
return false; return widths;
} }
// todo: simplify the loop
let mut i = 0; let mut i = 0;
while data.count_columns() > 0 { while data.count_columns() > 0 {
i += 1; i += 1;
@ -539,28 +565,29 @@ fn truncate_columns_by_columns(data: &mut Data, theme: &TableTheme, termwidth: u
} }
if i + 1 == data.count_columns() { if i + 1 == data.count_columns() {
return true; return vec![];
} }
data.truncate(data.count_columns() - i); truncate_columns(data, data.count_columns() - i);
widths.pop();
// Append columns with a trailing column // Append columns with a trailing column
let diff = termwidth - min_total; let diff = termwidth - min_total;
if diff > TRAILING_COLUMN_WIDTH { if diff > TRAILING_COLUMN_WIDTH {
let cell = Table::create_cell(TRAILING_COLUMN_STR, TextStyle::default()); push_empty_column(data);
data.push(cell); widths.push(3 + 2);
} else { } else {
if data.count_columns() == 1 { if data.count_columns() == 1 {
return true; return vec![];
} }
data.truncate(data.count_columns() - 1); truncate_columns(data, data.count_columns() - 1);
push_empty_column(data);
let cell = Table::create_cell(TRAILING_COLUMN_STR, TextStyle::default()); widths.pop();
data.push(cell); widths.push(3 + 2);
} }
false widths
} }
/// The same as [`tabled::peaker::PriorityMax`] but prioritizes left columns first in case of equal width. /// The same as [`tabled::peaker::PriorityMax`] but prioritizes left columns first in case of equal width.
@ -577,3 +604,95 @@ impl Peaker for PriorityMax {
col.filter(|&col| widths[col] != 0) col.filter(|&col| widths[col] != 0)
} }
} }
fn get_total_width2(widths: &[usize], cfg: &ColoredConfig) -> usize {
let total = widths.iter().sum::<usize>();
let countv = cfg.count_vertical(widths.len());
let margin = cfg.get_margin();
total + countv + margin.left.size + margin.right.size
}
fn get_config(theme: &TableTheme, with_header: bool, color: Option<Style>) -> ColoredConfig {
let mut table = Table::new([[""]]);
load_theme(&mut table, theme, false, with_header, color);
table.get_config().clone()
}
fn push_empty_column(data: &mut Data) {
let records = std::mem::take(data);
let mut inner: Vec<Vec<_>> = records.into();
let empty_cell = CellInfo::new(String::from("..."));
for row in &mut inner {
row.push(empty_cell.clone());
}
*data = VecRecords::new(inner);
}
fn duplicate_row(data: &mut Data, row: usize) {
let records = std::mem::take(data);
let mut inner: Vec<Vec<_>> = records.into();
let duplicate = inner[row].clone();
inner.push(duplicate);
*data = VecRecords::new(inner);
}
fn truncate_columns(data: &mut Data, count: usize) {
let records = std::mem::take(data);
let mut inner: Vec<Vec<_>> = records.into();
for row in &mut inner {
row.truncate(count);
}
*data = VecRecords::new(inner);
}
fn convert_alignment(alignment: nu_color_config::Alignment) -> AlignmentHorizontal {
match alignment {
nu_color_config::Alignment::Center => AlignmentHorizontal::Center,
nu_color_config::Alignment::Left => AlignmentHorizontal::Left,
nu_color_config::Alignment::Right => AlignmentHorizontal::Right,
}
}
struct SetAlignment(AlignmentHorizontal, Entity);
impl<R, D> TableOption<R, D, ColoredConfig> for SetAlignment {
fn change(self, _: &mut R, cfg: &mut ColoredConfig, _: &mut D) {
cfg.set_alignment_horizontal(self.1, self.0);
}
}
fn convert_style(style: Style) -> Color {
Color::new(style.prefix().to_string(), style.suffix().to_string())
}
struct SetDimensions(Vec<usize>);
impl<R> TableOption<R, CompleteDimensionVecRecords<'_>, ColoredConfig> for SetDimensions {
fn change(self, _: &mut R, _: &mut ColoredConfig, dims: &mut CompleteDimensionVecRecords<'_>) {
dims.set_widths(self.0);
}
}
// it assumes no spans is used.
fn build_width(records: &VecRecords<CellInfo<String>>) -> Vec<usize> {
use tabled::grid::records::vec_records::Cell;
const PAD: usize = 2;
let count_columns = records.count_columns();
let mut widths = vec![0; count_columns];
for columns in records.iter_rows() {
for (col, cell) in columns.iter().enumerate() {
let width = Cell::width(cell) + PAD;
widths[col] = std::cmp::max(widths[col], width);
}
}
widths
}

View file

@ -1,20 +1,17 @@
use tabled::{ use tabled::settings::style::{HorizontalLine, Line, RawStyle, Style};
style::RawStyle,
style::{HorizontalLine, Line, Style},
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TableTheme { pub struct TableTheme {
pub(crate) theme: RawStyle, theme: RawStyle,
full_theme: RawStyle,
has_inner: bool, has_inner: bool,
full_theme: Option<RawStyle>,
} }
impl TableTheme { impl TableTheme {
pub fn basic() -> TableTheme { pub fn basic() -> TableTheme {
Self { Self {
theme: Style::ascii().into(), theme: Style::ascii().into(),
full_theme: None, full_theme: Style::ascii().into(),
has_inner: true, has_inner: true,
} }
} }
@ -22,84 +19,87 @@ impl TableTheme {
pub fn thin() -> TableTheme { pub fn thin() -> TableTheme {
Self { Self {
theme: Style::modern().into(), theme: Style::modern().into(),
full_theme: None, full_theme: Style::modern().into(),
has_inner: true, has_inner: true,
} }
} }
pub fn light() -> TableTheme { pub fn light() -> TableTheme {
let theme = Style::blank()
.horizontals([HorizontalLine::new(
1,
Line::new(Some('─'), Some('─'), None, None),
)])
.into();
Self { Self {
theme: Style::blank() theme,
.horizontals([HorizontalLine::new( full_theme: Style::modern().into(),
1,
Line::new(Some('─'), Some('─'), None, None),
)])
.into(),
full_theme: Some(Style::modern().into()),
has_inner: true, has_inner: true,
} }
} }
pub fn compact() -> TableTheme { pub fn compact() -> TableTheme {
let theme = Style::modern()
.remove_left()
.remove_right()
.remove_horizontal()
.horizontals([HorizontalLine::new(1, Style::modern().get_horizontal())
.left(None)
.right(None)])
.into();
Self { Self {
theme: Style::modern() theme,
.off_left() full_theme: Style::modern().into(),
.off_right()
.off_horizontal()
.horizontals([HorizontalLine::new(1, Style::modern().get_horizontal())
.left(None)
.right(None)])
.into(),
full_theme: Some(Style::modern().into()),
has_inner: true, has_inner: true,
} }
} }
pub fn with_love() -> TableTheme { pub fn with_love() -> TableTheme {
let theme = Style::empty()
.top('❤')
.bottom('❤')
.vertical('❤')
.horizontals([HorizontalLine::new(
1,
Line::new(Some('❤'), Some('❤'), None, None),
)]);
let full_theme = Style::empty()
.top('❤')
.bottom('❤')
.vertical('❤')
.horizontal('❤')
.left('❤')
.right('❤')
.intersection_top('❤')
.corner_top_left('❤')
.corner_top_right('❤')
.intersection_bottom('❤')
.corner_bottom_left('❤')
.corner_bottom_right('❤')
.intersection_right('❤')
.intersection_left('❤')
.intersection('❤');
Self { Self {
theme: Style::empty() theme: theme.into(),
.top('❤') full_theme: full_theme.into(),
.bottom('❤')
.vertical('❤')
.horizontals([HorizontalLine::new(
1,
Line::new(Some('❤'), Some('❤'), None, None),
)])
.into(),
full_theme: Some(
Style::empty()
.top('❤')
.bottom('❤')
.vertical('❤')
.horizontal('❤')
.left('❤')
.right('❤')
.top_intersection('❤')
.top_left_corner('❤')
.top_right_corner('❤')
.bottom_intersection('❤')
.bottom_left_corner('❤')
.bottom_right_corner('❤')
.right_intersection('❤')
.left_intersection('❤')
.inner_intersection('❤')
.into(),
),
has_inner: true, has_inner: true,
} }
} }
pub fn compact_double() -> TableTheme { pub fn compact_double() -> TableTheme {
let theme = Style::extended()
.remove_left()
.remove_right()
.remove_horizontal()
.horizontals([HorizontalLine::new(1, Style::extended().get_horizontal())
.left(None)
.right(None)])
.into();
Self { Self {
theme: Style::extended() theme,
.off_left() full_theme: Style::extended().into(),
.off_right()
.off_horizontal()
.horizontals([HorizontalLine::new(1, Style::extended().get_horizontal())
.left(None)
.right(None)])
.into(),
full_theme: Some(Style::extended().into()),
has_inner: true, has_inner: true,
} }
} }
@ -107,74 +107,53 @@ impl TableTheme {
pub fn rounded() -> TableTheme { pub fn rounded() -> TableTheme {
Self { Self {
theme: Style::rounded().into(), theme: Style::rounded().into(),
full_theme: Some( full_theme: Style::modern()
Style::modern() .corner_top_left('╭')
.top_left_corner('╭') .corner_top_right('╮')
.top_right_corner('╮') .corner_bottom_left('╰')
.bottom_left_corner('╰') .corner_bottom_right('╯')
.bottom_right_corner('╯') .into(),
.into(),
),
has_inner: true, has_inner: true,
} }
} }
pub fn reinforced() -> TableTheme { pub fn reinforced() -> TableTheme {
let full_theme = Style::modern()
.corner_top_left('┏')
.corner_top_right('┓')
.corner_bottom_left('┗')
.corner_bottom_right('┛');
Self { Self {
theme: Style::modern() theme: full_theme.clone().remove_horizontal().into(),
.top_left_corner('┏') full_theme: full_theme.into(),
.top_right_corner('┓')
.bottom_left_corner('┗')
.bottom_right_corner('┛')
.off_horizontal()
.into(),
full_theme: Some(
Style::modern()
.top_left_corner('┏')
.top_right_corner('┓')
.bottom_left_corner('┗')
.bottom_right_corner('┛')
.into(),
),
has_inner: true, has_inner: true,
} }
} }
pub fn heavy() -> TableTheme { pub fn heavy() -> TableTheme {
let theme = Style::empty()
.top('━')
.bottom('━')
.vertical('┃')
.left('┃')
.right('┃')
.intersection_top('┳')
.intersection_bottom('┻')
.corner_top_left('┏')
.corner_top_right('┓')
.corner_bottom_left('┗')
.corner_bottom_right('┛')
.horizontals([HorizontalLine::new(1, Line::full('━', '╋', '┣', '┫'))]);
let full_theme = theme
.clone()
.remove_horizontals()
.horizontal('━')
.intersection_left('┣')
.intersection_right('┫')
.intersection('╋');
Self { Self {
theme: Style::empty() theme: theme.into(),
.top('━') full_theme: full_theme.into(),
.bottom('━')
.vertical('┃')
.left('┃')
.right('┃')
.top_intersection('┳')
.bottom_intersection('┻')
.top_left_corner('┏')
.top_right_corner('┓')
.bottom_left_corner('┗')
.bottom_right_corner('┛')
.horizontals([HorizontalLine::new(1, Line::full('━', '╋', '┣', '┫'))])
.into(),
full_theme: Some(
Style::modern()
.top('━')
.bottom('━')
.vertical('┃')
.left('┃')
.right('┃')
.top_intersection('┳')
.bottom_intersection('┻')
.top_left_corner('┏')
.top_right_corner('┓')
.bottom_left_corner('┗')
.bottom_right_corner('┛')
.horizontal('━')
.left_intersection('┣')
.right_intersection('┫')
.inner_intersection('╋')
.into(),
),
has_inner: true, has_inner: true,
} }
} }
@ -182,7 +161,7 @@ impl TableTheme {
pub fn none() -> TableTheme { pub fn none() -> TableTheme {
Self { Self {
theme: Style::blank().into(), theme: Style::blank().into(),
full_theme: None, full_theme: Style::blank().into(),
has_inner: true, has_inner: true,
} }
} }
@ -212,7 +191,11 @@ impl TableTheme {
self.has_inner self.has_inner
} }
pub fn into_full(&self) -> Option<RawStyle> { pub fn get_theme_full(&self) -> RawStyle {
self.full_theme.clone() self.full_theme.clone()
} }
pub fn get_theme(&self) -> RawStyle {
self.theme.clone()
}
} }

View file

@ -0,0 +1,80 @@
use nu_color_config::StyleComputer;
use nu_protocol::{Config, Span, Value};
use crate::UnstructuredTable;
use super::{
clean_charset, general::BuildConfig, get_index_style, load_theme_from_config,
value_to_styled_string, StringResult,
};
pub struct CollapsedTable;
impl CollapsedTable {
pub fn build(value: Value, opts: BuildConfig<'_>) -> StringResult {
collapsed_table(value, opts.config, opts.term_width, opts.style_computer)
}
}
fn collapsed_table(
mut value: Value,
config: &Config,
term_width: usize,
style_computer: &StyleComputer,
) -> StringResult {
colorize_value(&mut value, config, style_computer);
let theme = load_theme_from_config(config);
let mut table = UnstructuredTable::new(value, config);
let is_empty = table.truncate(&theme, term_width);
if is_empty {
return Ok(None);
}
let table = table.draw(style_computer, &theme);
Ok(Some(table))
}
fn colorize_value(value: &mut Value, config: &Config, style_computer: &StyleComputer) {
match value {
Value::Record { cols, vals, .. } => {
for val in vals {
colorize_value(val, config, style_computer);
}
let style = get_index_style(style_computer);
if let Some(color) = style.color_style {
for header in cols {
*header = color.paint(header.to_owned()).to_string();
}
}
}
Value::List { vals, .. } => {
for val in vals {
colorize_value(val, config, style_computer);
}
}
value => {
let (text, style) = value_to_styled_string(value, config, style_computer);
let is_string = matches!(value, Value::String { .. });
if is_string {
let mut text = clean_charset(&text);
if let Some(color) = style.color_style {
text = color.paint(text).to_string();
}
let span = value.span().unwrap_or(Span::unknown());
*value = Value::string(text, span);
return;
}
if let Some(color) = style.color_style {
let text = color.paint(text).to_string();
let span = value.span().unwrap_or(Span::unknown());
*value = Value::string(text, span);
}
}
}
}

View file

@ -0,0 +1,601 @@
use nu_color_config::{Alignment, StyleComputer, TextStyle};
use nu_engine::column::get_columns;
use nu_protocol::{ast::PathMember, Config, Span, TableIndexMode, Value};
use std::collections::HashMap;
use std::sync::Arc;
use std::{cmp::max, sync::atomic::AtomicBool};
use crate::{string_width, Cell, NuTable};
use super::{clean_charset, value_to_clean_styled_string};
use super::{
create_table_config, error_sign, general::BuildConfig, get_header_style, get_index_style,
load_theme_from_config, set_data_styles, value_to_styled_string, wrap_text, NuText,
StringResult, TableOutput, TableResult, INDEX_COLUMN_NAME,
};
#[derive(Debug, Clone)]
pub struct ExpandedTable {
expand_limit: Option<usize>,
flatten: bool,
flatten_sep: String,
}
impl ExpandedTable {
pub fn new(expand_limit: Option<usize>, flatten: bool, flatten_sep: String) -> Self {
Self {
expand_limit,
flatten,
flatten_sep,
}
}
pub fn build_value(&self, item: &Value, opts: BuildConfig<'_>) -> NuText {
let opts = Options {
ctrlc: opts.ctrlc,
config: opts.config,
style_computer: opts.style_computer,
available_width: opts.term_width,
span: opts.span,
format: self.clone(),
};
expanded_table_entry2(item, opts)
}
pub fn build_map(
&self,
cols: &[String],
vals: &[Value],
opts: BuildConfig<'_>,
) -> StringResult {
let opts = Options {
ctrlc: opts.ctrlc,
config: opts.config,
style_computer: opts.style_computer,
available_width: opts.term_width,
span: opts.span,
format: self.clone(),
};
expanded_table_kv(cols, vals, opts)
}
pub fn build_list(&self, vals: &[Value], opts: BuildConfig<'_>) -> StringResult {
let opts1 = Options {
ctrlc: opts.ctrlc,
config: opts.config,
style_computer: opts.style_computer,
available_width: opts.term_width,
span: opts.span,
format: self.clone(),
};
let out = match expanded_table_list(vals, 0, opts1)? {
Some(out) => out,
None => return Ok(None),
};
maybe_expand_table(out, opts.term_width, opts.config, opts.style_computer)
}
}
#[derive(Debug, Clone)]
struct Options<'a> {
ctrlc: Option<Arc<AtomicBool>>,
config: &'a Config,
style_computer: &'a StyleComputer<'a>,
available_width: usize,
format: ExpandedTable,
span: Span,
}
fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> TableResult {
const PADDING_SPACE: usize = 2;
const SPLIT_LINE_SPACE: usize = 1;
const ADDITIONAL_CELL_SPACE: usize = PADDING_SPACE + SPLIT_LINE_SPACE;
const MIN_CELL_CONTENT_WIDTH: usize = 1;
const TRUNCATE_CONTENT_WIDTH: usize = 3;
const TRUNCATE_CELL_WIDTH: usize = TRUNCATE_CONTENT_WIDTH + PADDING_SPACE;
if input.is_empty() {
return Ok(None);
}
// 2 - split lines
let mut available_width = opts
.available_width
.saturating_sub(SPLIT_LINE_SPACE + SPLIT_LINE_SPACE);
if available_width < MIN_CELL_CONTENT_WIDTH {
return Ok(None);
}
let headers = get_columns(input);
let with_index = match opts.config.table_index_mode {
TableIndexMode::Always => true,
TableIndexMode::Never => false,
TableIndexMode::Auto => headers.iter().any(|header| header == INDEX_COLUMN_NAME),
};
// The header with the INDEX is removed from the table headers since
// it is added to the natural table index
let headers: Vec<_> = headers
.into_iter()
.filter(|header| header != INDEX_COLUMN_NAME)
.collect();
let with_header = !headers.is_empty();
let mut data = vec![vec![]; input.len() + with_header as usize];
let mut data_styles = HashMap::new();
if with_index {
if with_header {
data[0].push(Cell::exact(String::from("#"), 1, vec![]));
}
for (row, item) in input.iter().enumerate() {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
return Ok(None);
}
if let Value::Error { error } = item {
return Err(*error.clone());
}
let index = row + row_offset;
let text = matches!(item, Value::Record { .. })
.then(|| lookup_index_value(item, opts.config).unwrap_or_else(|| index.to_string()))
.unwrap_or_else(|| index.to_string());
let value = Cell::new(text);
let row = row + with_header as usize;
data[row].push(value);
}
let column_width = string_width(data[data.len() - 1][0].as_ref());
if column_width + ADDITIONAL_CELL_SPACE > available_width {
available_width = 0;
} else {
available_width -= column_width + ADDITIONAL_CELL_SPACE;
}
}
if !with_header {
if available_width > ADDITIONAL_CELL_SPACE {
available_width -= PADDING_SPACE;
} else {
// it means we have no space left for actual content;
// which means there's no point in index itself if it was even used.
// so we do not print it.
return Ok(None);
}
for (row, item) in input.iter().enumerate() {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
return Ok(None);
}
if let Value::Error { error } = item {
return Err(*error.clone());
}
let mut oopts = opts.clone();
oopts.available_width = available_width;
let (mut text, style) = expanded_table_entry2(item, oopts.clone());
let value_width = string_width(&text);
if value_width > available_width {
// it must only happen when a string is produced, so we can safely wrap it.
// (it might be string table representation as well) (I guess I mean default { table ...} { list ...})
//
// todo: Maybe convert_to_table2_entry could do for strings to not mess caller code?
text = wrap_text(&text, available_width, opts.config);
}
let value = Cell::new(text);
data[row].push(value);
data_styles.insert((row, with_index as usize), style);
}
let mut table = NuTable::from(data);
table.set_index_style(get_index_style(opts.style_computer));
set_data_styles(&mut table, data_styles);
return Ok(Some(TableOutput::new(table, false, with_index)));
}
if !headers.is_empty() {
let mut pad_space = PADDING_SPACE;
if headers.len() > 1 {
pad_space += SPLIT_LINE_SPACE;
}
if available_width < pad_space {
// there's no space for actual data so we don't return index if it's present.
// (also see the comment after the loop)
return Ok(None);
}
}
let count_columns = headers.len();
let mut widths = Vec::new();
let mut truncate = false;
let mut rendered_column = 0;
for (col, header) in headers.into_iter().enumerate() {
let is_last_column = col + 1 == count_columns;
let mut pad_space = PADDING_SPACE;
if !is_last_column {
pad_space += SPLIT_LINE_SPACE;
}
let mut available = available_width - pad_space;
let mut column_width = string_width(&header);
if !is_last_column {
// we need to make sure that we have a space for a next column if we use available width
// so we might need to decrease a bit it.
// we consider a header width be a minimum width
let pad_space = PADDING_SPACE + TRUNCATE_CONTENT_WIDTH;
if available > pad_space {
// In we have no space for a next column,
// We consider showing something better then nothing,
// So we try to decrease the width to show at least a truncution column
available -= pad_space;
} else {
truncate = true;
break;
}
if available < column_width {
truncate = true;
break;
}
}
for (row, item) in input.iter().enumerate() {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
return Ok(None);
}
if let Value::Error { error } = item {
return Err(*error.clone());
}
let mut oopts = opts.clone();
oopts.available_width = available;
let (mut text, style) = expanded_table_entry(item, header.as_str(), oopts);
let mut value_width = string_width(&text);
if value_width > available {
// it must only happen when a string is produced, so we can safely wrap it.
// (it might be string table representation as well)
text = wrap_text(&text, available, opts.config);
value_width = available;
}
column_width = max(column_width, value_width);
let value = Cell::new(text);
data[row + 1].push(value);
data_styles.insert((row + 1, col + with_index as usize), style);
}
let head_cell = Cell::new(header);
data[0].push(head_cell);
if column_width > available {
// remove the column we just inserted
for row in &mut data {
row.pop();
}
truncate = true;
break;
}
widths.push(column_width);
available_width -= pad_space + column_width;
rendered_column += 1;
}
if truncate && rendered_column == 0 {
// it means that no actual data was rendered, there might be only index present,
// so there's no point in rendering the table.
//
// It's actually quite important in case it's called recursively,
// cause we will back up to the basic table view as a string e.g. '[table 123 columns]'.
//
// But potentially if its reached as a 1st called function we might would love to see the index.
return Ok(None);
}
if truncate {
if available_width < TRUNCATE_CELL_WIDTH {
// back up by removing last column.
// it's LIKELY that removing only 1 column will leave us enough space for a shift column.
while let Some(width) = widths.pop() {
for row in &mut data {
row.pop();
}
available_width += width + PADDING_SPACE;
if !widths.is_empty() {
available_width += SPLIT_LINE_SPACE;
}
if available_width > TRUNCATE_CELL_WIDTH {
break;
}
}
}
// this must be a RARE case or even NEVER happen,
// but we do check it just in case.
if available_width < TRUNCATE_CELL_WIDTH {
return Ok(None);
}
let is_last_column = widths.len() == count_columns;
if !is_last_column {
let shift = Cell::exact(String::from("..."), 3, vec![]);
for row in &mut data {
row.push(shift.clone());
}
widths.push(3);
}
}
let mut table = NuTable::from(data);
table.set_index_style(get_index_style(opts.style_computer));
table.set_header_style(get_header_style(opts.style_computer));
set_data_styles(&mut table, data_styles);
Ok(Some(TableOutput::new(table, true, with_index)))
}
fn expanded_table_kv(cols: &[String], vals: &[Value], opts: Options<'_>) -> StringResult {
let theme = load_theme_from_config(opts.config);
let key_width = cols.iter().map(|col| string_width(col)).max().unwrap_or(0);
let count_borders =
theme.has_inner() as usize + theme.has_right() as usize + theme.has_left() as usize;
let padding = 2;
if key_width + count_borders + padding + padding > opts.available_width {
return Ok(None);
}
let value_width = opts.available_width - key_width - count_borders - padding - padding;
let mut data = Vec::with_capacity(cols.len());
for (key, value) in cols.iter().zip(vals) {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
return Ok(None);
}
let is_limited = matches!(opts.format.expand_limit, Some(0));
let mut is_expanded = false;
let value = if is_limited {
let (text, _) = value_to_styled_string(value, opts.config, opts.style_computer);
clean_charset(&text)
} else {
match value {
Value::List { vals, span } => {
let mut oopts = dive_options(&opts, *span);
oopts.available_width = value_width;
let table = expanded_table_list(vals, 0, oopts)?;
match table {
Some(out) => {
is_expanded = true;
let table_config =
create_table_config(opts.config, opts.style_computer, &out);
let value = out.table.draw(table_config, value_width);
match value {
Some(result) => result,
None => return Ok(None),
}
}
None => {
// it means that the list is empty
let text =
value_to_styled_string(value, opts.config, opts.style_computer).0;
wrap_text(&text, value_width, opts.config)
}
}
}
Value::Record { cols, vals, span } => {
if cols.is_empty() {
// Like list case return styled string instead of empty value
let text =
value_to_styled_string(value, opts.config, opts.style_computer).0;
wrap_text(&text, value_width, opts.config)
} else {
let mut oopts = dive_options(&opts, *span);
oopts.available_width = value_width;
let result = expanded_table_kv(cols, vals, oopts)?;
match result {
Some(result) => {
is_expanded = true;
result
}
None => {
let failed_value =
value_to_styled_string(value, opts.config, opts.style_computer);
wrap_text(&failed_value.0, value_width, opts.config)
}
}
}
}
val => {
let text =
value_to_clean_styled_string(val, opts.config, opts.style_computer).0;
wrap_text(&text, value_width, opts.config)
}
}
};
// we want to have a key being aligned to 2nd line,
// we could use Padding for it but,
// the easiest way to do so is just push a new_line char before
let mut key = key.to_owned();
if !key.is_empty() && is_expanded && theme.has_top_line() {
key.insert(0, '\n');
}
let key = Cell::new(key);
let val = Cell::new(value);
let row = vec![key, val];
data.push(row);
}
let mut table = NuTable::from(data);
let keys_style = get_index_style(opts.style_computer).alignment(Alignment::Left);
table.set_index_style(keys_style);
let out = TableOutput::new(table, false, true);
maybe_expand_table(out, opts.available_width, opts.config, opts.style_computer)
}
fn expanded_table_entry(item: &Value, header: &str, opts: Options<'_>) -> NuText {
match item {
Value::Record { .. } => {
let val = header.to_owned();
let path = PathMember::String {
val,
span: opts.span,
optional: false,
};
let val = item.clone().follow_cell_path(&[path], false);
match val {
Ok(val) => expanded_table_entry2(&val, opts),
Err(_) => error_sign(opts.style_computer),
}
}
_ => expanded_table_entry2(item, opts),
}
}
fn expanded_table_entry2(item: &Value, opts: Options<'_>) -> NuText {
let is_limit_reached = matches!(opts.format.expand_limit, Some(0));
if is_limit_reached {
return value_to_clean_styled_string(item, opts.config, opts.style_computer);
}
match &item {
Value::Record { cols, vals, span } => {
if cols.is_empty() && vals.is_empty() {
return value_to_styled_string(item, opts.config, opts.style_computer);
}
// we verify what is the structure of a Record cause it might represent
let oopts = dive_options(&opts, *span);
let table = expanded_table_kv(cols, vals, oopts);
match table {
Ok(Some(table)) => (table, TextStyle::default()),
_ => value_to_styled_string(item, opts.config, opts.style_computer),
}
}
Value::List { vals, span } => {
if opts.format.flatten && is_simple_list(vals) {
return value_list_to_string(
vals,
opts.config,
opts.style_computer,
&opts.format.flatten_sep,
);
}
let oopts = dive_options(&opts, *span);
let table = expanded_table_list(vals, 0, oopts);
let out = match table {
Ok(Some(out)) => out,
_ => return value_to_styled_string(item, opts.config, opts.style_computer),
};
let table_config = create_table_config(opts.config, opts.style_computer, &out);
let table = out.table.draw(table_config, usize::MAX);
match table {
Some(table) => (table, TextStyle::default()),
None => value_to_styled_string(item, opts.config, opts.style_computer),
}
}
_ => value_to_clean_styled_string(item, opts.config, opts.style_computer),
}
}
fn is_simple_list(vals: &[Value]) -> bool {
vals.iter()
.all(|v| !matches!(v, Value::Record { .. } | Value::List { .. }))
}
fn value_list_to_string(
vals: &[Value],
config: &Config,
style_computer: &StyleComputer,
flatten_sep: &str,
) -> NuText {
let mut buf = String::new();
for (i, value) in vals.iter().enumerate() {
if i > 0 {
buf.push_str(flatten_sep);
}
let (text, _) = value_to_clean_styled_string(value, config, style_computer);
buf.push_str(&text);
}
(buf, TextStyle::default())
}
fn dive_options<'a, 'b>(opts: &'a Options<'b>, span: Span) -> Options<'b> {
let mut opts = opts.clone();
opts.span = span;
if let Some(deep) = opts.format.expand_limit.as_mut() {
*deep -= 1
}
opts
}
fn lookup_index_value(item: &Value, config: &Config) -> Option<String> {
item.get_data_by_key(INDEX_COLUMN_NAME)
.map(|value| value.into_string("", config))
}
fn maybe_expand_table(
out: TableOutput,
term_width: usize,
config: &Config,
style_computer: &StyleComputer,
) -> StringResult {
let mut table_config = create_table_config(config, style_computer, &out);
let total_width = out.table.total_width(&table_config);
if total_width < term_width {
const EXPAND_THRESHOLD: f32 = 0.80;
let used_percent = total_width as f32 / term_width as f32;
let need_expansion = total_width < term_width && used_percent > EXPAND_THRESHOLD;
if need_expansion {
table_config = table_config.expand(true);
}
}
let output = out.table.draw(table_config, term_width);
Ok(output)
}

View file

@ -0,0 +1,254 @@
use nu_color_config::{StyleComputer, TextStyle};
use nu_engine::column::get_columns;
use nu_protocol::{ast::PathMember, Config, ShellError, Span, TableIndexMode, Value};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use crate::{Cell, NuTable};
use super::{
clean_charset, create_table_config, get_empty_style, get_header_style, get_index_style,
get_value_style, StringResult, TableOutput, TableResult, INDEX_COLUMN_NAME,
};
pub struct JustTable;
impl JustTable {
pub fn table(input: &[Value], row_offset: usize, opts: BuildConfig<'_>) -> StringResult {
let out = match table(input, row_offset, opts.clone())? {
Some(out) => out,
None => return Ok(None),
};
let table_config = create_table_config(opts.config, opts.style_computer, &out);
let table = out.table.draw(table_config, opts.term_width);
Ok(table)
}
pub fn kv_table(cols: &[String], vals: &[Value], opts: BuildConfig<'_>) -> StringResult {
kv_table(cols, vals, opts)
}
}
#[derive(Debug, Clone)]
pub struct BuildConfig<'a> {
pub(crate) ctrlc: Option<Arc<AtomicBool>>,
pub(crate) config: &'a Config,
pub(crate) style_computer: &'a StyleComputer<'a>,
pub(crate) span: Span,
pub(crate) term_width: usize,
}
impl<'a> BuildConfig<'a> {
pub fn new(
ctrlc: Option<Arc<AtomicBool>>,
config: &'a Config,
style_computer: &'a StyleComputer<'a>,
span: Span,
term_width: usize,
) -> Self {
Self {
ctrlc,
config,
style_computer,
span,
term_width,
}
}
pub fn span(&self) -> Span {
self.span
}
pub fn term_width(&self) -> usize {
self.term_width
}
pub fn config(&self) -> &Config {
self.config
}
pub fn style_computer(&self) -> &StyleComputer {
self.style_computer
}
pub fn ctrlc(&self) -> Option<&Arc<AtomicBool>> {
self.ctrlc.as_ref()
}
}
fn kv_table(cols: &[String], vals: &[Value], opts: BuildConfig<'_>) -> StringResult {
let mut data = vec![Vec::with_capacity(2); cols.len()];
for ((column, value), row) in cols.iter().zip(vals.iter()).zip(data.iter_mut()) {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
return Ok(None);
}
let is_string_value = matches!(value, Value::String { .. });
let mut value = value.into_abbreviated_string(opts.config);
if is_string_value {
value = clean_charset(&value);
}
let key = Cell::new(column.to_string());
let value = Cell::new(value);
row.push(key);
row.push(value);
}
let mut table = NuTable::from(data);
table.set_index_style(TextStyle::default_field());
let out = TableOutput::new(table, false, true);
let table_config = create_table_config(opts.config, opts.style_computer, &out);
let table = out.table.draw(table_config, opts.term_width);
Ok(table)
}
fn table(input: &[Value], row_offset: usize, opts: BuildConfig<'_>) -> TableResult {
if input.is_empty() {
return Ok(None);
}
let mut headers = get_columns(input);
let with_index = match opts.config.table_index_mode {
TableIndexMode::Always => true,
TableIndexMode::Never => false,
TableIndexMode::Auto => headers.iter().any(|header| header == INDEX_COLUMN_NAME),
};
let with_header = !headers.is_empty();
if !with_header {
let table = to_table_with_no_header(input, with_index, row_offset, opts)?;
let table = table.map(|table| TableOutput::new(table, false, with_index));
return Ok(table);
}
if with_header && with_index {
headers.insert(0, "#".into());
}
// The header with the INDEX is removed from the table headers since
// it is added to the natural table index
let headers: Vec<_> = headers
.into_iter()
.filter(|header| header != INDEX_COLUMN_NAME)
.collect();
let table = to_table_with_header(input, headers, with_index, row_offset, opts)?;
let table = table.map(|table| TableOutput::new(table, true, with_index));
Ok(table)
}
fn to_table_with_header(
input: &[Value],
headers: Vec<String>,
with_index: bool,
row_offset: usize,
opts: BuildConfig<'_>,
) -> Result<Option<NuTable>, ShellError> {
let count_rows = input.len() + 1;
let count_columns = headers.len();
let mut table = NuTable::new(count_rows, count_columns);
table.set_header_style(get_header_style(opts.style_computer));
table.set_index_style(get_index_style(opts.style_computer));
for (i, text) in headers.iter().enumerate() {
table.insert((0, i), text.to_owned());
}
for (row, item) in input.iter().enumerate() {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
return Ok(None);
}
if let Value::Error { error } = item {
return Err(*error.clone());
}
if with_index {
let text = get_table_row_index(item, opts.config, row, row_offset);
table.insert((row + 1, 0), text);
}
let skip_index = usize::from(with_index);
for (col, header) in headers.iter().enumerate().skip(skip_index) {
let (text, style) = match item {
Value::Record { .. } => {
let path = PathMember::String {
val: header.clone(),
span: Span::unknown(),
optional: false,
};
let value = item.clone().follow_cell_path(&[path], false);
match value {
Ok(value) => get_value_style(&value, opts.config, opts.style_computer),
Err(_) => get_empty_style(opts.style_computer),
}
}
value if matches!(value, Value::String { .. }) => {
let (text, style) = get_value_style(value, opts.config, opts.style_computer);
let text = clean_charset(&text);
(text, style)
}
value => get_value_style(value, opts.config, opts.style_computer),
};
table.insert((row + 1, col), text);
table.set_cell_style((row + 1, col), style);
}
}
Ok(Some(table))
}
fn to_table_with_no_header(
input: &[Value],
with_index: bool,
row_offset: usize,
opts: BuildConfig<'_>,
) -> Result<Option<NuTable>, ShellError> {
let mut table = NuTable::new(input.len(), with_index as usize + 1);
table.set_index_style(get_index_style(opts.style_computer));
for (row, item) in input.iter().enumerate() {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
return Ok(None);
}
if let Value::Error { error } = item {
return Err(*error.clone());
}
if with_index {
let text = get_table_row_index(item, opts.config, row, row_offset);
table.insert((row, 0), text);
}
let (mut text, style) = get_value_style(item, opts.config, opts.style_computer);
let is_string_value = matches!(item, Value::String { .. });
if is_string_value {
text = clean_charset(&text);
}
let pos = (row, with_index as usize);
table.insert(pos, text);
table.set_cell_style(pos, style);
}
Ok(Some(table))
}
fn get_table_row_index(item: &Value, config: &Config, row: usize, offset: usize) -> String {
match item {
Value::Record { .. } => item
.get_data_by_key(INDEX_COLUMN_NAME)
.map(|value| value.into_string("", config))
.unwrap_or_else(|| (row + offset).to_string()),
_ => (row + offset).to_string(),
}
}

View file

@ -0,0 +1,207 @@
mod collapse;
mod expanded;
mod general;
use nu_color_config::{Alignment, StyleComputer, TextStyle};
use nu_protocol::TrimStrategy;
use nu_protocol::{Config, FooterMode, ShellError, Span, Value};
use std::collections::HashMap;
use crate::{string_wrap, NuTable, TableConfig, TableTheme};
pub use collapse::CollapsedTable;
pub use expanded::ExpandedTable;
pub use general::{BuildConfig, JustTable};
pub type NuText = (String, TextStyle);
pub type TableResult = Result<Option<TableOutput>, ShellError>;
pub type StringResult = Result<Option<String>, ShellError>;
pub struct TableOutput {
pub table: NuTable,
pub with_header: bool,
pub with_index: bool,
}
impl TableOutput {
pub fn new(table: NuTable, with_header: bool, with_index: bool) -> Self {
Self {
table,
with_header,
with_index,
}
}
}
pub fn value_to_styled_string(val: &Value, cfg: &Config, style: &StyleComputer) -> NuText {
let float_precision = cfg.float_precision as usize;
let text = val.into_abbreviated_string(cfg);
make_styled_string(style, text, Some(val), float_precision)
}
pub fn value_to_clean_styled_string(val: &Value, cfg: &Config, style: &StyleComputer) -> NuText {
let (text, style) = value_to_styled_string(val, cfg, style);
let text = clean_charset(&text);
(text, style)
}
pub fn clean_charset(text: &str) -> String {
text.replace(['\r', '\t'], " ")
}
const INDEX_COLUMN_NAME: &str = "index";
fn error_sign(style_computer: &StyleComputer) -> (String, TextStyle) {
make_styled_string(style_computer, String::from(""), None, 0)
}
fn wrap_text(text: &str, width: usize, config: &Config) -> String {
string_wrap(text, width, is_cfg_trim_keep_words(config))
}
fn make_styled_string(
style_computer: &StyleComputer,
text: String,
value: Option<&Value>, // None represents table holes.
float_precision: usize,
) -> NuText {
match value {
Some(value) => {
match value {
Value::Float { .. } => {
// set dynamic precision from config
let precise_number = match convert_with_precision(&text, float_precision) {
Ok(num) => num,
Err(e) => e.to_string(),
};
(precise_number, style_computer.style_primitive(value))
}
_ => (text, style_computer.style_primitive(value)),
}
}
None => {
// Though holes are not the same as null, the closure for "empty" is passed a null anyway.
(
text,
TextStyle::with_style(
Alignment::Center,
style_computer.compute("empty", &Value::nothing(Span::unknown())),
),
)
}
}
}
fn convert_with_precision(val: &str, precision: usize) -> Result<String, ShellError> {
// vall will always be a f64 so convert it with precision formatting
let val_float = match val.trim().parse::<f64>() {
Ok(f) => f,
Err(e) => {
return Err(ShellError::GenericError(
format!("error converting string [{}] to f64", &val),
"".to_string(),
None,
Some(e.to_string()),
Vec::new(),
));
}
};
Ok(format!("{val_float:.precision$}"))
}
fn is_cfg_trim_keep_words(config: &Config) -> bool {
matches!(
config.trim_strategy,
TrimStrategy::Wrap {
try_to_keep_words: true
}
)
}
fn load_theme_from_config(config: &Config) -> TableTheme {
match config.table_mode.as_str() {
"basic" => TableTheme::basic(),
"thin" => TableTheme::thin(),
"light" => TableTheme::light(),
"compact" => TableTheme::compact(),
"with_love" => TableTheme::with_love(),
"compact_double" => TableTheme::compact_double(),
"rounded" => TableTheme::rounded(),
"reinforced" => TableTheme::reinforced(),
"heavy" => TableTheme::heavy(),
"none" => TableTheme::none(),
_ => TableTheme::rounded(),
}
}
fn create_table_config(config: &Config, comp: &StyleComputer, out: &TableOutput) -> TableConfig {
let theme = load_theme_from_config(config);
let footer = with_footer(config, out.with_header, out.table.count_rows());
let line_style = lookup_separator_color(comp);
let trim = config.trim_strategy.clone();
TableConfig::new()
.theme(theme)
.with_footer(footer)
.with_header(out.with_header)
.with_index(out.with_index)
.line_style(line_style)
.trim(trim)
}
fn lookup_separator_color(style_computer: &StyleComputer) -> nu_ansi_term::Style {
style_computer.compute("separator", &Value::nothing(Span::unknown()))
}
fn with_footer(config: &Config, with_header: bool, count_records: usize) -> bool {
with_header && need_footer(config, count_records as u64)
}
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)
}
fn set_data_styles(table: &mut NuTable, styles: HashMap<(usize, usize), TextStyle>) {
for (pos, style) in styles {
table.set_cell_style(pos, style);
}
}
fn get_header_style(style_computer: &StyleComputer) -> TextStyle {
TextStyle::with_style(
Alignment::Center,
style_computer.compute("header", &Value::string("", Span::unknown())),
)
}
fn get_index_style(style_computer: &StyleComputer) -> TextStyle {
TextStyle::with_style(
Alignment::Right,
style_computer.compute("row_index", &Value::string("", Span::unknown())),
)
}
fn get_value_style(value: &Value, config: &Config, style_computer: &StyleComputer) -> NuText {
match value {
// Float precision is required here.
Value::Float { val, .. } => (
format!("{:.prec$}", val, prec = config.float_precision as usize),
style_computer.style_primitive(value),
),
_ => (
value.into_abbreviated_string(config),
style_computer.style_primitive(value),
),
}
}
fn get_empty_style(style_computer: &StyleComputer) -> NuText {
(
String::from(""),
TextStyle::with_style(
Alignment::Right,
style_computer.compute("empty", &Value::nothing(Span::unknown())),
),
)
}

View file

@ -0,0 +1,339 @@
use std::collections::HashMap;
use nu_color_config::StyleComputer;
use nu_protocol::{Config, Span, Value};
use tabled::{
grid::{
color::{AnsiColor, StaticColor},
config::{AlignmentHorizontal, Borders, CompactMultilineConfig},
dimension::{DimensionPriority, PoolTableDimension},
},
settings::{style::RawStyle, Color, TableOption},
tables::{PoolTable, TableValue},
};
use crate::{string_width, string_wrap, TableTheme};
/// UnstructuredTable has a recursive table representation of nu_protocol::Value.
///
/// It doesn't support alignment and a proper width control.
pub struct UnstructuredTable {
value: TableValue,
}
impl UnstructuredTable {
pub fn new(value: Value, config: &Config) -> Self {
let value = convert_nu_value_to_table_value(value, config);
Self { value }
}
pub fn truncate(&mut self, theme: &TableTheme, width: usize) -> bool {
let mut available = width;
let has_vertical = theme.has_left();
if has_vertical {
available = available.saturating_sub(2);
}
truncate_table_value(&mut self.value, has_vertical, available).is_none()
}
pub fn draw(self, style_computer: &StyleComputer, theme: &TableTheme) -> String {
build_table(self.value, style_computer, theme)
}
}
fn build_table(val: TableValue, style_computer: &StyleComputer, theme: &TableTheme) -> String {
let mut table = PoolTable::from(val);
let mut theme = theme.get_theme_full();
theme.set_horizontals(HashMap::default());
table.with(SetRawStyle(theme));
table.with(SetAlignment(AlignmentHorizontal::Left));
table.with(PoolTableDimension::new(
DimensionPriority::Last,
DimensionPriority::Last,
));
// color_config closures for "separator" are just given a null.
let color = style_computer.compute("separator", &Value::nothing(Span::unknown()));
let color = color.paint(" ").to_string();
if let Ok(color) = Color::try_from(color) {
// # SAFETY
//
// It's perfectly save to do cause table does not store the reference internally.
// We just need this unsafe section to cope with some limitations of [`PoolTable`].
// Mitigation of this is definitely on a todo list.
let color: AnsiColor<'_> = color.into();
let prefix = color.get_prefix();
let suffix = color.get_suffix();
let prefix: &'static str = unsafe { std::mem::transmute(prefix) };
let suffix: &'static str = unsafe { std::mem::transmute(suffix) };
table.with(SetBorderColor(StaticColor::new(prefix, suffix)));
let table = table.to_string();
return table;
}
table.to_string()
}
fn convert_nu_value_to_table_value(value: Value, config: &Config) -> TableValue {
match value {
Value::Record { cols, vals, .. } => build_vertical_map(cols, vals, config),
Value::List { vals, .. } => {
let rebuild_array_as_map = is_valid_record(&vals) && count_columns_in_record(&vals) > 0;
if rebuild_array_as_map {
build_map_from_record(vals, config)
} else {
build_vertical_array(vals, config)
}
}
value => {
let mut text = value.into_abbreviated_string(config);
if string_width(&text) > 50 {
text = string_wrap(&text, 30, false);
}
TableValue::Cell(text)
}
}
}
fn build_vertical_map(cols: Vec<String>, vals: Vec<Value>, config: &Config) -> TableValue {
let mut rows = Vec::with_capacity(cols.len());
for (key, value) in cols.into_iter().zip(vals) {
let val = convert_nu_value_to_table_value(value, config);
let row = TableValue::Row(vec![TableValue::Cell(key), val]);
rows.push(row);
}
let max_key_width = rows
.iter()
.map(|row| match row {
TableValue::Row(list) => match &list[0] {
TableValue::Cell(key) => string_width(key),
_ => unreachable!(),
},
_ => unreachable!(),
})
.max()
.unwrap_or(0);
rows.iter_mut().for_each(|row| {
match row {
TableValue::Row(list) => match &mut list[0] {
TableValue::Cell(key) => {
let width = string_width(key);
let rest = max_key_width - width;
key.extend(std::iter::repeat(' ').take(rest));
}
_ => unreachable!(),
},
_ => unreachable!(),
};
});
TableValue::Column(rows)
}
fn build_vertical_array(vals: Vec<Value>, config: &Config) -> TableValue {
let map = vals
.into_iter()
.map(|val| convert_nu_value_to_table_value(val, config))
.collect::<Vec<_>>();
TableValue::Column(map)
}
fn is_valid_record(vals: &[Value]) -> bool {
let mut used_cols: Option<&[String]> = None;
for val in vals {
match val {
Value::Record { cols, .. } => {
let cols_are_not_equal =
used_cols.is_some() && !matches!(used_cols, Some(used) if cols == used);
if cols_are_not_equal {
return false;
}
used_cols = Some(cols);
}
_ => return false,
}
}
true
}
fn count_columns_in_record(vals: &[Value]) -> usize {
match vals.iter().next() {
Some(Value::Record { cols, .. }) => cols.len(),
_ => 0,
}
}
fn build_map_from_record(vals: Vec<Value>, config: &Config) -> TableValue {
let mut list = vec![];
let head = get_columns_in_record(&vals);
let count_columns = head.len();
for col in head {
list.push(vec![TableValue::Cell(col)]);
}
for val in vals {
match val {
Value::Record { vals, .. } => {
for (i, cell) in vals.into_iter().take(count_columns).enumerate() {
let cell = convert_nu_value_to_table_value(cell, config);
list[i].push(cell);
}
}
_ => unreachable!(),
}
}
let columns = list.into_iter().map(TableValue::Column).collect::<Vec<_>>();
TableValue::Row(columns)
}
fn get_columns_in_record(vals: &[Value]) -> Vec<String> {
match vals.iter().next() {
Some(Value::Record { cols, .. }) => cols.clone(),
_ => vec![],
}
}
struct SetRawStyle(RawStyle);
impl<R, D> TableOption<R, D, CompactMultilineConfig> for SetRawStyle {
fn change(self, _: &mut R, cfg: &mut CompactMultilineConfig, _: &mut D) {
let borders = self.0.get_borders();
*cfg = cfg.set_borders(borders);
}
}
struct SetBorderColor(StaticColor);
impl<R, D> TableOption<R, D, CompactMultilineConfig> for SetBorderColor {
fn change(self, _: &mut R, cfg: &mut CompactMultilineConfig, _: &mut D) {
let borders = Borders::filled(self.0);
*cfg = cfg.set_borders_color(borders);
}
}
struct SetAlignment(AlignmentHorizontal);
impl<R, D> TableOption<R, D, CompactMultilineConfig> for SetAlignment {
fn change(self, _: &mut R, cfg: &mut CompactMultilineConfig, _: &mut D) {
*cfg = cfg.set_alignment_horizontal(self.0);
}
}
fn truncate_table_value(
value: &mut TableValue,
has_vertical: bool,
available: usize,
) -> Option<usize> {
const MIN_CONTENT_WIDTH: usize = 10;
const TRUNCATE_CELL_WIDTH: usize = 3;
const PAD: usize = 2;
match value {
TableValue::Row(row) => {
if row.is_empty() {
return Some(PAD);
}
if row.len() == 1 {
return truncate_table_value(&mut row[0], has_vertical, available);
}
let count_cells = row.len();
let mut row_width = 0;
let mut i = 0;
let mut last_used_width = 0;
for cell in row.iter_mut() {
let vertical = (has_vertical && i + 1 != count_cells) as usize;
if available < row_width + vertical {
break;
}
let available = available - row_width - vertical;
let width = match truncate_table_value(cell, has_vertical, available) {
Some(width) => width,
None => break,
};
row_width += width + vertical;
last_used_width = row_width;
i += 1;
}
if i == row.len() {
return Some(row_width);
}
if i == 0 {
if available >= PAD + TRUNCATE_CELL_WIDTH {
*value = TableValue::Cell(String::from("..."));
return Some(PAD + TRUNCATE_CELL_WIDTH);
} else {
return None;
}
}
let available = available - row_width;
let has_space_empty_cell = available >= PAD + TRUNCATE_CELL_WIDTH;
if has_space_empty_cell {
row[i] = TableValue::Cell(String::from("..."));
row.truncate(i + 1);
row_width += PAD + TRUNCATE_CELL_WIDTH;
} else if i == 0 {
return None;
} else {
row[i - 1] = TableValue::Cell(String::from("..."));
row.truncate(i);
row_width -= last_used_width;
row_width += PAD + TRUNCATE_CELL_WIDTH;
}
Some(row_width)
}
TableValue::Column(column) => {
let mut max_width = PAD;
for cell in column.iter_mut() {
let width = truncate_table_value(cell, has_vertical, available)?;
max_width = std::cmp::max(max_width, width);
}
Some(max_width)
}
TableValue::Cell(text) => {
if available <= PAD {
return None;
}
let available = available - PAD;
let width = string_width(text);
if width > available {
if available > MIN_CONTENT_WIDTH {
*text = string_wrap(text, available, false);
Some(available + PAD)
} else if available >= 3 {
*text = String::from("...");
Some(3 + PAD)
} else {
// situation where we have too little space
None
}
} else {
Some(width + PAD)
}
}
}
}

View file

@ -1,7 +1,11 @@
use tabled::{builder::Builder, object::Cell, Modify, Padding, Style, Width}; use tabled::{
builder::Builder,
grid::util::string::string_width_multiline,
settings::{width::Truncate, Modify, Padding, Style, Width},
};
pub fn string_width(text: &str) -> usize { pub fn string_width(text: &str) -> usize {
tabled::papergrid::util::string_width_multiline_tab(text, 4) string_width_multiline(text)
} }
pub fn string_wrap(text: &str, width: usize, keep_words: bool) -> String { pub fn string_wrap(text: &str, width: usize, keep_words: bool) -> String {
@ -24,7 +28,7 @@ pub fn string_wrap(text: &str, width: usize, keep_words: bool) -> String {
.build() .build()
.with(Style::empty()) .with(Style::empty())
.with(Padding::zero()) .with(Padding::zero())
.with(Modify::new(Cell(0, 0)).with(wrap)) .with(Modify::new((0, 0)).with(wrap))
.to_string() .to_string()
} }
@ -36,10 +40,5 @@ pub fn string_truncate(text: &str, width: usize) -> String {
None => return String::new(), None => return String::new(),
}; };
Builder::from_iter([[line]]) Truncate::truncate_text(line, width).into_owned()
.build()
.with(Style::empty())
.with(Padding::zero())
.with(Width::truncate(width))
.to_string()
} }

View file

@ -1,9 +1,8 @@
use nu_table::{string_width, Table, TableConfig, TextStyle}; #![allow(dead_code)]
use tabled::papergrid::records::{cell_info::CellInfo, tcell::TCell};
pub type VecCells = Vec<Vec<TCell<CellInfo<'static>, TextStyle>>>; use nu_table::{string_width, NuTable, TableConfig};
use tabled::grid::records::vec_records::CellInfo;
#[allow(dead_code)]
pub struct TestCase { pub struct TestCase {
cfg: TableConfig, cfg: TableConfig,
termwidth: usize, termwidth: usize,
@ -11,7 +10,6 @@ pub struct TestCase {
} }
impl TestCase { impl TestCase {
#[allow(dead_code)]
pub fn new(cfg: TableConfig, termwidth: usize, expected: Option<String>) -> Self { pub fn new(cfg: TableConfig, termwidth: usize, expected: Option<String>) -> Self {
Self { Self {
cfg, cfg,
@ -21,11 +19,9 @@ impl TestCase {
} }
} }
#[allow(dead_code)] type Data = Vec<Vec<CellInfo<String>>>;
pub fn test_table<I>(data: VecCells, tests: I)
where pub fn test_table<I: IntoIterator<Item = TestCase>>(data: Data, tests: I) {
I: IntoIterator<Item = TestCase>,
{
for (i, test) in tests.into_iter().enumerate() { for (i, test) in tests.into_iter().enumerate() {
let actual = create_table(data.clone(), test.cfg.clone(), test.termwidth); let actual = create_table(data.clone(), test.cfg.clone(), test.termwidth);
@ -41,28 +37,20 @@ where
} }
} }
pub fn create_table(data: VecCells, config: TableConfig, termwidth: usize) -> Option<String> { pub fn create_table(data: Data, config: TableConfig, termwidth: usize) -> Option<String> {
let mut size = (0, 0); let table = NuTable::from(data);
for row in &data {
size.0 += 1;
size.1 = std::cmp::max(size.1, row.len());
}
let table = Table::new(data, size);
table.draw(config, termwidth) table.draw(config, termwidth)
} }
pub fn create_row(count_columns: usize) -> Vec<TCell<CellInfo<'static>, TextStyle>> { pub fn create_row(count_columns: usize) -> Vec<CellInfo<String>> {
let mut row = Vec::with_capacity(count_columns); let mut row = Vec::with_capacity(count_columns);
for i in 0..count_columns { for i in 0..count_columns {
row.push(Table::create_cell(i.to_string(), TextStyle::default())); row.push(CellInfo::new(i.to_string()));
} }
row row
} }
#[allow(dead_code)] pub fn cell(text: &str) -> CellInfo<String> {
pub fn _str(s: &str) -> TCell<CellInfo<'static>, TextStyle> { CellInfo::new(text.to_string())
Table::create_cell(s.to_string(), TextStyle::default())
} }

View file

@ -1,16 +1,17 @@
mod common; mod common;
use nu_protocol::TrimStrategy; use nu_protocol::TrimStrategy;
use nu_table::{Table, TableConfig, TableTheme as theme}; use nu_table::{NuTable, TableConfig, TableTheme as theme};
use common::{_str, create_row, test_table, TestCase, VecCells}; use common::{create_row, test_table, TestCase};
use tabled::grid::records::vec_records::CellInfo;
#[test] #[test]
fn data_and_header_has_different_size() { fn data_and_header_has_different_size_doesnt_work() {
let table = Table::new(vec![create_row(3), create_row(5), create_row(5)], (3, 5)); let table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]);
let table = table.draw( let table = table.draw(
TableConfig::new(theme::heavy(), true, false, false), TableConfig::new().theme(theme::heavy()).with_header(true),
usize::MAX, usize::MAX,
); );
@ -18,7 +19,7 @@ fn data_and_header_has_different_size() {
table.as_deref(), table.as_deref(),
Some( Some(
"┏━━━┳━━━┳━━━┳━━━┳━━━┓\n\ "┏━━━┳━━━┳━━━┳━━━┳━━━┓\n\
0 1 2 \n\ 0 1 2 3 4 \n\
\n\ \n\
0 1 2 3 4 \n\ 0 1 2 3 4 \n\
0 1 2 3 4 \n\ 0 1 2 3 4 \n\
@ -26,10 +27,10 @@ fn data_and_header_has_different_size() {
) )
); );
let table = Table::new(vec![create_row(5), create_row(3), create_row(3)], (3, 5)); let table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]);
let table = table.draw( let table = table.draw(
TableConfig::new(theme::heavy(), true, false, false), TableConfig::new().theme(theme::heavy()).with_header(true),
usize::MAX, usize::MAX,
); );
@ -39,8 +40,8 @@ fn data_and_header_has_different_size() {
"┏━━━┳━━━┳━━━┳━━━┳━━━┓\n\ "┏━━━┳━━━┳━━━┳━━━┳━━━┓\n\
0 1 2 3 4 \n\ 0 1 2 3 4 \n\
\n\ \n\
0 1 2 \n\ 0 1 2 3 4 \n\
0 1 2 \n\ 0 1 2 3 4 \n\
" "
) )
); );
@ -50,14 +51,14 @@ fn data_and_header_has_different_size() {
fn termwidth_too_small() { fn termwidth_too_small() {
let test_loop = |config: TableConfig| { let test_loop = |config: TableConfig| {
for i in 0..10 { for i in 0..10 {
let table = Table::new(vec![create_row(3), create_row(3), create_row(5)], (3, 5)); let table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]);
let table = table.draw(config.clone(), i); let table = table.draw(config.clone(), i);
assert!(table.is_none()); assert!(table.is_none());
} }
}; };
let base_config = TableConfig::new(theme::heavy(), true, false, false); let base_config = TableConfig::new().theme(theme::heavy()).with_header(true);
let config = base_config.clone(); let config = base_config.clone();
test_loop(config); test_loop(config);
@ -185,9 +186,9 @@ fn truncate_with_suffix_test() {
#[test] #[test]
fn width_control_test_0() { fn width_control_test_0() {
let data = vec![ let data = vec![
vec![_str("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); 16], vec![common::cell("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); 16],
vec![_str("yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"); 16], vec![common::cell("yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"); 16],
vec![_str("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"); 16], vec![common::cell("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"); 16],
]; ];
let tests = [ let tests = [
@ -202,9 +203,12 @@ fn width_control_test_0() {
test_width(data, &tests); test_width(data, &tests);
} }
fn test_width(data: VecCells, tests: &[(usize, &str)]) { fn test_width(data: Vec<Vec<CellInfo<String>>>, tests: &[(usize, &str)]) {
let trim = TrimStrategy::truncate(Some(String::from("..."))); let trim = TrimStrategy::truncate(Some(String::from("...")));
let config = TableConfig::new(nu_table::TableTheme::heavy(), true, false, false).trim(trim); let config = TableConfig::new()
.theme(theme::heavy())
.with_header(true)
.trim(trim);
let tests = tests.iter().map(|&(termwidth, expected)| { let tests = tests.iter().map(|&(termwidth, expected)| {
TestCase::new(config.clone(), termwidth, Some(expected.to_owned())) TestCase::new(config.clone(), termwidth, Some(expected.to_owned()))
@ -214,24 +218,25 @@ fn test_width(data: VecCells, tests: &[(usize, &str)]) {
} }
fn test_trim(tests: &[(usize, Option<&str>)], trim: TrimStrategy) { fn test_trim(tests: &[(usize, Option<&str>)], trim: TrimStrategy) {
let config = TableConfig::new(nu_table::TableTheme::heavy(), true, false, false).trim(trim); let config = TableConfig::new()
.theme(theme::heavy())
.with_header(true)
.trim(trim);
let tests = tests.iter().map(|&(termwidth, expected)| { let tests = tests.iter().map(|&(termwidth, expected)| {
TestCase::new(config.clone(), termwidth, expected.map(|s| s.to_string())) TestCase::new(config.clone(), termwidth, expected.map(|s| s.to_string()))
}); });
let data = create_test_table0(); let data = vec![
vec![
common::cell("123 45678"),
common::cell("qweqw eqwe"),
common::cell("xxx xx xx x xx x xx xx"),
common::cell("qqq qqq qqqq qqq qq"),
common::cell("qw"),
],
create_row(5),
create_row(5),
];
test_table(data, tests); test_table(data, tests);
} }
fn create_test_table0() -> VecCells {
let header = vec![
_str("123 45678"),
_str("qweqw eqwe"),
_str("xxx xx xx x xx x xx xx"),
_str("qqq qqq qqqq qqq qq"),
_str("qw"),
];
vec![header, create_row(5), create_row(5)]
}

View file

@ -8,7 +8,10 @@ use nu_table::{TableConfig, TableTheme as theme};
fn test_expand() { fn test_expand() {
let table = create_table( let table = create_table(
vec![create_row(4); 3], vec![create_row(4); 3],
TableConfig::new(theme::rounded(), true, false, false).expand(), TableConfig::new()
.theme(theme::rounded())
.with_header(true)
.expand(true),
50, 50,
); );

View file

@ -1,7 +1,8 @@
mod common; mod common;
use common::{create_row as row, VecCells}; use common::create_row as row;
use nu_table::{TableConfig, TableTheme as theme}; use nu_table::{NuTable, TableConfig, TableTheme as theme};
use tabled::grid::records::vec_records::CellInfo;
#[test] #[test]
fn test_rounded() { fn test_rounded() {
@ -47,7 +48,7 @@ fn test_rounded() {
); );
assert_eq!( assert_eq!(
create_table_with_size(vec![row(4); 0], (0, 4), true, theme::rounded()), create_table_with_size(vec![row(4); 0], true, theme::rounded()),
"" ""
); );
} }
@ -98,7 +99,7 @@ fn test_basic() {
); );
assert_eq!( assert_eq!(
create_table_with_size(vec![row(4); 0], (0, 4), true, theme::basic()), create_table_with_size(vec![row(4); 0], true, theme::basic()),
"" ""
); );
} }
@ -145,7 +146,7 @@ fn test_reinforced() {
); );
assert_eq!( assert_eq!(
create_table_with_size(vec![row(4); 0], (0, 4), true, theme::reinforced()), create_table_with_size(vec![row(4); 0], true, theme::reinforced()),
"" ""
); );
} }
@ -196,7 +197,7 @@ fn test_compact() {
); );
assert_eq!( assert_eq!(
create_table_with_size(vec![row(4); 0], (0, 4), true, theme::compact()), create_table_with_size(vec![row(4); 0], true, theme::compact()),
"" ""
); );
} }
@ -247,7 +248,7 @@ fn test_compact_double() {
); );
assert_eq!( assert_eq!(
create_table_with_size(vec![row(4); 0], (0, 4), true, theme::compact_double()), create_table_with_size(vec![row(4); 0], true, theme::compact_double()),
"" ""
); );
} }
@ -296,7 +297,7 @@ fn test_heavy() {
); );
assert_eq!( assert_eq!(
create_table_with_size(vec![row(4); 0], (0, 4), true, theme::heavy()), create_table_with_size(vec![row(4); 0], true, theme::heavy()),
"" ""
); );
} }
@ -334,7 +335,7 @@ fn test_light() {
); );
assert_eq!( assert_eq!(
create_table_with_size(vec![row(4); 0], (0, 4), true, theme::light()), create_table_with_size(vec![row(4); 0], true, theme::light()),
"" ""
); );
} }
@ -367,7 +368,7 @@ fn test_none() {
); );
assert_eq!( assert_eq!(
create_table_with_size(vec![row(4); 0], (0, 4), true, theme::none()), create_table_with_size(vec![row(4); 0], true, theme::none()),
"" ""
); );
} }
@ -418,7 +419,7 @@ fn test_thin() {
); );
assert_eq!( assert_eq!(
create_table_with_size(vec![row(4); 0], (0, 4), true, theme::thin()), create_table_with_size(vec![row(4); 0], true, theme::thin()),
"" ""
); );
} }
@ -469,27 +470,33 @@ fn test_with_love() {
); );
assert_eq!( assert_eq!(
create_table_with_size(vec![row(4); 0], (0, 4), true, theme::with_love()), create_table_with_size(vec![row(4); 0], true, theme::with_love()),
"" ""
); );
} }
fn create_table(data: VecCells, with_header: bool, theme: theme) -> String { fn create_table(data: Vec<Vec<CellInfo<String>>>, with_header: bool, theme: theme) -> String {
let config = TableConfig::new(theme, with_header, false, false); let mut config = TableConfig::new().theme(theme);
if with_header {
config = config.with_header(true);
}
let out = common::create_table(data, config, usize::MAX); let out = common::create_table(data, config, usize::MAX);
out.expect("not expected to get None") out.expect("not expected to get None")
} }
fn create_table_with_size( fn create_table_with_size(
data: VecCells, data: Vec<Vec<CellInfo<String>>>,
size: (usize, usize),
with_header: bool, with_header: bool,
theme: theme, theme: theme,
) -> String { ) -> String {
let config = TableConfig::new(theme, with_header, false, false); let mut config = TableConfig::new().theme(theme);
if with_header {
config = config.with_header(true);
}
let table = nu_table::Table::new(data, size); let table = NuTable::from(data);
table table
.draw(config, usize::MAX) .draw(config, usize::MAX)