2023-12-21 17:04:35 +00:00
|
|
|
use std::{
|
2023-12-21 20:44:57 +00:00
|
|
|
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},
|
2023-12-21 17:04:35 +00:00
|
|
|
os::fd::AsRawFd,
|
|
|
|
};
|
|
|
|
|
2023-12-22 19:39:25 +00:00
|
|
|
#[cfg(feature = "mnemonic")]
|
|
|
|
use keyfork_mnemonic_util::Wordlist;
|
|
|
|
|
2023-12-21 17:04:35 +00:00
|
|
|
use crossterm::{
|
2023-12-21 20:44:57 +00:00
|
|
|
cursor,
|
2023-12-26 18:35:56 +00:00
|
|
|
event::{read, Event, KeyCode, KeyModifiers},
|
2023-12-21 19:02:42 +00:00
|
|
|
style::{Print, PrintStyledContent, Stylize},
|
2023-12-21 17:04:35 +00:00
|
|
|
terminal,
|
|
|
|
tty::IsTty,
|
2023-12-21 19:02:42 +00:00
|
|
|
QueueableCommand,
|
2023-12-21 17:04:35 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
mod alternate_screen;
|
|
|
|
mod raw_mode;
|
|
|
|
use alternate_screen::*;
|
|
|
|
use raw_mode::*;
|
|
|
|
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum Error {
|
|
|
|
#[error("The given handler is not a TTY")]
|
|
|
|
NotATTY,
|
|
|
|
|
|
|
|
#[error("IO Error: {0}")]
|
|
|
|
IO(#[from] std::io::Error),
|
|
|
|
}
|
|
|
|
|
|
|
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
|
|
|
|
|
|
|
pub struct PromptManager<R, W> {
|
2023-12-21 17:12:52 +00:00
|
|
|
read: BufReader<R>,
|
|
|
|
write: W,
|
2023-12-21 17:04:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<R, W> PromptManager<R, W>
|
|
|
|
where
|
|
|
|
R: Read + Sized,
|
|
|
|
W: Write + AsRawFd + Sized,
|
|
|
|
{
|
|
|
|
pub fn new(read_handle: R, write_handle: W) -> Result<Self> {
|
|
|
|
if !write_handle.is_tty() {
|
|
|
|
return Err(Error::NotATTY);
|
|
|
|
}
|
|
|
|
Ok(Self {
|
2023-12-21 17:12:52 +00:00
|
|
|
read: BufReader::new(read_handle),
|
|
|
|
write: write_handle,
|
2023-12-21 17:04:35 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn prompt_input(&mut self, prompt: &str) -> Result<String> {
|
2023-12-21 17:12:52 +00:00
|
|
|
let mut terminal = AlternateScreen::new(&mut self.write)?;
|
|
|
|
terminal
|
2023-12-21 19:02:42 +00:00
|
|
|
.queue(terminal::Clear(terminal::ClearType::All))?
|
2023-12-21 20:01:59 +00:00
|
|
|
.queue(cursor::MoveTo(0, 0))?;
|
|
|
|
let mut lines = prompt.lines().peekable();
|
|
|
|
while let Some(line) = lines.next() {
|
|
|
|
terminal.queue(Print(line))?;
|
|
|
|
if lines.peek().is_some() {
|
2023-12-21 20:44:57 +00:00
|
|
|
terminal
|
|
|
|
.queue(cursor::MoveDown(1))?
|
|
|
|
.queue(cursor::MoveToColumn(0))?;
|
2023-12-21 20:01:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
terminal.flush()?;
|
|
|
|
|
2023-12-21 17:04:35 +00:00
|
|
|
let mut line = String::new();
|
2023-12-21 17:12:52 +00:00
|
|
|
self.read.read_line(&mut line)?;
|
2023-12-21 17:04:35 +00:00
|
|
|
Ok(line)
|
|
|
|
}
|
|
|
|
|
2023-12-22 19:39:25 +00:00
|
|
|
#[cfg(feature = "mnemonic")]
|
|
|
|
pub fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String> {
|
|
|
|
let mut terminal = AlternateScreen::new(&mut self.write)?;
|
|
|
|
let mut terminal = RawMode::new(&mut terminal)?;
|
|
|
|
|
|
|
|
terminal
|
|
|
|
.queue(terminal::Clear(terminal::ClearType::All))?
|
|
|
|
.queue(cursor::MoveTo(0, 0))?;
|
|
|
|
let mut lines = prompt.lines().peekable();
|
|
|
|
let mut prefix_length = 0;
|
|
|
|
while let Some(line) = lines.next() {
|
|
|
|
prefix_length = line.len();
|
|
|
|
terminal.queue(Print(line))?;
|
|
|
|
if lines.peek().is_some() {
|
|
|
|
terminal
|
|
|
|
.queue(cursor::MoveDown(1))?
|
|
|
|
.queue(cursor::MoveToColumn(0))?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
terminal.flush()?;
|
|
|
|
|
|
|
|
let (mut cols, mut _rows) = terminal::size()?;
|
|
|
|
let mut input = String::new();
|
|
|
|
|
|
|
|
loop {
|
|
|
|
match read()? {
|
|
|
|
Event::Resize(new_cols, new_rows) => {
|
|
|
|
cols = new_cols;
|
|
|
|
_rows = new_rows;
|
|
|
|
}
|
|
|
|
Event::Key(k) => match k.code {
|
|
|
|
KeyCode::Enter => {
|
|
|
|
input.push('\n');
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
KeyCode::Backspace => {
|
|
|
|
input.pop();
|
|
|
|
}
|
2023-12-26 18:35:56 +00:00
|
|
|
KeyCode::Char('w') if k.modifiers.contains(KeyModifiers::CONTROL) => {
|
2023-12-27 00:06:59 +00:00
|
|
|
let mut has_deleted_text = true;
|
|
|
|
while input.pop().is_some_and(char::is_whitespace) {
|
|
|
|
has_deleted_text = false;
|
|
|
|
}
|
|
|
|
while input.pop().is_some_and(|c| !c.is_whitespace()) {
|
|
|
|
has_deleted_text = true;
|
|
|
|
}
|
|
|
|
if !input.is_empty() && has_deleted_text {
|
|
|
|
input.push(' ');
|
|
|
|
}
|
2023-12-26 18:35:56 +00:00
|
|
|
}
|
2023-12-22 19:39:25 +00:00
|
|
|
KeyCode::Char(' ') => {
|
|
|
|
if !input.chars().rev().next().is_some_and(char::is_whitespace) {
|
|
|
|
input.push(' ');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
KeyCode::Char(c) => {
|
|
|
|
input.push(c);
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
},
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
|
|
|
|
let usable_space = cols as usize - prefix_length - 1;
|
|
|
|
|
|
|
|
terminal
|
|
|
|
.queue(cursor::MoveToColumn(prefix_length as u16))?
|
|
|
|
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
|
|
|
|
.flush()?;
|
|
|
|
|
|
|
|
let printable_input_start = if input.len() > usable_space {
|
|
|
|
let start_index = input.len() - usable_space;
|
|
|
|
input
|
|
|
|
.chars()
|
|
|
|
.enumerate()
|
|
|
|
.skip(start_index)
|
|
|
|
.skip_while(|(_, ch)| !ch.is_whitespace())
|
|
|
|
.next()
|
|
|
|
.expect("any printable character")
|
|
|
|
.0
|
|
|
|
} else {
|
|
|
|
0
|
|
|
|
};
|
|
|
|
|
|
|
|
let printable_input = &input[printable_input_start..];
|
|
|
|
let mut iter = printable_input.split_whitespace().peekable();
|
|
|
|
|
|
|
|
while let Some(word) = iter.next() {
|
|
|
|
if wordlist.contains(word) {
|
|
|
|
terminal.queue(PrintStyledContent(word.green()))?;
|
|
|
|
} else {
|
|
|
|
terminal.queue(PrintStyledContent(word.red()))?;
|
|
|
|
}
|
|
|
|
if iter.peek().is_some()
|
|
|
|
|| printable_input
|
|
|
|
.chars()
|
|
|
|
.rev()
|
|
|
|
.next()
|
|
|
|
.is_some_and(char::is_whitespace)
|
|
|
|
{
|
|
|
|
terminal.queue(Print(" "))?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
terminal.flush()?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(input)
|
|
|
|
}
|
|
|
|
|
2023-12-21 17:04:35 +00:00
|
|
|
// TODO: return secrecy::Secret<String>
|
|
|
|
pub fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> {
|
2023-12-21 17:12:52 +00:00
|
|
|
let mut terminal = AlternateScreen::new(&mut self.write)?;
|
|
|
|
let mut terminal = RawMode::new(&mut terminal)?;
|
2023-12-21 20:01:59 +00:00
|
|
|
|
2023-12-21 17:12:52 +00:00
|
|
|
terminal
|
2023-12-21 19:02:42 +00:00
|
|
|
.queue(terminal::Clear(terminal::ClearType::All))?
|
2023-12-21 20:01:59 +00:00
|
|
|
.queue(cursor::MoveTo(0, 0))?;
|
|
|
|
let mut lines = prompt.lines().peekable();
|
2023-12-22 19:39:25 +00:00
|
|
|
let mut prefix_length = 0;
|
2023-12-21 20:01:59 +00:00
|
|
|
while let Some(line) = lines.next() {
|
2023-12-22 19:39:25 +00:00
|
|
|
prefix_length = line.len();
|
2023-12-21 20:01:59 +00:00
|
|
|
terminal.queue(Print(line))?;
|
|
|
|
if lines.peek().is_some() {
|
2023-12-21 20:44:57 +00:00
|
|
|
terminal
|
|
|
|
.queue(cursor::MoveDown(1))?
|
|
|
|
.queue(cursor::MoveToColumn(0))?;
|
2023-12-21 20:01:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
terminal.flush()?;
|
|
|
|
|
2023-12-22 19:39:25 +00:00
|
|
|
let (mut cols, mut _rows) = terminal::size()?;
|
|
|
|
|
2023-12-21 17:04:35 +00:00
|
|
|
let mut passphrase = String::new();
|
|
|
|
loop {
|
|
|
|
match read()? {
|
2023-12-22 19:39:25 +00:00
|
|
|
Event::Resize(new_cols, new_rows) => {
|
|
|
|
cols = new_cols;
|
|
|
|
_rows = new_rows;
|
|
|
|
}
|
2023-12-21 17:04:35 +00:00
|
|
|
Event::Key(k) => match k.code {
|
|
|
|
KeyCode::Enter => {
|
|
|
|
passphrase.push('\n');
|
|
|
|
break;
|
|
|
|
}
|
2023-12-21 20:01:59 +00:00
|
|
|
KeyCode::Backspace => {
|
2023-12-22 19:39:25 +00:00
|
|
|
let passphrase_len = passphrase.len();
|
|
|
|
if passphrase.pop().is_some()
|
|
|
|
&& prefix_length + passphrase_len < cols as usize
|
|
|
|
{
|
2023-12-21 20:01:59 +00:00
|
|
|
terminal
|
|
|
|
.queue(cursor::MoveLeft(1))?
|
|
|
|
.queue(Print(" "))?
|
|
|
|
.queue(cursor::MoveLeft(1))?
|
|
|
|
.flush()?;
|
|
|
|
}
|
|
|
|
}
|
2023-12-21 17:04:35 +00:00
|
|
|
KeyCode::Char(c) => {
|
2023-12-22 19:39:25 +00:00
|
|
|
if prefix_length + passphrase.len() < (cols - 1) as usize {
|
|
|
|
terminal.queue(Print("*"))?.flush()?;
|
|
|
|
}
|
2023-12-21 17:04:35 +00:00
|
|
|
passphrase.push(c);
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
},
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(passphrase)
|
|
|
|
}
|
2023-12-21 17:18:16 +00:00
|
|
|
|
|
|
|
pub fn prompt_message(&mut self, prompt: &str) -> Result<()> {
|
|
|
|
let mut terminal = AlternateScreen::new(&mut self.write)?;
|
|
|
|
let mut terminal = RawMode::new(&mut terminal)?;
|
2023-12-21 20:01:59 +00:00
|
|
|
|
2023-12-26 23:08:52 +00:00
|
|
|
loop {
|
|
|
|
// TODO: split on word boundaries
|
|
|
|
terminal
|
|
|
|
.queue(terminal::Clear(terminal::ClearType::All))?
|
|
|
|
.queue(cursor::MoveTo(0, 0))?;
|
|
|
|
let mut lines = prompt.lines().peekable();
|
|
|
|
while let Some(line) = lines.next() {
|
|
|
|
terminal.queue(Print(line))?;
|
|
|
|
if lines.peek().is_some() {
|
|
|
|
terminal
|
|
|
|
.queue(cursor::MoveDown(1))?
|
|
|
|
.queue(cursor::MoveToColumn(0))?;
|
|
|
|
}
|
2023-12-21 20:01:59 +00:00
|
|
|
}
|
2023-12-26 23:08:52 +00:00
|
|
|
terminal
|
|
|
|
.queue(cursor::DisableBlinking)?
|
|
|
|
.queue(cursor::MoveDown(1))?
|
|
|
|
.queue(cursor::MoveToColumn(0))?
|
|
|
|
.queue(PrintStyledContent(" OK ".negative()))?
|
|
|
|
.flush()?;
|
2023-12-21 20:01:59 +00:00
|
|
|
|
2023-12-21 17:18:16 +00:00
|
|
|
match read()? {
|
|
|
|
Event::Key(k) => match k.code {
|
|
|
|
KeyCode::Enter | KeyCode::Char(' ') => break,
|
|
|
|
_ => (),
|
|
|
|
},
|
|
|
|
_ => (),
|
|
|
|
}
|
2023-12-26 23:08:52 +00:00
|
|
|
|
|
|
|
|
2023-12-21 17:18:16 +00:00
|
|
|
}
|
2023-12-21 19:02:42 +00:00
|
|
|
terminal.queue(cursor::EnableBlinking)?.flush()?;
|
2023-12-21 17:18:16 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
2023-12-21 17:04:35 +00:00
|
|
|
}
|
2023-12-21 20:44:57 +00:00
|
|
|
|
|
|
|
pub type DefaultPromptManager = PromptManager<Stdin, Stderr>;
|
|
|
|
|
|
|
|
pub fn default_prompt_manager() -> Result<DefaultPromptManager> {
|
|
|
|
PromptManager::new(stdin(), stderr())
|
|
|
|
}
|