From 90e12ddc51eefc521e1a8977f029f63b8758a2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 17 May 2024 23:22:06 +0200 Subject: [PATCH 1/8] wip: design basic tree using petgraph --- Cargo.lock | 7 +- Cargo.toml | 10 +- src/account/config.rs | 8 + src/backend/mod.rs | 32 +++ src/config/mod.rs | 1 + src/email/envelope/command/mod.rs | 11 +- src/email/envelope/command/thread.rs | 330 +++++++++++++++++++++++++++ src/email/envelope/config.rs | 21 ++ 8 files changed, 410 insertions(+), 10 deletions(-) create mode 100644 src/email/envelope/command/thread.rs diff --git a/Cargo.lock b/Cargo.lock index 6c86e9b..fc00013 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1396,7 +1396,6 @@ dependencies = [ [[package]] name = "email-lib" version = "0.24.1" -source = "git+https://git.sr.ht/~soywod/pimalaya#033ba2a2e193769e1272c9493aa1d6c975346eb5" dependencies = [ "advisory-lock", "async-ctrlc", @@ -1425,6 +1424,7 @@ dependencies = [ "once_cell", "ouroboros", "paste", + "petgraph", "pgp-lib", "process-lib", "rayon", @@ -2074,6 +2074,7 @@ dependencies = [ "mml-lib", "oauth-lib", "once_cell", + "petgraph", "process-lib", "secret-lib", "serde", @@ -2267,7 +2268,6 @@ dependencies = [ [[package]] name = "imap-client" version = "0.1.0" -source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff" dependencies = [ "imap-flow", "once_cell", @@ -2297,7 +2297,6 @@ dependencies = [ [[package]] name = "imap-flow" version = "0.1.0" -source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff" dependencies = [ "bounded-static", "bytes", @@ -4577,7 +4576,6 @@ dependencies = [ [[package]] name = "tag-generator" version = "0.1.0" -source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff" dependencies = [ "imap-types", "rand", @@ -4592,7 +4590,6 @@ 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", diff --git a/Cargo.toml b/Cargo.toml index 0d2c31a..95a07de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,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"] } @@ -86,8 +87,11 @@ uuid = { version = "0.8", features = ["v4"] } [patch.crates-io] # WIP: transition from `imap` to `imap-codec` -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" } +email-lib = { path = "/home/soywod/sourcehut/pimalaya/email" } +imap-client = { path = "/home/soywod/code/imap-flow/client" } +tasks = { path = "/home/soywod/code/imap-flow/tasks" } +# 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-codec = { git = "https://github.com/duesee/imap-codec.git" } imap-types = { git = "https://github.com/duesee/imap-codec.git" } diff --git a/src/account/config.rs b/src/account/config.rs index ded830f..23abca7 100644 --- a/src/account/config.rs +++ b/src/account/config.rs @@ -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() diff --git a/src/backend/mod.rs b/src/backend/mod.rs index d5dab7c..f45f4c2 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod wizard; use async_trait::async_trait; use color_eyre::Result; +use petgraph::graphmap::DiGraphMap; use std::{fmt::Display, ops::Deref, sync::Arc}; #[cfg(feature = "imap")] @@ -23,6 +24,7 @@ use email::{ envelope::{ get::GetEnvelope, list::{ListEnvelopes, ListEnvelopesOptions}, + thread::ThreadEnvelopes, watch::WatchEnvelopes, Id, SingleId, }, @@ -337,6 +339,23 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder { } } + fn thread_envelopes(&self) -> Option> { + 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> { match self.toml_account_config.watch_envelopes_kind() { #[cfg(feature = "imap")] @@ -687,6 +706,19 @@ impl Backend { Ok(envelopes) } + pub async fn thread_envelopes( + &self, + folder: &str, + opts: ListEnvelopesOptions, + ) -> Result> { + 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 = + // Envelopes::from_backend(&self.backend.account_config, &id_mapper, envelopes)?; + Ok(envelopes) + } + pub async fn add_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> { let backend_kind = self.toml_account_config.add_flags_kind(); let id_mapper = self.build_id_mapper(folder, backend_kind)?; diff --git a/src/config/mod.rs b/src/config/mod.rs index 24ec9c4..fe0474a 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -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, diff --git a/src/email/envelope/command/mod.rs b/src/email/envelope/command/mod.rs index 3de79a6..975effc 100644 --- a/src/email/envelope/command/mod.rs +++ b/src/email/envelope/command/mod.rs @@ -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, } } diff --git a/src/email/envelope/command/thread.rs b/src/email/envelope/command/thread.rs new file mode 100644 index 0000000..22f7ed9 --- /dev/null +++ b/src/email/envelope/command/thread.rs @@ -0,0 +1,330 @@ +use ariadne::{Color, Label, Report, ReportKind, Source}; +use clap::Parser; +use color_eyre::Result; +use email::{ + backend::feature::BackendFeatureSource, + email::search_query, + envelope::list::ListEnvelopesOptions, + search_query::{filter::SearchEmailsFilterQuery, SearchEmailsQuery}, +}; +use petgraph::{graphmap::DiGraphMap, visit::IntoNodeIdentifiers, Direction}; +use std::{ + collections::{HashMap, HashSet}, + io::Write, + 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, +}; + +/// 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, + + /// The page number. + /// + /// The page number starts from 1 (which is the default). Giving a + /// page number to big will result in a out of bound error. + #[arg(long, short, value_name = "NUMBER", default_value = "1")] + pub page: usize, + + /// The page size. + /// + /// Determine the amount of envelopes a page should contain. + #[arg(long, short = 's', value_name = "NUMBER")] + pub page_size: Option, + + #[cfg(feature = "account-sync")] + #[command(flatten)] + pub cache: CacheDisableFlag, + + #[command(flatten)] + pub account: AccountNameFlag, + + /// The maximum width the table should not exceed. + /// + /// This argument will force the table not to exceed the given + /// width in pixels. Columns may shrink with ellipsis in order to + /// fit the width. + #[arg(long = "max-width", short = 'w')] + #[arg(name = "table_max_width", value_name = "PIXELS")] + pub table_max_width: Option, + + /// The thread envelopes filter and sort query. + /// + /// The query can be a filter query, a sort query or both + /// together. + /// + /// A filter query is composed of operators and conditions. There + /// is 3 operators and 8 conditions: + /// + /// • not → filter envelopes that do not match the + /// condition + /// + /// • and → filter envelopes that match + /// both conditions + /// + /// • or → filter envelopes that match + /// one of the conditions + /// + /// ◦ date → filter envelopes that match the given + /// date + /// + /// ◦ before → filter envelopes with date strictly + /// before the given one + /// + /// ◦ after → filter envelopes with date stricly + /// after the given one + /// + /// ◦ from → filter envelopes with senders matching the + /// given pattern + /// + /// ◦ to → filter envelopes with recipients matching + /// the given pattern + /// + /// ◦ subject → filter envelopes with subject matching + /// the given pattern + /// + /// ◦ body → filter envelopes with text bodies matching + /// the given pattern + /// + /// ◦ flag → filter envelopes matching the given flag + /// + /// A sort query starts by "order by", and is composed of kinds + /// and orders. There is 4 kinds and 2 orders: + /// + /// • date [order] → sort envelopes by date + /// + /// • from [order] → sort envelopes by sender + /// + /// • to [order] → sort envelopes by recipient + /// + /// • subject [order] → sort envelopes by subject + /// + /// ◦ asc → sort envelopes by the given kind in ascending + /// order + /// + /// ◦ desc → sort envelopes by the given kind in + /// descending order + /// + /// Examples: + /// + /// subject foo and body bar → filter envelopes containing "foo" + /// in their subject and "bar" in their text bodies + /// + /// order by date desc subject → sort envelopes by descending date + /// (most recent first), then by ascending subject + /// + /// subject foo and body bar order by date desc subject → + /// combination of the 2 previous examples + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + pub query: Option>, +} + +impl Default for ThreadEnvelopesCommand { + fn default() -> Self { + Self { + folder: Default::default(), + page: 1, + page_size: Default::default(), + #[cfg(feature = "account-sync")] + cache: Default::default(), + account: Default::default(), + query: Default::default(), + table_max_width: Default::default(), + } + } +} + +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 page = 1.max(self.page) - 1; + let page_size = self + .page_size + .unwrap_or_else(|| account_config.get_envelope_thread_page_size()); + + 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::()); + // 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(Color::Red), + // ) + // .finish() + // .eprint((source, Source::from(&query))) + // .unwrap(); + // } + + // exit(0) + // } + // }; + + let opts = ListEnvelopesOptions { + page, + page_size, + query: None, + }; + + let graph = backend.thread_envelopes(folder, opts).await?; + + println!("graph: {graph:#?}"); + + let mut stdout = std::io::stdout(); + write_tree(&mut stdout, &graph, 0, String::new(), 0)?; + stdout.flush()?; + + // printer.print_table(envelopes, self.table_max_width)?; + + Ok(()) + } +} + +pub fn write_tree( + w: &mut impl std::io::Write, + graph: &DiGraphMap, + parent: u32, + pad: String, + weight: u32, +) -> std::io::Result<()> { + let edges = graph + .all_edges() + .filter_map(|(a, b, w)| { + if a == parent && *w == weight { + Some(b) + } else { + None + } + }) + .collect::>(); + + writeln!(w, "{parent}")?; + + 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!(w, "{pad}{y}─ ")?; + let pad = format!("{pad}{x} "); + write_tree(w, graph, b, pad, weight + 1)?; + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use petgraph::graphmap::DiGraphMap; + + use super::write_tree; + + #[test] + fn tree_1() { + let mut buf = Vec::new(); + let mut graph = DiGraphMap::new(); + graph.add_edge(0, 1, 0); + graph.add_edge(0, 2, 0); + graph.add_edge(0, 3, 0); + + write_tree(&mut buf, &graph, 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 mut buf = Vec::new(); + let mut graph = DiGraphMap::new(); + graph.add_edge(0, 1, 0); + graph.add_edge(1, 2, 1); + graph.add_edge(1, 3, 1); + + write_tree(&mut buf, &graph, 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 mut buf = Vec::new(); + let mut graph = DiGraphMap::new(); + graph.add_edge(0, 1, 0); + graph.add_edge(1, 2, 1); + graph.add_edge(2, 22, 2); + graph.add_edge(1, 3, 1); + graph.add_edge(0, 4, 0); + graph.add_edge(4, 5, 1); + graph.add_edge(5, 6, 2); + + write_tree(&mut buf, &graph, 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) + } +} diff --git a/src/email/envelope/config.rs b/src/email/envelope/config.rs index 6575f40..4b5e53b 100644 --- a/src/email/envelope/config.rs +++ b/src/email/envelope/config.rs @@ -8,6 +8,7 @@ use crate::backend::BackendKind; #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct EnvelopeConfig { pub list: Option, + pub thread: Option, pub watch: Option, pub get: Option, #[cfg(feature = "account-sync")] @@ -54,6 +55,26 @@ impl ListEnvelopesConfig { } } +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct ThreadEnvelopesConfig { + pub backend: Option, + + #[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, From 55ba8924362ebe9d5cb079954f19983eb3f5533b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 18 May 2024 09:45:03 +0200 Subject: [PATCH 2/8] wip: use custom struct ThreadedEnvelopes --- src/backend/mod.rs | 4 +-- src/email/envelope/command/thread.rs | 44 +++++++++++++--------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index f45f4c2..a549655 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -26,7 +26,7 @@ use email::{ list::{ListEnvelopes, ListEnvelopesOptions}, thread::ThreadEnvelopes, watch::WatchEnvelopes, - Id, SingleId, + Id, SingleId, ThreadedEnvelopes, }, flag::{add::AddFlags, remove::RemoveFlags, set::SetFlags, Flag, Flags}, folder::{ @@ -710,7 +710,7 @@ impl Backend { &self, folder: &str, opts: ListEnvelopesOptions, - ) -> Result> { + ) -> Result { 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?; diff --git a/src/email/envelope/command/thread.rs b/src/email/envelope/command/thread.rs index 22f7ed9..8202ba7 100644 --- a/src/email/envelope/command/thread.rs +++ b/src/email/envelope/command/thread.rs @@ -204,12 +204,10 @@ impl ThreadEnvelopesCommand { query: None, }; - let graph = backend.thread_envelopes(folder, opts).await?; - - println!("graph: {graph:#?}"); + let envelopes = backend.thread_envelopes(folder, opts).await?; let mut stdout = std::io::stdout(); - write_tree(&mut stdout, &graph, 0, String::new(), 0)?; + write_tree(&mut stdout, envelopes.graph(), "root", String::new(), 0)?; stdout.flush()?; // printer.print_table(envelopes, self.table_max_width)?; @@ -220,10 +218,10 @@ impl ThreadEnvelopesCommand { pub fn write_tree( w: &mut impl std::io::Write, - graph: &DiGraphMap, - parent: u32, + graph: &DiGraphMap<&str, u8>, + parent: &str, pad: String, - weight: u32, + weight: u8, ) -> std::io::Result<()> { let edges = graph .all_edges() @@ -264,11 +262,11 @@ mod test { fn tree_1() { let mut buf = Vec::new(); let mut graph = DiGraphMap::new(); - graph.add_edge(0, 1, 0); - graph.add_edge(0, 2, 0); - graph.add_edge(0, 3, 0); + graph.add_edge("0", "1", 0); + graph.add_edge("0", "2", 0); + graph.add_edge("0", "3", 0); - write_tree(&mut buf, &graph, 0, String::new(), 0).unwrap(); + write_tree(&mut buf, &graph, "0", String::new(), 0).unwrap(); let buf = String::from_utf8_lossy(&buf); let expected = " @@ -284,11 +282,11 @@ mod test { fn tree_2() { let mut buf = Vec::new(); let mut graph = DiGraphMap::new(); - graph.add_edge(0, 1, 0); - graph.add_edge(1, 2, 1); - graph.add_edge(1, 3, 1); + graph.add_edge("0", "1", 0); + graph.add_edge("1", "2", 1); + graph.add_edge("1", "3", 1); - write_tree(&mut buf, &graph, 0, String::new(), 0).unwrap(); + write_tree(&mut buf, &graph, "0", String::new(), 0).unwrap(); let buf = String::from_utf8_lossy(&buf); let expected = " @@ -304,15 +302,15 @@ mod test { fn tree_3() { let mut buf = Vec::new(); let mut graph = DiGraphMap::new(); - graph.add_edge(0, 1, 0); - graph.add_edge(1, 2, 1); - graph.add_edge(2, 22, 2); - graph.add_edge(1, 3, 1); - graph.add_edge(0, 4, 0); - graph.add_edge(4, 5, 1); - graph.add_edge(5, 6, 2); + graph.add_edge("0", "1", 0); + graph.add_edge("1", "2", 1); + graph.add_edge("2", "22", 2); + graph.add_edge("1", "3", 1); + graph.add_edge("0", "4", 0); + graph.add_edge("4", "5", 1); + graph.add_edge("5", "6", 2); - write_tree(&mut buf, &graph, 0, String::new(), 0).unwrap(); + write_tree(&mut buf, &graph, "0", String::new(), 0).unwrap(); let buf = String::from_utf8_lossy(&buf); let expected = " From 2eff215934d0a3c6dda586f1a42bf5e048c315f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 21 May 2024 15:25:24 +0200 Subject: [PATCH 3/8] wip: style thread tree using crossterm --- Cargo.lock | 4 + Cargo.toml | 1 + src/email/envelope/command/thread.rs | 177 +++++++++++++++++++-------- 3 files changed, 134 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc00013..a6728b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1006,7 +1006,10 @@ dependencies = [ "bitflags 2.5.0", "crossterm_winapi", "libc", + "mio", "parking_lot 0.12.1", + "signal-hook", + "signal-hook-mio", "winapi", ] @@ -2063,6 +2066,7 @@ dependencies = [ "color-eyre", "comfy-table", "console", + "crossterm 0.27.0", "dirs 4.0.0", "email-lib", "email_address", diff --git a/Cargo.toml b/Cargo.toml index 95a07de..862deb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/email/envelope/command/thread.rs b/src/email/envelope/command/thread.rs index 8202ba7..cad69f0 100644 --- a/src/email/envelope/command/thread.rs +++ b/src/email/envelope/command/thread.rs @@ -1,10 +1,16 @@ -use ariadne::{Color, Label, Report, ReportKind, Source}; +use ariadne::{Label, Report, ReportKind, Source}; use clap::Parser; use color_eyre::Result; +use crossterm::{ + cursor::{self, MoveToColumn}, + style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor}, + terminal, ExecutableCommand, +}; use email::{ + account::config::AccountConfig, backend::feature::BackendFeatureSource, email::search_query, - envelope::list::ListEnvelopesOptions, + envelope::{list::ListEnvelopesOptions, ThreadedEnvelope}, search_query::{filter::SearchEmailsFilterQuery, SearchEmailsQuery}, }; use petgraph::{graphmap::DiGraphMap, visit::IntoNodeIdentifiers, Direction}; @@ -172,42 +178,55 @@ impl ThreadEnvelopesCommand { ) .await?; - // let query = self - // .query - // .map(|query| query.join(" ").parse::()); - // 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(Color::Red), - // ) - // .finish() - // .eprint((source, Source::from(&query))) - // .unwrap(); - // } + let query = self + .query + .map(|query| query.join(" ").parse::()); + 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) - // } - // }; + exit(0) + } + }; let opts = ListEnvelopesOptions { page, page_size, - query: None, + query, }; let envelopes = backend.thread_envelopes(folder, opts).await?; let mut stdout = std::io::stdout(); - write_tree(&mut stdout, envelopes.graph(), "root", String::new(), 0)?; + write_tree( + &account_config, + &mut stdout, + envelopes.graph(), + ThreadedEnvelope { + id: "0", + message_id: "0", + from: "", + subject: "", + date: Default::default(), + }, + String::new(), + 0, + )?; stdout.flush()?; // printer.print_table(envelopes, self.table_max_width)?; @@ -217,9 +236,10 @@ impl ThreadEnvelopesCommand { } pub fn write_tree( + config: &AccountConfig, w: &mut impl std::io::Write, - graph: &DiGraphMap<&str, u8>, - parent: &str, + graph: &DiGraphMap, u8>, + parent: ThreadedEnvelope<'_>, pad: String, weight: u8, ) -> std::io::Result<()> { @@ -234,7 +254,50 @@ pub fn write_tree( }) .collect::>(); - writeln!(w, "{parent}")?; + 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)?; + + if !parent.subject.is_empty() { + w.execute(SetForegroundColor(Color::Green))? + .execute(Print(parent.subject))? + .execute(ResetColor)? + .execute(Print(" "))?; + } + + 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)?; + } + + let date = parent.format_date(config); + let cursor_date_begin_col = terminal::size()?.0 - date.len() as u16; + + w.execute(Print(" "))? + .execute(SetForegroundColor(Color::DarkGrey))? + .execute(Print("·".repeat( + (cursor_date_begin_col - cursor::position()?.0 - 1) as usize, + )))? + .execute(ResetColor)? + .execute(Print(" "))?; + + w.execute(MoveToColumn(terminal::size()?.0 - date.len() as u16))? + .execute(SetForegroundColor(Color::DarkYellow))? + .execute(Print(date))? + .execute(ResetColor)?; + } + + writeln!(w)?; let edges_count = edges.len(); for (i, b) in edges.into_iter().enumerate() { @@ -244,9 +307,11 @@ pub fn write_tree( } else { ('│', '├') }; + write!(w, "{pad}{y}─ ")?; + let pad = format!("{pad}{x} "); - write_tree(w, graph, b, pad, weight + 1)?; + write_tree(config, w, graph, b, pad, weight + 1)?; } Ok(()) @@ -254,19 +319,33 @@ pub fn write_tree( #[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("0", "1", 0); - graph.add_edge("0", "2", 0); - graph.add_edge("0", "3", 0); + 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(&mut buf, &graph, "0", String::new(), 0).unwrap(); + write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); let buf = String::from_utf8_lossy(&buf); let expected = " @@ -280,13 +359,14 @@ mod test { #[test] fn tree_2() { + let config = AccountConfig::default(); let mut buf = Vec::new(); let mut graph = DiGraphMap::new(); - graph.add_edge("0", "1", 0); - graph.add_edge("1", "2", 1); - graph.add_edge("1", "3", 1); + 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(&mut buf, &graph, "0", String::new(), 0).unwrap(); + write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); let buf = String::from_utf8_lossy(&buf); let expected = " @@ -300,17 +380,18 @@ mod test { #[test] fn tree_3() { + let config = AccountConfig::default(); let mut buf = Vec::new(); let mut graph = DiGraphMap::new(); - graph.add_edge("0", "1", 0); - graph.add_edge("1", "2", 1); - graph.add_edge("2", "22", 2); - graph.add_edge("1", "3", 1); - graph.add_edge("0", "4", 0); - graph.add_edge("4", "5", 1); - graph.add_edge("5", "6", 2); + 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(&mut buf, &graph, "0", String::new(), 0).unwrap(); + write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); let buf = String::from_utf8_lossy(&buf); let expected = " From 6cbfc57c838a0a1aa8b02c2cb93b84827fb82a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 22 May 2024 11:07:40 +0200 Subject: [PATCH 4/8] wip: add message thread command --- src/backend/mod.rs | 18 ++- src/email/envelope/command/thread.rs | 134 +++------------------- src/email/message/command/mod.rs | 9 +- src/email/message/command/thread.rs | 159 +++++++++++++++++++++++++++ 4 files changed, 197 insertions(+), 123 deletions(-) create mode 100644 src/email/message/command/thread.rs diff --git a/src/backend/mod.rs b/src/backend/mod.rs index a549655..aeb47d6 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -3,7 +3,6 @@ pub(crate) mod wizard; use async_trait::async_trait; use color_eyre::Result; -use petgraph::graphmap::DiGraphMap; use std::{fmt::Display, ops::Deref, sync::Arc}; #[cfg(feature = "imap")] @@ -719,6 +718,23 @@ impl Backend { Ok(envelopes) } + pub async fn thread_envelope( + &self, + folder: &str, + id: usize, + opts: ListEnvelopesOptions, + ) -> Result { + 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_envelope(folder, SingleId::from(id), opts) + .await?; + // let envelopes = + // Envelopes::from_backend(&self.backend.account_config, &id_mapper, envelopes)?; + Ok(envelopes) + } + pub async fn add_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> { let backend_kind = self.toml_account_config.add_flags_kind(); let id_mapper = self.build_id_mapper(folder, backend_kind)?; diff --git a/src/email/envelope/command/thread.rs b/src/email/envelope/command/thread.rs index cad69f0..a0c36d2 100644 --- a/src/email/envelope/command/thread.rs +++ b/src/email/envelope/command/thread.rs @@ -3,7 +3,7 @@ use clap::Parser; use color_eyre::Result; use crossterm::{ cursor::{self, MoveToColumn}, - style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor}, + style::{Color, Print, ResetColor, SetForegroundColor}, terminal, ExecutableCommand, }; use email::{ @@ -11,14 +11,10 @@ use email::{ backend::feature::BackendFeatureSource, email::search_query, envelope::{list::ListEnvelopesOptions, ThreadedEnvelope}, - search_query::{filter::SearchEmailsFilterQuery, SearchEmailsQuery}, -}; -use petgraph::{graphmap::DiGraphMap, visit::IntoNodeIdentifiers, Direction}; -use std::{ - collections::{HashMap, HashSet}, - io::Write, - process::exit, + search_query::SearchEmailsQuery, }; +use petgraph::graphmap::DiGraphMap; +use std::{io::Write, process::exit}; use tracing::info; #[cfg(feature = "account-sync")] @@ -37,19 +33,6 @@ pub struct ThreadEnvelopesCommand { #[command(flatten)] pub folder: FolderNameOptionalFlag, - /// The page number. - /// - /// The page number starts from 1 (which is the default). Giving a - /// page number to big will result in a out of bound error. - #[arg(long, short, value_name = "NUMBER", default_value = "1")] - pub page: usize, - - /// The page size. - /// - /// Determine the amount of envelopes a page should contain. - #[arg(long, short = 's', value_name = "NUMBER")] - pub page_size: Option, - #[cfg(feature = "account-sync")] #[command(flatten)] pub cache: CacheDisableFlag, @@ -57,103 +40,16 @@ pub struct ThreadEnvelopesCommand { #[command(flatten)] pub account: AccountNameFlag, - /// The maximum width the table should not exceed. - /// - /// This argument will force the table not to exceed the given - /// width in pixels. Columns may shrink with ellipsis in order to - /// fit the width. - #[arg(long = "max-width", short = 'w')] - #[arg(name = "table_max_width", value_name = "PIXELS")] - pub table_max_width: Option, + /// Show only threads that contain the given envelope identifier. + #[arg(long, short)] + pub id: Option, - /// The thread envelopes filter and sort query. - /// - /// The query can be a filter query, a sort query or both - /// together. - /// - /// A filter query is composed of operators and conditions. There - /// is 3 operators and 8 conditions: - /// - /// • not → filter envelopes that do not match the - /// condition - /// - /// • and → filter envelopes that match - /// both conditions - /// - /// • or → filter envelopes that match - /// one of the conditions - /// - /// ◦ date → filter envelopes that match the given - /// date - /// - /// ◦ before → filter envelopes with date strictly - /// before the given one - /// - /// ◦ after → filter envelopes with date stricly - /// after the given one - /// - /// ◦ from → filter envelopes with senders matching the - /// given pattern - /// - /// ◦ to → filter envelopes with recipients matching - /// the given pattern - /// - /// ◦ subject → filter envelopes with subject matching - /// the given pattern - /// - /// ◦ body → filter envelopes with text bodies matching - /// the given pattern - /// - /// ◦ flag → filter envelopes matching the given flag - /// - /// A sort query starts by "order by", and is composed of kinds - /// and orders. There is 4 kinds and 2 orders: - /// - /// • date [order] → sort envelopes by date - /// - /// • from [order] → sort envelopes by sender - /// - /// • to [order] → sort envelopes by recipient - /// - /// • subject [order] → sort envelopes by subject - /// - /// ◦ asc → sort envelopes by the given kind in ascending - /// order - /// - /// ◦ desc → sort envelopes by the given kind in - /// descending order - /// - /// Examples: - /// - /// subject foo and body bar → filter envelopes containing "foo" - /// in their subject and "bar" in their text bodies - /// - /// order by date desc subject → sort envelopes by descending date - /// (most recent first), then by ascending subject - /// - /// subject foo and body bar order by date desc subject → - /// combination of the 2 previous examples #[arg(allow_hyphen_values = true, trailing_var_arg = true)] pub query: Option>, } -impl Default for ThreadEnvelopesCommand { - fn default() -> Self { - Self { - folder: Default::default(), - page: 1, - page_size: Default::default(), - #[cfg(feature = "account-sync")] - cache: Default::default(), - account: Default::default(), - query: Default::default(), - table_max_width: Default::default(), - } - } -} - 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( @@ -163,11 +59,6 @@ impl ThreadEnvelopesCommand { )?; let folder = &self.folder.name; - let page = 1.max(self.page) - 1; - let page_size = self - .page_size - .unwrap_or_else(|| account_config.get_envelope_thread_page_size()); - let thread_envelopes_kind = toml_account_config.thread_envelopes_kind(); let backend = Backend::new( @@ -205,12 +96,15 @@ impl ThreadEnvelopesCommand { }; let opts = ListEnvelopesOptions { - page, - page_size, + page: 0, + page_size: 0, query, }; - let envelopes = backend.thread_envelopes(folder, opts).await?; + let envelopes = match self.id { + Some(id) => backend.thread_envelope(folder, id, opts).await, + None => backend.thread_envelopes(folder, opts).await, + }?; let mut stdout = std::io::stdout(); write_tree( diff --git a/src/email/message/command/mod.rs b/src/email/message/command/mod.rs index dd99709..910c8d9 100644 --- a/src/email/message/command/mod.rs +++ b/src/email/message/command/mod.rs @@ -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, diff --git a/src/email/message/command/thread.rs b/src/email/message/command/thread.rs new file mode 100644 index 0000000..c4e2379 --- /dev/null +++ b/src/email/message/command/thread.rs @@ -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, + + #[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::().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.print(bodies) + } +} From b773218c944a29aad27892cc35a9c76a92e5d40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Thu, 23 May 2024 15:04:48 +0200 Subject: [PATCH 5/8] wip: fix printer, make thread compatible with it --- Cargo.lock | 10 - Cargo.toml | 1 - src/account/command/check_up.rs | 16 +- src/account/command/configure.rs | 2 +- src/account/command/list.rs | 11 +- src/account/command/sync.rs | 30 +- src/account/mod.rs | 144 ++++----- src/backend/mod.rs | 2 +- src/cli.rs | 26 +- src/email/envelope/command/list.rs | 6 +- src/email/envelope/command/thread.rs | 278 ++++++------------ src/email/envelope/command/watch.rs | 2 +- src/email/envelope/flag/command/add.rs | 2 +- src/email/envelope/flag/command/remove.rs | 2 +- src/email/envelope/flag/command/set.rs | 2 +- src/email/envelope/mod.rs | 241 +++++++++++---- .../message/attachment/command/download.rs | 12 +- src/email/message/command/copy.rs | 2 +- src/email/message/command/delete.rs | 2 +- src/email/message/command/move.rs | 2 +- src/email/message/command/read.rs | 2 +- src/email/message/command/save.rs | 2 +- src/email/message/command/send.rs | 2 +- src/email/message/command/thread.rs | 2 +- src/email/message/template/command/forward.rs | 2 +- src/email/message/template/command/reply.rs | 2 +- src/email/message/template/command/save.rs | 2 +- src/email/message/template/command/send.rs | 2 +- src/email/message/template/command/write.rs | 2 +- src/email/message/template/mod.rs | 12 - src/folder/command/add.rs | 2 +- src/folder/command/delete.rs | 2 +- src/folder/command/expunge.rs | 2 +- src/folder/command/list.rs | 10 +- src/folder/command/purge.rs | 2 +- src/folder/mod.rs | 120 ++++---- src/main.rs | 2 +- src/manual/command.rs | 6 +- src/output/mod.rs | 1 - src/output/output.rs | 63 +--- src/printer.rs | 73 +++++ src/printer/mod.rs | 13 - src/printer/print.rs | 21 -- src/printer/printer.rs | 102 ------- src/ui/editor.rs | 8 +- 45 files changed, 556 insertions(+), 694 deletions(-) create mode 100644 src/printer.rs delete mode 100644 src/printer/mod.rs delete mode 100644 src/printer/print.rs delete mode 100644 src/printer/printer.rs diff --git a/Cargo.lock b/Cargo.lock index a6728b8..9eb0bcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2086,7 +2086,6 @@ dependencies = [ "serde_json", "shellexpand-utils", "sled", - "termcolor", "terminal_size 0.1.17", "tokio", "toml", @@ -4624,15 +4623,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" version = "0.1.17" diff --git a/Cargo.toml b/Cargo.toml index 862deb3..9c9c5eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,6 @@ serde-toml-merge = "0.3" serde_json = "1" shellexpand-utils = "=0.2.1" sled = "=0.34.7" -termcolor = "1" terminal_size = "0.1" tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] } toml = "0.8" diff --git a/src/account/command/check_up.rs b/src/account/command/check_up.rs index 9e94839..8d18332 100644 --- a/src/account/command/check_up.rs +++ b/src/account/command/check_up.rs @@ -24,7 +24,7 @@ impl AccountCheckUpCommand { let account = self.account.name.as_ref().map(String::as_str); - printer.print_log("Checking configuration integrity…")?; + printer.log("Checking configuration integrity…")?; let (toml_account_config, account_config) = config.clone().into_account_configs( account, @@ -33,7 +33,7 @@ impl AccountCheckUpCommand { )?; let used_backends = toml_account_config.get_used_backends(); - printer.print_log("Checking backend context integrity…")?; + printer.log("Checking backend context integrity…")?; let ctx_builder = backend::BackendContextBuilder::new( toml_account_config.clone(), @@ -46,7 +46,7 @@ impl AccountCheckUpCommand { #[cfg(feature = "maildir")] { - printer.print_log("Checking Maildir integrity…")?; + printer.log("Checking Maildir integrity…")?; let maildir = ctx_builder .maildir @@ -61,7 +61,7 @@ impl AccountCheckUpCommand { #[cfg(feature = "imap")] { - printer.print_log("Checking IMAP integrity…")?; + printer.log("Checking IMAP integrity…")?; let imap = ctx_builder .imap @@ -76,7 +76,7 @@ impl AccountCheckUpCommand { #[cfg(feature = "notmuch")] { - printer.print_log("Checking Notmuch integrity…")?; + printer.print("Checking Notmuch integrity…")?; let notmuch = ctx_builder .notmuch @@ -91,7 +91,7 @@ impl AccountCheckUpCommand { #[cfg(feature = "smtp")] { - printer.print_log("Checking SMTP integrity…")?; + printer.log("Checking SMTP integrity…")?; let smtp = ctx_builder .smtp @@ -106,7 +106,7 @@ impl AccountCheckUpCommand { #[cfg(feature = "sendmail")] { - printer.print_log("Checking Sendmail integrity…")?; + printer.log("Checking Sendmail integrity…")?; let sendmail = ctx_builder .sendmail @@ -119,6 +119,6 @@ impl AccountCheckUpCommand { } } - printer.print("Checkup successfully completed!") + printer.out("Checkup successfully completed!") } } diff --git a/src/account/command/configure.rs b/src/account/command/configure.rs index 69be407..cddde83 100644 --- a/src/account/command/configure.rs +++ b/src/account/command/configure.rs @@ -105,7 +105,7 @@ impl AccountConfigureCommand { .await?; } - printer.print(format!( + printer.out(format!( "Account {account} successfully {}configured!", if self.reset { "re" } else { "" } )) diff --git a/src/account/command/list.rs b/src/account/command/list.rs index fa30b4b..d98531c 100644 --- a/src/account/command/list.rs +++ b/src/account/command/list.rs @@ -2,7 +2,11 @@ use clap::Parser; use color_eyre::Result; use tracing::info; -use crate::{account::Accounts, config::TomlConfig, printer::Printer}; +use crate::{ + account::{Accounts, AccountsTable}, + config::TomlConfig, + printer::Printer, +}; /// List all accounts. /// @@ -23,9 +27,10 @@ impl AccountListCommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing list accounts command"); - let accounts: Accounts = config.accounts.iter().into(); + let accounts = Accounts::from(config.accounts.iter()); + let table = AccountsTable::from(accounts).with_some_width(self.table_max_width); - printer.print_table(accounts, self.table_max_width)?; + printer.out(table)?; Ok(()) } } diff --git a/src/account/command/sync.rs b/src/account/command/sync.rs index 929dd78..0950eb2 100644 --- a/src/account/command/sync.rs +++ b/src/account/command/sync.rs @@ -138,28 +138,28 @@ impl AccountSyncCommand { let mut hunks_count = report.folder.patch.len(); if !report.folder.patch.is_empty() { - printer.print_log("Folders patch:")?; + printer.log("Folders patch:")?; for (hunk, _) in report.folder.patch { - printer.print_log(format!(" - {hunk}"))?; + printer.log(format!(" - {hunk}"))?; } - printer.print_log("")?; + printer.log("")?; } if !report.email.patch.is_empty() { - printer.print_log("Envelopes patch:")?; + printer.log("Envelopes patch:")?; for (hunk, _) in report.email.patch { hunks_count += 1; - printer.print_log(format!(" - {hunk}"))?; + printer.log(format!(" - {hunk}"))?; } - printer.print_log("")?; + printer.log("")?; } - printer.print(format!( + printer.out(format!( "Estimated patch length for account {account_name} to be synchronized: {hunks_count}" ))?; } else if printer.is_json() { sync_builder.sync().await?; - printer.print(format!("Account {account_name} successfully synchronized!"))?; + printer.out(format!("Account {account_name} successfully synchronized!"))?; } else { let multi = MultiProgress::new(); let sub_progresses = Mutex::new(HashMap::new()); @@ -239,11 +239,11 @@ impl AccountSyncCommand { .filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err))) .collect::>(); if !folders_patch_err.is_empty() { - printer.print_log("")?; - printer.print_log("Errors occurred while applying the folders patch:")?; + printer.log("")?; + printer.log("Errors occurred while applying the folders patch:")?; folders_patch_err .iter() - .try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?; + .try_for_each(|(hunk, err)| printer.log(format!(" - {hunk}: {err}")))?; } let envelopes_patch_err = report @@ -253,14 +253,14 @@ impl AccountSyncCommand { .filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err))) .collect::>(); if !envelopes_patch_err.is_empty() { - printer.print_log("")?; - printer.print_log("Errors occurred while applying the envelopes patch:")?; + printer.log("")?; + printer.log("Errors occurred while applying the envelopes patch:")?; for (hunk, err) in envelopes_patch_err { - printer.print_log(format!(" - {hunk}: {err}"))?; + printer.log(format!(" - {hunk}: {err}"))?; } } - printer.print(format!("Account {account_name} successfully synchronized!"))?; + printer.out(format!("Account {account_name} successfully synchronized!"))?; } Ok(()) diff --git a/src/account/mod.rs b/src/account/mod.rs index 65ab97e..1cf6e9a 100644 --- a/src/account/mod.rs +++ b/src/account/mod.rs @@ -3,13 +3,10 @@ pub mod command; pub mod config; pub(crate) mod wizard; -use color_eyre::Result; use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table}; -use serde::Serialize; +use serde::{Serialize, Serializer}; use std::{collections::hash_map::Iter, fmt, ops::Deref}; -use crate::printer::{PrintTable, WriteColor}; - use self::config::TomlAccountConfig; /// Represents the printable account. @@ -31,6 +28,16 @@ impl Account { default, } } + + pub fn to_row(&self) -> Row { + let mut row = Row::new(); + + row.add_cell(Cell::new(&self.name).fg(Color::Green)); + row.add_cell(Cell::new(&self.backend).fg(Color::Blue)); + row.add_cell(Cell::new(if self.default { "yes" } else { "" }).fg(Color::White)); + + row + } } impl fmt::Display for Account { @@ -39,28 +46,27 @@ impl fmt::Display for Account { } } -impl From for Row { - fn from(account: Account) -> Self { - let mut r = Row::new(); - r.add_cell(Cell::new(account.name).fg(Color::Green)); - r.add_cell(Cell::new(account.backend).fg(Color::Blue)); - r.add_cell(Cell::new(if account.default { "yes" } else { "" }).fg(Color::White)); - r - } -} -impl From<&Account> for Row { - fn from(account: &Account) -> Self { - let mut r = Row::new(); - r.add_cell(Cell::new(&account.name).fg(Color::Green)); - r.add_cell(Cell::new(&account.backend).fg(Color::Blue)); - r.add_cell(Cell::new(if account.default { "yes" } else { "" }).fg(Color::White)); - r - } -} - /// Represents the list of printable accounts. #[derive(Debug, Default, Serialize)] -pub struct Accounts(pub Vec); +pub struct Accounts(Vec); + +impl Accounts { + pub fn to_table(&self) -> Table { + let mut table = Table::new(); + + table + .load_preset(presets::NOTHING) + .set_content_arrangement(ContentArrangement::Dynamic) + .set_header(Row::from([ + Cell::new("NAME").add_attribute(Attribute::Reverse), + Cell::new("BACKENDS").add_attribute(Attribute::Reverse), + Cell::new("DEFAULT").add_attribute(Attribute::Reverse), + ])) + .add_rows(self.iter().map(Account::to_row)); + + table + } +} impl Deref for Accounts { type Target = Vec; @@ -70,51 +76,6 @@ impl Deref for Accounts { } } -impl From for Table { - fn from(accounts: Accounts) -> Self { - let mut table = Table::new(); - table - .load_preset(presets::NOTHING) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(Row::from([ - Cell::new("NAME").add_attribute(Attribute::Reverse), - Cell::new("BACKENDS").add_attribute(Attribute::Reverse), - Cell::new("DEFAULT").add_attribute(Attribute::Reverse), - ])) - .add_rows(accounts.0.into_iter().map(Row::from)); - table - } -} - -impl From<&Accounts> for Table { - fn from(accounts: &Accounts) -> Self { - let mut table = Table::new(); - table - .load_preset(presets::NOTHING) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(Row::from([ - Cell::new("NAME").add_attribute(Attribute::Reverse), - Cell::new("BACKENDS").add_attribute(Attribute::Reverse), - Cell::new("DEFAULT").add_attribute(Attribute::Reverse), - ])) - .add_rows(accounts.0.iter().map(Row::from)); - table - } -} - -impl PrintTable for Accounts { - fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option) -> Result<()> { - let mut table = Table::from(self); - if let Some(width) = table_max_width { - table.set_width(width); - } - writeln!(writer)?; - write!(writer, "{}", table)?; - writeln!(writer)?; - Ok(()) - } -} - impl From> for Accounts { fn from(map: Iter<'_, String, TomlAccountConfig>) -> Self { let mut accounts: Vec<_> = map @@ -169,3 +130,48 @@ impl From> for Accounts { Self(accounts) } } + +pub struct AccountsTable { + accounts: Accounts, + width: Option, +} + +impl AccountsTable { + pub fn with_some_width(mut self, width: Option) -> Self { + self.width = width; + self + } +} + +impl From for AccountsTable { + fn from(accounts: Accounts) -> Self { + Self { + accounts, + width: None, + } + } +} + +impl fmt::Display for AccountsTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = self.accounts.to_table(); + + if let Some(width) = self.width { + table.set_width(width); + } + + writeln!(f)?; + write!(f, "{table}")?; + writeln!(f)?; + Ok(()) + } +} + +impl Serialize for AccountsTable { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.accounts.serialize(serializer) + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index aeb47d6..56f712c 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -701,7 +701,7 @@ impl Backend { let id_mapper = self.build_id_mapper(folder, backend_kind)?; let envelopes = self.backend.list_envelopes(folder, opts).await?; let envelopes = - Envelopes::from_backend(&self.backend.account_config, &id_mapper, envelopes)?; + Envelopes::try_from_backend(&self.backend.account_config, &id_mapper, envelopes)?; Ok(envelopes) } diff --git a/src/cli.rs b/src/cli.rs index 440f08b..22044d4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -14,7 +14,7 @@ use crate::{ attachment::command::AttachmentSubcommand, command::MessageSubcommand, template::command::TemplateSubcommand, }, - output::{ColorFmt, OutputFmt}, + output::OutputFmt, printer::Printer, }; @@ -52,30 +52,6 @@ pub struct Cli { #[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())] pub output: OutputFmt, - /// Control when to use colors - /// - /// The default setting is 'auto', which means himalaya will try - /// to guess when to use colors. For example, if himalaya is - /// printing to a terminal, then it will use colors, but if it is - /// redirected to a file or a pipe, then it will suppress color - /// output. himalaya will suppress color output in some other - /// circumstances as well. For example, if the TERM environment - /// variable is not set or set to 'dumb', then himalaya will not - /// use colors. - /// - /// The possible values are: - /// - /// - never: colors will never be used - /// - /// - always: colors will always be used regardless of where output is sent - /// - /// - ansi: like 'always', but emits ANSI escapes (even in a Windows console) - /// - /// - auto: himalaya tries to be smart - #[arg(long, short = 'C', global = true)] - #[arg(value_name = "MODE", value_enum, default_value_t = Default::default())] - pub color: ColorFmt, - /// Enable logs with spantrace. /// /// This is the same as running the command with `RUST_LOG=debug` diff --git a/src/email/envelope/command/list.rs b/src/email/envelope/command/list.rs index cbee4c2..a97f19d 100644 --- a/src/email/envelope/command/list.rs +++ b/src/email/envelope/command/list.rs @@ -12,7 +12,7 @@ use tracing::info; use crate::cache::arg::disable::CacheDisableFlag; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, - folder::arg::name::FolderNameOptionalFlag, printer::Printer, + envelope::EnvelopesTable, folder::arg::name::FolderNameOptionalFlag, printer::Printer, }; /// List all envelopes. @@ -198,9 +198,9 @@ impl ListEnvelopesCommand { }; let envelopes = backend.list_envelopes(folder, opts).await?; + let table = EnvelopesTable::from(envelopes).with_some_width(self.table_max_width); - printer.print_table(envelopes, self.table_max_width)?; - + printer.out(table)?; Ok(()) } } diff --git a/src/email/envelope/command/thread.rs b/src/email/envelope/command/thread.rs index a0c36d2..387bd60 100644 --- a/src/email/envelope/command/thread.rs +++ b/src/email/envelope/command/thread.rs @@ -1,27 +1,18 @@ use ariadne::{Label, Report, ReportKind, Source}; use clap::Parser; use color_eyre::Result; -use crossterm::{ - cursor::{self, MoveToColumn}, - style::{Color, Print, ResetColor, SetForegroundColor}, - terminal, ExecutableCommand, -}; use email::{ - account::config::AccountConfig, - backend::feature::BackendFeatureSource, - email::search_query, - envelope::{list::ListEnvelopesOptions, ThreadedEnvelope}, - search_query::SearchEmailsQuery, + backend::feature::BackendFeatureSource, email::search_query, + envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery, }; -use petgraph::graphmap::DiGraphMap; -use std::{io::Write, process::exit}; +use std::process::exit; use tracing::info; #[cfg(feature = "account-sync")] use crate::cache::arg::disable::CacheDisableFlag; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, - folder::arg::name::FolderNameOptionalFlag, printer::Printer, + envelope::EnvelopesTree, folder::arg::name::FolderNameOptionalFlag, printer::Printer, }; /// Thread all envelopes. @@ -49,7 +40,7 @@ pub struct ThreadEnvelopesCommand { } impl ThreadEnvelopesCommand { - pub async fn execute(self, _printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing thread envelopes command"); let (toml_account_config, account_config) = config.clone().into_account_configs( @@ -106,198 +97,101 @@ impl ThreadEnvelopesCommand { None => backend.thread_envelopes(folder, opts).await, }?; - let mut stdout = std::io::stdout(); - write_tree( - &account_config, - &mut stdout, - envelopes.graph(), - ThreadedEnvelope { - id: "0", - message_id: "0", - from: "", - subject: "", - date: Default::default(), - }, - String::new(), - 0, - )?; - stdout.flush()?; + let tree = EnvelopesTree::new(account_config, envelopes); - // printer.print_table(envelopes, self.table_max_width)?; + printer.out(tree)?; Ok(()) } } -pub fn write_tree( - config: &AccountConfig, - w: &mut impl std::io::Write, - graph: &DiGraphMap, u8>, - parent: ThreadedEnvelope<'_>, - pad: String, - weight: u8, -) -> std::io::Result<()> { - let edges = graph - .all_edges() - .filter_map(|(a, b, w)| { - if a == parent && *w == weight { - Some(b) - } else { - None - } - }) - .collect::>(); +// #[cfg(test)] +// mod test { +// use email::{account::config::AccountConfig, envelope::ThreadedEnvelope}; +// use petgraph::graphmap::DiGraphMap; - if parent.id == "0" { - w.execute(Print("root"))?; - } else { - w.execute(SetForegroundColor(Color::Red))? - .execute(Print(parent.id))? - .execute(SetForegroundColor(Color::DarkGrey))? - .execute(Print(") "))? - .execute(ResetColor)?; +// use super::write_tree; - if !parent.subject.is_empty() { - w.execute(SetForegroundColor(Color::Green))? - .execute(Print(parent.subject))? - .execute(ResetColor)? - .execute(Print(" "))?; - } +// macro_rules! e { +// ($id:literal) => { +// ThreadedEnvelope { +// id: $id, +// message_id: $id, +// from: "", +// subject: "", +// date: Default::default(), +// } +// }; +// } - if !parent.from.is_empty() { - w.execute(SetForegroundColor(Color::DarkGrey))? - .execute(Print("<"))? - .execute(SetForegroundColor(Color::Blue))? - .execute(Print(parent.from))? - .execute(SetForegroundColor(Color::DarkGrey))? - .execute(Print(">"))? - .execute(ResetColor)?; - } +// #[test] +// fn tree_1() { +// let config = AccountConfig::default(); +// let mut buf = Vec::new(); +// let mut graph = DiGraphMap::new(); +// graph.add_edge(e!("0"), e!("1"), 0); +// graph.add_edge(e!("0"), e!("2"), 0); +// graph.add_edge(e!("0"), e!("3"), 0); - let date = parent.format_date(config); - let cursor_date_begin_col = terminal::size()?.0 - date.len() as u16; +// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); +// let buf = String::from_utf8_lossy(&buf); - w.execute(Print(" "))? - .execute(SetForegroundColor(Color::DarkGrey))? - .execute(Print("·".repeat( - (cursor_date_begin_col - cursor::position()?.0 - 1) as usize, - )))? - .execute(ResetColor)? - .execute(Print(" "))?; +// let expected = " +// 0 +// ├─ 1 +// ├─ 2 +// └─ 3 +// "; +// assert_eq!(expected.trim_start(), buf) +// } - w.execute(MoveToColumn(terminal::size()?.0 - date.len() as u16))? - .execute(SetForegroundColor(Color::DarkYellow))? - .execute(Print(date))? - .execute(ResetColor)?; - } +// #[test] +// fn tree_2() { +// let config = AccountConfig::default(); +// let mut buf = Vec::new(); +// let mut graph = DiGraphMap::new(); +// graph.add_edge(e!("0"), e!("1"), 0); +// graph.add_edge(e!("1"), e!("2"), 1); +// graph.add_edge(e!("1"), e!("3"), 1); - writeln!(w)?; +// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); +// let buf = String::from_utf8_lossy(&buf); - let edges_count = edges.len(); - for (i, b) in edges.into_iter().enumerate() { - let is_last = edges_count == i + 1; - let (x, y) = if is_last { - (' ', '└') - } else { - ('│', '├') - }; +// let expected = " +// 0 +// └─ 1 +// ├─ 2 +// └─ 3 +// "; +// assert_eq!(expected.trim_start(), buf) +// } - write!(w, "{pad}{y}─ ")?; +// #[test] +// fn tree_3() { +// let config = AccountConfig::default(); +// let mut buf = Vec::new(); +// let mut graph = DiGraphMap::new(); +// graph.add_edge(e!("0"), e!("1"), 0); +// graph.add_edge(e!("1"), e!("2"), 1); +// graph.add_edge(e!("2"), e!("22"), 2); +// graph.add_edge(e!("1"), e!("3"), 1); +// graph.add_edge(e!("0"), e!("4"), 0); +// graph.add_edge(e!("4"), e!("5"), 1); +// graph.add_edge(e!("5"), e!("6"), 2); - let pad = format!("{pad}{x} "); - write_tree(config, w, graph, b, pad, weight + 1)?; - } +// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); +// let buf = String::from_utf8_lossy(&buf); - Ok(()) -} - -#[cfg(test)] -mod test { - use email::{account::config::AccountConfig, envelope::ThreadedEnvelope}; - use petgraph::graphmap::DiGraphMap; - - use super::write_tree; - - macro_rules! e { - ($id:literal) => { - ThreadedEnvelope { - id: $id, - message_id: $id, - from: "", - subject: "", - date: Default::default(), - } - }; - } - - #[test] - fn tree_1() { - let config = AccountConfig::default(); - let mut buf = Vec::new(); - let mut graph = DiGraphMap::new(); - graph.add_edge(e!("0"), e!("1"), 0); - graph.add_edge(e!("0"), e!("2"), 0); - graph.add_edge(e!("0"), e!("3"), 0); - - write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); - let buf = String::from_utf8_lossy(&buf); - - let expected = " -0 -├─ 1 -├─ 2 -└─ 3 -"; - assert_eq!(expected.trim_start(), buf) - } - - #[test] - fn tree_2() { - let config = AccountConfig::default(); - let mut buf = Vec::new(); - let mut graph = DiGraphMap::new(); - graph.add_edge(e!("0"), e!("1"), 0); - graph.add_edge(e!("1"), e!("2"), 1); - graph.add_edge(e!("1"), e!("3"), 1); - - write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); - let buf = String::from_utf8_lossy(&buf); - - let expected = " -0 -└─ 1 - ├─ 2 - └─ 3 -"; - assert_eq!(expected.trim_start(), buf) - } - - #[test] - fn tree_3() { - let config = AccountConfig::default(); - let mut buf = Vec::new(); - let mut graph = DiGraphMap::new(); - graph.add_edge(e!("0"), e!("1"), 0); - graph.add_edge(e!("1"), e!("2"), 1); - graph.add_edge(e!("2"), e!("22"), 2); - graph.add_edge(e!("1"), e!("3"), 1); - graph.add_edge(e!("0"), e!("4"), 0); - graph.add_edge(e!("4"), e!("5"), 1); - graph.add_edge(e!("5"), e!("6"), 2); - - write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); - let buf = String::from_utf8_lossy(&buf); - - let expected = " -0 -├─ 1 -│ ├─ 2 -│ │ └─ 22 -│ └─ 3 -└─ 4 - └─ 5 - └─ 6 -"; - assert_eq!(expected.trim_start(), buf) - } -} +// let expected = " +// 0 +// ├─ 1 +// │ ├─ 2 +// │ │ └─ 22 +// │ └─ 3 +// └─ 4 +// └─ 5 +// └─ 6 +// "; +// assert_eq!(expected.trim_start(), buf) +// } +// } diff --git a/src/email/envelope/command/watch.rs b/src/email/envelope/command/watch.rs index 1f93458..f4cc656 100644 --- a/src/email/envelope/command/watch.rs +++ b/src/email/envelope/command/watch.rs @@ -48,7 +48,7 @@ impl WatchEnvelopesCommand { ) .await?; - printer.print_log(format!( + printer.out(format!( "Start watching folder {folder} for envelopes changes…" ))?; diff --git a/src/email/envelope/flag/command/add.rs b/src/email/envelope/flag/command/add.rs index dcfc458..ed7da11 100644 --- a/src/email/envelope/flag/command/add.rs +++ b/src/email/envelope/flag/command/add.rs @@ -58,6 +58,6 @@ impl FlagAddCommand { backend.add_flags(folder, &ids, &flags).await?; - printer.print(format!("Flag(s) {flags} successfully added!")) + printer.out(format!("Flag(s) {flags} successfully added!")) } } diff --git a/src/email/envelope/flag/command/remove.rs b/src/email/envelope/flag/command/remove.rs index 1ef7c23..6e8fc2a 100644 --- a/src/email/envelope/flag/command/remove.rs +++ b/src/email/envelope/flag/command/remove.rs @@ -58,6 +58,6 @@ impl FlagRemoveCommand { backend.remove_flags(folder, &ids, &flags).await?; - printer.print(format!("Flag(s) {flags} successfully removed!")) + printer.out(format!("Flag(s) {flags} successfully removed!")) } } diff --git a/src/email/envelope/flag/command/set.rs b/src/email/envelope/flag/command/set.rs index 3cdd9f7..7913e35 100644 --- a/src/email/envelope/flag/command/set.rs +++ b/src/email/envelope/flag/command/set.rs @@ -58,6 +58,6 @@ impl FlagSetCommand { backend.set_flags(folder, &ids, &flags).await?; - printer.print(format!("Flag(s) {flags} successfully replaced!")) + printer.out(format!("Flag(s) {flags} successfully replaced!")) } } diff --git a/src/email/envelope/mod.rs b/src/email/envelope/mod.rs index 46c9acc..e4ac959 100644 --- a/src/email/envelope/mod.rs +++ b/src/email/envelope/mod.rs @@ -4,15 +4,19 @@ pub mod config; pub mod flag; use color_eyre::Result; -use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table}; -use email::account::config::AccountConfig; -use serde::Serialize; -use std::ops; +use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table}; +use crossterm::{cursor, style::Stylize, terminal}; +use email::{ + account::config::AccountConfig, + envelope::{ThreadedEnvelope, ThreadedEnvelopes}, +}; +use petgraph::graphmap::DiGraphMap; +use serde::{Serialize, Serializer}; +use std::{fmt, ops::Deref, sync::Arc}; use crate::{ cache::IdMapper, flag::{Flag, Flags}, - printer::{PrintTable, WriteColor}, }; #[derive(Clone, Debug, Default, Serialize)] @@ -60,17 +64,17 @@ impl From for Row { row.add_cell( Cell::new(envelope.id) .add_attributes(all_attributes.clone()) - .fg(Color::Red), + .fg(comfy_table::Color::Red), ) .add_cell( Cell::new(flags) .add_attributes(all_attributes.clone()) - .fg(Color::White), + .fg(comfy_table::Color::White), ) .add_cell( Cell::new(envelope.subject) .add_attributes(all_attributes.clone()) - .fg(Color::Green), + .fg(comfy_table::Color::Green), ) .add_cell( Cell::new(if let Some(name) = envelope.from.name { @@ -79,12 +83,12 @@ impl From for Row { envelope.from.addr }) .add_attributes(all_attributes.clone()) - .fg(Color::Blue), + .fg(comfy_table::Color::Blue), ) .add_cell( Cell::new(envelope.date) .add_attributes(all_attributes) - .fg(Color::Yellow), + .fg(comfy_table::Color::Yellow), ); row @@ -121,17 +125,17 @@ impl From<&Envelope> for Row { row.add_cell( Cell::new(&envelope.id) .add_attributes(all_attributes.clone()) - .fg(Color::Red), + .fg(comfy_table::Color::Red), ) .add_cell( Cell::new(flags) .add_attributes(all_attributes.clone()) - .fg(Color::White), + .fg(comfy_table::Color::White), ) .add_cell( Cell::new(&envelope.subject) .add_attributes(all_attributes.clone()) - .fg(Color::Green), + .fg(comfy_table::Color::Green), ) .add_cell( Cell::new(if let Some(name) = &envelope.from.name { @@ -140,12 +144,12 @@ impl From<&Envelope> for Row { &envelope.from.addr }) .add_attributes(all_attributes.clone()) - .fg(Color::Blue), + .fg(comfy_table::Color::Blue), ) .add_cell( Cell::new(&envelope.date) .add_attributes(all_attributes) - .fg(Color::Yellow), + .fg(comfy_table::Color::Yellow), ); row @@ -156,46 +160,8 @@ impl From<&Envelope> for Row { #[derive(Clone, Debug, Default, Serialize)] pub struct Envelopes(Vec); -impl From for Table { - fn from(envelopes: Envelopes) -> Self { - let mut table = Table::new(); - table - .load_preset(presets::NOTHING) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(Row::from([ - Cell::new("ID").add_attribute(Attribute::Reverse), - Cell::new("FLAGS").add_attribute(Attribute::Reverse), - Cell::new("SUBJECT").add_attribute(Attribute::Reverse), - Cell::new("FROM").add_attribute(Attribute::Reverse), - Cell::new("DATE").add_attribute(Attribute::Reverse), - ])) - .add_rows(envelopes.0.into_iter().map(Row::from)); - - table - } -} - -impl From<&Envelopes> for Table { - fn from(envelopes: &Envelopes) -> Self { - let mut table = Table::new(); - table - .load_preset(presets::NOTHING) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(Row::from([ - Cell::new("ID").add_attribute(Attribute::Reverse), - Cell::new("FLAGS").add_attribute(Attribute::Reverse), - Cell::new("SUBJECT").add_attribute(Attribute::Reverse), - Cell::new("FROM").add_attribute(Attribute::Reverse), - Cell::new("DATE").add_attribute(Attribute::Reverse), - ])) - .add_rows(envelopes.0.iter().map(Row::from)); - - table - } -} - impl Envelopes { - pub fn from_backend( + pub fn try_from_backend( config: &AccountConfig, id_mapper: &IdMapper, envelopes: email::envelope::Envelopes, @@ -222,9 +188,27 @@ impl Envelopes { Ok(Envelopes(envelopes)) } + + pub fn to_table(&self) -> Table { + let mut table = Table::new(); + + table + .load_preset(presets::NOTHING) + .set_content_arrangement(ContentArrangement::Dynamic) + .set_header(Row::from([ + Cell::new("ID").add_attribute(Attribute::Reverse), + Cell::new("FLAGS").add_attribute(Attribute::Reverse), + Cell::new("SUBJECT").add_attribute(Attribute::Reverse), + Cell::new("FROM").add_attribute(Attribute::Reverse), + Cell::new("DATE").add_attribute(Attribute::Reverse), + ])) + .add_rows(self.iter().map(Row::from)); + + table + } } -impl ops::Deref for Envelopes { +impl Deref for Envelopes { type Target = Vec; fn deref(&self) -> &Self::Target { @@ -232,15 +216,148 @@ impl ops::Deref for Envelopes { } } -impl PrintTable for Envelopes { - fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option) -> Result<()> { - let mut table = Table::from(self); - if let Some(width) = table_max_width { +pub struct EnvelopesTable { + envelopes: Envelopes, + width: Option, +} + +impl EnvelopesTable { + pub fn with_some_width(mut self, width: Option) -> Self { + self.width = width; + self + } +} + +impl From for EnvelopesTable { + fn from(envelopes: Envelopes) -> Self { + Self { + envelopes, + width: None, + } + } +} + +impl fmt::Display for EnvelopesTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = self.envelopes.to_table(); + + if let Some(width) = self.width { table.set_width(width); } - writeln!(writer)?; - write!(writer, "{}", table)?; - writeln!(writer)?; + + writeln!(f)?; + write!(f, "{table}")?; + writeln!(f)?; Ok(()) } } + +impl Serialize for EnvelopesTable { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.envelopes.serialize(serializer) + } +} + +pub struct EnvelopesTree { + config: Arc, + envelopes: ThreadedEnvelopes, +} + +impl EnvelopesTree { + pub fn new(config: Arc, envelopes: ThreadedEnvelopes) -> Self { + Self { config, envelopes } + } + + pub fn fmt( + f: &mut fmt::Formatter, + config: &AccountConfig, + graph: &DiGraphMap, u8>, + parent: ThreadedEnvelope<'_>, + pad: String, + weight: u8, + ) -> fmt::Result { + let edges = graph + .all_edges() + .filter_map(|(a, b, w)| { + if a == parent && *w == weight { + Some(b) + } else { + None + } + }) + .collect::>(); + + if parent.id == "0" { + f.write_str("root")?; + } else { + write!(f, "{}{}", parent.id.red(), ") ".dark_grey())?; + + if !parent.subject.is_empty() { + write!(f, "{} ", parent.subject.green())?; + } + + if !parent.from.is_empty() { + let left = "<".dark_grey(); + let right = ">".dark_grey(); + write!(f, "{left}{}{right}", parent.from.blue())?; + } + + let date = parent.format_date(config); + let cursor_date_begin_col = terminal::size().unwrap().0 - date.len() as u16; + + let dots = + "·".repeat((cursor_date_begin_col - cursor::position().unwrap().0 - 2) as usize); + write!(f, " {} {}", dots.dark_grey(), date.dark_yellow())?; + } + + writeln!(f)?; + + let edges_count = edges.len(); + for (i, b) in edges.into_iter().enumerate() { + let is_last = edges_count == i + 1; + let (x, y) = if is_last { + (' ', '└') + } else { + ('│', '├') + }; + + write!(f, "{pad}{y}─ ")?; + + let pad = format!("{pad}{x} "); + Self::fmt(f, config, graph, b, pad, weight + 1)?; + } + + Ok(()) + } +} + +impl fmt::Display for EnvelopesTree { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + EnvelopesTree::fmt( + f, + &self.config, + self.envelopes.graph(), + ThreadedEnvelope { + id: "0", + message_id: "0", + from: "", + subject: "", + date: Default::default(), + }, + String::new(), + 0, + ) + } +} + +impl Serialize for EnvelopesTree { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.envelopes.serialize(serializer) + } +} diff --git a/src/email/message/attachment/command/download.rs b/src/email/message/attachment/command/download.rs index 4c73edc..1187717 100644 --- a/src/email/message/attachment/command/download.rs +++ b/src/email/message/attachment/command/download.rs @@ -67,13 +67,13 @@ impl AttachmentDownloadCommand { let attachments = email.attachments()?; if attachments.is_empty() { - printer.print_log(format!("No attachment found for message {id}!"))?; + printer.log(format!("No attachment found for message {id}!"))?; continue; } else { emails_count += 1; } - printer.print_log(format!( + printer.log(format!( "{} attachment(s) found for message {id}!", attachments.len() ))?; @@ -84,7 +84,7 @@ impl AttachmentDownloadCommand { .unwrap_or_else(|| Uuid::new_v4().to_string()) .into(); let filepath = account_config.get_download_file_path(&filename)?; - printer.print_log(format!("Downloading {:?}…", filepath))?; + printer.log(format!("Downloading {:?}…", filepath))?; fs::write(&filepath, &attachment.body) .with_context(|| format!("cannot save attachment at {filepath:?}"))?; attachments_count += 1; @@ -92,9 +92,9 @@ impl AttachmentDownloadCommand { } match attachments_count { - 0 => printer.print("No attachment found!"), - 1 => printer.print("Downloaded 1 attachment!"), - n => printer.print(format!( + 0 => printer.out("No attachment found!"), + 1 => printer.out("Downloaded 1 attachment!"), + n => printer.out(format!( "Downloaded {} attachment(s) from {} messages(s)!", n, emails_count, )), diff --git a/src/email/message/command/copy.rs b/src/email/message/command/copy.rs index 171e31f..d57f553 100644 --- a/src/email/message/command/copy.rs +++ b/src/email/message/command/copy.rs @@ -60,7 +60,7 @@ impl MessageCopyCommand { backend.copy_messages(source, target, ids).await?; - printer.print(format!( + printer.out(format!( "Message(s) successfully copied from {source} to {target}!" )) } diff --git a/src/email/message/command/delete.rs b/src/email/message/command/delete.rs index 7965c5a..28b8d6f 100644 --- a/src/email/message/command/delete.rs +++ b/src/email/message/command/delete.rs @@ -58,6 +58,6 @@ impl MessageDeleteCommand { backend.delete_messages(folder, ids).await?; - printer.print(format!("Message(s) successfully removed from {folder}!")) + printer.out(format!("Message(s) successfully removed from {folder}!")) } } diff --git a/src/email/message/command/move.rs b/src/email/message/command/move.rs index 3c63ea3..c5f4772 100644 --- a/src/email/message/command/move.rs +++ b/src/email/message/command/move.rs @@ -61,7 +61,7 @@ impl MessageMoveCommand { backend.move_messages(source, target, ids).await?; - printer.print(format!( + printer.out(format!( "Message(s) successfully moved from {source} to {target}!" )) } diff --git a/src/email/message/command/read.rs b/src/email/message/command/read.rs index 1747b1f..edbc5a5 100644 --- a/src/email/message/command/read.rs +++ b/src/email/message/command/read.rs @@ -139,6 +139,6 @@ impl MessageReadCommand { glue = "\n\n"; } - printer.print(bodies) + printer.out(bodies) } } diff --git a/src/email/message/command/save.rs b/src/email/message/command/save.rs index 6bfebde..4d00af2 100644 --- a/src/email/message/command/save.rs +++ b/src/email/message/command/save.rs @@ -68,6 +68,6 @@ impl MessageSaveCommand { backend.add_message(folder, msg.as_bytes()).await?; - printer.print(format!("Message successfully saved to {folder}!")) + printer.out(format!("Message successfully saved to {folder}!")) } } diff --git a/src/email/message/command/send.rs b/src/email/message/command/send.rs index 000bfad..6ef0ea3 100644 --- a/src/email/message/command/send.rs +++ b/src/email/message/command/send.rs @@ -68,6 +68,6 @@ impl MessageSendCommand { backend.send_message_then_save_copy(msg.as_bytes()).await?; - printer.print("Message successfully sent!") + printer.out("Message successfully sent!") } } diff --git a/src/email/message/command/thread.rs b/src/email/message/command/thread.rs index c4e2379..7ba430f 100644 --- a/src/email/message/command/thread.rs +++ b/src/email/message/command/thread.rs @@ -154,6 +154,6 @@ impl MessageThreadCommand { glue = "\n\n"; } - printer.print(bodies) + printer.out(bodies) } } diff --git a/src/email/message/template/command/forward.rs b/src/email/message/template/command/forward.rs index 35ef478..4d6495f 100644 --- a/src/email/message/template/command/forward.rs +++ b/src/email/message/template/command/forward.rs @@ -76,6 +76,6 @@ impl TemplateForwardCommand { .build() .await?; - printer.print(tpl) + printer.out(tpl) } } diff --git a/src/email/message/template/command/reply.rs b/src/email/message/template/command/reply.rs index 940759a..7276186 100644 --- a/src/email/message/template/command/reply.rs +++ b/src/email/message/template/command/reply.rs @@ -81,6 +81,6 @@ impl TemplateReplyCommand { .build() .await?; - printer.print(tpl) + printer.out(tpl) } } diff --git a/src/email/message/template/command/save.rs b/src/email/message/template/command/save.rs index 4c4a718..1a91409 100644 --- a/src/email/message/template/command/save.rs +++ b/src/email/message/template/command/save.rs @@ -80,6 +80,6 @@ impl TemplateSaveCommand { backend.add_message(folder, &msg).await?; - printer.print(format!("Template successfully saved to {folder}!")) + printer.out(format!("Template successfully saved to {folder}!")) } } diff --git a/src/email/message/template/command/send.rs b/src/email/message/template/command/send.rs index 4c5a988..1220c3c 100644 --- a/src/email/message/template/command/send.rs +++ b/src/email/message/template/command/send.rs @@ -79,6 +79,6 @@ impl TemplateSendCommand { backend.send_message_then_save_copy(&msg).await?; - printer.print("Message successfully sent!") + printer.out("Message successfully sent!") } } diff --git a/src/email/message/template/command/write.rs b/src/email/message/template/command/write.rs index 8aa9c93..15536ec 100644 --- a/src/email/message/template/command/write.rs +++ b/src/email/message/template/command/write.rs @@ -47,6 +47,6 @@ impl TemplateWriteCommand { .build() .await?; - printer.print(tpl) + printer.out(tpl) } } diff --git a/src/email/message/template/mod.rs b/src/email/message/template/mod.rs index 94bf7e4..1a3a73a 100644 --- a/src/email/message/template/mod.rs +++ b/src/email/message/template/mod.rs @@ -1,14 +1,2 @@ pub mod arg; pub mod command; - -use color_eyre::Result; -use email::template::Template; - -use crate::printer::{Print, WriteColor}; - -impl Print for Template { - fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { - self.as_str().print(writer)?; - Ok(writer.reset()?) - } -} diff --git a/src/folder/command/add.rs b/src/folder/command/add.rs index bf30278..1622d84 100644 --- a/src/folder/command/add.rs +++ b/src/folder/command/add.rs @@ -50,6 +50,6 @@ impl AddFolderCommand { backend.add_folder(folder).await?; - printer.print(format!("Folder {folder} successfully created!")) + printer.log(format!("Folder {folder} successfully created!")) } } diff --git a/src/folder/command/delete.rs b/src/folder/command/delete.rs index 05aa211..3454372 100644 --- a/src/folder/command/delete.rs +++ b/src/folder/command/delete.rs @@ -60,6 +60,6 @@ impl FolderDeleteCommand { backend.delete_folder(folder).await?; - printer.print(format!("Folder {folder} successfully deleted!")) + printer.log(format!("Folder {folder} successfully deleted!")) } } diff --git a/src/folder/command/expunge.rs b/src/folder/command/expunge.rs index d28b17f..e7f3ee2 100644 --- a/src/folder/command/expunge.rs +++ b/src/folder/command/expunge.rs @@ -51,6 +51,6 @@ impl FolderExpungeCommand { backend.expunge_folder(folder).await?; - printer.print(format!("Folder {folder} successfully expunged!")) + printer.log(format!("Folder {folder} successfully expunged!")) } } diff --git a/src/folder/command/list.rs b/src/folder/command/list.rs index e80033f..3ed4391 100644 --- a/src/folder/command/list.rs +++ b/src/folder/command/list.rs @@ -6,7 +6,10 @@ use tracing::info; #[cfg(feature = "account-sync")] use crate::cache::arg::disable::CacheDisableFlag; use crate::{ - account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, folder::Folders, + account::arg::name::AccountNameFlag, + backend::Backend, + config::TomlConfig, + folder::{Folders, FoldersTable}, printer::Printer, }; @@ -51,9 +54,10 @@ impl FolderListCommand { ) .await?; - let folders: Folders = backend.list_folders().await?.into(); + let folders = Folders::from(backend.list_folders().await?); + let table = FoldersTable::from(folders).with_some_width(self.table_max_width); - printer.print_table(folders, self.table_max_width)?; + printer.log(table)?; Ok(()) } } diff --git a/src/folder/command/purge.rs b/src/folder/command/purge.rs index 68d4bb2..1db9630 100644 --- a/src/folder/command/purge.rs +++ b/src/folder/command/purge.rs @@ -60,6 +60,6 @@ impl FolderPurgeCommand { backend.purge_folder(folder).await?; - printer.print(format!("Folder {folder} successfully purged!")) + printer.log(format!("Folder {folder} successfully purged!")) } } diff --git a/src/folder/mod.rs b/src/folder/mod.rs index eaa6a50..00af7c5 100644 --- a/src/folder/mod.rs +++ b/src/folder/mod.rs @@ -2,12 +2,9 @@ pub mod arg; pub mod command; pub mod config; -use color_eyre::Result; -use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table}; -use serde::Serialize; -use std::ops; - -use crate::printer::{PrintTable, WriteColor}; +use comfy_table::{presets, Attribute, Cell, Row, Table}; +use serde::{Serialize, Serializer}; +use std::{fmt, ops::Deref}; #[derive(Clone, Debug, Default, Serialize)] pub struct Folder { @@ -15,67 +12,46 @@ pub struct Folder { pub desc: String, } -impl From<&email::folder::Folder> for Folder { - fn from(folder: &email::folder::Folder) -> Self { +impl Folder { + pub fn to_row(&self) -> Row { + let mut row = Row::new(); + + row.add_cell(Cell::new(&self.name).fg(comfy_table::Color::Blue)); + row.add_cell(Cell::new(&self.desc).fg(comfy_table::Color::Green)); + + row + } +} + +impl From for Folder { + fn from(folder: email::folder::Folder) -> Self { Folder { - name: folder.name.clone(), - desc: folder.desc.clone(), + name: folder.name, + desc: folder.desc, } } } -impl From<&Folder> for Row { - fn from(folder: &Folder) -> Self { - let mut row = Row::new(); - row.add_cell(Cell::new(&folder.name).fg(comfy_table::Color::Blue)); - row.add_cell(Cell::new(&folder.desc).fg(comfy_table::Color::Green)); - - row - } -} - -impl From for Row { - fn from(folder: Folder) -> Self { - let mut row = Row::new(); - row.add_cell(Cell::new(folder.name).fg(comfy_table::Color::Blue)); - row.add_cell(Cell::new(folder.desc).fg(comfy_table::Color::Green)); - - row - } -} #[derive(Clone, Debug, Default, Serialize)] pub struct Folders(Vec); -impl From for Table { - fn from(folders: Folders) -> Self { +impl Folders { + pub fn to_table(&self) -> Table { let mut table = Table::new(); + table .load_preset(presets::NOTHING) .set_header(Row::from([ Cell::new("NAME").add_attribute(Attribute::Reverse), Cell::new("DESC").add_attribute(Attribute::Reverse), ])) - .add_rows(folders.0.into_iter().map(Row::from)); + .add_rows(self.iter().map(Folder::to_row)); + table } } -impl From<&Folders> for Table { - fn from(folders: &Folders) -> Self { - let mut table = Table::new(); - table - .load_preset(presets::NOTHING) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(Row::from([ - Cell::new("NAME").add_attribute(Attribute::Reverse), - Cell::new("DESC").add_attribute(Attribute::Reverse), - ])) - .add_rows(folders.0.iter().map(Row::from)); - table - } -} - -impl ops::Deref for Folders { +impl Deref for Folders { type Target = Vec; fn deref(&self) -> &Self::Target { @@ -85,19 +61,51 @@ impl ops::Deref for Folders { impl From for Folders { fn from(folders: email::folder::Folders) -> Self { - Folders(folders.iter().map(Folder::from).collect()) + Folders(folders.into_iter().map(Folder::from).collect()) } } -impl PrintTable for Folders { - fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option) -> Result<()> { - let mut table = Table::from(self); - if let Some(width) = table_max_width { +pub struct FoldersTable { + folders: Folders, + width: Option, +} + +impl FoldersTable { + pub fn with_some_width(mut self, width: Option) -> Self { + self.width = width; + self + } +} + +impl From for FoldersTable { + fn from(folders: Folders) -> Self { + Self { + folders, + width: None, + } + } +} + +impl fmt::Display for FoldersTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = self.folders.to_table(); + + if let Some(width) = self.width { table.set_width(width); } - writeln!(writer)?; - write!(writer, "{}", table)?; - writeln!(writer)?; + + writeln!(f)?; + write!(f, "{table}")?; + writeln!(f)?; Ok(()) } } + +impl Serialize for FoldersTable { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.folders.serialize(serializer) + } +} diff --git a/src/main.rs b/src/main.rs index e14db80..871d8d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ async fn main() -> Result<()> { } let cli = Cli::parse(); - let mut printer = StdoutPrinter::new(cli.output, cli.color); + let mut printer = StdoutPrinter::new(cli.output); let mut res = match cli.command { Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await, None => { diff --git a/src/manual/command.rs b/src/manual/command.rs index bcbafba..ba69222 100644 --- a/src/manual/command.rs +++ b/src/manual/command.rs @@ -33,7 +33,7 @@ impl ManualGenerateCommand { Man::new(cmd).render(&mut buffer)?; fs::create_dir_all(&self.dir)?; - printer.print_log(format!("Generating man page for command {cmd_name}…"))?; + printer.log(format!("Generating man page for command {cmd_name}…"))?; fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?; for subcmd in subcmds { @@ -42,14 +42,14 @@ impl ManualGenerateCommand { let mut buffer = Vec::new(); Man::new(subcmd).render(&mut buffer)?; - printer.print_log(format!("Generating man page for subcommand {subcmd_name}…"))?; + printer.log(format!("Generating man page for subcommand {subcmd_name}…"))?; fs::write( self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)), buffer, )?; } - printer.print(format!( + printer.log(format!( "{subcmds_len} man page(s) successfully generated in {:?}!", self.dir ))?; diff --git a/src/output/mod.rs b/src/output/mod.rs index f7241bd..a1ef5bc 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -1,4 +1,3 @@ -pub mod args; #[allow(clippy::module_inception)] pub mod output; diff --git a/src/output/output.rs b/src/output/output.rs index 3ff1383..11fa63c 100644 --- a/src/output/output.rs +++ b/src/output/output.rs @@ -1,12 +1,7 @@ use clap::ValueEnum; use color_eyre::{eyre::eyre, eyre::Error, Result}; use serde::Serialize; -use std::{ - fmt, - io::{self, IsTerminal}, - str::FromStr, -}; -use termcolor::ColorChoice; +use std::{fmt, str::FromStr}; /// Represents the available output formats. #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] @@ -49,59 +44,3 @@ impl OutputJson { Self { response } } } - -/// Represent the available color configs. -#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, ValueEnum)] -pub enum ColorFmt { - Never, - Always, - Ansi, - #[default] - Auto, -} - -impl FromStr for ColorFmt { - type Err = Error; - - fn from_str(fmt: &str) -> Result { - match fmt { - fmt if fmt.eq_ignore_ascii_case("never") => Ok(Self::Never), - fmt if fmt.eq_ignore_ascii_case("always") => Ok(Self::Always), - fmt if fmt.eq_ignore_ascii_case("ansi") => Ok(Self::Ansi), - fmt if fmt.eq_ignore_ascii_case("auto") => Ok(Self::Auto), - unknown => Err(eyre!("cannot parse color format {}", unknown)), - } - } -} - -impl From for ColorChoice { - fn from(fmt: ColorFmt) -> Self { - match fmt { - ColorFmt::Never => Self::Never, - ColorFmt::Always => Self::Always, - ColorFmt::Ansi => Self::AlwaysAnsi, - ColorFmt::Auto => { - if io::stdout().is_terminal() { - // Otherwise let's `termcolor` decide by - // inspecting the environment. From the [doc]: - // - // * If `NO_COLOR` is set to any value, then - // colors will be suppressed. - // - // * If `TERM` is set to dumb, then colors will be - // suppressed. - // - // * In non-Windows environments, if `TERM` is not - // set, then colors will be suppressed. - // - // [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection - Self::Auto - } else { - // Colors should be deactivated if the terminal is - // not a tty. - Self::Never - } - } - } - } -} diff --git a/src/printer.rs b/src/printer.rs new file mode 100644 index 0000000..e895286 --- /dev/null +++ b/src/printer.rs @@ -0,0 +1,73 @@ +use color_eyre::{eyre::Context, Result}; +use std::{ + fmt, + io::{self, Write}, +}; + +use crate::output::OutputFmt; + +pub trait PrintTable { + fn print(&self, writer: &mut dyn io::Write, table_max_width: Option) -> Result<()>; +} + +pub trait Printer { + fn out(&mut self, data: T) -> Result<()>; + + fn log(&mut self, data: T) -> Result<()> { + self.out(data) + } + + fn is_json(&self) -> bool { + false + } +} + +pub struct StdoutPrinter { + stdout: io::Stdout, + stderr: io::Stderr, + output: OutputFmt, +} + +impl StdoutPrinter { + pub fn new(output: OutputFmt) -> Self { + Self { + stdout: io::stdout(), + stderr: io::stderr(), + output, + } + } +} + +impl Default for StdoutPrinter { + fn default() -> Self { + Self::new(Default::default()) + } +} + +impl Printer for StdoutPrinter { + fn out(&mut self, data: T) -> Result<()> { + match self.output { + OutputFmt::Plain => { + write!(self.stdout, "{data}")?; + } + OutputFmt::Json => { + serde_json::to_writer(&mut self.stdout, &data) + .context("cannot write json to writer")?; + } + }; + + Ok(()) + } + + fn log(&mut self, data: T) -> Result<()> { + if let OutputFmt::Plain = self.output { + write!(&mut self.stderr, "{data}")?; + } + + Ok(()) + } + + fn is_json(&self) -> bool { + self.output == OutputFmt::Json + } +} diff --git a/src/printer/mod.rs b/src/printer/mod.rs deleted file mode 100644 index 2ab3840..0000000 --- a/src/printer/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod print; -#[allow(clippy::module_inception)] -pub mod printer; - -use std::io; - -pub use print::*; -pub use printer::*; -use termcolor::StandardStream; - -pub trait WriteColor: io::Write + termcolor::WriteColor {} - -impl WriteColor for StandardStream {} diff --git a/src/printer/print.rs b/src/printer/print.rs deleted file mode 100644 index 81360f2..0000000 --- a/src/printer/print.rs +++ /dev/null @@ -1,21 +0,0 @@ -use color_eyre::{eyre::Context, Result}; - -use crate::printer::WriteColor; - -pub trait Print { - fn print(&self, writer: &mut dyn WriteColor) -> Result<()>; -} - -impl Print for &str { - fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { - writeln!(writer, "{}", self).context("cannot write string to writer")?; - Ok(writer.reset()?) - } -} - -impl Print for String { - fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { - self.as_str().print(writer)?; - Ok(writer.reset()?) - } -} diff --git a/src/printer/printer.rs b/src/printer/printer.rs deleted file mode 100644 index e914ebf..0000000 --- a/src/printer/printer.rs +++ /dev/null @@ -1,102 +0,0 @@ -use clap::ArgMatches; -use color_eyre::{eyre::Context, Report, Result}; -use std::fmt::Debug; -use termcolor::StandardStream; - -use crate::{ - output::{args, ColorFmt, OutputFmt}, - printer::{Print, WriteColor}, -}; -pub trait PrintTable { - fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option) -> Result<()>; -} - -pub trait Printer { - // TODO: rename end - fn print(&mut self, data: T) -> Result<()>; - // TODO: rename log - fn print_log(&mut self, data: T) -> Result<()>; - // TODO: rename table - fn print_table( - &mut self, - data: T, - table_max_width: Option, - ) -> Result<()>; - - fn is_json(&self) -> bool; -} - -pub struct StdoutPrinter { - pub writer: Box, - pub fmt: OutputFmt, -} - -impl Default for StdoutPrinter { - fn default() -> Self { - let fmt = OutputFmt::default(); - let writer = Box::new(StandardStream::stdout(ColorFmt::default().into())); - Self { fmt, writer } - } -} - -impl StdoutPrinter { - pub fn new(fmt: OutputFmt, color: ColorFmt) -> Self { - let writer = Box::new(StandardStream::stdout(color.into())); - Self { fmt, writer } - } -} - -impl Printer for StdoutPrinter { - fn print_log(&mut self, data: T) -> Result<()> { - match self.fmt { - OutputFmt::Plain => data.print(self.writer.as_mut()), - OutputFmt::Json => Ok(()), - } - } - - fn print(&mut self, data: T) -> Result<()> { - match self.fmt { - OutputFmt::Plain => data.print(self.writer.as_mut()), - OutputFmt::Json => serde_json::to_writer(self.writer.as_mut(), &data) - .context("cannot write json to writer"), - } - } - - fn is_json(&self) -> bool { - self.fmt == OutputFmt::Json - } - - fn print_table( - &mut self, - data: T, - table_max_width: Option, - ) -> Result<()> { - data.print_table(self.writer.as_mut(), table_max_width) - } -} - -impl From for StdoutPrinter { - fn from(fmt: OutputFmt) -> Self { - Self::new(fmt, ColorFmt::Auto) - } -} - -impl TryFrom<&ArgMatches> for StdoutPrinter { - type Error = Report; - - fn try_from(m: &ArgMatches) -> Result { - let fmt: OutputFmt = m - .get_one::(args::ARG_OUTPUT) - .map(String::as_str) - .unwrap() - .parse()?; - - let color: ColorFmt = m - .get_one::(args::ARG_COLOR) - .map(String::as_str) - .unwrap() - .parse()?; - - Ok(Self::new(fmt, color)) - } -} diff --git a/src/ui/editor.rs b/src/ui/editor.rs index e8a3164..91b1419 100644 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -80,7 +80,7 @@ pub async fn edit_tpl_with_editor( loop { match choice::post_edit() { Ok(PostEditChoice::Send) => { - printer.print_log("Sending email…")?; + printer.log("Sending email…")?; #[allow(unused_mut)] let mut compiler = MmlCompilerBuilder::new(); @@ -93,7 +93,7 @@ pub async fn edit_tpl_with_editor( backend.send_message_then_save_copy(&email).await?; remove_local_draft()?; - printer.print("Done!")?; + printer.log("Done!")?; break; } Ok(PostEditChoice::Edit) => { @@ -101,7 +101,7 @@ pub async fn edit_tpl_with_editor( continue; } Ok(PostEditChoice::LocalDraft) => { - printer.print("Email successfully saved locally")?; + printer.log("Email successfully saved locally")?; break; } Ok(PostEditChoice::RemoteDraft) => { @@ -121,7 +121,7 @@ pub async fn edit_tpl_with_editor( ) .await?; remove_local_draft()?; - printer.print("Email successfully saved to drafts")?; + printer.log("Email successfully saved to drafts")?; break; } Ok(PostEditChoice::Discard) => { From 16d273febc59d510f3e44a69e213abe0f37a7793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 26 May 2024 09:30:05 +0200 Subject: [PATCH 6/8] wip: fix thread id mapping --- src/backend/mod.rs | 15 ++++--- src/email/envelope/mod.rs | 94 +++++++++++++++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 14 deletions(-) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 56f712c..9fe88c4 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -25,7 +25,7 @@ use email::{ list::{ListEnvelopes, ListEnvelopesOptions}, thread::ThreadEnvelopes, watch::WatchEnvelopes, - Id, SingleId, ThreadedEnvelopes, + Id, SingleId, }, flag::{add::AddFlags, remove::RemoveFlags, set::SetFlags, Flag, Flags}, folder::{ @@ -46,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")] @@ -713,8 +717,7 @@ impl Backend { 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 = - // Envelopes::from_backend(&self.backend.account_config, &id_mapper, envelopes)?; + let envelopes = ThreadedEnvelopes::try_from_backend(&id_mapper, envelopes)?; Ok(envelopes) } @@ -726,12 +729,12 @@ impl Backend { ) -> Result { 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 = - // Envelopes::from_backend(&self.backend.account_config, &id_mapper, envelopes)?; + let envelopes = ThreadedEnvelopes::try_from_backend(&id_mapper, envelopes)?; Ok(envelopes) } diff --git a/src/email/envelope/mod.rs b/src/email/envelope/mod.rs index e4ac959..1b0bbeb 100644 --- a/src/email/envelope/mod.rs +++ b/src/email/envelope/mod.rs @@ -6,13 +6,10 @@ pub mod flag; use color_eyre::Result; use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table}; use crossterm::{cursor, style::Stylize, terminal}; -use email::{ - account::config::AccountConfig, - envelope::{ThreadedEnvelope, ThreadedEnvelopes}, -}; +use email::{account::config::AccountConfig, envelope::ThreadedEnvelope}; use petgraph::graphmap::DiGraphMap; use serde::{Serialize, Serializer}; -use std::{fmt, ops::Deref, sync::Arc}; +use std::{collections::HashMap, fmt, ops::Deref, sync::Arc}; use crate::{ cache::IdMapper, @@ -156,7 +153,6 @@ impl From<&Envelope> for Row { } } -/// Represents the list of envelopes. #[derive(Clone, Debug, Default, Serialize)] pub struct Envelopes(Vec); @@ -261,6 +257,80 @@ impl Serialize for EnvelopesTable { } } +pub struct ThreadedEnvelopes(email::envelope::ThreadedEnvelopes); + +impl ThreadedEnvelopes { + pub fn try_from_backend( + id_mapper: &IdMapper, + envelopes: email::envelope::ThreadedEnvelopes, + ) -> Result { + 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::>>()?; + + 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::>>()?; + + let envelopes = email::envelope::ThreadedEnvelopes::build(envelopes, move |envelopes| { + let mut graph = DiGraphMap::::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, envelopes: ThreadedEnvelopes, @@ -339,7 +409,7 @@ impl fmt::Display for EnvelopesTree { EnvelopesTree::fmt( f, &self.config, - self.envelopes.graph(), + self.envelopes.0.graph(), ThreadedEnvelope { id: "0", message_id: "0", @@ -358,6 +428,14 @@ impl Serialize for EnvelopesTree { where S: Serializer, { - self.envelopes.serialize(serializer) + self.envelopes.0.serialize(serializer) + } +} + +impl Deref for EnvelopesTree { + type Target = ThreadedEnvelopes; + + fn deref(&self) -> &Self::Target { + &self.envelopes } } From ec3f9159227063f034daa8fadfe32bfaeff74247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 26 May 2024 13:36:25 +0200 Subject: [PATCH 7/8] bump email-lib and imap-flow suite --- Cargo.lock | 24 ++++++++++-------------- Cargo.toml | 9 +++------ 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9eb0bcf..e3e0463 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1399,6 +1399,7 @@ dependencies = [ [[package]] name = "email-lib" version = "0.24.1" +source = "git+https://git.sr.ht/~soywod/pimalaya?branch=thread#bbb579312d7f10dd3359cc5068b6eb7d5d7fee50" dependencies = [ "advisory-lock", "async-ctrlc", @@ -2187,7 +2188,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.6", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -2221,7 +2222,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core", ] [[package]] @@ -2271,6 +2272,7 @@ dependencies = [ [[package]] name = "imap-client" version = "0.1.0" +source = "git+https://github.com/soywod/imap-flow.git?branch=session#7d60dd27829b82602e7c7f794633aef432f4c191" dependencies = [ "imap-flow", "once_cell", @@ -2285,7 +2287,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", @@ -2300,6 +2302,7 @@ dependencies = [ [[package]] name = "imap-flow" version = "0.1.0" +source = "git+https://github.com/soywod/imap-flow.git?branch=session#7d60dd27829b82602e7c7f794633aef432f4c191" dependencies = [ "bounded-static", "bytes", @@ -2315,7 +2318,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", @@ -4579,6 +4582,7 @@ dependencies = [ [[package]] name = "tag-generator" version = "0.1.0" +source = "git+https://github.com/soywod/imap-flow.git?branch=session#7d60dd27829b82602e7c7f794633aef432f4c191" dependencies = [ "imap-types", "rand", @@ -4593,6 +4597,7 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tasks" version = "0.1.0" +source = "git+https://github.com/soywod/imap-flow.git?branch=session#7d60dd27829b82602e7c7f794633aef432f4c191" dependencies = [ "imap-flow", "imap-types", @@ -5235,7 +5240,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", ] @@ -5248,15 +5253,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" diff --git a/Cargo.toml b/Cargo.toml index 9c9c5eb..4b70b34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,11 +87,8 @@ uuid = { version = "0.8", features = ["v4"] } [patch.crates-io] # WIP: transition from `imap` to `imap-codec` -email-lib = { path = "/home/soywod/sourcehut/pimalaya/email" } -imap-client = { path = "/home/soywod/code/imap-flow/client" } -tasks = { path = "/home/soywod/code/imap-flow/tasks" } -# 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" } +email-lib = { git = "https://git.sr.ht/~soywod/pimalaya", branch = "thread" } +imap-client = { git = "https://github.com/soywod/imap-flow.git", branch = "session" } +tasks = { git = "https://github.com/soywod/imap-flow.git", branch = "session" } imap-codec = { git = "https://github.com/duesee/imap-codec.git" } imap-types = { git = "https://github.com/duesee/imap-codec.git" } From c6cf93a27611bf2d85e12eec43bd69d975939fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 29 May 2024 10:55:21 +0200 Subject: [PATCH 8/8] bump imap libs --- Cargo.lock | 23 ++++++----------------- Cargo.toml | 7 +++---- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3e0463..9ff77ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1399,7 +1399,7 @@ dependencies = [ [[package]] name = "email-lib" version = "0.24.1" -source = "git+https://git.sr.ht/~soywod/pimalaya?branch=thread#bbb579312d7f10dd3359cc5068b6eb7d5d7fee50" +source = "git+https://git.sr.ht/~soywod/pimalaya#a28e746a634c066f4b9b0b15cd6f742fa530164d" dependencies = [ "advisory-lock", "async-ctrlc", @@ -2272,12 +2272,13 @@ dependencies = [ [[package]] name = "imap-client" version = "0.1.0" -source = "git+https://github.com/soywod/imap-flow.git?branch=session#7d60dd27829b82602e7c7f794633aef432f4c191" +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", @@ -2302,7 +2303,7 @@ dependencies = [ [[package]] name = "imap-flow" version = "0.1.0" -source = "git+https://github.com/soywod/imap-flow.git?branch=session#7d60dd27829b82602e7c7f794633aef432f4c191" +source = "git+https://github.com/soywod/imap-flow?branch=into-inner-stream#b705adbc03976367330f2b24e99a9623e5da3733" dependencies = [ "bounded-static", "bytes", @@ -4582,7 +4583,7 @@ dependencies = [ [[package]] name = "tag-generator" version = "0.1.0" -source = "git+https://github.com/soywod/imap-flow.git?branch=session#7d60dd27829b82602e7c7f794633aef432f4c191" +source = "git+https://github.com/duesee/imap-flow#9ffda2b321247896b3f452072ccfd38789bb547a" dependencies = [ "imap-types", "rand", @@ -4594,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#7d60dd27829b82602e7c7f794633aef432f4c191" -dependencies = [ - "imap-flow", - "imap-types", - "tag-generator", - "thiserror", - "tracing", -] - [[package]] name = "tauri-winrt-notification" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 4b70b34..f37ddea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,9 +86,8 @@ url = "2.2" uuid = { version = "0.8", features = ["v4"] } [patch.crates-io] -# WIP: transition from `imap` to `imap-codec` -email-lib = { git = "https://git.sr.ht/~soywod/pimalaya", branch = "thread" } -imap-client = { git = "https://github.com/soywod/imap-flow.git", branch = "session" } -tasks = { git = "https://github.com/soywod/imap-flow.git", branch = "session" } +# 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-client.git" } imap-codec = { git = "https://github.com/duesee/imap-codec.git" } imap-types = { git = "https://github.com/duesee/imap-codec.git" }