//! Prompt display and interaction management. use std::io::IsTerminal; #[cfg(feature = "mnemonic")] use keyfork_mnemonic::Wordlist; pub mod headless; pub mod terminal; pub mod validators; pub use headless::Headless; pub use terminal::{default_terminal, DefaultTerminal, Terminal}; /// An error occurred in the process of loading a default handler. #[derive(thiserror::Error, Debug)] pub enum DefaultHandlerError { /// An invalid handler was loaded. #[error("An invalid handler was loaded: {handler} ({error})")] InvalidHandler { /// The handle that caused an error. handler: String, /// The error that occurred. error: String, }, /// An unknown handler was requested. #[error("An unknown handler was requested: {handler}")] UnknownHandler { /// The requested, but unknown, handler. handler: String, }, } /// An error occurred while displaying a prompt. #[derive(thiserror::Error, Debug)] pub enum Error { /// The given handler is not a TTY and can't be used to display prompts. #[error("The given handler is not a TTY")] NotATTY, /// Validating user input failed. #[error("Validation of the input failed after {0} retries (last error: {1})")] Validation(u8, String), /// A ctrl-c interrupt was caught by the handler. #[error("User pressed ctrl-c, terminating the session")] CtrlC, /// An error occurred while interacting with a terminal. #[error("IO Error: {0}")] IO(#[from] std::io::Error), } #[allow(missing_docs)] pub type Result = std::result::Result; /// A message displayed by [`PromptHandler::prompt_message`]. pub enum Message { /// A textual message, wrapping at space boundaries when reaching the end of the terminal. Text(String), /// A data message, with no word wrapping, and automatic hiding of the message when a terminal /// is too small. Data(String), } #[doc(hidden)] pub type BoxResult = std::result::Result<(), Box>; /// A trait to allow displaying prompts and accepting input. pub trait PromptHandler { /// Prompt the user for input. /// /// # Errors /// The method may return an error if the message was not able to be displayed or if the input /// could not be read. fn prompt_input(&mut self, prompt: &str) -> Result; /// Prompt the user for input based on a wordlist. A language must be specified as the generic /// parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist. /// /// # Errors /// The method may return an error if the message was not able to be displayed or if the input /// could not be read. fn prompt_wordlist(&mut self, prompt: &str, wordlist: &[&str]) -> Result; /// Prompt the user for a passphrase, which is hidden while typing. /// /// # Errors /// The method may return an error if the message was not able to be displayed or if the input /// could not be read. fn prompt_passphrase(&mut self, prompt: &str) -> Result; /// Prompt the user with a [`Message`]. /// /// # Errors /// The method may return an error if the message was not able to be displayed or if an error /// occurred while waiting for the user to dismiss the message. fn prompt_message(&mut self, prompt: Message) -> Result<()>; /// 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 /// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist. /// /// This method MUST NOT be used directly. Instead, use /// [`prompt_validated_wordlist`]. /// /// # Errors /// The method may return an error if the message was not able to be displayed, if the input /// could not be read, or if the parser returned an error. fn prompt_validated_wordlist( &mut self, prompt: &str, retries: u8, wordlist: &[&str], validator_fn: &mut dyn FnMut(String) -> BoxResult, ) -> Result<(), Error>; /// Prompt the user for a passphrase, which is hidden while typing, and validate the passphrase /// using a provided parser function, returning the type from the parser. /// /// This method MUST NOT be used directly. Instead, use /// [`prompt_validated_wordlist`]. /// /// # Errors /// The method may return an error if the message was not able to be displayed, if the input /// could not be read, or if the parser returned an error. fn prompt_validated_passphrase( &mut self, prompt: &str, retries: u8, validator_fn: &mut dyn FnMut(String) -> BoxResult, ) -> Result<(), Error>; } /// 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 /// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist. /// /// # Errors /// The method may return an error if the message was not able to be displayed, if the input /// could not be read, or if the parser returned an error. #[cfg(feature = "mnemonic")] #[allow(clippy::missing_panics_doc)] pub fn prompt_validated_wordlist( handler: &mut dyn PromptHandler, prompt: &str, retries: u8, validator_fn: &dyn Fn(String) -> Result>, ) -> Result where X: Wordlist, { let wordlist = X::get_singleton(); let words = wordlist.to_str_array(); let mut opt: Option = None; handler.prompt_validated_wordlist(prompt, retries, &words, &mut |string| { opt = Some(validator_fn(string)?); Ok(()) })?; Ok(opt.unwrap()) } /// Prompt the user for a passphrase, which is hidden while typing, and validate the passphrase /// using a provided parser function, returning the type from the parser. /// /// # Errors /// The method may return an error if the message was not able to be displayed, if the input /// could not be read, or if the parser returned an error. #[allow(clippy::missing_panics_doc)] pub fn prompt_validated_passphrase( handler: &mut dyn PromptHandler, prompt: &str, retries: u8, validator_fn: impl Fn(String) -> Result>, ) -> Result { let mut opt: Option = None; handler.prompt_validated_passphrase(prompt, retries, &mut |string| { opt = Some(validator_fn(string)?); Ok(()) })?; Ok(opt.unwrap()) } /// Get a Prompt Handler that is most suitable for the given environment. /// /// The following handlers will be used based on the `KEYFORK_PROMPT_TYPE` variable: /// * `KEYFORK_PROMPT_TYPE=terminal`: [`DefaultTerminal`] /// * `KEYFORK_PROMPT_TYPE=headless`: [`Headless`] /// /// Otherwise, the following heuristics are followed: /// * [`std::io::IsTerminal::is_terminal`]: [`DefaultTerminal`] /// * default: [`Headless`] /// /// # Errors /// /// The function will return an error if a specific handler was requested but could not be /// constructed. pub fn default_handler() -> Result, DefaultHandlerError> { if let Some((_, value)) = std::env::vars().find(|(k, _)| k == "KEYFORK_PROMPT_TYPE") { match value.as_str() { "terminal" => match default_terminal() { Ok(terminal) => return Ok(Box::new(terminal)), Err(e) => { return Err(DefaultHandlerError::InvalidHandler { handler: value, error: e.to_string(), }) } }, "headless" => { return Ok(Box::new(Headless::new())); } _ => { return Err(DefaultHandlerError::UnknownHandler { handler: value }); } } } // we can revert stdin to a readable input by using raw mode, but we can't do the more // significant operations if we don't have access to a terminal stderr if std::io::stderr().is_terminal() { // because this is a "guessed" handler, let's take the nice route and not error, just skip. if let Ok(terminal) = default_terminal() { return Ok(Box::new(terminal)); } } Ok(Box::new(Headless::new())) }