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);
}