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