From 2c33dd2f9fef8a805824e5259b4f002e2b78fc7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 6 Dec 2023 23:12:06 +0100 Subject: [PATCH] refactor envelope with clap derive api --- src/cli.rs | 6 + src/email/envelope/args.rs | 91 ----------- src/email/envelope/command/list.rs | 68 ++++++++ src/email/envelope/command/mod.rs | 24 +++ src/email/envelope/handlers.rs | 32 ---- src/email/envelope/mod.rs | 3 +- src/email/message/args.rs | 10 +- src/folder/arg/name.rs | 9 ++ src/folder/args.rs | 241 ---------------------------- src/folder/command/mod.rs | 191 ++++++++++++++++++++++ src/folder/handlers.rs | 247 ----------------------------- src/folder/mod.rs | 2 - src/ui/table/args.rs | 21 --- src/ui/table/mod.rs | 1 - 14 files changed, 304 insertions(+), 642 deletions(-) delete mode 100644 src/email/envelope/args.rs create mode 100644 src/email/envelope/command/list.rs create mode 100644 src/email/envelope/command/mod.rs delete mode 100644 src/email/envelope/handlers.rs delete mode 100644 src/folder/args.rs delete mode 100644 src/folder/handlers.rs delete mode 100644 src/ui/table/args.rs diff --git a/src/cli.rs b/src/cli.rs index 2b86351..0670587 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,6 +6,7 @@ use crate::{ account::command::AccountSubcommand, completion::command::CompletionGenerateCommand, config::{self, TomlConfig}, + envelope::command::EnvelopeSubcommand, folder::command::FolderSubcommand, manual::command::ManualGenerateCommand, output::{ColorFmt, OutputFmt}, @@ -96,6 +97,10 @@ pub enum HimalayaCommand { #[command(subcommand)] Folder(FolderSubcommand), + /// Subcommand to manage envelopes + #[command(subcommand)] + Envelope(EnvelopeSubcommand), + /// Generate manual pages to a directory #[command(arg_required_else_help = true)] Manual(ManualGenerateCommand), @@ -110,6 +115,7 @@ impl HimalayaCommand { match self { Self::Account(cmd) => cmd.execute(printer, config).await, Self::Folder(cmd) => cmd.execute(printer, config).await, + Self::Envelope(cmd) => cmd.execute(printer, config).await, Self::Manual(cmd) => cmd.execute(printer).await, Self::Completion(cmd) => cmd.execute(printer).await, } diff --git a/src/email/envelope/args.rs b/src/email/envelope/args.rs deleted file mode 100644 index 8a7e8ce..0000000 --- a/src/email/envelope/args.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Email CLI module. -//! -//! This module contains the command matcher, the subcommands and the -//! arguments related to the email domain. - -use anyhow::Result; -use clap::{Arg, ArgMatches, Command}; - -use crate::ui::table; - -const ARG_PAGE: &str = "page"; -const ARG_PAGE_SIZE: &str = "page-size"; -const CMD_LIST: &str = "list"; -const CMD_ENVELOPE: &str = "envelope"; - -pub type Page = usize; -pub type PageSize = usize; - -/// Represents the email commands. -#[derive(Debug, PartialEq, Eq)] -pub enum Cmd { - List(table::args::MaxTableWidth, Option, Page), -} - -/// Email command matcher. -pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_ENVELOPE) { - if let Some(m) = m.subcommand_matches(CMD_LIST) { - let max_table_width = table::args::parse_max_width(m); - let page_size = parse_page_size_arg(m); - let page = parse_page_arg(m); - Some(Cmd::List(max_table_width, page_size, page)) - } else { - Some(Cmd::List(None, None, 0)) - } - } else { - None - }; - - Ok(cmd) -} - -/// Represents the envelope subcommand. -pub fn subcmd() -> Command { - Command::new(CMD_ENVELOPE) - .about("Subcommand to manage envelopes") - .long_about("Subcommand to manage envelopes like list") - .subcommands([Command::new(CMD_LIST) - .alias("lst") - .about("List envelopes") - .arg(page_size_arg()) - .arg(page_arg()) - .arg(table::args::max_width())]) -} - -/// Represents the page size argument. -fn page_size_arg() -> Arg { - Arg::new(ARG_PAGE_SIZE) - .help("Page size") - .long("page-size") - .short('s') - .value_name("INT") -} - -/// Represents the page size argument parser. -fn parse_page_size_arg(matches: &ArgMatches) -> Option { - matches - .get_one::(ARG_PAGE_SIZE) - .and_then(|s| s.parse().ok()) -} - -/// Represents the page argument. -fn page_arg() -> Arg { - Arg::new(ARG_PAGE) - .help("Page number") - .short('p') - .long("page") - .value_name("INT") - .default_value("1") -} - -/// Represents the page argument parser. -fn parse_page_arg(matches: &ArgMatches) -> usize { - matches - .get_one::(ARG_PAGE) - .unwrap() - .parse() - .ok() - .map(|page| 1.max(page) - 1) - .unwrap_or_default() -} diff --git a/src/email/envelope/command/list.rs b/src/email/envelope/command/list.rs new file mode 100644 index 0000000..6e9ba1b --- /dev/null +++ b/src/email/envelope/command/list.rs @@ -0,0 +1,68 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + folder::arg::name::FolderNameOptionalArg, + printer::{PrintTableOpts, Printer}, + ui::arg::max_width::MaxTableWidthFlag, +}; + +/// List all envelopes from a folder +#[derive(Debug, Parser)] +pub struct EnvelopeListCommand { + #[command(flatten)] + pub folder: FolderNameOptionalArg, + + /// The page number + #[arg(long, short, value_name = "NUMBER", default_value = "1")] + pub page: usize, + + /// The page size + #[arg(long, short = 's', value_name = "NUMBER")] + pub page_size: Option, + + #[command(flatten)] + pub table: MaxTableWidthFlag, + + #[command(flatten)] + pub account: AccountNameFlag, + + #[command(flatten)] + pub cache: DisableCacheFlag, +} + +impl EnvelopeListCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing envelope list command"); + + let folder = &self.folder.name; + + let some_account_name = self.account.name.as_ref().map(String::as_str); + let (toml_account_config, account_config) = config + .clone() + .into_account_configs(some_account_name, self.cache.disable)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let page_size = self + .page_size + .unwrap_or(account_config.email_listing_page_size()); + let page = 1.max(self.page) - 1; + + let envelopes = backend.list_envelopes(folder, page_size, page).await?; + + printer.print_table( + Box::new(envelopes), + PrintTableOpts { + format: &account_config.email_reading_format, + max_width: self.table.max_width, + }, + )?; + + Ok(()) + } +} diff --git a/src/email/envelope/command/mod.rs b/src/email/envelope/command/mod.rs new file mode 100644 index 0000000..8bda915 --- /dev/null +++ b/src/email/envelope/command/mod.rs @@ -0,0 +1,24 @@ +pub mod list; + +use anyhow::Result; +use clap::Subcommand; + +use crate::{config::TomlConfig, printer::Printer}; + +use self::list::EnvelopeListCommand; + +/// Subcommand to manage envelopes +#[derive(Debug, Subcommand)] +pub enum EnvelopeSubcommand { + /// List all envelopes from a folder + #[command(alias = "lst")] + List(EnvelopeListCommand), +} + +impl EnvelopeSubcommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + match self { + Self::List(cmd) => cmd.execute(printer, config).await, + } + } +} diff --git a/src/email/envelope/handlers.rs b/src/email/envelope/handlers.rs deleted file mode 100644 index 0477a66..0000000 --- a/src/email/envelope/handlers.rs +++ /dev/null @@ -1,32 +0,0 @@ -use anyhow::Result; -use email::account::config::AccountConfig; -use log::{debug, trace}; - -use crate::{ - backend::Backend, - printer::{PrintTableOpts, Printer}, -}; - -pub async fn list( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - max_width: Option, - page_size: Option, - page: usize, -) -> Result<()> { - let page_size = page_size.unwrap_or(config.email_listing_page_size()); - debug!("page size: {}", page_size); - - let envelopes = backend.list_envelopes(&folder, page_size, page).await?; - trace!("envelopes: {:?}", envelopes); - - printer.print_table( - Box::new(envelopes), - PrintTableOpts { - format: &config.email_reading_format, - max_width, - }, - ) -} diff --git a/src/email/envelope/mod.rs b/src/email/envelope/mod.rs index b43c520..9b025e7 100644 --- a/src/email/envelope/mod.rs +++ b/src/email/envelope/mod.rs @@ -1,7 +1,6 @@ -pub mod args; +pub mod command; pub mod config; pub mod flag; -pub mod handlers; use anyhow::Result; use email::account::config::AccountConfig; diff --git a/src/email/message/args.rs b/src/email/message/args.rs index 65448c0..80dbb43 100644 --- a/src/email/message/args.rs +++ b/src/email/message/args.rs @@ -6,7 +6,7 @@ use anyhow::Result; use clap::{Arg, ArgAction, ArgMatches, Command}; -use crate::{folder, template}; +use crate::template; const ARG_CRITERIA: &str = "criterion"; const ARG_HEADERS: &str = "headers"; @@ -71,7 +71,7 @@ pub fn matches(m: &ArgMatches) -> Result> { Some(Cmd::Attachments(ids)) } else if let Some(m) = m.subcommand_matches(CMD_COPY) { let ids = parse_ids_arg(m); - let folder = folder::args::parse_target_arg(m); + let folder = "INBOX"; Some(Cmd::Copy(ids, folder)) } else if let Some(m) = m.subcommand_matches(CMD_DELETE) { let ids = parse_ids_arg(m); @@ -83,7 +83,7 @@ pub fn matches(m: &ArgMatches) -> Result> { Some(Cmd::Forward(id, headers, body)) } else if let Some(m) = m.subcommand_matches(CMD_MOVE) { let ids = parse_ids_arg(m); - let folder = folder::args::parse_target_arg(m); + let folder = "INBOX"; Some(Cmd::Move(ids, folder)) } else if let Some(m) = m.subcommand_matches(CMD_READ) { let ids = parse_ids_arg(m); @@ -158,12 +158,12 @@ pub fn subcmd() -> Command { Command::new(CMD_COPY) .alias("cp") .about("Copy emails to the given folder") - .arg(folder::args::target_arg()) + // .arg(folder::args::target_arg()) .arg(ids_arg()), Command::new(CMD_MOVE) .alias("mv") .about("Move emails to the given folder") - .arg(folder::args::target_arg()) + // .arg(folder::args::target_arg()) .arg(ids_arg()), Command::new(CMD_DELETE) .aliases(["remove", "rm"]) diff --git a/src/folder/arg/name.rs b/src/folder/arg/name.rs index fe80733..9004fec 100644 --- a/src/folder/arg/name.rs +++ b/src/folder/arg/name.rs @@ -1,4 +1,5 @@ use clap::Parser; +use email::account::config::DEFAULT_INBOX_FOLDER; /// The folder name argument parser #[derive(Debug, Parser)] @@ -7,3 +8,11 @@ pub struct FolderNameArg { #[arg(name = "folder-name", value_name = "FOLDER")] pub name: String, } + +/// The optional folder name argument parser +#[derive(Debug, Parser)] +pub struct FolderNameOptionalArg { + /// The name of the folder + #[arg(name = "folder-name", value_name = "FOLDER", default_value = DEFAULT_INBOX_FOLDER)] + pub name: String, +} diff --git a/src/folder/args.rs b/src/folder/args.rs deleted file mode 100644 index f9d0c7e..0000000 --- a/src/folder/args.rs +++ /dev/null @@ -1,241 +0,0 @@ -//! Folder CLI module. -//! -//! 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, 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_GLOBAL_SOURCE: &str = "global-source"; -const ARG_SOURCE: &str = "source"; -const ARG_TARGET: &str = "target"; -const CMD_CREATE: &str = "create"; -const CMD_DELETE: &str = "delete"; -const CMD_EXPUNGE: &str = "expunge"; -const CMD_FOLDER: &str = "folder"; -const CMD_LIST: &str = "list"; - -/// Represents the folder commands. -#[derive(Debug, PartialEq, Eq)] -pub enum Cmd { - Create, - List(table::args::MaxTableWidth), - Expunge, - Delete, -} - -/// Represents the folder command matcher. -pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDER) { - if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) { - info!("expunge folder subcommand matched"); - Some(Cmd::Expunge) - } else if let Some(_) = m.subcommand_matches(CMD_CREATE) { - debug!("create folder command matched"); - Some(Cmd::Create) - } else if let Some(m) = m.subcommand_matches(CMD_LIST) { - debug!("list folders command matched"); - let max_table_width = table::args::parse_max_width(m); - Some(Cmd::List(max_table_width)) - } else if let Some(_) = m.subcommand_matches(CMD_DELETE) { - debug!("delete folder command matched"); - Some(Cmd::Delete) - } else { - info!("no folder subcommand matched, falling back to subcommand list"); - Some(Cmd::List(None)) - } - } else { - None - }; - - Ok(cmd) -} - -/// Represents the folder subcommand. -pub fn subcmd() -> Command { - Command::new(CMD_FOLDER) - .about("Subcommand to manage folders") - .long_about("Subcommand to manage folders like list, expunge or delete") - .subcommands([ - Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"), - Command::new(CMD_CREATE) - .aliases(["add", "new"]) - .about("Create a new folder"), - Command::new(CMD_LIST) - .about("List folders") - .arg(table::args::max_width()), - Command::new(CMD_DELETE) - .aliases(["remove", "rm"]) - .about("Delete a folder with all its emails"), - ]) -} - -/// Represents the source folder argument. -pub fn global_args() -> impl IntoIterator { - [Arg::new(ARG_GLOBAL_SOURCE) - .help("Override the default INBOX folder") - .long_help( - "Override the default INBOX folder. - -The given folder will be used by default for all other commands (when -applicable).", - ) - .long("folder") - .short('f') - .global(true) - .value_name("name")] -} - -pub fn parse_global_source_arg(matches: &ArgMatches) -> Option<&str> { - matches - .get_one::(ARG_GLOBAL_SOURCE) - .map(String::as_str) -} - -pub fn source_arg(help: &'static str) -> Arg { - Arg::new(ARG_SOURCE).help(help).value_name("name") -} - -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 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 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) - .help("Specifies the target folder") - .value_name("TARGET") - .required(true) -} - -/// Represents the target folder argument parser. -pub fn parse_target_arg(matches: &ArgMatches) -> &str { - matches.get_one::(ARG_TARGET).unwrap().as_str() -} - -#[cfg(test)] -mod tests { - use clap::{error::ErrorKind, Command}; - - use super::*; - - #[test] - fn it_should_match_cmds() { - let arg = Command::new("himalaya") - .subcommand(subcmd()) - .get_matches_from(&["himalaya", "folders"]); - assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap()); - - let arg = Command::new("himalaya") - .subcommand(subcmd()) - .get_matches_from(&["himalaya", "folders", "list", "--max-width", "20"]); - assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap()); - } - - #[test] - fn it_should_match_source_arg() { - macro_rules! get_matches_from { - ($($arg:expr),*) => { - Command::new("himalaya") - .arg(source_arg()) - .get_matches_from(&["himalaya", $($arg,)*]) - }; - } - - let app = get_matches_from![]; - assert_eq!(None, app.get_one::(ARG_SOURCE).map(String::as_str)); - - let app = get_matches_from!["-f", "SOURCE"]; - assert_eq!( - Some("SOURCE"), - app.get_one::(ARG_SOURCE).map(String::as_str) - ); - - let app = get_matches_from!["--folder", "SOURCE"]; - assert_eq!( - Some("SOURCE"), - app.get_one::(ARG_SOURCE).map(String::as_str) - ); - } - - #[test] - fn it_should_match_target_arg() { - macro_rules! get_matches_from { - ($($arg:expr),*) => { - Command::new("himalaya") - .arg(target_arg()) - .try_get_matches_from_mut(&["himalaya", $($arg,)*]) - }; - } - - let app = get_matches_from![]; - assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind()); - - let app = get_matches_from!["TARGET"]; - assert_eq!( - Some("TARGET"), - app.unwrap() - .get_one::(ARG_TARGET) - .map(String::as_str) - ); - } -} diff --git a/src/folder/command/mod.rs b/src/folder/command/mod.rs index e00b0f9..54868ae 100644 --- a/src/folder/command/mod.rs +++ b/src/folder/command/mod.rs @@ -49,3 +49,194 @@ impl FolderSubcommand { } } } + +#[cfg(test)] +mod tests { + use async_trait::async_trait; + use email::{ + account::config::AccountConfig, + backend::Backend, + envelope::{Envelope, Envelopes}, + flag::Flags, + folder::{Folder, Folders}, + message::Messages, + }; + use std::{any::Any, fmt::Debug, io}; + use termcolor::ColorSpec; + + use crate::printer::{Print, PrintTable, WriteColor}; + + use super::*; + + #[tokio::test] + async fn it_should_list_mboxes() { + #[derive(Debug, Default, Clone)] + struct StringWriter { + content: String, + } + + impl io::Write for StringWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + 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( + &mut self, + data: Box, + opts: PrintTableOpts, + ) -> anyhow::Result<()> { + data.print_table(&mut self.writer, opts)?; + Ok(()) + } + fn print_log(&mut self, _data: T) -> anyhow::Result<()> { + unimplemented!() + } + fn print( + &mut self, + _data: T, + ) -> anyhow::Result<()> { + unimplemented!() + } + fn is_json(&self) -> bool { + unimplemented!() + } + } + + struct TestBackend; + + #[async_trait] + impl Backend for TestBackend { + fn name(&self) -> String { + unimplemented!(); + } + async fn add_folder(&mut self, _: &str) -> email::Result<()> { + unimplemented!(); + } + async fn list_folders(&mut self) -> email::Result { + Ok(Folders::from_iter([ + Folder { + name: "INBOX".into(), + desc: "desc".into(), + }, + Folder { + name: "Sent".into(), + desc: "desc".into(), + }, + ])) + } + async fn expunge_folder(&mut self, _: &str) -> email::Result<()> { + unimplemented!(); + } + async fn purge_folder(&mut self, _: &str) -> email::Result<()> { + unimplemented!(); + } + async fn delete_folder(&mut self, _: &str) -> email::Result<()> { + unimplemented!(); + } + async fn get_envelope(&mut self, _: &str, _: &str) -> email::Result { + unimplemented!(); + } + async fn list_envelopes( + &mut self, + _: &str, + _: usize, + _: usize, + ) -> email::Result { + unimplemented!() + } + async fn search_envelopes( + &mut self, + _: &str, + _: &str, + _: &str, + _: usize, + _: usize, + ) -> email::Result { + unimplemented!() + } + async fn add_email(&mut self, _: &str, _: &[u8], _: &Flags) -> email::Result { + unimplemented!() + } + async fn get_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result { + unimplemented!() + } + async fn preview_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result { + unimplemented!() + } + async fn copy_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> { + unimplemented!() + } + async fn move_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> { + unimplemented!() + } + async fn delete_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<()> { + unimplemented!() + } + async fn add_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> { + unimplemented!() + } + async fn set_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> { + unimplemented!() + } + async fn remove_flags( + &mut self, + _: &str, + _: Vec<&str>, + _: &Flags, + ) -> email::Result<()> { + unimplemented!() + } + fn as_any(&self) -> &dyn Any { + unimplemented!() + } + } + + let account_config = AccountConfig::default(); + let mut printer = PrinterServiceTest::default(); + let mut backend = TestBackend {}; + + assert!(list(&account_config, &mut printer, &mut backend, None) + .await + .is_ok()); + assert_eq!( + concat![ + "\n", + "NAME │DESC \n", + "INBOX │desc \n", + "Sent │desc \n", + "\n" + ], + printer.writer.content + ); + } +} diff --git a/src/folder/handlers.rs b/src/folder/handlers.rs deleted file mode 100644 index e43d34a..0000000 --- a/src/folder/handlers.rs +++ /dev/null @@ -1,247 +0,0 @@ -//! Folder handling module. -//! -//! This module gathers all folder actions triggered by the CLI. - -use anyhow::Result; -use dialoguer::Confirm; -use email::account::config::AccountConfig; -use std::process; - -use crate::{ - backend::Backend, - printer::{PrintTableOpts, Printer}, -}; - -use super::Folders; - -pub async fn expunge(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> { - backend.expunge_folder(folder).await?; - printer.print(format!("Folder {folder} successfully expunged!")) -} - -pub async fn list( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - max_width: Option, -) -> Result<()> { - let folders: Folders = backend.list_folders().await?.into(); - printer.print_table( - // TODO: remove Box - Box::new(folders), - PrintTableOpts { - format: &config.email_reading_format, - max_width, - }, - ) -} - -pub async fn create(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> { - backend.add_folder(folder).await?; - printer.print("Folder successfully created!") -} - -pub async fn delete(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> { - if let Some(false) | None = Confirm::new() - .with_prompt(format!("Confirm deletion of folder {folder}?")) - .default(false) - .report(false) - .interact_opt()? - { - process::exit(0); - }; - - backend.delete_folder(folder).await?; - printer.print("Folder successfully deleted!") -} - -#[cfg(test)] -mod tests { - use async_trait::async_trait; - use email::{ - account::config::AccountConfig, - backend::Backend, - envelope::{Envelope, Envelopes}, - flag::Flags, - folder::{Folder, Folders}, - message::Messages, - }; - use std::{any::Any, fmt::Debug, io}; - use termcolor::ColorSpec; - - use crate::printer::{Print, PrintTable, WriteColor}; - - use super::*; - - #[tokio::test] - async fn it_should_list_mboxes() { - #[derive(Debug, Default, Clone)] - struct StringWriter { - content: String, - } - - impl io::Write for StringWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - 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( - &mut self, - data: Box, - opts: PrintTableOpts, - ) -> anyhow::Result<()> { - data.print_table(&mut self.writer, opts)?; - Ok(()) - } - fn print_log(&mut self, _data: T) -> anyhow::Result<()> { - unimplemented!() - } - fn print( - &mut self, - _data: T, - ) -> anyhow::Result<()> { - unimplemented!() - } - fn is_json(&self) -> bool { - unimplemented!() - } - } - - struct TestBackend; - - #[async_trait] - impl Backend for TestBackend { - fn name(&self) -> String { - unimplemented!(); - } - async fn add_folder(&mut self, _: &str) -> email::Result<()> { - unimplemented!(); - } - async fn list_folders(&mut self) -> email::Result { - Ok(Folders::from_iter([ - Folder { - name: "INBOX".into(), - desc: "desc".into(), - }, - Folder { - name: "Sent".into(), - desc: "desc".into(), - }, - ])) - } - async fn expunge_folder(&mut self, _: &str) -> email::Result<()> { - unimplemented!(); - } - async fn purge_folder(&mut self, _: &str) -> email::Result<()> { - unimplemented!(); - } - async fn delete_folder(&mut self, _: &str) -> email::Result<()> { - unimplemented!(); - } - async fn get_envelope(&mut self, _: &str, _: &str) -> email::Result { - unimplemented!(); - } - async fn list_envelopes( - &mut self, - _: &str, - _: usize, - _: usize, - ) -> email::Result { - unimplemented!() - } - async fn search_envelopes( - &mut self, - _: &str, - _: &str, - _: &str, - _: usize, - _: usize, - ) -> email::Result { - unimplemented!() - } - async fn add_email(&mut self, _: &str, _: &[u8], _: &Flags) -> email::Result { - unimplemented!() - } - async fn get_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result { - unimplemented!() - } - async fn preview_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result { - unimplemented!() - } - async fn copy_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> { - unimplemented!() - } - async fn move_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> { - unimplemented!() - } - async fn delete_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<()> { - unimplemented!() - } - async fn add_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> { - unimplemented!() - } - async fn set_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> { - unimplemented!() - } - async fn remove_flags( - &mut self, - _: &str, - _: Vec<&str>, - _: &Flags, - ) -> email::Result<()> { - unimplemented!() - } - fn as_any(&self) -> &dyn Any { - unimplemented!() - } - } - - let account_config = AccountConfig::default(); - let mut printer = PrinterServiceTest::default(); - let mut backend = TestBackend {}; - - assert!(list(&account_config, &mut printer, &mut backend, None) - .await - .is_ok()); - assert_eq!( - concat![ - "\n", - "NAME │DESC \n", - "INBOX │desc \n", - "Sent │desc \n", - "\n" - ], - printer.writer.content - ); - } -} diff --git a/src/folder/mod.rs b/src/folder/mod.rs index 67fd4c0..577abab 100644 --- a/src/folder/mod.rs +++ b/src/folder/mod.rs @@ -1,8 +1,6 @@ pub mod arg; -pub mod args; pub mod command; pub mod config; -pub mod handlers; use anyhow::Result; use serde::Serialize; diff --git a/src/ui/table/args.rs b/src/ui/table/args.rs deleted file mode 100644 index e29a8f0..0000000 --- a/src/ui/table/args.rs +++ /dev/null @@ -1,21 +0,0 @@ -use clap::{Arg, ArgMatches}; - -const ARG_MAX_TABLE_WIDTH: &str = "max-table-width"; - -pub(crate) type MaxTableWidth = Option; - -/// Represents the max table width argument. -pub fn max_width() -> Arg { - Arg::new(ARG_MAX_TABLE_WIDTH) - .help("Defines a maximum width for the table") - .long("max-width") - .short('w') - .value_name("INT") -} - -/// Represents the max table width argument parser. -pub fn parse_max_width(matches: &ArgMatches) -> Option { - matches - .get_one::(ARG_MAX_TABLE_WIDTH) - .and_then(|s| s.parse().ok()) -} diff --git a/src/ui/table/mod.rs b/src/ui/table/mod.rs index b07ca63..fb49b3c 100644 --- a/src/ui/table/mod.rs +++ b/src/ui/table/mod.rs @@ -1,5 +1,4 @@ pub mod arg; -pub mod args; pub mod table; pub use table::*;