use std::fmt::Display;
use std::io::IsTerminal;
use clap::ArgMatches;
use inquire::{
ui::RenderConfig, Confirm, Editor, InquireError, MultiSelect, Password, PasswordDisplayMode,
Select, Text,
};
use crate::collector::InputCollector;
use crate::InputError;
fn map_inquire_error(e: InquireError) -> InputError {
match e {
InquireError::OperationCanceled | InquireError::OperationInterrupted => {
InputError::PromptCancelled
}
other => InputError::PromptFailed(other.to_string()),
}
}
pub struct InquireText {
message: String,
default: Option<String>,
placeholder: Option<String>,
help_message: Option<String>,
}
impl InquireText {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
default: None,
placeholder: None,
help_message: None,
}
}
pub fn default(mut self, default: impl Into<String>) -> Self {
self.default = Some(default.into());
self
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
pub fn help(mut self, help: impl Into<String>) -> Self {
self.help_message = Some(help.into());
self
}
pub fn prompt(&self) -> Result<String, InputError> {
if let Some(value) =
crate::responder::intercept_text(crate::PromptKind::Text, &self.message)?
{
return Ok(value);
}
let matches = crate::collector::empty_matches();
if !self.is_available(matches) {
return Err(InputError::NoInput);
}
self.collect(matches)?.ok_or(InputError::NoInput)
}
}
impl InputCollector<String> for InquireText {
fn name(&self) -> &'static str {
"prompt"
}
fn is_available(&self, _matches: &ArgMatches) -> bool {
std::io::stdin().is_terminal()
}
fn collect(&self, _matches: &ArgMatches) -> Result<Option<String>, InputError> {
let mut prompt = Text::new(&self.message);
if let Some(default) = &self.default {
prompt = prompt.with_default(default);
}
if let Some(placeholder) = &self.placeholder {
prompt = prompt.with_placeholder(placeholder);
}
if let Some(help) = &self.help_message {
prompt = prompt.with_help_message(help);
}
let result = prompt.prompt().map_err(map_inquire_error)?;
if result.is_empty() {
Ok(None)
} else {
Ok(Some(result))
}
}
fn can_retry(&self) -> bool {
true
}
}
pub struct InquireConfirm {
message: String,
default: Option<bool>,
help_message: Option<String>,
}
impl InquireConfirm {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
default: None,
help_message: None,
}
}
pub fn default(mut self, default: bool) -> Self {
self.default = Some(default);
self
}
pub fn help(mut self, help: impl Into<String>) -> Self {
self.help_message = Some(help.into());
self
}
pub fn prompt(&self) -> Result<bool, InputError> {
if let Some(value) =
crate::responder::intercept_bool(crate::PromptKind::Confirm, &self.message)?
{
return Ok(value);
}
let matches = crate::collector::empty_matches();
if !self.is_available(matches) {
return Err(InputError::NoInput);
}
self.collect(matches)?.ok_or(InputError::NoInput)
}
}
impl InputCollector<bool> for InquireConfirm {
fn name(&self) -> &'static str {
"prompt"
}
fn is_available(&self, _matches: &ArgMatches) -> bool {
std::io::stdin().is_terminal()
}
fn collect(&self, _matches: &ArgMatches) -> Result<Option<bool>, InputError> {
let mut prompt = Confirm::new(&self.message);
if let Some(default) = self.default {
prompt = prompt.with_default(default);
}
if let Some(help) = &self.help_message {
prompt = prompt.with_help_message(help);
}
let result = prompt.prompt().map_err(map_inquire_error)?;
Ok(Some(result))
}
fn can_retry(&self) -> bool {
true
}
}
pub struct InquireSelect<T> {
message: String,
options: Vec<T>,
help_message: Option<String>,
page_size: usize,
}
impl<T: Display + Clone + Send + Sync + 'static> InquireSelect<T> {
pub fn new(message: impl Into<String>, options: Vec<T>) -> Self {
Self {
message: message.into(),
options,
help_message: None,
page_size: 10,
}
}
pub fn help(mut self, help: impl Into<String>) -> Self {
self.help_message = Some(help.into());
self
}
pub fn page_size(mut self, size: usize) -> Self {
self.page_size = size;
self
}
pub fn prompt(&self) -> Result<T, InputError> {
if let Some(i) = crate::responder::intercept_choice(&self.message, self.options.len())? {
return Ok(self.options[i].clone());
}
let matches = crate::collector::empty_matches();
if !self.is_available(matches) {
return Err(InputError::NoInput);
}
self.collect(matches)?.ok_or(InputError::NoInput)
}
}
impl<T: Display + Clone + Send + Sync + 'static> InputCollector<T> for InquireSelect<T> {
fn name(&self) -> &'static str {
"prompt"
}
fn is_available(&self, _matches: &ArgMatches) -> bool {
std::io::stdin().is_terminal() && !self.options.is_empty()
}
fn collect(&self, _matches: &ArgMatches) -> Result<Option<T>, InputError> {
if self.options.is_empty() {
return Ok(None);
}
let mut prompt =
Select::new(&self.message, self.options.clone()).with_page_size(self.page_size);
if let Some(help) = &self.help_message {
prompt = prompt.with_help_message(help);
}
let result = prompt.prompt().map_err(map_inquire_error)?;
Ok(Some(result))
}
fn can_retry(&self) -> bool {
true
}
}
pub struct InquireMultiSelect<T> {
message: String,
options: Vec<T>,
help_message: Option<String>,
page_size: usize,
min_selections: Option<usize>,
max_selections: Option<usize>,
}
impl<T: Display + Clone + Send + Sync + 'static> InquireMultiSelect<T> {
pub fn new(message: impl Into<String>, options: Vec<T>) -> Self {
Self {
message: message.into(),
options,
help_message: None,
page_size: 10,
min_selections: None,
max_selections: None,
}
}
pub fn help(mut self, help: impl Into<String>) -> Self {
self.help_message = Some(help.into());
self
}
pub fn page_size(mut self, size: usize) -> Self {
self.page_size = size;
self
}
pub fn min_selections(mut self, min: usize) -> Self {
self.min_selections = Some(min);
self
}
pub fn max_selections(mut self, max: usize) -> Self {
self.max_selections = Some(max);
self
}
pub fn prompt(&self) -> Result<Vec<T>, InputError> {
if let Some(indices) =
crate::responder::intercept_choices(&self.message, self.options.len())?
{
return Ok(indices.iter().map(|&i| self.options[i].clone()).collect());
}
let matches = crate::collector::empty_matches();
if !self.is_available(matches) {
return Err(InputError::NoInput);
}
self.collect(matches)?.ok_or(InputError::NoInput)
}
}
impl<T: Display + Clone + Send + Sync + 'static> InputCollector<Vec<T>> for InquireMultiSelect<T> {
fn name(&self) -> &'static str {
"prompt"
}
fn is_available(&self, _matches: &ArgMatches) -> bool {
std::io::stdin().is_terminal() && !self.options.is_empty()
}
fn collect(&self, _matches: &ArgMatches) -> Result<Option<Vec<T>>, InputError> {
if self.options.is_empty() {
return Ok(None);
}
let mut prompt =
MultiSelect::new(&self.message, self.options.clone()).with_page_size(self.page_size);
if let Some(help) = &self.help_message {
prompt = prompt.with_help_message(help);
}
let result = prompt.prompt().map_err(map_inquire_error)?;
if let Some(min) = self.min_selections {
if result.len() < min {
return Err(InputError::ValidationFailed(format!(
"At least {} selection(s) required",
min
)));
}
}
if let Some(max) = self.max_selections {
if result.len() > max {
return Err(InputError::ValidationFailed(format!(
"At most {} selection(s) allowed",
max
)));
}
}
if result.is_empty() {
Ok(None)
} else {
Ok(Some(result))
}
}
fn can_retry(&self) -> bool {
true
}
}
pub struct InquirePassword {
message: String,
help_message: Option<String>,
display_mode: PasswordDisplayMode,
confirmation: Option<String>,
}
impl InquirePassword {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
help_message: None,
display_mode: PasswordDisplayMode::Masked,
confirmation: None,
}
}
pub fn help(mut self, help: impl Into<String>) -> Self {
self.help_message = Some(help.into());
self
}
pub fn hidden(mut self) -> Self {
self.display_mode = PasswordDisplayMode::Hidden;
self
}
pub fn masked(mut self) -> Self {
self.display_mode = PasswordDisplayMode::Masked;
self
}
pub fn full(mut self) -> Self {
self.display_mode = PasswordDisplayMode::Full;
self
}
pub fn with_confirmation(mut self, message: impl Into<String>) -> Self {
self.confirmation = Some(message.into());
self
}
pub fn prompt(&self) -> Result<String, InputError> {
if let Some(value) =
crate::responder::intercept_text(crate::PromptKind::Password, &self.message)?
{
return Ok(value);
}
let matches = crate::collector::empty_matches();
if !self.is_available(matches) {
return Err(InputError::NoInput);
}
self.collect(matches)?.ok_or(InputError::NoInput)
}
}
impl InputCollector<String> for InquirePassword {
fn name(&self) -> &'static str {
"prompt"
}
fn is_available(&self, _matches: &ArgMatches) -> bool {
std::io::stdin().is_terminal()
}
fn collect(&self, _matches: &ArgMatches) -> Result<Option<String>, InputError> {
let mut prompt = Password::new(&self.message).with_display_mode(self.display_mode);
if let Some(help) = &self.help_message {
prompt = prompt.with_help_message(help);
}
if let Some(confirmation) = &self.confirmation {
prompt = prompt.with_display_toggle_enabled();
prompt = prompt.with_custom_confirmation_message(confirmation);
}
let result = prompt.prompt().map_err(map_inquire_error)?;
if result.is_empty() {
Ok(None)
} else {
Ok(Some(result))
}
}
fn can_retry(&self) -> bool {
true
}
}
pub struct InquireEditor {
message: String,
help_message: Option<String>,
file_extension: String,
predefined_text: Option<String>,
render_config: Option<RenderConfig<'static>>,
}
impl InquireEditor {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
help_message: None,
file_extension: ".txt".to_string(),
predefined_text: None,
render_config: None,
}
}
pub fn help(mut self, help: impl Into<String>) -> Self {
self.help_message = Some(help.into());
self
}
pub fn extension(mut self, ext: impl Into<String>) -> Self {
self.file_extension = ext.into();
self
}
pub fn predefined_text(mut self, text: impl Into<String>) -> Self {
self.predefined_text = Some(text.into());
self
}
pub fn render_config(mut self, config: RenderConfig<'static>) -> Self {
self.render_config = Some(config);
self
}
pub fn prompt(&self) -> Result<String, InputError> {
if let Some(value) =
crate::responder::intercept_text(crate::PromptKind::Editor, &self.message)?
{
return Ok(value);
}
let matches = crate::collector::empty_matches();
if !self.is_available(matches) {
return Err(InputError::NoInput);
}
self.collect(matches)?.ok_or(InputError::NoInput)
}
}
impl InputCollector<String> for InquireEditor {
fn name(&self) -> &'static str {
"editor"
}
fn is_available(&self, _matches: &ArgMatches) -> bool {
std::io::stdin().is_terminal()
}
fn collect(&self, _matches: &ArgMatches) -> Result<Option<String>, InputError> {
let mut prompt = Editor::new(&self.message).with_file_extension(&self.file_extension);
if let Some(help) = &self.help_message {
prompt = prompt.with_help_message(help);
}
if let Some(text) = &self.predefined_text {
prompt = prompt.with_predefined_text(text);
}
if let Some(config) = &self.render_config {
prompt = prompt.with_render_config(*config);
}
let result = prompt.prompt().map_err(map_inquire_error)?;
let trimmed = result.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
fn can_retry(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_matches() -> ArgMatches {
clap::Command::new("test")
.try_get_matches_from(["test"])
.unwrap()
}
#[test]
fn inquire_text_construction() {
let source = InquireText::new("Name?")
.default("Alice")
.placeholder("Your name...")
.help("Enter your full name");
assert_eq!(source.name(), "prompt");
assert!(source.can_retry());
}
#[test]
fn inquire_confirm_construction() {
let source = InquireConfirm::new("Proceed?")
.default(true)
.help("Are you sure?");
assert_eq!(source.name(), "prompt");
assert!(source.can_retry());
}
#[test]
fn inquire_select_construction() {
let source = InquireSelect::new("Choose:", vec!["a", "b", "c"])
.help("Select one")
.page_size(5);
assert_eq!(source.name(), "prompt");
assert!(source.can_retry());
}
#[test]
fn inquire_select_empty_options_unavailable() {
let source: InquireSelect<String> = InquireSelect::new("Choose:", vec![]);
let _ = source.is_available(&empty_matches());
}
#[test]
fn inquire_multiselect_construction() {
let source = InquireMultiSelect::new("Select:", vec!["x", "y", "z"])
.help("Select multiple")
.page_size(10)
.min_selections(1)
.max_selections(2);
assert_eq!(source.name(), "prompt");
assert!(source.can_retry());
}
#[test]
fn inquire_password_construction() {
let source = InquirePassword::new("Password:")
.help("Enter securely")
.masked()
.with_confirmation("Confirm:");
assert_eq!(source.name(), "prompt");
assert!(source.can_retry());
}
#[test]
fn inquire_password_display_modes() {
let _ = InquirePassword::new("P:").hidden();
let _ = InquirePassword::new("P:").masked();
let _ = InquirePassword::new("P:").full();
}
#[test]
fn inquire_editor_construction() {
let source = InquireEditor::new("Message:")
.help("Enter in editor")
.extension(".md")
.predefined_text("# Title\n");
assert_eq!(source.name(), "editor");
assert!(source.can_retry());
}
use crate::{
reset_default_prompt_responder, set_default_prompt_responder, PromptResponse,
ScriptedResponder,
};
use serial_test::serial;
use std::sync::Arc;
struct ResponderGuard;
impl ResponderGuard {
fn install(responder: ScriptedResponder) -> Self {
set_default_prompt_responder(Arc::new(responder));
Self
}
}
impl Drop for ResponderGuard {
fn drop(&mut self) {
reset_default_prompt_responder();
}
}
#[test]
#[serial(prompt_responder)]
fn inquire_text_prompt_via_responder() {
let _g = ResponderGuard::install(ScriptedResponder::new([PromptResponse::text("Bob")]));
let value = InquireText::new("Name?").prompt().unwrap();
assert_eq!(value, "Bob");
}
#[test]
#[serial(prompt_responder)]
fn inquire_text_prompt_cancel_via_responder() {
let _g = ResponderGuard::install(ScriptedResponder::new([PromptResponse::Cancel]));
let err = InquireText::new("Name?").prompt().unwrap_err();
assert!(matches!(err, InputError::PromptCancelled));
}
#[test]
#[serial(prompt_responder)]
fn inquire_text_prompt_skip_via_responder() {
let _g = ResponderGuard::install(ScriptedResponder::new([PromptResponse::Skip]));
let err = InquireText::new("Name?").prompt().unwrap_err();
assert!(matches!(err, InputError::NoInput));
}
#[test]
#[serial(prompt_responder)]
fn inquire_confirm_prompt_via_responder() {
let _g = ResponderGuard::install(ScriptedResponder::new([
PromptResponse::Bool(true),
PromptResponse::Bool(false),
]));
assert!(InquireConfirm::new("Yes?").prompt().unwrap());
assert!(!InquireConfirm::new("Yes?").prompt().unwrap());
}
#[test]
#[serial(prompt_responder)]
fn inquire_select_prompt_via_responder_returns_typed_value() {
let _g = ResponderGuard::install(ScriptedResponder::new([PromptResponse::Choice(2)]));
let env: &'static str = InquireSelect::new("Env:", vec!["dev", "staging", "prod"])
.prompt()
.unwrap();
assert_eq!(env, "prod");
}
#[test]
#[serial(prompt_responder)]
fn inquire_select_prompt_cancel_via_responder() {
let _g = ResponderGuard::install(ScriptedResponder::new([PromptResponse::Cancel]));
let err = InquireSelect::new("Env:", vec!["dev", "prod"])
.prompt()
.unwrap_err();
assert!(matches!(err, InputError::PromptCancelled));
}
#[test]
#[serial(prompt_responder)]
fn inquire_multiselect_prompt_via_responder_returns_typed_values() {
let _g = ResponderGuard::install(ScriptedResponder::new([PromptResponse::choices([0, 2])]));
let picks: Vec<&'static str> = InquireMultiSelect::new("Pick:", vec!["a", "b", "c", "d"])
.prompt()
.unwrap();
assert_eq!(picks, vec!["a", "c"]);
}
#[test]
#[serial(prompt_responder)]
fn inquire_password_prompt_via_responder() {
let _g = ResponderGuard::install(ScriptedResponder::new([PromptResponse::text("hunter2")]));
let value = InquirePassword::new("Pwd:").prompt().unwrap();
assert_eq!(value, "hunter2");
}
#[test]
#[serial(prompt_responder)]
fn inquire_editor_prompt_via_responder() {
let _g = ResponderGuard::install(ScriptedResponder::new([PromptResponse::text(
"edited content",
)]));
let value = InquireEditor::new("Notes:").prompt().unwrap();
assert_eq!(value, "edited content");
}
#[test]
#[serial(prompt_responder)]
fn responder_advances_through_multi_step_wizard() {
let _g = ResponderGuard::install(ScriptedResponder::new([
PromptResponse::text("foo"),
PromptResponse::Bool(true),
PromptResponse::Choice(1),
]));
assert_eq!(InquireText::new("Name:").prompt().unwrap(), "foo");
assert!(InquireConfirm::new("OK?").prompt().unwrap());
let env: &'static str = InquireSelect::new("Env:", vec!["dev", "prod"])
.prompt()
.unwrap();
assert_eq!(env, "prod");
}
}