From b773218c944a29aad27892cc35a9c76a92e5d40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Thu, 23 May 2024 15:04:48 +0200 Subject: [PATCH] wip: fix printer, make thread compatible with it --- Cargo.lock | 10 - Cargo.toml | 1 - src/account/command/check_up.rs | 16 +- src/account/command/configure.rs | 2 +- src/account/command/list.rs | 11 +- src/account/command/sync.rs | 30 +- src/account/mod.rs | 144 ++++----- src/backend/mod.rs | 2 +- src/cli.rs | 26 +- src/email/envelope/command/list.rs | 6 +- src/email/envelope/command/thread.rs | 278 ++++++------------ src/email/envelope/command/watch.rs | 2 +- src/email/envelope/flag/command/add.rs | 2 +- src/email/envelope/flag/command/remove.rs | 2 +- src/email/envelope/flag/command/set.rs | 2 +- src/email/envelope/mod.rs | 241 +++++++++++---- .../message/attachment/command/download.rs | 12 +- src/email/message/command/copy.rs | 2 +- src/email/message/command/delete.rs | 2 +- src/email/message/command/move.rs | 2 +- src/email/message/command/read.rs | 2 +- src/email/message/command/save.rs | 2 +- src/email/message/command/send.rs | 2 +- src/email/message/command/thread.rs | 2 +- src/email/message/template/command/forward.rs | 2 +- src/email/message/template/command/reply.rs | 2 +- src/email/message/template/command/save.rs | 2 +- src/email/message/template/command/send.rs | 2 +- src/email/message/template/command/write.rs | 2 +- src/email/message/template/mod.rs | 12 - src/folder/command/add.rs | 2 +- src/folder/command/delete.rs | 2 +- src/folder/command/expunge.rs | 2 +- src/folder/command/list.rs | 10 +- src/folder/command/purge.rs | 2 +- src/folder/mod.rs | 120 ++++---- src/main.rs | 2 +- src/manual/command.rs | 6 +- src/output/mod.rs | 1 - src/output/output.rs | 63 +--- src/printer.rs | 73 +++++ src/printer/mod.rs | 13 - src/printer/print.rs | 21 -- src/printer/printer.rs | 102 ------- src/ui/editor.rs | 8 +- 45 files changed, 556 insertions(+), 694 deletions(-) create mode 100644 src/printer.rs delete mode 100644 src/printer/mod.rs delete mode 100644 src/printer/print.rs delete mode 100644 src/printer/printer.rs diff --git a/Cargo.lock b/Cargo.lock index a6728b8..9eb0bcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2086,7 +2086,6 @@ dependencies = [ "serde_json", "shellexpand-utils", "sled", - "termcolor", "terminal_size 0.1.17", "tokio", "toml", @@ -4624,15 +4623,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" version = "0.1.17" diff --git a/Cargo.toml b/Cargo.toml index 862deb3..9c9c5eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,6 @@ serde-toml-merge = "0.3" serde_json = "1" shellexpand-utils = "=0.2.1" sled = "=0.34.7" -termcolor = "1" terminal_size = "0.1" tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] } toml = "0.8" diff --git a/src/account/command/check_up.rs b/src/account/command/check_up.rs index 9e94839..8d18332 100644 --- a/src/account/command/check_up.rs +++ b/src/account/command/check_up.rs @@ -24,7 +24,7 @@ impl AccountCheckUpCommand { let account = self.account.name.as_ref().map(String::as_str); - printer.print_log("Checking configuration integrity…")?; + printer.log("Checking configuration integrity…")?; let (toml_account_config, account_config) = config.clone().into_account_configs( account, @@ -33,7 +33,7 @@ impl AccountCheckUpCommand { )?; let used_backends = toml_account_config.get_used_backends(); - printer.print_log("Checking backend context integrity…")?; + printer.log("Checking backend context integrity…")?; let ctx_builder = backend::BackendContextBuilder::new( toml_account_config.clone(), @@ -46,7 +46,7 @@ impl AccountCheckUpCommand { #[cfg(feature = "maildir")] { - printer.print_log("Checking Maildir integrity…")?; + printer.log("Checking Maildir integrity…")?; let maildir = ctx_builder .maildir @@ -61,7 +61,7 @@ impl AccountCheckUpCommand { #[cfg(feature = "imap")] { - printer.print_log("Checking IMAP integrity…")?; + printer.log("Checking IMAP integrity…")?; let imap = ctx_builder .imap @@ -76,7 +76,7 @@ impl AccountCheckUpCommand { #[cfg(feature = "notmuch")] { - printer.print_log("Checking Notmuch integrity…")?; + printer.print("Checking Notmuch integrity…")?; let notmuch = ctx_builder .notmuch @@ -91,7 +91,7 @@ impl AccountCheckUpCommand { #[cfg(feature = "smtp")] { - printer.print_log("Checking SMTP integrity…")?; + printer.log("Checking SMTP integrity…")?; let smtp = ctx_builder .smtp @@ -106,7 +106,7 @@ impl AccountCheckUpCommand { #[cfg(feature = "sendmail")] { - printer.print_log("Checking Sendmail integrity…")?; + printer.log("Checking Sendmail integrity…")?; let sendmail = ctx_builder .sendmail @@ -119,6 +119,6 @@ impl AccountCheckUpCommand { } } - printer.print("Checkup successfully completed!") + printer.out("Checkup successfully completed!") } } diff --git a/src/account/command/configure.rs b/src/account/command/configure.rs index 69be407..cddde83 100644 --- a/src/account/command/configure.rs +++ b/src/account/command/configure.rs @@ -105,7 +105,7 @@ impl AccountConfigureCommand { .await?; } - printer.print(format!( + printer.out(format!( "Account {account} successfully {}configured!", if self.reset { "re" } else { "" } )) diff --git a/src/account/command/list.rs b/src/account/command/list.rs index fa30b4b..d98531c 100644 --- a/src/account/command/list.rs +++ b/src/account/command/list.rs @@ -2,7 +2,11 @@ use clap::Parser; use color_eyre::Result; use tracing::info; -use crate::{account::Accounts, config::TomlConfig, printer::Printer}; +use crate::{ + account::{Accounts, AccountsTable}, + config::TomlConfig, + printer::Printer, +}; /// List all accounts. /// @@ -23,9 +27,10 @@ impl AccountListCommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing list accounts command"); - let accounts: Accounts = config.accounts.iter().into(); + let accounts = Accounts::from(config.accounts.iter()); + let table = AccountsTable::from(accounts).with_some_width(self.table_max_width); - printer.print_table(accounts, self.table_max_width)?; + printer.out(table)?; Ok(()) } } diff --git a/src/account/command/sync.rs b/src/account/command/sync.rs index 929dd78..0950eb2 100644 --- a/src/account/command/sync.rs +++ b/src/account/command/sync.rs @@ -138,28 +138,28 @@ impl AccountSyncCommand { let mut hunks_count = report.folder.patch.len(); if !report.folder.patch.is_empty() { - printer.print_log("Folders patch:")?; + printer.log("Folders patch:")?; for (hunk, _) in report.folder.patch { - printer.print_log(format!(" - {hunk}"))?; + printer.log(format!(" - {hunk}"))?; } - printer.print_log("")?; + printer.log("")?; } if !report.email.patch.is_empty() { - printer.print_log("Envelopes patch:")?; + printer.log("Envelopes patch:")?; for (hunk, _) in report.email.patch { hunks_count += 1; - printer.print_log(format!(" - {hunk}"))?; + printer.log(format!(" - {hunk}"))?; } - printer.print_log("")?; + printer.log("")?; } - printer.print(format!( + printer.out(format!( "Estimated patch length for account {account_name} to be synchronized: {hunks_count}" ))?; } else if printer.is_json() { sync_builder.sync().await?; - printer.print(format!("Account {account_name} successfully synchronized!"))?; + printer.out(format!("Account {account_name} successfully synchronized!"))?; } else { let multi = MultiProgress::new(); let sub_progresses = Mutex::new(HashMap::new()); @@ -239,11 +239,11 @@ impl AccountSyncCommand { .filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err))) .collect::>(); if !folders_patch_err.is_empty() { - printer.print_log("")?; - printer.print_log("Errors occurred while applying the folders patch:")?; + printer.log("")?; + printer.log("Errors occurred while applying the folders patch:")?; folders_patch_err .iter() - .try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?; + .try_for_each(|(hunk, err)| printer.log(format!(" - {hunk}: {err}")))?; } let envelopes_patch_err = report @@ -253,14 +253,14 @@ impl AccountSyncCommand { .filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err))) .collect::>(); if !envelopes_patch_err.is_empty() { - printer.print_log("")?; - printer.print_log("Errors occurred while applying the envelopes patch:")?; + printer.log("")?; + printer.log("Errors occurred while applying the envelopes patch:")?; for (hunk, err) in envelopes_patch_err { - printer.print_log(format!(" - {hunk}: {err}"))?; + printer.log(format!(" - {hunk}: {err}"))?; } } - printer.print(format!("Account {account_name} successfully synchronized!"))?; + printer.out(format!("Account {account_name} successfully synchronized!"))?; } Ok(()) diff --git a/src/account/mod.rs b/src/account/mod.rs index 65ab97e..1cf6e9a 100644 --- a/src/account/mod.rs +++ b/src/account/mod.rs @@ -3,13 +3,10 @@ pub mod command; 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 serde::{Serialize, Serializer}; use std::{collections::hash_map::Iter, fmt, ops::Deref}; -use crate::printer::{PrintTable, WriteColor}; - use self::config::TomlAccountConfig; /// Represents the printable account. @@ -31,6 +28,16 @@ impl Account { default, } } + + pub fn to_row(&self) -> Row { + let mut row = Row::new(); + + row.add_cell(Cell::new(&self.name).fg(Color::Green)); + row.add_cell(Cell::new(&self.backend).fg(Color::Blue)); + row.add_cell(Cell::new(if self.default { "yes" } else { "" }).fg(Color::White)); + + row + } } impl fmt::Display for Account { @@ -39,28 +46,27 @@ impl fmt::Display for Account { } } -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 - } -} -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 - } -} - /// Represents the list of printable accounts. #[derive(Debug, Default, Serialize)] -pub struct Accounts(pub Vec); +pub struct Accounts(Vec); + +impl Accounts { + pub fn to_table(&self) -> Table { + 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(self.iter().map(Account::to_row)); + + table + } +} impl Deref for Accounts { type Target = Vec; @@ -70,51 +76,6 @@ 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, table_max_width: Option) -> Result<()> { - let mut table = Table::from(self); - if let Some(width) = table_max_width { - table.set_width(width); - } - writeln!(writer)?; - write!(writer, "{}", table)?; - writeln!(writer)?; - Ok(()) - } -} - impl From> for Accounts { fn from(map: Iter<'_, String, TomlAccountConfig>) -> Self { let mut accounts: Vec<_> = map @@ -169,3 +130,48 @@ impl From> for Accounts { Self(accounts) } } + +pub struct AccountsTable { + accounts: Accounts, + width: Option, +} + +impl AccountsTable { + pub fn with_some_width(mut self, width: Option) -> Self { + self.width = width; + self + } +} + +impl From for AccountsTable { + fn from(accounts: Accounts) -> Self { + Self { + accounts, + width: None, + } + } +} + +impl fmt::Display for AccountsTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = self.accounts.to_table(); + + if let Some(width) = self.width { + table.set_width(width); + } + + writeln!(f)?; + write!(f, "{table}")?; + writeln!(f)?; + Ok(()) + } +} + +impl Serialize for AccountsTable { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.accounts.serialize(serializer) + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index aeb47d6..56f712c 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -701,7 +701,7 @@ impl Backend { let id_mapper = self.build_id_mapper(folder, backend_kind)?; let envelopes = self.backend.list_envelopes(folder, opts).await?; let envelopes = - Envelopes::from_backend(&self.backend.account_config, &id_mapper, envelopes)?; + Envelopes::try_from_backend(&self.backend.account_config, &id_mapper, envelopes)?; Ok(envelopes) } diff --git a/src/cli.rs b/src/cli.rs index 440f08b..22044d4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -14,7 +14,7 @@ use crate::{ attachment::command::AttachmentSubcommand, command::MessageSubcommand, template::command::TemplateSubcommand, }, - output::{ColorFmt, OutputFmt}, + output::OutputFmt, printer::Printer, }; @@ -52,30 +52,6 @@ pub struct Cli { #[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())] pub output: OutputFmt, - /// Control when to use colors - /// - /// The default setting is 'auto', which means himalaya will try - /// to guess when to use colors. For example, if himalaya is - /// printing to a terminal, then it will use colors, but if it is - /// redirected to a file or a pipe, then it will suppress color - /// output. himalaya will suppress color output in some other - /// circumstances as well. For example, if the TERM environment - /// variable is not set or set to 'dumb', then himalaya will not - /// use colors. - /// - /// The possible values are: - /// - /// - never: colors will never be used - /// - /// - always: colors will always be used regardless of where output is sent - /// - /// - ansi: like 'always', but emits ANSI escapes (even in a Windows console) - /// - /// - auto: himalaya tries to be smart - #[arg(long, short = 'C', global = true)] - #[arg(value_name = "MODE", value_enum, default_value_t = Default::default())] - pub color: ColorFmt, - /// Enable logs with spantrace. /// /// This is the same as running the command with `RUST_LOG=debug` diff --git a/src/email/envelope/command/list.rs b/src/email/envelope/command/list.rs index cbee4c2..a97f19d 100644 --- a/src/email/envelope/command/list.rs +++ b/src/email/envelope/command/list.rs @@ -12,7 +12,7 @@ use tracing::info; use crate::cache::arg::disable::CacheDisableFlag; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, - folder::arg::name::FolderNameOptionalFlag, printer::Printer, + envelope::EnvelopesTable, folder::arg::name::FolderNameOptionalFlag, printer::Printer, }; /// List all envelopes. @@ -198,9 +198,9 @@ impl ListEnvelopesCommand { }; let envelopes = backend.list_envelopes(folder, opts).await?; + let table = EnvelopesTable::from(envelopes).with_some_width(self.table_max_width); - printer.print_table(envelopes, self.table_max_width)?; - + printer.out(table)?; Ok(()) } } diff --git a/src/email/envelope/command/thread.rs b/src/email/envelope/command/thread.rs index a0c36d2..387bd60 100644 --- a/src/email/envelope/command/thread.rs +++ b/src/email/envelope/command/thread.rs @@ -1,27 +1,18 @@ use ariadne::{Label, Report, ReportKind, Source}; use clap::Parser; use color_eyre::Result; -use crossterm::{ - cursor::{self, MoveToColumn}, - style::{Color, Print, ResetColor, SetForegroundColor}, - terminal, ExecutableCommand, -}; use email::{ - account::config::AccountConfig, - backend::feature::BackendFeatureSource, - email::search_query, - envelope::{list::ListEnvelopesOptions, ThreadedEnvelope}, - search_query::SearchEmailsQuery, + backend::feature::BackendFeatureSource, email::search_query, + envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery, }; -use petgraph::graphmap::DiGraphMap; -use std::{io::Write, process::exit}; +use std::process::exit; 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::Printer, + envelope::EnvelopesTree, folder::arg::name::FolderNameOptionalFlag, printer::Printer, }; /// Thread all envelopes. @@ -49,7 +40,7 @@ pub struct ThreadEnvelopesCommand { } impl ThreadEnvelopesCommand { - pub async fn execute(self, _printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing thread envelopes command"); let (toml_account_config, account_config) = config.clone().into_account_configs( @@ -106,198 +97,101 @@ impl ThreadEnvelopesCommand { None => backend.thread_envelopes(folder, opts).await, }?; - let mut stdout = std::io::stdout(); - write_tree( - &account_config, - &mut stdout, - envelopes.graph(), - ThreadedEnvelope { - id: "0", - message_id: "0", - from: "", - subject: "", - date: Default::default(), - }, - String::new(), - 0, - )?; - stdout.flush()?; + let tree = EnvelopesTree::new(account_config, envelopes); - // printer.print_table(envelopes, self.table_max_width)?; + printer.out(tree)?; Ok(()) } } -pub fn write_tree( - config: &AccountConfig, - w: &mut impl std::io::Write, - graph: &DiGraphMap, u8>, - parent: ThreadedEnvelope<'_>, - pad: String, - weight: u8, -) -> std::io::Result<()> { - let edges = graph - .all_edges() - .filter_map(|(a, b, w)| { - if a == parent && *w == weight { - Some(b) - } else { - None - } - }) - .collect::>(); +// #[cfg(test)] +// mod test { +// use email::{account::config::AccountConfig, envelope::ThreadedEnvelope}; +// use petgraph::graphmap::DiGraphMap; - if parent.id == "0" { - w.execute(Print("root"))?; - } else { - w.execute(SetForegroundColor(Color::Red))? - .execute(Print(parent.id))? - .execute(SetForegroundColor(Color::DarkGrey))? - .execute(Print(") "))? - .execute(ResetColor)?; +// use super::write_tree; - if !parent.subject.is_empty() { - w.execute(SetForegroundColor(Color::Green))? - .execute(Print(parent.subject))? - .execute(ResetColor)? - .execute(Print(" "))?; - } +// macro_rules! e { +// ($id:literal) => { +// ThreadedEnvelope { +// id: $id, +// message_id: $id, +// from: "", +// subject: "", +// date: Default::default(), +// } +// }; +// } - if !parent.from.is_empty() { - w.execute(SetForegroundColor(Color::DarkGrey))? - .execute(Print("<"))? - .execute(SetForegroundColor(Color::Blue))? - .execute(Print(parent.from))? - .execute(SetForegroundColor(Color::DarkGrey))? - .execute(Print(">"))? - .execute(ResetColor)?; - } +// #[test] +// fn tree_1() { +// let config = AccountConfig::default(); +// let mut buf = Vec::new(); +// let mut graph = DiGraphMap::new(); +// graph.add_edge(e!("0"), e!("1"), 0); +// graph.add_edge(e!("0"), e!("2"), 0); +// graph.add_edge(e!("0"), e!("3"), 0); - let date = parent.format_date(config); - let cursor_date_begin_col = terminal::size()?.0 - date.len() as u16; +// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); +// let buf = String::from_utf8_lossy(&buf); - w.execute(Print(" "))? - .execute(SetForegroundColor(Color::DarkGrey))? - .execute(Print("·".repeat( - (cursor_date_begin_col - cursor::position()?.0 - 1) as usize, - )))? - .execute(ResetColor)? - .execute(Print(" "))?; +// let expected = " +// 0 +// ├─ 1 +// ├─ 2 +// └─ 3 +// "; +// assert_eq!(expected.trim_start(), buf) +// } - w.execute(MoveToColumn(terminal::size()?.0 - date.len() as u16))? - .execute(SetForegroundColor(Color::DarkYellow))? - .execute(Print(date))? - .execute(ResetColor)?; - } +// #[test] +// fn tree_2() { +// let config = AccountConfig::default(); +// let mut buf = Vec::new(); +// let mut graph = DiGraphMap::new(); +// graph.add_edge(e!("0"), e!("1"), 0); +// graph.add_edge(e!("1"), e!("2"), 1); +// graph.add_edge(e!("1"), e!("3"), 1); - writeln!(w)?; +// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); +// let buf = String::from_utf8_lossy(&buf); - let edges_count = edges.len(); - for (i, b) in edges.into_iter().enumerate() { - let is_last = edges_count == i + 1; - let (x, y) = if is_last { - (' ', '└') - } else { - ('│', '├') - }; +// let expected = " +// 0 +// └─ 1 +// ├─ 2 +// └─ 3 +// "; +// assert_eq!(expected.trim_start(), buf) +// } - write!(w, "{pad}{y}─ ")?; +// #[test] +// fn tree_3() { +// let config = AccountConfig::default(); +// let mut buf = Vec::new(); +// let mut graph = DiGraphMap::new(); +// graph.add_edge(e!("0"), e!("1"), 0); +// graph.add_edge(e!("1"), e!("2"), 1); +// graph.add_edge(e!("2"), e!("22"), 2); +// graph.add_edge(e!("1"), e!("3"), 1); +// graph.add_edge(e!("0"), e!("4"), 0); +// graph.add_edge(e!("4"), e!("5"), 1); +// graph.add_edge(e!("5"), e!("6"), 2); - let pad = format!("{pad}{x} "); - write_tree(config, w, graph, b, pad, weight + 1)?; - } +// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); +// let buf = String::from_utf8_lossy(&buf); - Ok(()) -} - -#[cfg(test)] -mod test { - use email::{account::config::AccountConfig, envelope::ThreadedEnvelope}; - use petgraph::graphmap::DiGraphMap; - - use super::write_tree; - - macro_rules! e { - ($id:literal) => { - ThreadedEnvelope { - id: $id, - message_id: $id, - from: "", - subject: "", - date: Default::default(), - } - }; - } - - #[test] - fn tree_1() { - let config = AccountConfig::default(); - let mut buf = Vec::new(); - let mut graph = DiGraphMap::new(); - graph.add_edge(e!("0"), e!("1"), 0); - graph.add_edge(e!("0"), e!("2"), 0); - graph.add_edge(e!("0"), e!("3"), 0); - - write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); - let buf = String::from_utf8_lossy(&buf); - - let expected = " -0 -├─ 1 -├─ 2 -└─ 3 -"; - assert_eq!(expected.trim_start(), buf) - } - - #[test] - fn tree_2() { - let config = AccountConfig::default(); - let mut buf = Vec::new(); - let mut graph = DiGraphMap::new(); - graph.add_edge(e!("0"), e!("1"), 0); - graph.add_edge(e!("1"), e!("2"), 1); - graph.add_edge(e!("1"), e!("3"), 1); - - write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); - let buf = String::from_utf8_lossy(&buf); - - let expected = " -0 -└─ 1 - ├─ 2 - └─ 3 -"; - assert_eq!(expected.trim_start(), buf) - } - - #[test] - fn tree_3() { - let config = AccountConfig::default(); - let mut buf = Vec::new(); - let mut graph = DiGraphMap::new(); - graph.add_edge(e!("0"), e!("1"), 0); - graph.add_edge(e!("1"), e!("2"), 1); - graph.add_edge(e!("2"), e!("22"), 2); - graph.add_edge(e!("1"), e!("3"), 1); - graph.add_edge(e!("0"), e!("4"), 0); - graph.add_edge(e!("4"), e!("5"), 1); - graph.add_edge(e!("5"), e!("6"), 2); - - write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); - let buf = String::from_utf8_lossy(&buf); - - let expected = " -0 -├─ 1 -│ ├─ 2 -│ │ └─ 22 -│ └─ 3 -└─ 4 - └─ 5 - └─ 6 -"; - assert_eq!(expected.trim_start(), buf) - } -} +// let expected = " +// 0 +// ├─ 1 +// │ ├─ 2 +// │ │ └─ 22 +// │ └─ 3 +// └─ 4 +// └─ 5 +// └─ 6 +// "; +// assert_eq!(expected.trim_start(), buf) +// } +// } diff --git a/src/email/envelope/command/watch.rs b/src/email/envelope/command/watch.rs index 1f93458..f4cc656 100644 --- a/src/email/envelope/command/watch.rs +++ b/src/email/envelope/command/watch.rs @@ -48,7 +48,7 @@ impl WatchEnvelopesCommand { ) .await?; - printer.print_log(format!( + printer.out(format!( "Start watching folder {folder} for envelopes changes…" ))?; diff --git a/src/email/envelope/flag/command/add.rs b/src/email/envelope/flag/command/add.rs index dcfc458..ed7da11 100644 --- a/src/email/envelope/flag/command/add.rs +++ b/src/email/envelope/flag/command/add.rs @@ -58,6 +58,6 @@ impl FlagAddCommand { backend.add_flags(folder, &ids, &flags).await?; - printer.print(format!("Flag(s) {flags} successfully added!")) + printer.out(format!("Flag(s) {flags} successfully added!")) } } diff --git a/src/email/envelope/flag/command/remove.rs b/src/email/envelope/flag/command/remove.rs index 1ef7c23..6e8fc2a 100644 --- a/src/email/envelope/flag/command/remove.rs +++ b/src/email/envelope/flag/command/remove.rs @@ -58,6 +58,6 @@ impl FlagRemoveCommand { backend.remove_flags(folder, &ids, &flags).await?; - printer.print(format!("Flag(s) {flags} successfully removed!")) + printer.out(format!("Flag(s) {flags} successfully removed!")) } } diff --git a/src/email/envelope/flag/command/set.rs b/src/email/envelope/flag/command/set.rs index 3cdd9f7..7913e35 100644 --- a/src/email/envelope/flag/command/set.rs +++ b/src/email/envelope/flag/command/set.rs @@ -58,6 +58,6 @@ impl FlagSetCommand { backend.set_flags(folder, &ids, &flags).await?; - printer.print(format!("Flag(s) {flags} successfully replaced!")) + printer.out(format!("Flag(s) {flags} successfully replaced!")) } } diff --git a/src/email/envelope/mod.rs b/src/email/envelope/mod.rs index 46c9acc..e4ac959 100644 --- a/src/email/envelope/mod.rs +++ b/src/email/envelope/mod.rs @@ -4,15 +4,19 @@ 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; +use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table}; +use crossterm::{cursor, style::Stylize, terminal}; +use email::{ + account::config::AccountConfig, + envelope::{ThreadedEnvelope, ThreadedEnvelopes}, +}; +use petgraph::graphmap::DiGraphMap; +use serde::{Serialize, Serializer}; +use std::{fmt, ops::Deref, sync::Arc}; use crate::{ cache::IdMapper, flag::{Flag, Flags}, - printer::{PrintTable, WriteColor}, }; #[derive(Clone, Debug, Default, Serialize)] @@ -60,17 +64,17 @@ impl From for Row { row.add_cell( Cell::new(envelope.id) .add_attributes(all_attributes.clone()) - .fg(Color::Red), + .fg(comfy_table::Color::Red), ) .add_cell( Cell::new(flags) .add_attributes(all_attributes.clone()) - .fg(Color::White), + .fg(comfy_table::Color::White), ) .add_cell( Cell::new(envelope.subject) .add_attributes(all_attributes.clone()) - .fg(Color::Green), + .fg(comfy_table::Color::Green), ) .add_cell( Cell::new(if let Some(name) = envelope.from.name { @@ -79,12 +83,12 @@ impl From for Row { envelope.from.addr }) .add_attributes(all_attributes.clone()) - .fg(Color::Blue), + .fg(comfy_table::Color::Blue), ) .add_cell( Cell::new(envelope.date) .add_attributes(all_attributes) - .fg(Color::Yellow), + .fg(comfy_table::Color::Yellow), ); row @@ -121,17 +125,17 @@ impl From<&Envelope> for Row { row.add_cell( Cell::new(&envelope.id) .add_attributes(all_attributes.clone()) - .fg(Color::Red), + .fg(comfy_table::Color::Red), ) .add_cell( Cell::new(flags) .add_attributes(all_attributes.clone()) - .fg(Color::White), + .fg(comfy_table::Color::White), ) .add_cell( Cell::new(&envelope.subject) .add_attributes(all_attributes.clone()) - .fg(Color::Green), + .fg(comfy_table::Color::Green), ) .add_cell( Cell::new(if let Some(name) = &envelope.from.name { @@ -140,12 +144,12 @@ impl From<&Envelope> for Row { &envelope.from.addr }) .add_attributes(all_attributes.clone()) - .fg(Color::Blue), + .fg(comfy_table::Color::Blue), ) .add_cell( Cell::new(&envelope.date) .add_attributes(all_attributes) - .fg(Color::Yellow), + .fg(comfy_table::Color::Yellow), ); row @@ -156,46 +160,8 @@ impl From<&Envelope> for Row { #[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( + pub fn try_from_backend( config: &AccountConfig, id_mapper: &IdMapper, envelopes: email::envelope::Envelopes, @@ -222,9 +188,27 @@ impl Envelopes { Ok(Envelopes(envelopes)) } + + pub fn to_table(&self) -> Table { + 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(self.iter().map(Row::from)); + + table + } } -impl ops::Deref for Envelopes { +impl Deref for Envelopes { type Target = Vec; fn deref(&self) -> &Self::Target { @@ -232,15 +216,148 @@ impl ops::Deref for Envelopes { } } -impl PrintTable for Envelopes { - 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 { +pub struct EnvelopesTable { + envelopes: Envelopes, + width: Option, +} + +impl EnvelopesTable { + pub fn with_some_width(mut self, width: Option) -> Self { + self.width = width; + self + } +} + +impl From for EnvelopesTable { + fn from(envelopes: Envelopes) -> Self { + Self { + envelopes, + width: None, + } + } +} + +impl fmt::Display for EnvelopesTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = self.envelopes.to_table(); + + if let Some(width) = self.width { table.set_width(width); } - writeln!(writer)?; - write!(writer, "{}", table)?; - writeln!(writer)?; + + writeln!(f)?; + write!(f, "{table}")?; + writeln!(f)?; Ok(()) } } + +impl Serialize for EnvelopesTable { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.envelopes.serialize(serializer) + } +} + +pub struct EnvelopesTree { + config: Arc, + envelopes: ThreadedEnvelopes, +} + +impl EnvelopesTree { + pub fn new(config: Arc, envelopes: ThreadedEnvelopes) -> Self { + Self { config, envelopes } + } + + pub fn fmt( + f: &mut fmt::Formatter, + config: &AccountConfig, + graph: &DiGraphMap, u8>, + parent: ThreadedEnvelope<'_>, + pad: String, + weight: u8, + ) -> fmt::Result { + let edges = graph + .all_edges() + .filter_map(|(a, b, w)| { + if a == parent && *w == weight { + Some(b) + } else { + None + } + }) + .collect::>(); + + if parent.id == "0" { + f.write_str("root")?; + } else { + write!(f, "{}{}", parent.id.red(), ") ".dark_grey())?; + + if !parent.subject.is_empty() { + write!(f, "{} ", parent.subject.green())?; + } + + if !parent.from.is_empty() { + let left = "<".dark_grey(); + let right = ">".dark_grey(); + write!(f, "{left}{}{right}", parent.from.blue())?; + } + + let date = parent.format_date(config); + let cursor_date_begin_col = terminal::size().unwrap().0 - date.len() as u16; + + let dots = + "·".repeat((cursor_date_begin_col - cursor::position().unwrap().0 - 2) as usize); + write!(f, " {} {}", dots.dark_grey(), date.dark_yellow())?; + } + + writeln!(f)?; + + let edges_count = edges.len(); + for (i, b) in edges.into_iter().enumerate() { + let is_last = edges_count == i + 1; + let (x, y) = if is_last { + (' ', '└') + } else { + ('│', '├') + }; + + write!(f, "{pad}{y}─ ")?; + + let pad = format!("{pad}{x} "); + Self::fmt(f, config, graph, b, pad, weight + 1)?; + } + + Ok(()) + } +} + +impl fmt::Display for EnvelopesTree { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + EnvelopesTree::fmt( + f, + &self.config, + self.envelopes.graph(), + ThreadedEnvelope { + id: "0", + message_id: "0", + from: "", + subject: "", + date: Default::default(), + }, + String::new(), + 0, + ) + } +} + +impl Serialize for EnvelopesTree { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.envelopes.serialize(serializer) + } +} diff --git a/src/email/message/attachment/command/download.rs b/src/email/message/attachment/command/download.rs index 4c73edc..1187717 100644 --- a/src/email/message/attachment/command/download.rs +++ b/src/email/message/attachment/command/download.rs @@ -67,13 +67,13 @@ impl AttachmentDownloadCommand { let attachments = email.attachments()?; if attachments.is_empty() { - printer.print_log(format!("No attachment found for message {id}!"))?; + printer.log(format!("No attachment found for message {id}!"))?; continue; } else { emails_count += 1; } - printer.print_log(format!( + printer.log(format!( "{} attachment(s) found for message {id}!", attachments.len() ))?; @@ -84,7 +84,7 @@ impl AttachmentDownloadCommand { .unwrap_or_else(|| Uuid::new_v4().to_string()) .into(); let filepath = account_config.get_download_file_path(&filename)?; - printer.print_log(format!("Downloading {:?}…", filepath))?; + printer.log(format!("Downloading {:?}…", filepath))?; fs::write(&filepath, &attachment.body) .with_context(|| format!("cannot save attachment at {filepath:?}"))?; attachments_count += 1; @@ -92,9 +92,9 @@ impl AttachmentDownloadCommand { } match attachments_count { - 0 => printer.print("No attachment found!"), - 1 => printer.print("Downloaded 1 attachment!"), - n => printer.print(format!( + 0 => printer.out("No attachment found!"), + 1 => printer.out("Downloaded 1 attachment!"), + n => printer.out(format!( "Downloaded {} attachment(s) from {} messages(s)!", n, emails_count, )), diff --git a/src/email/message/command/copy.rs b/src/email/message/command/copy.rs index 171e31f..d57f553 100644 --- a/src/email/message/command/copy.rs +++ b/src/email/message/command/copy.rs @@ -60,7 +60,7 @@ impl MessageCopyCommand { backend.copy_messages(source, target, ids).await?; - printer.print(format!( + printer.out(format!( "Message(s) successfully copied from {source} to {target}!" )) } diff --git a/src/email/message/command/delete.rs b/src/email/message/command/delete.rs index 7965c5a..28b8d6f 100644 --- a/src/email/message/command/delete.rs +++ b/src/email/message/command/delete.rs @@ -58,6 +58,6 @@ impl MessageDeleteCommand { backend.delete_messages(folder, ids).await?; - printer.print(format!("Message(s) successfully removed from {folder}!")) + printer.out(format!("Message(s) successfully removed from {folder}!")) } } diff --git a/src/email/message/command/move.rs b/src/email/message/command/move.rs index 3c63ea3..c5f4772 100644 --- a/src/email/message/command/move.rs +++ b/src/email/message/command/move.rs @@ -61,7 +61,7 @@ impl MessageMoveCommand { backend.move_messages(source, target, ids).await?; - printer.print(format!( + printer.out(format!( "Message(s) successfully moved from {source} to {target}!" )) } diff --git a/src/email/message/command/read.rs b/src/email/message/command/read.rs index 1747b1f..edbc5a5 100644 --- a/src/email/message/command/read.rs +++ b/src/email/message/command/read.rs @@ -139,6 +139,6 @@ impl MessageReadCommand { glue = "\n\n"; } - printer.print(bodies) + printer.out(bodies) } } diff --git a/src/email/message/command/save.rs b/src/email/message/command/save.rs index 6bfebde..4d00af2 100644 --- a/src/email/message/command/save.rs +++ b/src/email/message/command/save.rs @@ -68,6 +68,6 @@ impl MessageSaveCommand { backend.add_message(folder, msg.as_bytes()).await?; - printer.print(format!("Message successfully saved to {folder}!")) + printer.out(format!("Message successfully saved to {folder}!")) } } diff --git a/src/email/message/command/send.rs b/src/email/message/command/send.rs index 000bfad..6ef0ea3 100644 --- a/src/email/message/command/send.rs +++ b/src/email/message/command/send.rs @@ -68,6 +68,6 @@ impl MessageSendCommand { backend.send_message_then_save_copy(msg.as_bytes()).await?; - printer.print("Message successfully sent!") + printer.out("Message successfully sent!") } } diff --git a/src/email/message/command/thread.rs b/src/email/message/command/thread.rs index c4e2379..7ba430f 100644 --- a/src/email/message/command/thread.rs +++ b/src/email/message/command/thread.rs @@ -154,6 +154,6 @@ impl MessageThreadCommand { glue = "\n\n"; } - printer.print(bodies) + printer.out(bodies) } } diff --git a/src/email/message/template/command/forward.rs b/src/email/message/template/command/forward.rs index 35ef478..4d6495f 100644 --- a/src/email/message/template/command/forward.rs +++ b/src/email/message/template/command/forward.rs @@ -76,6 +76,6 @@ impl TemplateForwardCommand { .build() .await?; - printer.print(tpl) + printer.out(tpl) } } diff --git a/src/email/message/template/command/reply.rs b/src/email/message/template/command/reply.rs index 940759a..7276186 100644 --- a/src/email/message/template/command/reply.rs +++ b/src/email/message/template/command/reply.rs @@ -81,6 +81,6 @@ impl TemplateReplyCommand { .build() .await?; - printer.print(tpl) + printer.out(tpl) } } diff --git a/src/email/message/template/command/save.rs b/src/email/message/template/command/save.rs index 4c4a718..1a91409 100644 --- a/src/email/message/template/command/save.rs +++ b/src/email/message/template/command/save.rs @@ -80,6 +80,6 @@ impl TemplateSaveCommand { backend.add_message(folder, &msg).await?; - printer.print(format!("Template successfully saved to {folder}!")) + printer.out(format!("Template successfully saved to {folder}!")) } } diff --git a/src/email/message/template/command/send.rs b/src/email/message/template/command/send.rs index 4c5a988..1220c3c 100644 --- a/src/email/message/template/command/send.rs +++ b/src/email/message/template/command/send.rs @@ -79,6 +79,6 @@ impl TemplateSendCommand { backend.send_message_then_save_copy(&msg).await?; - printer.print("Message successfully sent!") + printer.out("Message successfully sent!") } } diff --git a/src/email/message/template/command/write.rs b/src/email/message/template/command/write.rs index 8aa9c93..15536ec 100644 --- a/src/email/message/template/command/write.rs +++ b/src/email/message/template/command/write.rs @@ -47,6 +47,6 @@ impl TemplateWriteCommand { .build() .await?; - printer.print(tpl) + printer.out(tpl) } } diff --git a/src/email/message/template/mod.rs b/src/email/message/template/mod.rs index 94bf7e4..1a3a73a 100644 --- a/src/email/message/template/mod.rs +++ b/src/email/message/template/mod.rs @@ -1,14 +1,2 @@ pub mod arg; pub mod command; - -use color_eyre::Result; -use email::template::Template; - -use crate::printer::{Print, WriteColor}; - -impl Print for Template { - fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { - self.as_str().print(writer)?; - Ok(writer.reset()?) - } -} diff --git a/src/folder/command/add.rs b/src/folder/command/add.rs index bf30278..1622d84 100644 --- a/src/folder/command/add.rs +++ b/src/folder/command/add.rs @@ -50,6 +50,6 @@ impl AddFolderCommand { backend.add_folder(folder).await?; - printer.print(format!("Folder {folder} successfully created!")) + printer.log(format!("Folder {folder} successfully created!")) } } diff --git a/src/folder/command/delete.rs b/src/folder/command/delete.rs index 05aa211..3454372 100644 --- a/src/folder/command/delete.rs +++ b/src/folder/command/delete.rs @@ -60,6 +60,6 @@ impl FolderDeleteCommand { backend.delete_folder(folder).await?; - printer.print(format!("Folder {folder} successfully deleted!")) + printer.log(format!("Folder {folder} successfully deleted!")) } } diff --git a/src/folder/command/expunge.rs b/src/folder/command/expunge.rs index d28b17f..e7f3ee2 100644 --- a/src/folder/command/expunge.rs +++ b/src/folder/command/expunge.rs @@ -51,6 +51,6 @@ impl FolderExpungeCommand { backend.expunge_folder(folder).await?; - printer.print(format!("Folder {folder} successfully expunged!")) + printer.log(format!("Folder {folder} successfully expunged!")) } } diff --git a/src/folder/command/list.rs b/src/folder/command/list.rs index e80033f..3ed4391 100644 --- a/src/folder/command/list.rs +++ b/src/folder/command/list.rs @@ -6,7 +6,10 @@ 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, + account::arg::name::AccountNameFlag, + backend::Backend, + config::TomlConfig, + folder::{Folders, FoldersTable}, printer::Printer, }; @@ -51,9 +54,10 @@ impl FolderListCommand { ) .await?; - let folders: Folders = backend.list_folders().await?.into(); + let folders = Folders::from(backend.list_folders().await?); + let table = FoldersTable::from(folders).with_some_width(self.table_max_width); - printer.print_table(folders, self.table_max_width)?; + printer.log(table)?; Ok(()) } } diff --git a/src/folder/command/purge.rs b/src/folder/command/purge.rs index 68d4bb2..1db9630 100644 --- a/src/folder/command/purge.rs +++ b/src/folder/command/purge.rs @@ -60,6 +60,6 @@ impl FolderPurgeCommand { backend.purge_folder(folder).await?; - printer.print(format!("Folder {folder} successfully purged!")) + printer.log(format!("Folder {folder} successfully purged!")) } } diff --git a/src/folder/mod.rs b/src/folder/mod.rs index eaa6a50..00af7c5 100644 --- a/src/folder/mod.rs +++ b/src/folder/mod.rs @@ -2,12 +2,9 @@ pub mod arg; 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, WriteColor}; +use comfy_table::{presets, Attribute, Cell, Row, Table}; +use serde::{Serialize, Serializer}; +use std::{fmt, ops::Deref}; #[derive(Clone, Debug, Default, Serialize)] pub struct Folder { @@ -15,67 +12,46 @@ pub struct Folder { pub desc: String, } -impl From<&email::folder::Folder> for Folder { - fn from(folder: &email::folder::Folder) -> Self { +impl Folder { + pub fn to_row(&self) -> Row { + let mut row = Row::new(); + + row.add_cell(Cell::new(&self.name).fg(comfy_table::Color::Blue)); + row.add_cell(Cell::new(&self.desc).fg(comfy_table::Color::Green)); + + row + } +} + +impl From for Folder { + fn from(folder: email::folder::Folder) -> Self { Folder { - name: folder.name.clone(), - desc: folder.desc.clone(), + name: folder.name, + desc: folder.desc, } } } -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 - } -} - -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 { +impl Folders { + pub fn to_table(&self) -> Table { 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)); + .add_rows(self.iter().map(Folder::to_row)); + 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 Deref for Folders { type Target = Vec; fn deref(&self) -> &Self::Target { @@ -85,19 +61,51 @@ impl ops::Deref for Folders { impl From for Folders { fn from(folders: email::folder::Folders) -> Self { - Folders(folders.iter().map(Folder::from).collect()) + Folders(folders.into_iter().map(Folder::from).collect()) } } -impl PrintTable for Folders { - 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 { +pub struct FoldersTable { + folders: Folders, + width: Option, +} + +impl FoldersTable { + pub fn with_some_width(mut self, width: Option) -> Self { + self.width = width; + self + } +} + +impl From for FoldersTable { + fn from(folders: Folders) -> Self { + Self { + folders, + width: None, + } + } +} + +impl fmt::Display for FoldersTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = self.folders.to_table(); + + if let Some(width) = self.width { table.set_width(width); } - writeln!(writer)?; - write!(writer, "{}", table)?; - writeln!(writer)?; + + writeln!(f)?; + write!(f, "{table}")?; + writeln!(f)?; Ok(()) } } + +impl Serialize for FoldersTable { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.folders.serialize(serializer) + } +} diff --git a/src/main.rs b/src/main.rs index e14db80..871d8d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ async fn main() -> Result<()> { } let cli = Cli::parse(); - let mut printer = StdoutPrinter::new(cli.output, cli.color); + let mut printer = StdoutPrinter::new(cli.output); let mut res = match cli.command { Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await, None => { diff --git a/src/manual/command.rs b/src/manual/command.rs index bcbafba..ba69222 100644 --- a/src/manual/command.rs +++ b/src/manual/command.rs @@ -33,7 +33,7 @@ impl ManualGenerateCommand { Man::new(cmd).render(&mut buffer)?; fs::create_dir_all(&self.dir)?; - printer.print_log(format!("Generating man page for command {cmd_name}…"))?; + printer.log(format!("Generating man page for command {cmd_name}…"))?; fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?; for subcmd in subcmds { @@ -42,14 +42,14 @@ impl ManualGenerateCommand { let mut buffer = Vec::new(); Man::new(subcmd).render(&mut buffer)?; - printer.print_log(format!("Generating man page for subcommand {subcmd_name}…"))?; + printer.log(format!("Generating man page for subcommand {subcmd_name}…"))?; fs::write( self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)), buffer, )?; } - printer.print(format!( + printer.log(format!( "{subcmds_len} man page(s) successfully generated in {:?}!", self.dir ))?; diff --git a/src/output/mod.rs b/src/output/mod.rs index f7241bd..a1ef5bc 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -1,4 +1,3 @@ -pub mod args; #[allow(clippy::module_inception)] pub mod output; diff --git a/src/output/output.rs b/src/output/output.rs index 3ff1383..11fa63c 100644 --- a/src/output/output.rs +++ b/src/output/output.rs @@ -1,12 +1,7 @@ use clap::ValueEnum; use color_eyre::{eyre::eyre, eyre::Error, Result}; use serde::Serialize; -use std::{ - fmt, - io::{self, IsTerminal}, - str::FromStr, -}; -use termcolor::ColorChoice; +use std::{fmt, str::FromStr}; /// Represents the available output formats. #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] @@ -49,59 +44,3 @@ impl OutputJson { Self { response } } } - -/// Represent the available color configs. -#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, ValueEnum)] -pub enum ColorFmt { - Never, - Always, - Ansi, - #[default] - Auto, -} - -impl FromStr for ColorFmt { - type Err = Error; - - fn from_str(fmt: &str) -> Result { - match fmt { - fmt if fmt.eq_ignore_ascii_case("never") => Ok(Self::Never), - fmt if fmt.eq_ignore_ascii_case("always") => Ok(Self::Always), - fmt if fmt.eq_ignore_ascii_case("ansi") => Ok(Self::Ansi), - fmt if fmt.eq_ignore_ascii_case("auto") => Ok(Self::Auto), - unknown => Err(eyre!("cannot parse color format {}", unknown)), - } - } -} - -impl From for ColorChoice { - fn from(fmt: ColorFmt) -> Self { - match fmt { - ColorFmt::Never => Self::Never, - ColorFmt::Always => Self::Always, - ColorFmt::Ansi => Self::AlwaysAnsi, - ColorFmt::Auto => { - if io::stdout().is_terminal() { - // Otherwise let's `termcolor` decide by - // inspecting the environment. From the [doc]: - // - // * If `NO_COLOR` is set to any value, then - // colors will be suppressed. - // - // * If `TERM` is set to dumb, then colors will be - // suppressed. - // - // * In non-Windows environments, if `TERM` is not - // set, then colors will be suppressed. - // - // [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection - Self::Auto - } else { - // Colors should be deactivated if the terminal is - // not a tty. - Self::Never - } - } - } - } -} diff --git a/src/printer.rs b/src/printer.rs new file mode 100644 index 0000000..e895286 --- /dev/null +++ b/src/printer.rs @@ -0,0 +1,73 @@ +use color_eyre::{eyre::Context, Result}; +use std::{ + fmt, + io::{self, Write}, +}; + +use crate::output::OutputFmt; + +pub trait PrintTable { + fn print(&self, writer: &mut dyn io::Write, table_max_width: Option) -> Result<()>; +} + +pub trait Printer { + fn out(&mut self, data: T) -> Result<()>; + + fn log(&mut self, data: T) -> Result<()> { + self.out(data) + } + + fn is_json(&self) -> bool { + false + } +} + +pub struct StdoutPrinter { + stdout: io::Stdout, + stderr: io::Stderr, + output: OutputFmt, +} + +impl StdoutPrinter { + pub fn new(output: OutputFmt) -> Self { + Self { + stdout: io::stdout(), + stderr: io::stderr(), + output, + } + } +} + +impl Default for StdoutPrinter { + fn default() -> Self { + Self::new(Default::default()) + } +} + +impl Printer for StdoutPrinter { + fn out(&mut self, data: T) -> Result<()> { + match self.output { + OutputFmt::Plain => { + write!(self.stdout, "{data}")?; + } + OutputFmt::Json => { + serde_json::to_writer(&mut self.stdout, &data) + .context("cannot write json to writer")?; + } + }; + + Ok(()) + } + + fn log(&mut self, data: T) -> Result<()> { + if let OutputFmt::Plain = self.output { + write!(&mut self.stderr, "{data}")?; + } + + Ok(()) + } + + fn is_json(&self) -> bool { + self.output == OutputFmt::Json + } +} diff --git a/src/printer/mod.rs b/src/printer/mod.rs deleted file mode 100644 index 2ab3840..0000000 --- a/src/printer/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod print; -#[allow(clippy::module_inception)] -pub mod printer; - -use std::io; - -pub use print::*; -pub use printer::*; -use termcolor::StandardStream; - -pub trait WriteColor: io::Write + termcolor::WriteColor {} - -impl WriteColor for StandardStream {} diff --git a/src/printer/print.rs b/src/printer/print.rs deleted file mode 100644 index 81360f2..0000000 --- a/src/printer/print.rs +++ /dev/null @@ -1,21 +0,0 @@ -use color_eyre::{eyre::Context, Result}; - -use crate::printer::WriteColor; - -pub trait Print { - fn print(&self, writer: &mut dyn WriteColor) -> Result<()>; -} - -impl Print for &str { - fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { - writeln!(writer, "{}", self).context("cannot write string to writer")?; - Ok(writer.reset()?) - } -} - -impl Print for String { - fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { - self.as_str().print(writer)?; - Ok(writer.reset()?) - } -} diff --git a/src/printer/printer.rs b/src/printer/printer.rs deleted file mode 100644 index e914ebf..0000000 --- a/src/printer/printer.rs +++ /dev/null @@ -1,102 +0,0 @@ -use clap::ArgMatches; -use color_eyre::{eyre::Context, Report, Result}; -use std::fmt::Debug; -use termcolor::StandardStream; - -use crate::{ - output::{args, ColorFmt, OutputFmt}, - 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 - fn print(&mut self, data: T) -> Result<()>; - // TODO: rename log - fn print_log(&mut self, data: T) -> Result<()>; - // TODO: rename table - fn print_table( - &mut self, - data: T, - table_max_width: Option, - ) -> Result<()>; - - fn is_json(&self) -> bool; -} - -pub struct StdoutPrinter { - pub writer: Box, - pub fmt: OutputFmt, -} - -impl Default for StdoutPrinter { - fn default() -> Self { - let fmt = OutputFmt::default(); - let writer = Box::new(StandardStream::stdout(ColorFmt::default().into())); - Self { fmt, writer } - } -} - -impl StdoutPrinter { - pub fn new(fmt: OutputFmt, color: ColorFmt) -> Self { - let writer = Box::new(StandardStream::stdout(color.into())); - Self { fmt, writer } - } -} - -impl Printer for StdoutPrinter { - fn print_log(&mut self, data: T) -> Result<()> { - match self.fmt { - OutputFmt::Plain => data.print(self.writer.as_mut()), - OutputFmt::Json => Ok(()), - } - } - - fn print(&mut self, data: T) -> Result<()> { - match self.fmt { - OutputFmt::Plain => data.print(self.writer.as_mut()), - OutputFmt::Json => serde_json::to_writer(self.writer.as_mut(), &data) - .context("cannot write json to writer"), - } - } - - 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 { - fn from(fmt: OutputFmt) -> Self { - Self::new(fmt, ColorFmt::Auto) - } -} - -impl TryFrom<&ArgMatches> for StdoutPrinter { - type Error = Report; - - fn try_from(m: &ArgMatches) -> Result { - let fmt: OutputFmt = m - .get_one::(args::ARG_OUTPUT) - .map(String::as_str) - .unwrap() - .parse()?; - - let color: ColorFmt = m - .get_one::(args::ARG_COLOR) - .map(String::as_str) - .unwrap() - .parse()?; - - Ok(Self::new(fmt, color)) - } -} diff --git a/src/ui/editor.rs b/src/ui/editor.rs index e8a3164..91b1419 100644 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -80,7 +80,7 @@ pub async fn edit_tpl_with_editor( loop { match choice::post_edit() { Ok(PostEditChoice::Send) => { - printer.print_log("Sending email…")?; + printer.log("Sending email…")?; #[allow(unused_mut)] let mut compiler = MmlCompilerBuilder::new(); @@ -93,7 +93,7 @@ pub async fn edit_tpl_with_editor( backend.send_message_then_save_copy(&email).await?; remove_local_draft()?; - printer.print("Done!")?; + printer.log("Done!")?; break; } Ok(PostEditChoice::Edit) => { @@ -101,7 +101,7 @@ pub async fn edit_tpl_with_editor( continue; } Ok(PostEditChoice::LocalDraft) => { - printer.print("Email successfully saved locally")?; + printer.log("Email successfully saved locally")?; break; } Ok(PostEditChoice::RemoteDraft) => { @@ -121,7 +121,7 @@ pub async fn edit_tpl_with_editor( ) .await?; remove_local_draft()?; - printer.print("Email successfully saved to drafts")?; + printer.log("Email successfully saved to drafts")?; break; } Ok(PostEditChoice::Discard) => {