283 lines
9.8 KiB
Rust
283 lines
9.8 KiB
Rust
//! 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),
|
|
|
|
/// An unexpected error occurred.
|
|
#[error("{0}")]
|
|
Custom(String),
|
|
}
|
|
|
|
#[allow(missing_docs)]
|
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
|
|
|
/// 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),
|
|
}
|
|
|
|
/// A type that may represent an identifier to be used when using a choice prompt.
|
|
pub trait Choice: std::fmt::Display {
|
|
/// The identifier for the type.
|
|
fn identifier(&self) -> Option<char> {
|
|
None
|
|
}
|
|
}
|
|
|
|
// this way, we can make Box<dyn T> from &T
|
|
impl<T: Choice> Choice for &T {
|
|
fn identifier(&self) -> Option<char> {
|
|
Choice::identifier(*self)
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
pub type BoxResult = std::result::Result<(), Box<dyn std::error::Error>>;
|
|
|
|
/// 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<String>;
|
|
|
|
/// 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<String>;
|
|
|
|
/// 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<String>;
|
|
|
|
/// 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 a choice between the provided options. The returned value is the index
|
|
/// of the given choice.
|
|
///
|
|
/// This method SHOULD NOT be used directly. Instead, use [`prompt_choice`].
|
|
///
|
|
/// # 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_choice_num(&mut self, prompt: &str, choices: &[Box<dyn Choice>]) -> Result<usize>;
|
|
|
|
/// 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 a choice between the provided options. The returned value is the selected
|
|
/// choice.
|
|
///
|
|
/// # Errors
|
|
/// The method may return an error if the message was not able to be displayed or if the input
|
|
/// could not be read.
|
|
#[allow(clippy::missing_panics_doc)]
|
|
pub fn prompt_choice<T>(
|
|
handler: &mut dyn PromptHandler,
|
|
prompt: &str,
|
|
choices: &'static [T],
|
|
) -> Result<T>
|
|
where
|
|
T: Choice + Copy + 'static,
|
|
{
|
|
let boxed_choices = choices
|
|
.iter()
|
|
.map(|c| Box::new(c) as Box<dyn Choice>)
|
|
.collect::<Vec<_>>();
|
|
let choice = handler.prompt_choice_num(prompt, boxed_choices.as_slice())?;
|
|
Ok(choices[choice])
|
|
}
|
|
|
|
/// 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<X, V>(
|
|
handler: &mut dyn PromptHandler,
|
|
prompt: &str,
|
|
retries: u8,
|
|
validator_fn: &dyn Fn(String) -> Result<V, Box<dyn std::error::Error>>,
|
|
) -> Result<V, Error>
|
|
where
|
|
X: Wordlist,
|
|
{
|
|
let wordlist = X::get_singleton();
|
|
let words = wordlist.to_str_array();
|
|
let mut opt: Option<V> = 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<V>(
|
|
handler: &mut dyn PromptHandler,
|
|
prompt: &str,
|
|
retries: u8,
|
|
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
|
|
) -> Result<V, Error> {
|
|
let mut opt: Option<V> = 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<Box<dyn PromptHandler>, 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()))
|
|
}
|