use comfy-table instead of builtin impl for table

This is to out-source the table making in terminal to the external
library.

I removed the in-house table implementation since it is not used any
more, and had been replaced by comfy-table, we use this instead.

I also have reimplemented table_max_width since new implementation
removed max width , with the new implemetation it will work again.

Signed-off-by: Perma Alesheikh <me@prma.dev>
This commit is contained in:
Perma Alesheikh 2024-05-07 13:20:25 +03:30 committed by Clément DOUIN
parent 1e448e56eb
commit 098ae380c3
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
16 changed files with 371 additions and 628 deletions

53
Cargo.lock generated
View file

@ -855,6 +855,18 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "comfy-table"
version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7"
dependencies = [
"crossterm 0.27.0",
"strum",
"strum_macros",
"unicode-width",
]
[[package]]
name = "concurrent-queue"
version = "2.4.0"
@ -985,6 +997,19 @@ dependencies = [
"winapi",
]
[[package]]
name = "crossterm"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.5.0",
"crossterm_winapi",
"libc",
"parking_lot 0.12.1",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
@ -2036,6 +2061,7 @@ dependencies = [
"clap_complete",
"clap_mangen",
"color-eyre",
"comfy-table",
"console",
"dirs 4.0.0",
"email-lib",
@ -2360,7 +2386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe95f33091b9b7b517a5849bce4dce1b550b430fc20d58059fcaa319ed895d8b"
dependencies = [
"bitflags 2.5.0",
"crossterm",
"crossterm 0.25.0",
"dyn-clone",
"fuzzy-matcher",
"fxhash",
@ -4034,6 +4060,12 @@ dependencies = [
"untrusted 0.9.0",
]
[[package]]
name = "rustversion"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
[[package]]
name = "ryu"
version = "1.0.17"
@ -4455,6 +4487,25 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
[[package]]
name = "strum_macros"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.59",
]
[[package]]
name = "subtle"
version = "2.5.0"

View file

@ -52,6 +52,7 @@ clap = { version = "4.4", features = ["derive", "wrap_help"] }
clap_complete = "4.4"
clap_mangen = "0.2"
color-eyre = "0.6.3"
comfy-table = "7.1.1"
console = "0.15.2"
dirs = "4"
email-lib = { version = "=0.24.1", default-features = false, features = ["derive", "tracing"] }

View file

@ -2,12 +2,7 @@ use clap::Parser;
use color_eyre::Result;
use tracing::info;
use crate::{
account::Accounts,
config::TomlConfig,
printer::{PrintTableOpts, Printer},
ui::arg::max_width::TableMaxWidthFlag,
};
use crate::{account::Accounts, config::TomlConfig, printer::Printer};
/// List all accounts.
///
@ -15,8 +10,13 @@ use crate::{
/// file.
#[derive(Debug, Parser)]
pub struct AccountListCommand {
#[command(flatten)]
pub table: TableMaxWidthFlag,
/// The maximum width the table should not exceed.
///
/// This argument will force the table not to exceed the given
/// width in pixels. Columns may shrink with ellipsis in order to
/// fit the width.
#[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")]
pub table_max_width: Option<u16>,
}
impl AccountListCommand {
@ -25,12 +25,7 @@ impl AccountListCommand {
let accounts: Accounts = config.accounts.iter().into();
printer.print_table(
Box::new(accounts),
PrintTableOpts {
format: &Default::default(),
max_width: self.table.max_width,
},
)
printer.print_table(accounts, self.table_max_width)?;
Ok(())
}
}

View file

@ -4,13 +4,11 @@ pub mod config;
pub(crate) mod wizard;
use color_eyre::Result;
use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table};
use serde::Serialize;
use std::{collections::hash_map::Iter, fmt, ops::Deref};
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::table::{Cell, Row, Table},
};
use crate::printer::{PrintTable, WriteColor};
use self::config::TomlAccountConfig;
@ -41,20 +39,22 @@ impl fmt::Display for Account {
}
}
impl Table for Account {
fn head() -> Row {
Row::new()
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
.cell(Cell::new("BACKENDS").bold().underline().white())
.cell(Cell::new("DEFAULT").bold().underline().white())
impl From<Account> for Row {
fn from(account: Account) -> Self {
let mut r = Row::new();
r.add_cell(Cell::new(account.name).fg(Color::Green));
r.add_cell(Cell::new(account.backend).fg(Color::Blue));
r.add_cell(Cell::new(if account.default { "yes" } else { "" }).fg(Color::White));
r
}
fn row(&self) -> Row {
let default = if self.default { "yes" } else { "" };
Row::new()
.cell(Cell::new(&self.name).shrinkable().green())
.cell(Cell::new(&self.backend).blue())
.cell(Cell::new(default).white())
}
impl From<&Account> for Row {
fn from(account: &Account) -> Self {
let mut r = Row::new();
r.add_cell(Cell::new(&account.name).fg(Color::Green));
r.add_cell(Cell::new(&account.backend).fg(Color::Blue));
r.add_cell(Cell::new(if account.default { "yes" } else { "" }).fg(Color::White));
r
}
}
@ -70,10 +70,46 @@ impl Deref for Accounts {
}
}
impl From<Accounts> for Table {
fn from(accounts: Accounts) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("BACKENDS").add_attribute(Attribute::Reverse),
Cell::new("DEFAULT").add_attribute(Attribute::Reverse),
]))
.add_rows(accounts.0.into_iter().map(Row::from));
table
}
}
impl From<&Accounts> for Table {
fn from(accounts: &Accounts) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("BACKENDS").add_attribute(Attribute::Reverse),
Cell::new("DEFAULT").add_attribute(Attribute::Reverse),
]))
.add_rows(accounts.0.iter().map(Row::from));
table
}
}
impl PrintTable for Accounts {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option<u16>) -> Result<()> {
let mut table = Table::from(self);
if let Some(width) = table_max_width {
table.set_width(width);
}
writeln!(writer)?;
Table::print(writer, self, opts)?;
write!(writer, "{}", table)?;
writeln!(writer)?;
Ok(())
}

