mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 15:44:59 +02:00
5
crates/nu-table/src/lib.rs
Normal file
5
crates/nu-table/src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod table;
|
||||
mod wrap;
|
||||
|
||||
pub use table::{draw_table, StyledString, Table, TextStyle, Theme};
|
||||
pub use wrap::Alignment;
|
23
crates/nu-table/src/main.rs
Normal file
23
crates/nu-table/src/main.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use nu_table::{draw_table, StyledString, Table, TextStyle, Theme};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
|
||||
let width = args[1].parse::<usize>().expect("Need a width in columns");
|
||||
let msg = args[2..]
|
||||
.iter()
|
||||
.map(|x| StyledString::new(x.to_owned(), TextStyle::basic()))
|
||||
.collect();
|
||||
|
||||
let t = Table::new(
|
||||
vec![
|
||||
StyledString::new("Test me".to_owned(), TextStyle::default_header()),
|
||||
StyledString::new("Long column name".to_owned(), TextStyle::default_header()),
|
||||
StyledString::new("Another".to_owned(), TextStyle::default_header()),
|
||||
],
|
||||
vec![msg; 2],
|
||||
Theme::compact(),
|
||||
);
|
||||
|
||||
draw_table(&t, width);
|
||||
}
|
673
crates/nu-table/src/table.rs
Normal file
673
crates/nu-table/src/table.rs
Normal file
@ -0,0 +1,673 @@
|
||||
use crate::wrap::{column_width, split_sublines, wrap, Alignment, Subline, WrappedCell};
|
||||
|
||||
enum SeparatorPosition {
|
||||
Top,
|
||||
Middle,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Table {
|
||||
pub headers: Vec<StyledString>,
|
||||
pub data: Vec<Vec<StyledString>>,
|
||||
pub theme: Theme,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StyledString {
|
||||
pub contents: String,
|
||||
pub style: TextStyle,
|
||||
}
|
||||
|
||||
impl StyledString {
|
||||
pub fn new(contents: String, style: TextStyle) -> StyledString {
|
||||
StyledString { contents, style }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextStyle {
|
||||
pub is_bold: bool,
|
||||
pub alignment: Alignment,
|
||||
pub color: Option<ansi_term::Colour>,
|
||||
}
|
||||
|
||||
impl TextStyle {
|
||||
pub fn basic() -> TextStyle {
|
||||
TextStyle {
|
||||
is_bold: false,
|
||||
alignment: Alignment::Left,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn basic_right() -> TextStyle {
|
||||
TextStyle {
|
||||
is_bold: false,
|
||||
alignment: Alignment::Right,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_header() -> TextStyle {
|
||||
TextStyle {
|
||||
is_bold: true,
|
||||
alignment: Alignment::Center,
|
||||
color: Some(ansi_term::Colour::Green),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Theme {
|
||||
pub top_left: char,
|
||||
pub middle_left: char,
|
||||
pub bottom_left: char,
|
||||
pub top_center: char,
|
||||
pub center: char,
|
||||
pub bottom_center: char,
|
||||
pub top_right: char,
|
||||
pub middle_right: char,
|
||||
pub bottom_right: char,
|
||||
pub top_horizontal: char,
|
||||
pub middle_horizontal: char,
|
||||
pub bottom_horizontal: char,
|
||||
pub left_vertical: char,
|
||||
pub center_vertical: char,
|
||||
pub right_vertical: char,
|
||||
|
||||
pub separate_header: bool,
|
||||
pub separate_rows: bool,
|
||||
|
||||
pub print_left_border: bool,
|
||||
pub print_right_border: bool,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
#[allow(unused)]
|
||||
pub fn basic() -> Theme {
|
||||
Theme {
|
||||
top_left: '+',
|
||||
middle_left: '+',
|
||||
bottom_left: '+',
|
||||
top_center: '+',
|
||||
center: '+',
|
||||
bottom_center: '+',
|
||||
top_right: '+',
|
||||
middle_right: '+',
|
||||
bottom_right: '+',
|
||||
top_horizontal: '-',
|
||||
middle_horizontal: '-',
|
||||
bottom_horizontal: '-',
|
||||
left_vertical: '|',
|
||||
center_vertical: '|',
|
||||
right_vertical: '|',
|
||||
|
||||
separate_header: true,
|
||||
separate_rows: true,
|
||||
|
||||
print_left_border: true,
|
||||
print_right_border: true,
|
||||
}
|
||||
}
|
||||
#[allow(unused)]
|
||||
pub fn thin() -> Theme {
|
||||
Theme {
|
||||
top_left: '┌',
|
||||
middle_left: '├',
|
||||
bottom_left: '└',
|
||||
top_center: '┬',
|
||||
center: '┼',
|
||||
bottom_center: '┴',
|
||||
top_right: '┐',
|
||||
middle_right: '┤',
|
||||
bottom_right: '┘',
|
||||
|
||||
top_horizontal: '─',
|
||||
middle_horizontal: '─',
|
||||
bottom_horizontal: '─',
|
||||
|
||||
left_vertical: '│',
|
||||
center_vertical: '│',
|
||||
right_vertical: '│',
|
||||
|
||||
separate_header: true,
|
||||
separate_rows: true,
|
||||
|
||||
print_left_border: true,
|
||||
print_right_border: true,
|
||||
}
|
||||
}
|
||||
#[allow(unused)]
|
||||
pub fn light() -> Theme {
|
||||
Theme {
|
||||
top_left: ' ',
|
||||
middle_left: '─',
|
||||
bottom_left: ' ',
|
||||
top_center: ' ',
|
||||
center: '─',
|
||||
bottom_center: ' ',
|
||||
top_right: ' ',
|
||||
middle_right: '─',
|
||||
bottom_right: ' ',
|
||||
|
||||
top_horizontal: ' ',
|
||||
middle_horizontal: '─',
|
||||
bottom_horizontal: ' ',
|
||||
|
||||
left_vertical: ' ',
|
||||
center_vertical: ' ',
|
||||
right_vertical: ' ',
|
||||
|
||||
separate_header: true,
|
||||
separate_rows: true,
|
||||
|
||||
print_left_border: true,
|
||||
print_right_border: true,
|
||||
}
|
||||
}
|
||||
#[allow(unused)]
|
||||
pub fn compact() -> Theme {
|
||||
Theme {
|
||||
top_left: '─',
|
||||
middle_left: '─',
|
||||
bottom_left: '─',
|
||||
top_center: '┬',
|
||||
center: '┼',
|
||||
bottom_center: '┴',
|
||||
top_right: '─',
|
||||
middle_right: '─',
|
||||
bottom_right: '─',
|
||||
top_horizontal: '─',
|
||||
middle_horizontal: '─',
|
||||
bottom_horizontal: '─',
|
||||
|
||||
left_vertical: ' ',
|
||||
center_vertical: '│',
|
||||
right_vertical: ' ',
|
||||
|
||||
separate_header: true,
|
||||
separate_rows: false,
|
||||
|
||||
print_left_border: false,
|
||||
print_right_border: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Table {
|
||||
pub fn new(headers: Vec<StyledString>, data: Vec<Vec<StyledString>>, theme: Theme) -> Table {
|
||||
Table {
|
||||
headers,
|
||||
data,
|
||||
theme,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProcessedTable<'a> {
|
||||
pub headers: Vec<ProcessedCell<'a>>,
|
||||
pub data: Vec<Vec<ProcessedCell<'a>>>,
|
||||
pub theme: Theme,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProcessedCell<'a> {
|
||||
pub contents: Vec<Subline<'a>>,
|
||||
pub style: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WrappedTable {
|
||||
pub column_widths: Vec<usize>,
|
||||
pub headers: Vec<WrappedCell>,
|
||||
pub data: Vec<Vec<WrappedCell>>,
|
||||
pub theme: Theme,
|
||||
}
|
||||
|
||||
impl WrappedTable {
|
||||
//TODO: optimize this
|
||||
fn print_separator(&self, separator_position: SeparatorPosition) {
|
||||
let column_count = self.column_widths.len();
|
||||
|
||||
match separator_position {
|
||||
SeparatorPosition::Top => {
|
||||
for column in self.column_widths.iter().enumerate() {
|
||||
if column.0 == 0 && self.theme.print_left_border {
|
||||
print!("{}", self.theme.top_left);
|
||||
}
|
||||
print!(
|
||||
"{}",
|
||||
std::iter::repeat(self.theme.top_horizontal)
|
||||
.take(*column.1)
|
||||
.collect::<String>()
|
||||
);
|
||||
|
||||
print!("{}{}", self.theme.top_horizontal, self.theme.top_horizontal);
|
||||
if column.0 == column_count - 1 {
|
||||
if self.theme.print_right_border {
|
||||
print!("{}", self.theme.top_right);
|
||||
}
|
||||
} else {
|
||||
print!("{}", self.theme.top_center);
|
||||
}
|
||||
}
|
||||
}
|
||||
SeparatorPosition::Middle => {
|
||||
for column in self.column_widths.iter().enumerate() {
|
||||
if column.0 == 0 && self.theme.print_left_border {
|
||||
print!("{}", self.theme.middle_left);
|
||||
}
|
||||
print!(
|
||||
"{}",
|
||||
std::iter::repeat(self.theme.middle_horizontal)
|
||||
.take(*column.1)
|
||||
.collect::<String>()
|
||||
);
|
||||
|
||||
print!(
|
||||
"{}{}",
|
||||
self.theme.middle_horizontal, self.theme.middle_horizontal
|
||||
);
|
||||
if column.0 == column_count - 1 {
|
||||
if self.theme.print_right_border {
|
||||
print!("{}", self.theme.middle_right);
|
||||
}
|
||||
} else {
|
||||
print!("{}", self.theme.center);
|
||||
}
|
||||
}
|
||||
}
|
||||
SeparatorPosition::Bottom => {
|
||||
for column in self.column_widths.iter().enumerate() {
|
||||
if column.0 == 0 && self.theme.print_left_border {
|
||||
print!("{}", self.theme.bottom_left);
|
||||
}
|
||||
print!(
|
||||
"{}",
|
||||
std::iter::repeat(self.theme.bottom_horizontal)
|
||||
.take(*column.1)
|
||||
.collect::<String>()
|
||||
);
|
||||
|
||||
print!(
|
||||
"{}{}",
|
||||
self.theme.bottom_horizontal, self.theme.bottom_horizontal
|
||||
);
|
||||
if column.0 == column_count - 1 {
|
||||
if self.theme.print_right_border {
|
||||
print!("{}", self.theme.bottom_right);
|
||||
}
|
||||
} else {
|
||||
print!("{}", self.theme.bottom_center);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_cell_contents(&self, cells: &[WrappedCell]) {
|
||||
for current_line in 0.. {
|
||||
let mut lines_printed = 0;
|
||||
|
||||
let mut output = if self.theme.print_left_border {
|
||||
self.theme.left_vertical.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
for column in cells.iter().enumerate() {
|
||||
if let Some(line) = (column.1).lines.get(current_line) {
|
||||
let remainder = self.column_widths[column.0] - line.width;
|
||||
output.push(' ');
|
||||
|
||||
match column.1.style.alignment {
|
||||
Alignment::Left => {
|
||||
if let Some(color) = column.1.style.color {
|
||||
let color = if column.1.style.is_bold {
|
||||
color.bold()
|
||||
} else {
|
||||
color.normal()
|
||||
};
|
||||
|
||||
output.push_str(&color.paint(&line.line).to_string());
|
||||
} else {
|
||||
output.push_str(&line.line);
|
||||
}
|
||||
for _ in 0..remainder {
|
||||
output.push(' ');
|
||||
}
|
||||
}
|
||||
Alignment::Center => {
|
||||
for _ in 0..remainder / 2 {
|
||||
output.push(' ');
|
||||
}
|
||||
if let Some(color) = column.1.style.color {
|
||||
let color = if column.1.style.is_bold {
|
||||
color.bold()
|
||||
} else {
|
||||
color.normal()
|
||||
};
|
||||
|
||||
output.push_str(&color.paint(&line.line).to_string());
|
||||
} else {
|
||||
output.push_str(&line.line);
|
||||
}
|
||||
for _ in 0..(remainder / 2 + remainder % 2) {
|
||||
output.push(' ');
|
||||
}
|
||||
}
|
||||
Alignment::Right => {
|
||||
for _ in 0..remainder {
|
||||
output.push(' ');
|
||||
}
|
||||
if let Some(color) = column.1.style.color {
|
||||
let color = if column.1.style.is_bold {
|
||||
color.bold()
|
||||
} else {
|
||||
color.normal()
|
||||
};
|
||||
|
||||
output.push_str(&color.paint(&line.line).to_string());
|
||||
} else {
|
||||
output.push_str(&line.line);
|
||||
}
|
||||
}
|
||||
}
|
||||
output.push(' ');
|
||||
lines_printed += 1;
|
||||
} else {
|
||||
for _ in 0..self.column_widths[column.0] + 2 {
|
||||
output.push(' ');
|
||||
}
|
||||
}
|
||||
if column.0 < cells.len() - 1 {
|
||||
output.push(self.theme.center_vertical);
|
||||
} else if self.theme.print_right_border {
|
||||
output.push(self.theme.right_vertical);
|
||||
}
|
||||
}
|
||||
if lines_printed == 0 {
|
||||
break;
|
||||
} else {
|
||||
println!("{}", output);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn new_print_table(&self) {
|
||||
if self.data.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.print_separator(SeparatorPosition::Top);
|
||||
println!();
|
||||
|
||||
self.print_cell_contents(&self.headers);
|
||||
|
||||
let mut first_row = true;
|
||||
|
||||
for row in &self.data {
|
||||
if !first_row {
|
||||
if self.theme.separate_rows {
|
||||
self.print_separator(SeparatorPosition::Middle);
|
||||
println!();
|
||||
}
|
||||
} else {
|
||||
first_row = false;
|
||||
|
||||
if self.theme.separate_header {
|
||||
self.print_separator(SeparatorPosition::Middle);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
self.print_cell_contents(row);
|
||||
}
|
||||
self.print_separator(SeparatorPosition::Bottom);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
fn process_table(table: &Table) -> ProcessedTable {
|
||||
let mut processed_data = vec![];
|
||||
for row in &table.data {
|
||||
let mut out_row = vec![];
|
||||
for column in row {
|
||||
out_row.push(ProcessedCell {
|
||||
contents: split_sublines(&column.contents).collect::<Vec<_>>(),
|
||||
style: column.style.clone(),
|
||||
});
|
||||
}
|
||||
processed_data.push(out_row);
|
||||
}
|
||||
|
||||
let mut processed_headers = vec![];
|
||||
for header in &table.headers {
|
||||
processed_headers.push(ProcessedCell {
|
||||
contents: split_sublines(&header.contents).collect::<Vec<_>>(),
|
||||
style: header.style.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
ProcessedTable {
|
||||
headers: processed_headers,
|
||||
data: processed_data,
|
||||
theme: table.theme.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_max_column_widths(processed_table: &ProcessedTable) -> Vec<usize> {
|
||||
use std::cmp::max;
|
||||
|
||||
let mut max_num_columns = 0;
|
||||
|
||||
max_num_columns = max(max_num_columns, processed_table.headers.len());
|
||||
|
||||
for row in &processed_table.data {
|
||||
max_num_columns = max(max_num_columns, row.len());
|
||||
}
|
||||
|
||||
let mut output = vec![0; max_num_columns];
|
||||
|
||||
for column in processed_table.headers.iter().enumerate() {
|
||||
output[column.0] = max(output[column.0], column_width(column.1.contents.iter()));
|
||||
}
|
||||
|
||||
for row in &processed_table.data {
|
||||
for column in row.iter().enumerate() {
|
||||
output[column.0] = max(output[column.0], column_width(column.1.contents.iter()));
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn draw_table(table: &Table, termwidth: usize) {
|
||||
// Remove the edges, if used
|
||||
let termwidth = if table.theme.print_left_border && table.theme.print_right_border {
|
||||
termwidth - 2
|
||||
} else if table.theme.print_left_border || table.theme.print_right_border {
|
||||
termwidth - 1
|
||||
} else {
|
||||
termwidth
|
||||
};
|
||||
|
||||
let processed_table = process_table(table);
|
||||
|
||||
let max_per_column = get_max_column_widths(&processed_table);
|
||||
|
||||
// maybe_truncate_columns(&mut headers, &mut entries, termwidth);
|
||||
let headers_len = table.headers.len();
|
||||
|
||||
// Measure how big our columns need to be (accounting for separators also)
|
||||
let max_naive_column_width = (termwidth - 3 * (headers_len - 1)) / headers_len;
|
||||
|
||||
let column_space = ColumnSpace::measure(&max_per_column, max_naive_column_width, headers_len);
|
||||
|
||||
// This gives us the max column width
|
||||
let max_column_width = column_space.max_width(termwidth);
|
||||
|
||||
// This width isn't quite right, as we're rounding off some of our space
|
||||
let column_space = column_space.fix_almost_column_width(
|
||||
&max_per_column,
|
||||
max_naive_column_width,
|
||||
max_column_width,
|
||||
headers_len,
|
||||
);
|
||||
|
||||
// This should give us the final max column width
|
||||
let max_column_width = column_space.max_width(termwidth);
|
||||
|
||||
let wrapped_table = wrap_cells(
|
||||
processed_table,
|
||||
// max_per_column,
|
||||
// max_naive_column_width,
|
||||
max_column_width,
|
||||
);
|
||||
|
||||
wrapped_table.new_print_table();
|
||||
}
|
||||
|
||||
fn wrap_cells(processed_table: ProcessedTable, max_column_width: usize) -> WrappedTable {
|
||||
let mut column_widths = vec![0; processed_table.headers.len()];
|
||||
let mut output_headers = vec![];
|
||||
for header in processed_table.headers.into_iter().enumerate() {
|
||||
let wrapped = wrap(
|
||||
max_column_width,
|
||||
header.1.contents.into_iter(),
|
||||
header.1.style,
|
||||
);
|
||||
if column_widths[header.0] < wrapped.max_width {
|
||||
column_widths[header.0] = wrapped.max_width;
|
||||
}
|
||||
output_headers.push(wrapped);
|
||||
}
|
||||
|
||||
let mut output_data = vec![];
|
||||
for row in processed_table.data.into_iter() {
|
||||
let mut output_row = vec![];
|
||||
for column in row.into_iter().enumerate() {
|
||||
let wrapped = wrap(
|
||||
max_column_width,
|
||||
column.1.contents.into_iter(),
|
||||
column.1.style,
|
||||
);
|
||||
if column_widths[column.0] < wrapped.max_width {
|
||||
column_widths[column.0] = wrapped.max_width;
|
||||
}
|
||||
output_row.push(wrapped);
|
||||
}
|
||||
output_data.push(output_row);
|
||||
}
|
||||
|
||||
WrappedTable {
|
||||
column_widths,
|
||||
headers: output_headers,
|
||||
data: output_data,
|
||||
theme: processed_table.theme,
|
||||
}
|
||||
}
|
||||
|
||||
struct ColumnSpace {
|
||||
num_overages: usize,
|
||||
underage_sum: usize,
|
||||
overage_separator_sum: usize,
|
||||
}
|
||||
|
||||
impl ColumnSpace {
|
||||
/// Measure how much space we have once we subtract off the columns who are small enough
|
||||
fn measure(
|
||||
max_per_column: &[usize],
|
||||
max_naive_column_width: usize,
|
||||
headers_len: usize,
|
||||
) -> ColumnSpace {
|
||||
let mut num_overages = 0;
|
||||
let mut underage_sum = 0;
|
||||
let mut overage_separator_sum = 0;
|
||||
let iter = max_per_column.iter().enumerate().take(headers_len);
|
||||
|
||||
for (i, &column_max) in iter {
|
||||
if column_max > max_naive_column_width {
|
||||
num_overages += 1;
|
||||
if i != (headers_len - 1) {
|
||||
overage_separator_sum += 3;
|
||||
}
|
||||
if i == 0 {
|
||||
overage_separator_sum += 1;
|
||||
}
|
||||
} else {
|
||||
underage_sum += column_max;
|
||||
// if column isn't last, add 3 for its separator
|
||||
if i != (headers_len - 1) {
|
||||
underage_sum += 3;
|
||||
}
|
||||
if i == 0 {
|
||||
underage_sum += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnSpace {
|
||||
num_overages,
|
||||
underage_sum,
|
||||
overage_separator_sum,
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_almost_column_width(
|
||||
self,
|
||||
max_per_column: &[usize],
|
||||
max_naive_column_width: usize,
|
||||
max_column_width: usize,
|
||||
headers_len: usize,
|
||||
) -> ColumnSpace {
|
||||
let mut num_overages = 0;
|
||||
let mut overage_separator_sum = 0;
|
||||
let mut underage_sum = self.underage_sum;
|
||||
let iter = max_per_column.iter().enumerate().take(headers_len);
|
||||
|
||||
for (i, &column_max) in iter {
|
||||
if column_max > max_naive_column_width {
|
||||
if column_max <= max_column_width {
|
||||
underage_sum += column_max;
|
||||
// if column isn't last, add 3 for its separator
|
||||
if i != (headers_len - 1) {
|
||||
underage_sum += 3;
|
||||
}
|
||||
if i == 0 {
|
||||
underage_sum += 1;
|
||||
}
|
||||
} else {
|
||||
// Column is still too large, so let's count it
|
||||
num_overages += 1;
|
||||
if i != (headers_len - 1) {
|
||||
overage_separator_sum += 3;
|
||||
}
|
||||
if i == 0 {
|
||||
overage_separator_sum += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnSpace {
|
||||
num_overages,
|
||||
underage_sum,
|
||||
overage_separator_sum,
|
||||
}
|
||||
}
|
||||
|
||||
fn max_width(&self, termwidth: usize) -> usize {
|
||||
let ColumnSpace {
|
||||
num_overages,
|
||||
underage_sum,
|
||||
overage_separator_sum,
|
||||
} = self;
|
||||
|
||||
if *num_overages > 0 {
|
||||
(termwidth - 1 - *underage_sum - *overage_separator_sum) / *num_overages
|
||||
} else {
|
||||
99999
|
||||
}
|
||||
}
|
||||
}
|
226
crates/nu-table/src/wrap.rs
Normal file
226
crates/nu-table/src/wrap.rs
Normal file
@ -0,0 +1,226 @@
|
||||
use crate::table::TextStyle;
|
||||
use std::{fmt::Display, iter::Iterator};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Subline<'a> {
|
||||
pub subline: &'a str,
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Line<'a> {
|
||||
pub sublines: Vec<Subline<'a>>,
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WrappedLine {
|
||||
pub line: String,
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WrappedCell {
|
||||
pub lines: Vec<WrappedLine>,
|
||||
pub max_width: usize,
|
||||
|
||||
pub style: TextStyle,
|
||||
}
|
||||
|
||||
impl<'a> Display for Line<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut first = true;
|
||||
for subline in &self.sublines {
|
||||
if !first {
|
||||
write!(f, " ")?;
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
write!(f, "{}", subline.subline)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split_sublines(input: &str) -> impl Iterator<Item = Subline> {
|
||||
input.split_terminator(' ').map(|x| Subline {
|
||||
subline: x,
|
||||
width: UnicodeWidthStr::width(x),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn column_width<'a>(input: impl Iterator<Item = &'a Subline<'a>>) -> usize {
|
||||
let mut total = 0;
|
||||
|
||||
let mut first = true;
|
||||
for inp in input {
|
||||
if !first {
|
||||
// Account for the space
|
||||
total += 1;
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
total += inp.width;
|
||||
}
|
||||
|
||||
total
|
||||
}
|
||||
|
||||
fn split_word<'a>(cell_width: usize, word: &'a str) -> Vec<Subline<'a>> {
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
let mut output = vec![];
|
||||
let mut current_width = 0;
|
||||
let mut start_index = 0;
|
||||
let mut end_index;
|
||||
|
||||
for c in word.char_indices() {
|
||||
if let Some(width) = c.1.width() {
|
||||
end_index = c.0;
|
||||
if current_width + width > cell_width {
|
||||
output.push(Subline {
|
||||
subline: &word[start_index..end_index],
|
||||
width: current_width,
|
||||
});
|
||||
|
||||
start_index = c.0;
|
||||
current_width = width;
|
||||
} else {
|
||||
current_width += width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if start_index != word.len() {
|
||||
output.push(Subline {
|
||||
subline: &word[start_index..],
|
||||
width: current_width,
|
||||
});
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn wrap<'a>(
|
||||
cell_width: usize,
|
||||
mut input: impl Iterator<Item = Subline<'a>>,
|
||||
style: TextStyle,
|
||||
) -> WrappedCell {
|
||||
let mut lines = vec![];
|
||||
let mut current_line: Vec<Subline> = vec![];
|
||||
let mut current_width = 0;
|
||||
let mut first = true;
|
||||
let mut max_width = 0;
|
||||
loop {
|
||||
// println!("{:?}", current_line);
|
||||
match input.next() {
|
||||
Some(item) => {
|
||||
if !first {
|
||||
current_width += 1;
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
if item.width + current_width > cell_width {
|
||||
// If this is a really long single word, we need to split the word
|
||||
if current_line.len() == 1 && current_width > cell_width {
|
||||
max_width = cell_width;
|
||||
let sublines = split_word(cell_width, ¤t_line[0].subline);
|
||||
for subline in sublines {
|
||||
let width = subline.width;
|
||||
lines.push(Line {
|
||||
sublines: vec![subline],
|
||||
width,
|
||||
});
|
||||
}
|
||||
|
||||
first = true;
|
||||
|
||||
current_width = item.width;
|
||||
current_line = vec![item];
|
||||
} else {
|
||||
if !current_line.is_empty() {
|
||||
lines.push(Line {
|
||||
sublines: current_line,
|
||||
width: current_width,
|
||||
});
|
||||
}
|
||||
|
||||
first = true;
|
||||
|
||||
current_width = item.width;
|
||||
current_line = vec![item];
|
||||
max_width = std::cmp::max(max_width, current_width);
|
||||
}
|
||||
} else {
|
||||
current_width += item.width;
|
||||
current_line.push(item);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if current_width > cell_width {
|
||||
// We need to break up the last word
|
||||
let sublines = split_word(cell_width, ¤t_line[0].subline);
|
||||
for subline in sublines {
|
||||
let width = subline.width;
|
||||
lines.push(Line {
|
||||
sublines: vec![subline],
|
||||
width,
|
||||
});
|
||||
}
|
||||
} else if current_width > 0 {
|
||||
lines.push(Line {
|
||||
sublines: current_line,
|
||||
width: current_width,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut current_max = 0;
|
||||
let mut output = vec![];
|
||||
|
||||
for line in lines {
|
||||
let mut current_line_width = 0;
|
||||
let mut first = true;
|
||||
let mut current_line = String::new();
|
||||
|
||||
for subline in line.sublines {
|
||||
if !first {
|
||||
current_line_width += 1 + subline.width;
|
||||
current_line.push(' ');
|
||||
current_line.push_str(subline.subline);
|
||||
} else {
|
||||
first = false;
|
||||
current_line_width = subline.width;
|
||||
current_line.push_str(subline.subline);
|
||||
}
|
||||
}
|
||||
|
||||
if current_line_width > current_max {
|
||||
current_max = current_line_width;
|
||||
}
|
||||
|
||||
output.push(WrappedLine {
|
||||
line: current_line,
|
||||
width: current_line_width,
|
||||
});
|
||||
}
|
||||
|
||||
WrappedCell {
|
||||
lines: output,
|
||||
max_width: current_max,
|
||||
style,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user