cfgd_core/output/
prompts.rs1use std::io::IsTerminal;
10
11use super::Printer;
12use super::printer::PromptAnswer;
13
14fn non_interactive_err(prompt: &str) -> inquire::InquireError {
19 inquire::InquireError::Custom(
20 format!(
21 "refusing to prompt for '{prompt}' in non-interactive/structured output \
22 mode (re-run without -o json or supply the answer via a flag / env var)"
23 )
24 .into(),
25 )
26}
27
28fn stdin_is_tty() -> bool {
32 std::io::stdin().is_terminal()
33}
34
35impl Printer {
36 pub fn prompt_confirm(&self, message: &str) -> Result<bool, inquire::InquireError> {
37 if let Some(answer) = self.pop_prompt_answer()
38 && let PromptAnswer::Confirm(b) = answer
39 {
40 return Ok(b);
41 }
42 if self.is_structured() || !stdin_is_tty() {
43 return Err(non_interactive_err(message));
44 }
45 inquire::Confirm::new(message).with_default(false).prompt()
46 }
47
48 pub fn prompt_select<'a>(
49 &self,
50 message: &str,
51 options: &'a [String],
52 ) -> Result<&'a String, inquire::InquireError> {
53 if let Some(answer) = self.pop_prompt_answer()
54 && let PromptAnswer::Select(s) = answer
55 {
56 return options.iter().find(|o| **o == s).ok_or_else(|| {
57 inquire::InquireError::Custom(
58 format!("test prompt response '{s}' not in option list").into(),
59 )
60 });
61 }
62 if self.is_structured() || !stdin_is_tty() {
63 return Err(non_interactive_err(message));
64 }
65 if options.is_empty() {
66 return Err(inquire::InquireError::Custom("no options available".into()));
67 }
68 let chosen = inquire::Select::new(message, options.to_vec()).prompt()?;
69 Ok(options
70 .iter()
71 .find(|o| **o == chosen)
72 .unwrap_or(&options[0]))
73 }
74
75 pub fn prompt_text(
76 &self,
77 message: &str,
78 default: &str,
79 ) -> Result<String, inquire::InquireError> {
80 if let Some(answer) = self.pop_prompt_answer()
81 && let PromptAnswer::Text(s) = answer
82 {
83 return Ok(s);
84 }
85 if self.is_structured() || !stdin_is_tty() {
86 return Err(non_interactive_err(message));
87 }
88 inquire::Text::new(message).with_default(default).prompt()
89 }
90
91 pub(crate) fn pop_prompt_answer(&self) -> Option<PromptAnswer> {
92 self.prompt_queue
93 .as_ref()?
94 .lock()
95 .unwrap_or_else(|e| e.into_inner())
96 .pop_front()
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::super::{OutputFormat, Verbosity};
103 use super::*;
104
105 #[test]
106 fn structured_mode_refuses_prompt() {
107 let p = Printer::with_format(Verbosity::Normal, None, OutputFormat::Json);
108 let r = p.prompt_confirm("really?");
109 assert!(r.is_err());
110 }
111
112 #[test]
113 fn seeded_select_returns_matching_option() {
114 let (printer, _buf) =
115 Printer::for_test_with_prompt_responses(vec![PromptAnswer::Select("yes".into())]);
116 let options = vec!["yes".to_string(), "no".to_string()];
117 let chosen = printer
118 .prompt_select("pick", &options)
119 .expect("seeded select must resolve to a listed option");
120 assert_eq!(chosen, "yes");
121 }
122
123 #[test]
124 fn seeded_select_with_unknown_response_is_custom_error() {
125 let (printer, _buf) =
126 Printer::for_test_with_prompt_responses(vec![PromptAnswer::Select("missing".into())]);
127 let options = vec!["yes".to_string(), "no".to_string()];
128 let err = printer
129 .prompt_select("pick", &options)
130 .expect_err("response not in options must Err");
131 let msg = format!("{err}");
132 assert!(msg.contains("missing"), "msg must echo unknown: {msg}");
133 }
134
135 #[test]
136 fn structured_select_refuses_when_no_seeded_answer() {
137 let p = Printer::with_format(Verbosity::Normal, None, OutputFormat::Json);
138 let options = vec!["a".to_string(), "b".to_string()];
139 let err = p
140 .prompt_select("pick", &options)
141 .expect_err("structured mode must refuse");
142 let msg = format!("{err}");
143 assert!(
144 msg.contains("non-interactive") || msg.contains("structured"),
145 "expected non-interactive refusal: {msg}"
146 );
147 }
148
149 #[test]
150 fn seeded_text_returns_value() {
151 let (printer, _buf) =
152 Printer::for_test_with_prompt_responses(vec![PromptAnswer::Text("answer".into())]);
153 let text = printer.prompt_text("name", "").expect("seeded text answer");
154 assert_eq!(text, "answer");
155 }
156
157 #[test]
158 fn structured_text_refuses_when_no_seeded_answer() {
159 let p = Printer::with_format(Verbosity::Normal, None, OutputFormat::Json);
160 let err = p.prompt_text("name", "").expect_err("structured refuse");
161 let msg = format!("{err}");
162 assert!(
163 msg.contains("non-interactive") || msg.contains("structured"),
164 "expected non-interactive refusal: {msg}"
165 );
166 }
167
168 #[test]
169 fn seeded_confirm_returns_bool() {
170 let (printer, _b1) =
171 Printer::for_test_with_prompt_responses(vec![PromptAnswer::Confirm(true)]);
172 assert!(printer.prompt_confirm("really?").expect("seeded confirm"));
173 let (printer2, _b2) =
174 Printer::for_test_with_prompt_responses(vec![PromptAnswer::Confirm(false)]);
175 assert!(
176 !printer2
177 .prompt_confirm("really?")
178 .expect("seeded confirm false")
179 );
180 }
181}