improve global options, add account config sync-folders-strategy

This commit is contained in:
Clément DOUIN 2023-02-22 13:14:21 +01:00
parent 22fb1b8dee
commit fb324878fa
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
13 changed files with 194 additions and 43 deletions

View file

@ -12,6 +12,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `create` and `delete` folder commands [sourcehut#54]. - Added `create` and `delete` folder commands [sourcehut#54].
- Added generated completions and man pages to releases - Added generated completions and man pages to releases
[sourcehut#43]. [sourcehut#43].
- Added new account config option `sync-folders-strategy` which allows
to choose a folders synchronization strategy [sourcehut#59]:
- `sync-folders-strategy = "all"`: synchronize all existing folders
for the current account
- `sync-folders-strategy.include = ["folder1", "folder2", …]`:
synchronize only the given folders for the current account
- `sync-folders-strategy.exclude = ["folder1", "folder2", …]`:
synchronizes all folders except the given ones for the current
account
Also added new `account sync` arguments that override the account
config option:
- `-A|--all-folders`: include all folders to the synchronization.
- `-F|--include-folder`: include given folders to the
synchronization. They can be repeated `-F folder1 folder2` or `-F
folder1 -F folder2`.
- `-x|--exclude-folder`: exclude given folders from the
synchronization. They can be repeated `-x folder1 folder2` or `-x
folder1 -F folder2`.
### Changed
- Made global options truly global, which means they can be used
everywhere (not only *before* commands but also *after*)
[sourcehut#60].
### Fixed ### Fixed
@ -679,3 +706,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[sourcehut#43]: https://todo.sr.ht/~soywod/pimalaya/43 [sourcehut#43]: https://todo.sr.ht/~soywod/pimalaya/43
[sourcehut#54]: https://todo.sr.ht/~soywod/pimalaya/54 [sourcehut#54]: https://todo.sr.ht/~soywod/pimalaya/54
[sourcehut#59]: https://todo.sr.ht/~soywod/pimalaya/59
[sourcehut#60]: https://todo.sr.ht/~soywod/pimalaya/60

2
Cargo.lock generated
View file

@ -804,7 +804,7 @@ dependencies = [
[[package]] [[package]]
name = "himalaya-lib" name = "himalaya-lib"
version = "0.6.0" version = "0.6.0"
source = "git+https://git.sr.ht/~soywod/himalaya-lib?branch=develop#26d3b9e74c978f6dc94689bff809d48fe5ba4c85" source = "git+https://git.sr.ht/~soywod/himalaya-lib?branch=develop#9617be67bcec1f1a80924288be34d3476a260890"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"chrono", "chrono",

View file

@ -11,7 +11,7 @@ email-writing-sign-cmd = "gpg -o - -saq"
email-writing-encrypt-cmd = "gpg -o - -eqar <recipient>" email-writing-encrypt-cmd = "gpg -o - -eqar <recipient>"
[example] [example]
default = false default = true
display-name = "Display NAME (gmail)" display-name = "Display NAME (gmail)"
email = "display.name@gmail.local" email = "display.name@gmail.local"
@ -36,6 +36,7 @@ smtp-starttls = false
sync = true sync = true
sync-dir = "/tmp/sync/gmail" sync-dir = "/tmp/sync/gmail"
sync-folders-strategy.only = ["INBOX"]
[example.folder-aliases] [example.folder-aliases]
inbox = "INBOX" inbox = "INBOX"

7
src/cache/args.rs vendored
View file

@ -8,8 +8,13 @@ const ARG_DISABLE_CACHE: &str = "disable-cache";
/// the user to disable any sort of cache. /// the user to disable any sort of cache.
pub fn arg() -> Arg { pub fn arg() -> Arg {
Arg::new(ARG_DISABLE_CACHE) Arg::new(ARG_DISABLE_CACHE)
.long("disable-cache")
.help("Disable any sort of cache") .help("Disable any sort of cache")
.long_help(
"Disable any sort of cache. The action depends on
the command it applies on.",
)
.long("disable-cache")
.global(true)
.action(ArgAction::SetTrue) .action(ArgAction::SetTrue)
} }

View file

@ -8,9 +8,10 @@ const ARG_CONFIG: &str = "config";
/// user to customize the config file path. /// user to customize the config file path.
pub fn arg() -> Arg { pub fn arg() -> Arg {
Arg::new(ARG_CONFIG) Arg::new(ARG_CONFIG)
.help("Set a custom configuration file path")
.long("config") .long("config")
.short('c') .short('c')
.help("Forces a specific config file path") .global(true)
.value_name("PATH") .value_name("PATH")
} }

View file

@ -1,9 +1,9 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use himalaya_lib::{ use himalaya_lib::{
EmailHooks, EmailSender, EmailTextPlainFormat, MaildirConfig, SendmailConfig, SmtpConfig, folder::sync::Strategy as SyncFoldersStrategy, EmailHooks, EmailSender, EmailTextPlainFormat,
MaildirConfig, SendmailConfig, SmtpConfig,
}; };
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, path::PathBuf};
#[cfg(feature = "imap-backend")] #[cfg(feature = "imap-backend")]
use himalaya_lib::ImapConfig; use himalaya_lib::ImapConfig;
@ -111,3 +111,15 @@ pub struct EmailHooksDef {
/// Represents the hook called just before sending an email. /// Represents the hook called just before sending an email.
pub pre_send: Option<String>, pub pre_send: Option<String>,
} }
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "SyncFoldersStrategy", rename_all = "kebab-case")]
pub enum SyncFoldersStrategyDef {
#[default]
All,
#[serde(alias = "only")]
Include(HashSet<String>),
#[serde(alias = "except")]
#[serde(alias = "ignore")]
Exclude(HashSet<String>),
}

View file

@ -2,9 +2,11 @@
use anyhow::Result; use anyhow::Result;
use clap::{Arg, ArgAction, ArgMatches, Command}; use clap::{Arg, ArgAction, ArgMatches, Command};
use himalaya_lib::folder::sync::Strategy as SyncFoldersStrategy;
use log::info; use log::info;
use std::collections::HashSet;
use crate::ui::table; use crate::{folder, ui::table};
const ARG_ACCOUNT: &str = "account"; const ARG_ACCOUNT: &str = "account";
const ARG_DRY_RUN: &str = "dry-run"; const ARG_DRY_RUN: &str = "dry-run";
@ -20,7 +22,7 @@ pub enum Cmd {
/// Represents the list accounts command. /// Represents the list accounts command.
List(table::args::MaxTableWidth), List(table::args::MaxTableWidth),
/// Represents the sync account command. /// Represents the sync account command.
Sync(DryRun), Sync(Option<SyncFoldersStrategy>, DryRun),
} }
/// Represents the account command matcher. /// Represents the account command matcher.
@ -29,7 +31,22 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
if let Some(m) = m.subcommand_matches(CMD_SYNC) { if let Some(m) = m.subcommand_matches(CMD_SYNC) {
info!("sync account subcommand matched"); info!("sync account subcommand matched");
let dry_run = parse_dry_run_arg(m); let dry_run = parse_dry_run_arg(m);
Some(Cmd::Sync(dry_run)) let include = folder::args::parse_include_arg(m);
let exclude = folder::args::parse_exclude_arg(m);
let folders_strategy = if let Some(folder) = folder::args::parse_source_arg(m) {
Some(SyncFoldersStrategy::Include(HashSet::from_iter([
folder.to_owned()
])))
} else if !include.is_empty() {
Some(SyncFoldersStrategy::Include(include.to_owned()))
} else if !exclude.is_empty() {
Some(SyncFoldersStrategy::Exclude(exclude))
} else if folder::args::parse_all_arg(m) {
Some(SyncFoldersStrategy::All)
} else {
None
};
Some(Cmd::Sync(folders_strategy, dry_run))
} else if let Some(m) = m.subcommand_matches(CMD_LIST) { } else if let Some(m) = m.subcommand_matches(CMD_LIST) {
info!("list accounts subcommand matched"); info!("list accounts subcommand matched");
let max_table_width = table::args::parse_max_width(m); let max_table_width = table::args::parse_max_width(m);
@ -55,6 +72,13 @@ pub fn subcmd() -> Command {
.arg(table::args::max_width()), .arg(table::args::max_width()),
Command::new(CMD_SYNC) Command::new(CMD_SYNC)
.about("Synchronize the given account locally") .about("Synchronize the given account locally")
.arg(folder::args::all_arg("Synchronize all folders"))
.arg(folder::args::include_arg(
"Synchronize only the given folders",
))
.arg(folder::args::exclude_arg(
"Synchronize all folders except the given ones",
))
.arg(dry_run()), .arg(dry_run()),
]) ])
} }
@ -63,9 +87,10 @@ pub fn subcmd() -> Command {
/// the user to select a different account than the default one. /// the user to select a different account than the default one.
pub fn arg() -> Arg { pub fn arg() -> Arg {
Arg::new(ARG_ACCOUNT) Arg::new(ARG_ACCOUNT)
.help("Set the account")
.long("account") .long("account")
.short('a') .short('a')
.help("Select a specific account by name") .global(true)
.value_name("STRING") .value_name("STRING")
} }

View file

@ -4,8 +4,11 @@
//! account in the accounts section of the user configuration file. //! account in the accounts section of the user configuration file.
use himalaya_lib::{ use himalaya_lib::{
AccountConfig, BackendConfig, EmailHooks, EmailSender, EmailTextPlainFormat, MaildirConfig, folder::sync::Strategy as SyncFoldersStrategy, AccountConfig, BackendConfig, EmailHooks,
EmailSender, EmailTextPlainFormat, MaildirConfig,
}; };
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf};
#[cfg(feature = "imap-backend")] #[cfg(feature = "imap-backend")]
use himalaya_lib::ImapConfig; use himalaya_lib::ImapConfig;
@ -13,9 +16,6 @@ use himalaya_lib::ImapConfig;
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
use himalaya_lib::NotmuchConfig; use himalaya_lib::NotmuchConfig;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf};
use crate::config::{prelude::*, DeserializedConfig}; use crate::config::{prelude::*, DeserializedConfig};
/// Represents all existing kind of account config. /// Represents all existing kind of account config.
@ -104,6 +104,8 @@ pub struct DeserializedBaseAccountConfig {
#[serde(default)] #[serde(default)]
pub sync: bool, pub sync: bool,
pub sync_dir: Option<PathBuf>, pub sync_dir: Option<PathBuf>,
#[serde(default, with = "SyncFoldersStrategyDef")]
pub sync_folders_strategy: SyncFoldersStrategy,
} }
impl DeserializedBaseAccountConfig { impl DeserializedBaseAccountConfig {
@ -207,6 +209,7 @@ impl DeserializedBaseAccountConfig {
}, },
sync: self.sync, sync: self.sync,
sync_dir: self.sync_dir.clone(), sync_dir: self.sync_dir.clone(),
sync_folders_strategy: self.sync_folders_strategy.clone(),
} }
} }
} }

