From 3fd992d5826a93e76dd230d73d4343d52f6c466a Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 1 Aug 2024 07:16:05 -0400 Subject: [PATCH] keyfork-prompt: add choice mechanism --- .../examples/test-basic-prompt.rs | 44 +++++------ crates/util/keyfork-prompt/src/lib.rs | 25 +++++- crates/util/keyfork-prompt/src/terminal.rs | 78 ++++++++++++++++++- 3 files changed, 121 insertions(+), 26 deletions(-) diff --git a/crates/util/keyfork-prompt/examples/test-basic-prompt.rs b/crates/util/keyfork-prompt/examples/test-basic-prompt.rs index 04c7391..5b06758 100644 --- a/crates/util/keyfork-prompt/examples/test-basic-prompt.rs +++ b/crates/util/keyfork-prompt/examples/test-basic-prompt.rs @@ -3,36 +3,34 @@ use std::io::{stdin, stdout}; use keyfork_prompt::{ - validators::{mnemonic, Validator}, - Terminal, PromptHandler, + MaybeIdentifier, PromptHandler, Terminal, }; -use keyfork_mnemonic_util::English; +#[derive(PartialEq, Eq, Debug)] +pub enum Example { + RetryQR, + UseMnemonic, +} + +impl MaybeIdentifier for Example {} + +impl std::fmt::Display for Example { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Example::RetryQR => f.write_str("Retry QR Code"), + Example::UseMnemonic => f.write_str("Use Mnemonic"), + } + } +} fn main() -> Result<(), Box> { let mut mgr = Terminal::new(stdin(), stdout())?; - let transport_validator = mnemonic::MnemonicSetValidator { - word_lengths: [9, 24], - }; - let combine_validator = mnemonic::MnemonicSetValidator { - word_lengths: [24, 48], - }; - let mnemonics = mgr.prompt_validated_wordlist::( - "Enter a 9-word and 24-word mnemonic: ", - 3, - transport_validator.to_fn(), + let choice = mgr.prompt_choice( + "Unable to detect QR code.", + &[Example::RetryQR, Example::UseMnemonic], )?; - assert_eq!(mnemonics[0].as_bytes().len(), 12); - assert_eq!(mnemonics[1].as_bytes().len(), 32); - - let mnemonics = mgr.prompt_validated_wordlist::( - "Enter a 24 and 48-word mnemonic: ", - 3, - combine_validator.to_fn(), - )?; - assert_eq!(mnemonics[0].as_bytes().len(), 32); - assert_eq!(mnemonics[1].as_bytes().len(), 64); + dbg!(choice); Ok(()) } diff --git a/crates/util/keyfork-prompt/src/lib.rs b/crates/util/keyfork-prompt/src/lib.rs index e12597b..d39c602 100644 --- a/crates/util/keyfork-prompt/src/lib.rs +++ b/crates/util/keyfork-prompt/src/lib.rs @@ -8,7 +8,7 @@ use keyfork_mnemonic_util::Wordlist; /// pub mod terminal; pub mod validators; -pub use terminal::{Terminal, DefaultTerminal, default_terminal}; +pub use terminal::{default_terminal, DefaultTerminal, Terminal}; /// An error occurred while displaying a prompt. #[derive(thiserror::Error, Debug)] @@ -42,6 +42,12 @@ pub enum Message { Data(String), } +pub trait MaybeIdentifier { + fn identifier(&self) -> Option { + None + } +} + /// A trait to allow displaying prompts and accepting input. pub trait PromptHandler { /// Prompt the user for input. @@ -58,7 +64,9 @@ pub trait PromptHandler { /// The method may return an error if the message was not able to be displayed or if the input /// could not be read. #[cfg(feature = "mnemonic")] - fn prompt_wordlist(&mut self, prompt: &str) -> Result where X: Wordlist; + fn prompt_wordlist(&mut self, prompt: &str) -> Result + where + X: Wordlist; /// Prompt the user for input based on a wordlist, while validating the wordlist using a /// provided parser function, returning the type from the parser. A language must be specified @@ -97,6 +105,19 @@ pub trait PromptHandler { validator_fn: impl Fn(String) -> Result>, ) -> Result; + /// Prompt the user to select a choice between multiple options. + /// + /// # Errors + /// The metho may return an error if the message was not able to be displayed or if a choice + /// could not be received. + fn prompt_choice<'a, T>( + &mut self, + prompt: &str, + choices: &'a [T], + ) -> Result<&'a T, Error> + where + T: std::fmt::Display + PartialEq + MaybeIdentifier; + /// Prompt the user with a [`Message`]. /// /// # Errors diff --git a/crates/util/keyfork-prompt/src/terminal.rs b/crates/util/keyfork-prompt/src/terminal.rs index e431215..68e7c11 100644 --- a/crates/util/keyfork-prompt/src/terminal.rs +++ b/crates/util/keyfork-prompt/src/terminal.rs @@ -15,7 +15,7 @@ use keyfork_crossterm::{ use keyfork_bug::bug; -use crate::{Error, Message, PromptHandler, Wordlist}; +use crate::{Error, MaybeIdentifier, Message, PromptHandler, Wordlist}; #[allow(missing_docs)] pub type Result = std::result::Result; @@ -122,6 +122,9 @@ where W: Write + AsRawFd, { fn drop(&mut self) { + self.write + .execute(cursor::Show) + .expect(bug!("can't enable cursor blinking")); self.write .execute(DisableBracketedPaste) .expect(bug!("can't restore bracketed paste")); @@ -455,6 +458,79 @@ where Ok(passphrase) } + fn prompt_choice<'a, T>(&mut self, prompt: &str, choices: &'a [T]) -> Result<&'a T, Error> + where + T: std::fmt::Display + PartialEq + MaybeIdentifier, + { + let mut terminal = self.lock().alternate_screen()?.raw_mode()?; + + terminal + .queue(terminal::Clear(terminal::ClearType::All))? + .queue(cursor::MoveTo(0, 0))? + .queue(cursor::Hide)?; + + for line in prompt.lines() { + terminal.queue(Print(line))?; + terminal + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + + } + terminal.flush()?; + + let mut active_choice = 0; + + let mut redraw = |active_choice| { + terminal.queue(cursor::MoveToColumn(0))?; + + let mut iter = choices.iter().enumerate().peekable(); + while let Some((i, choice)) = iter.next() { + // if active choice, flip foreground and background + // if active choice, wrap in [] + // if not, wrap in spaces, to preserve spacing + if i == active_choice { + terminal.queue(PrintStyledContent(format!("[{choice}]").to_string().reverse()))?; + } else { + terminal.queue(Print(format!(" {choice} ").to_string()))?; + } + if iter.peek().is_some() { + terminal.queue(Print(" "))?; + } + } + terminal.flush()?; + Ok::<_, Error>(()) + }; + + redraw(active_choice)?; + + loop { + if let Event::Key(k) = read()? { + match k.code { + KeyCode::Char('c') if k.modifiers.contains(KeyModifiers::CONTROL) => { + return Err(Error::CtrlC); + } + KeyCode::Left => { + eprintln!("{active_choice}"); + // prevent underflow + // if 0, max is 1, -1 is 0, no underflow + // if 1, max is 1, -1 is 0 + // if 2 or higher, max is 2 or higher, -1 is fine + active_choice = std::cmp::max(1, active_choice) - 1; + } + KeyCode::Right => { + eprintln!("{active_choice}"); + active_choice = std::cmp::min(choices.len() - 1, active_choice + 1); + } + KeyCode::Enter => { + return Ok(&choices[active_choice]); + } + _ => {} + } + } + redraw(active_choice)?; + } + } + fn prompt_message(&mut self, prompt: impl Borrow) -> Result<()> { let mut terminal = self.lock().alternate_screen()?.raw_mode()?;