mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-21 18:40:19 +00:00
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:
parent
1e448e56eb
commit
098ae380c3
16 changed files with 371 additions and 628 deletions
53
Cargo.lock
generated
53
Cargo.lock
generated
|
@ -855,6 +855,18 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
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]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
|
@ -985,6 +997,19 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "crossterm_winapi"
|
name = "crossterm_winapi"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
|
@ -2036,6 +2061,7 @@ dependencies = [
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"clap_mangen",
|
"clap_mangen",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
|
"comfy-table",
|
||||||
"console",
|
"console",
|
||||||
"dirs 4.0.0",
|
"dirs 4.0.0",
|
||||||
"email-lib",
|
"email-lib",
|
||||||
|
@ -2360,7 +2386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fe95f33091b9b7b517a5849bce4dce1b550b430fc20d58059fcaa319ed895d8b"
|
checksum = "fe95f33091b9b7b517a5849bce4dce1b550b430fc20d58059fcaa319ed895d8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
"crossterm",
|
"crossterm 0.25.0",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
"fxhash",
|
"fxhash",
|
||||||
|
@ -4034,6 +4060,12 @@ dependencies = [
|
||||||
"untrusted 0.9.0",
|
"untrusted 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.17"
|
version = "1.0.17"
|
||||||
|
@ -4455,6 +4487,25 @@ version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
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]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
|
@ -52,6 +52,7 @@ clap = { version = "4.4", features = ["derive", "wrap_help"] }
|
||||||
clap_complete = "4.4"
|
clap_complete = "4.4"
|
||||||
clap_mangen = "0.2"
|
clap_mangen = "0.2"
|
||||||
color-eyre = "0.6.3"
|
color-eyre = "0.6.3"
|
||||||
|
comfy-table = "7.1.1"
|
||||||
console = "0.15.2"
|
console = "0.15.2"
|
||||||
dirs = "4"
|
dirs = "4"
|
||||||
email-lib = { version = "=0.24.1", default-features = false, features = ["derive", "tracing"] }
|
email-lib = { version = "=0.24.1", default-features = false, features = ["derive", "tracing"] }
|
||||||
|
|
|
@ -2,12 +2,7 @@ use clap::Parser;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{account::Accounts, config::TomlConfig, printer::Printer};
|
||||||
account::Accounts,
|
|
||||||
config::TomlConfig,
|
|
||||||
printer::{PrintTableOpts, Printer},
|
|
||||||
ui::arg::max_width::TableMaxWidthFlag,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// List all accounts.
|
/// List all accounts.
|
||||||
///
|
///
|
||||||
|
@ -15,8 +10,13 @@ use crate::{
|
||||||
/// file.
|
/// file.
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
pub struct AccountListCommand {
|
pub struct AccountListCommand {
|
||||||
#[command(flatten)]
|
/// The maximum width the table should not exceed.
|
||||||
pub table: TableMaxWidthFlag,
|
///
|
||||||
|
/// 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 {
|
impl AccountListCommand {
|
||||||
|
@ -25,12 +25,7 @@ impl AccountListCommand {
|
||||||
|
|
||||||
let accounts: Accounts = config.accounts.iter().into();
|
let accounts: Accounts = config.accounts.iter().into();
|
||||||
|
|
||||||
printer.print_table(
|
printer.print_table(accounts, self.table_max_width)?;
|
||||||
Box::new(accounts),
|
Ok(())
|
||||||
PrintTableOpts {
|
|
||||||
format: &Default::default(),
|
|
||||||
max_width: self.table.max_width,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,11 @@ pub mod config;
|
||||||
pub(crate) mod wizard;
|
pub(crate) mod wizard;
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{collections::hash_map::Iter, fmt, ops::Deref};
|
use std::{collections::hash_map::Iter, fmt, ops::Deref};
|
||||||
|
|
||||||
use crate::{
|
use crate::printer::{PrintTable, WriteColor};
|
||||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
|
||||||
ui::table::{Cell, Row, Table},
|
|
||||||
};
|
|
||||||
|
|
||||||
use self::config::TomlAccountConfig;
|
use self::config::TomlAccountConfig;
|
||||||
|
|
||||||
|
@ -41,20 +39,22 @@ impl fmt::Display for Account {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Table for Account {
|
impl From<Account> for Row {
|
||||||
fn head() -> Row {
|
fn from(account: Account) -> Self {
|
||||||
Row::new()
|
let mut r = Row::new();
|
||||||
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
|
r.add_cell(Cell::new(account.name).fg(Color::Green));
|
||||||
.cell(Cell::new("BACKENDS").bold().underline().white())
|
r.add_cell(Cell::new(account.backend).fg(Color::Blue));
|
||||||
.cell(Cell::new("DEFAULT").bold().underline().white())
|
r.add_cell(Cell::new(if account.default { "yes" } else { "" }).fg(Color::White));
|
||||||
|
r
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fn row(&self) -> Row {
|
impl From<&Account> for Row {
|
||||||
let default = if self.default { "yes" } else { "" };
|
fn from(account: &Account) -> Self {
|
||||||
Row::new()
|
let mut r = Row::new();
|
||||||
.cell(Cell::new(&self.name).shrinkable().green())
|
r.add_cell(Cell::new(&account.name).fg(Color::Green));
|
||||||
.cell(Cell::new(&self.backend).blue())
|
r.add_cell(Cell::new(&account.backend).fg(Color::Blue));
|
||||||
.cell(Cell::new(default).white())
|
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 {
|
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)?;
|
writeln!(writer)?;
|
||||||
Table::print(writer, self, opts)?;
|
write!(writer, "{}", table)?;
|
||||||
writeln!(writer)?;
|
writeln!(writer)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,8 @@ use tracing::info;
|
||||||
#[cfg(feature = "account-sync")]
|
#[cfg(feature = "account-sync")]
|
||||||
use crate::cache::arg::disable::CacheDisableFlag;
|
use crate::cache::arg::disable::CacheDisableFlag;
|
||||||
use crate::{
|
use crate::{
|
||||||
account::arg::name::AccountNameFlag,
|
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||||
backend::Backend,
|
folder::arg::name::FolderNameOptionalFlag, printer::Printer,
|
||||||
config::TomlConfig,
|
|
||||||
folder::arg::name::FolderNameOptionalFlag,
|
|
||||||
printer::{PrintTableOpts, Printer},
|
|
||||||
ui::arg::max_width::TableMaxWidthFlag,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// List all envelopes.
|
/// List all envelopes.
|
||||||
|
@ -41,9 +37,6 @@ pub struct ListEnvelopesCommand {
|
||||||
#[arg(long, short = 's', value_name = "NUMBER")]
|
#[arg(long, short = 's', value_name = "NUMBER")]
|
||||||
pub page_size: Option<usize>,
|
pub page_size: Option<usize>,
|
||||||
|
|
||||||
#[command(flatten)]
|
|
||||||
pub table: TableMaxWidthFlag,
|
|
||||||
|
|
||||||
#[cfg(feature = "account-sync")]
|
#[cfg(feature = "account-sync")]
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub cache: CacheDisableFlag,
|
pub cache: CacheDisableFlag,
|
||||||
|
@ -51,6 +44,14 @@ pub struct ListEnvelopesCommand {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub account: AccountNameFlag,
|
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 list envelopes filter and sort query.
|
||||||
///
|
///
|
||||||
/// The query can be a filter query, a sort query or both
|
/// The query can be a filter query, a sort query or both
|
||||||
|
@ -128,11 +129,11 @@ impl Default for ListEnvelopesCommand {
|
||||||
folder: Default::default(),
|
folder: Default::default(),
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: Default::default(),
|
page_size: Default::default(),
|
||||||
table: Default::default(),
|
|
||||||
#[cfg(feature = "account-sync")]
|
#[cfg(feature = "account-sync")]
|
||||||
cache: Default::default(),
|
cache: Default::default(),
|
||||||
account: Default::default(),
|
account: Default::default(),
|
||||||
query: Default::default(),
|
query: Default::default(),
|
||||||
|
table_max_width: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,13 +198,7 @@ impl ListEnvelopesCommand {
|
||||||
|
|
||||||
let envelopes = backend.list_envelopes(folder, opts).await?;
|
let envelopes = backend.list_envelopes(folder, opts).await?;
|
||||||
|
|
||||||
printer.print_table(
|
printer.print_table(envelopes, self.table_max_width)?;
|
||||||
Box::new(envelopes),
|
|
||||||
PrintTableOpts {
|
|
||||||
format: &account_config.get_message_read_format(),
|
|
||||||
max_width: self.table.max_width,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ pub mod config;
|
||||||
pub mod flag;
|
pub mod flag;
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table};
|
||||||
use email::account::config::AccountConfig;
|
use email::account::config::AccountConfig;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
@ -11,8 +12,7 @@ use std::ops;
|
||||||
use crate::{
|
use crate::{
|
||||||
cache::IdMapper,
|
cache::IdMapper,
|
||||||
flag::{Flag, Flags},
|
flag::{Flag, Flags},
|
||||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
printer::{PrintTable, WriteColor},
|
||||||
ui::{Cell, Row, Table},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
|
@ -30,49 +30,125 @@ pub struct Envelope {
|
||||||
pub to: Mailbox,
|
pub to: Mailbox,
|
||||||
pub date: String,
|
pub date: String,
|
||||||
}
|
}
|
||||||
|
impl From<Envelope> for Row {
|
||||||
|
fn from(envelope: Envelope) -> Self {
|
||||||
|
let mut all_attributes = vec![];
|
||||||
|
|
||||||
impl Table for Envelope {
|
let unseen = !envelope.flags.contains(&Flag::Seen);
|
||||||
fn head() -> Row {
|
if unseen {
|
||||||
Row::new()
|
all_attributes.push(Attribute::Bold)
|
||||||
.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())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn row(&self) -> Row {
|
|
||||||
let id = self.id.to_string();
|
|
||||||
let unseen = !self.flags.contains(&Flag::Seen);
|
|
||||||
let flags = {
|
let flags = {
|
||||||
let mut flags = String::new();
|
let mut flags = String::new();
|
||||||
flags.push_str(if !unseen { " " } else { "✷" });
|
flags.push(if !unseen { ' ' } else { '✷' });
|
||||||
flags.push_str(if self.flags.contains(&Flag::Answered) {
|
flags.push(if envelope.flags.contains(&Flag::Answered) {
|
||||||
"↵"
|
'↵'
|
||||||
} else {
|
} else {
|
||||||
" "
|
' '
|
||||||
});
|
});
|
||||||
flags.push_str(if self.flags.contains(&Flag::Flagged) {
|
flags.push(if envelope.flags.contains(&Flag::Flagged) {
|
||||||
"⚑"
|
'⚑'
|
||||||
} else {
|
} else {
|
||||||
" "
|
' '
|
||||||
});
|
});
|
||||||
flags
|
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()
|
let mut row = Row::new();
|
||||||
.cell(Cell::new(id).bold_if(unseen).red())
|
|
||||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
row.add_cell(
|
||||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
Cell::new(envelope.id)
|
||||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
.add_attributes(all_attributes.clone())
|
||||||
.cell(Cell::new(date).bold_if(unseen).yellow())
|
.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)]
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
pub struct Envelopes(Vec<Envelope>);
|
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 {
|
impl Envelopes {
|
||||||
pub fn from_backend(
|
pub fn from_backend(
|
||||||
config: &AccountConfig,
|
config: &AccountConfig,
|
||||||
|
@ -119,9 +233,13 @@ impl ops::Deref for Envelopes {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrintTable 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)?;
|
writeln!(writer)?;
|
||||||
Table::print(writer, self, opts)?;
|
write!(writer, "{}", table)?;
|
||||||
writeln!(writer)?;
|
writeln!(writer)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,8 @@ use tracing::info;
|
||||||
#[cfg(feature = "account-sync")]
|
#[cfg(feature = "account-sync")]
|
||||||
use crate::cache::arg::disable::CacheDisableFlag;
|
use crate::cache::arg::disable::CacheDisableFlag;
|
||||||
use crate::{
|
use crate::{
|
||||||
account::arg::name::AccountNameFlag,
|
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, folder::Folders,
|
||||||
backend::Backend,
|
printer::Printer,
|
||||||
config::TomlConfig,
|
|
||||||
folder::Folders,
|
|
||||||
printer::{PrintTableOpts, Printer},
|
|
||||||
ui::arg::max_width::TableMaxWidthFlag,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// List all folders.
|
/// List all folders.
|
||||||
|
@ -19,15 +15,20 @@ use crate::{
|
||||||
/// This command allows you to list all exsting folders.
|
/// This command allows you to list all exsting folders.
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
pub struct FolderListCommand {
|
pub struct FolderListCommand {
|
||||||
#[command(flatten)]
|
|
||||||
pub table: TableMaxWidthFlag,
|
|
||||||
|
|
||||||
#[cfg(feature = "account-sync")]
|
#[cfg(feature = "account-sync")]
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub cache: CacheDisableFlag,
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub account: AccountNameFlag,
|
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 {
|
impl FolderListCommand {
|
||||||
|
@ -52,12 +53,7 @@ impl FolderListCommand {
|
||||||
|
|
||||||
let folders: Folders = backend.list_folders().await?.into();
|
let folders: Folders = backend.list_folders().await?.into();
|
||||||
|
|
||||||
printer.print_table(
|
printer.print_table(folders, self.table_max_width)?;
|
||||||
Box::new(folders),
|
Ok(())
|
||||||
PrintTableOpts {
|
|
||||||
format: &account_config.get_message_read_format(),
|
|
||||||
max_width: self.table.max_width,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,11 @@ pub mod command;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
|
||||||
use crate::{
|
use crate::printer::{PrintTable, WriteColor};
|
||||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
|
||||||
ui::{Cell, Row, Table},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
pub struct Folder {
|
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 {
|
row
|
||||||
fn head() -> Row {
|
|
||||||
Row::new()
|
|
||||||
.cell(Cell::new("NAME").bold().underline().white())
|
|
||||||
.cell(Cell::new("DESC").bold().underline().white())
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn row(&self) -> Row {
|
impl From<Folder> for Row {
|
||||||
Row::new()
|
fn from(folder: Folder) -> Self {
|
||||||
.cell(Cell::new(&self.name).blue())
|
let mut row = Row::new();
|
||||||
.cell(Cell::new(&self.desc).green())
|
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)]
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
pub struct Folders(Vec<Folder>);
|
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 {
|
impl ops::Deref for Folders {
|
||||||
type Target = Vec<Folder>;
|
type Target = Vec<Folder>;
|
||||||
|
|
||||||
|
@ -58,9 +90,13 @@ impl From<email::folder::Folders> for Folders {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrintTable 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)?;
|
writeln!(writer)?;
|
||||||
Table::print(writer, self, opts)?;
|
write!(writer, "{}", table)?;
|
||||||
writeln!(writer)?;
|
writeln!(writer)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
pub mod print;
|
pub mod print;
|
||||||
pub mod print_table;
|
|
||||||
#[allow(clippy::module_inception)]
|
#[allow(clippy::module_inception)]
|
||||||
pub mod printer;
|
pub mod printer;
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
|
||||||
pub use print::*;
|
pub use print::*;
|
||||||
pub use print_table::*;
|
|
||||||
pub use printer::*;
|
pub use printer::*;
|
||||||
|
use termcolor::StandardStream;
|
||||||
|
|
||||||
|
pub trait WriteColor: io::Write + termcolor::WriteColor {}
|
||||||
|
|
||||||
|
impl WriteColor for StandardStream {}
|
||||||
|
|
|
@ -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>,
|
|
||||||
}
|
|
|
@ -1,12 +1,15 @@
|
||||||
use clap::ArgMatches;
|
use clap::ArgMatches;
|
||||||
use color_eyre::{eyre::Context, Report, Result};
|
use color_eyre::{eyre::Context, Report, Result};
|
||||||
use std::fmt::{self, Debug};
|
use std::fmt::Debug;
|
||||||
use termcolor::StandardStream;
|
use termcolor::StandardStream;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
output::{args, ColorFmt, OutputFmt},
|
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 {
|
pub trait Printer {
|
||||||
// TODO: rename end
|
// TODO: rename end
|
||||||
|
@ -14,12 +17,12 @@ pub trait Printer {
|
||||||
// TODO: rename log
|
// TODO: rename log
|
||||||
fn print_log<T: Debug + Print>(&mut self, data: T) -> Result<()>;
|
fn print_log<T: Debug + Print>(&mut self, data: T) -> Result<()>;
|
||||||
// TODO: rename table
|
// TODO: rename table
|
||||||
fn print_table<T: Debug + erased_serde::Serialize + PrintTable + ?Sized>(
|
fn print_table<T: Debug + PrintTable>(
|
||||||
&mut self,
|
&mut self,
|
||||||
// TODO: remove Box
|
data: T,
|
||||||
data: Box<T>,
|
table_max_width: Option<u16>,
|
||||||
opts: PrintTableOpts,
|
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
fn is_json(&self) -> bool;
|
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 {
|
fn is_json(&self) -> bool {
|
||||||
self.fmt == OutputFmt::Json
|
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 {
|
impl From<OutputFmt> for StdoutPrinter {
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
pub mod choice;
|
pub mod choice;
|
||||||
pub mod editor;
|
pub mod editor;
|
||||||
pub(crate) mod prompt;
|
pub(crate) mod prompt;
|
||||||
pub mod table;
|
|
||||||
|
|
||||||
pub use self::table::*;
|
|
||||||
|
|
|
@ -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>,
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod max_width;
|
|
|
@ -1,5 +0,0 @@
|
||||||
pub mod arg;
|
|
||||||
#[allow(clippy::module_inception)]
|
|
||||||
pub mod table;
|
|
||||||
|
|
||||||
pub use table::*;
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue