From 098ae380c32b5cfbf513ec10059f853488a251ea Mon Sep 17 00:00:00 2001 From: Perma Alesheikh Date: Tue, 7 May 2024 13:20:25 +0330 Subject: [PATCH] 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 --- Cargo.lock | 53 +++- Cargo.toml | 1 + src/account/command/list.rs | 25 +- src/account/mod.rs | 74 +++-- src/email/envelope/command/list.rs | 29 +- src/email/envelope/mod.rs | 190 +++++++++--- src/folder/command/list.rs | 28 +- src/folder/mod.rs | 66 ++++- src/printer/mod.rs | 9 +- src/printer/print_table.rs | 17 -- src/printer/printer.rs | 39 ++- src/ui/mod.rs | 3 - src/ui/table/arg/max_width.rs | 13 - src/ui/table/arg/mod.rs | 1 - src/ui/table/mod.rs | 5 - src/ui/table/table.rs | 446 ----------------------------- 16 files changed, 371 insertions(+), 628 deletions(-) delete mode 100644 src/printer/print_table.rs delete mode 100644 src/ui/table/arg/max_width.rs delete mode 100644 src/ui/table/arg/mod.rs delete mode 100644 src/ui/table/mod.rs delete mode 100644 src/ui/table/table.rs diff --git a/Cargo.lock b/Cargo.lock index c225c0e..6db639d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index b678851..0d2c31a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/account/command/list.rs b/src/account/command/list.rs index c135ad9..fa30b4b 100644 --- a/src/account/command/list.rs +++ b/src/account/command/list.rs @@ -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, } 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(()) } } diff --git a/src/account/mod.rs b/src/account/mod.rs index 6b741ff..65ab97e 100644 --- a/src/account/mod.rs +++ b/src/account/mod.rs @@ -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 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 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) -> 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(()) } diff --git a/src/email/envelope/command/list.rs b/src/email/envelope/command/list.rs index 2a0cbf4..a2e5425 100644 --- a/src/email/envelope/command/list.rs +++ b/src/email/envelope/command/list.rs @@ -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, - #[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, + /// 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(()) } diff --git a/src/email/envelope/mod.rs b/src/email/envelope/mod.rs index a842521..46c9acc 100644 --- a/src/email/envelope/mod.rs +++ b/src/email/envelope/mod.rs @@ -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 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); +impl From 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) -> 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(()) } diff --git a/src/folder/command/list.rs b/src/folder/command/list.rs index 324776f..e80033f 100644 --- a/src/folder/command/list.rs +++ b/src/folder/command/list.rs @@ -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, } 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(()) } } diff --git a/src/folder/mod.rs b/src/folder/mod.rs index db35ec0..eaa6a50 100644 --- a/src/folder/mod.rs +++ b/src/folder/mod.rs @@ -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 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); +impl From 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; @@ -58,9 +90,13 @@ impl From 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) -> 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(()) } diff --git a/src/printer/mod.rs b/src/printer/mod.rs index b9199fb..2ab3840 100644 --- a/src/printer/mod.rs +++ b/src/printer/mod.rs @@ -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 {} diff --git a/src/printer/print_table.rs b/src/printer/print_table.rs deleted file mode 100644 index 5b58379..0000000 --- a/src/printer/print_table.rs +++ /dev/null @@ -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, -} diff --git a/src/printer/printer.rs b/src/printer/printer.rs index 13cf90e..e914ebf 100644 --- a/src/printer/printer.rs +++ b/src/printer/printer.rs @@ -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) -> Result<()>; +} pub trait Printer { // TODO: rename end @@ -14,12 +17,12 @@ pub trait Printer { // TODO: rename log fn print_log(&mut self, data: T) -> Result<()>; // TODO: rename table - fn print_table( + fn print_table( &mut self, - // TODO: remove Box - data: Box, - opts: PrintTableOpts, + data: T, + table_max_width: Option, ) -> Result<()>; + fn is_json(&self) -> bool; } @@ -59,25 +62,17 @@ impl Printer for StdoutPrinter { } } - fn print_table( - &mut self, - data: Box, - 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 ::erase(json); - data.erased_serialize(ser).unwrap(); - Ok(()) - } - } - } - fn is_json(&self) -> bool { self.fmt == OutputFmt::Json } + + fn print_table( + &mut self, + data: T, + table_max_width: Option, + ) -> Result<()> { + data.print_table(self.writer.as_mut(), table_max_width) + } } impl From for StdoutPrinter { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 0ca3337..1eb8655 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,6 +1,3 @@ pub mod choice; pub mod editor; pub(crate) mod prompt; -pub mod table; - -pub use self::table::*; diff --git a/src/ui/table/arg/max_width.rs b/src/ui/table/arg/max_width.rs deleted file mode 100644 index 214a7c2..0000000 --- a/src/ui/table/arg/max_width.rs +++ /dev/null @@ -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, -} diff --git a/src/ui/table/arg/mod.rs b/src/ui/table/arg/mod.rs deleted file mode 100644 index e5a0593..0000000 --- a/src/ui/table/arg/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod max_width; diff --git a/src/ui/table/mod.rs b/src/ui/table/mod.rs deleted file mode 100644 index d538667..0000000 --- a/src/ui/table/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod arg; -#[allow(clippy::module_inception)] -pub mod table; - -pub use table::*; diff --git a/src/ui/table/table.rs b/src/ui/table/table.rs deleted file mode 100644 index 775c4ee..0000000 --- a/src/ui/table/table.rs +++ /dev/null @@ -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>(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, -); - -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 = - 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::>(), - ); - trace!("cell widths: {:?}", cell_widths); - - let spaces_plus_separators_len = cell_widths.len() * 2 - 1; - let table_width = cell_widths.iter().sum::() + 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 { - 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); - } -}