use crate::{
config::get_configuration,
error::{InquireError, InquireResult},
formatter::StringFormatter,
input::Input,
terminal::get_default_terminal,
ui::{Backend, Key, KeyModifiers, PasswordBackend, RenderConfig},
validator::{ErrorMessage, StringValidator, Validation},
};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PasswordDisplayMode {
Hidden,
Masked,
Full,
}
struct PasswordConfirmation<'a> {
message: &'a str,
error_message: &'a str,
input: Input,
}
#[derive(Clone)]
pub struct Password<'a> {
pub message: &'a str,
pub custom_confirmation_message: Option<&'a str>,
pub custom_confirmation_error_message: Option<&'a str>,
pub help_message: Option<&'a str>,
pub formatter: StringFormatter<'a>,
pub display_mode: PasswordDisplayMode,
pub enable_display_toggle: bool,
pub enable_confirmation: bool,
pub validators: Vec<Box<dyn StringValidator>>,
pub render_config: RenderConfig<'a>,
}
impl<'a> Password<'a> {
pub const DEFAULT_FORMATTER: StringFormatter<'a> = &|_| String::from("********");
pub const DEFAULT_VALIDATORS: Vec<Box<dyn StringValidator>> = vec![];
pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = None;
pub const DEFAULT_ENABLE_DISPLAY_TOGGLE: bool = false;
pub const DEFAULT_ENABLE_CONFIRMATION: bool = true;
pub const DEFAULT_DISPLAY_MODE: PasswordDisplayMode = PasswordDisplayMode::Hidden;
pub fn new(message: &'a str) -> Self {
Self {
message,
custom_confirmation_message: None,
custom_confirmation_error_message: None,
enable_confirmation: Self::DEFAULT_ENABLE_CONFIRMATION,
enable_display_toggle: Self::DEFAULT_ENABLE_DISPLAY_TOGGLE,
display_mode: Self::DEFAULT_DISPLAY_MODE,
help_message: Self::DEFAULT_HELP_MESSAGE,
formatter: Self::DEFAULT_FORMATTER,
validators: Self::DEFAULT_VALIDATORS,
render_config: get_configuration(),
}
}
pub fn with_help_message(mut self, message: &'a str) -> Self {
self.help_message = Some(message);
self
}
pub fn with_display_toggle_enabled(mut self) -> Self {
self.enable_display_toggle = true;
self
}
pub fn without_confirmation(mut self) -> Self {
self.enable_confirmation = false;
self
}
pub fn with_custom_confirmation_message(mut self, message: &'a str) -> Self {
self.custom_confirmation_message.replace(message);
self
}
pub fn with_custom_confirmation_error_message(mut self, message: &'a str) -> Self {
self.custom_confirmation_error_message.replace(message);
self
}
pub fn with_display_mode(mut self, mode: PasswordDisplayMode) -> Self {
self.display_mode = mode;
self
}
pub fn with_formatter(mut self, formatter: StringFormatter<'a>) -> Self {
self.formatter = formatter;
self
}
pub fn with_validator<V>(mut self, validator: V) -> Self
where
V: StringValidator + 'static,
{
if self.validators.capacity() == 0 {
self.validators.reserve(5);
}
self.validators.push(Box::new(validator));
self
}
pub fn with_validators(mut self, validators: &[Box<dyn StringValidator>]) -> Self {
for validator in validators {
#[allow(suspicious_double_ref_op)]
self.validators.push(validator.clone());
}
self
}
pub fn with_render_config(mut self, render_config: RenderConfig<'a>) -> Self {
self.render_config = render_config;
self
}
pub fn prompt_skippable(self) -> InquireResult<Option<String>> {
match self.prompt() {
Ok(answer) => Ok(Some(answer)),
Err(InquireError::OperationCanceled) => Ok(None),
Err(err) => Err(err),
}
}
pub fn prompt(self) -> InquireResult<String> {
let terminal = get_default_terminal()?;
let mut backend = Backend::new(terminal, self.render_config)?;
self.prompt_with_backend(&mut backend)
}
pub(crate) fn prompt_with_backend<B: PasswordBackend>(
self,
backend: &mut B,
) -> InquireResult<String> {
PasswordPrompt::from(self).prompt(backend)
}
}
struct PasswordPrompt<'a> {
message: &'a str,
help_message: Option<&'a str>,
input: Input,
standard_display_mode: PasswordDisplayMode,
display_mode: PasswordDisplayMode,
enable_display_toggle: bool,
confirmation: Option<PasswordConfirmation<'a>>, confirmation_stage: bool,
formatter: StringFormatter<'a>,
validators: Vec<Box<dyn StringValidator>>,
error: Option<ErrorMessage>,
}
impl<'a> From<Password<'a>> for PasswordPrompt<'a> {
fn from(so: Password<'a>) -> Self {
let confirmation = match so.enable_confirmation {
true => Some(PasswordConfirmation {
message: so.custom_confirmation_message.unwrap_or("Confirmation:"),
error_message: so
.custom_confirmation_error_message
.unwrap_or("The answers don't match."),
input: Input::new(),
}),
false => None,
};
Self {
message: so.message,
help_message: so.help_message,
standard_display_mode: so.display_mode,
display_mode: so.display_mode,
enable_display_toggle: so.enable_display_toggle,
confirmation,
confirmation_stage: false,
formatter: so.formatter,
validators: so.validators,
input: Input::new(),
error: None,
}
}
}
impl<'a> From<&'a str> for Password<'a> {
fn from(val: &'a str) -> Self {
Password::new(val)
}
}
impl<'a> PasswordPrompt<'a> {
fn active_input(&self) -> &Input {
match &self.confirmation {
Some(confirmation) if self.confirmation_stage => &confirmation.input,
_ => &self.input,
}
}
fn active_input_mut(&mut self) -> &mut Input {
match &mut self.confirmation {
Some(confirmation) if self.confirmation_stage => &mut confirmation.input,
_ => &mut self.input,
}
}
fn on_change(&mut self, key: Key) {
match key {
Key::Char('r', m) | Key::Char('R', m)
if m.contains(KeyModifiers::CONTROL) && self.enable_display_toggle =>
{
self.toggle_display_mode();
}
_ => {
self.active_input_mut().handle_key(key);
}
};
}
fn toggle_display_mode(&mut self) {
self.display_mode = match self.display_mode {
PasswordDisplayMode::Hidden => PasswordDisplayMode::Full,
PasswordDisplayMode::Masked => PasswordDisplayMode::Full,
PasswordDisplayMode::Full => self.standard_display_mode,
}
}
fn handle_cancel(&mut self) -> bool {
if self.confirmation_stage && self.confirmation.is_some() {
if self.display_mode == PasswordDisplayMode::Hidden {
self.input.clear();
}
self.error = None;
self.confirmation_stage = false;
true
} else {
false
}
}
fn handle_submit(&mut self) -> InquireResult<Option<String>> {
let answer = match self.validate_current_answer()? {
Validation::Valid => self.confirm_current_answer(),
Validation::Invalid(msg) => {
self.error = Some(msg);
None
}
};
Ok(answer)
}
fn confirm_current_answer(&mut self) -> Option<String> {
let cur_answer = self.cur_answer();
match &mut self.confirmation {
None => Some(cur_answer),
Some(confirmation) => {
if !self.confirmation_stage {
if self.display_mode == PasswordDisplayMode::Hidden {
confirmation.input.clear();
}
self.error = None;
self.confirmation_stage = true;
None
} else if self.input.content() == cur_answer {
Some(confirmation.input.content().into())
} else {
confirmation.input.clear();
self.error = Some(confirmation.error_message.into());
self.confirmation_stage = false;
None
}
}
}
}
fn validate_current_answer(&self) -> InquireResult<Validation> {
for validator in &self.validators {
match validator.validate(self.active_input().content()) {
Ok(Validation::Valid) => {}
Ok(Validation::Invalid(msg)) => return Ok(Validation::Invalid(msg)),
Err(err) => return Err(InquireError::Custom(err)),
}
}
Ok(Validation::Valid)
}
fn cur_answer(&self) -> String {
self.active_input().content().into()
}
fn render<B: PasswordBackend>(&mut self, backend: &mut B) -> InquireResult<()> {
backend.frame_setup()?;
if let Some(err) = &self.error {
backend.render_error_message(err)?;
}
match self.display_mode {
PasswordDisplayMode::Hidden => {
backend.render_prompt(self.message)?;
match &self.confirmation {
Some(confirmation) if self.confirmation_stage => {
backend.render_prompt(confirmation.message)?
}
_ => {}
}
}
PasswordDisplayMode::Masked => {
backend.render_prompt_with_masked_input(self.message, &self.input)?;
match &self.confirmation {
Some(confirmation) if self.confirmation_stage => {
backend.render_prompt_with_masked_input(
confirmation.message,
&confirmation.input,
)?;
}
_ => {}
}
}
PasswordDisplayMode::Full => {
backend.render_prompt_with_full_input(self.message, &self.input)?;
match &self.confirmation {
Some(confirmation) if self.confirmation_stage => {
backend.render_prompt_with_full_input(
confirmation.message,
&confirmation.input,
)?;
}
_ => {}
}
}
}
if let Some(message) = self.help_message {
backend.render_help_message(message)?;
}
backend.frame_finish()?;
Ok(())
}
fn prompt<B: PasswordBackend>(mut self, backend: &mut B) -> InquireResult<String> {
let final_answer = loop {
self.render(backend)?;
let key = backend.read_key()?;
match key {
Key::Interrupt => interrupt_prompt!(),
Key::Cancel => {
if !self.handle_cancel() {
cancel_prompt!(backend, self.message);
}
}
Key::Submit => {
if let Some(answer) = self.handle_submit()? {
break answer;
}
}
key => self.on_change(key),
}
};
let formatted = (self.formatter)(&final_answer);
finish_prompt_with_answer!(backend, self.message, &formatted, final_answer);
}
}
#[cfg(test)]
#[cfg(feature = "crossterm")]
mod test {
use super::Password;
use crate::{
terminal::crossterm::CrosstermTerminal,
ui::{Backend, RenderConfig},
validator::{ErrorMessage, Validation},
};
use crossterm::event::{KeyCode, KeyEvent};
macro_rules! text_to_events {
($text:expr) => {{
$text.chars().map(KeyCode::Char)
}};
}
macro_rules! password_test {
($(#[$meta:meta])? $name:ident,$input:expr,$output:expr,$prompt:expr) => {
#[test]
$(#[$meta])?
fn $name() {
let read: Vec<KeyEvent> = $input.into_iter().map(KeyEvent::from).collect();
let mut read = read.iter();
let mut write: Vec<u8> = Vec::new();
let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read);
let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap();
let ans = $prompt.prompt_with_backend(&mut backend).unwrap();
assert_eq!($output, ans);
}
};
}
password_test!(
empty,
vec![KeyCode::Char('q')],
"",
Password::new("").without_confirmation()
);
password_test!(
single_letter,
vec![KeyCode::Char('b'), KeyCode::Char('q')],
"b",
Password::new("").without_confirmation()
);
password_test!(
letters_and_enter,
text_to_events!("normal inputq"),
"normal input",
Password::new("").without_confirmation()
);
password_test!(
letters_and_enter_with_emoji,
text_to_events!("with emoji 🧘🏻♂️, 🌍, 🍞, 🚗, 📞q"),
"with emoji 🧘🏻♂️, 🌍, 🍞, 🚗, 📞",
Password::new("").without_confirmation()
);
password_test!(
input_and_correction,
{
let mut events = vec![];
events.append(&mut text_to_events!("anor").collect());
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.append(&mut text_to_events!("normal input").collect());
events.push(KeyCode::Char('q'));
events
},
"normal input",
Password::new("").without_confirmation()
);
password_test!(
input_and_excessive_correction,
{
let mut events = vec![];
events.append(&mut text_to_events!("anor").collect());
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.append(&mut text_to_events!("normal input").collect());
events.push(KeyCode::Char('q'));
events
},
"normal input",
Password::new("").without_confirmation()
);
password_test!(
input_correction_after_validation,
{
let mut events = vec![];
events.append(&mut text_to_events!("1234567890").collect());
events.push(KeyCode::Char('q'));
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.push(KeyCode::Backspace);
events.append(&mut text_to_events!("yes").collect());
events.push(KeyCode::Char('q'));
events
},
"12345yes",
Password::new("")
.without_confirmation()
.with_validator(|ans: &str| match ans.len() {
len if len > 5 && len < 10 => Ok(Validation::Valid),
_ => Ok(Validation::Invalid(ErrorMessage::Default)),
})
);
password_test!(
input_confirmation_same,
{
let mut events = vec![];
events.append(&mut text_to_events!("1234567890").collect());
events.push(KeyCode::Char('q'));
events.append(&mut text_to_events!("1234567890").collect());
events.push(KeyCode::Char('q'));
events
},
"1234567890",
Password::new("")
);
password_test!(
#[should_panic(expected = "Custom stream of characters has ended")]
input_confirmation_different,
{
let mut events = vec![];
events.append(&mut text_to_events!("1234567890").collect());
events.push(KeyCode::Char('q'));
events.append(&mut text_to_events!("abcdefghij").collect());
events.push(KeyCode::Char('q'));
events
},
"",
Password::new("")
);
}