yapp/
lib.rs

1//! ### Yet Another Password Prompt
2//!
3//! [![Crates.io Version](https://img.shields.io/crates/v/yapp)](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}