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 <me@prma.dev>
This commit is contained in:
Perma Alesheikh
2024-05-06 15:49:58 +03:30
committed by Clément DOUIN
parent d54dd6429e
commit 1e448e56eb
17 changed files with 441 additions and 543 deletions
+139 -174
View File
@@ -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<Validation, inquire::CustomUserError> {
if input.parse::<u16>().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<BackendConfig> {
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::<u16>().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::<u16>().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<Option<String>> {
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<bool> {
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<BackendConfig> {
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::<u16>().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::<u16>().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<Backend
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")
.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<Backend
.set_only_keyring(&client_secret)
.await?;
config.auth_url = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 authorization URL")
.interact()?;
config.auth_url = Text::new("IMAP OAuth 2.0 authorization URL").prompt()?;
config.token_url = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 token URL")
.interact()?;
config.token_url = Text::new("IMAP OAuth 2.0 token URL").prompt()?;
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
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<Backend
}
let confirm_additional_scope = || -> Result<bool> {
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<Backend
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:");
@@ -505,26 +473,23 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
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(),
};