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
//!
//! [![Crates.io Version](https://img.shields.io/crates/v/yapp)](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)
    }
}