From 1e448e56ebb85a7f0ce3feb27dedb0b0c8c03995 Mon Sep 17 00:00:00 2001 From: Perma Alesheikh Date: Mon, 6 May 2024 15:49:58 +0330 Subject: [PATCH] replace dialoguer with inquire In order to reduce our dependencies, we are replacing the dependencies that use console_rs with those that use crossterm. This commit will completely replace dialoguer with inquire. Signed-off-by: Perma Alesheikh --- Cargo.lock | 19 --- Cargo.toml | 1 - src/account/wizard.rs | 15 +- src/backend/mod.rs | 44 ++--- src/backend/wizard.rs | 24 ++- src/config/mod.rs | 16 +- src/config/wizard.rs | 41 ++--- src/folder/command/delete.rs | 11 +- src/folder/command/purge.rs | 11 +- src/imap/wizard.rs | 313 ++++++++++++++++------------------- src/maildir/wizard.rs | 20 ++- src/notmuch/wizard.rs | 31 ++-- src/sendmail/wizard.rs | 11 +- src/smtp/wizard.rs | 313 +++++++++++++++-------------------- src/ui/choice.rs | 74 +++++---- src/ui/mod.rs | 5 - src/ui/prompt.rs | 35 ++-- 17 files changed, 441 insertions(+), 543 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e195a3..c225c0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1217,18 +1217,6 @@ dependencies = [ "cipher 0.4.4", ] -[[package]] -name = "dialoguer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" -dependencies = [ - "console", - "shell-words", - "tempfile", - "zeroize", -] - [[package]] name = "digest" version = "0.10.7" @@ -2049,7 +2037,6 @@ dependencies = [ "clap_mangen", "color-eyre", "console", - "dialoguer", "dirs 4.0.0", "email-lib", "email_address", @@ -4297,12 +4284,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - [[package]] name = "shellexpand" version = "3.1.0" diff --git a/Cargo.toml b/Cargo.toml index fea7b4a..b678851 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,6 @@ clap_complete = "4.4" clap_mangen = "0.2" color-eyre = "0.6.3" console = "0.15.2" -dialoguer = "0.10.2" dirs = "4" email-lib = { version = "=0.24.1", default-features = false, features = ["derive", "tracing"] } email_address = "0.2.4" diff --git a/src/account/wizard.rs b/src/account/wizard.rs index a2176e3..296560a 100644 --- a/src/account/wizard.rs +++ b/src/account/wizard.rs @@ -2,13 +2,10 @@ use crate::account::config::SyncConfig; use color_eyre::{eyre::OptionExt, Result}; #[cfg(feature = "account-sync")] -use dialoguer::Confirm; use email_address::EmailAddress; use inquire::validator::{ErrorMessage, Validation}; use std::{path::PathBuf, str::FromStr}; -#[cfg(feature = "account-sync")] -use crate::wizard_prompt; #[cfg(feature = "account-discovery")] use crate::wizard_warn; use crate::{ @@ -144,13 +141,11 @@ pub(crate) async fn configure() -> Result> { #[cfg(feature = "account-sync")] { - let should_configure_sync = Confirm::new() - .with_prompt(wizard_prompt!( - "Do you need offline access for your account?" - )) - .default(false) - .interact_opt()? - .unwrap_or_default(); + let should_configure_sync = + inquire::Confirm::new("Do you need offline access for your account?") + .with_default(false) + .prompt_skippable()? + .unwrap_or_default(); if should_configure_sync { config.sync = Some(SyncConfig { diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 81e5bad..d5dab7c 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,9 +1,9 @@ pub mod config; pub(crate) mod wizard; -use color_eyre::Result; use async_trait::async_trait; -use std::{ops::Deref, sync::Arc}; +use color_eyre::Result; +use std::{fmt::Display, ops::Deref, sync::Arc}; #[cfg(feature = "imap")] use email::imap::{ImapContextBuilder, ImapContextSync}; @@ -70,30 +70,32 @@ pub enum BackendKind { Sendmail, } -impl ToString for BackendKind { - fn to_string(&self) -> String { - let kind = match self { - Self::None => "None", +impl Display for BackendKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::None => "None", - #[cfg(feature = "imap")] - Self::Imap => "IMAP", - #[cfg(all(feature = "imap", feature = "account-sync"))] - Self::ImapCache => "IMAP cache", + #[cfg(feature = "imap")] + Self::Imap => "IMAP", + #[cfg(all(feature = "imap", feature = "account-sync"))] + Self::ImapCache => "IMAP cache", - #[cfg(feature = "maildir")] - Self::Maildir => "Maildir", + #[cfg(feature = "maildir")] + Self::Maildir => "Maildir", - #[cfg(feature = "notmuch")] - Self::Notmuch => "Notmuch", + #[cfg(feature = "notmuch")] + Self::Notmuch => "Notmuch", - #[cfg(feature = "smtp")] - Self::Smtp => "SMTP", + #[cfg(feature = "smtp")] + Self::Smtp => "SMTP", - #[cfg(feature = "sendmail")] - Self::Sendmail => "Sendmail", - }; - - kind.to_string() + #[cfg(feature = "sendmail")] + Self::Sendmail => "Sendmail", + } + ) } } diff --git a/src/backend/wizard.rs b/src/backend/wizard.rs index ccd0583..b24dca1 100644 --- a/src/backend/wizard.rs +++ b/src/backend/wizard.rs @@ -1,7 +1,7 @@ use color_eyre::Result; -use dialoguer::Select; #[cfg(feature = "account-discovery")] use email::account::discover::config::AutoConfig; +use inquire::Select; #[cfg(feature = "imap")] use crate::imap; @@ -13,7 +13,6 @@ use crate::notmuch; use crate::sendmail; #[cfg(feature = "smtp")] use crate::smtp; -use crate::ui::THEME; use super::{config::BackendConfig, BackendKind}; @@ -38,12 +37,9 @@ pub(crate) async fn configure( email: &str, #[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>, ) -> Result> { - let kind = Select::with_theme(&*THEME) - .with_prompt("Default email backend") - .items(DEFAULT_BACKEND_KINDS) - .default(0) - .interact_opt()? - .and_then(|idx| DEFAULT_BACKEND_KINDS.get(idx).map(Clone::clone)); + let kind = Select::new("Default email backend", DEFAULT_BACKEND_KINDS.to_vec()) + .with_starting_cursor(0) + .prompt_skippable()?; let config = match kind { #[cfg(feature = "imap")] @@ -71,12 +67,12 @@ pub(crate) async fn configure_sender( email: &str, #[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>, ) -> Result> { - let kind = Select::with_theme(&*THEME) - .with_prompt("Backend for sending messages") - .items(SEND_MESSAGE_BACKEND_KINDS) - .default(0) - .interact_opt()? - .and_then(|idx| SEND_MESSAGE_BACKEND_KINDS.get(idx).map(Clone::clone)); + let kind = Select::new( + "Backend for sending messages", + SEND_MESSAGE_BACKEND_KINDS.to_vec(), + ) + .with_starting_cursor(0) + .prompt_skippable()?; let config = match kind { #[cfg(feature = "smtp")] diff --git a/src/config/mod.rs b/src/config/mod.rs index 997147b..24ec9c4 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -18,7 +18,7 @@ use tracing::debug; #[cfg(feature = "account-sync")] use crate::backend::BackendKind; -use crate::{account::config::TomlAccountConfig, wizard_prompt, wizard_warn}; +use crate::{account::config::TomlAccountConfig, wizard_warn}; /// Represents the user config file. #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] @@ -46,8 +46,8 @@ impl TomlConfig { 1 => { let path = &paths[0]; - let ref content = fs::read_to_string(path) - .context(format!("cannot read config file at {path:?}"))?; + let content = &(fs::read_to_string(path) + .context(format!("cannot read config file at {path:?}"))?); toml::from_str(content).context(format!("cannot parse config file at {path:?}")) } @@ -85,17 +85,13 @@ impl TomlConfig { /// /// NOTE: the wizard can only be used with interactive shells. async fn from_wizard(path: &PathBuf) -> Result { - use dialoguer::Confirm; use std::process; wizard_warn!("Cannot find existing configuration at {path:?}."); - let confirm = Confirm::new() - .with_prompt(wizard_prompt!( - "Would you like to create one with the wizard?" - )) - .default(true) - .interact_opt()? + let confirm = inquire::Confirm::new("Would you like to create one with the wizard? ") + .with_default(true) + .prompt_skippable()? .unwrap_or_default(); if !confirm { diff --git a/src/config/wizard.rs b/src/config/wizard.rs index 79e59e3..bcc8f4f 100644 --- a/src/config/wizard.rs +++ b/src/config/wizard.rs @@ -1,10 +1,10 @@ use color_eyre::Result; -use dialoguer::{Confirm, Input, Select}; +use inquire::{Confirm, Select, Text}; use shellexpand_utils::expand; -use std::{fs, path::PathBuf, process}; +use std::{fs, path::Path, process}; use toml_edit::{DocumentMut, Item}; -use crate::{account, ui::THEME}; +use crate::account; use super::TomlConfig; @@ -31,7 +31,7 @@ macro_rules! wizard_log { }; } -pub(crate) async fn configure(path: &PathBuf) -> Result { +pub(crate) async fn configure(path: &Path) -> Result { wizard_log!("Configuring your first account:"); let mut config = TomlConfig::default(); @@ -39,12 +39,9 @@ pub(crate) async fn configure(path: &PathBuf) -> Result { while let Some((name, account_config)) = account::wizard::configure().await? { config.accounts.insert(name, account_config); - if !Confirm::new() - .with_prompt(wizard_prompt!( - "Would you like to configure another account?" - )) - .default(false) - .interact_opt()? + if !Confirm::new("Would you like to configure another account?") + .with_default(false) + .prompt_skippable()? .unwrap_or_default() { break; @@ -68,14 +65,13 @@ pub(crate) async fn configure(path: &PathBuf) -> Result { println!("{} accounts have been configured.", accounts.len()); - Select::with_theme(&*THEME) - .with_prompt(wizard_prompt!( - "Which account would you like to set as your default?" - )) - .items(&accounts) - .default(0) - .interact_opt()? - .and_then(|idx| config.accounts.get_mut(accounts[idx])) + Select::new( + "Which account would you like to set as your default?", + accounts, + ) + .with_starting_cursor(0) + .prompt_skippable()? + .and_then(|input| config.accounts.get_mut(input)) } }; @@ -85,12 +81,9 @@ pub(crate) async fn configure(path: &PathBuf) -> Result { process::exit(0) } - let path = Input::with_theme(&*THEME) - .with_prompt(wizard_prompt!( - "Where would you like to save your configuration?" - )) - .default(path.to_string_lossy().to_string()) - .interact()?; + let path = Text::new("Where would you like to save your configuration?") + .with_default(&path.to_string_lossy()) + .prompt()?; let path = expand::path(path); println!("Writing the configuration to {path:?}…"); diff --git a/src/folder/command/delete.rs b/src/folder/command/delete.rs index 38f568e..05aa211 100644 --- a/src/folder/command/delete.rs +++ b/src/folder/command/delete.rs @@ -1,7 +1,7 @@ use clap::Parser; use color_eyre::Result; -use dialoguer::Confirm; use email::{backend::feature::BackendFeatureSource, folder::delete::DeleteFolder}; +use inquire::Confirm; use std::process; use tracing::info; @@ -35,12 +35,9 @@ impl FolderDeleteCommand { let folder = &self.folder.name; - let confirm_msg = format!("Do you really want to delete the folder {folder}? All emails will be definitely deleted."); - let confirm = Confirm::new() - .with_prompt(confirm_msg) - .default(false) - .report(false) - .interact_opt()?; + let confirm = Confirm::new(&format!("Do you really want to delete the folder {folder}? All emails will be definitely deleted.")) + .with_default(false).prompt_skippable()?; + if let Some(false) | None = confirm { process::exit(0); }; diff --git a/src/folder/command/purge.rs b/src/folder/command/purge.rs index b36c02f..68d4bb2 100644 --- a/src/folder/command/purge.rs +++ b/src/folder/command/purge.rs @@ -1,6 +1,5 @@ use clap::Parser; use color_eyre::Result; -use dialoguer::Confirm; use email::{backend::feature::BackendFeatureSource, folder::purge::PurgeFolder}; use std::process; use tracing::info; @@ -35,12 +34,10 @@ impl FolderPurgeCommand { let folder = &self.folder.name; - let confirm_msg = format!("Do you really want to purge the folder {folder}? All emails will be definitely deleted."); - let confirm = Confirm::new() - .with_prompt(confirm_msg) - .default(false) - .report(false) - .interact_opt()?; + let confirm = inquire::Confirm::new(&format!("Do you really want to purge the folder {folder}? All emails will be definitely deleted.")) + .with_default(false) + .prompt_skippable()?; + if let Some(false) | None = confirm { process::exit(0); }; diff --git a/src/imap/wizard.rs b/src/imap/wizard.rs index 53296b4..d3e4e91 100644 --- a/src/imap/wizard.rs +++ b/src/imap/wizard.rs @@ -1,5 +1,4 @@ use color_eyre::Result; -use dialoguer::{Confirm, Input, Password, Select}; #[cfg(feature = "account-discovery")] use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType}; use email::{ @@ -9,14 +8,11 @@ use email::{ }, imap::config::{ImapAuthConfig, ImapConfig, ImapEncryptionKind}, }; +use inquire::validator::{ErrorMessage, StringValidator, Validation}; use oauth::v2_0::{AuthorizationCodeGrant, Client}; use secret::Secret; -use crate::{ - backend::config::BackendConfig, - ui::{prompt, THEME}, - wizard_log, wizard_prompt, -}; +use crate::{backend::config::BackendConfig, ui::prompt, wizard_log}; const ENCRYPTIONS: &[ImapEncryptionKind] = &[ ImapEncryptionKind::Tls, @@ -33,12 +29,35 @@ const KEYRING: &str = "Ask my password, then save it in my system's global keyri const RAW: &str = "Ask my password, then save it in the configuration file (not safe)"; const CMD: &str = "Ask me a shell command that exposes my password"; +#[derive(Clone, Copy)] +struct U16Validator; + +impl StringValidator for U16Validator { + fn validate( + &self, + input: &str, + ) -> std::prelude::v1::Result { + if input.parse::().is_ok() { + Ok(Validation::Valid) + } else { + Ok(Validation::Invalid(ErrorMessage::Custom(format!( + "you should enter a number between {} and {}", + u16::MIN, + u16::MAX + )))) + } + } +} + #[cfg(feature = "account-discovery")] pub(crate) async fn configure( account_name: &str, email: &str, autoconfig: Option<&AutoConfig>, ) -> Result { + use color_eyre::eyre::OptionExt as _; + use inquire::{validator::MinLengthValidator, Confirm, Password, Select, Text}; + let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2()); let autoconfig_server = autoconfig.and_then(|c| { c.email_provider() @@ -54,10 +73,9 @@ pub(crate) async fn configure( let default_host = autoconfig_host.unwrap_or_else(|| format!("imap.{}", email.rsplit_once('@').unwrap().1)); - let host = Input::with_theme(&*THEME) - .with_prompt("IMAP hostname") - .default(default_host) - .interact()?; + let host = Text::new("IMAP hostname") + .with_default(&default_host) + .prompt()?; let autoconfig_encryption = autoconfig_server .and_then(|imap| { @@ -75,11 +93,9 @@ pub(crate) async fn configure( ImapEncryptionKind::None => 2, }; - let encryption_idx = Select::with_theme(&*THEME) - .with_prompt("IMAP encryption") - .items(ENCRYPTIONS) - .default(default_encryption_idx) - .interact_opt()?; + let encryption_idx = Select::new("IMAP encryption", ENCRYPTIONS.to_vec()) + .with_starting_cursor(default_encryption_idx) + .prompt_skippable()?; let autoconfig_port = autoconfig_server .and_then(|s| s.port()) @@ -91,23 +107,26 @@ pub(crate) async fn configure( }); let (encryption, default_port) = match encryption_idx { - Some(idx) if idx == default_encryption_idx => { + Some(enc_kind) + if &enc_kind + == ENCRYPTIONS.get(default_encryption_idx).ok_or_eyre( + "something impossible happened while selecting the encryption of imap.", + )? => + { (Some(autoconfig_encryption), autoconfig_port) } - Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::Tls => { - (Some(ImapEncryptionKind::Tls), 993) - } - Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::StartTls => { - (Some(ImapEncryptionKind::StartTls), 143) - } + Some(ImapEncryptionKind::Tls) => (Some(ImapEncryptionKind::Tls), 993), + Some(ImapEncryptionKind::StartTls) => (Some(ImapEncryptionKind::StartTls), 143), _ => (Some(ImapEncryptionKind::None), 143), }; - let port = Input::with_theme(&*THEME) - .with_prompt("IMAP port") - .validate_with(|input: &String| input.parse::().map(|_| ())) - .default(default_port.to_string()) - .interact() + let port = Text::new("IMAP port") + .with_validators(&[ + Box::new(MinLengthValidator::new(1)), + Box::new(U16Validator {}), + ]) + .with_default(&default_port.to_string()) + .prompt() .map(|input| input.parse::().unwrap())?; let autoconfig_login = autoconfig_server.map(|imap| match imap.username() { @@ -118,10 +137,9 @@ pub(crate) async fn configure( let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned()); - let login = Input::with_theme(&*THEME) - .with_prompt("IMAP login") - .default(default_login) - .interact()?; + let login = Text::new("IMAP login") + .with_default(&default_login) + .prompt()?; let default_oauth2_enabled = autoconfig_server .and_then(|imap| { @@ -132,10 +150,9 @@ pub(crate) async fn configure( .filter(|_| autoconfig_oauth2.is_some()) .unwrap_or_default(); - let oauth2_enabled = Confirm::new() - .with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?")) - .default(default_oauth2_enabled) - .interact_opt()? + let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?") + .with_default(default_oauth2_enabled) + .prompt_skippable()? .unwrap_or_default(); let auth = if oauth2_enabled { @@ -143,25 +160,19 @@ pub(crate) async fn configure( let redirect_host = OAuth2Config::LOCALHOST.to_owned(); let redirect_port = OAuth2Config::get_first_available_port()?; - let method_idx = Select::with_theme(&*THEME) - .with_prompt("IMAP OAuth 2.0 mechanism") - .items(OAUTH2_MECHANISMS) - .default(0) - .interact_opt()?; + let method_idx = Select::new("IMAP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec()) + .with_starting_cursor(0) + .prompt_skippable()?; config.method = match method_idx { - Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2, - Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer, + Some(XOAUTH2) => OAuth2Method::XOAuth2, + Some(OAUTHBEARER) => OAuth2Method::OAuthBearer, _ => OAuth2Method::XOAuth2, }; - config.client_id = Input::with_theme(&*THEME) - .with_prompt("IMAP OAuth 2.0 client id") - .interact()?; + config.client_id = Text::new("IMAP OAuth 2.0 client id").prompt()?; - let client_secret: String = Password::with_theme(&*THEME) - .with_prompt("IMAP OAuth 2.0 client secret") - .interact()?; + let client_secret: String = Password::new("IMAP OAuth 2.0 client secret").prompt()?; config.client_secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?; config @@ -172,38 +183,28 @@ pub(crate) async fn configure( let default_auth_url = autoconfig_oauth2 .map(|o| o.auth_url().to_owned()) .unwrap_or_default(); - config.auth_url = Input::with_theme(&*THEME) - .with_prompt("IMAP OAuth 2.0 authorization URL") - .default(default_auth_url) - .interact()?; + config.auth_url = Text::new("IMAP OAuth 2.0 authorization URL") + .with_default(&default_auth_url) + .prompt()?; let default_token_url = autoconfig_oauth2 .map(|o| o.token_url().to_owned()) .unwrap_or_default(); - config.token_url = Input::with_theme(&*THEME) - .with_prompt("IMAP OAuth 2.0 token URL") - .default(default_token_url) - .interact()?; + config.token_url = Text::new("IMAP OAuth 2.0 token URL") + .with_default(&default_token_url) + .prompt()?; let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope()); let prompt_scope = |prompt: &str| -> Result> { Ok(match &autoconfig_scopes { - Some(scopes) => Select::with_theme(&*THEME) - .with_prompt(prompt) - .items(scopes) - .default(0) - .interact_opt()? - .and_then(|idx| scopes.get(idx)) - .map(|scope| scope.to_string()), - None => Some( - Input::with_theme(&*THEME) - .with_prompt(prompt) - .default(String::default()) - .interact()? - .to_owned(), - ) - .filter(|scope| !scope.is_empty()), + Some(scopes) => Select::new(prompt, scopes.to_vec()) + .with_starting_cursor(0) + .prompt_skippable()? + .map(ToOwned::to_owned), + None => { + Some(Text::new(prompt).prompt()?.to_owned()).filter(|scope| !scope.is_empty()) + } }) }; @@ -212,12 +213,9 @@ pub(crate) async fn configure( } let confirm_additional_scope = || -> Result { - let confirm = Confirm::new() - .with_prompt(wizard_prompt!( - "Would you like to add more IMAP OAuth 2.0 scopes?" - )) - .default(false) - .interact_opt()? + let confirm = Confirm::new("Would you like to add more IMAP OAuth 2.0 scopes?") + .with_default(false) + .prompt_skippable()? .unwrap_or_default(); Ok(confirm) @@ -236,12 +234,9 @@ pub(crate) async fn configure( config.scopes = OAuth2Scopes::Scopes(scopes); } - config.pkce = Confirm::new() - .with_prompt(wizard_prompt!( - "Would you like to enable PKCE verification?" - )) - .default(true) - .interact_opt()? + config.pkce = Confirm::new("Would you like to enable PKCE verification?") + .with_default(true) + .prompt_skippable()? .unwrap_or(true); wizard_log!("To complete your OAuth 2.0 setup, click on the following link:"); @@ -289,26 +284,23 @@ pub(crate) async fn configure( ImapAuthConfig::OAuth2(config) } else { - let secret_idx = Select::with_theme(&*THEME) - .with_prompt("IMAP authentication strategy") - .items(SECRETS) - .default(0) - .interact_opt()?; + let secret_idx = Select::new("IMAP authentication strategy", SECRETS.to_vec()) + .with_starting_cursor(0) + .prompt_skippable()?; let secret = match secret_idx { - Some(idx) if SECRETS[idx] == KEYRING => { + Some(KEYRING) => { let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?; secret .set_only_keyring(prompt::passwd("IMAP password")?) .await?; secret } - Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("IMAP password")?), - Some(idx) if SECRETS[idx] == CMD => Secret::new_command( - Input::with_theme(&*THEME) - .with_prompt("Shell command") - .default(format!("pass show {account_name}-imap-passwd")) - .interact()?, + Some(RAW) => Secret::new_raw(prompt::passwd("IMAP password")?), + Some(CMD) => Secret::new_command( + Text::new("Shell command") + .with_default(&format!("pass show {account_name}-imap-passwd")) + .prompt()?, ), _ => Default::default(), }; @@ -330,47 +322,44 @@ pub(crate) async fn configure( #[cfg(not(feature = "account-discovery"))] pub(crate) async fn configure(account_name: &str, email: &str) -> Result { + use inquire::{ + validator::MinLengthValidator, Confirm, Password, PasswordDisplayMode, Select, Text, + }; + let default_host = format!("imap.{}", email.rsplit_once('@').unwrap().1); - let host = Input::with_theme(&*THEME) - .with_prompt("IMAP hostname") - .default(default_host) - .interact()?; + let host = Text::new("IMAP hostname") + .with_default(&default_host) + .prompt()?; - let encryption_idx = Select::with_theme(&*THEME) - .with_prompt("IMAP encryption") - .items(ENCRYPTIONS) - .default(0) - .interact_opt()?; + let encryption_idx = Select::new("IMAP encryption", ENCRYPTIONS.to_vec()) + .with_starting_cursor(0) + .prompt_skippable()?; let (encryption, default_port) = match encryption_idx { - Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::Tls => { - (Some(ImapEncryptionKind::Tls), 993) - } - Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::StartTls => { - (Some(ImapEncryptionKind::StartTls), 143) - } + Some(ImapEncryptionKind::Tls) => (Some(ImapEncryptionKind::Tls), 993), + Some(ImapEncryptionKind::StartTls) => (Some(ImapEncryptionKind::StartTls), 143), _ => (Some(ImapEncryptionKind::None), 143), }; - let port = Input::with_theme(&*THEME) - .with_prompt("IMAP port") - .validate_with(|input: &String| input.parse::().map(|_| ())) - .default(default_port.to_string()) - .interact() + let port = Text::new("IMAP port") + .with_validators(&[ + Box::new(MinLengthValidator::new(1)), + Box::new(U16Validator {}), + ]) + .with_default(&default_port.to_string()) + .prompt() .map(|input| input.parse::().unwrap())?; let default_login = email.to_owned(); - let login = Input::with_theme(&*THEME) - .with_prompt("IMAP login") - .default(default_login) - .interact()?; + let login = Text::new("IMAP login") + .with_default(&default_login) + .prompt()?; - let oauth2_enabled = Confirm::new() - .with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?")) - .default(false) - .interact_opt()? + let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?") + .with_default(false) + .prompt_skippable()? .unwrap_or_default(); let auth = if oauth2_enabled { @@ -378,25 +367,21 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result OAuth2Method::XOAuth2, - Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer, + Some(XOAUTH2) => OAuth2Method::XOAuth2, + Some(OAUTHBEARER) => OAuth2Method::OAuthBearer, _ => OAuth2Method::XOAuth2, }; - config.client_id = Input::with_theme(&*THEME) - .with_prompt("IMAP OAuth 2.0 client id") - .interact()?; + config.client_id = Text::new("IMAP OAuth 2.0 client id").prompt()?; - let client_secret: String = Password::with_theme(&*THEME) - .with_prompt("IMAP OAuth 2.0 client secret") - .interact()?; + let client_secret: String = Password::new("IMAP OAuth 2.0 client secret") + .with_display_mode(PasswordDisplayMode::Masked) + .prompt()?; config.client_secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?; config @@ -404,23 +389,12 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result Result> { - Ok(Some( - Input::with_theme(&*THEME) - .with_prompt(prompt) - .default(String::default()) - .interact()? - .to_owned(), - ) - .filter(|scope| !scope.is_empty())) + Ok(Some(Text::new(prompt).prompt()?.to_owned()).filter(|scope| !scope.is_empty())) }; if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? { @@ -428,12 +402,9 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result Result { - let confirm = Confirm::new() - .with_prompt(wizard_prompt!( - "Would you like to add more IMAP OAuth 2.0 scopes?" - )) - .default(false) - .interact_opt()? + let confirm = Confirm::new("Would you like to add more IMAP OAuth 2.0 scopes?") + .with_default(false) + .prompt_skippable()? .unwrap_or_default(); Ok(confirm) @@ -452,12 +423,9 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result Result { + Some(KEYRING) => { let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?; secret .set_only_keyring(prompt::passwd("IMAP password")?) .await?; secret } - Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("IMAP password")?), - Some(idx) if SECRETS[idx] == CMD => Secret::new_command( - Input::with_theme(&*THEME) - .with_prompt("Shell command") - .default(format!("pass show {account_name}-imap-passwd")) - .interact()?, + Some(RAW) => Secret::new_raw(prompt::passwd("IMAP password")?), + Some(CMD) => Secret::new_command( + Text::new("Shell command") + .with_default(&format!("pass show {account_name}-imap-passwd")) + .prompt()?, ), _ => Default::default(), }; diff --git a/src/maildir/wizard.rs b/src/maildir/wizard.rs index 20c0a26..a21112b 100644 --- a/src/maildir/wizard.rs +++ b/src/maildir/wizard.rs @@ -1,23 +1,25 @@ use color_eyre::Result; -use dialoguer::Input; use dirs::home_dir; use email::maildir::config::MaildirConfig; +use inquire::Text; -use crate::{backend::config::BackendConfig, ui::THEME}; +use crate::backend::config::BackendConfig; pub(crate) fn configure() -> Result { let mut config = MaildirConfig::default(); - let mut input = Input::with_theme(&*THEME); + let mut input = Text::new("Maildir directory"); - if let Some(home) = home_dir() { - input.default(home.join("Mail").display().to_string()); + let Some(home) = home_dir() else { + config.root_dir = input.prompt()?.into(); + + return Ok(BackendConfig::Maildir(config)); }; - config.root_dir = input - .with_prompt("Maildir directory") - .interact_text()? - .into(); + let def = home.join("Mail").display().to_string(); + input = input.with_default(&def); + + config.root_dir = input.prompt()?.into(); Ok(BackendConfig::Maildir(config)) } diff --git a/src/notmuch/wizard.rs b/src/notmuch/wizard.rs index ba0505f..4c3ba54 100644 --- a/src/notmuch/wizard.rs +++ b/src/notmuch/wizard.rs @@ -1,24 +1,23 @@ use color_eyre::Result; -use dialoguer::Input; use email::notmuch::config::NotmuchConfig; +use inquire::Text; -use crate::{backend::config::BackendConfig, ui::THEME}; +use crate::backend::config::BackendConfig; pub(crate) fn configure() -> Result { - let mut config = NotmuchConfig::default(); - - let default_database_path = NotmuchConfig::get_default_database_path() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - config.database_path = Some( - Input::with_theme(&*THEME) - .with_prompt("Notmuch database path") - .default(default_database_path) - .interact_text()? - .into(), - ); + let config = NotmuchConfig { + database_path: Some( + Text::new("Notmuch database path") + .with_default( + &NotmuchConfig::get_default_database_path() + .unwrap_or_default() + .to_string_lossy(), + ) + .prompt()? + .into(), + ), + ..Default::default() + }; Ok(BackendConfig::Notmuch(config)) } diff --git a/src/sendmail/wizard.rs b/src/sendmail/wizard.rs index e6418df..bd7df9d 100644 --- a/src/sendmail/wizard.rs +++ b/src/sendmail/wizard.rs @@ -1,15 +1,14 @@ use color_eyre::Result; -use dialoguer::Input; use email::sendmail::config::SendmailConfig; +use inquire::Text; -use crate::{backend::config::BackendConfig, ui::THEME}; +use crate::backend::config::BackendConfig; pub(crate) fn configure() -> Result { let config = SendmailConfig { - cmd: Input::with_theme(&*THEME) - .with_prompt("Sendmail-compatible shell command to send emails") - .default(String::from("/usr/bin/msmtp")) - .interact()? + cmd: Text::new("Sendmail-compatible shell command to send emails") + .with_default("/usr/bin/msmtp") + .prompt()? .into(), }; diff --git a/src/smtp/wizard.rs b/src/smtp/wizard.rs index f22605f..d682c7c 100644 --- a/src/smtp/wizard.rs +++ b/src/smtp/wizard.rs @@ -1,5 +1,4 @@ use color_eyre::Result; -use dialoguer::{Confirm, Input, Password, Select}; #[cfg(feature = "account-discovery")] use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType}; use email::{ @@ -9,14 +8,11 @@ use email::{ }, smtp::config::{SmtpAuthConfig, SmtpConfig, SmtpEncryptionKind}, }; +use inquire::validator::{ErrorMessage, StringValidator, Validation}; use oauth::v2_0::{AuthorizationCodeGrant, Client}; use secret::Secret; -use crate::{ - backend::config::BackendConfig, - ui::{prompt, THEME}, - wizard_log, wizard_prompt, -}; +use crate::{backend::config::BackendConfig, ui::prompt, wizard_log}; const ENCRYPTIONS: &[SmtpEncryptionKind] = &[ SmtpEncryptionKind::Tls, @@ -33,12 +29,35 @@ const KEYRING: &str = "Ask my password, then save it in my system's global keyri const RAW: &str = "Ask my password, then save it in the configuration file (not safe)"; const CMD: &str = "Ask me a shell command that exposes my password"; +#[derive(Clone, Copy)] +struct U16Validator; + +impl StringValidator for U16Validator { + fn validate( + &self, + input: &str, + ) -> std::prelude::v1::Result { + if input.parse::().is_ok() { + Ok(Validation::Valid) + } else { + Ok(Validation::Invalid(ErrorMessage::Custom(format!( + "you should enter a number between {} and {}", + u16::MIN, + u16::MAX + )))) + } + } +} + #[cfg(feature = "account-discovery")] pub(crate) async fn configure( account_name: &str, email: &str, autoconfig: Option<&AutoConfig>, ) -> Result { + use color_eyre::eyre::OptionExt as _; + use inquire::{validator, Confirm, Password, Select, Text}; + let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2()); let autoconfig_server = autoconfig.and_then(|c| { c.email_provider() @@ -54,10 +73,9 @@ pub(crate) async fn configure( let default_host = autoconfig_host.unwrap_or_else(|| format!("smtp.{}", email.rsplit_once('@').unwrap().1)); - let host = Input::with_theme(&*THEME) - .with_prompt("SMTP hostname") - .default(default_host) - .interact()?; + let host = Text::new("SMTP hostname") + .with_default(&default_host) + .prompt()?; let autoconfig_encryption = autoconfig_server .and_then(|smtp| { @@ -75,11 +93,9 @@ pub(crate) async fn configure( SmtpEncryptionKind::None => 2, }; - let encryption_idx = Select::with_theme(&*THEME) - .with_prompt("SMTP encryption") - .items(ENCRYPTIONS) - .default(default_encryption_idx) - .interact_opt()?; + let encryption_kind = Select::new("SMTP encryption", ENCRYPTIONS.to_vec()) + .with_starting_cursor(default_encryption_idx) + .prompt_skippable()?; let autoconfig_port = autoconfig_server .and_then(|s| s.port()) @@ -90,24 +106,27 @@ pub(crate) async fn configure( SmtpEncryptionKind::None => 25, }); - let (encryption, default_port) = match encryption_idx { - Some(idx) if idx == default_encryption_idx => { + let (encryption, default_port) = match encryption_kind { + Some(idx) + if &idx + == ENCRYPTIONS.get(default_encryption_idx).ok_or_eyre( + "something impossible happened during finding default match for encryption.", + )? => + { (Some(autoconfig_encryption), autoconfig_port) } - Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::Tls => { - (Some(SmtpEncryptionKind::Tls), 465) - } - Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::StartTls => { - (Some(SmtpEncryptionKind::StartTls), 587) - } + Some(SmtpEncryptionKind::Tls) => (Some(SmtpEncryptionKind::Tls), 465), + Some(SmtpEncryptionKind::StartTls) => (Some(SmtpEncryptionKind::StartTls), 587), _ => (Some(SmtpEncryptionKind::None), 25), }; - let port = Input::with_theme(&*THEME) - .with_prompt("SMTP port") - .validate_with(|input: &String| input.parse::().map(|_| ())) - .default(default_port.to_string()) - .interact() + let port = Text::new("SMTP port") + .with_validators(&[ + Box::new(validator::MinLengthValidator::new(1)), + Box::new(U16Validator {}), + ]) + .with_default(&default_port.to_string()) + .prompt() .map(|input| input.parse::().unwrap())?; let autoconfig_login = autoconfig_server.map(|smtp| match smtp.username() { @@ -118,10 +137,9 @@ pub(crate) async fn configure( let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned()); - let login = Input::with_theme(&*THEME) - .with_prompt("SMTP login") - .default(default_login) - .interact()?; + let login = Text::new("SMTP login") + .with_default(&default_login) + .prompt()?; let default_oauth2_enabled = autoconfig_server .and_then(|smtp| { @@ -132,10 +150,9 @@ pub(crate) async fn configure( .filter(|_| autoconfig_oauth2.is_some()) .unwrap_or_default(); - let oauth2_enabled = Confirm::new() - .with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?")) - .default(default_oauth2_enabled) - .interact_opt()? + let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?") + .with_default(default_oauth2_enabled) + .prompt_skippable()? .unwrap_or_default(); let auth = if oauth2_enabled { @@ -143,25 +160,21 @@ pub(crate) async fn configure( let redirect_host = OAuth2Config::LOCALHOST; let redirect_port = OAuth2Config::get_first_available_port()?; - let method_idx = Select::with_theme(&*THEME) - .with_prompt("SMTP OAuth 2.0 mechanism") - .items(OAUTH2_MECHANISMS) - .default(0) - .interact_opt()?; + let method_idx = Select::new("SMTP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec()) + .with_starting_cursor(0) + .prompt_skippable()?; config.method = match method_idx { - Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2, - Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer, + Some(choice) if choice == XOAUTH2 => OAuth2Method::XOAuth2, + Some(choice) if choice == OAUTHBEARER => OAuth2Method::OAuthBearer, _ => OAuth2Method::XOAuth2, }; - config.client_id = Input::with_theme(&*THEME) - .with_prompt("SMTP OAuth 2.0 client id") - .interact()?; + config.client_id = Text::new("SMTP OAuth 2.0 client id").prompt()?; - let client_secret: String = Password::with_theme(&*THEME) - .with_prompt("SMTP OAuth 2.0 client secret") - .interact()?; + let client_secret: String = Password::new("SMTP OAuth 2.0 client secret") + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .prompt()?; config.client_secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?; config @@ -172,38 +185,26 @@ pub(crate) async fn configure( let default_auth_url = autoconfig_oauth2 .map(|o| o.auth_url().to_owned()) .unwrap_or_default(); - config.auth_url = Input::with_theme(&*THEME) - .with_prompt("SMTP OAuth 2.0 authorization URL") - .default(default_auth_url) - .interact()?; + config.auth_url = Text::new("SMTP OAuth 2.0 authorization URL") + .with_default(&default_auth_url) + .prompt()?; let default_token_url = autoconfig_oauth2 .map(|o| o.token_url().to_owned()) .unwrap_or_default(); - config.token_url = Input::with_theme(&*THEME) - .with_prompt("SMTP OAuth 2.0 token URL") - .default(default_token_url) - .interact()?; + config.token_url = Text::new("SMTP OAuth 2.0 token URL") + .with_default(&default_token_url) + .prompt()?; let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope()); let prompt_scope = |prompt: &str| -> Result> { Ok(match &autoconfig_scopes { - Some(scopes) => Select::with_theme(&*THEME) - .with_prompt(prompt) - .items(scopes) - .default(0) - .interact_opt()? - .and_then(|idx| scopes.get(idx)) - .map(|scope| scope.to_string()), - None => Some( - Input::with_theme(&*THEME) - .with_prompt(prompt) - .default(String::default()) - .interact()? - .to_owned(), - ) - .filter(|scope| !scope.is_empty()), + Some(scopes) => Select::new(prompt, scopes.to_vec()) + .with_starting_cursor(0) + .prompt_skippable()? + .map(ToOwned::to_owned), + None => Some(Text::new(prompt).prompt()?).filter(|scope| !scope.is_empty()), }) }; @@ -212,12 +213,9 @@ pub(crate) async fn configure( } let confirm_additional_scope = || -> Result { - let confirm = Confirm::new() - .with_prompt(wizard_prompt!( - "Would you like to add more SMTP OAuth 2.0 scopes?" - )) - .default(false) - .interact_opt()? + let confirm = Confirm::new("Would you like to add more SMTP OAuth 2.0 scopes?") + .with_default(false) + .prompt_skippable()? .unwrap_or_default(); Ok(confirm) @@ -236,12 +234,9 @@ pub(crate) async fn configure( config.scopes = OAuth2Scopes::Scopes(scopes); } - config.pkce = Confirm::new() - .with_prompt(wizard_prompt!( - "Would you like to enable PKCE verification?" - )) - .default(true) - .interact_opt()? + config.pkce = Confirm::new("Would you like to enable PKCE verification?") + .with_default(true) + .prompt_skippable()? .unwrap_or(true); wizard_log!("To complete your OAuth 2.0 setup, click on the following link:"); @@ -289,26 +284,23 @@ pub(crate) async fn configure( SmtpAuthConfig::OAuth2(config) } else { - let secret_idx = Select::with_theme(&*THEME) - .with_prompt("SMTP authentication strategy") - .items(SECRETS) - .default(0) - .interact_opt()?; + let secret_idx = Select::new("SMTP authentication strategy", SECRETS.to_vec()) + .with_starting_cursor(0) + .prompt_skippable()?; let secret = match secret_idx { - Some(idx) if SECRETS[idx] == KEYRING => { + Some(sec) if sec == KEYRING => { let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?; secret .set_only_keyring(prompt::passwd("SMTP password")?) .await?; secret } - Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("SMTP password")?), - Some(idx) if SECRETS[idx] == CMD => Secret::new_command( - Input::with_theme(&*THEME) - .with_prompt("Shell command") - .default(format!("pass show {account_name}-smtp-passwd")) - .interact()?, + Some(sec) if sec == RAW => Secret::new_raw(prompt::passwd("SMTP password")?), + Some(sec) if sec == CMD => Secret::new_command( + Text::new("Shell command") + .with_default(&format!("pass show {account_name}-smtp-passwd")) + .prompt()?, ), _ => Default::default(), }; @@ -329,47 +321,42 @@ pub(crate) async fn configure( #[cfg(not(feature = "account-discovery"))] pub(crate) async fn configure(account_name: &str, email: &str) -> Result { + use inquire::{validator::MinLengthValidator, Confirm, Password, Select, Text}; + let default_host = format!("smtp.{}", email.rsplit_once('@').unwrap().1); - let host = Input::with_theme(&*THEME) - .with_prompt("SMTP hostname") - .default(default_host) - .interact()?; + let host = Text::new("SMTP hostname") + .with_default(&default_host) + .prompt()?; - let encryption_idx = Select::with_theme(&*THEME) - .with_prompt("SMTP encryption") - .items(ENCRYPTIONS) - .default(0) - .interact_opt()?; + let encryption_idx = Select::new("SMTP encryption", ENCRYPTIONS.to_vec()) + .with_starting_cursor(0) + .prompt_skippable()?; let (encryption, default_port) = match encryption_idx { - Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::Tls => { - (Some(SmtpEncryptionKind::Tls), 465) - } - Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::StartTls => { - (Some(SmtpEncryptionKind::StartTls), 587) - } + Some(SmtpEncryptionKind::Tls) => (Some(SmtpEncryptionKind::Tls), 465), + Some(SmtpEncryptionKind::StartTls) => (Some(SmtpEncryptionKind::StartTls), 587), _ => (Some(SmtpEncryptionKind::None), 25), }; - let port = Input::with_theme(&*THEME) - .with_prompt("SMTP port") - .validate_with(|input: &String| input.parse::().map(|_| ())) - .default(default_port.to_string()) - .interact() + let port = Text::new("SMTP port") + .with_validators(&[ + Box::new(MinLengthValidator::new(1)), + Box::new(U16Validator {}), + ]) + .with_default(&default_port.to_string()) + .prompt() .map(|input| input.parse::().unwrap())?; let default_login = email.to_owned(); - let login = Input::with_theme(&*THEME) - .with_prompt("SMTP login") - .default(default_login) - .interact()?; + let login = Text::new("SMTP login") + .with_default(&default_login) + .prompt()?; - let oauth2_enabled = Confirm::new() - .with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?")) - .default(false) - .interact_opt()? + let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?") + .with_default(false) + .prompt_skippable()? .unwrap_or_default(); let auth = if oauth2_enabled { @@ -377,25 +364,21 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result OAuth2Method::XOAuth2, - Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer, + Some(XOAUTH2) => OAuth2Method::XOAuth2, + Some(OAUTHBEARER) => OAuth2Method::OAuthBearer, _ => OAuth2Method::XOAuth2, }; - config.client_id = Input::with_theme(&*THEME) - .with_prompt("SMTP OAuth 2.0 client id") - .interact()?; + config.client_id = Text::new("SMTP OAuth 2.0 client id").prompt()?; - let client_secret: String = Password::with_theme(&*THEME) - .with_prompt("SMTP OAuth 2.0 client secret") - .interact()?; + let client_secret: String = Password::new("SMTP OAuth 2.0 client secret") + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .prompt()?; config.client_secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?; config @@ -403,23 +386,12 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result Result> { - Ok(Some( - Input::with_theme(&*THEME) - .with_prompt(prompt) - .default(String::default()) - .interact()? - .to_owned(), - ) - .filter(|scope| !scope.is_empty())) + Ok(Some(Text::new(prompt).prompt()?.to_owned()).filter(|scope| !scope.is_empty())) }; if let Some(scope) = prompt_scope("SMTP OAuth 2.0 main scope")? { @@ -427,12 +399,9 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result Result { - let confirm = Confirm::new() - .with_prompt(wizard_prompt!( - "Would you like to add more SMTP OAuth 2.0 scopes?" - )) - .default(false) - .interact_opt()? + let confirm = Confirm::new("Would you like to add more SMTP OAuth 2.0 scopes?") + .with_default(false) + .prompt_skippable()? .unwrap_or_default(); Ok(confirm) @@ -451,12 +420,9 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result Result { + Some(KEYRING) => { let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?; secret .set_only_keyring(prompt::passwd("SMTP password")?) .await?; secret } - Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("SMTP password")?), - Some(idx) if SECRETS[idx] == CMD => Secret::new_command( - Input::with_theme(&*THEME) - .with_prompt("Shell command") - .default(format!("pass show {account_name}-smtp-passwd")) - .interact()?, + Some(RAW) => Secret::new_raw(prompt::passwd("SMTP password")?), + Some(CMD) => Secret::new_command( + Text::new("Shell command") + .with_default(&format!("pass show {account_name}-smtp-passwd")) + .prompt()?, ), _ => Default::default(), }; diff --git a/src/ui/choice.rs b/src/ui/choice.rs index 9d56e62..bd3a4fd 100644 --- a/src/ui/choice.rs +++ b/src/ui/choice.rs @@ -1,7 +1,7 @@ -use color_eyre::Result; -use dialoguer::Select; +use std::fmt::Display; -use super::THEME; +use color_eyre::Result; +use inquire::Select; #[derive(Clone, Debug)] pub enum PreEditChoice { @@ -10,13 +10,17 @@ pub enum PreEditChoice { Quit, } -impl ToString for PreEditChoice { - fn to_string(&self) -> String { - match self { - Self::Edit => "Edit it".into(), - Self::Discard => "Discard it".into(), - Self::Quit => "Quit".into(), - } +impl Display for PreEditChoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Edit => "Edit it", + Self::Discard => "Discard it", + Self::Quit => "Quit", + } + ) } } @@ -27,13 +31,15 @@ pub fn pre_edit() -> Result { PreEditChoice::Quit, ]; - let choice_idx = Select::with_theme(&*THEME) - .with_prompt("A draft was found, what would you like to do with it?") - .items(&choices) - .default(0) - .interact()?; + let user_choice = Select::new( + "A draft was found, what would you like to do with it?", + choices.to_vec(), + ) + .with_starting_cursor(0) + .with_vim_mode(true) + .prompt()?; - Ok(choices[choice_idx].clone()) + Ok(user_choice) } #[derive(Clone, Debug, Eq, PartialEq)] @@ -45,15 +51,19 @@ pub enum PostEditChoice { Discard, } -impl ToString for PostEditChoice { - fn to_string(&self) -> String { - match self { - Self::Send => "Send it".into(), - Self::Edit => "Edit it again".into(), - Self::LocalDraft => "Save it as local draft".into(), - Self::RemoteDraft => "Save it as remote draft".into(), - Self::Discard => "Discard it".into(), - } +impl Display for PostEditChoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Send => "Send it", + Self::Edit => "Edit it again", + Self::LocalDraft => "Save it as local draft", + Self::RemoteDraft => "Save it as remote draft", + Self::Discard => "Discard it", + } + ) } } @@ -66,11 +76,13 @@ pub fn post_edit() -> Result { PostEditChoice::Discard, ]; - let choice_idx = Select::with_theme(&*THEME) - .with_prompt("What would you like to do with this message?") - .items(&choices) - .default(0) - .interact()?; + let user_choice = inquire::Select::new( + "What would you like to do with this message?", + choices.to_vec(), + ) + .with_starting_cursor(0) + .with_vim_mode(true) + .prompt()?; - Ok(choices[choice_idx].clone()) + Ok(user_choice) } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e58f35c..0ca3337 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,9 +3,4 @@ pub mod editor; pub(crate) mod prompt; pub mod table; -use dialoguer::theme::ColorfulTheme; -use once_cell::sync::Lazy; - pub use self::table::*; - -pub(crate) static THEME: Lazy = Lazy::new(ColorfulTheme::default); diff --git a/src/ui/prompt.rs b/src/ui/prompt.rs index 8f5a66b..811469d 100644 --- a/src/ui/prompt.rs +++ b/src/ui/prompt.rs @@ -1,21 +1,28 @@ -use dialoguer::Password; use std::io; -use super::THEME; - pub(crate) fn passwd(prompt: &str) -> io::Result { - Password::with_theme(&*THEME) - .with_prompt(prompt) - .with_confirmation( - "Confirm password", - "Passwords do not match, please try again.", - ) - .interact() + inquire::Password::new(prompt) + .with_custom_confirmation_message("Confirm password") + .with_custom_confirmation_error_message("Passwords do not match, please try again.") + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .prompt() + .map_err(|e| { + io::Error::new( + io::ErrorKind::Interrupted, + format!("failed to get password: {e}"), + ) + }) } pub(crate) fn secret(prompt: &str) -> io::Result { - Password::with_theme(&*THEME) - .with_prompt(prompt) - .report(false) - .interact() + inquire::Password::new(prompt) + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .without_confirmation() + .prompt() + .map_err(|e| { + io::Error::new( + io::ErrorKind::Interrupted, + format!("failed to get secret: {e}"), + ) + }) }