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