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, MultiSelect, 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    /// Prompting several choices from a list failed.
40    #[error("cannot prompt items from list")]
41    Items(#[source] InquireError),
42}
43
44/// Result of a prompt helper.
45pub type PromptResult<T> = Result<T, PromptError>;
46
47/// Prompts for a u16 integer, with an optional default.
48pub fn u16(prompt: impl AsRef<str>, default: Option<u16>) -> PromptResult<u16> {
49    let prompt = Text::new(prompt.as_ref()).with_validator(U16Validator);
50
51    let number = if let Some(default) = default {
52        prompt.with_default(&default.to_string()).prompt()
53    } else {
54        prompt.prompt()
55    };
56
57    match number {
58        Ok(number) => Ok(number.parse().unwrap()),
59        Err(err) => Err(PromptError::U16(err)),
60    }
61}
62
63/// Prompts for a usize integer, with an optional default.
64pub fn usize(prompt: impl AsRef<str>, default: Option<usize>) -> PromptResult<usize> {
65    let prompt = Text::new(prompt.as_ref()).with_validator(UsizeValidator);
66
67    let number = if let Some(default) = default {
68        prompt.with_default(&default.to_string()).prompt()
69    } else {
70        prompt.prompt()
71    };
72
73    match number {
74        Ok(number) => Ok(number.parse().unwrap()),
75        Err(err) => Err(PromptError::Usize(err)),
76    }
77}
78
79/// Prompts for a masked secret without confirmation.
80pub fn secret(prompt: impl AsRef<str>) -> PromptResult<String> {
81    Password::new(prompt.as_ref())
82        .with_display_mode(PasswordDisplayMode::Masked)
83        .without_confirmation()
84        .prompt()
85        .map_err(PromptError::Secret)
86}
87
88/// Prompts for a masked secret, returning `None` when skipped.
89pub fn some_secret(prompt: impl AsRef<str>) -> PromptResult<Option<SecretString>> {
90    Password::new(prompt.as_ref())
91        .with_display_mode(PasswordDisplayMode::Masked)
92        .without_confirmation()
93        .prompt_skippable()
94        .map(|secret| secret.map(Into::into))
95        .map_err(PromptError::Secret)
96}
97
98/// Prompts for a masked password with a confirmation prompt.
99pub fn password(prompt: impl AsRef<str>, confirm: impl AsRef<str>) -> PromptResult<SecretString> {
100    Password::new(prompt.as_ref())
101        .with_display_mode(PasswordDisplayMode::Masked)
102        .with_custom_confirmation_message(confirm.as_ref())
103        .prompt()
104        .map(Into::into)
105        .map_err(PromptError::Password)
106}
107
108/// Prompts for free text, with an optional default.
109pub fn text<T: AsRef<str>>(prompt: T, default: Option<T>) -> PromptResult<String> {
110    let mut prompt = Text::new(prompt.as_ref());
111
112    if let Some(default) = default.as_ref() {
113        prompt = prompt.with_default(default.as_ref())
114    }
115
116    prompt.prompt().map_err(PromptError::Text)
117}
118
119/// Prompts for free text, returning `None` when skipped.
120pub fn some_text<T: AsRef<str>>(prompt: T, default: Option<T>) -> PromptResult<Option<String>> {
121    let mut prompt = Text::new(prompt.as_ref());
122
123    if let Some(default) = default.as_ref() {
124        prompt = prompt.with_default(default.as_ref())
125    }
126
127    prompt.prompt_skippable().map_err(PromptError::Text)
128}
129
130/// Prompts for a yes/no confirmation, with a default.
131pub fn bool(prompt: impl AsRef<str>, default: bool) -> PromptResult<bool> {
132    Confirm::new(prompt.as_ref())
133        .with_default(default)
134        .prompt()
135        .map_err(PromptError::Bool)
136}
137
138/// Prompts for one item chosen from a list, with an optional default
139/// selection.
140pub fn item<T: fmt::Display + Eq>(
141    prompt: impl AsRef<str>,
142    items: impl IntoIterator<Item = T>,
143    default: Option<T>,
144) -> PromptResult<T> {
145    let items: Vec<_> = items.into_iter().collect();
146
147    let default = if let Some(default) = default.as_ref() {
148        items
149            .iter()
150            .enumerate()
151            .find_map(|(i, item)| if item == default { Some(i) } else { None })
152    } else {
153        None
154    };
155
156    let mut prompt = Select::new(prompt.as_ref(), items).without_filtering();
157
158    if let Some(default) = default.as_ref() {
159        prompt = prompt.with_starting_cursor(*default);
160    }
161
162    prompt.prompt().map_err(PromptError::Item)
163}
164
165/// Prompts for several items chosen from a list (a multi-select),
166/// with the given item indices selected by default.
167pub fn items<T: fmt::Display + Eq>(
168    prompt: impl AsRef<str>,
169    items: impl IntoIterator<Item = T>,
170    default: impl IntoIterator<Item = usize>,
171) -> PromptResult<Vec<T>> {
172    let items: Vec<_> = items.into_iter().collect();
173    let default: Vec<usize> = default.into_iter().collect();
174
175    MultiSelect::new(prompt.as_ref(), items)
176        .without_filtering()
177        .with_default(&default)
178        .prompt()
179        .map_err(PromptError::Items)
180}