View file

@ -11,12 +11,8 @@ use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag,
printer::{PrintTableOpts, Printer},
ui::arg::max_width::TableMaxWidthFlag,
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag, printer::Printer,
};
/// List all envelopes.
@ -41,9 +37,6 @@ pub struct ListEnvelopesCommand {
#[arg(long, short = 's', value_name = "NUMBER")]
pub page_size: Option<usize>,
#[command(flatten)]
pub table: TableMaxWidthFlag,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
@ -51,6 +44,14 @@ pub struct ListEnvelopesCommand {
#[command(flatten)]
pub account: AccountNameFlag,
/// The maximum width the table should not exceed.
///
/// This argument will force the table not to exceed the given
/// width in pixels. Columns may shrink with ellipsis in order to
/// fit the width.
#[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")]
pub table_max_width: Option<u16>,
/// The list envelopes filter and sort query.
///
/// The query can be a filter query, a sort query or both
@ -128,11 +129,11 @@ impl Default for ListEnvelopesCommand {
folder: Default::default(),
page: 1,
page_size: Default::default(),
table: Default::default(),
#[cfg(feature = "account-sync")]
cache: Default::default(),
account: Default::default(),
query: Default::default(),
table_max_width: Default::default(),
}
}
}
@ -197,13 +198,7 @@ impl ListEnvelopesCommand {
let envelopes = backend.list_envelopes(folder, opts).await?;
printer.print_table(
Box::new(envelopes),
PrintTableOpts {
format: &account_config.get_message_read_format(),
max_width: self.table.max_width,
},
)?;
printer.print_table(envelopes, self.table_max_width)?;
Ok(())
}

View file

