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
177
178
//! Actual implementation

use std::io::{self, stdout};

use crossterm::{
    cursor::{position, MoveLeft, MoveToNextLine},
    event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
    execute,
    style::Print,
    terminal,
};

/// Error setting up, reading, or shutting down
#[derive(thiserror::Error, Debug)]
#[error(transparent)]
pub struct Error {
    /// The actual error type
    inner: io::Error,
}

impl Error {
    /// Convert an [`io::Error`] into `Self`
    fn from_io(other: io::Error) -> Self {
        Self {
            inner: other,
        }
    }
}

/// Attempts to read a password from standard input
///
/// `echo` controls whether a replacement character should be printed each time
/// the user enters a character, and if so, which character. The result is
/// either a [`String`] or a [`crossterm::ErrorKind`]. Input begins wherever the
/// cursor was before calling this function, which is likely to be on its own
/// empty line.
///
/// # Errors
///
/// This function can fail if the password cannot be read from standard input.
pub fn try_scanpw(echo: Option<char>) -> Result<String, Error> {
    // Enter raw mode so we can control character echoing
    terminal::enable_raw_mode().map_err(Error::from_io)?;

    // In case anything was printed prior to the beginning of the input on the
    // same line, store the column the cursor started at
    let (max_left, _height) = position().map_err(Error::from_io)?;

    // The password
    let mut pw = String::new();

    loop {
        if let Event::Key(k) = event::read().map_err(Error::from_io)? {
            match k {
                // Password input completed
                KeyEvent {
                    kind: KeyEventKind::Press | KeyEventKind::Repeat,
                    code: KeyCode::Enter,
                    ..
                } => {
                    execute!(stdout(), MoveToNextLine(1))
                        .map_err(Error::from_io)?;
                    break;
                }

                // Handle backspace
                KeyEvent {
                    kind: KeyEventKind::Press | KeyEventKind::Repeat,
                    code: KeyCode::Backspace,
                    ..
                } => {
                    // If echo characters are enabled and any exist, remove the
                    // rightmost one
                    if echo.is_some() {
                        let (cur_left, _height) =
                            position().map_err(Error::from_io)?;

                        // True if the next position isn't past the left of the
                        // column where the cursor started
                        let not_too_far = cur_left
                            .checked_sub(1)
                            .map_or(false, |np| np >= max_left);

                        if not_too_far {
                            execute!(
                                stdout(),
                                MoveLeft(1),
                                Print(" "),
                                MoveLeft(1)
                            )
                            .map_err(Error::from_io)?;
                        }
                    }

                    // Delete the character from the password
                    pw.pop();
                }

                // Pass Ctrl+C through as a signal like normal
                KeyEvent {
                    kind: KeyEventKind::Press | KeyEventKind::Repeat,
                    code: KeyCode::Char('c'),
                    modifiers,
                    ..
                } if modifiers == KeyModifiers::CONTROL => {
                    // Reset the terminal back to normal and exit
                    terminal::disable_raw_mode().map_err(Error::from_io)?;

                    ctrl_c_self();
                }

                // Normal character input
                KeyEvent {
                    kind: KeyEventKind::Press | KeyEventKind::Repeat,
                    code: KeyCode::Char(c),
                    ..
                } => {
                    if let Some(c) = echo {
                        execute!(stdout(), Print(c)).map_err(Error::from_io)?;
                    }

                    // Add the character to the password
                    pw.push(c);
                }

                // Ignore other cases
                _ => (),
            }
        }
    }

    // Reset the terminal back to normal
    terminal::disable_raw_mode().map_err(Error::from_io)?;

    Ok(pw)
}

/// Deduplicates the documentation for `ctrl_c_self`
macro_rules! ctrl_c_self_doc {
    () => {
        "Send the signal generated by `C-c` to the current process"
    };
}

#[cfg(unix)]
#[doc = ctrl_c_self_doc!()]
fn ctrl_c_self() {
    use nix::sys::signal::{raise, Signal::SIGINT};

    assert!(
        raise(SIGINT).is_ok(),
        "failed to send SIGINT to the current process"
    );
}

#[cfg(windows)]
#[doc = ctrl_c_self_doc!()]
fn ctrl_c_self() {
    use windows::Win32::System::{
        Console::{GenerateConsoleCtrlEvent, CTRL_C_EVENT},
        Threading::GetCurrentProcessId,
    };

    let res = unsafe {
        GenerateConsoleCtrlEvent(CTRL_C_EVENT, GetCurrentProcessId())
    };

    assert!(
        res.ok().is_ok(),
        "failed to send CTRL_C_EVENT to the current process"
    );
}

#[cfg(not(any(unix, windows)))]
#[doc = ctrl_c_self_doc!()]
fn ctrl_c_self() {
    std::process::exit(1);
}