yapp/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
//! ### Yet Another Password Prompt
//!
//! [](https://crates.io/crates/yapp)
//!
//! A library for reading passwords from the user.
//!
//! This library provides a `PasswordReader` trait for reading passwords from the user, and
//! `IsInteractive` trait for checking if terminal is attended by the user or e.g. ran as a
//! background service.
//! It includes a default implementation of the `PasswordReader` and `IsInteractive` traits, `Yapp`,
//! which can read passwords from an interactive terminal or from standard input.
//! The library also includes a `new` function for creating a new `PasswordReader + IsInteractive`
//! instance.
//!
//! ### Features
//!
//! * Reads user passwords from the input, optionally with a prompt and
//! echoing replacement symbols (`*`, or another of your choice).
//! * Reads passwords interactively or non-interactively (e.g. when input is redirected through
//! a pipe).
//! * Using the `PasswordReader` (optionally `PasswordReader + IsInteractive`) trait in your code
//! allows for mocking the entire library in tests
//! (see an [example1](https://github.com/Caleb9/yapp/blob/main/examples/mock_yapp.rs) and
//! [example2](https://github.com/Caleb9/yapp/blob/main/examples/mock_yapp_with_is_interactive.rs))
//! * Thanks to using the `console` library underneath, it handles unicode
//! correctly (tested on Windows and Linux).
//!
//! ### Usage Example
//!
//! ```rust
//! use yapp::PasswordReader;
//!
//! fn my_func<P: PasswordReader>(yapp: &mut P) {
//! let password = yapp.read_password_with_prompt("Type your password: ").unwrap();
//! println!("You typed: {password}");
//! }
//!
//! fn main() {
//! let mut yapp = yapp::new().with_echo_symbol('*');
//! my_func(&mut yapp);
//! }
//! ```
//!
//! The `yapp::new()` function returns an instance of `PasswordReader + IsInteractive`
//! traits. Alternatively, instantiate with `yapp::Yapp::default()` to use the
//! concrete struct type.
//!
//! See [examples](https://github.com/Caleb9/yapp/tree/main/examples) for more.
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;
/// A trait for reading passwords from the user.
///
/// Use the `new` function to obtain a new instance
pub trait PasswordReader {
/// Reads a password from the user.
fn read_password(&mut self) -> io::Result<String>;
/// Reads a password from the user with a prompt.
fn read_password_with_prompt(&mut self, prompt: &str) -> io::Result<String>;
/// Sets the echoed replacement symbol for the password characters.
///
/// Set to None to not echo any characters
fn with_echo_symbol<C>(self, c: C) -> Self
where
C: 'static + Into<Option<char>>;
}
/// A trait providing interactivity check for `Yapp`
pub trait IsInteractive {
/// Checks if the terminal is interactive.
///
/// A terminal is not interactive when stdin is redirected, e.g. another process
/// pipes its output to this process' input.
fn is_interactive(&self) -> bool;
}
/// Creates a new password reader. Returns an instance of `PasswordReader` trait.
pub fn new() -> impl PasswordReader + IsInteractive {
Yapp::default()
}
/// An implementation of the `PasswordReader` trait.
#[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()
}
fn with_echo_symbol<C>(mut self, s: C) -> Self
where
C: Into<Option<char>>,
{
self.echo_symbol = s.into();
self
}
}
impl IsInteractive for Yapp {
fn is_interactive(&self) -> bool {
stdin().is_terminal()
}
}
impl Yapp {
/// Create new Yapp instance without echo symbol
pub const fn new() -> Self {
Yapp { echo_symbol: None }
}
/// Reads a password from a non-interactive terminal.
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)
}
/// Reads a password from an interactive terminal.
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)
}
}