@ -4,6 +4,7 @@ pub mod config;
pub mod flag;
use color_eyre::Result;
use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table};
use email::account::config::AccountConfig;
use serde::Serialize;
use std::ops;
@ -11,8 +12,7 @@ use std::ops;
use crate::{
cache::IdMapper,
flag::{Flag, Flags},
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
printer::{PrintTable, WriteColor},
};
#[derive(Clone, Debug, Default, Serialize)]
@ -30,49 +30,125 @@ pub struct Envelope {
pub to: Mailbox,
pub date: String,
}
impl From<Envelope> for Row {
fn from(envelope: Envelope) -> Self {
let mut all_attributes = vec![];
impl Table for Envelope {
fn head() -> Row {
Row::new()
.cell(Cell::new("ID").bold().underline().white())
.cell(Cell::new("FLAGS").bold().underline().white())
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
.cell(Cell::new("FROM").bold().underline().white())
.cell(Cell::new("DATE").bold().underline().white())
}
let unseen = !envelope.flags.contains(&Flag::Seen);
if unseen {
all_attributes.push(Attribute::Bold)
}
fn row(&self) -> Row {
let id = self.id.to_string();
let unseen = !self.flags.contains(&Flag::Seen);
let flags = {
let mut flags = String::new();
flags.push_str(if !unseen { " " } else { "" });
flags.push_str(if self.flags.contains(&Flag::Answered) {
""
flags.push(if !unseen { ' ' } else { '✷' });
flags.push(if envelope.flags.contains(&Flag::Answered) {
'↵'
} else {
" "
' '
});
flags.push_str(if self.flags.contains(&Flag::Flagged) {
""
flags.push(if envelope.flags.contains(&Flag::Flagged) {
'⚑'
} else {
" "
' '
});
flags
};
let subject = &self.subject;
let sender = if let Some(name) = &self.from.name {
name
} else {
&self.from.addr
};
let date = &self.date;
Row::new()
.cell(Cell::new(id).bold_if(unseen).red())
.cell(Cell::new(flags).bold_if(unseen).white())
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
.cell(Cell::new(sender).bold_if(unseen).blue())
.cell(Cell::new(date).bold_if(unseen).yellow())
let mut row = Row::new();
row.add_cell(
Cell::new(envelope.id)
.add_attributes(all_attributes.clone())
.fg(Color::Red),
)
.add_cell(
Cell::new(flags)
.add_attributes(all_attributes.clone())
.fg(Color::White),
)
.add_cell(
Cell::new(envelope.subject)
.add_attributes(all_attributes.clone())
.fg(Color::Green),
)
.add_cell(
Cell::new(if let Some(name) = envelope.from.name {
name
} else {
envelope.from.addr
})
.add_attributes(all_attributes.clone())
.fg(Color::Blue),
)
.add_cell(
Cell::new(envelope.date)
.add_attributes(all_attributes)
.fg(Color::Yellow),
);
row
}
}
impl From<&Envelope> for Row {
fn from(envelope: &Envelope) -> Self {
let mut all_attributes = vec![];
let unseen = !envelope.flags.contains(&Flag::Seen);
if unseen {
all_attributes.push(Attribute::Bold)
}
let flags = {
let mut flags = String::new();
flags.push(if !unseen { ' ' } else { '✷' });
flags.push(if envelope.flags.contains(&Flag::Answered) {
'↵'
} else {
' '
});
flags.push(if envelope.flags.contains(&Flag::Flagged) {
'⚑'
} else {
' '
});
flags
};
let mut row = Row::new();
row.add_cell(
Cell::new(&envelope.id)
.add_attributes(all_attributes.clone())
.fg(Color::Red),
)
.add_cell(
Cell::new(flags)
.add_attributes(all_attributes.clone())
.fg(Color::White),
)
.add_cell(
Cell::new(&envelope.subject)
.add_attributes(all_attributes.clone())
.fg(Color::Green),
)
.add_cell(
Cell::new(if let Some(name) = &envelope.from.name {
name
} else {
&envelope.from.addr
})
.add_attributes(all_attributes.clone())
.fg(Color::Blue),
)
.add_cell(
Cell::new(&envelope.date)
.add_attributes(all_attributes)
.fg(Color::Yellow),
);
row
}
}
@ -80,6 +156,44 @@ impl Table for Envelope {
#[derive(Clone, Debug, Default, Serialize)]
pub struct Envelopes(Vec<Envelope>);
impl From<Envelopes> for Table {
fn from(envelopes: Envelopes) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("ID").add_attribute(Attribute::Reverse),
Cell::new("FLAGS").add_attribute(Attribute::Reverse),
Cell::new("SUBJECT").add_attribute(Attribute::Reverse),
Cell::new("FROM").add_attribute(Attribute::Reverse),
Cell::new("DATE").add_attribute(Attribute::Reverse),
]))
.add_rows(envelopes.0.into_iter().map(Row::from));
table
}
}
impl From<&Envelopes> for Table {
fn from(envelopes: &Envelopes) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("ID").add_attribute(Attribute::Reverse),
Cell::new("FLAGS").add_attribute(Attribute::Reverse),
Cell::new("SUBJECT").add_attribute(Attribute::Reverse),
Cell::new("FROM").add_attribute(Attribute::Reverse),
Cell::new("DATE").add_attribute(Attribute::Reverse),
]))
.add_rows(envelopes.0.iter().map(Row::from));
table
}
}
impl Envelopes {
pub fn from_backend(
config: &AccountConfig,
@ -119,9 +233,13 @@ impl ops::Deref for Envelopes {
}
impl PrintTable for Envelopes {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option<u16>) -> Result<()> {
let mut table = Table::from(self);
if let Some(width) = table_max_width {
table.set_width(width);
}
writeln!(writer)?;
Table::print(writer, self, opts)?;
write!(writer, "{}", table)?;
writeln!(writer)?;
Ok(())
}

View file

@ -6,12 +6,8 @@ use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
folder::Folders,
printer::{PrintTableOpts, Printer},
ui::arg::max_width::TableMaxWidthFlag,
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, folder::Folders,
printer::Printer,
};
/// List all folders.
@ -19,15 +15,20 @@ use crate::{
/// This command allows you to list all exsting folders.
#[derive(Debug, Parser)]
pub struct FolderListCommand {
#[command(flatten)]
pub table: TableMaxWidthFlag,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
/// The maximum width the table should not exceed.
///
/// This argument will force the table not to exceed the given
/// width in pixels. Columns may shrink with ellipsis in order to
/// fit the width.
#[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")]
pub table_max_width: Option<u16>,
}
impl FolderListCommand {
@ -52,12 +53,7 @@ impl FolderListCommand {
let folders: Folders = backend.list_folders().await?.into();
printer.print_table(
Box::new(folders),
PrintTableOpts {
format: &account_config.get_message_read_format(),
max_width: self.table.max_width,
},
)
printer.print_table(folders, self.table_max_width)?;
Ok(())
}
}

View file

@ -3,13 +3,11 @@ pub mod command;
pub mod config;
use color_eyre::Result;
use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table};
use serde::Serialize;
use std::ops;
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
use crate::printer::{PrintTable, WriteColor};
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folder {
@ -25,24 +23,58 @@ impl From<&email::folder::Folder> for Folder {
}
}
}
impl From<&Folder> for Row {
fn from(folder: &Folder) -> Self {
let mut row = Row::new();
row.add_cell(Cell::new(&folder.name).fg(comfy_table::Color::Blue));
row.add_cell(Cell::new(&folder.desc).fg(comfy_table::Color::Green));
impl Table for Folder {
fn head() -> Row {
Row::new()
.cell(Cell::new("NAME").bold().underline().white())
.cell(Cell::new("DESC").bold().underline().white())
row
}
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(&self.name).blue())
.cell(Cell::new(&self.desc).green())
impl From<Folder> for Row {
fn from(folder: Folder) -> Self {
let mut row = Row::new();
row.add_cell(Cell::new(folder.name).fg(comfy_table::Color::Blue));
row.add_cell(Cell::new(folder.desc).fg(comfy_table::Color::Green));
row
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folders(Vec<Folder>);
impl From<Folders> for Table {
fn from(folders: Folders) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("DESC").add_attribute(Attribute::Reverse),
]))
.add_rows(folders.0.into_iter().map(Row::from));
table
}
}
impl From<&Folders> for Table {
fn from(folders: &Folders) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("DESC").add_attribute(Attribute::Reverse),
]))
.add_rows(folders.0.iter().map(Row::from));
table
}
}
impl ops::Deref for Folders {
type Target = Vec<Folder>;
@ -58,9 +90,13 @@ impl From<email::folder::Folders> for Folders {
}
impl PrintTable for Folders {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option<u16>) -> Result<()> {
let mut table = Table::from(self);
if let Some(width) = table_max_width {
table.set_width(width);
}
writeln!(writer)?;
Table::print(writer, self, opts)?;
write!(writer, "{}", table)?;
writeln!(writer)?;
Ok(())
}

View file

@ -1,8 +1,13 @@
pub mod print;
pub mod print_table;
#[allow(clippy::module_inception)]
pub mod printer;
use std::io;
pub use print::*;
pub use print_table::*;
pub use printer::*;
use termcolor::StandardStream;
pub trait WriteColor: io::Write + termcolor::WriteColor {}
impl WriteColor for StandardStream {}

View file

@ -1,17 +0,0 @@
use color_eyre::Result;
use email::email::config::EmailTextPlainFormat;
use std::io;
use termcolor::{self, StandardStream};
pub trait WriteColor: io::Write + termcolor::WriteColor {}
impl WriteColor for StandardStream {}
pub trait PrintTable {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()>;
}
pub struct PrintTableOpts<'a> {
pub format: &'a EmailTextPlainFormat,
pub max_width: Option<usize>,
}

