From fb324878fa5c4f34d111d39b8b37a854e3b48c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 22 Feb 2023 13:14:21 +0100 Subject: [PATCH] improve global options, add account config `sync-folders-strategy` --- CHANGELOG.md | 29 +++++++++++++ Cargo.lock | 2 +- config.sample.toml | 3 +- src/cache/args.rs | 7 +++- src/config/args.rs | 3 +- src/config/prelude.rs | 20 +++++++-- src/domain/account/args.rs | 33 +++++++++++++-- src/domain/account/config.rs | 11 +++-- src/domain/account/handlers.rs | 15 ++++--- src/domain/folder/args.rs | 74 +++++++++++++++++++++++++++++++++- src/main.rs | 8 +--- src/man/args.rs | 9 +++-- src/output/args.rs | 23 ++++++----- 13 files changed, 194 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9c0353..78629b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 generated completions and man pages to releases [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 @@ -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#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 diff --git a/Cargo.lock b/Cargo.lock index b152a64..affed63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -804,7 +804,7 @@ dependencies = [ [[package]] name = "himalaya-lib" 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 = [ "ammonia", "chrono", diff --git a/config.sample.toml b/config.sample.toml index a18f3a8..8870953 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -11,7 +11,7 @@ email-writing-sign-cmd = "gpg -o - -saq" email-writing-encrypt-cmd = "gpg -o - -eqar " [example] -default = false +default = true display-name = "Display NAME (gmail)" email = "display.name@gmail.local" @@ -36,6 +36,7 @@ smtp-starttls = false sync = true sync-dir = "/tmp/sync/gmail" +sync-folders-strategy.only = ["INBOX"] [example.folder-aliases] inbox = "INBOX" diff --git a/src/cache/args.rs b/src/cache/args.rs index 9da7989..cd40a09 100644 --- a/src/cache/args.rs +++ b/src/cache/args.rs @@ -8,8 +8,13 @@ const ARG_DISABLE_CACHE: &str = "disable-cache"; /// the user to disable any sort of cache. pub fn arg() -> Arg { Arg::new(ARG_DISABLE_CACHE) - .long("disable-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) } diff --git a/src/config/args.rs b/src/config/args.rs index 20ba201..ec425f5 100644 --- a/src/config/args.rs +++ b/src/config/args.rs @@ -8,9 +8,10 @@ const ARG_CONFIG: &str = "config"; /// user to customize the config file path. pub fn arg() -> Arg { Arg::new(ARG_CONFIG) + .help("Set a custom configuration file path") .long("config") .short('c') - .help("Forces a specific config file path") + .global(true) .value_name("PATH") } diff --git a/src/config/prelude.rs b/src/config/prelude.rs index 8d4f830..82f5528 100644 --- a/src/config/prelude.rs +++ b/src/config/prelude.rs @@ -1,9 +1,9 @@ -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - 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")] use himalaya_lib::ImapConfig; @@ -111,3 +111,15 @@ pub struct EmailHooksDef { /// Represents the hook called just before sending an email. pub pre_send: Option, } + +#[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), + #[serde(alias = "except")] + #[serde(alias = "ignore")] + Exclude(HashSet), +} diff --git a/src/domain/account/args.rs b/src/domain/account/args.rs index 701552b..7449f74 100644 --- a/src/domain/account/args.rs +++ b/src/domain/account/args.rs @@ -2,9 +2,11 @@ use anyhow::Result; use clap::{Arg, ArgAction, ArgMatches, Command}; +use himalaya_lib::folder::sync::Strategy as SyncFoldersStrategy; use log::info; +use std::collections::HashSet; -use crate::ui::table; +use crate::{folder, ui::table}; const ARG_ACCOUNT: &str = "account"; const ARG_DRY_RUN: &str = "dry-run"; @@ -20,7 +22,7 @@ pub enum Cmd { /// Represents the list accounts command. List(table::args::MaxTableWidth), /// Represents the sync account command. - Sync(DryRun), + Sync(Option, DryRun), } /// Represents the account command matcher. @@ -29,7 +31,22 @@ pub fn matches(m: &ArgMatches) -> Result> { if let Some(m) = m.subcommand_matches(CMD_SYNC) { info!("sync account subcommand matched"); 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) { info!("list accounts subcommand matched"); let max_table_width = table::args::parse_max_width(m); @@ -55,6 +72,13 @@ pub fn subcmd() -> Command { .arg(table::args::max_width()), Command::new(CMD_SYNC) .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()), ]) } @@ -63,9 +87,10 @@ pub fn subcmd() -> Command { /// the user to select a different account than the default one. pub fn arg() -> Arg { Arg::new(ARG_ACCOUNT) + .help("Set the account") .long("account") .short('a') - .help("Select a specific account by name") + .global(true) .value_name("STRING") } diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs index 046d7e8..6086dbb 100644 --- a/src/domain/account/config.rs +++ b/src/domain/account/config.rs @@ -4,8 +4,11 @@ //! account in the accounts section of the user configuration file. 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")] use himalaya_lib::ImapConfig; @@ -13,9 +16,6 @@ use himalaya_lib::ImapConfig; #[cfg(feature = "notmuch-backend")] use himalaya_lib::NotmuchConfig; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, path::PathBuf}; - use crate::config::{prelude::*, DeserializedConfig}; /// Represents all existing kind of account config. @@ -104,6 +104,8 @@ pub struct DeserializedBaseAccountConfig { #[serde(default)] pub sync: bool, pub sync_dir: Option, + #[serde(default, with = "SyncFoldersStrategyDef")] + pub sync_folders_strategy: SyncFoldersStrategy, } impl DeserializedBaseAccountConfig { @@ -207,6 +209,7 @@ impl DeserializedBaseAccountConfig { }, sync: self.sync, sync_dir: self.sync_dir.clone(), + sync_folders_strategy: self.sync_folders_strategy.clone(), } } } diff --git a/src/domain/account/handlers.rs b/src/domain/account/handlers.rs index 0f5aeca..442ad04 100644 --- a/src/domain/account/handlers.rs +++ b/src/domain/account/handlers.rs @@ -3,7 +3,10 @@ //! This module gathers all account actions triggered by the CLI. 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 log::{info, trace}; @@ -43,15 +46,17 @@ pub fn sync( account_config: &AccountConfig, printer: &mut P, backend: &dyn Backend, - folder: &Option, + folders_strategy: Option, dry_run: bool, ) -> Result<()> { 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); - 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 { diff --git a/src/domain/folder/args.rs b/src/domain/folder/args.rs index e55c08f..f83b28f 100644 --- a/src/domain/folder/args.rs +++ b/src/domain/folder/args.rs @@ -3,12 +3,17 @@ //! This module provides subcommands, arguments and a command matcher //! related to the folder domain. +use std::collections::HashSet; + use anyhow::Result; -use clap::{self, Arg, ArgMatches, Command}; +use clap::{self, Arg, ArgAction, ArgMatches, Command}; use log::{debug, info}; 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_TARGET: &str = "target"; const CMD_CREATE: &str = "create"; @@ -74,9 +79,10 @@ pub fn subcmd() -> Command { /// Represents the source folder argument. pub fn source_arg() -> Arg { Arg::new(ARG_SOURCE) + .help("Set the source folder") .long("folder") .short('f') - .help("Specifies the source folder") + .global(true) .value_name("SOURCE") } @@ -85,6 +91,70 @@ pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> { matches.get_one::(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 { + m.get_many::(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 { + m.get_many::(ARG_EXCLUDE) + .unwrap_or_default() + .map(ToOwned::to_owned) + .collect() +} + /// Represents the target folder argument. pub fn target_arg() -> Arg { Arg::new(ARG_TARGET) diff --git a/src/main.rs b/src/main.rs index 320f5e0..bfd2ac4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -122,11 +122,7 @@ fn main() -> Result<()> { Some(account::args::Cmd::List(max_width)) => { return account::handlers::list(max_width, &account_config, &config, &mut printer); } - Some(account::args::Cmd::Sync(dry_run)) => { - let folder = match folder { - Some(folder) => Some(account_config.folder_alias(folder)?), - None => None, - }; + Some(account::args::Cmd::Sync(folders_strategy, dry_run)) => { let backend = BackendBuilder::new() .sessions_pool_size(8) .disable_cache(true) @@ -135,7 +131,7 @@ fn main() -> Result<()> { &account_config, &mut printer, backend.as_ref(), - &folder, + folders_strategy, dry_run, )?; backend.close()?; diff --git a/src/man/args.rs b/src/man/args.rs index 6361c47..a85f953 100644 --- a/src/man/args.rs +++ b/src/man/args.rs @@ -30,11 +30,14 @@ pub fn matches(m: &ArgMatches) -> Result> { /// Man subcommands. pub fn subcmd() -> Command { Command::new(CMD_MAN) - .about("Generates all man pages to the specified directory.") + .about("Generate all man pages to the given directory") .arg( Arg::new(ARG_DIR) - .help("Directory where to generate man files") - .long_help("Represents the directory where all man files of all commands and subcommands should be generated in.") + .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.", + ) .required(true), ) } diff --git a/src/output/args.rs b/src/output/args.rs index aa5776a..d5a98a1 100644 --- a/src/output/args.rs +++ b/src/output/args.rs @@ -11,22 +11,23 @@ pub(crate) const ARG_OUTPUT: &str = "output"; pub fn args() -> Vec { vec![ Arg::new(ARG_OUTPUT) - .help("Defines the output format") + .help("Set the output format") .long("output") .short('o') + .global(true) .value_name("FMT") .value_parser(["plain", "json"]) .default_value("plain"), Arg::new(ARG_COLOR) - .help("Controls when to use colors.") + .help("Control when to use colors.") .long_help( - " -This flag controls 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 + "This flag controls 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 for this flag are: @@ -34,11 +35,11 @@ The possible values for this flag are: never Colors will never be used. auto The default. himalaya tries to be smart. 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") .short('C') + .global(true) .value_parser(["never", "auto", "always", "ansi"]) .default_value("auto") .value_name("WHEN"),