use console::Key;
use std::io::{self, Write};
#[cfg(not(test))]
use {
console::Term,
std::io::{stdin, stdout, IsTerminal},
};
#[cfg(test)]
use tests::mocks::{stdin, stdout, TermMock as Term};
#[cfg(test)]
mod tests;
pub trait PasswordReader {
fn read_password(&mut self) -> io::Result<String>;
fn read_password_with_prompt(&mut self, prompt: &str) -> io::Result<String>;
}
pub trait IsInteractive {
fn is_interactive(&self) -> bool;
}
#[derive(Debug, Default, Copy, Clone)]
pub struct Yapp {
echo_symbol: Option<char>,
}
impl PasswordReader for Yapp {
fn read_password(&mut self) -> io::Result<String> {
if self.is_interactive() {
self.read_interactive()
} else {
self.read_non_interactive()
}
}
fn read_password_with_prompt(&mut self, prompt: &str) -> io::Result<String> {
write!(stdout(), "{prompt}")?;
stdout().flush()?;
self.read_password()
}
}
impl IsInteractive for Yapp {
fn is_interactive(&self) -> bool {
stdin().is_terminal()
}
}
impl Yapp {
pub const fn new() -> Self {
Yapp { echo_symbol: None }
}
pub fn with_echo_symbol<C>(mut self, s: C) -> Self
where
C: Into<Option<char>>,
{
self.echo_symbol = s.into();
self
}
fn read_non_interactive(&self) -> io::Result<String> {
let mut input = String::new();
let stdin = stdin();
stdin.read_line(&mut input)?;
if let Some(s) = self.echo_symbol {
writeln!(stdout(), "{}", format!("{s}").repeat(input.len()))?;
}
Ok(input)
}
fn read_interactive(&self) -> io::Result<String> {
let mut term = Term::stdout();
let mut input = String::new();
loop {
let key = term.read_key()?;
match key {
Key::Char(c) => {
input.push(c);
if let Some(s) = self.echo_symbol {
write!(term, "{s}")?;
}
}
Key::Backspace if !input.is_empty() => {
input.pop();
term.clear_chars(1)?;
}
Key::Enter => {
term.write_line("")?;
break;
}
_ => {}
}
}
Ok(input)
}
}