mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-22 02:50:19 +00:00
Merge branch 'thread'
Replaced `imap` crate by `imap-{types,codec,flow,client}`, and added thread support for IMAP and Maildir.
This commit is contained in:
commit
f9b92e6e7a
50 changed files with 1023 additions and 541 deletions
58
Cargo.lock
generated
58
Cargo.lock
generated
|
@ -1006,7 +1006,10 @@ dependencies = [
|
|||
"bitflags 2.5.0",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot 0.12.1",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
|
@ -1396,7 +1399,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "email-lib"
|
||||
version = "0.24.1"
|
||||
source = "git+https://git.sr.ht/~soywod/pimalaya#033ba2a2e193769e1272c9493aa1d6c975346eb5"
|
||||
source = "git+https://git.sr.ht/~soywod/pimalaya#a28e746a634c066f4b9b0b15cd6f742fa530164d"
|
||||
dependencies = [
|
||||
"advisory-lock",
|
||||
"async-ctrlc",
|
||||
|
@ -1425,6 +1428,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"ouroboros",
|
||||
"paste",
|
||||
"petgraph",
|
||||
"pgp-lib",
|
||||
"process-lib",
|
||||
"rayon",
|
||||
|
@ -2063,6 +2067,7 @@ dependencies = [
|
|||
"color-eyre",
|
||||
"comfy-table",
|
||||
"console",
|
||||
"crossterm 0.27.0",
|
||||
"dirs 4.0.0",
|
||||
"email-lib",
|
||||
"email_address",
|
||||
|
@ -2074,6 +2079,7 @@ dependencies = [
|
|||
"mml-lib",
|
||||
"oauth-lib",
|
||||
"once_cell",
|
||||
"petgraph",
|
||||
"process-lib",
|
||||
"secret-lib",
|
||||
"serde",
|
||||
|
@ -2081,7 +2087,6 @@ dependencies = [
|
|||
"serde_json",
|
||||
"shellexpand-utils",
|
||||
"sled",
|
||||
"termcolor",
|
||||
"terminal_size 0.1.17",
|
||||
"tokio",
|
||||
"toml",
|
||||
|
@ -2183,7 +2188,7 @@ dependencies = [
|
|||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.6",
|
||||
"socket2 0.4.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
|
@ -2217,7 +2222,7 @@ dependencies = [
|
|||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.52.0",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2267,12 +2272,13 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "imap-client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
|
||||
source = "git+https://github.com/soywod/imap-client.git#4533995f3ebe6efdb503128af15a867b60e48645"
|
||||
dependencies = [
|
||||
"imap-flow",
|
||||
"imap-types",
|
||||
"once_cell",
|
||||
"rustls-native-certs 0.7.0",
|
||||
"tasks",
|
||||
"tag-generator",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.0",
|
||||
|
@ -2282,7 +2288,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "imap-codec"
|
||||
version = "2.0.0"
|
||||
source = "git+https://github.com/duesee/imap-codec.git#d6b265fd01123334db2d48100537eb140932589c"
|
||||
source = "git+https://github.com/duesee/imap-codec.git#638924e92d9a8ea82208397d8e739110296daf01"
|
||||
dependencies = [
|
||||
"abnf-core",
|
||||
"base64 0.21.7",
|
||||
|
@ -2297,7 +2303,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "imap-flow"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
|
||||
source = "git+https://github.com/soywod/imap-flow?branch=into-inner-stream#b705adbc03976367330f2b24e99a9623e5da3733"
|
||||
dependencies = [
|
||||
"bounded-static",
|
||||
"bytes",
|
||||
|
@ -2313,7 +2319,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "imap-types"
|
||||
version = "2.0.0"
|
||||
source = "git+https://github.com/duesee/imap-codec.git#d6b265fd01123334db2d48100537eb140932589c"
|
||||
source = "git+https://github.com/duesee/imap-codec.git#638924e92d9a8ea82208397d8e739110296daf01"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bounded-static",
|
||||
|
@ -4577,7 +4583,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "tag-generator"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
|
||||
source = "git+https://github.com/duesee/imap-flow#9ffda2b321247896b3f452072ccfd38789bb547a"
|
||||
dependencies = [
|
||||
"imap-types",
|
||||
"rand",
|
||||
|
@ -4589,18 +4595,6 @@ version = "0.12.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
|
||||
|
||||
[[package]]
|
||||
name = "tasks"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
|
||||
dependencies = [
|
||||
"imap-flow",
|
||||
"imap-types",
|
||||
"tag-generator",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-winrt-notification"
|
||||
version = "0.1.3"
|
||||
|
@ -4623,15 +4617,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"
|
||||
|
@ -5244,7 +5229,7 @@ version = "0.51.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9"
|
||||
dependencies = [
|
||||
"windows-core 0.51.1",
|
||||
"windows-core",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
|
@ -5257,15 +5242,6 @@ dependencies = [
|
|||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
|
|
|
@ -54,6 +54,7 @@ clap_mangen = "0.2"
|
|||
color-eyre = "0.6.3"
|
||||
comfy-table = "7.1.1"
|
||||
console = "0.15.2"
|
||||
crossterm = "0.27"
|
||||
dirs = "4"
|
||||
email-lib = { version = "=0.24.1", default-features = false, features = ["derive", "tracing"] }
|
||||
email_address = "0.2.4"
|
||||
|
@ -65,6 +66,7 @@ md5 = "0.7"
|
|||
mml-lib = { version = "=1.0.12", default-features = false, features = ["derive"] }
|
||||
oauth-lib = "=0.1.1"
|
||||
once_cell = "1.16"
|
||||
petgraph = "0.6"
|
||||
process-lib = { version = "=0.4.2", features = ["derive"] }
|
||||
secret-lib = { version = "=0.4.4", features = ["derive"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
@ -72,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"
|
||||
|
@ -85,9 +86,8 @@ url = "2.2"
|
|||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
||||
[patch.crates-io]
|
||||
# WIP: transition from `imap` to `imap-codec`
|
||||
# WIP: transition from `imap` to `imap-{types,codec,client}`
|
||||
email-lib = { git = "https://git.sr.ht/~soywod/pimalaya" }
|
||||
imap-client = { git = "https://github.com/soywod/imap-flow.git", branch = "session" }
|
||||
tasks = { git = "https://github.com/soywod/imap-flow.git", branch = "session" }
|
||||
imap-client = { git = "https://github.com/soywod/imap-client.git" }
|
||||
imap-codec = { git = "https://github.com/duesee/imap-codec.git" }
|
||||
imap-types = { git = "https://github.com/duesee/imap-codec.git" }
|
||||
|
|
|
@ -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!")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ impl AccountConfigureCommand {
|
|||
.await?;
|
||||
}
|
||||
|
||||
printer.print(format!(
|
||||
printer.out(format!(
|
||||
"Account {account} successfully {}configured!",
|
||||
if self.reset { "re" } else { "" }
|
||||
))
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -142,6 +142,14 @@ impl TomlAccountConfig {
|
|||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn thread_envelopes_kind(&self) -> Option<&BackendKind> {
|
||||
self.envelope
|
||||
.as_ref()
|
||||
.and_then(|envelope| envelope.thread.as_ref())
|
||||
.and_then(|thread| thread.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn watch_envelopes_kind(&self) -> Option<&BackendKind> {
|
||||
self.envelope
|
||||
.as_ref()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ use email::{
|
|||
envelope::{
|
||||
get::GetEnvelope,
|
||||
list::{ListEnvelopes, ListEnvelopesOptions},
|
||||
thread::ThreadEnvelopes,
|
||||
watch::WatchEnvelopes,
|
||||
Id, SingleId,
|
||||
},
|
||||
|
@ -45,7 +46,11 @@ use email::{
|
|||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{account::config::TomlAccountConfig, cache::IdMapper, envelope::Envelopes};
|
||||
use crate::{
|
||||
account::config::TomlAccountConfig,
|
||||
cache::IdMapper,
|
||||
envelope::{Envelopes, ThreadedEnvelopes},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
|
@ -337,6 +342,23 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
fn thread_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn ThreadEnvelopes>> {
|
||||
match self.toml_account_config.thread_envelopes_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.thread_envelopes_with_some(&self.imap),
|
||||
#[cfg(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.thread_envelopes()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.thread_envelopes_with_some(&self.maildir),
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.thread_envelopes_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn watch_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn WatchEnvelopes>> {
|
||||
match self.toml_account_config.watch_envelopes_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
|
@ -683,7 +705,36 @@ 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)
|
||||
}
|
||||
|
||||
pub async fn thread_envelopes(
|
||||
&self,
|
||||
folder: &str,
|
||||
opts: ListEnvelopesOptions,
|
||||
) -> Result<ThreadedEnvelopes> {
|
||||
let backend_kind = self.toml_account_config.thread_envelopes_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let envelopes = self.backend.thread_envelopes(folder, opts).await?;
|
||||
let envelopes = ThreadedEnvelopes::try_from_backend(&id_mapper, envelopes)?;
|
||||
Ok(envelopes)
|
||||
}
|
||||
|
||||
pub async fn thread_envelope(
|
||||
&self,
|
||||
folder: &str,
|
||||
id: usize,
|
||||
opts: ListEnvelopesOptions,
|
||||
) -> Result<ThreadedEnvelopes> {
|
||||
let backend_kind = self.toml_account_config.thread_envelopes_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let id = id_mapper.get_id(id)?;
|
||||
let envelopes = self
|
||||
.backend
|
||||
.thread_envelope(folder, SingleId::from(id), opts)
|
||||
.await?;
|
||||
let envelopes = ThreadedEnvelopes::try_from_backend(&id_mapper, envelopes)?;
|
||||
Ok(envelopes)
|
||||
}
|
||||
|
||||
|
|
26
src/cli.rs
26
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`
|
||||
|
|
|
@ -244,6 +244,7 @@ impl TomlConfig {
|
|||
}),
|
||||
envelope: config.envelope.map(|c| EnvelopeConfig {
|
||||
list: c.list.map(|c| c.remote),
|
||||
thread: c.thread.map(|c| c.remote),
|
||||
watch: c.watch.map(|c| c.remote),
|
||||
#[cfg(feature = "account-sync")]
|
||||
sync: c.sync,
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
pub mod list;
|
||||
pub mod thread;
|
||||
pub mod watch;
|
||||
|
||||
use color_eyre::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
|
||||
use self::{list::ListEnvelopesCommand, watch::WatchEnvelopesCommand};
|
||||
use self::{
|
||||
list::ListEnvelopesCommand, thread::ThreadEnvelopesCommand, watch::WatchEnvelopesCommand,
|
||||
};
|
||||
|
||||
/// Manage envelopes.
|
||||
///
|
||||
|
@ -19,6 +22,9 @@ pub enum EnvelopeSubcommand {
|
|||
#[command(alias = "lst")]
|
||||
List(ListEnvelopesCommand),
|
||||
|
||||
#[command()]
|
||||
Thread(ThreadEnvelopesCommand),
|
||||
|
||||
#[command()]
|
||||
Watch(WatchEnvelopesCommand),
|
||||
}
|
||||
|
@ -28,6 +34,7 @@ impl EnvelopeSubcommand {
|
|||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::List(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Thread(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Watch(cmd) => cmd.execute(printer, config).await,
|
||||
}
|
||||
}
|
||||
|
|
197
src/email/envelope/command/thread.rs
Normal file
197
src/email/envelope/command/thread.rs
Normal file
|
@ -0,0 +1,197 @@
|
|||
use ariadne::{Label, Report, ReportKind, Source};
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
backend::feature::BackendFeatureSource, email::search_query,
|
||||
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
|
||||
};
|
||||
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,
|
||||
envelope::EnvelopesTree, folder::arg::name::FolderNameOptionalFlag, printer::Printer,
|
||||
};
|
||||
|
||||
/// Thread all envelopes.
|
||||
///
|
||||
/// This command allows you to thread all envelopes included in the
|
||||
/// given folder.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ThreadEnvelopesCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
|
||||
/// Show only threads that contain the given envelope identifier.
|
||||
#[arg(long, short)]
|
||||
pub id: Option<usize>,
|
||||
|
||||
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
|
||||
pub query: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl ThreadEnvelopesCommand {
|
||||
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(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let thread_envelopes_kind = toml_account_config.thread_envelopes_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
thread_envelopes_kind,
|
||||
|builder| builder.set_thread_envelopes(BackendFeatureSource::Context),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let query = self
|
||||
.query
|
||||
.map(|query| query.join(" ").parse::<SearchEmailsQuery>());
|
||||
let query = match query {
|
||||
None => None,
|
||||
Some(Ok(query)) => Some(query),
|
||||
Some(Err(main_err)) => {
|
||||
let source = "query";
|
||||
let search_query::error::Error::ParseError(errs, query) = &main_err;
|
||||
for err in errs {
|
||||
Report::build(ReportKind::Error, source, err.span().start)
|
||||
.with_message(main_err.to_string())
|
||||
.with_label(
|
||||
Label::new((source, err.span().into_range()))
|
||||
.with_message(err.reason().to_string())
|
||||
.with_color(ariadne::Color::Red),
|
||||
)
|
||||
.finish()
|
||||
.eprint((source, Source::from(&query)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
exit(0)
|
||||
}
|
||||
};
|
||||
|
||||
let opts = ListEnvelopesOptions {
|
||||
page: 0,
|
||||
page_size: 0,
|
||||
query,
|
||||
};
|
||||
|
||||
let envelopes = match self.id {
|
||||
Some(id) => backend.thread_envelope(folder, id, opts).await,
|
||||
None => backend.thread_envelopes(folder, opts).await,
|
||||
}?;
|
||||
|
||||
let tree = EnvelopesTree::new(account_config, envelopes);
|
||||
|
||||
printer.out(tree)?;
|
||||
|
||||
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)
|
||||
// }
|
||||
// }
|
|
@ -48,7 +48,7 @@ impl WatchEnvelopesCommand {
|
|||
)
|
||||
.await?;
|
||||
|
||||
printer.print_log(format!(
|
||||
printer.out(format!(
|
||||
"Start watching folder {folder} for envelopes changes…"
|
||||
))?;
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ use crate::backend::BackendKind;
|
|||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct EnvelopeConfig {
|
||||
pub list: Option<ListEnvelopesConfig>,
|
||||
pub thread: Option<ThreadEnvelopesConfig>,
|
||||
pub watch: Option<WatchEnvelopesConfig>,
|
||||
pub get: Option<GetEnvelopeConfig>,
|
||||
#[cfg(feature = "account-sync")]
|
||||
|
@ -54,6 +55,26 @@ impl ListEnvelopesConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ThreadEnvelopesConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub remote: email::envelope::thread::config::EnvelopeThreadConfig,
|
||||
}
|
||||
|
||||
impl ThreadEnvelopesConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct WatchEnvelopesConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
|
|
|
@ -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!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,15 +4,16 @@ 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};
|
||||
use petgraph::graphmap::DiGraphMap;
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
cache::IdMapper,
|
||||
flag::{Flag, Flags},
|
||||
printer::{PrintTable, WriteColor},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
|
@ -60,17 +61,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 +80,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 +122,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,62 +141,23 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the list of envelopes.
|
||||
#[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 +184,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 +212,230 @@ 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 ThreadedEnvelopes(email::envelope::ThreadedEnvelopes);
|
||||
|
||||
impl ThreadedEnvelopes {
|
||||
pub fn try_from_backend(
|
||||
id_mapper: &IdMapper,
|
||||
envelopes: email::envelope::ThreadedEnvelopes,
|
||||
) -> Result<ThreadedEnvelopes> {
|
||||
let prev_edges = envelopes
|
||||
.graph()
|
||||
.all_edges()
|
||||
.map(|(a, b, w)| {
|
||||
let a = id_mapper.get_or_create_alias(&a.id)?;
|
||||
let b = id_mapper.get_or_create_alias(&b.id)?;
|
||||
Ok((a, b, *w))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let envelopes = envelopes
|
||||
.map()
|
||||
.iter()
|
||||
.map(|(_, envelope)| {
|
||||
let id = id_mapper.get_or_create_alias(&envelope.id)?;
|
||||
let envelope = email::envelope::Envelope {
|
||||
id: id.clone(),
|
||||
message_id: envelope.message_id.clone(),
|
||||
in_reply_to: envelope.in_reply_to.clone(),
|
||||
flags: envelope.flags.clone(),
|
||||
subject: envelope.subject.clone(),
|
||||
from: envelope.from.clone(),
|
||||
to: envelope.to.clone(),
|
||||
date: envelope.date.clone(),
|
||||
};
|
||||
|
||||
Ok((id, envelope))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>>>()?;
|
||||
|
||||
let envelopes = email::envelope::ThreadedEnvelopes::build(envelopes, move |envelopes| {
|
||||
let mut graph = DiGraphMap::<ThreadedEnvelope, u8>::new();
|
||||
|
||||
for (a, b, w) in prev_edges.clone() {
|
||||
let eb = envelopes.get(&b).unwrap();
|
||||
match envelopes.get(&a) {
|
||||
Some(ea) => {
|
||||
graph.add_edge(ea.as_threaded(), eb.as_threaded(), w);
|
||||
}
|
||||
None => {
|
||||
let ea = ThreadedEnvelope {
|
||||
id: "0",
|
||||
message_id: "0",
|
||||
subject: "",
|
||||
from: "",
|
||||
date: Default::default(),
|
||||
};
|
||||
graph.add_edge(ea, eb.as_threaded(), w);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graph
|
||||
});
|
||||
|
||||
Ok(ThreadedEnvelopes(envelopes))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ThreadedEnvelopes {
|
||||
type Target = email::envelope::ThreadedEnvelopes;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
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.0.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.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EnvelopesTree {
|
||||
type Target = ThreadedEnvelopes;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.envelopes
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)),
|
||||
|
|
|
@ -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}!"
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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}!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,11 @@ pub mod read;
|
|||
pub mod reply;
|
||||
pub mod save;
|
||||
pub mod send;
|
||||
pub mod thread;
|
||||
pub mod write;
|
||||
|
||||
use color_eyre::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
|
||||
|
@ -18,7 +19,7 @@ use self::{
|
|||
copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand,
|
||||
mailto::MessageMailtoCommand, r#move::MessageMoveCommand, read::MessageReadCommand,
|
||||
reply::MessageReplyCommand, save::MessageSaveCommand, send::MessageSendCommand,
|
||||
write::MessageWriteCommand,
|
||||
thread::MessageThreadCommand, write::MessageWriteCommand,
|
||||
};
|
||||
|
||||
/// Manage messages.
|
||||
|
@ -32,6 +33,9 @@ pub enum MessageSubcommand {
|
|||
#[command(arg_required_else_help = true)]
|
||||
Read(MessageReadCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
Thread(MessageThreadCommand),
|
||||
|
||||
#[command(aliases = ["add", "create", "new", "compose"])]
|
||||
Write(MessageWriteCommand),
|
||||
|
||||
|
@ -66,6 +70,7 @@ impl MessageSubcommand {
|
|||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::Read(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Thread(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Write(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Reply(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Forward(cmd) => cmd.execute(printer, config).await,
|
||||
|
|
|
@ -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}!"
|
||||
))
|
||||
}
|
||||
|
|
|
@ -139,6 +139,6 @@ impl MessageReadCommand {
|
|||
glue = "\n\n";
|
||||
}
|
||||
|
||||
printer.print(bodies)
|
||||
printer.out(bodies)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!")
|
||||
}
|
||||
}
|
||||
|
|
159
src/email/message/command/thread.rs
Normal file
159
src/email/message/command/thread.rs
Normal file
|
@ -0,0 +1,159 @@
|
|||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use mml::message::FilterParts;
|
||||
use tracing::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::envelope::arg::ids::EnvelopeIdArg;
|
||||
#[allow(unused)]
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Thread a message.
|
||||
///
|
||||
/// This command allows you to thread a message. When threading a message,
|
||||
/// the "seen" flag is automatically applied to the corresponding
|
||||
/// envelope. To prevent this behaviour, use the --preview flag.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageThreadCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelope: EnvelopeIdArg,
|
||||
|
||||
/// Thread the message without applying the "seen" flag to its
|
||||
/// corresponding envelope.
|
||||
#[arg(long, short)]
|
||||
pub preview: bool,
|
||||
|
||||
/// Thread the raw version of the given message.
|
||||
///
|
||||
/// The raw message represents the headers and the body as it is
|
||||
/// on the backend, unedited: not decoded nor decrypted. This is
|
||||
/// useful for debugging faulty messages, but also for
|
||||
/// saving/sending/transfering messages.
|
||||
#[arg(long, short)]
|
||||
#[arg(conflicts_with = "no_headers")]
|
||||
#[arg(conflicts_with = "headers")]
|
||||
pub raw: bool,
|
||||
|
||||
/// Thread only body of text/html parts.
|
||||
///
|
||||
/// This argument is useful when you need to thread the HTML version
|
||||
/// of a message. Combined with --no-headers, you can write it to
|
||||
/// a .html file and open it with your favourite browser.
|
||||
#[arg(long)]
|
||||
#[arg(conflicts_with = "raw")]
|
||||
pub html: bool,
|
||||
|
||||
/// Thread only the body of the message.
|
||||
///
|
||||
/// All headers will be removed from the message.
|
||||
#[arg(long)]
|
||||
#[arg(conflicts_with = "raw")]
|
||||
#[arg(conflicts_with = "headers")]
|
||||
pub no_headers: bool,
|
||||
|
||||
/// List of headers that should be visible at the top of the
|
||||
/// message.
|
||||
///
|
||||
/// If a given header is not found in the message, it will not be
|
||||
/// visible. If no header is given, defaults to the one set up in
|
||||
/// your TOML configuration file.
|
||||
#[arg(long = "header", short = 'H', value_name = "NAME")]
|
||||
#[arg(conflicts_with = "raw")]
|
||||
#[arg(conflicts_with = "no_headers")]
|
||||
pub headers: Vec<String>,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageThreadCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing thread message(s) command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let id = &self.envelope.id;
|
||||
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let get_messages_kind = toml_account_config.get_messages_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
get_messages_kind,
|
||||
|builder| {
|
||||
builder.set_thread_envelopes(BackendFeatureSource::Context);
|
||||
builder.set_get_messages(BackendFeatureSource::Context);
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let envelopes = backend
|
||||
.thread_envelope(folder, *id, Default::default())
|
||||
.await?;
|
||||
|
||||
let ids: Vec<_> = envelopes
|
||||
.graph()
|
||||
.nodes()
|
||||
.map(|e| e.id.parse::<usize>().unwrap())
|
||||
.collect();
|
||||
|
||||
let emails = if self.preview {
|
||||
backend.peek_messages(folder, &ids).await
|
||||
} else {
|
||||
backend.get_messages(folder, &ids).await
|
||||
}?;
|
||||
|
||||
let mut glue = "";
|
||||
let mut bodies = String::default();
|
||||
|
||||
for (i, email) in emails.to_vec().iter().enumerate() {
|
||||
bodies.push_str(glue);
|
||||
bodies.push_str(&format!("-------- Message {} --------\n\n", ids[i + 1]));
|
||||
|
||||
if self.raw {
|
||||
// emails do not always have valid utf8, uses "lossy" to
|
||||
// display what can be displayed
|
||||
bodies.push_str(&String::from_utf8_lossy(email.raw()?));
|
||||
} else {
|
||||
let tpl = email
|
||||
.to_read_tpl(&account_config, |mut tpl| {
|
||||
if self.no_headers {
|
||||
tpl = tpl.with_hide_all_headers();
|
||||
} else if !self.headers.is_empty() {
|
||||
tpl = tpl.with_show_only_headers(&self.headers);
|
||||
}
|
||||
|
||||
if self.html {
|
||||
tpl = tpl.with_filter_parts(FilterParts::Only("text/html".into()));
|
||||
}
|
||||
|
||||
tpl
|
||||
})
|
||||
.await?;
|
||||
bodies.push_str(&tpl);
|
||||
}
|
||||
|
||||
glue = "\n\n";
|
||||
}
|
||||
|
||||
printer.out(bodies)
|
||||
}
|
||||
}
|
|
@ -76,6 +76,6 @@ impl TemplateForwardCommand {
|
|||
.build()
|
||||
.await?;
|
||||
|
||||
printer.print(tpl)
|
||||
printer.out(tpl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,6 +81,6 @@ impl TemplateReplyCommand {
|
|||
.build()
|
||||
.await?;
|
||||
|
||||
printer.print(tpl)
|
||||
printer.out(tpl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,6 @@ impl TemplateSendCommand {
|
|||
|
||||
backend.send_message_then_save_copy(&msg).await?;
|
||||
|
||||
printer.print("Message successfully sent!")
|
||||
printer.out("Message successfully sent!")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,6 @@ impl TemplateWriteCommand {
|
|||
.build()
|
||||
.await?;
|
||||
|
||||
printer.print(tpl)
|
||||
printer.out(tpl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
))?;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
pub mod args;
|
||||
#[allow(clippy::module_inception)]
|
||||
pub mod output;
|
||||
|
||||
|
|
|
@ -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
73
src/printer.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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()?)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in a new issue