mirror of
https://github.com/nushell/nushell
synced 2025-01-15 22:54:16 +00:00
9d0e52b94d
# Description This PR bumps the required rust version to 1.66.1. # User-Facing Changes # Tests + Formatting Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass # After Submitting If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date.
579 lines
15 KiB
Rust
579 lines
15 KiB
Rust
use crate::table_theme::TableTheme;
|
|
use nu_ansi_term::Style;
|
|
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::{
|
|
records::{
|
|
cell_info::CellInfo, tcell::TCell, vec_records::VecRecords, Records, RecordsMut,
|
|
},
|
|
util::string_width_multiline,
|
|
width::{CfgWidthFunction, WidthEstimator},
|
|
Estimate,
|
|
},
|
|
peaker::Peaker,
|
|
Alignment, Modify, ModifyObject, TableOption, Width,
|
|
};
|
|
|
|
/// Table represent a table view.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Table {
|
|
data: Data,
|
|
}
|
|
|
|
type Data = VecRecords<TCell<CellInfo<'static>, TextStyle>>;
|
|
|
|
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
|
|
|
|
let mut data = data;
|
|
make_data_consistent(&mut data, size);
|
|
|
|
let data = VecRecords::with_hint(data, size.1);
|
|
|
|
Table { data }
|
|
}
|
|
|
|
pub fn count_rows(&self) -> usize {
|
|
self.data.count_rows()
|
|
}
|
|
|
|
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 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();
|
|
}
|
|
|
|
if total > width {
|
|
truncated = true;
|
|
self.data.truncate(self.data.count_columns() - 1);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let is_empty = self.data.count_rows() == 0 || self.data.count_columns() == 0;
|
|
if is_empty {
|
|
return true;
|
|
}
|
|
|
|
if truncated {
|
|
self.data.push(Table::create_cell(
|
|
String::from("..."),
|
|
TextStyle::default(),
|
|
));
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
}
|
|
|
|
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()),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct TableConfig {
|
|
theme: TableTheme,
|
|
alignments: Alignments,
|
|
trim: TrimStrategy,
|
|
split_color: Option<Style>,
|
|
expand: bool,
|
|
with_index: bool,
|
|
with_header: bool,
|
|
with_footer: bool,
|
|
}
|
|
|
|
impl TableConfig {
|
|
pub fn new(
|
|
theme: TableTheme,
|
|
with_header: bool,
|
|
with_index: bool,
|
|
append_footer: bool,
|
|
) -> Self {
|
|
Self {
|
|
theme,
|
|
with_header,
|
|
with_index,
|
|
with_footer: append_footer,
|
|
expand: false,
|
|
alignments: Alignments::default(),
|
|
trim: TrimStrategy::truncate(None),
|
|
split_color: None,
|
|
}
|
|
}
|
|
|
|
pub fn expand(mut self) -> Self {
|
|
self.expand = true;
|
|
self
|
|
}
|
|
|
|
pub fn trim(mut self, strategy: TrimStrategy) -> Self {
|
|
self.trim = strategy;
|
|
self
|
|
}
|
|
|
|
pub fn splitline_style(mut self, color: Style) -> Self {
|
|
self.split_color = Some(color);
|
|
self
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct Alignments {
|
|
pub(crate) data: AlignmentHorizontal,
|
|
pub(crate) index: AlignmentHorizontal,
|
|
pub(crate) header: AlignmentHorizontal,
|
|
}
|
|
|
|
impl Default for Alignments {
|
|
fn default() -> Self {
|
|
Self {
|
|
data: AlignmentHorizontal::Center,
|
|
index: AlignmentHorizontal::Right,
|
|
header: AlignmentHorizontal::Center,
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
return None;
|
|
}
|
|
|
|
if cfg.with_footer {
|
|
data.duplicate_row(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,
|
|
)
|
|
}
|
|
|
|
#[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,
|
|
termwidth: usize,
|
|
) -> Option<String> {
|
|
let mut table = Builder::custom(data).build();
|
|
load_theme(&mut table, theme, with_footer, with_header, split_color);
|
|
align_table(&mut table, alignments, with_index, with_header, with_footer);
|
|
|
|
if expand {
|
|
table.with(Width::increase(termwidth));
|
|
}
|
|
|
|
table_trim_columns(&mut table, termwidth, trim_strategy);
|
|
|
|
let text = table.to_string();
|
|
if string_width_multiline(&text) > termwidth {
|
|
None
|
|
} else {
|
|
Some(text)
|
|
}
|
|
}
|
|
|
|
fn align_table(
|
|
table: &mut tabled::Table<Data>,
|
|
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),
|
|
);
|
|
|
|
if with_header {
|
|
let alignment = Alignment::Horizontal(alignments.header);
|
|
if with_footer {
|
|
table.with(Modify::new(Rows::last()).with(alignment.clone()));
|
|
}
|
|
|
|
table.with(Modify::new(Rows::first()).with(alignment));
|
|
}
|
|
|
|
if with_index {
|
|
table.with(Modify::new(Columns::first()).with(Alignment::Horizontal(alignments.index)));
|
|
}
|
|
|
|
override_alignments(table, with_header, with_index, alignments);
|
|
}
|
|
|
|
fn override_alignments(
|
|
table: &mut tabled::Table<Data>,
|
|
header_present: bool,
|
|
index_present: bool,
|
|
alignments: Alignments,
|
|
) {
|
|
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 alignment == alignments.data {
|
|
continue;
|
|
}
|
|
|
|
table.with(
|
|
Cell(row, col)
|
|
.modify()
|
|
.with(Alignment::Horizontal(alignment)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn load_theme<R>(
|
|
table: &mut tabled::Table<R>,
|
|
theme: &TableTheme,
|
|
with_footer: bool,
|
|
with_header: bool,
|
|
separator_color: Option<Style>,
|
|
) where
|
|
R: Records,
|
|
{
|
|
let mut theme = theme.theme.clone();
|
|
if !with_header {
|
|
theme.set_horizontals(HashMap::default());
|
|
}
|
|
|
|
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),
|
|
);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn table_trim_columns(
|
|
table: &mut tabled::Table<Data>,
|
|
termwidth: usize,
|
|
trim_strategy: &TrimStrategy,
|
|
) {
|
|
table.with(TrimStrategyModifier::new(termwidth, trim_strategy));
|
|
}
|
|
|
|
pub struct TrimStrategyModifier<'a> {
|
|
termwidth: usize,
|
|
trim_strategy: &'a TrimStrategy,
|
|
}
|
|
|
|
impl<'a> TrimStrategyModifier<'a> {
|
|
pub fn new(termwidth: usize, trim_strategy: &'a TrimStrategy) -> Self {
|
|
Self {
|
|
termwidth,
|
|
trim_strategy,
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
const TERMWIDTH_THRESHOLD: usize = 120;
|
|
|
|
if data.count_columns() == 0 {
|
|
return true;
|
|
}
|
|
|
|
let truncate = if termwidth > TERMWIDTH_THRESHOLD {
|
|
truncate_columns_by_columns
|
|
} else {
|
|
truncate_columns_by_content
|
|
};
|
|
|
|
truncate(data, theme, termwidth)
|
|
}
|
|
|
|
// 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 {
|
|
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();
|
|
}
|
|
|
|
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 {
|
|
width += column_width;
|
|
width += vertical_border_i;
|
|
|
|
if width >= termwidth {
|
|
// check whether we CAN limit the column width
|
|
width -= column_width;
|
|
width += MIN_ACCEPTABLE_WIDTH;
|
|
|
|
if width <= termwidth {
|
|
truncate_pos += 1;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
truncate_pos += 1;
|
|
}
|
|
|
|
// we don't need any truncation then (is it possible?)
|
|
if truncate_pos == data.count_columns() {
|
|
return false;
|
|
}
|
|
|
|
if truncate_pos == 0 {
|
|
return true;
|
|
}
|
|
|
|
data.truncate(truncate_pos);
|
|
|
|
// Append columns with a trailing column
|
|
|
|
let min_width = borders.has_left() as usize
|
|
+ borders.has_right() as usize
|
|
+ data.count_columns() * MIN_ACCEPTABLE_WIDTH
|
|
+ (data.count_columns() - 1) * vertical_border_i;
|
|
|
|
let diff = termwidth - min_width;
|
|
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);
|
|
} else {
|
|
if data.count_columns() == 1 {
|
|
return true;
|
|
}
|
|
|
|
data.truncate(data.count_columns() - 1);
|
|
|
|
let cell = Table::create_cell(String::from(TRAILING_COLUMN_STR), TextStyle::default());
|
|
data.push(cell);
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
// 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 {
|
|
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();
|
|
}
|
|
|
|
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;
|
|
|
|
if min_total <= termwidth {
|
|
return false;
|
|
}
|
|
|
|
let mut i = 0;
|
|
while data.count_columns() > 0 {
|
|
i += 1;
|
|
|
|
let column = data.count_columns() - 1 - i;
|
|
let width = min(widths[column], ACCEPTABLE_WIDTH);
|
|
min_total -= width;
|
|
|
|
if config.get_borders().has_vertical() {
|
|
min_total -= 1;
|
|
}
|
|
|
|
if min_total <= termwidth {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if i + 1 == data.count_columns() {
|
|
return true;
|
|
}
|
|
|
|
data.truncate(data.count_columns() - i);
|
|
|
|
// 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);
|
|
} else {
|
|
if data.count_columns() == 1 {
|
|
return true;
|
|
}
|
|
|
|
data.truncate(data.count_columns() - 1);
|
|
|
|
let cell = Table::create_cell(TRAILING_COLUMN_STR, TextStyle::default());
|
|
data.push(cell);
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// The same as [`tabled::peaker::PriorityMax`] but prioritizes left columns first in case of equal width.
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct PriorityMax;
|
|
|
|
impl Peaker for PriorityMax {
|
|
fn create() -> Self {
|
|
Self
|
|
}
|
|
|
|
fn peak(&mut self, _: &[usize], widths: &[usize]) -> Option<usize> {
|
|
let col = (0..widths.len()).rev().max_by_key(|&i| widths[i]);
|
|
col.filter(|&col| widths[col] != 0)
|
|
}
|
|
}
|