Skip to main content

pimalaya_cli/
prompt.rs

1//! Interactive prompt helpers wrapping the inquire crate.
2//!
3//! Thin typed wrappers for prompting integers, secrets, passwords,
4//! free text, booleans and a choice from a list, each returning a
5//! [`PromptResult`].
6
7use core::fmt;
8
9use inquire::{Confirm, InquireError, Password, PasswordDisplayMode, Select, Text};
10use secrecy::SecretString;
11use thiserror::Error;
12
13use crate::validator::{U16Validator, UsizeValidator};
14
15/// Error raised when a prompt fails or is cancelled.
16#[derive(Debug, Error)]
17pub enum PromptError {
18    /// Prompting a u16 integer failed.
19    #[error("cannot prompt unsigned integer (u16)")]
20    U16(#[source] InquireError),
21    /// Prompting a usize integer failed.
22    #[error("cannot prompt unsigned integer (usize)")]
23    Usize(#[source] InquireError),
24    /// Prompting a masked secret failed.
25    #[error("cannot prompt secret")]
26    Secret(#[source] InquireError),
27    /// Prompting a confirmed password failed.
28    #[error("cannot prompt password")]
29    Password(#[source] InquireError),
30    /// Prompting free text failed.
31    #[error("cannot prompt text")]
32    Text(#[source] InquireError),
33    /// Prompting a yes/no confirmation failed.
34    #[error("cannot prompt boolean")]
35    Bool(#[source] InquireError),
36    /// Prompting a choice from a list failed.
37    #[error("cannot prompt item from list")]
38    Item(#[source] InquireError),
39}
40
41/// Result of a prompt helper.
42pub type PromptResult<T> = Result<T, PromptError>;
43
44/// Prompts for a u16 integer, with an optional default.
45pub fn u16(prompt: impl AsRef<str>, default: Option<u16>) -> PromptResult<u16> {
46    let prompt = Text::new(prompt.as_ref()).with_validator(U16Validator);
47
48    let number = if let Some(default) = default {
49        prompt.with_default(&default.to_string()).prompt()
50    } else {
51        prompt.prompt()
52    };
53
54    match number {
55        Ok(number) => Ok(number.parse().unwrap()),
56        Err(err) => Err(PromptError::U16(err)),
57    }
58}
59
60/// Prompts for a usize integer, with an optional default.
61pub fn usize(prompt: impl AsRef<str>, default: Option<usize>) -> PromptResult<usize> {
62    let prompt = Text::new(prompt.as_ref()).with_validator(UsizeValidator);
63
64    let number = if let Some(default) = default {
65        prompt.with_default(&default.to_string()).prompt()
66    } else {
67        prompt.prompt()
68    };
69
70    match number {
71        Ok(number) => Ok(number.parse().unwrap()),
72        Err(err) => Err(PromptError::Usize(err)),
73    }
74}
75
76/// Prompts for a masked secret without confirmation.
77pub fn secret(prompt: impl AsRef<str>) -> PromptResult<String> {
78    Password::new(prompt.as_ref())
79        .with_display_mode(PasswordDisplayMode::Masked)
80        .without_confirmation()
81        .prompt()
82        .map_err(PromptError::Secret)
83}
84
85/// Prompts for a masked secret, returning `None` when skipped.
86pub fn some_secret(prompt: impl AsRef<str>) -> PromptResult<Option<SecretString>> {
87    Password::new(prompt.as_ref())
88        .with_display_mode(PasswordDisplayMode::Masked)
89        .without_confirmation()
90        .prompt_skippable()
91        .map(|secret| secret.map(Into::into))
92        .map_err(PromptError::Secret)
93}
94
95/// Prompts for a masked password with a confirmation prompt.
96pub fn password(prompt: impl AsRef<str>, confirm: impl AsRef<str>) -> PromptResult<SecretString> {
97    Password::new(prompt.as_ref())
98        .with_display_mode(PasswordDisplayMode::Masked)
99        .with_custom_confirmation_message(confirm.as_ref())
100        .prompt()
101        .map(Into::into)
102        .map_err(PromptError::Password)
103}
104
105/// Prompts for free text, with an optional default.
106pub fn text<T: AsRef<str>>(prompt: T, default: Option<T>) -> PromptResult<String> {
107    let mut prompt = Text::new(prompt.as_ref());
108
109    if let Some(default) = default.as_ref() {
110        prompt = prompt.with_default(default.as_ref())
111    }
112
113    prompt.prompt().map_err(PromptError::Text)
114}
115
116/// Prompts for free text, returning `None` when skipped.
117pub fn some_text<T: AsRef<str>>(prompt: T, default: Option<T>) -> PromptResult<Option<String>> {
118    let mut prompt = Text::new(prompt.as_ref());
119
120    if let Some(default) = default.as_ref() {
121        prompt = prompt.with_default(default.as_ref())
122    }
123
124    prompt.prompt_skippable().map_err(PromptError::Text)
125}
126
127/// Prompts for a yes/no confirmation, with a default.
128pub fn bool(prompt: impl AsRef<str>, default: bool) -> PromptResult<bool> {
129    Confirm::new(prompt.as_ref())
130        .with_default(default)
131        .prompt()
132        .map_err(PromptError::Bool)
133}
134
135/// Prompts for one item chosen from a list, with an optional default
136/// selection.
137pub fn item<T: fmt::Display + Eq>(
138    prompt: impl AsRef<str>,
139    items: impl IntoIterator<Item = T>,
140    default: Option<T>,
141) -> PromptResult<T> {
142    let items: Vec<_> = items.into_iter().collect();
143
144    let default = if let Some(default) = default.as_ref() {
145        items
146            .iter()
147            .enumerate()
148            .find_map(|(i, item)| if item == default { Some(i) } else { None })
149    } else {
150        None
151    };
152
153    let mut prompt = Select::new(prompt.as_ref(), items);
154
155    if let Some(default) = default.as_ref() {
156        prompt = prompt.with_starting_cursor(*default);
157    }
158
159    prompt.prompt().map_err(PromptError::Item)
160}