1use std::io::{self, IsTerminal, Write};
2
3pub fn stdout_is_tty() -> bool {
5 std::io::stdout().is_terminal()
6}
7
8pub fn stderr_is_tty() -> bool {
10 std::io::stderr().is_terminal()
11}
12
13pub fn stdin_is_tty() -> bool {
15 std::io::stdin().is_terminal()
16}
17
18pub fn format_severity(s: &str, use_color: bool) -> String {
20 if !use_color {
21 return s.to_string();
22 }
23 match s.to_lowercase().as_str() {
24 "critical" => format!("\x1b[31m{s}\x1b[0m"),
25 "high" => format!("\x1b[91m{s}\x1b[0m"),
26 "medium" => format!("\x1b[33m{s}\x1b[0m"),
27 "low" => format!("\x1b[36m{s}\x1b[0m"),
28 _ => s.to_string(),
29 }
30}
31
32pub fn color(text: &str, code: &str, use_color: bool) -> String {
34 if use_color {
35 format!("\x1b[{code}m{text}\x1b[0m")
36 } else {
37 text.to_string()
38 }
39}
40
41pub enum SelectError {
43 Cancelled,
45 JsonModeNeedsExplicit,
47}
48
49pub fn confirm(prompt: &str, default_yes: bool, skip_prompt: bool, is_json: bool) -> bool {
55 if skip_prompt || is_json {
56 return default_yes;
57 }
58 if !stdin_is_tty() {
59 eprintln!("Non-interactive mode detected, proceeding with default.");
60 return default_yes;
61 }
62 let hint = if default_yes { "[Y/n]" } else { "[y/N]" };
63 eprint!("{prompt} {hint} ");
64 io::stderr().flush().unwrap();
65 let mut answer = String::new();
66 io::stdin().read_line(&mut answer).unwrap();
67 let answer = answer.trim().to_lowercase();
68 if answer.is_empty() {
69 return default_yes;
70 }
71 answer == "y" || answer == "yes"
72}
73
74pub fn select_one(prompt: &str, options: &[String], is_json: bool) -> Result<usize, SelectError> {
80 if is_json {
81 return Err(SelectError::JsonModeNeedsExplicit);
82 }
83 if !stdin_is_tty() {
84 eprintln!("Non-interactive mode: auto-selecting first option.");
85 return Ok(0);
86 }
87 let selection = dialoguer::Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
88 .with_prompt(prompt)
89 .items(options)
90 .default(0)
91 .interact_opt()
92 .map_err(|_| SelectError::Cancelled)?;
93 match selection {
94 Some(idx) => Ok(idx),
95 None => Err(SelectError::Cancelled),
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
106 fn format_severity_critical_with_color() {
107 let out = format_severity("critical", true);
108 assert!(out.starts_with("\x1b["), "expected ANSI prefix: {out:?}");
109 assert!(out.contains("critical"), "expected input verbatim: {out:?}");
110 assert!(out.ends_with("\x1b[0m"), "expected ANSI reset: {out:?}");
111 assert!(out.contains("31"), "expected red code 31: {out:?}");
112 }
113
114 #[test]
115 fn format_severity_high_with_color() {
116 let out = format_severity("high", true);
117 assert!(out.starts_with("\x1b["), "expected ANSI prefix: {out:?}");
118 assert!(out.contains("high"), "expected input verbatim: {out:?}");
119 assert!(out.ends_with("\x1b[0m"), "expected ANSI reset: {out:?}");
120 assert!(out.contains("91"), "expected bright-red code 91: {out:?}");
121 }
122
123 #[test]
124 fn format_severity_medium_with_color() {
125 let out = format_severity("medium", true);
126 assert!(out.starts_with("\x1b["), "expected ANSI prefix: {out:?}");
127 assert!(out.contains("medium"), "expected input verbatim: {out:?}");
128 assert!(out.ends_with("\x1b[0m"), "expected ANSI reset: {out:?}");
129 assert!(out.contains("33"), "expected yellow code 33: {out:?}");
130 }
131
132 #[test]
133 fn format_severity_low_with_color() {
134 let out = format_severity("low", true);
135 assert!(out.starts_with("\x1b["), "expected ANSI prefix: {out:?}");
136 assert!(out.contains("low"), "expected input verbatim: {out:?}");
137 assert!(out.ends_with("\x1b[0m"), "expected ANSI reset: {out:?}");
138 assert!(out.contains("36"), "expected cyan code 36: {out:?}");
139 }
140
141 #[test]
142 fn format_severity_case_insensitive_critical_uppercase() {
143 let out = format_severity("CRITICAL", true);
144 assert!(out.starts_with("\x1b["), "expected ANSI prefix: {out:?}");
145 assert!(out.contains("CRITICAL"), "expected input verbatim: {out:?}");
146 assert!(out.ends_with("\x1b[0m"), "expected ANSI reset: {out:?}");
147 assert!(out.contains("31"), "expected red code 31: {out:?}");
148 }
149
150 #[test]
151 fn format_severity_case_insensitive_critical_titlecase() {
152 let out = format_severity("Critical", true);
153 assert!(out.starts_with("\x1b["), "expected ANSI prefix: {out:?}");
154 assert!(out.contains("Critical"), "expected input verbatim: {out:?}");
155 assert!(out.ends_with("\x1b[0m"), "expected ANSI reset: {out:?}");
156 assert!(out.contains("31"), "expected red code 31: {out:?}");
157 }
158
159 #[test]
160 fn format_severity_case_insensitive_high_lowercase() {
161 let out = format_severity("high", true);
162 assert!(out.starts_with("\x1b["), "expected ANSI prefix: {out:?}");
163 assert!(out.contains("high"), "expected input verbatim: {out:?}");
164 assert!(out.ends_with("\x1b[0m"), "expected ANSI reset: {out:?}");
165 }
166
167 #[test]
168 fn format_severity_case_insensitive_high_uppercase() {
169 let out = format_severity("HIGH", true);
170 assert!(out.starts_with("\x1b["), "expected ANSI prefix: {out:?}");
171 assert!(out.contains("HIGH"), "expected input verbatim: {out:?}");
172 assert!(out.ends_with("\x1b[0m"), "expected ANSI reset: {out:?}");
173 assert!(out.contains("91"), "expected bright-red code 91: {out:?}");
174 }
175
176 #[test]
177 fn format_severity_unknown_passes_through_with_color() {
178 let out = format_severity("unknown", true);
179 assert_eq!(out, "unknown");
180 }
181
182 #[test]
183 fn format_severity_critical_no_color() {
184 assert_eq!(format_severity("critical", false), "critical");
185 }
186
187 #[test]
188 fn format_severity_high_no_color() {
189 assert_eq!(format_severity("high", false), "high");
190 }
191
192 #[test]
193 fn format_severity_medium_no_color() {
194 assert_eq!(format_severity("medium", false), "medium");
195 }
196
197 #[test]
198 fn format_severity_low_no_color() {
199 assert_eq!(format_severity("low", false), "low");
200 }
201
202 #[test]
203 fn format_severity_unknown_no_color() {
204 assert_eq!(format_severity("unknown", false), "unknown");
205 }
206
207 #[test]
208 fn format_severity_empty_with_color_passes_through() {
209 let out = format_severity("", true);
210 assert_eq!(out, "");
211 }
212
213 #[test]
216 fn color_with_color_on() {
217 assert_eq!(color("hi", "31", true), "\x1b[31mhi\x1b[0m");
218 }
219
220 #[test]
221 fn color_with_color_off() {
222 assert_eq!(color("hi", "31", false), "hi");
223 }
224
225 #[test]
226 fn color_with_empty_text_and_color_on() {
227 assert_eq!(color("", "1;32", true), "\x1b[1;32m\x1b[0m");
228 }
229
230 #[test]
233 fn confirm_skip_prompt_returns_default_yes_true() {
234 assert!(confirm("?", true, true, false));
235 }
236
237 #[test]
238 fn confirm_skip_prompt_returns_default_yes_false() {
239 assert!(!confirm("?", false, true, false));
240 }
241
242 #[test]
243 fn confirm_is_json_returns_default_yes_true() {
244 assert!(confirm("?", true, false, true));
245 }
246
247 #[test]
248 fn confirm_is_json_returns_default_yes_false() {
249 assert!(!confirm("?", false, false, true));
250 }
251
252 #[test]
253 fn confirm_skip_prompt_and_is_json_both_set_returns_default_yes() {
254 assert!(confirm("?", true, true, true));
255 }
256}