wip: fix printer, make thread compatible with it

This commit is contained in:
Clément DOUIN 2024-05-23 15:04:48 +02:00
parent 6cbfc57c83
commit b773218c94
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
45 changed files with 556 additions and 694 deletions

10
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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!")
}
}

View file

@ -105,7 +105,7 @@ impl AccountConfigureCommand {
.await?;
}
printer.print(format!(
printer.out(format!(
"Account {account} successfully {}configured!",
if self.reset { "re" } else { "" }
))

View file

@ -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(())
}
}

View file

@ -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::<Vec<_>>();
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::<Vec<_>>();
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(())

View file

@ -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<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
}
}
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<Account>);
pub struct Accounts(Vec<Account>);
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<Account>;
@ -70,51 +76,6 @@ impl Deref for Accounts {
}
}
impl From<Accounts> for Table {
fn from(accounts: Accounts) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("BACKENDS").add_attribute(Attribute::Reverse),
Cell::new("DEFAULT").add_attribute(Attribute::Reverse),
]))
.add_rows(accounts.0.into_iter().map(Row::from));
table
}
}
impl From<&Accounts> for Table {
fn from(accounts: &Accounts) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("BACKENDS").add_attribute(Attribute::Reverse),
Cell::new("DEFAULT").add_attribute(Attribute::Reverse),
]))
.add_rows(accounts.0.iter().map(Row::from));
table
}
}
impl PrintTable for Accounts {
fn print_table(&self, writer: &mut dyn WriteColor, 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)?;
write!(writer, "{}", table)?;
writeln!(writer)?;
Ok(())
}
}
impl From<Iter<'_, String, TomlAccountConfig>> for Accounts {
fn from(map: Iter<'_, String, TomlAccountConfig>) -> Self {
let mut accounts: Vec<_> = map
@ -169,3 +130,48 @@ impl From<Iter<'_, String, TomlAccountConfig>> for Accounts {
Self(accounts)
}
}
pub struct AccountsTable {
accounts: Accounts,
width: Option<u16>,
}
impl AccountsTable {
pub fn with_some_width(mut self, width: Option<u16>) -> Self {
self.width = width;
self
}
}
impl From<Accounts> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.accounts.serialize(serializer)
}
}

View file

@ -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)
}

View file