View file

@ -1,12 +1,15 @@
use clap::ArgMatches;
use color_eyre::{eyre::Context, Report, Result};
use std::fmt::{self, Debug};
use std::fmt::Debug;
use termcolor::StandardStream;
use crate::{
output::{args, ColorFmt, OutputFmt},
printer::{Print, PrintTable, PrintTableOpts, WriteColor},
printer::{Print, WriteColor},
};
pub trait PrintTable {
fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option<u16>) -> Result<()>;
}
pub trait Printer {
// TODO: rename end
@ -14,12 +17,12 @@ pub trait Printer {
// TODO: rename log
fn print_log<T: Debug + Print>(&mut self, data: T) -> Result<()>;
// TODO: rename table
fn print_table<T: Debug + erased_serde::Serialize + PrintTable + ?Sized>(
fn print_table<T: Debug + PrintTable>(
&mut self,
// TODO: remove Box
data: Box<T>,
opts: PrintTableOpts,
data: T,
table_max_width: Option<u16>,
) -> Result<()>;
fn is_json(&self) -> bool;
}
@ -59,25 +62,17 @@ impl Printer for StdoutPrinter {
}
}
fn print_table<T: fmt::Debug + erased_serde::Serialize + PrintTable + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> Result<()> {
match self.fmt {
OutputFmt::Plain => data.print_table(self.writer.as_mut(), opts),
OutputFmt::Json => {
let json = &mut serde_json::Serializer::new(self.writer.as_mut());
let ser = &mut <dyn erased_serde::Serializer>::erase(json);
data.erased_serialize(ser).unwrap();
Ok(())
}
}
}
fn is_json(&self) -> bool {
self.fmt == OutputFmt::Json
}
fn print_table<T: Debug + PrintTable>(
&mut self,
data: T,
table_max_width: Option<u16>,
) -> Result<()> {
data.print_table(self.writer.as_mut(), table_max_width)
}
}
impl From<OutputFmt> for StdoutPrinter {

View file

@ -1,6 +1,3 @@
pub mod choice;
pub mod editor;
pub(crate) mod prompt;
pub mod table;
pub use self::table::*;

View file

@ -1,13 +0,0 @@
use clap::Parser;
/// The table max width argument parser.
#[derive(Debug, Default, Parser)]
pub struct TableMaxWidthFlag {
/// The maximum width the table should not exceed.
///
/// This argument will force the table not to exceed the given
/// width in pixels. Columns may shrink with ellipsis in order to
/// fit the width.
#[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")]
pub max_width: Option<usize>,
}

View file

@ -1 +0,0 @@
pub mod max_width;

View file

@ -1,5 +0,0 @@
pub mod arg;
#[allow(clippy::module_inception)]
pub mod table;
pub use table::*;

View file

@ -1,446 +0,0 @@
//! Toolbox for building responsive tables.
//! A table is composed of rows, a row is composed of cells.
//! The toolbox uses the [builder design pattern].
//!
//! [builder design pattern]: https://refactoring.guru/design-patterns/builder
use color_eyre::{eyre::Context, Result};
use email::email::config::EmailTextPlainFormat;
use termcolor::{Color, ColorSpec};
use terminal_size::terminal_size;
use tracing::trace;
use unicode_width::UnicodeWidthStr;
use crate::printer::{Print, PrintTableOpts, WriteColor};
/// Defines the default terminal size.
/// This is used when the size cannot be determined by the `terminal_size` crate.
/// TODO: make this customizable.
pub const DEFAULT_TERM_WIDTH: usize = 80;
/// Defines the minimum size of a shrunk cell.
/// TODO: make this customizable.
pub const MAX_SHRINK_WIDTH: usize = 5;
/// Represents a cell in a table.
#[derive(Debug, Default)]
pub struct Cell {
/// Represents the style of the cell.
style: ColorSpec,
/// Represents the content of the cell.
value: String,
/// (Dis)allowes the cell to shrink when the table exceeds the container width.
shrinkable: bool,
}
impl Cell {
pub fn new<T: AsRef<str>>(value: T) -> Self {
Self {
// Removes carriage returns, new line feeds, tabulations
// and [variation selectors].
//
// [variation selectors]: https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)
value: String::from(value.as_ref()).replace(
|c| ['\r', '\n', '\t', '\u{fe0e}', '\u{fe0f}'].contains(&c),
"",
),
..Self::default()
}
}
/// Returns the unicode width of the cell's value.
pub fn unicode_width(&self) -> usize {
UnicodeWidthStr::width(self.value.as_str())
}
/// Makes the cell shrinkable. If the table exceeds the terminal width, this cell will be the
/// one to shrink in order to prevent the table to overflow.
pub fn shrinkable(mut self) -> Self {
self.shrinkable = true;
self
}
/// Returns the shrinkable state of a cell.
pub fn is_shrinkable(&self) -> bool {
self.shrinkable
}
/// Applies the bold style to the cell.
pub fn bold(mut self) -> Self {
self.style.set_bold(true);
self
}
/// Applies the bold style to the cell conditionally.
pub fn bold_if(self, predicate: bool) -> Self {
if predicate {
self.bold()
} else {
self
}
}
/// Applies the underline style to the cell.
pub fn underline(mut self) -> Self {
self.style.set_underline(true);
self
}
/// Applies the red color to the cell.
pub fn red(mut self) -> Self {
self.style.set_fg(Some(Color::Red));
self
}
/// Applies the green color to the cell.
pub fn green(mut self) -> Self {
self.style.set_fg(Some(Color::Green));
self
}
/// Applies the yellow color to the cell.
pub fn yellow(mut self) -> Self {
self.style.set_fg(Some(Color::Yellow));
self
}
/// Applies the blue color to the cell.
pub fn blue(mut self) -> Self {
self.style.set_fg(Some(Color::Blue));
self
}
/// Applies the white color to the cell.
pub fn white(mut self) -> Self {
self.style.set_fg(Some(Color::White));
self
}
/// Applies the custom ansi color to the cell.
pub fn ansi_256(mut self, code: u8) -> Self {
self.style.set_fg(Some(Color::Ansi256(code)));
self
}
}
/// Makes the cell printable.
impl Print for Cell {
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
// Applies colors to the cell
writer
.set_color(&self.style)
.context(format!(r#"cannot apply colors to cell "{}""#, self.value))?;
// Writes the colorized cell to stdout
write!(writer, "{}", self.value)
.context(format!(r#"cannot print cell "{}""#, self.value))?;
Ok(writer.reset()?)
}
}
/// Represents a row in a table.
#[derive(Debug, Default)]
pub struct Row(
/// Represents a list of cells.
pub Vec<Cell>,
);
impl Row {
pub fn new() -> Self {
Self::default()
}
pub fn cell(mut self, cell: Cell) -> Self {
self.0.push(cell);
self
}
}
/// Represents a table abstraction.
pub trait Table
where
Self: Sized,
{
/// Defines the header row.
fn head() -> Row;
/// Defines the row template.
fn row(&self) -> Row;
/// Writes the table to the writer.
fn print(writer: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> {
let is_format_flowed = matches!(opts.format, EmailTextPlainFormat::Flowed);
let max_width = match opts.format {
EmailTextPlainFormat::Fixed(width) => opts.max_width.unwrap_or(*width),
EmailTextPlainFormat::Flowed => 0,
EmailTextPlainFormat::Auto => opts
.max_width
.or_else(|| terminal_size().map(|(w, _)| w.0 as usize))
.unwrap_or(DEFAULT_TERM_WIDTH),
};
let mut table = vec![Self::head()];
let mut cell_widths: Vec<usize> =
table[0].0.iter().map(|cell| cell.unicode_width()).collect();
table.extend(
items
.iter()
.map(|item| {
let row = item.row();
row.0.iter().enumerate().for_each(|(i, cell)| {
cell_widths[i] = cell_widths[i].max(cell.unicode_width());
});
row
})
.collect::<Vec<_>>(),
);
trace!("cell widths: {:?}", cell_widths);
let spaces_plus_separators_len = cell_widths.len() * 2 - 1;
let table_width = cell_widths.iter().sum::<usize>() + spaces_plus_separators_len;
trace!("table width: {}", table_width);
for row in table.iter_mut() {
let mut glue = Cell::default();
for (i, cell) in row.0.iter_mut().enumerate() {
glue.print(writer)?;
let table_is_overflowing = table_width > max_width;
if table_is_overflowing && !is_format_flowed && cell.is_shrinkable() {
trace!("table is overflowing and cell is shrinkable");
let shrink_width = table_width - max_width;
trace!("shrink width: {}", shrink_width);
let cell_width = if shrink_width + MAX_SHRINK_WIDTH < cell_widths[i] {
cell_widths[i] - shrink_width
} else {
MAX_SHRINK_WIDTH
};
trace!("cell width: {}", cell_width);
trace!("cell unicode width: {}", cell.unicode_width());
let cell_is_overflowing = cell.unicode_width() > cell_width;
if cell_is_overflowing {
trace!("cell is overflowing");
let mut value = String::new();
let mut chars_width = 0;
for c in cell.value.chars() {
let char_width = UnicodeWidthStr::width(c.to_string().as_str());
if chars_width + char_width >= cell_width {
break;
}
chars_width += char_width;
value.push(c);
}
value.push_str("");
trace!("chars width: {}", chars_width);
trace!("shrunk value: {}", value);
let spaces_count = cell_width - chars_width - 1;
trace!("number of spaces added to shrunk value: {}", spaces_count);
value.push_str(&" ".repeat(spaces_count));
cell.value = value;
} else {
trace!("cell is not overflowing");
let spaces_count = cell_width - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count));
}
} else {
trace!("table is not overflowing or cell is not shrinkable");
trace!("cell width: {}", cell_widths[i]);
trace!("cell unicode width: {}", cell.unicode_width());
let spaces_count = cell_widths[i] - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count));
}
cell.print(writer)?;
glue = Cell::new("").ansi_256(8);
}
writeln!(writer)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use email::email::config::EmailTextPlainFormat;
use std::io;
use super::*;
#[derive(Debug, Default)]
struct StringWriter {
content: String,
}
impl io::Write for StringWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWriter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWriter {}
struct Item {
id: u16,
name: String,
desc: String,
}
impl<'a> Item {
pub fn new(id: u16, name: &'a str, desc: &'a str) -> Self {
Self {
id,
name: String::from(name),
desc: String::from(desc),
}
}
}
impl Table for Item {
fn head() -> Row {
Row::new()
.cell(Cell::new("ID"))
.cell(Cell::new("NAME").shrinkable())
.cell(Cell::new("DESC"))
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(self.id.to_string()))
.cell(Cell::new(self.name.as_str()).shrinkable())
.cell(Cell::new(self.desc.as_str()))
}
}
macro_rules! write_items {
($writer:expr, $($item:expr),*) => {
Table::print($writer, &[$($item,)*], PrintTableOpts { format: &EmailTextPlainFormat::Auto, max_width: Some(20) }).unwrap();
};
}
#[test]
fn row_smaller_than_head() {
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1, "a", "aa"),
Item::new(2, "b", "bb"),
Item::new(3, "c", "cc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │a │aa \n",
"2 │b │bb \n",
"3 │c │cc \n",
];
assert_eq!(expected, writer.content);
}
#[test]
fn row_bigger_than_head() {
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1, "a", "aa"),
Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "c", "cc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │a │aa \n",
"2222 │bbbbb │bbbbb \n",
"3 │c │cc \n",
];
assert_eq!(expected, writer.content);
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1, "a", "aa"),
Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "cccccc", "cc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │a │aa \n",
"2222 │bbbbb │bbbbb \n",
"3 │cccccc │cc \n",
];
assert_eq!(expected, writer.content);
}
#[test]
fn basic_shrink() {
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1, "", "desc"),
Item::new(2, "short", "desc"),
Item::new(3, "loooooong", "desc"),
Item::new(4, "shriiiiink", "desc"),
Item::new(5, "shriiiiiiiiiink", "desc"),
Item::new(6, "😍😍😍😍", "desc"),
Item::new(7, "😍😍😍😍😍", "desc"),
Item::new(8, "!😍😍😍😍😍", "desc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │ │desc \n",
"2 │short │desc \n",
"3 │loooooong │desc \n",
"4 │shriiiii… │desc \n",
"5 │shriiiii… │desc \n",
"6 │😍😍😍😍 │desc \n",
"7 │😍😍😍😍… │desc \n",
"8 │!😍😍😍… │desc \n",
];
assert_eq!(expected, writer.content);
}
#[test]
fn max_shrink_width() {
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1111, "shriiiiiiiink", "desc very looong"),
Item::new(2222, "shriiiiiiiink", "desc very loooooooooong")
];
let expected = concat![
"ID │NAME │DESC \n",
"1111 │shri… │desc very looong \n",
"2222 │shri… │desc very loooooooooong \n",
];
assert_eq!(expected, writer.content);
}
}