use std::{ io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write}, os::fd::AsRawFd, }; #[cfg(feature = "mnemonic")] use keyfork_mnemonic_util::Wordlist; use crossterm::{ cursor, event::{read, Event, KeyCode, KeyModifiers}, style::{Print, PrintStyledContent, Stylize}, terminal, tty::IsTty, QueueableCommand, }; 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 = std::result::Result; pub struct PromptManager { read: BufReader, write: W, } impl PromptManager where R: Read + Sized, W: Write + AsRawFd + Sized, { pub fn new(read_handle: R, write_handle: W) -> Result { if !write_handle.is_tty() { return Err(Error::NotATTY); } Ok(Self { read: BufReader::new(read_handle), write: write_handle, }) } pub fn prompt_input(&mut self, prompt: &str) -> Result { let mut terminal = AlternateScreen::new(&mut self.write)?; 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))?; } } terminal.flush()?; let mut line = String::new(); self.read.read_line(&mut line)?; Ok(line) } #[cfg(feature = "mnemonic")] pub fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result { 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(); } KeyCode::Char('w') if k.modifiers.contains(KeyModifiers::CONTROL) => { 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(' '); } } 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) } // TODO: return secrecy::Secret pub fn prompt_passphrase(&mut self, prompt: &str) -> Result { 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 passphrase = 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 => { passphrase.push('\n'); break; } KeyCode::Backspace => { let passphrase_len = passphrase.len(); if passphrase.pop().is_some() && prefix_length + passphrase_len < cols as usize { terminal .queue(cursor::MoveLeft(1))? .queue(Print(" "))? .queue(cursor::MoveLeft(1))? .flush()?; } } KeyCode::Char(c) => { if prefix_length + passphrase.len() < (cols - 1) as usize { terminal.queue(Print("*"))?.flush()?; } passphrase.push(c); } _ => (), }, _ => (), } } Ok(passphrase) } pub fn prompt_message(&mut self, prompt: &str) -> Result<()> { let mut terminal = AlternateScreen::new(&mut self.write)?; let mut terminal = RawMode::new(&mut terminal)?; 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))?; } } terminal .queue(cursor::DisableBlinking)? .queue(cursor::MoveDown(1))? .queue(cursor::MoveToColumn(0))? .queue(PrintStyledContent(" OK ".negative()))? .flush()?; match read()? { Event::Key(k) => match k.code { KeyCode::Enter | KeyCode::Char(' ') => break, _ => (), }, _ => (), } } terminal.queue(cursor::EnableBlinking)?.flush()?; Ok(()) } } pub type DefaultPromptManager = PromptManager; pub fn default_prompt_manager() -> Result { PromptManager::new(stdin(), stderr()) }