mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-17 05:07:55 +08:00
wip: design basic tree using petgraph
This commit is contained in:
@@ -142,6 +142,14 @@ impl TomlAccountConfig {
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn thread_envelopes_kind(&self) -> Option<&BackendKind> {
|
||||
self.envelope
|
||||
.as_ref()
|
||||
.and_then(|envelope| envelope.thread.as_ref())
|
||||
.and_then(|thread| thread.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn watch_envelopes_kind(&self) -> Option<&BackendKind> {
|
||||
self.envelope
|
||||
.as_ref()
|
||||
|
||||
@@ -3,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<BackendFeature<Self::Context, dyn ThreadEnvelopes>> {
|
||||
match self.toml_account_config.thread_envelopes_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.thread_envelopes_with_some(&self.imap),
|
||||
#[cfg(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.thread_envelopes()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.thread_envelopes_with_some(&self.maildir),
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.thread_envelopes_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn watch_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn WatchEnvelopes>> {
|
||||
match self.toml_account_config.watch_envelopes_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
@@ -687,6 +706,19 @@ impl Backend {
|
||||
Ok(envelopes)
|
||||
}
|
||||
|
||||
pub async fn thread_envelopes(
|
||||
&self,
|
||||
folder: &str,
|
||||
opts: ListEnvelopesOptions,
|
||||
) -> Result<DiGraphMap<u32, u32>> {
|
||||
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)?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<usize>,
|
||||
|
||||
#[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<u16>,
|
||||
|
||||
/// 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 <condition> → filter envelopes that do not match the
|
||||
/// condition
|
||||
///
|
||||
/// • <condition> and <condition> → filter envelopes that match
|
||||
/// both conditions
|
||||
///
|
||||
/// • <condition> or <condition> → filter envelopes that match
|
||||
/// one of the conditions
|
||||
///
|
||||
/// ◦ date <yyyy-mm-dd> → filter envelopes that match the given
|
||||
/// date
|
||||
///
|
||||
/// ◦ before <yyyy-mm-dd> → filter envelopes with date strictly
|
||||
/// before the given one
|
||||
///
|
||||
/// ◦ after <yyyy-mm-dd> → filter envelopes with date stricly
|
||||
/// after the given one
|
||||
///
|
||||
/// ◦ from <pattern> → filter envelopes with senders matching the
|
||||
/// given pattern
|
||||
///
|
||||
/// ◦ to <pattern> → filter envelopes with recipients matching
|
||||
/// the given pattern
|
||||
///
|
||||
/// ◦ subject <pattern> → filter envelopes with subject matching
|
||||
/// the given pattern
|
||||
///
|
||||
/// ◦ body <pattern> → filter envelopes with text bodies matching
|
||||
/// the given pattern
|
||||
///
|
||||
/// ◦ flag <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
|
||||
///
|
||||
/// ◦ <kind> asc → sort envelopes by the given kind in ascending
|
||||
/// order
|
||||
///
|
||||
/// ◦ <kind> 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<Vec<String>>,
|
||||
}
|
||||
|
||||
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::<SearchEmailsQuery>());
|
||||
// let query = match query {
|
||||
// None => None,
|
||||
// Some(Ok(query)) => Some(query),
|
||||
// Some(Err(main_err)) => {
|
||||
// let source = "query";
|
||||
// let search_query::error::Error::ParseError(errs, query) = &main_err;
|
||||
// for err in errs {
|
||||
// Report::build(ReportKind::Error, source, err.span().start)
|
||||
// .with_message(main_err.to_string())
|
||||
// .with_label(
|
||||
// Label::new((source, err.span().into_range()))
|
||||
// .with_message(err.reason().to_string())
|
||||
// .with_color(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<u32, u32>,
|
||||
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::<Vec<_>>();
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use crate::backend::BackendKind;
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct EnvelopeConfig {
|
||||
pub list: Option<ListEnvelopesConfig>,
|
||||
pub thread: Option<ThreadEnvelopesConfig>,
|
||||
pub watch: Option<WatchEnvelopesConfig>,
|
||||
pub get: Option<GetEnvelopeConfig>,
|
||||
#[cfg(feature = "account-sync")]
|
||||
@@ -54,6 +55,26 @@ impl ListEnvelopesConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ThreadEnvelopesConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub remote: email::envelope::thread::config::EnvelopeThreadConfig,
|
||||
}
|
||||
|
||||
impl ThreadEnvelopesConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct WatchEnvelopesConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
|
||||
Reference in New Issue
Block a user