View file

@ -3,7 +3,10 @@
//! This module gathers all account actions triggered by the CLI. //! This module gathers all account actions triggered by the CLI.
use anyhow::Result; use anyhow::Result;
use himalaya_lib::{AccountConfig, Backend, BackendSyncBuilder, BackendSyncProgressEvent}; use himalaya_lib::{
folder::sync::Strategy as SyncFoldersStrategy, AccountConfig, Backend, BackendSyncBuilder,
BackendSyncProgressEvent,
};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use log::{info, trace}; use log::{info, trace};
@ -43,15 +46,17 @@ pub fn sync<P: Printer>(
account_config: &AccountConfig, account_config: &AccountConfig,
printer: &mut P, printer: &mut P,
backend: &dyn Backend, backend: &dyn Backend,
folder: &Option<String>, folders_strategy: Option<SyncFoldersStrategy>,
dry_run: bool, dry_run: bool,
) -> Result<()> { ) -> Result<()> {
info!("entering the sync accounts handler"); info!("entering the sync accounts handler");
trace!("dry run: {}", dry_run); trace!("dry run: {dry_run}");
trace!("folders strategy: {folders_strategy:#?}");
let mut sync_builder = BackendSyncBuilder::new(account_config); let mut sync_builder = BackendSyncBuilder::new(account_config);
if let Some(folder) = folder {
sync_builder = sync_builder.only_folder(folder); if let Some(strategy) = folders_strategy {
sync_builder = sync_builder.folders_strategy(strategy);
} }
if dry_run { if dry_run {

View file

@ -3,12 +3,17 @@
//! This module provides subcommands, arguments and a command matcher //! This module provides subcommands, arguments and a command matcher
//! related to the folder domain. //! related to the folder domain.
use std::collections::HashSet;
use anyhow::Result; use anyhow::Result;
use clap::{self, Arg, ArgMatches, Command}; use clap::{self, Arg, ArgAction, ArgMatches, Command};
use log::{debug, info}; use log::{debug, info};
use crate::ui::table; use crate::ui::table;
const ARG_ALL: &str = "all";
const ARG_EXCLUDE: &str = "exclude";
const ARG_INCLUDE: &str = "include";
const ARG_SOURCE: &str = "source"; const ARG_SOURCE: &str = "source";
const ARG_TARGET: &str = "target"; const ARG_TARGET: &str = "target";
const CMD_CREATE: &str = "create"; const CMD_CREATE: &str = "create";
@ -74,9 +79,10 @@ pub fn subcmd() -> Command {
/// Represents the source folder argument. /// Represents the source folder argument.
pub fn source_arg() -> Arg { pub fn source_arg() -> Arg {
Arg::new(ARG_SOURCE) Arg::new(ARG_SOURCE)
.help("Set the source folder")
.long("folder") .long("folder")
.short('f') .short('f')
.help("Specifies the source folder") .global(true)
.value_name("SOURCE") .value_name("SOURCE")
} }
@ -85,6 +91,70 @@ pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> {
matches.get_one::<String>(ARG_SOURCE).map(String::as_str) matches.get_one::<String>(ARG_SOURCE).map(String::as_str)
} }
/// Represents the all folders argument.
pub fn all_arg(help: &'static str) -> Arg {
Arg::new(ARG_ALL)
.help(help)
.long("all-folders")
.alias("all")
.short('A')
.action(ArgAction::SetTrue)
.conflicts_with(ARG_SOURCE)
.conflicts_with(ARG_INCLUDE)
.conflicts_with(ARG_EXCLUDE)
}
/// Represents the all folders argument parser.
pub fn parse_all_arg(m: &ArgMatches) -> bool {
m.get_flag(ARG_ALL)
}
/// Represents the folders to include argument.
pub fn include_arg(help: &'static str) -> Arg {
Arg::new(ARG_INCLUDE)
.help(help)
.long("include-folder")
.alias("only")
.short('F')
.value_name("FOLDER")
.num_args(1..)
.action(ArgAction::Append)
.conflicts_with(ARG_SOURCE)
.conflicts_with(ARG_ALL)
.conflicts_with(ARG_EXCLUDE)
}
/// Represents the folders to include argument parser.
pub fn parse_include_arg(m: &ArgMatches) -> HashSet<String> {
m.get_many::<String>(ARG_INCLUDE)
.unwrap_or_default()
.map(ToOwned::to_owned)
.collect()
}
/// Represents the folders to exclude argument.
pub fn exclude_arg(help: &'static str) -> Arg {
Arg::new(ARG_EXCLUDE)
.help(help)
.long("exclude-folder")
.alias("except")
.short('x')
.value_name("FOLDER")
.num_args(1..)
.action(ArgAction::Append)
.conflicts_with(ARG_SOURCE)
.conflicts_with(ARG_ALL)
.conflicts_with(ARG_INCLUDE)
}
/// Represents the folders to exclude argument parser.
pub fn parse_exclude_arg(m: &ArgMatches) -> HashSet<String> {
m.get_many::<String>(ARG_EXCLUDE)
.unwrap_or_default()
.map(ToOwned::to_owned)
.collect()
}
/// Represents the target folder argument. /// Represents the target folder argument.
pub fn target_arg() -> Arg { pub fn target_arg() -> Arg {
Arg::new(ARG_TARGET) Arg::new(ARG_TARGET)

View file

@ -122,11 +122,7 @@ fn main() -> Result<()> {
Some(account::args::Cmd::List(max_width)) => { Some(account::args::Cmd::List(max_width)) => {
return account::handlers::list(max_width, &account_config, &config, &mut printer); return account::handlers::list(max_width, &account_config, &config, &mut printer);
} }
Some(account::args::Cmd::Sync(dry_run)) => { Some(account::args::Cmd::Sync(folders_strategy, dry_run)) => {
let folder = match folder {
Some(folder) => Some(account_config.folder_alias(folder)?),
None => None,
};
let backend = BackendBuilder::new() let backend = BackendBuilder::new()
.sessions_pool_size(8) .sessions_pool_size(8)
.disable_cache(true) .disable_cache(true)
@ -135,7 +131,7 @@ fn main() -> Result<()> {
&account_config, &account_config,
&mut printer, &mut printer,
backend.as_ref(), backend.as_ref(),
&folder, folders_strategy,
dry_run, dry_run,
)?; )?;
backend.close()?; backend.close()?;

View file

@ -30,11 +30,14 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
/// Man subcommands. /// Man subcommands.
pub fn subcmd() -> Command { pub fn subcmd() -> Command {
Command::new(CMD_MAN) Command::new(CMD_MAN)
.about("Generates all man pages to the specified directory.") .about("Generate all man pages to the given directory")
.arg( .arg(
Arg::new(ARG_DIR) Arg::new(ARG_DIR)
.help("Directory where to generate man files") .help("Directory to generate man files in")
.long_help("Represents the directory where all man files of all commands and subcommands should be generated in.") .long_help(
"Represents the directory where all man files of
all commands and subcommands should be generated in.",
)
.required(true), .required(true),
) )
} }

View file

@ -11,22 +11,23 @@ pub(crate) const ARG_OUTPUT: &str = "output";
pub fn args() -> Vec<Arg> { pub fn args() -> Vec<Arg> {
vec![ vec![
Arg::new(ARG_OUTPUT) Arg::new(ARG_OUTPUT)
.help("Defines the output format") .help("Set the output format")
.long("output") .long("output")
.short('o') .short('o')
.global(true)
.value_name("FMT") .value_name("FMT")
.value_parser(["plain", "json"]) .value_parser(["plain", "json"])
.default_value("plain"), .default_value("plain"),
Arg::new(ARG_COLOR) Arg::new(ARG_COLOR)
.help("Controls when to use colors.") .help("Control when to use colors.")
.long_help( .long_help(
" "This flag controls when to use colors. The default
This flag controls when to use colors. The default setting is 'auto', which setting is 'auto', which means himalaya will try to guess when to use
means himalaya will try to guess when to use colors. For example, if himalaya is colors. For example, if himalaya is printing to a terminal, then it
printing to a terminal, then it will use colors, but if it is redirected to a will use colors, but if it is redirected to a file or a pipe, then it
file or a pipe, then it will suppress color output. himalaya will suppress color will suppress color output. himalaya will suppress color output in
output in some other circumstances as well. For example, if the TERM some other circumstances as well. For example, if the TERM environment
environment variable is not set or set to 'dumb', then himalaya will not use variable is not set or set to 'dumb', then himalaya will not use
colors. colors.
The possible values for this flag are: The possible values for this flag are:
@ -34,11 +35,11 @@ The possible values for this flag are:
never Colors will never be used. never Colors will never be used.
auto The default. himalaya tries to be smart. auto The default. himalaya tries to be smart.
always Colors will always be used regardless of where output is sent. always Colors will always be used regardless of where output is sent.
ansi Like 'always', but emits ANSI escapes (even in a Windows console). ansi Like 'always', but emits ANSI escapes (even in a Windows console).",
",
) )
.long("color") .long("color")
.short('C') .short('C')
.global(true)
.value_parser(["never", "auto", "always", "ansi"]) .value_parser(["never", "auto", "always", "ansi"])
.default_value("auto") .default_value("auto")
.value_name("WHEN"), .value_name("WHEN"),