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 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

2
Cargo.lock generated
View file

@ -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",

View file

@ -11,7 +11,7 @@ email-writing-sign-cmd = "gpg -o - -saq"
email-writing-encrypt-cmd = "gpg -o - -eqar <recipient>"
[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"

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.
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)
}

View file

@ -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")
}

View file

@ -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<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 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<SyncFoldersStrategy>, DryRun),
}
/// 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) {
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")
}

View file

@ -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<PathBuf>,
#[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(),
}
}
}

View file

@ -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<P: Printer>(
account_config: &AccountConfig,
printer: &mut P,
backend: &dyn Backend,
folder: &Option<String>,
folders_strategy: Option<SyncFoldersStrategy>,
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 {

View file

@ -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::<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.
pub fn target_arg() -> Arg {
Arg::new(ARG_TARGET)

View file

@ -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()?;

View file

@ -30,11 +30,14 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
/// 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),
)
}

View file

@ -11,22 +11,23 @@ pub(crate) const ARG_OUTPUT: &str = "output";
pub fn args() -> Vec<Arg> {
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"),