@ -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`

View file

@ -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(())
}
}

View file

@ -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<ThreadedEnvelope<'_>, 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::<Vec<_>>();
// #[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)
// }
// }

View file

@ -48,7 +48,7 @@ impl WatchEnvelopesCommand {
)
.await?;
printer.print_log(format!(
printer.out(format!(
"Start watching folder {folder} for envelopes changes…"
))?;

View file

@ -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!"))
}
}

View file

@ -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!"))
}
}

View file

@ -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!"))
}
}

View file

@ -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<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 {
@ -79,12 +83,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
@ -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<Envelope>);
impl From<Envelopes> for Table {
fn from(envelopes: Envelopes) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("ID").add_attribute(Attribute::Reverse),
Cell::new("FLAGS").add_attribute(Attribute::Reverse),
Cell::new("SUBJECT").add_attribute(Attribute::Reverse),
Cell::new("FROM").add_attribute(Attribute::Reverse),
Cell::new("DATE").add_attribute(Attribute::Reverse),
]))
.add_rows(envelopes.0.into_iter().map(Row::from));
table
}
}
impl From<&Envelopes> for Table {
fn from(envelopes: &Envelopes) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("ID").add_attribute(Attribute::Reverse),
Cell::new("FLAGS").add_attribute(Attribute::Reverse),
Cell::new("SUBJECT").add_attribute(Attribute::Reverse),
Cell::new("FROM").add_attribute(Attribute::Reverse),
Cell::new("DATE").add_attribute(Attribute::Reverse),
]))
.add_rows(envelopes.0.iter().map(Row::from));
table
}
}
impl Envelopes {
pub fn from_backend(
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<Envelope>;
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<u16>) -> Result<()> {
let mut table = Table::from(self);
if let Some(width) = table_max_width {
pub struct EnvelopesTable {
envelopes: Envelopes,
width: Option<u16>,
}
impl EnvelopesTable {
pub fn with_some_width(mut self, width: Option<u16>) -> Self {
self.width = width;
self
}
}
impl From<Envelopes> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.envelopes.serialize(serializer)
}
}
pub struct EnvelopesTree {
config: Arc<AccountConfig>,
envelopes: ThreadedEnvelopes,
}
impl EnvelopesTree {
pub fn new(config: Arc<AccountConfig>, envelopes: ThreadedEnvelopes) -> Self {
Self { config, envelopes }
}
pub fn fmt(
f: &mut fmt::Formatter,
config: &AccountConfig,
graph: &DiGraphMap<ThreadedEnvelope<'_>, 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::<Vec<_>>();
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.envelopes.serialize(serializer)
}
}

View file

@ -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,
)),

View file

@ -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}!"
))
}

View file

@ -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}!"))
}
}

View file

@ -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}!"
))
}

View file

@ -139,6 +139,6 @@ impl MessageReadCommand {
glue = "\n\n";
}
printer.print(bodies)
printer.out(bodies)
}
}

View file

@ -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}!"))
}
}

View file

@ -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!")
}
}

View file

@ -154,6 +154,6 @@ impl MessageThreadCommand {
glue = "\n\n";
}
printer.print(bodies)
printer.out(bodies)
}
}

View file

@ -76,6 +76,6 @@ impl TemplateForwardCommand {
.build()
.await?;
printer.print(tpl)
printer.out(tpl)
}
}

View file

@ -81,6 +81,6 @@ impl TemplateReplyCommand {
.build()
.await?;
printer.print(tpl)
printer.out(tpl)
}
}

View file

@ -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}!"))
}
}

View file

@ -79,6 +79,6 @@ impl TemplateSendCommand {
backend.send_message_then_save_copy(&msg).await?;
printer.print("Message successfully sent!")
printer.out("Message successfully sent!")
}
}

View file

@ -47,6 +47,6 @@ impl TemplateWriteCommand {
.build()
.await?;
printer.print(tpl)
printer.out(tpl)
}
}

View file

@ -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()?)
}
}

View file

@ -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!"))
}
}

View file

@ -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!"))
}
}

View file

@ -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!"))
}
}

View file

@ -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(())
}
}

View file

@ -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!"))
}
}

View file

@ -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<email::folder::Folder> 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<Folder> for Row {
fn from(folder: Folder) -> Self {
let mut row = Row::new();
row.add_cell(Cell::new(folder.name).fg(comfy_table::Color::Blue));
row.add_cell(Cell::new(folder.desc).fg(comfy_table::Color::Green));
row
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folders(Vec<Folder>);
impl From<Folders> for Table {
fn from(folders: Folders) -> Self {
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<Folder>;
fn deref(&self) -> &Self::Target {
@ -85,19 +61,51 @@ impl ops::Deref for Folders {
impl From<email::folder::Folders> 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<u16>) -> Result<()> {
let mut table = Table::from(self);
if let Some(width) = table_max_width {
pub struct FoldersTable {
folders: Folders,
width: Option<u16>,
}
impl FoldersTable {
pub fn with_some_width(mut self, width: Option<u16>) -> Self {
self.width = width;
self
}
}
impl From<Folders> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.folders.serialize(serializer)
}
}

View file

@ -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 => {

View file

@ -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
))?;

View file

@ -1,4 +1,3 @@
pub mod args;
#[allow(clippy::module_inception)]
pub mod output;

View file

@ -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<T: Serialize> OutputJson<T> {
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<Self, Self::Err> {
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<ColorFmt> 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
}
}
}
}
}

73
src/printer.rs Normal file
View file

@ -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<u16>) -> Result<()>;
}
pub trait Printer {
fn out<T: fmt::Display + serde::Serialize>(&mut self, data: T) -> Result<()>;
fn log<T: fmt::Display + serde::Serialize>(&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<T: fmt::Display + serde::Serialize>(&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<T: fmt::Display + serde::Serialize>(&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
}
}

View file

@ -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 {}

View file

@ -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()?)
}
}

View file

@ -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<u16>) -> Result<()>;
}
pub trait Printer {
// TODO: rename end
fn print<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()>;
// TODO: rename log
fn print_log<T: Debug + Print>(&mut self, data: T) -> Result<()>;
// TODO: rename table
fn print_table<T: Debug + PrintTable>(
&mut self,
data: T,
table_max_width: Option<u16>,
) -> Result<()>;
fn is_json(&self) -> bool;
}
pub struct StdoutPrinter {
pub writer: Box<dyn WriteColor>,
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<T: Debug + Print>(&mut self, data: T) -> Result<()> {
match self.fmt {
OutputFmt::Plain => data.print(self.writer.as_mut()),
OutputFmt::Json => Ok(()),
}
}
fn print<T: Debug + Print + serde::Serialize>(&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<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 {
fn from(fmt: OutputFmt) -> Self {
Self::new(fmt, ColorFmt::Auto)
}
}
impl TryFrom<&ArgMatches> for StdoutPrinter {
type Error = Report;
fn try_from(m: &ArgMatches) -> Result<Self, Self::Error> {
let fmt: OutputFmt = m
.get_one::<String>(args::ARG_OUTPUT)
.map(String::as_str)
.unwrap()
.parse()?;
let color: ColorFmt = m
.get_one::<String>(args::ARG_COLOR)
.map(String::as_str)
.unwrap()
.parse()?;
Ok(Self::new(fmt, color))
}
}

View file

@ -80,7 +80,7 @@ pub async fn edit_tpl_with_editor<P: Printer>(
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<P: Printer>(
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<P: Printer>(
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<P: Printer>(
)
.await?;
remove_local_draft()?;
printer.print("Email successfully saved to drafts")?;
printer.log("Email successfully saved to drafts")?;
break;
}
Ok(PostEditChoice::Discard) => {