1use std::collections::VecDeque;
11use std::sync::Mutex;
12
13use crate::CliConfig;
14use anyhow::{Result, anyhow};
15use dialoguer::theme::ColorfulTheme;
16use dialoguer::{Confirm, Input, Password, Select};
17
18pub trait Prompter: Send + Sync {
19 fn confirm(&self, message: &str, default: bool) -> Result<bool>;
20 fn input(&self, prompt: &str) -> Result<String>;
21 fn input_with_default(&self, prompt: &str, default: &str) -> Result<String>;
22 fn select(&self, prompt: &str, items: &[String]) -> Result<usize>;
23 fn password(&self, prompt: &str) -> Result<String>;
24}
25
26#[derive(Debug, Clone, Copy, Default)]
27pub struct DialoguerPrompter;
28
29impl Prompter for DialoguerPrompter {
30 fn confirm(&self, message: &str, default: bool) -> Result<bool> {
31 Ok(Confirm::with_theme(&ColorfulTheme::default())
32 .with_prompt(message)
33 .default(default)
34 .interact()?)
35 }
36
37 fn input(&self, prompt: &str) -> Result<String> {
38 Ok(Input::<String>::with_theme(&ColorfulTheme::default())
39 .with_prompt(prompt)
40 .interact_text()?)
41 }
42
43 fn input_with_default(&self, prompt: &str, default: &str) -> Result<String> {
44 Ok(Input::<String>::with_theme(&ColorfulTheme::default())
45 .with_prompt(prompt)
46 .default(default.to_owned())
47 .interact_text()?)
48 }
49
50 fn select(&self, prompt: &str, items: &[String]) -> Result<usize> {
51 Ok(Select::with_theme(&ColorfulTheme::default())
52 .with_prompt(prompt)
53 .items(items)
54 .default(0)
55 .interact()?)
56 }
57
58 fn password(&self, prompt: &str) -> Result<String> {
59 Ok(Password::with_theme(&ColorfulTheme::default())
60 .with_prompt(prompt)
61 .interact()?)
62 }
63}
64
65#[derive(Debug, Default)]
66pub struct ScriptedPrompter {
67 answers: Mutex<VecDeque<String>>,
68}
69
70impl ScriptedPrompter {
71 #[must_use]
72 pub fn new(answers: impl IntoIterator<Item = impl Into<String>>) -> Self {
73 Self {
74 answers: Mutex::new(answers.into_iter().map(Into::into).collect()),
75 }
76 }
77
78 fn next_answer(&self, prompt: &str) -> Result<String> {
79 self.answers
80 .lock()
81 .map_err(|e| anyhow!("Scripted prompter lock poisoned: {e}"))?
82 .pop_front()
83 .ok_or_else(|| anyhow!("Scripted prompter exhausted at prompt: {prompt}"))
84 }
85}
86
87impl Prompter for ScriptedPrompter {
88 fn confirm(&self, message: &str, _default: bool) -> Result<bool> {
89 let answer = self.next_answer(message)?;
90 Ok(matches!(
91 answer.to_lowercase().as_str(),
92 "y" | "yes" | "true"
93 ))
94 }
95
96 fn input(&self, prompt: &str) -> Result<String> {
97 self.next_answer(prompt)
98 }
99
100 fn input_with_default(&self, prompt: &str, default: &str) -> Result<String> {
101 match self.next_answer(prompt) {
102 Ok(answer) if answer.is_empty() => Ok(default.to_owned()),
103 other => other,
104 }
105 }
106
107 fn select(&self, prompt: &str, items: &[String]) -> Result<usize> {
108 let answer = self.next_answer(prompt)?;
109 let idx: usize = answer
110 .parse()
111 .map_err(|e| anyhow!("Scripted select answer '{answer}' is not an index: {e}"))?;
112 if idx >= items.len() {
113 return Err(anyhow!(
114 "Scripted select index {idx} out of range for {} items",
115 items.len()
116 ));
117 }
118 Ok(idx)
119 }
120
121 fn password(&self, prompt: &str) -> Result<String> {
122 self.next_answer(prompt)
123 }
124}
125
126pub fn require_confirmation(
127 prompter: &dyn Prompter,
128 message: &str,
129 skip_confirmation: bool,
130 config: &CliConfig,
131) -> Result<()> {
132 require_confirmation_with(prompter, message, skip_confirmation, false, config)
133}
134
135pub fn require_confirmation_default_yes(
136 prompter: &dyn Prompter,
137 message: &str,
138 skip_confirmation: bool,
139 config: &CliConfig,
140) -> Result<()> {
141 require_confirmation_with(prompter, message, skip_confirmation, true, config)
142}
143
144fn require_confirmation_with(
145 prompter: &dyn Prompter,
146 message: &str,
147 skip_confirmation: bool,
148 default: bool,
149 config: &CliConfig,
150) -> Result<()> {
151 if skip_confirmation {
152 return Ok(());
153 }
154
155 if !config.is_interactive() {
156 return Err(anyhow!("--yes is required in non-interactive mode"));
157 }
158
159 if prompter.confirm(message, default)? {
160 Ok(())
161 } else {
162 Err(anyhow!("Operation cancelled"))
163 }
164}
165
166pub fn resolve_required<T, F>(
167 value: Option<T>,
168 flag_name: &str,
169 config: &CliConfig,
170 prompt_fn: F,
171) -> Result<T>
172where
173 F: FnOnce() -> Result<T>,
174{
175 match value {
176 Some(v) => Ok(v),
177 None if config.is_interactive() => prompt_fn(),
178 None => Err(anyhow!(
179 "--{} is required in non-interactive mode",
180 flag_name
181 )),
182 }
183}
184
185pub fn select_from_list<T: ToString + Clone>(
186 prompter: &dyn Prompter,
187 prompt: &str,
188 items: &[T],
189 flag_name: &str,
190 config: &CliConfig,
191) -> Result<T> {
192 if items.is_empty() {
193 return Err(anyhow!("No items available for selection"));
194 }
195
196 if !config.is_interactive() {
197 return Err(anyhow!(
198 "--{} is required in non-interactive mode",
199 flag_name
200 ));
201 }
202
203 let display: Vec<String> = items.iter().map(ToString::to_string).collect();
204 let idx = prompter.select(prompt, &display)?;
205 Ok(items[idx].clone())
206}
207
208pub fn select_index(
209 prompter: &dyn Prompter,
210 prompt: &str,
211 items: &[&str],
212 config: &CliConfig,
213) -> Result<Option<usize>> {
214 if !config.is_interactive() {
215 return Ok(None);
216 }
217
218 let display: Vec<String> = items.iter().map(|s| (*s).to_owned()).collect();
219 Ok(Some(prompter.select(prompt, &display)?))
220}
221
222pub fn prompt_input(
223 prompter: &dyn Prompter,
224 prompt: &str,
225 flag_name: &str,
226 config: &CliConfig,
227) -> Result<String> {
228 if !config.is_interactive() {
229 return Err(anyhow!(
230 "--{} is required in non-interactive mode",
231 flag_name
232 ));
233 }
234
235 prompter.input(prompt)
236}
237
238pub fn prompt_input_with_default(
239 prompter: &dyn Prompter,
240 prompt: &str,
241 default: &str,
242 config: &CliConfig,
243) -> Result<String> {
244 if !config.is_interactive() {
245 return Ok(default.to_owned());
246 }
247
248 prompter.input_with_default(prompt, default)
249}
250
251pub fn confirm_optional(
252 prompter: &dyn Prompter,
253 message: &str,
254 default: bool,
255 config: &CliConfig,
256) -> Result<bool> {
257 if !config.is_interactive() {
258 return Ok(default);
259 }
260
261 prompter.confirm(message, default)
262}