mirror of
https://github.com/soywod/himalaya.git
synced 2024-07-05 17:15:12 +00:00
refactor account with clap derive api
This commit is contained in:
parent
d2308221d7
commit
abe4c7f4ea
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -483,6 +483,7 @@ dependencies = [
|
|||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim 0.10.0",
|
||||
"terminal_size 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2094,7 +2095,7 @@ dependencies = [
|
|||
"shellexpand-utils 0.1.0",
|
||||
"tempfile",
|
||||
"termcolor",
|
||||
"terminal_size",
|
||||
"terminal_size 0.1.17",
|
||||
"tokio",
|
||||
"toml 0.7.8",
|
||||
"toml_edit 0.19.15",
|
||||
|
@ -4104,6 +4105,16 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.50"
|
||||
|
|
|
@ -59,11 +59,11 @@ version = "0.2"
|
|||
version = "0.4.24"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.0"
|
||||
features = ["derive"]
|
||||
version = "4.4"
|
||||
features = ["derive", "wrap_help"]
|
||||
|
||||
[dependencies.clap_complete]
|
||||
version = "4.0"
|
||||
version = "4.4"
|
||||
|
||||
[dependencies.clap_mangen]
|
||||
version = "0.2"
|
||||
|
|
|
@ -1,160 +0,0 @@
|
|||
//! This module provides arguments related to the user account config.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use email::folder::sync::FolderSyncStrategy;
|
||||
use log::info;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{folder, ui::table};
|
||||
|
||||
const ARG_ACCOUNT: &str = "account";
|
||||
const ARG_DRY_RUN: &str = "dry-run";
|
||||
const ARG_RESET: &str = "reset";
|
||||
const CMD_ACCOUNT: &str = "account";
|
||||
const CMD_CONFIGURE: &str = "configure";
|
||||
const CMD_LIST: &str = "list";
|
||||
const CMD_SYNC: &str = "sync";
|
||||
|
||||
type DryRun = bool;
|
||||
type Reset = bool;
|
||||
|
||||
/// Represents the account commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd {
|
||||
/// Represents the list accounts command.
|
||||
List(table::args::MaxTableWidth),
|
||||
/// Represents the sync account command.
|
||||
Sync(Option<FolderSyncStrategy>, DryRun),
|
||||
/// Configure the current selected account.
|
||||
Configure(Reset),
|
||||
}
|
||||
|
||||
/// Represents the account command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNT) {
|
||||
if let Some(m) = m.subcommand_matches(CMD_CONFIGURE) {
|
||||
info!("configure account subcommand matched");
|
||||
let reset = parse_reset_flag(m);
|
||||
Some(Cmd::Configure(reset))
|
||||
} 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);
|
||||
Some(Cmd::List(max_table_width))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SYNC) {
|
||||
info!("sync account subcommand matched");
|
||||
let dry_run = parse_dry_run_arg(m);
|
||||
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_global_source_arg(m) {
|
||||
Some(FolderSyncStrategy::Include(HashSet::from_iter([
|
||||
folder.to_owned()
|
||||
])))
|
||||
} else if !include.is_empty() {
|
||||
Some(FolderSyncStrategy::Include(include.to_owned()))
|
||||
} else if !exclude.is_empty() {
|
||||
Some(FolderSyncStrategy::Exclude(exclude))
|
||||
} else if folder::args::parse_all_arg(m) {
|
||||
Some(FolderSyncStrategy::All)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(Cmd::Sync(folders_strategy, dry_run))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the account subcommand.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_ACCOUNT)
|
||||
.about("Subcommand to manage accounts")
|
||||
.long_about("Subcommand to manage accounts like configure, list or sync")
|
||||
.aliases(["accounts", "acc"])
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(
|
||||
Command::new(CMD_CONFIGURE)
|
||||
.about("Configure the given account")
|
||||
.aliases(["config", "conf", "cfg"])
|
||||
.arg(reset_flag())
|
||||
.arg(folder::args::source_arg(
|
||||
"Define the account to be configured",
|
||||
)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_LIST)
|
||||
.about("List all accounts")
|
||||
.long_about("List all accounts that are set up in the configuration file")
|
||||
.arg(table::args::max_width()),
|
||||
)
|
||||
.subcommand(
|
||||
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()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Represents the user account name argument. This argument allows
|
||||
/// the user to select a different account than the default one.
|
||||
pub fn global_args() -> impl IntoIterator<Item = Arg> {
|
||||
[Arg::new(ARG_ACCOUNT)
|
||||
.help("Override the default account")
|
||||
.long_help(
|
||||
"Override the default account
|
||||
|
||||
The given account will be used by default for all other commands (when applicable).",
|
||||
)
|
||||
.long("account")
|
||||
.short('a')
|
||||
.global(true)
|
||||
.value_name("name")]
|
||||
}
|
||||
|
||||
/// Represents the user account name argument parser.
|
||||
pub fn parse_global_arg(matches: &ArgMatches) -> Option<&str> {
|
||||
matches.get_one::<String>(ARG_ACCOUNT).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Represents the user account sync dry run flag. This flag allows
|
||||
/// the user to see the changes of a sync without applying them.
|
||||
pub fn dry_run() -> Arg {
|
||||
Arg::new(ARG_DRY_RUN)
|
||||
.help("Do not apply changes of the synchronization")
|
||||
.long_help(
|
||||
"Do not apply changes of the synchronization.
|
||||
Changes can be visualized with the RUST_LOG=trace environment variable.",
|
||||
)
|
||||
.short('d')
|
||||
.long("dry-run")
|
||||
.action(ArgAction::SetTrue)
|
||||
}
|
||||
|
||||
/// Represents the user account sync dry run flag parser.
|
||||
pub fn parse_dry_run_arg(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_DRY_RUN)
|
||||
}
|
||||
|
||||
pub fn reset_flag() -> Arg {
|
||||
Arg::new(ARG_RESET)
|
||||
.help("Reset the configuration")
|
||||
.short('r')
|
||||
.long("reset")
|
||||
.action(ArgAction::SetTrue)
|
||||
}
|
||||
|
||||
pub fn parse_reset_flag(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_RESET)
|
||||
}
|
115
src/account/command/configure.rs
Normal file
115
src/account/command/configure.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::config::ImapAuthConfig;
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::config::SmtpAuthConfig;
|
||||
use log::{debug, info, warn};
|
||||
|
||||
use crate::{
|
||||
config::{
|
||||
wizard::{prompt_passwd, prompt_secret},
|
||||
TomlConfig,
|
||||
},
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Configure the given account
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command {
|
||||
/// The name of the account that needs to be configured
|
||||
///
|
||||
/// The account names are taken from the table at the root level
|
||||
/// of your TOML configuration file.
|
||||
#[arg(value_name = "NAME")]
|
||||
pub account_name: String,
|
||||
|
||||
/// Force the account to reconfigure, even if it is already
|
||||
/// configured
|
||||
#[arg(long, short)]
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing account configure command");
|
||||
|
||||
let (_, account_config) =
|
||||
config.into_toml_account_config(Some(self.account_name.as_str()))?;
|
||||
|
||||
if self.force {
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(ref config) = account_config.imap {
|
||||
let reset = match &config.auth {
|
||||
ImapAuthConfig::Passwd(config) => config.reset(),
|
||||
ImapAuthConfig::OAuth2(config) => config.reset(),
|
||||
};
|
||||
if let Err(err) = reset {
|
||||
warn!("error while resetting imap secrets: {err}");
|
||||
debug!("error while resetting imap secrets: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(ref config) = account_config.smtp {
|
||||
let reset = match &config.auth {
|
||||
SmtpAuthConfig::Passwd(config) => config.reset(),
|
||||
SmtpAuthConfig::OAuth2(config) => config.reset(),
|
||||
};
|
||||
if let Err(err) = reset {
|
||||
warn!("error while resetting smtp secrets: {err}");
|
||||
debug!("error while resetting smtp secrets: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
if let Some(ref config) = account_config.pgp {
|
||||
account_config.pgp.reset().await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(ref config) = account_config.imap {
|
||||
match &config.auth {
|
||||
ImapAuthConfig::Passwd(config) => {
|
||||
config.configure(|| prompt_passwd("IMAP password")).await
|
||||
}
|
||||
ImapAuthConfig::OAuth2(config) => {
|
||||
config
|
||||
.configure(|| prompt_secret("IMAP OAuth 2.0 client secret"))
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(ref config) = account_config.smtp {
|
||||
match &config.auth {
|
||||
SmtpAuthConfig::Passwd(config) => {
|
||||
config.configure(|| prompt_passwd("SMTP password")).await
|
||||
}
|
||||
SmtpAuthConfig::OAuth2(config) => {
|
||||
config
|
||||
.configure(|| prompt_secret("SMTP OAuth 2.0 client secret"))
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
if let Some(ref config) = config.pgp {
|
||||
config
|
||||
.pgp
|
||||
.configure(&config.email, || prompt_passwd("PGP secret key password"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
printer.print(format!(
|
||||
"Account {} successfully {}configured!",
|
||||
self.account_name,
|
||||
if self.force { "re" } else { "" }
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
141
src/account/command/list.rs
Normal file
141
src/account/command/list.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
account::Accounts,
|
||||
config::TomlConfig,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
};
|
||||
|
||||
/// List all accounts
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command {
|
||||
/// Define a maximum width for the table
|
||||
#[arg(long, short = 'w', name = "PIXELS")]
|
||||
pub max_width: Option<usize>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing account list command");
|
||||
|
||||
let accounts: Accounts = config.accounts.iter().into();
|
||||
|
||||
printer.print_table(
|
||||
Box::new(accounts),
|
||||
PrintTableOpts {
|
||||
format: config
|
||||
.email_reading_format
|
||||
.as_ref()
|
||||
.unwrap_or(&Default::default()),
|
||||
max_width: self.max_width,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use email::{account::config::AccountConfig, imap::config::ImapConfig};
|
||||
use std::{collections::HashMap, fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::{
|
||||
account::TomlAccountConfig,
|
||||
backend::BackendKind,
|
||||
printer::{Print, PrintTable, WriteColor},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_match_cmds_accounts() {
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct StringWriter {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl io::Write for StringWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.content
|
||||
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.content = String::default();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl termcolor::WriteColor for StringWriter {
|
||||
fn supports_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
|
||||
io::Result::Ok(())
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> io::Result<()> {
|
||||
io::Result::Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteColor for StringWriter {}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PrinterServiceTest {
|
||||
pub writer: StringWriter,
|
||||
}
|
||||
|
||||
impl Printer for PrinterServiceTest {
|
||||
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
|
||||
&mut self,
|
||||
data: Box<T>,
|
||||
opts: PrintTableOpts,
|
||||
) -> Result<()> {
|
||||
data.print_table(&mut self.writer, opts)?;
|
||||
Ok(())
|
||||
}
|
||||
fn print_log<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print<T: Debug + Print + serde::Serialize>(&mut self, _data: T) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn is_json(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
let config = AccountConfig::default();
|
||||
let deserialized_config = TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account-1".into(),
|
||||
TomlAccountConfig {
|
||||
default: Some(true),
|
||||
backend: Some(BackendKind::Imap),
|
||||
imap: Some(ImapConfig::default()),
|
||||
..Default::default()
|
||||
},
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
};
|
||||
|
||||
assert!(list(None, &config, &deserialized_config, &mut printer).is_ok());
|
||||
assert_eq!(
|
||||
concat![
|
||||
"\n",
|
||||
"NAME │BACKEND │DEFAULT \n",
|
||||
"account-1 │imap │yes \n",
|
||||
"\n"
|
||||
],
|
||||
printer.writer.content
|
||||
);
|
||||
}
|
||||
}
|
34
src/account/command/mod.rs
Normal file
34
src/account/command/mod.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
mod configure;
|
||||
mod list;
|
||||
mod sync;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
|
||||
/// Subcommand to manage accounts
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
/// Configure the given account
|
||||
#[command(alias = "cfg")]
|
||||
Configure(configure::Command),
|
||||
|
||||
/// List all exsting accounts
|
||||
#[command(alias = "lst")]
|
||||
List(list::Command),
|
||||
|
||||
/// Synchronize the given account locally
|
||||
#[command()]
|
||||
Sync(sync::Command),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::Configure(cmd) => cmd.execute(printer, config).await,
|
||||
Self::List(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Sync(cmd) => cmd.execute(printer, config).await,
|
||||
}
|
||||
}
|
||||
}
|
233
src/account/command/sync.rs
Normal file
233
src/account/command/sync.rs
Normal file
|
@ -0,0 +1,233 @@
|
|||
use anyhow::Result;
|
||||
use clap::{ArgAction, Parser};
|
||||
use email::{
|
||||
account::sync::{AccountSyncBuilder, AccountSyncProgressEvent},
|
||||
folder::sync::FolderSyncStrategy,
|
||||
};
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
|
||||
use log::info;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use crate::{backend::BackendBuilder, config::TomlConfig, printer::Printer};
|
||||
|
||||
const MAIN_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||
ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap()
|
||||
});
|
||||
|
||||
const SUB_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||
ProgressStyle::with_template(
|
||||
" {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ",
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
const SUB_PROGRESS_DONE_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||
ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap()
|
||||
});
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command {
|
||||
/// The name of the account that needs to be synchronized
|
||||
///
|
||||
/// The account names are taken from the table at the root level
|
||||
/// of your TOML configuration file.
|
||||
#[arg(value_name = "ACCOUNT")]
|
||||
pub account_name: String,
|
||||
|
||||
/// Run the synchronization without applying changes
|
||||
///
|
||||
/// Instead, a report will be printed to stdout containing all the
|
||||
/// changes the synchronization plan to do.
|
||||
#[arg(long, short)]
|
||||
pub dry_run: bool,
|
||||
|
||||
#[arg(long, short = 'f', value_name = "FOLDER", action = ArgAction::Append, conflicts_with = "exclude_folder", conflicts_with = "all_folders")]
|
||||
pub include_folder: Vec<String>,
|
||||
|
||||
#[arg(long, short = 'x', value_name = "FOLDER", action = ArgAction::Append, conflicts_with = "include_folder", conflicts_with = "all_folders")]
|
||||
pub exclude_folder: Vec<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
short = 'A',
|
||||
conflicts_with = "include_folder",
|
||||
conflicts_with = "exclude_folder"
|
||||
)]
|
||||
pub all_folders: bool,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing account sync command");
|
||||
|
||||
let included_folders = HashSet::from_iter(self.include_folder);
|
||||
let excluded_folders = HashSet::from_iter(self.exclude_folder);
|
||||
|
||||
let strategy = if !included_folders.is_empty() {
|
||||
Some(FolderSyncStrategy::Include(included_folders))
|
||||
} else if !excluded_folders.is_empty() {
|
||||
Some(FolderSyncStrategy::Exclude(excluded_folders))
|
||||
} else if self.all_folders {
|
||||
Some(FolderSyncStrategy::All)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(Some(self.account_name.as_str()), true)?;
|
||||
|
||||
let backend_builder =
|
||||
BackendBuilder::new(toml_account_config, account_config.clone(), false).await?;
|
||||
let sync_builder = AccountSyncBuilder::new(backend_builder.into())
|
||||
.await?
|
||||
.with_some_folders_strategy(strategy)
|
||||
.with_dry_run(self.dry_run);
|
||||
|
||||
if self.dry_run {
|
||||
let report = sync_builder.sync().await?;
|
||||
let mut hunks_count = report.folders_patch.len();
|
||||
|
||||
if !report.folders_patch.is_empty() {
|
||||
printer.print_log("Folders patch:")?;
|
||||
for (hunk, _) in report.folders_patch {
|
||||
printer.print_log(format!(" - {hunk}"))?;
|
||||
}
|
||||
printer.print_log("")?;
|
||||
}
|
||||
|
||||
if !report.emails_patch.is_empty() {
|
||||
printer.print_log("Envelopes patch:")?;
|
||||
for (hunk, _) in report.emails_patch {
|
||||
hunks_count += 1;
|
||||
printer.print_log(format!(" - {hunk}"))?;
|
||||
}
|
||||
printer.print_log("")?;
|
||||
}
|
||||
|
||||
printer.print(format!(
|
||||
"Estimated patch length for account to be synchronized: {hunks_count}",
|
||||
))?;
|
||||
} else if printer.is_json() {
|
||||
sync_builder.sync().await?;
|
||||
printer.print("Account successfully synchronized!")?;
|
||||
} else {
|
||||
let multi = MultiProgress::new();
|
||||
let sub_progresses = Mutex::new(HashMap::new());
|
||||
let main_progress = multi.add(
|
||||
ProgressBar::new(100)
|
||||
.with_style(MAIN_PROGRESS_STYLE.clone())
|
||||
.with_message("Synchronizing folders…"),
|
||||
);
|
||||
|
||||
// Force the progress bar to show
|
||||
main_progress.set_position(0);
|
||||
|
||||
let report = sync_builder
|
||||
.with_on_progress(move |evt| {
|
||||
use AccountSyncProgressEvent::*;
|
||||
Ok(match evt {
|
||||
ApplyFolderPatches(..) => {
|
||||
main_progress.inc(3);
|
||||
}
|
||||
ApplyEnvelopePatches(patches) => {
|
||||
let mut envelopes_progresses = sub_progresses.lock().unwrap();
|
||||
let patches_len =
|
||||
patches.values().fold(0, |sum, patch| sum + patch.len());
|
||||
main_progress.set_length((110 * patches_len / 100) as u64);
|
||||
main_progress.set_position((5 * patches_len / 100) as u64);
|
||||
main_progress.set_message("Synchronizing envelopes…");
|
||||
|
||||
for (folder, patch) in patches {
|
||||
let progress = ProgressBar::new(patch.len() as u64)
|
||||
.with_style(SUB_PROGRESS_STYLE.clone())
|
||||
.with_prefix(folder.clone())
|
||||
.with_finish(ProgressFinish::AndClear);
|
||||
let progress = multi.add(progress);
|
||||
envelopes_progresses.insert(folder, progress.clone());
|
||||
}
|
||||
}
|
||||
ApplyEnvelopeHunk(hunk) => {
|
||||
main_progress.inc(1);
|
||||
let mut progresses = sub_progresses.lock().unwrap();
|
||||
if let Some(progress) = progresses.get_mut(hunk.folder()) {
|
||||
progress.inc(1);
|
||||
if progress.position() == (progress.length().unwrap() - 1) {
|
||||
progress.set_style(SUB_PROGRESS_DONE_STYLE.clone())
|
||||
} else {
|
||||
progress.set_message(format!("{hunk}…"));
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplyEnvelopeCachePatch(_patch) => {
|
||||
main_progress.set_length(100);
|
||||
main_progress.set_position(95);
|
||||
main_progress.set_message("Saving cache database…");
|
||||
}
|
||||
ExpungeFolders(folders) => {
|
||||
let mut progresses = sub_progresses.lock().unwrap();
|
||||
for progress in progresses.values() {
|
||||
progress.finish_and_clear()
|
||||
}
|
||||
progresses.clear();
|
||||
|
||||
main_progress.set_position(100);
|
||||
main_progress
|
||||
.set_message(format!("Expunging {} folders…", folders.len()));
|
||||
}
|
||||
_ => (),
|
||||
})
|
||||
})
|
||||
.sync()
|
||||
.await?;
|
||||
|
||||
let folders_patch_err = report
|
||||
.folders_patch
|
||||
.iter()
|
||||
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
|
||||
.collect::<Vec<_>>();
|
||||
if !folders_patch_err.is_empty() {
|
||||
printer.print_log("")?;
|
||||
printer.print_log("Errors occurred while applying the folders patch:")?;
|
||||
folders_patch_err
|
||||
.iter()
|
||||
.try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?;
|
||||
}
|
||||
|
||||
if let Some(err) = report.folders_cache_patch.1 {
|
||||
printer.print_log("")?;
|
||||
printer.print_log(format!(
|
||||
"Error occurred while applying the folder cache patch: {err}"
|
||||
))?;
|
||||
}
|
||||
|
||||
let envelopes_patch_err = report
|
||||
.emails_patch
|
||||
.iter()
|
||||
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
|
||||
.collect::<Vec<_>>();
|
||||
if !envelopes_patch_err.is_empty() {
|
||||
printer.print_log("")?;
|
||||
printer.print_log("Errors occurred while applying the envelopes patch:")?;
|
||||
for (hunk, err) in folders_patch_err {
|
||||
printer.print_log(format!(" - {hunk}: {err}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(err) = report.emails_cache_patch.1 {
|
||||
printer.print_log("")?;
|
||||
printer.print_log(format!(
|
||||
"Error occurred while applying the envelopes cache patch: {err}"
|
||||
))?;
|
||||
}
|
||||
|
||||
printer.print("Account successfully synchronized!")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,401 +0,0 @@
|
|||
//! Account handlers module.
|
||||
//!
|
||||
//! This module gathers all account actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use email::account::{
|
||||
config::AccountConfig,
|
||||
sync::{AccountSyncBuilder, AccountSyncProgressEvent},
|
||||
};
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::config::ImapAuthConfig;
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::config::SmtpAuthConfig;
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
|
||||
use log::{debug, info, trace, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{collections::HashMap, sync::Mutex};
|
||||
|
||||
use crate::{
|
||||
account::Accounts,
|
||||
backend::BackendContextBuilder,
|
||||
config::{
|
||||
wizard::{prompt_passwd, prompt_secret},
|
||||
TomlConfig,
|
||||
},
|
||||
printer::{PrintTableOpts, Printer},
|
||||
};
|
||||
|
||||
use super::TomlAccountConfig;
|
||||
|
||||
const MAIN_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||
ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap()
|
||||
});
|
||||
|
||||
const SUB_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||
ProgressStyle::with_template(
|
||||
" {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ",
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
const SUB_PROGRESS_DONE_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||
ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap()
|
||||
});
|
||||
|
||||
/// Configure the current selected account
|
||||
pub async fn configure(config: &TomlAccountConfig, reset: bool) -> Result<()> {
|
||||
info!("entering the configure account handler");
|
||||
|
||||
if reset {
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(ref config) = config.imap {
|
||||
let reset = match &config.auth {
|
||||
ImapAuthConfig::Passwd(config) => config.reset(),
|
||||
ImapAuthConfig::OAuth2(config) => config.reset(),
|
||||
};
|
||||
if let Err(err) = reset {
|
||||
warn!("error while resetting imap secrets: {err}");
|
||||
debug!("error while resetting imap secrets: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(ref config) = config.smtp {
|
||||
let reset = match &config.auth {
|
||||
SmtpAuthConfig::Passwd(config) => config.reset(),
|
||||
SmtpAuthConfig::OAuth2(config) => config.reset(),
|
||||
};
|
||||
if let Err(err) = reset {
|
||||
warn!("error while resetting smtp secrets: {err}");
|
||||
debug!("error while resetting smtp secrets: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
if let Some(ref config) = config.pgp {
|
||||
config.pgp.reset().await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(ref config) = config.imap {
|
||||
match &config.auth {
|
||||
ImapAuthConfig::Passwd(config) => {
|
||||
config.configure(|| prompt_passwd("IMAP password")).await
|
||||
}
|
||||
ImapAuthConfig::OAuth2(config) => {
|
||||
config
|
||||
.configure(|| prompt_secret("IMAP OAuth 2.0 client secret"))
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(ref config) = config.smtp {
|
||||
match &config.auth {
|
||||
SmtpAuthConfig::Passwd(config) => {
|
||||
config.configure(|| prompt_passwd("SMTP password")).await
|
||||
}
|
||||
SmtpAuthConfig::OAuth2(config) => {
|
||||
config
|
||||
.configure(|| prompt_secret("SMTP OAuth 2.0 client secret"))
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
if let Some(ref config) = config.pgp {
|
||||
config
|
||||
.pgp
|
||||
.configure(&config.email, || prompt_passwd("PGP secret key password"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
println!(
|
||||
"Account successfully {}configured!",
|
||||
if reset { "re" } else { "" }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lists all accounts.
|
||||
pub fn list<'a, P: Printer>(
|
||||
max_width: Option<usize>,
|
||||
config: &AccountConfig,
|
||||
deserialized_config: &TomlConfig,
|
||||
printer: &mut P,
|
||||
) -> Result<()> {
|
||||
info!("entering the list accounts handler");
|
||||
|
||||
let accounts: Accounts = deserialized_config.accounts.iter().into();
|
||||
trace!("accounts: {:?}", accounts);
|
||||
|
||||
printer.print_table(
|
||||
Box::new(accounts),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)?;
|
||||
|
||||
info!("<< account list handler");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronizes the account defined using argument `-a|--account`. If
|
||||
/// no account given, synchronizes the default one.
|
||||
pub async fn sync<P: Printer>(
|
||||
printer: &mut P,
|
||||
sync_builder: AccountSyncBuilder<BackendContextBuilder>,
|
||||
dry_run: bool,
|
||||
) -> Result<()> {
|
||||
info!("entering the sync accounts handler");
|
||||
trace!("dry run: {dry_run}");
|
||||
|
||||
if dry_run {
|
||||
let report = sync_builder.sync().await?;
|
||||
let mut hunks_count = report.folders_patch.len();
|
||||
|
||||
if !report.folders_patch.is_empty() {
|
||||
printer.print_log("Folders patch:")?;
|
||||
for (hunk, _) in report.folders_patch {
|
||||
printer.print_log(format!(" - {hunk}"))?;
|
||||
}
|
||||
printer.print_log("")?;
|
||||
}
|
||||
|
||||
if !report.emails_patch.is_empty() {
|
||||
printer.print_log("Envelopes patch:")?;
|
||||
for (hunk, _) in report.emails_patch {
|
||||
hunks_count += 1;
|
||||
printer.print_log(format!(" - {hunk}"))?;
|
||||
}
|
||||
printer.print_log("")?;
|
||||
}
|
||||
|
||||
printer.print(format!(
|
||||
"Estimated patch length for account to be synchronized: {hunks_count}",
|
||||
))?;
|
||||
} else if printer.is_json() {
|
||||
sync_builder.sync().await?;
|
||||
printer.print("Account successfully synchronized!")?;
|
||||
} else {
|
||||
let multi = MultiProgress::new();
|
||||
let sub_progresses = Mutex::new(HashMap::new());
|
||||
let main_progress = multi.add(
|
||||
ProgressBar::new(100)
|
||||
.with_style(MAIN_PROGRESS_STYLE.clone())
|
||||
.with_message("Synchronizing folders…"),
|
||||
);
|
||||
|
||||
// Force the progress bar to show
|
||||
main_progress.set_position(0);
|
||||
|
||||
let report = sync_builder
|
||||
.with_on_progress(move |evt| {
|
||||
use AccountSyncProgressEvent::*;
|
||||
Ok(match evt {
|
||||
ApplyFolderPatches(..) => {
|
||||
main_progress.inc(3);
|
||||
}
|
||||
ApplyEnvelopePatches(patches) => {
|
||||
let mut envelopes_progresses = sub_progresses.lock().unwrap();
|
||||
let patches_len = patches.values().fold(0, |sum, patch| sum + patch.len());
|
||||
main_progress.set_length((110 * patches_len / 100) as u64);
|
||||
main_progress.set_position((5 * patches_len / 100) as u64);
|
||||
main_progress.set_message("Synchronizing envelopes…");
|
||||
|
||||
for (folder, patch) in patches {
|
||||
let progress = ProgressBar::new(patch.len() as u64)
|
||||
.with_style(SUB_PROGRESS_STYLE.clone())
|
||||
.with_prefix(folder.clone())
|
||||
.with_finish(ProgressFinish::AndClear);
|
||||
let progress = multi.add(progress);
|
||||
envelopes_progresses.insert(folder, progress.clone());
|
||||
}
|
||||
}
|
||||
ApplyEnvelopeHunk(hunk) => {
|
||||
main_progress.inc(1);
|
||||
let mut progresses = sub_progresses.lock().unwrap();
|
||||
if let Some(progress) = progresses.get_mut(hunk.folder()) {
|
||||
progress.inc(1);
|
||||
if progress.position() == (progress.length().unwrap() - 1) {
|
||||
progress.set_style(SUB_PROGRESS_DONE_STYLE.clone())
|
||||
} else {
|
||||
progress.set_message(format!("{hunk}…"));
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplyEnvelopeCachePatch(_patch) => {
|
||||
main_progress.set_length(100);
|
||||
main_progress.set_position(95);
|
||||
main_progress.set_message("Saving cache database…");
|
||||
}
|
||||
ExpungeFolders(folders) => {
|
||||
let mut progresses = sub_progresses.lock().unwrap();
|
||||
for progress in progresses.values() {
|
||||
progress.finish_and_clear()
|
||||
}
|
||||
progresses.clear();
|
||||
|
||||
main_progress.set_position(100);
|
||||
main_progress.set_message(format!("Expunging {} folders…", folders.len()));
|
||||
}
|
||||
_ => (),
|
||||
})
|
||||
})
|
||||
.sync()
|
||||
.await?;
|
||||
|
||||
let folders_patch_err = report
|
||||
.folders_patch
|
||||
.iter()
|
||||
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
|
||||
.collect::<Vec<_>>();
|
||||
if !folders_patch_err.is_empty() {
|
||||
printer.print_log("")?;
|
||||
printer.print_log("Errors occurred while applying the folders patch:")?;
|
||||
folders_patch_err
|
||||
.iter()
|
||||
.try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?;
|
||||
}
|
||||
|
||||
if let Some(err) = report.folders_cache_patch.1 {
|
||||
printer.print_log("")?;
|
||||
printer.print_log(format!(
|
||||
"Error occurred while applying the folder cache patch: {err}"
|
||||
))?;
|
||||
}
|
||||
|
||||
let envelopes_patch_err = report
|
||||
.emails_patch
|
||||
.iter()
|
||||
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
|
||||
.collect::<Vec<_>>();
|
||||
if !envelopes_patch_err.is_empty() {
|
||||
printer.print_log("")?;
|
||||
printer.print_log("Errors occurred while applying the envelopes patch:")?;
|
||||
for (hunk, err) in folders_patch_err {
|
||||
printer.print_log(format!(" - {hunk}: {err}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(err) = report.emails_cache_patch.1 {
|
||||
printer.print_log("")?;
|
||||
printer.print_log(format!(
|
||||
"Error occurred while applying the envelopes cache patch: {err}"
|
||||
))?;
|
||||
}
|
||||
|
||||
printer.print("Account successfully synchronized!")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use email::{account::config::AccountConfig, imap::config::ImapConfig};
|
||||
use std::{collections::HashMap, fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::{
|
||||
account::TomlAccountConfig,
|
||||
backend::BackendKind,
|
||||
printer::{Print, PrintTable, WriteColor},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_match_cmds_accounts() {
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct StringWriter {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl io::Write for StringWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.content
|
||||
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.content = String::default();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl termcolor::WriteColor for StringWriter {
|
||||
fn supports_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
|
||||
io::Result::Ok(())
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> io::Result<()> {
|
||||
io::Result::Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteColor for StringWriter {}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PrinterServiceTest {
|
||||
pub writer: StringWriter,
|
||||
}
|
||||
|
||||
impl Printer for PrinterServiceTest {
|
||||
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
|
||||
&mut self,
|
||||
data: Box<T>,
|
||||
opts: PrintTableOpts,
|
||||
) -> Result<()> {
|
||||
data.print_table(&mut self.writer, opts)?;
|
||||
Ok(())
|
||||
}
|
||||
fn print_log<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print<T: Debug + Print + serde::Serialize>(&mut self, _data: T) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn is_json(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
let config = AccountConfig::default();
|
||||
let deserialized_config = TomlConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account-1".into(),
|
||||
TomlAccountConfig {
|
||||
default: Some(true),
|
||||
backend: Some(BackendKind::Imap),
|
||||
imap: Some(ImapConfig::default()),
|
||||
..Default::default()
|
||||
},
|
||||
)]),
|
||||
..TomlConfig::default()
|
||||
};
|
||||
|
||||
assert!(list(None, &config, &deserialized_config, &mut printer).is_ok());
|
||||
assert_eq!(
|
||||
concat![
|
||||
"\n",
|
||||
"NAME │BACKEND │DEFAULT \n",
|
||||
"account-1 │imap │yes \n",
|
||||
"\n"
|
||||
],
|
||||
printer.writer.content
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
pub mod args;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod handlers;
|
||||
pub(crate) mod wizard;
|
||||
|
||||
use anyhow::Result;
|
||||
|
@ -45,7 +44,7 @@ impl Table for Account {
|
|||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("BACKEND").bold().underline().white())
|
||||
.cell(Cell::new("BACKENDS").bold().underline().white())
|
||||
.cell(Cell::new("DEFAULT").bold().underline().white())
|
||||
}
|
||||
|
||||
|
|
113
src/cli.rs
Normal file
113
src/cli.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
account::command as account,
|
||||
completion::command as completion,
|
||||
config::{self, TomlConfig},
|
||||
man::command as man,
|
||||
output::{ColorFmt, OutputFmt},
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "himalaya",
|
||||
author,
|
||||
version,
|
||||
about,
|
||||
propagate_version = true,
|
||||
infer_subcommands = true
|
||||
)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
|
||||
/// Override the default configuration file path
|
||||
///
|
||||
/// The given path is shell-expanded then canonicalized (if
|
||||
/// applicable). If the path does not point to a valid file, the
|
||||
/// wizard will propose to assist you in the creation of the
|
||||
/// configuration file.
|
||||
#[arg(long, short, value_name = "PATH", global = true, value_parser = config::path_parser)]
|
||||
pub config: Option<PathBuf>,
|
||||
|
||||
/// Customize the output format
|
||||
///
|
||||
/// The output format determine how to display commands output to
|
||||
/// the terminal.
|
||||
///
|
||||
/// The possible values are:
|
||||
///
|
||||
/// - json: output will be in a form of a JSON-compatible object
|
||||
///
|
||||
/// - plain: output will be in a form of either a plain text or
|
||||
/// table, depending on the command
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
value_name = "FORMAT",
|
||||
global = true,
|
||||
value_enum,
|
||||
default_value_t = Default::default(),
|
||||
)]
|
||||
pub output: OutputFmt,
|
||||
|
||||
/// Control 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 are:
|
||||
///
|
||||
/// - never: colors will never be used
|
||||
///
|
||||
/// - always: colors will always be used regardless of where output is sent
|
||||
///
|
||||
/// - ansi: like 'always', but emits ANSI escapes (even in a Windows console)
|
||||
///
|
||||
/// - auto: himalaya tries to be smart
|
||||
#[arg(
|
||||
long,
|
||||
short = 'C',
|
||||
value_name = "MODE",
|
||||
global = true,
|
||||
value_enum,
|
||||
default_value_t = Default::default(),
|
||||
)]
|
||||
pub color: ColorFmt,
|
||||
}
|
||||
|
||||
/// Top-level CLI commands.
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Command {
|
||||
/// Subcommand to manage accounts
|
||||
#[command(subcommand)]
|
||||
Account(account::Command),
|
||||
|
||||
/// Generate all man pages to the given directory
|
||||
#[command(arg_required_else_help = true, alias = "mans")]
|
||||
Man(man::Command),
|
||||
|
||||
/// Print completion script for the given shell to stdout
|
||||
#[command(arg_required_else_help = true, aliases = ["completions", "compl", "comp"])]
|
||||
Completion(completion::Command),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::Account(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Man(cmd) => cmd.execute(printer).await,
|
||||
Self::Completion(cmd) => cmd.execute(printer).await,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,32 @@
|
|||
use clap::{value_parser, Parser};
|
||||
use anyhow::Result;
|
||||
use clap::{value_parser, CommandFactory, Parser};
|
||||
use clap_complete::Shell;
|
||||
use log::info;
|
||||
use std::io;
|
||||
|
||||
use crate::{cli::Cli, printer::Printer};
|
||||
|
||||
/// Print completion script for the given shell to stdout
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Generate {
|
||||
pub struct Command {
|
||||
/// Shell that completion script should be generated for
|
||||
#[arg(value_parser = value_parser!(Shell))]
|
||||
pub shell: Shell,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub async fn execute(self, printer: &mut impl Printer) -> Result<()> {
|
||||
info!("executing completion generate command");
|
||||
|
||||
let mut cmd = Cli::command();
|
||||
let name = cmd.get_name().to_string();
|
||||
clap_complete::generate(self.shell, &mut cmd, name, &mut io::stdout());
|
||||
|
||||
printer.print(format!(
|
||||
"Shell script successfully generated for shell {}!",
|
||||
self.shell
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use clap::Command;
|
||||
use clap_complete::Shell;
|
||||
use std::io::stdout;
|
||||
|
||||
use crate::printer::Printer;
|
||||
|
||||
pub fn generate(printer: &mut impl Printer, mut cmd: Command, shell: Shell) -> Result<()> {
|
||||
let name = cmd.get_name().to_string();
|
||||
|
||||
clap_complete::generate(shell, &mut cmd, name, &mut stdout());
|
||||
|
||||
printer.print(format!(
|
||||
"Shell script successfully generated for shell {shell}!"
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,2 +1 @@
|
|||
pub mod command;
|
||||
pub mod handler;
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
//! Deserialized config module.
|
||||
//!
|
||||
//! This module contains the raw deserialized representation of the
|
||||
//! user configuration file.
|
||||
|
||||
pub mod args;
|
||||
pub mod prelude;
|
||||
pub mod wizard;
|
||||
|
@ -16,6 +11,7 @@ use email::{
|
|||
email::config::{EmailHooks, EmailTextPlainFormat},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shellexpand_utils::{canonicalize, expand};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
|
@ -162,12 +158,10 @@ impl TomlConfig {
|
|||
.filter(|p| p.exists())
|
||||
}
|
||||
|
||||
/// Build account configurations from a given account name.
|
||||
pub fn into_account_configs(
|
||||
self,
|
||||
pub fn into_toml_account_config(
|
||||
&self,
|
||||
account_name: Option<&str>,
|
||||
disable_cache: bool,
|
||||
) -> Result<(TomlAccountConfig, AccountConfig)> {
|
||||
) -> Result<(String, TomlAccountConfig)> {
|
||||
let (account_name, mut toml_account_config) = match account_name {
|
||||
Some("default") | Some("") | None => self
|
||||
.accounts
|
||||
|
@ -200,6 +194,18 @@ impl TomlConfig {
|
|||
.replace_undefined_keyring_entries(&account_name);
|
||||
}
|
||||
|
||||
Ok((account_name, toml_account_config))
|
||||
}
|
||||
|
||||
/// Build account configurations from a given account name.
|
||||
pub fn into_account_configs(
|
||||
self,
|
||||
account_name: Option<&str>,
|
||||
disable_cache: bool,
|
||||
) -> Result<(TomlAccountConfig, AccountConfig)> {
|
||||
let (account_name, mut toml_account_config) =
|
||||
self.into_toml_account_config(account_name)?;
|
||||
|
||||
if let Some(true) = toml_account_config.sync {
|
||||
if !disable_cache {
|
||||
toml_account_config.backend = Some(BackendKind::MaildirForSync);
|
||||
|
@ -267,6 +273,15 @@ impl TomlConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parse a configuration file path as [`PathBuf`].
|
||||
///
|
||||
/// The path is shell-expanded then canonicalized (if applicable).
|
||||
pub fn path_parser(path: &str) -> Result<PathBuf, String> {
|
||||
expand::try_path(path)
|
||||
.map(canonicalize::path)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use email::{
|
||||
|
|
|
@ -121,11 +121,6 @@ pub fn all_arg(help: &'static str) -> Arg {
|
|||
.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)
|
||||
|
@ -141,14 +136,6 @@ pub fn include_arg(help: &'static str) -> Arg {
|
|||
.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)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
pub mod account;
|
||||
pub mod backend;
|
||||
pub mod cache;
|
||||
pub mod cli;
|
||||
pub mod completion;
|
||||
pub mod config;
|
||||
pub mod email;
|
||||
|
|
667
src/main.rs
667
src/main.rs
|
@ -1,361 +1,7 @@
|
|||
use ::email::account::{config::DEFAULT_INBOX_FOLDER, sync::AccountSyncBuilder};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::{Command, CommandFactory, Parser, Subcommand};
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use env_logger::{Builder as LoggerBuilder, Env, DEFAULT_FILTER_ENV};
|
||||
use log::{debug, warn};
|
||||
use std::env;
|
||||
use url::Url;
|
||||
|
||||
use himalaya::{
|
||||
account,
|
||||
backend::{Backend, BackendBuilder},
|
||||
cache, completion,
|
||||
config::{self, TomlConfig},
|
||||
envelope, flag, folder, man, message, output,
|
||||
printer::StdoutPrinter,
|
||||
template,
|
||||
};
|
||||
|
||||
fn _create_app() -> Command {
|
||||
Command::new(env!("CARGO_PKG_NAME"))
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about(env!("CARGO_PKG_DESCRIPTION"))
|
||||
.author(env!("CARGO_PKG_AUTHORS"))
|
||||
.propagate_version(true)
|
||||
.infer_subcommands(true)
|
||||
.args(config::args::global_args())
|
||||
.args(account::args::global_args())
|
||||
.args(folder::args::global_args())
|
||||
.args(cache::args::global_args())
|
||||
.args(output::args::global_args())
|
||||
.subcommand(account::args::subcmd())
|
||||
.subcommand(folder::args::subcmd())
|
||||
.subcommand(envelope::args::subcmd())
|
||||
.subcommand(flag::args::subcmd())
|
||||
.subcommand(message::args::subcmd())
|
||||
.subcommand(template::args::subcmd())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn _old_main() -> Result<()> {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Err((_, err)) = coredump::register_panic_handler() {
|
||||
warn!("cannot register custom panic handler: {err}");
|
||||
debug!("cannot register custom panic handler: {err:?}");
|
||||
}
|
||||
|
||||
let default_env_filter = env_logger::DEFAULT_FILTER_ENV;
|
||||
env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off"));
|
||||
|
||||
// check mailto command before app initialization
|
||||
let raw_args: Vec<String> = env::args().collect();
|
||||
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
|
||||
let url = Url::parse(&raw_args[1])?;
|
||||
let (toml_account_config, account_config) = TomlConfig::from_default_paths()
|
||||
.await?
|
||||
.into_account_configs(None, false)?;
|
||||
let backend_builder =
|
||||
BackendBuilder::new(toml_account_config, account_config.clone(), true).await?;
|
||||
let backend = backend_builder.build().await?;
|
||||
let mut printer = StdoutPrinter::default();
|
||||
|
||||
return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await;
|
||||
}
|
||||
|
||||
let app = _create_app();
|
||||
let m = app.get_matches();
|
||||
|
||||
let some_config_path = config::args::parse_global_arg(&m);
|
||||
let some_account_name = account::args::parse_global_arg(&m);
|
||||
let disable_cache = cache::args::parse_disable_cache_arg(&m);
|
||||
let folder = folder::args::parse_global_source_arg(&m);
|
||||
|
||||
let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?;
|
||||
|
||||
let mut printer = StdoutPrinter::try_from(&m)?;
|
||||
|
||||
// FIXME
|
||||
// #[cfg(feature = "imap")]
|
||||
// if let BackendConfig::Imap(imap_config) = &account_config.backend {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// match imap::args::matches(&m)? {
|
||||
// Some(imap::args::Cmd::Notify(keepalive)) => {
|
||||
// let backend =
|
||||
// ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?;
|
||||
// imap::handlers::notify(&mut backend, &folder, keepalive).await?;
|
||||
// return Ok(());
|
||||
// }
|
||||
// Some(imap::args::Cmd::Watch(keepalive)) => {
|
||||
// let backend =
|
||||
// ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?;
|
||||
// imap::handlers::watch(&mut backend, &folder, keepalive).await?;
|
||||
// return Ok(());
|
||||
// }
|
||||
// _ => (),
|
||||
// }
|
||||
// }
|
||||
|
||||
match account::args::matches(&m)? {
|
||||
Some(account::args::Cmd::List(max_width)) => {
|
||||
let (_, account_config) = toml_config
|
||||
.clone()
|
||||
.into_account_configs(some_account_name, disable_cache)?;
|
||||
return account::handlers::list(max_width, &account_config, &toml_config, &mut printer);
|
||||
}
|
||||
Some(account::args::Cmd::Sync(strategy, dry_run)) => {
|
||||
let (toml_account_config, account_config) = toml_config
|
||||
.clone()
|
||||
.into_account_configs(some_account_name, true)?;
|
||||
let backend_builder =
|
||||
BackendBuilder::new(toml_account_config, account_config.clone(), false).await?;
|
||||
let sync_builder = AccountSyncBuilder::new(backend_builder.into())
|
||||
.await?
|
||||
.with_some_folders_strategy(strategy)
|
||||
.with_dry_run(dry_run);
|
||||
return account::handlers::sync(&mut printer, sync_builder, dry_run).await;
|
||||
}
|
||||
Some(account::args::Cmd::Configure(reset)) => {
|
||||
let (toml_account_config, _) = toml_config
|
||||
.clone()
|
||||
.into_account_configs(some_account_name, disable_cache)?;
|
||||
return account::handlers::configure(&toml_account_config, reset).await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let (toml_account_config, account_config) = toml_config
|
||||
.clone()
|
||||
.into_account_configs(some_account_name, disable_cache)?;
|
||||
|
||||
// checks folder commands
|
||||
match folder::args::matches(&m)? {
|
||||
Some(folder::args::Cmd::Create) => {
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
let folder = folder
|
||||
.ok_or_else(|| anyhow!("the folder argument is missing"))
|
||||
.context("cannot create folder")?;
|
||||
return folder::handlers::create(&mut printer, &backend, &folder).await;
|
||||
}
|
||||
Some(folder::args::Cmd::List(max_width)) => {
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return folder::handlers::list(&account_config, &mut printer, &backend, max_width)
|
||||
.await;
|
||||
}
|
||||
Some(folder::args::Cmd::Expunge) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return folder::handlers::expunge(&mut printer, &backend, &folder).await;
|
||||
}
|
||||
Some(folder::args::Cmd::Delete) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return folder::handlers::delete(&mut printer, &backend, &folder).await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match envelope::args::matches(&m)? {
|
||||
Some(envelope::args::Cmd::List(max_width, page_size, page)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return envelope::handlers::list(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
max_width,
|
||||
page_size,
|
||||
page,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match flag::args::matches(&m)? {
|
||||
Some(flag::args::Cmd::Set(ids, ref flags)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await;
|
||||
}
|
||||
Some(flag::args::Cmd::Add(ids, ref flags)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await;
|
||||
}
|
||||
Some(flag::args::Cmd::Remove(ids, ref flags)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match message::args::matches(&m)? {
|
||||
Some(message::args::Cmd::Attachments(ids)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return message::handlers::attachments(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
ids,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(message::args::Cmd::Copy(ids, to_folder)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return message::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await;
|
||||
}
|
||||
Some(message::args::Cmd::Delete(ids)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return message::handlers::delete(&mut printer, &backend, &folder, ids).await;
|
||||
}
|
||||
Some(message::args::Cmd::Forward(id, headers, body)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
return message::handlers::forward(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
id,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(message::args::Cmd::Move(ids, to_folder)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return message::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await;
|
||||
}
|
||||
Some(message::args::Cmd::Read(ids, text_mime, raw, headers)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return message::handlers::read(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
ids,
|
||||
text_mime,
|
||||
raw,
|
||||
headers,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(message::args::Cmd::Reply(id, all, headers, body)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
return message::handlers::reply(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
id,
|
||||
all,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(message::args::Cmd::Save(raw_email)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return message::handlers::save(&mut printer, &backend, &folder, raw_email).await;
|
||||
}
|
||||
Some(message::args::Cmd::Send(raw_email)) => {
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
return message::handlers::send(&account_config, &mut printer, &backend, raw_email)
|
||||
.await;
|
||||
}
|
||||
Some(message::args::Cmd::Write(headers, body)) => {
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
return message::handlers::write(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match template::args::matches(&m)? {
|
||||
Some(template::args::Cmd::Forward(id, headers, body)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return template::handlers::forward(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
id,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(template::args::Cmd::Write(headers, body)) => {
|
||||
return template::handlers::write(&account_config, &mut printer, headers, body).await;
|
||||
}
|
||||
Some(template::args::Cmd::Reply(id, all, headers, body)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return template::handlers::reply(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
id,
|
||||
all,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(template::args::Cmd::Save(template)) => {
|
||||
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
return template::handlers::save(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
&backend,
|
||||
&folder,
|
||||
template,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(template::args::Cmd::Send(template)) => {
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
return template::handlers::send(&account_config, &mut printer, &backend, template)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name= "himalaya", author, version, about, long_about = None, propagate_version = true)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
Man(man::command::Generate),
|
||||
#[command(aliases = ["completions", "compl", "comp"])]
|
||||
Completion(completion::command::Generate),
|
||||
}
|
||||
use himalaya::{cli::Cli, config::TomlConfig, printer::StdoutPrinter};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
|
@ -364,12 +10,305 @@ async fn main() -> Result<()> {
|
|||
.format_timestamp(None)
|
||||
.init();
|
||||
|
||||
let mut printer = StdoutPrinter::default();
|
||||
let cli = Cli::parse();
|
||||
|
||||
match Cli::parse().command {
|
||||
Commands::Man(cmd) => man::handler::generate(&mut printer, Cli::command(), cmd.dir),
|
||||
Commands::Completion(cmd) => {
|
||||
completion::handler::generate(&mut printer, Cli::command(), cmd.shell)
|
||||
}
|
||||
}
|
||||
let mut printer = StdoutPrinter::new(cli.output, cli.color);
|
||||
let config = TomlConfig::from_some_path_or_default(cli.config.as_ref()).await?;
|
||||
|
||||
cli.command.execute(&mut printer, &config).await
|
||||
}
|
||||
|
||||
// fn create_app() -> clap::Command {
|
||||
// clap::Command::new(env!("CARGO_PKG_NAME"))
|
||||
// .version(env!("CARGO_PKG_VERSION"))
|
||||
// .about(env!("CARGO_PKG_DESCRIPTION"))
|
||||
// .author(env!("CARGO_PKG_AUTHORS"))
|
||||
// .propagate_version(true)
|
||||
// .infer_subcommands(true)
|
||||
// .args(folder::args::global_args())
|
||||
// .args(cache::args::global_args())
|
||||
// .args(output::args::global_args())
|
||||
// .subcommand(folder::args::subcmd())
|
||||
// .subcommand(envelope::args::subcmd())
|
||||
// .subcommand(flag::args::subcmd())
|
||||
// .subcommand(message::args::subcmd())
|
||||
// .subcommand(template::args::subcmd())
|
||||
// }
|
||||
|
||||
// #[tokio::main]
|
||||
// async fn main() -> Result<()> {
|
||||
// #[cfg(not(target_os = "windows"))]
|
||||
// if let Err((_, err)) = coredump::register_panic_handler() {
|
||||
// warn!("cannot register custom panic handler: {err}");
|
||||
// debug!("cannot register custom panic handler: {err:?}");
|
||||
// }
|
||||
|
||||
// let default_env_filter = env_logger::DEFAULT_FILTER_ENV;
|
||||
// env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off"));
|
||||
|
||||
// // check mailto command before app initialization
|
||||
// let raw_args: Vec<String> = env::args().collect();
|
||||
// if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
|
||||
// let url = Url::parse(&raw_args[1])?;
|
||||
// let (toml_account_config, account_config) = TomlConfig::from_default_paths()
|
||||
// .await?
|
||||
// .into_account_configs(None, false)?;
|
||||
// let backend_builder =
|
||||
// BackendBuilder::new(toml_account_config, account_config.clone(), true).await?;
|
||||
// let backend = backend_builder.build().await?;
|
||||
// let mut printer = StdoutPrinter::default();
|
||||
|
||||
// return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await;
|
||||
// }
|
||||
|
||||
// let app = _create_app();
|
||||
// let m = app.get_matches();
|
||||
|
||||
// let some_config_path = config::args::parse_global_arg(&m);
|
||||
// let some_account_name = account::command::parse_global_arg(&m);
|
||||
// let disable_cache = cache::args::parse_disable_cache_arg(&m);
|
||||
// let folder = folder::args::parse_global_source_arg(&m);
|
||||
|
||||
// let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?;
|
||||
|
||||
// let mut printer = StdoutPrinter::try_from(&m)?;
|
||||
|
||||
// #[cfg(feature = "imap")]
|
||||
// if let BackendConfig::Imap(imap_config) = &account_config.backend {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// match imap::args::matches(&m)? {
|
||||
// Some(imap::args::Cmd::Notify(keepalive)) => {
|
||||
// let backend =
|
||||
// ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?;
|
||||
// imap::handlers::notify(&mut backend, &folder, keepalive).await?;
|
||||
// return Ok(());
|
||||
// }
|
||||
// Some(imap::args::Cmd::Watch(keepalive)) => {
|
||||
// let backend =
|
||||
// ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?;
|
||||
// imap::handlers::watch(&mut backend, &folder, keepalive).await?;
|
||||
// return Ok(());
|
||||
// }
|
||||
// _ => (),
|
||||
// }
|
||||
// }
|
||||
|
||||
// let (toml_account_config, account_config) = toml_config
|
||||
// .clone()
|
||||
// .into_account_configs(some_account_name, disable_cache)?;
|
||||
|
||||
// // checks folder commands
|
||||
// match folder::args::matches(&m)? {
|
||||
// Some(folder::args::Cmd::Create) => {
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// let folder = folder
|
||||
// .ok_or_else(|| anyhow!("the folder argument is missing"))
|
||||
// .context("cannot create folder")?;
|
||||
// return folder::handlers::create(&mut printer, &backend, &folder).await;
|
||||
// }
|
||||
// Some(folder::args::Cmd::List(max_width)) => {
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return folder::handlers::list(&account_config, &mut printer, &backend, max_width)
|
||||
// .await;
|
||||
// }
|
||||
// Some(folder::args::Cmd::Expunge) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return folder::handlers::expunge(&mut printer, &backend, &folder).await;
|
||||
// }
|
||||
// Some(folder::args::Cmd::Delete) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return folder::handlers::delete(&mut printer, &backend, &folder).await;
|
||||
// }
|
||||
// _ => (),
|
||||
// }
|
||||
|
||||
// match envelope::args::matches(&m)? {
|
||||
// Some(envelope::args::Cmd::List(max_width, page_size, page)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return envelope::handlers::list(
|
||||
// &account_config,
|
||||
// &mut printer,
|
||||
// &backend,
|
||||
// &folder,
|
||||
// max_width,
|
||||
// page_size,
|
||||
// page,
|
||||
// )
|
||||
// .await;
|
||||
// }
|
||||
// _ => (),
|
||||
// }
|
||||
|
||||
// match flag::args::matches(&m)? {
|
||||
// Some(flag::args::Cmd::Set(ids, ref flags)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await;
|
||||
// }
|
||||
// Some(flag::args::Cmd::Add(ids, ref flags)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await;
|
||||
// }
|
||||
// Some(flag::args::Cmd::Remove(ids, ref flags)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await;
|
||||
// }
|
||||
// _ => (),
|
||||
// }
|
||||
|
||||
// match message::args::matches(&m)? {
|
||||
// Some(message::args::Cmd::Attachments(ids)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return message::handlers::attachments(
|
||||
// &account_config,
|
||||
// &mut printer,
|
||||
// &backend,
|
||||
// &folder,
|
||||
// ids,
|
||||
// )
|
||||
// .await;
|
||||
// }
|
||||
// Some(message::args::Cmd::Copy(ids, to_folder)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return message::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await;
|
||||
// }
|
||||
// Some(message::args::Cmd::Delete(ids)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return message::handlers::delete(&mut printer, &backend, &folder, ids).await;
|
||||
// }
|
||||
// Some(message::args::Cmd::Forward(id, headers, body)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
// return message::handlers::forward(
|
||||
// &account_config,
|
||||
// &mut printer,
|
||||
// &backend,
|
||||
// &folder,
|
||||
// id,
|
||||
// headers,
|
||||
// body,
|
||||
// )
|
||||
// .await;
|
||||
// }
|
||||
// Some(message::args::Cmd::Move(ids, to_folder)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return message::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await;
|
||||
// }
|
||||
// Some(message::args::Cmd::Read(ids, text_mime, raw, headers)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return message::handlers::read(
|
||||
// &account_config,
|
||||
// &mut printer,
|
||||
// &backend,
|
||||
// &folder,
|
||||
// ids,
|
||||
// text_mime,
|
||||
// raw,
|
||||
// headers,
|
||||
// )
|
||||
// .await;
|
||||
// }
|
||||
// Some(message::args::Cmd::Reply(id, all, headers, body)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
// return message::handlers::reply(
|
||||
// &account_config,
|
||||
// &mut printer,
|
||||
// &backend,
|
||||
// &folder,
|
||||
// id,
|
||||
// all,
|
||||
// headers,
|
||||
// body,
|
||||
// )
|
||||
// .await;
|
||||
// }
|
||||
// Some(message::args::Cmd::Save(raw_email)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return message::handlers::save(&mut printer, &backend, &folder, raw_email).await;
|
||||
// }
|
||||
// Some(message::args::Cmd::Send(raw_email)) => {
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
// return message::handlers::send(&account_config, &mut printer, &backend, raw_email)
|
||||
// .await;
|
||||
// }
|
||||
// Some(message::args::Cmd::Write(headers, body)) => {
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
// return message::handlers::write(
|
||||
// &account_config,
|
||||
// &mut printer,
|
||||
// &backend,
|
||||
// headers,
|
||||
// body,
|
||||
// )
|
||||
// .await;
|
||||
// }
|
||||
// _ => (),
|
||||
// }
|
||||
|
||||
// match template::args::matches(&m)? {
|
||||
// Some(template::args::Cmd::Forward(id, headers, body)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return template::handlers::forward(
|
||||
// &account_config,
|
||||
// &mut printer,
|
||||
// &backend,
|
||||
// &folder,
|
||||
// id,
|
||||
// headers,
|
||||
// body,
|
||||
// )
|
||||
// .await;
|
||||
// }
|
||||
// Some(template::args::Cmd::Write(headers, body)) => {
|
||||
// return template::handlers::write(&account_config, &mut printer, headers, body).await;
|
||||
// }
|
||||
// Some(template::args::Cmd::Reply(id, all, headers, body)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return template::handlers::reply(
|
||||
// &account_config,
|
||||
// &mut printer,
|
||||
// &backend,
|
||||
// &folder,
|
||||
// id,
|
||||
// all,
|
||||
// headers,
|
||||
// body,
|
||||
// )
|
||||
// .await;
|
||||
// }
|
||||
// Some(template::args::Cmd::Save(template)) => {
|
||||
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
// return template::handlers::save(
|
||||
// &account_config,
|
||||
// &mut printer,
|
||||
// &backend,
|
||||
// &folder,
|
||||
// template,
|
||||
// )
|
||||
// .await;
|
||||
// }
|
||||
// Some(template::args::Cmd::Send(template)) => {
|
||||
// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
// return template::handlers::send(&account_config, &mut printer, &backend, template)
|
||||
// .await;
|
||||
// }
|
||||
// _ => (),
|
||||
// }
|
||||
|
||||
// Ok(())
|
||||
// }
|
||||
|
|
|
@ -1,16 +1,58 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use clap_mangen::Man;
|
||||
use log::info;
|
||||
use shellexpand_utils::{canonicalize, expand};
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::{cli::Cli, printer::Printer};
|
||||
|
||||
/// Generate all man pages to the given directory
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Generate {
|
||||
pub struct Command {
|
||||
/// Directory where man files should be generated in
|
||||
#[arg(value_parser = dir_parser)]
|
||||
pub dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub async fn execute(self, printer: &mut impl Printer) -> Result<()> {
|
||||
info!("executing man generate command");
|
||||
|
||||
let cmd = Cli::command();
|
||||
let cmd_name = cmd.get_name().to_string();
|
||||
let subcmds = cmd.get_subcommands().cloned().collect::<Vec<_>>();
|
||||
let subcmds_len = subcmds.len() + 1;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
Man::new(cmd).render(&mut buffer)?;
|
||||
|
||||
fs::create_dir_all(&self.dir)?;
|
||||
printer.print_log(format!("Generating man page for command {cmd_name}…"))?;
|
||||
fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?;
|
||||
|
||||
for subcmd in subcmds {
|
||||
let subcmd_name = subcmd.get_name().to_string();
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
Man::new(subcmd).render(&mut buffer)?;
|
||||
|
||||
printer.print_log(format!("Generating man page for subcommand {subcmd_name}…"))?;
|
||||
fs::write(
|
||||
self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)),
|
||||
buffer,
|
||||
)?;
|
||||
}
|
||||
|
||||
printer.print(format!(
|
||||
"{subcmds_len} man page(s) successfully generated in {:?}!",
|
||||
self.dir
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the given [`str`] as [`PathBuf`].
|
||||
///
|
||||
/// The path is first shell expanded, then canonicalized (if
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use clap::Command;
|
||||
use clap_mangen::Man;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::printer::Printer;
|
||||
|
||||
pub fn generate(printer: &mut impl Printer, cmd: Command, dir: PathBuf) -> Result<()> {
|
||||
let cmd_name = cmd.get_name().to_string();
|
||||
let subcmds = cmd.get_subcommands().cloned().collect::<Vec<_>>();
|
||||
let subcmds_len = subcmds.len() + 1;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
Man::new(cmd).render(&mut buffer)?;
|
||||
|
||||
fs::create_dir_all(&dir)?;
|
||||
printer.print_log(format!("Generating man page for command {cmd_name}…"))?;
|
||||
fs::write(dir.join(format!("{}.1", cmd_name)), buffer)?;
|
||||
|
||||
for subcmd in subcmds {
|
||||
let subcmd_name = subcmd.get_name().to_string();
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
Man::new(subcmd).render(&mut buffer)?;
|
||||
|
||||
printer.print_log(format!("Generating man page for subcommand {subcmd_name}…"))?;
|
||||
fs::write(dir.join(format!("{}-{}.1", cmd_name, subcmd_name)), buffer)?;
|
||||
}
|
||||
|
||||
printer.print(format!(
|
||||
"Successfully generated {subcmds_len} man page(s) in {dir:?}!"
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,2 +1 @@
|
|||
pub mod command;
|
||||
pub mod handler;
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
use anyhow::{anyhow, Error, Result};
|
||||
use atty::Stream;
|
||||
use clap::ValueEnum;
|
||||
use serde::Serialize;
|
||||
use std::{fmt, str::FromStr};
|
||||
use termcolor::ColorChoice;
|
||||
|
||||
/// Represents the available output formats.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
|
||||
pub enum OutputFmt {
|
||||
#[default]
|
||||
Plain,
|
||||
Json,
|
||||
}
|
||||
|
||||
impl Default for OutputFmt {
|
||||
fn default() -> Self {
|
||||
Self::Plain
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for OutputFmt {
|
||||
type Err = Error;
|
||||
|
||||
|
@ -52,20 +48,15 @@ impl<T: Serialize> OutputJson<T> {
|
|||
}
|
||||
|
||||
/// Represent the available color configs.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, ValueEnum)]
|
||||
pub enum ColorFmt {
|
||||
Never,
|
||||
Always,
|
||||
Ansi,
|
||||
#[default]
|
||||
Auto,
|
||||
}
|
||||
|
||||
impl Default for ColorFmt {
|
||||
fn default() -> Self {
|
||||
Self::Auto
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ColorFmt {
|
||||
type Err = Error;
|
||||
|
||||
|
|
Loading…
Reference in a new issue