mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-21 18:40:19 +00:00
wip: design basic tree using petgraph
This commit is contained in:
parent
7a951b4830
commit
90e12ddc51
8 changed files with 410 additions and 10 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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",
|
||||
|
|
10
Cargo.toml
10
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" }
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
330
src/email/envelope/command/thread.rs
Normal file
330
src/email/envelope/command/thread.rs
Normal file
|
@ -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>,
|
||||
|
|
Loading…
Reference in a new issue