mirror of
https://github.com/nushell/nushell
synced 2024-12-24 12:03:18 +00:00
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:
parent
07c9f681c7
commit
8d8b011702
26 changed files with 3041 additions and 3230 deletions
41
Cargo.lock
generated
41
Cargo.lock
generated
|
@ -87,15 +87,6 @@ version = "0.1.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "ansi-str"
|
||||
version = "0.7.2"
|
||||
|
@ -2087,16 +2078,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
|
@ -2823,7 +2804,6 @@ dependencies = [
|
|||
"nu-test-support",
|
||||
"nu-utils",
|
||||
"serde",
|
||||
"tabled",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2947,7 +2927,7 @@ dependencies = [
|
|||
name = "nu-explore"
|
||||
version = "0.79.1"
|
||||
dependencies = [
|
||||
"ansi-str 0.7.2",
|
||||
"ansi-str",
|
||||
"crossterm 0.26.1",
|
||||
"lscolors",
|
||||
"nu-ansi-term",
|
||||
|
@ -3082,13 +3062,11 @@ dependencies = [
|
|||
name = "nu-table"
|
||||
version = "0.79.1"
|
||||
dependencies = [
|
||||
"json_to_table",
|
||||
"nu-ansi-term",
|
||||
"nu-color-config",
|
||||
"nu-engine",
|
||||
"nu-protocol",
|
||||
"nu-utils",
|
||||
"serde_json",
|
||||
"tabled",
|
||||
]
|
||||
|
||||
|
@ -3472,11 +3450,11 @@ checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
|
|||
|
||||
[[package]]
|
||||
name = "papergrid"
|
||||
version = "0.7.1"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1526bb6aa9f10ec339fb10360f22c57edf81d5678d0278e93bc12a47ffbe4b01"
|
||||
checksum = "1fdfe703c51ddc52887ad78fc69cd2ea78d895ffcd6e955c9d03566db8ab5bb1"
|
||||
dependencies = [
|
||||
"ansi-str 0.5.0",
|
||||
"ansi-str",
|
||||
"ansitok",
|
||||
"bytecount",
|
||||
"fnv",
|
||||
|
@ -5128,11 +5106,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tabled"
|
||||
version = "0.10.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c3ee73732ffceaea7b8f6b719ce3bb17f253fa27461ffeaf568ebd0cdb4b85"
|
||||
checksum = "da1a2e56bbf7bfdd08aaa7592157a742205459eff774b73bc01809ae2d99dc2a"
|
||||
dependencies = [
|
||||
"ansi-str 0.5.0",
|
||||
"ansi-str",
|
||||
"ansitok",
|
||||
"papergrid",
|
||||
"tabled_derive",
|
||||
"unicode-width",
|
||||
|
@ -5140,9 +5119,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tabled_derive"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "beca1b4eaceb4f2755df858b88d9b9315b7ccfd1ffd0d7a48a52602301f01a57"
|
||||
checksum = "99f688a08b54f4f02f0a3c382aefdb7884d3d69609f785bd253dc033243e3fe4"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
|
|
|
@ -12,8 +12,6 @@ bench = false
|
|||
|
||||
[dependencies]
|
||||
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-ansi-term = "0.47.0"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::text_style::Alignment;
|
||||
use crate::{color_record_to_nustyle, lookup_ansi_color_style, TextStyle};
|
||||
use nu_ansi_term::{Color, Style};
|
||||
use nu_engine::eval_block;
|
||||
|
@ -5,7 +6,6 @@ use nu_protocol::{
|
|||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
CliError, IntoPipelineData, Value,
|
||||
};
|
||||
use tabled::alignment::AlignmentHorizontal;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
|
@ -111,34 +111,28 @@ impl<'a> StyleComputer<'a> {
|
|||
|
||||
// Used only by the `table` command.
|
||||
pub fn style_primitive(&self, value: &Value) -> TextStyle {
|
||||
use Alignment::*;
|
||||
let s = self.compute(&value.get_type().get_non_specified_string(), value);
|
||||
match *value {
|
||||
Value::Bool { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
|
||||
|
||||
Value::Int { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s),
|
||||
|
||||
Value::Filesize { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s),
|
||||
|
||||
Value::Duration { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s),
|
||||
|
||||
Value::Date { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
|
||||
|
||||
Value::Range { .. } => TextStyle::with_style(AlignmentHorizontal::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::Bool { .. } => TextStyle::with_style(Left, s),
|
||||
Value::Int { .. } => TextStyle::with_style(Right, s),
|
||||
Value::Filesize { .. } => TextStyle::with_style(Right, s),
|
||||
Value::Duration { .. } => TextStyle::with_style(Right, s),
|
||||
Value::Date { .. } => TextStyle::with_style(Left, s),
|
||||
Value::Range { .. } => TextStyle::with_style(Left, s),
|
||||
Value::Float { .. } => TextStyle::with_style(Right, s),
|
||||
Value::String { .. } => TextStyle::with_style(Left, s),
|
||||
Value::Nothing { .. } => TextStyle::with_style(Left, s),
|
||||
Value::Binary { .. } => TextStyle::with_style(Left, s),
|
||||
Value::CellPath { .. } => TextStyle::with_style(Left, s),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
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)]
|
||||
pub struct TextStyle {
|
||||
|
@ -240,23 +244,3 @@ impl Default for TextStyle {
|
|||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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-utils = { path = "../nu-utils", version = "0.79.1" }
|
||||
num-format = { version = "0.4.3" }
|
||||
|
||||
nu-ansi-term = "0.47.0"
|
||||
|
||||
# Potential dependencies for extras
|
||||
|
@ -89,7 +88,7 @@ percent-encoding = "2.2.0"
|
|||
rusqlite = { version = "0.28.0", features = ["bundled"], optional = true }
|
||||
sqlparser = { version = "0.32.0", features = ["serde"], optional = true }
|
||||
sysinfo = "0.28.2"
|
||||
tabled = "0.10.0"
|
||||
tabled = "0.12.0"
|
||||
terminal_size = "0.2.1"
|
||||
thiserror = "1.0.31"
|
||||
titlecase = "2.0.0"
|
||||
|
|
|
@ -1,123 +1,193 @@
|
|||
use nu_protocol::Value;
|
||||
use nu_table::{string_width, string_wrap};
|
||||
use tabled::{
|
||||
builder::Builder,
|
||||
peaker::PriorityMax,
|
||||
width::{MinWidth, Wrap},
|
||||
Style,
|
||||
grid::config::ColoredConfig,
|
||||
settings::{peaker::PriorityMax, width::Wrap, Settings, Style},
|
||||
Table,
|
||||
};
|
||||
|
||||
use self::{
|
||||
global_horizontal_char::SetHorizontalCharOnFirstRow, peak2::Peak2,
|
||||
table_column_width::get_first_cell_width, truncate_table::TruncateTable,
|
||||
width_increase::IncWidth,
|
||||
use crate::debug::inspect_table::{
|
||||
global_horizontal_char::SetHorizontalChar, set_widths::SetWidths,
|
||||
};
|
||||
|
||||
pub fn build_table(value: Value, description: String, termsize: usize) -> String {
|
||||
let (head, mut data) = util::collect_input(value);
|
||||
let count_columns = head.len();
|
||||
data.insert(0, head);
|
||||
|
||||
let mut val_table = Builder::from(data).build();
|
||||
let val_table_width = val_table.total_width();
|
||||
let mut desc = description;
|
||||
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 desc_table_width = desc_table.total_width();
|
||||
let val_table_width = get_total_width2(&widths, &cfg);
|
||||
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)]
|
||||
let width = val_table_width.max(desc_table_width).min(termsize);
|
||||
|
||||
desc_table
|
||||
.with(Style::rounded().off_bottom())
|
||||
.with(Wrap::new(width).priority::<PriorityMax>())
|
||||
.with(MinWidth::new(width).priority::<Peak2>());
|
||||
let mut desc_table = Table::from_iter([[String::from("description"), desc]]);
|
||||
desc_table.with(Style::rounded().remove_bottom().remove_horizontals());
|
||||
|
||||
val_table
|
||||
.with(Style::rounded().top_left_corner('├').top_right_corner('┤'))
|
||||
.with(TruncateTable(width))
|
||||
.with(Wrap::new(width).priority::<PriorityMax>())
|
||||
.with(IncWidth(width));
|
||||
|
||||
// we use only 1, cause left border considered 0 position
|
||||
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));
|
||||
let mut val_table = Table::from_iter(data);
|
||||
val_table.with(
|
||||
Settings::default()
|
||||
.with(Style::rounded().corner_top_left('├').corner_top_right('┤'))
|
||||
.with(SetWidths(widths))
|
||||
.with(Wrap::new(width).priority::<PriorityMax>())
|
||||
.with(SetHorizontalChar::new('┼', '┴', 11 + 2 + 1)),
|
||||
);
|
||||
|
||||
format!("{desc_table}\n{val_table}")
|
||||
}
|
||||
|
||||
mod truncate_table {
|
||||
use tabled::{
|
||||
papergrid::{
|
||||
records::{Records, RecordsMut, Resizable},
|
||||
width::{CfgWidthFunction, WidthEstimator},
|
||||
Estimate,
|
||||
},
|
||||
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)
|
||||
}
|
||||
fn get_data_widths(data: &[Vec<String>], count_columns: usize) -> Vec<usize> {
|
||||
let mut widths = vec![0; count_columns];
|
||||
for row in data {
|
||||
for col in 0..count_columns {
|
||||
let text = &row[col];
|
||||
let width = string_width(text);
|
||||
widths[col] = std::cmp::max(widths[col], width);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -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 {
|
||||
use tabled::{
|
||||
papergrid::{records::Records, width::WidthEstimator, Estimate, Offset::Begin},
|
||||
Table, TableOption,
|
||||
grid::{
|
||||
config::{ColoredConfig, Offset},
|
||||
dimension::{CompleteDimensionVecRecords, Dimension},
|
||||
records::{ExactRecords, Records},
|
||||
},
|
||||
settings::TableOption,
|
||||
};
|
||||
|
||||
pub struct SetHorizontalCharOnFirstRow {
|
||||
c1: char,
|
||||
c2: char,
|
||||
pos: usize,
|
||||
pub struct SetHorizontalChar {
|
||||
intersection: char,
|
||||
split: char,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl SetHorizontalCharOnFirstRow {
|
||||
pub fn new(c1: char, c2: char, pos: usize) -> Self {
|
||||
Self { c1, c2, pos }
|
||||
impl SetHorizontalChar {
|
||||
pub fn new(intersection: char, split: char, index: usize) -> Self {
|
||||
Self {
|
||||
intersection,
|
||||
split,
|
||||
index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> TableOption<R> for SetHorizontalCharOnFirstRow
|
||||
where
|
||||
R: Records,
|
||||
impl<R: Records + ExactRecords> TableOption<R, CompleteDimensionVecRecords<'_>, ColoredConfig>
|
||||
for SetHorizontalChar
|
||||
{
|
||||
fn change(&mut self, table: &mut Table<R>) {
|
||||
if table.is_empty() {
|
||||
fn change(
|
||||
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;
|
||||
}
|
||||
|
||||
let shape = table.shape();
|
||||
let widths = get_widths(dimension, records.count_columns());
|
||||
|
||||
let mut evaluator = WidthEstimator::default();
|
||||
evaluator.estimate(table.get_records(), table.get_config());
|
||||
let widths: Vec<_> = evaluator.into();
|
||||
|
||||
let has_vertical = table.get_config().has_vertical(0, shape.1);
|
||||
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);
|
||||
let has_vertical = cfg.has_vertical(0, count_columns);
|
||||
if has_vertical && self.index == 0 {
|
||||
let mut border = cfg.get_border((0, 0), (count_rows, count_columns));
|
||||
border.left_top_corner = Some(self.intersection);
|
||||
cfg.set_border((0, 0), border);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut i = 1;
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for (col, width) in widths.into_iter().enumerate() {
|
||||
if self.pos < i + width {
|
||||
let o = self.pos - i;
|
||||
table
|
||||
.get_config_mut()
|
||||
.override_horizontal_border((0, col), self.c2, Begin(o));
|
||||
if self.index < i + width {
|
||||
let o = self.index - i;
|
||||
cfg.set_horizontal_char((0, col), self.split, Offset::Begin(o));
|
||||
return;
|
||||
}
|
||||
|
||||
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 self.pos == i {
|
||||
let mut border = table.get_config().get_border((0, col), shape);
|
||||
border.right_top_corner = Some(self.c1);
|
||||
table.get_config_mut().set_border((0, col), border);
|
||||
if self.index == i {
|
||||
let mut border = cfg.get_border((0, col), (count_rows, count_columns));
|
||||
border.right_top_corner = Some(self.intersection);
|
||||
cfg.set_border((0, col), border);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -360,96 +369,33 @@ mod global_horizontal_char {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod width_increase {
|
||||
use tabled::{
|
||||
object::Cell,
|
||||
papergrid::{
|
||||
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;
|
||||
fn get_widths(dims: &CompleteDimensionVecRecords<'_>, count_columns: usize) -> Vec<usize> {
|
||||
let mut widths = vec![0; count_columns];
|
||||
for (col, width) in widths.iter_mut().enumerate() {
|
||||
*width = dims.get_width(col);
|
||||
}
|
||||
|
||||
widths
|
||||
}
|
||||
}
|
||||
|
||||
fn get_table_widths_with_total<R>(records: R, cfg: &GridConfig) -> (Vec<usize>, usize)
|
||||
where
|
||||
R: Records,
|
||||
{
|
||||
let mut evaluator = WidthEstimator::default();
|
||||
evaluator.estimate(&records, cfg);
|
||||
let total_width = get_table_total_width(&records, cfg, &evaluator);
|
||||
let widths = evaluator.into();
|
||||
mod set_widths {
|
||||
use tabled::{
|
||||
grid::{config::ColoredConfig, dimension::CompleteDimensionVecRecords},
|
||||
settings::TableOption,
|
||||
};
|
||||
|
||||
(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
|
||||
where
|
||||
W: Estimate<R>,
|
||||
R: Records,
|
||||
{
|
||||
ctrl.total()
|
||||
+ cfg.count_vertical(records.count_columns())
|
||||
+ cfg.get_margin().left.size
|
||||
+ cfg.get_margin().right.size
|
||||
impl<R> TableOption<R, CompleteDimensionVecRecords<'_>, ColoredConfig> for SetWidths {
|
||||
fn change(
|
||||
self,
|
||||
_: &mut R,
|
||||
_: &mut ColoredConfig,
|
||||
dims: &mut CompleteDimensionVecRecords<'_>,
|
||||
) {
|
||||
dims.set_widths(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -154,11 +154,11 @@ fn table_collapse_none() {
|
|||
assert_eq!(
|
||||
actual.out,
|
||||
concat!(
|
||||
" a b c ",
|
||||
" 1 2 3 ",
|
||||
" 4 5 1 ",
|
||||
" 2 ",
|
||||
" 3 ",
|
||||
" a b c ",
|
||||
" 1 2 3 ",
|
||||
" 4 5 1 ",
|
||||
" 2 ",
|
||||
" 3 ",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -232,11 +232,20 @@ fn table_collapse_hearts() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn table_collapse_doesnot_support_width_control() {
|
||||
fn table_collapse_does_wrapping_for_long_strings() {
|
||||
let actual = nu!(
|
||||
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]
|
||||
|
@ -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 {
|
||||
lines
|
||||
.into_iter()
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use nu_protocol::Value;
|
||||
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![];
|
||||
|
||||
for item in input {
|
||||
let Value::Record { cols, .. } = item else {
|
||||
return vec![];
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
use nu_color_config::{Alignment, StyleComputer, TextStyle};
|
||||
use nu_engine::column::get_columns;
|
||||
use nu_protocol::{ast::PathMember, Config, ShellError, Span, TableIndexMode, Value};
|
||||
use nu_protocol::{FooterMode, TrimStrategy};
|
||||
use nu_table::{string_width, Table as NuTable, TableConfig, TableTheme};
|
||||
use nu_color_config::StyleComputer;
|
||||
use nu_protocol::{Span, Value};
|
||||
use nu_table::{value_to_clean_styled_string, value_to_styled_string, BuildConfig, ExpandedTable};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
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;
|
||||
|
||||
pub fn try_build_table(
|
||||
|
@ -21,10 +13,13 @@ pub fn try_build_table(
|
|||
value: Value,
|
||||
) -> String {
|
||||
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 } => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
@ -37,18 +32,8 @@ fn try_build_map(
|
|||
ctrlc: Option<Arc<AtomicBool>>,
|
||||
config: &NuConfig,
|
||||
) -> String {
|
||||
let result = build_expanded_table(
|
||||
cols.clone(),
|
||||
vals.clone(),
|
||||
span,
|
||||
ctrlc,
|
||||
config,
|
||||
style_computer,
|
||||
usize::MAX,
|
||||
None,
|
||||
false,
|
||||
"",
|
||||
);
|
||||
let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX);
|
||||
let result = ExpandedTable::new(None, false, String::new()).build_map(&cols, &vals, opts);
|
||||
match result {
|
||||
Ok(Some(result)) => result,
|
||||
Ok(None) | Err(_) => {
|
||||
|
@ -59,896 +44,18 @@ fn try_build_map(
|
|||
|
||||
fn try_build_list(
|
||||
vals: Vec<Value>,
|
||||
ctrlc: &Option<Arc<AtomicBool>>,
|
||||
ctrlc: Option<Arc<AtomicBool>>,
|
||||
config: &NuConfig,
|
||||
span: Span,
|
||||
style_computer: &StyleComputer,
|
||||
) -> String {
|
||||
let table = convert_to_table2(
|
||||
0,
|
||||
vals.iter(),
|
||||
ctrlc.clone(),
|
||||
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
|
||||
})
|
||||
}
|
||||
let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX);
|
||||
let result = ExpandedTable::new(None, false, String::new()).build_list(&vals, opts);
|
||||
match result {
|
||||
Ok(Some(out)) => out,
|
||||
Ok(None) | Err(_) => {
|
||||
// it means that the list is empty
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -18,9 +18,7 @@ nu-color-config = { path = "../nu-color-config", version = "0.79.1" }
|
|||
|
||||
nu-ansi-term = "0.47.0"
|
||||
|
||||
tabled = { version = "0.10.0", features = ["color"], default-features = false }
|
||||
json_to_table = { version = "0.3.1", features = ["color"] }
|
||||
serde_json = "1"
|
||||
tabled = { version = "0.12.0", features = ["color"], default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
# nu-test-support = { path="../nu-test-support", version = "0.79.1" }
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use nu_ansi_term::{Color, Style};
|
||||
use nu_color_config::TextStyle;
|
||||
use nu_table::{Table, TableConfig, TableTheme};
|
||||
use tabled::papergrid::records::{cell_info::CellInfo, tcell::TCell};
|
||||
use nu_table::{NuTable, TableConfig, TableTheme};
|
||||
use tabled::grid::records::vec_records::CellInfo;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
let mut width = 0;
|
||||
|
||||
if args.len() > 1 {
|
||||
// Width in terminal characters
|
||||
width = args[1].parse::<usize>().expect("Need a width in columns");
|
||||
}
|
||||
|
||||
|
@ -16,31 +16,26 @@ fn main() {
|
|||
width = 80;
|
||||
}
|
||||
|
||||
// The mocked up table data
|
||||
let (table_headers, row_data) = make_table_data();
|
||||
|
||||
// The table headers
|
||||
let headers = vec_of_str_to_vec_of_styledstr(&table_headers, true);
|
||||
let headers = to_cell_info_vec(&table_headers);
|
||||
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];
|
||||
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 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
|
||||
.draw(table_cfg, width)
|
||||
.unwrap_or_else(|| format!("Couldn't fit table into {width} columns!"));
|
||||
|
||||
// Draw the table
|
||||
println!("{output_table}")
|
||||
}
|
||||
|
||||
|
@ -82,24 +77,11 @@ fn make_table_data() -> (Vec<&'static str>, Vec<&'static str>) {
|
|||
(table_headers, row_data)
|
||||
}
|
||||
|
||||
fn vec_of_str_to_vec_of_styledstr(
|
||||
data: &[&str],
|
||||
is_header: bool,
|
||||
) -> Vec<TCell<CellInfo<'static>, TextStyle>> {
|
||||
fn to_cell_info_vec(data: &[&str]) -> Vec<CellInfo<String>> {
|
||||
let mut v = vec![];
|
||||
|
||||
for x in data {
|
||||
if is_header {
|
||||
v.push(Table::create_cell(
|
||||
String::from(*x),
|
||||
TextStyle::default_header(),
|
||||
))
|
||||
} else {
|
||||
v.push(Table::create_cell(
|
||||
String::from(*x),
|
||||
TextStyle::basic_left(),
|
||||
))
|
||||
}
|
||||
v.push(CellInfo::new(String::from(*x)));
|
||||
}
|
||||
|
||||
v
|
||||
}
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
mod nu_protocol_table;
|
||||
mod table;
|
||||
mod table_theme;
|
||||
mod types;
|
||||
mod unstructured_table;
|
||||
mod util;
|
||||
|
||||
pub use nu_color_config::TextStyle;
|
||||
pub use nu_protocol_table::NuTable;
|
||||
pub use table::{Alignments, Table, TableConfig};
|
||||
pub use table::{Alignments, Cell, NuTable, TableConfig};
|
||||
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::*;
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
|
@ -4,108 +4,145 @@ use nu_color_config::TextStyle;
|
|||
use nu_protocol::TrimStrategy;
|
||||
use std::{cmp::min, collections::HashMap};
|
||||
use tabled::{
|
||||
alignment::AlignmentHorizontal,
|
||||
builder::Builder,
|
||||
color::Color,
|
||||
formatting::AlignmentStrategy,
|
||||
object::{Cell, Columns, Rows, Segment},
|
||||
papergrid::{
|
||||
grid::{
|
||||
color::AnsiColor,
|
||||
config::{AlignmentHorizontal, ColoredConfig, Entity, EntityMap, Position},
|
||||
dimension::CompleteDimensionVecRecords,
|
||||
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,
|
||||
Alignment, Modify, ModifyObject, TableOption, Width,
|
||||
settings::{
|
||||
formatting::AlignmentStrategy, object::Segment, peaker::Peaker, Color, Modify, Settings,
|
||||
TableOption, Width,
|
||||
},
|
||||
Table,
|
||||
};
|
||||
|
||||
/// Table represent a table view.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Table {
|
||||
pub struct NuTable {
|
||||
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 {
|
||||
/// Creates a [Table] instance.
|
||||
///
|
||||
/// 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
|
||||
type Data = VecRecords<Cell>;
|
||||
pub type Cell = CellInfo<String>;
|
||||
|
||||
let mut data = data;
|
||||
make_data_consistent(&mut data, size);
|
||||
|
||||
let data = VecRecords::with_hint(data, size.1);
|
||||
|
||||
Table { data }
|
||||
impl NuTable {
|
||||
/// Creates an empty [Table] instance.
|
||||
pub fn new(count_rows: usize, count_columns: usize) -> Self {
|
||||
let data = VecRecords::new(vec![vec![CellInfo::default(); count_columns]; count_rows]);
|
||||
Self {
|
||||
data,
|
||||
size: (count_rows, count_columns),
|
||||
styles: Styles::default(),
|
||||
alignments: Alignments::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count_rows(&self) -> usize {
|
||||
self.data.count_rows()
|
||||
self.size.0
|
||||
}
|
||||
|
||||
pub fn create_cell(
|
||||
text: impl Into<String>,
|
||||
style: TextStyle,
|
||||
) -> TCell<CellInfo<'static>, TextStyle> {
|
||||
TCell::new(CellInfo::new(text.into(), CfgWidthFunction::new(4)), style)
|
||||
pub fn count_columns(&self) -> usize {
|
||||
self.size.1
|
||||
}
|
||||
|
||||
pub fn truncate(&mut self, width: usize, theme: &TableTheme) -> bool {
|
||||
let mut truncated = false;
|
||||
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();
|
||||
}
|
||||
pub fn insert(&mut self, pos: Position, text: String) {
|
||||
self.data[pos.0][pos.1] = CellInfo::new(text);
|
||||
}
|
||||
|
||||
if total > width {
|
||||
truncated = true;
|
||||
self.data.truncate(self.data.count_columns() - 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
pub fn set_column_style(&mut self, column: usize, style: TextStyle) {
|
||||
if let Some(style) = style.color_style {
|
||||
let style = AnsiColor::from(convert_style(style));
|
||||
self.styles.data.insert(Entity::Column(column), style);
|
||||
self.styles.data_is_set = true;
|
||||
}
|
||||
|
||||
let is_empty = self.data.count_rows() == 0 || self.data.count_columns() == 0;
|
||||
if is_empty {
|
||||
return true;
|
||||
let alignment = convert_alignment(style.alignment);
|
||||
if alignment != self.alignments.data {
|
||||
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 {
|
||||
self.data.push(Table::create_cell(
|
||||
String::from("..."),
|
||||
TextStyle::default(),
|
||||
));
|
||||
let alignment = convert_alignment(style.alignment);
|
||||
if alignment != self.alignments.data {
|
||||
self.alignments.cells.insert(pos, alignment);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
///
|
||||
/// It returns None in case where table cannot be fit to a terminal width.
|
||||
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)) {
|
||||
for row in data {
|
||||
if row.len() < size.1 {
|
||||
row.extend(
|
||||
std::iter::repeat(Table::create_cell(String::default(), TextStyle::default()))
|
||||
.take(size.1 - row.len()),
|
||||
);
|
||||
impl From<Vec<Vec<CellInfo<String>>>> for NuTable {
|
||||
fn from(value: Vec<Vec<CellInfo<String>>>) -> Self {
|
||||
let data = VecRecords::new(value);
|
||||
let size = (data.count_rows(), data.count_columns());
|
||||
Self {
|
||||
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)]
|
||||
pub struct TableConfig {
|
||||
theme: TableTheme,
|
||||
alignments: Alignments,
|
||||
trim: TrimStrategy,
|
||||
split_color: Option<Style>,
|
||||
expand: bool,
|
||||
|
@ -123,26 +159,20 @@ pub struct TableConfig {
|
|||
}
|
||||
|
||||
impl TableConfig {
|
||||
pub fn new(
|
||||
theme: TableTheme,
|
||||
with_header: bool,
|
||||
with_index: bool,
|
||||
append_footer: bool,
|
||||
) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
theme,
|
||||
with_header,
|
||||
with_index,
|
||||
with_footer: append_footer,
|
||||
theme: TableTheme::basic(),
|
||||
with_header: false,
|
||||
with_index: false,
|
||||
with_footer: false,
|
||||
expand: false,
|
||||
alignments: Alignments::default(),
|
||||
trim: TrimStrategy::truncate(None),
|
||||
split_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand(mut self) -> Self {
|
||||
self.expand = true;
|
||||
pub fn expand(mut self, on: bool) -> Self {
|
||||
self.expand = on;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -151,248 +181,256 @@ impl TableConfig {
|
|||
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
|
||||
}
|
||||
|
||||
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(crate) data: AlignmentHorizontal,
|
||||
pub(crate) index: AlignmentHorizontal,
|
||||
pub(crate) header: AlignmentHorizontal,
|
||||
data: AlignmentHorizontal,
|
||||
index: AlignmentHorizontal,
|
||||
header: AlignmentHorizontal,
|
||||
columns: HashMap<usize, AlignmentHorizontal>,
|
||||
cells: HashMap<Position, AlignmentHorizontal>,
|
||||
}
|
||||
|
||||
impl Default for Alignments {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
data: AlignmentHorizontal::Center,
|
||||
data: AlignmentHorizontal::Left,
|
||||
index: AlignmentHorizontal::Right,
|
||||
header: AlignmentHorizontal::Center,
|
||||
columns: HashMap::default(),
|
||||
cells: HashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_table(mut data: Data, cfg: TableConfig, termwidth: usize) -> Option<String> {
|
||||
let is_empty = maybe_truncate_columns(&mut data, &cfg.theme, termwidth);
|
||||
if is_empty {
|
||||
fn build_table(
|
||||
mut data: Data,
|
||||
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;
|
||||
}
|
||||
|
||||
if cfg.with_footer {
|
||||
data.duplicate_row(0);
|
||||
if cfg.with_header && cfg.with_footer {
|
||||
duplicate_row(&mut data, 0);
|
||||
}
|
||||
|
||||
draw_table(
|
||||
data,
|
||||
&cfg.theme,
|
||||
cfg.alignments,
|
||||
cfg.with_index,
|
||||
cfg.with_header,
|
||||
cfg.with_footer,
|
||||
cfg.expand,
|
||||
cfg.split_color,
|
||||
&cfg.trim,
|
||||
termwidth,
|
||||
)
|
||||
draw_table(data, alignments, styles, widths, cfg, termwidth)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_table(
|
||||
data: Data,
|
||||
theme: &TableTheme,
|
||||
alignments: Alignments,
|
||||
with_index: bool,
|
||||
with_header: bool,
|
||||
with_footer: bool,
|
||||
expand: bool,
|
||||
split_color: Option<Style>,
|
||||
trim_strategy: &TrimStrategy,
|
||||
styles: Styles,
|
||||
widths: Vec<usize>,
|
||||
cfg: TableConfig,
|
||||
termwidth: usize,
|
||||
) -> Option<String> {
|
||||
let mut table = Builder::custom(data).build();
|
||||
load_theme(&mut table, theme, with_footer, with_header, split_color);
|
||||
let data: Vec<Vec<_>> = data.into();
|
||||
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);
|
||||
colorize_table(&mut table, styles, with_index, with_header, with_footer);
|
||||
|
||||
if expand {
|
||||
table.with(Width::increase(termwidth));
|
||||
}
|
||||
let total_width = get_total_width2(&widths, table.get_config());
|
||||
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 string_width_multiline(&text) > termwidth {
|
||||
if total_width > termwidth {
|
||||
None
|
||||
} else {
|
||||
Some(text)
|
||||
let content = table.to_string();
|
||||
Some(content)
|
||||
}
|
||||
}
|
||||
|
||||
fn align_table(
|
||||
table: &mut tabled::Table<Data>,
|
||||
table: &mut Table,
|
||||
alignments: Alignments,
|
||||
with_index: bool,
|
||||
with_header: bool,
|
||||
with_footer: bool,
|
||||
) {
|
||||
table.with(
|
||||
Modify::new(Segment::all())
|
||||
.with(Alignment::Horizontal(alignments.data))
|
||||
.with(AlignmentStrategy::PerLine),
|
||||
);
|
||||
table
|
||||
.with(Modify::new(Segment::all()).with(AlignmentStrategy::PerLine))
|
||||
.with(SetAlignment(alignments.data, Entity::Global));
|
||||
|
||||
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 {
|
||||
let alignment = Alignment::Horizontal(alignments.header);
|
||||
if with_footer {
|
||||
table.with(Modify::new(Rows::last()).with(alignment.clone()));
|
||||
}
|
||||
table.with(SetAlignment(alignments.header, Entity::Row(0)));
|
||||
|
||||
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 {
|
||||
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(
|
||||
table: &mut tabled::Table<Data>,
|
||||
header_present: bool,
|
||||
index_present: bool,
|
||||
alignments: Alignments,
|
||||
fn colorize_table(
|
||||
table: &mut Table,
|
||||
mut styles: Styles,
|
||||
with_index: bool,
|
||||
with_header: bool,
|
||||
with_footer: bool,
|
||||
) {
|
||||
let offset = usize::from(header_present);
|
||||
let (count_rows, count_columns) = table.shape();
|
||||
for row in offset..count_rows {
|
||||
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 with_index {
|
||||
styles.data.insert(Entity::Column(0), styles.index);
|
||||
styles.data_is_set = true;
|
||||
}
|
||||
|
||||
if alignment == alignments.data {
|
||||
continue;
|
||||
}
|
||||
if with_header {
|
||||
styles.data.insert(Entity::Row(0), styles.header.clone());
|
||||
styles.data_is_set = true;
|
||||
|
||||
table.with(
|
||||
Cell(row, col)
|
||||
.modify()
|
||||
.with(Alignment::Horizontal(alignment)),
|
||||
);
|
||||
if with_footer {
|
||||
let count_rows = table.count_rows();
|
||||
if count_rows > 1 {
|
||||
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>(
|
||||
table: &mut tabled::Table<R>,
|
||||
fn load_theme(
|
||||
table: &mut Table,
|
||||
theme: &TableTheme,
|
||||
with_footer: bool,
|
||||
with_header: bool,
|
||||
separator_color: Option<Style>,
|
||||
) where
|
||||
R: Records,
|
||||
{
|
||||
let mut theme = theme.theme.clone();
|
||||
sep_color: Option<Style>,
|
||||
) {
|
||||
let mut theme = theme.get_theme();
|
||||
|
||||
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);
|
||||
|
||||
if let Some(color) = separator_color {
|
||||
let color = color.paint(" ").to_string();
|
||||
if let Ok(color) = Color::try_from(color) {
|
||||
table.with(color);
|
||||
}
|
||||
}
|
||||
|
||||
if with_footer {
|
||||
table.with(FooterStyle).with(
|
||||
Modify::new(Rows::last())
|
||||
.with(Alignment::center())
|
||||
.with(AlignmentStrategy::PerCell),
|
||||
);
|
||||
if let Some(style) = sep_color {
|
||||
let color = convert_style(style);
|
||||
let color = AnsiColor::from(color);
|
||||
table.get_config_mut().set_border_color_global(color);
|
||||
}
|
||||
}
|
||||
|
||||
struct FooterStyle;
|
||||
|
||||
impl<R> TableOption<R> for FooterStyle
|
||||
where
|
||||
R: Records,
|
||||
{
|
||||
fn change(&mut self, table: &mut tabled::Table<R>) {
|
||||
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);
|
||||
impl<R: ExactRecords, D> TableOption<R, D, ColoredConfig> for FooterStyle {
|
||||
fn change(self, records: &mut R, cfg: &mut ColoredConfig, _: &mut D) {
|
||||
if let Some(line) = cfg.get_horizontal_line(1).cloned() {
|
||||
let count_rows = records.count_rows();
|
||||
cfg.insert_horizontal_line(count_rows - 1, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn table_trim_columns(
|
||||
table: &mut tabled::Table<Data>,
|
||||
table: &mut Table,
|
||||
widths: Vec<usize>,
|
||||
termwidth: usize,
|
||||
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> {
|
||||
termwidth: usize,
|
||||
trim_strategy: &'a TrimStrategy,
|
||||
}
|
||||
table.with(Settings::new(SetDimensions(widths), wrap));
|
||||
}
|
||||
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> {
|
||||
pub fn new(termwidth: usize, trim_strategy: &'a TrimStrategy) -> Self {
|
||||
Self {
|
||||
termwidth,
|
||||
trim_strategy,
|
||||
table.with(Settings::new(SetDimensions(widths), truncate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> tabled::TableOption<R> for TrimStrategyModifier<'_>
|
||||
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 {
|
||||
fn maybe_truncate_columns(data: &mut Data, theme: &TableTheme, termwidth: usize) -> Vec<usize> {
|
||||
const TERMWIDTH_THRESHOLD: usize = 120;
|
||||
|
||||
if data.count_columns() == 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
let truncate = if termwidth > TERMWIDTH_THRESHOLD {
|
||||
truncate_columns_by_columns
|
||||
} 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.
|
||||
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 TRAILING_COLUMN_WIDTH: usize = 5;
|
||||
const TRAILING_COLUMN_STR: &str = "...";
|
||||
|
||||
let config;
|
||||
let total;
|
||||
{
|
||||
let mut table = Builder::custom(&*data).build();
|
||||
load_theme(&mut table, theme, false, false, None);
|
||||
total = table.total_width();
|
||||
config = table.get_config().clone();
|
||||
let config = get_config(theme, false, None);
|
||||
let mut widths = build_width(&*data);
|
||||
let total_width = get_total_width2(&widths, &config);
|
||||
if total_width <= termwidth {
|
||||
return widths;
|
||||
}
|
||||
|
||||
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 vertical_border_i = borders.has_vertical() as usize;
|
||||
|
||||
let mut width = borders.has_left() as usize + borders.has_right() as usize;
|
||||
let mut truncate_pos = 0;
|
||||
for column_width in widths {
|
||||
for column_width in &widths {
|
||||
width += column_width;
|
||||
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?)
|
||||
if truncate_pos == data.count_columns() {
|
||||
return false;
|
||||
return widths;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
@ -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;
|
||||
|
||||
if can_be_squeezed {
|
||||
let cell = Table::create_cell(String::from(TRAILING_COLUMN_STR), TextStyle::default());
|
||||
data.push(cell);
|
||||
push_empty_column(data);
|
||||
widths.push(3 + 2);
|
||||
} else {
|
||||
if data.count_columns() == 1 {
|
||||
return true;
|
||||
return vec![];
|
||||
}
|
||||
|
||||
data.truncate(data.count_columns() - 1);
|
||||
|
||||
let cell = Table::create_cell(String::from(TRAILING_COLUMN_STR), TextStyle::default());
|
||||
data.push(cell);
|
||||
truncate_columns(data, data.count_columns() - 1);
|
||||
push_empty_column(data);
|
||||
widths.pop();
|
||||
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
|
||||
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 TRAILING_COLUMN_WIDTH: usize = 3 + 2;
|
||||
const TRAILING_COLUMN_STR: &str = "...";
|
||||
|
||||
let config;
|
||||
let total;
|
||||
{
|
||||
let mut table = Builder::custom(&*data).build();
|
||||
load_theme(&mut table, theme, false, false, None);
|
||||
total = table.total_width();
|
||||
config = table.get_config().clone();
|
||||
let config = get_config(theme, false, None);
|
||||
let mut widths = build_width(&*data);
|
||||
let total_width = get_total_width2(&widths, &config);
|
||||
if total_width <= termwidth {
|
||||
return widths;
|
||||
}
|
||||
|
||||
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 min_widths = widths
|
||||
.iter()
|
||||
.map(|w| min(*w, ACCEPTABLE_WIDTH))
|
||||
.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 {
|
||||
return false;
|
||||
return widths;
|
||||
}
|
||||
|
||||
// todo: simplify the loop
|
||||
let mut i = 0;
|
||||
while data.count_columns() > 0 {
|
||||
i += 1;
|
||||
|
@ -539,28 +565,29 @@ fn truncate_columns_by_columns(data: &mut Data, theme: &TableTheme, termwidth: u
|
|||
}
|
||||
|
||||
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
|
||||
let diff = termwidth - min_total;
|
||||
if diff > TRAILING_COLUMN_WIDTH {
|
||||
let cell = Table::create_cell(TRAILING_COLUMN_STR, TextStyle::default());
|
||||
data.push(cell);
|
||||
push_empty_column(data);
|
||||
widths.push(3 + 2);
|
||||
} else {
|
||||
if data.count_columns() == 1 {
|
||||
return true;
|
||||
return vec![];
|
||||
}
|
||||
|
||||
data.truncate(data.count_columns() - 1);
|
||||
|
||||
let cell = Table::create_cell(TRAILING_COLUMN_STR, TextStyle::default());
|
||||
data.push(cell);
|
||||
truncate_columns(data, data.count_columns() - 1);
|
||||
push_empty_column(data);
|
||||
widths.pop();
|
||||
widths.push(3 + 2);
|
||||
}
|
||||
|
||||
false
|
||||
widths
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
use tabled::{
|
||||
style::RawStyle,
|
||||
style::{HorizontalLine, Line, Style},
|
||||
};
|
||||
use tabled::settings::style::{HorizontalLine, Line, RawStyle, Style};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TableTheme {
|
||||
pub(crate) theme: RawStyle,
|
||||
theme: RawStyle,
|
||||
full_theme: RawStyle,
|
||||
has_inner: bool,
|
||||
full_theme: Option<RawStyle>,
|
||||
}
|
||||
|
||||
impl TableTheme {
|
||||
pub fn basic() -> TableTheme {
|
||||
Self {
|
||||
theme: Style::ascii().into(),
|
||||
full_theme: None,
|
||||
full_theme: Style::ascii().into(),
|
||||
has_inner: true,
|
||||
}
|
||||
}
|
||||
|
@ -22,84 +19,87 @@ impl TableTheme {
|
|||
pub fn thin() -> TableTheme {
|
||||
Self {
|
||||
theme: Style::modern().into(),
|
||||
full_theme: None,
|
||||
full_theme: Style::modern().into(),
|
||||
has_inner: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn light() -> TableTheme {
|
||||
let theme = Style::blank()
|
||||
.horizontals([HorizontalLine::new(
|
||||
1,
|
||||
Line::new(Some('─'), Some('─'), None, None),
|
||||
)])
|
||||
.into();
|
||||
Self {
|
||||
theme: Style::blank()
|
||||
.horizontals([HorizontalLine::new(
|
||||
1,
|
||||
Line::new(Some('─'), Some('─'), None, None),
|
||||
)])
|
||||
.into(),
|
||||
full_theme: Some(Style::modern().into()),
|
||||
theme,
|
||||
full_theme: Style::modern().into(),
|
||||
has_inner: true,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
theme: Style::modern()
|
||||
.off_left()
|
||||
.off_right()
|
||||
.off_horizontal()
|
||||
.horizontals([HorizontalLine::new(1, Style::modern().get_horizontal())
|
||||
.left(None)
|
||||
.right(None)])
|
||||
.into(),
|
||||
full_theme: Some(Style::modern().into()),
|
||||
theme,
|
||||
full_theme: Style::modern().into(),
|
||||
has_inner: true,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
theme: Style::empty()
|
||||
.top('❤')
|
||||
.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(),
|
||||
),
|
||||
theme: theme.into(),
|
||||
full_theme: full_theme.into(),
|
||||
has_inner: true,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
theme: Style::extended()
|
||||
.off_left()
|
||||
.off_right()
|
||||
.off_horizontal()
|
||||
.horizontals([HorizontalLine::new(1, Style::extended().get_horizontal())
|
||||
.left(None)
|
||||
.right(None)])
|
||||
.into(),
|
||||
full_theme: Some(Style::extended().into()),
|
||||
theme,
|
||||
full_theme: Style::extended().into(),
|
||||
has_inner: true,
|
||||
}
|
||||
}
|
||||
|
@ -107,74 +107,53 @@ impl TableTheme {
|
|||
pub fn rounded() -> TableTheme {
|
||||
Self {
|
||||
theme: Style::rounded().into(),
|
||||
full_theme: Some(
|
||||
Style::modern()
|
||||
.top_left_corner('╭')
|
||||
.top_right_corner('╮')
|
||||
.bottom_left_corner('╰')
|
||||
.bottom_right_corner('╯')
|
||||
.into(),
|
||||
),
|
||||
full_theme: Style::modern()
|
||||
.corner_top_left('╭')
|
||||
.corner_top_right('╮')
|
||||
.corner_bottom_left('╰')
|
||||
.corner_bottom_right('╯')
|
||||
.into(),
|
||||
has_inner: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reinforced() -> TableTheme {
|
||||
let full_theme = Style::modern()
|
||||
.corner_top_left('┏')
|
||||
.corner_top_right('┓')
|
||||
.corner_bottom_left('┗')
|
||||
.corner_bottom_right('┛');
|
||||
Self {
|
||||
theme: Style::modern()
|
||||
.top_left_corner('┏')
|
||||
.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(),
|
||||
),
|
||||
theme: full_theme.clone().remove_horizontal().into(),
|
||||
full_theme: full_theme.into(),
|
||||
has_inner: true,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
theme: Style::empty()
|
||||
.top('━')
|
||||
.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(),
|
||||
),
|
||||
theme: theme.into(),
|
||||
full_theme: full_theme.into(),
|
||||
has_inner: true,
|
||||
}
|
||||
}
|
||||
|
@ -182,7 +161,7 @@ impl TableTheme {
|
|||
pub fn none() -> TableTheme {
|
||||
Self {
|
||||
theme: Style::blank().into(),
|
||||
full_theme: None,
|
||||
full_theme: Style::blank().into(),
|
||||
has_inner: true,
|
||||
}
|
||||
}
|
||||
|
@ -212,7 +191,11 @@ impl TableTheme {
|
|||
self.has_inner
|
||||
}
|
||||
|
||||
pub fn into_full(&self) -> Option<RawStyle> {
|
||||
pub fn get_theme_full(&self) -> RawStyle {
|
||||
self.full_theme.clone()
|
||||
}
|
||||
|
||||
pub fn get_theme(&self) -> RawStyle {
|
||||
self.theme.clone()
|
||||
}
|
||||
}
|
||||
|
|
80
crates/nu-table/src/types/collapse.rs
Normal file
80
crates/nu-table/src/types/collapse.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
601
crates/nu-table/src/types/expanded.rs
Normal file
601
crates/nu-table/src/types/expanded.rs
Normal 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)
|
||||
}
|
254
crates/nu-table/src/types/general.rs
Normal file
254
crates/nu-table/src/types/general.rs
Normal 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(),
|
||||
}
|
||||
}
|
207
crates/nu-table/src/types/mod.rs
Normal file
207
crates/nu-table/src/types/mod.rs
Normal 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())),
|
||||
),
|
||||
)
|
||||
}
|
339
crates/nu-table/src/unstructured_table.rs
Normal file
339
crates/nu-table/src/unstructured_table.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
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 {
|
||||
|
@ -24,7 +28,7 @@ pub fn string_wrap(text: &str, width: usize, keep_words: bool) -> String {
|
|||
.build()
|
||||
.with(Style::empty())
|
||||
.with(Padding::zero())
|
||||
.with(Modify::new(Cell(0, 0)).with(wrap))
|
||||
.with(Modify::new((0, 0)).with(wrap))
|
||||
.to_string()
|
||||
}
|
||||
|
||||
|
@ -36,10 +40,5 @@ pub fn string_truncate(text: &str, width: usize) -> String {
|
|||
None => return String::new(),
|
||||
};
|
||||
|
||||
Builder::from_iter([[line]])
|
||||
.build()
|
||||
.with(Style::empty())
|
||||
.with(Padding::zero())
|
||||
.with(Width::truncate(width))
|
||||
.to_string()
|
||||
Truncate::truncate_text(line, width).into_owned()
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use nu_table::{string_width, Table, TableConfig, TextStyle};
|
||||
use tabled::papergrid::records::{cell_info::CellInfo, tcell::TCell};
|
||||
#![allow(dead_code)]
|
||||
|
||||
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 {
|
||||
cfg: TableConfig,
|
||||
termwidth: usize,
|
||||
|
@ -11,7 +10,6 @@ pub struct TestCase {
|
|||
}
|
||||
|
||||
impl TestCase {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(cfg: TableConfig, termwidth: usize, expected: Option<String>) -> Self {
|
||||
Self {
|
||||
cfg,
|
||||
|
@ -21,11 +19,9 @@ impl TestCase {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn test_table<I>(data: VecCells, tests: I)
|
||||
where
|
||||
I: IntoIterator<Item = TestCase>,
|
||||
{
|
||||
type Data = Vec<Vec<CellInfo<String>>>;
|
||||
|
||||
pub fn test_table<I: IntoIterator<Item = TestCase>>(data: Data, tests: I) {
|
||||
for (i, test) in tests.into_iter().enumerate() {
|
||||
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> {
|
||||
let mut size = (0, 0);
|
||||
for row in &data {
|
||||
size.0 += 1;
|
||||
size.1 = std::cmp::max(size.1, row.len());
|
||||
}
|
||||
|
||||
let table = Table::new(data, size);
|
||||
pub fn create_table(data: Data, config: TableConfig, termwidth: usize) -> Option<String> {
|
||||
let table = NuTable::from(data);
|
||||
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);
|
||||
|
||||
for i in 0..count_columns {
|
||||
row.push(Table::create_cell(i.to_string(), TextStyle::default()));
|
||||
row.push(CellInfo::new(i.to_string()));
|
||||
}
|
||||
|
||||
row
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn _str(s: &str) -> TCell<CellInfo<'static>, TextStyle> {
|
||||
Table::create_cell(s.to_string(), TextStyle::default())
|
||||
pub fn cell(text: &str) -> CellInfo<String> {
|
||||
CellInfo::new(text.to_string())
|
||||
}
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
mod common;
|
||||
|
||||
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]
|
||||
fn data_and_header_has_different_size() {
|
||||
let table = Table::new(vec![create_row(3), create_row(5), create_row(5)], (3, 5));
|
||||
fn data_and_header_has_different_size_doesnt_work() {
|
||||
let table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]);
|
||||
|
||||
let table = table.draw(
|
||||
TableConfig::new(theme::heavy(), true, false, false),
|
||||
TableConfig::new().theme(theme::heavy()).with_header(true),
|
||||
usize::MAX,
|
||||
);
|
||||
|
||||
|
@ -18,7 +19,7 @@ fn data_and_header_has_different_size() {
|
|||
table.as_deref(),
|
||||
Some(
|
||||
"┏━━━┳━━━┳━━━┳━━━┳━━━┓\n\
|
||||
┃ 0 ┃ 1 ┃ 2 ┃ ┃ ┃\n\
|
||||
┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃ 4 ┃\n\
|
||||
┣━━━╋━━━╋━━━╋━━━╋━━━┫\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(
|
||||
TableConfig::new(theme::heavy(), true, false, false),
|
||||
TableConfig::new().theme(theme::heavy()).with_header(true),
|
||||
usize::MAX,
|
||||
);
|
||||
|
||||
|
@ -39,8 +40,8 @@ fn data_and_header_has_different_size() {
|
|||
"┏━━━┳━━━┳━━━┳━━━┳━━━┓\n\
|
||||
┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃ 4 ┃\n\
|
||||
┣━━━╋━━━╋━━━╋━━━╋━━━┫\n\
|
||||
┃ 0 ┃ 1 ┃ 2 ┃ ┃ ┃\n\
|
||||
┃ 0 ┃ 1 ┃ 2 ┃ ┃ ┃\n\
|
||||
┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃ 4 ┃\n\
|
||||
┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃ 4 ┃\n\
|
||||
┗━━━┻━━━┻━━━┻━━━┻━━━┛"
|
||||
)
|
||||
);
|
||||
|
@ -50,14 +51,14 @@ fn data_and_header_has_different_size() {
|
|||
fn termwidth_too_small() {
|
||||
let test_loop = |config: TableConfig| {
|
||||
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);
|
||||
|
||||
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();
|
||||
test_loop(config);
|
||||
|
@ -185,9 +186,9 @@ fn truncate_with_suffix_test() {
|
|||
#[test]
|
||||
fn width_control_test_0() {
|
||||
let data = vec![
|
||||
vec![_str("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); 16],
|
||||
vec![_str("yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"); 16],
|
||||
vec![_str("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"); 16],
|
||||
vec![common::cell("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); 16],
|
||||
vec![common::cell("yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"); 16],
|
||||
vec![common::cell("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"); 16],
|
||||
];
|
||||
|
||||
let tests = [
|
||||
|
@ -202,9 +203,12 @@ fn width_control_test_0() {
|
|||
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 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)| {
|
||||
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) {
|
||||
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)| {
|
||||
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);
|
||||
}
|
||||
|
||||
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)]
|
||||
}
|
||||
|
|
|
@ -8,7 +8,10 @@ use nu_table::{TableConfig, TableTheme as theme};
|
|||
fn test_expand() {
|
||||
let table = create_table(
|
||||
vec![create_row(4); 3],
|
||||
TableConfig::new(theme::rounded(), true, false, false).expand(),
|
||||
TableConfig::new()
|
||||
.theme(theme::rounded())
|
||||
.with_header(true)
|
||||
.expand(true),
|
||||
50,
|
||||
);
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
mod common;
|
||||
|
||||
use common::{create_row as row, VecCells};
|
||||
use nu_table::{TableConfig, TableTheme as theme};
|
||||
use common::create_row as row;
|
||||
use nu_table::{NuTable, TableConfig, TableTheme as theme};
|
||||
use tabled::grid::records::vec_records::CellInfo;
|
||||
|
||||
#[test]
|
||||
fn test_rounded() {
|
||||
|
@ -47,7 +48,7 @@ fn test_rounded() {
|
|||
);
|
||||
|
||||
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!(
|
||||
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!(
|
||||
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!(
|
||||
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!(
|
||||
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!(
|
||||
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!(
|
||||
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!(
|
||||
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!(
|
||||
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!(
|
||||
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 {
|
||||
let config = TableConfig::new(theme, with_header, false, false);
|
||||
fn create_table(data: Vec<Vec<CellInfo<String>>>, with_header: bool, theme: theme) -> String {
|
||||
let mut config = TableConfig::new().theme(theme);
|
||||
if with_header {
|
||||
config = config.with_header(true);
|
||||
}
|
||||
|
||||
let out = common::create_table(data, config, usize::MAX);
|
||||
|
||||
out.expect("not expected to get None")
|
||||
}
|
||||
|
||||
fn create_table_with_size(
|
||||
data: VecCells,
|
||||
size: (usize, usize),
|
||||
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 table = nu_table::Table::new(data, size);
|
||||
let table = NuTable::from(data);
|
||||
|
||||
table
|
||||
.draw(config, usize::MAX)
|
||||
|
|
Loading…
Reference in a new issue