use std::io::{IsTerminal, Write};
use std::time::Duration;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crossterm::terminal;
pub fn is_interactive() -> bool {
std::io::stdin().is_terminal()
}
pub fn read_password(prompt: &str) -> std::io::Result<Option<String>> {
eprint!("{prompt}");
std::io::stderr().flush()?;
terminal::enable_raw_mode()?;
let result = read_line_raw();
let _ = terminal::disable_raw_mode();
eprintln!();
result
}
const MAX_DISCARDED_EVENTS: usize = 4096;
pub fn discard_type_ahead() {
if !is_interactive() {
return;
}
for _ in 0..MAX_DISCARDED_EVENTS {
match event::poll(Duration::ZERO) {
Ok(true) => {
if event::read().is_err() {
return;
}
}
_ => return,
}
}
}
fn read_line_raw() -> std::io::Result<Option<String>> {
let mut buf = String::new();
loop {
let Event::Key(key) = event::read()? else {
continue;
};
if key.kind == KeyEventKind::Release {
continue;
}
match password_key(&key) {
PasswordKey::Done => return Ok(Some(buf).filter(|s| !s.is_empty())),
PasswordKey::Cancel => return Ok(None),
PasswordKey::Erase => {
buf.pop();
}
PasswordKey::Text(c) => buf.push(c),
PasswordKey::Nothing => {}
}
}
}
#[derive(Debug, PartialEq)]
enum PasswordKey {
Done,
Cancel,
Erase,
Text(char),
Nothing,
}
fn password_key(key: &KeyEvent) -> PasswordKey {
match key.code {
KeyCode::Enter => PasswordKey::Done,
KeyCode::Esc => PasswordKey::Cancel,
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => PasswordKey::Cancel,
KeyCode::Backspace => PasswordKey::Erase,
KeyCode::Char(c)
if !key.modifiers.contains(KeyModifiers::CONTROL)
|| crate::shared::keys::is_altgr_text(key) =>
{
PasswordKey::Text(c)
}
_ => PasswordKey::Nothing,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_password_takes_text_altgr_included_and_no_shortcut() {
let key = |code, modifiers| KeyEvent::new(code, modifiers);
let ctrl_alt = KeyModifiers::CONTROL | KeyModifiers::ALT;
crate::shared::keys::pretend_altgr(Some('@'));
for (k, want) in [
(
key(KeyCode::Char('p'), KeyModifiers::NONE),
PasswordKey::Text('p'),
),
(
key(KeyCode::Char('P'), KeyModifiers::SHIFT),
PasswordKey::Text('P'),
),
(key(KeyCode::Char('@'), ctrl_alt), PasswordKey::Text('@')),
(key(KeyCode::Char('a'), ctrl_alt), PasswordKey::Nothing),
(
key(KeyCode::Char('v'), KeyModifiers::CONTROL),
PasswordKey::Nothing,
),
(
key(KeyCode::Char('c'), KeyModifiers::CONTROL),
PasswordKey::Cancel,
),
(key(KeyCode::Esc, KeyModifiers::NONE), PasswordKey::Cancel),
(key(KeyCode::Enter, KeyModifiers::NONE), PasswordKey::Done),
(
key(KeyCode::Backspace, KeyModifiers::NONE),
PasswordKey::Erase,
),
(key(KeyCode::Tab, KeyModifiers::NONE), PasswordKey::Nothing),
] {
assert_eq!(password_key(&k), want, "{k:?}");
}
crate::shared::keys::pretend_altgr(None);
}
}