teapot/runtime/
accessible.rs1use std::io::{self, BufRead, Write};
30
31#[derive(Debug, Clone)]
33pub enum AccessibleInput {
34 Text(String),
36 Selection(usize),
38 MultiSelection(Vec<usize>),
40 Yes,
42 No,
44 Cancel,
46 Empty,
48}
49
50impl AccessibleInput {
51 pub fn parse_text(input: &str) -> Self {
53 let trimmed = input.trim();
54 if trimmed.is_empty() {
55 AccessibleInput::Empty
56 } else {
57 AccessibleInput::Text(trimmed.to_string())
58 }
59 }
60
61 pub fn parse_selection(input: &str, max: usize) -> Self {
63 let trimmed = input.trim().to_lowercase();
64
65 if trimmed.is_empty() {
66 return AccessibleInput::Empty;
67 }
68
69 if trimmed == "q" || trimmed == "quit" || trimmed == "cancel" {
70 return AccessibleInput::Cancel;
71 }
72
73 if let Ok(n) = trimmed.parse::<usize>() {
75 if n >= 1 && n <= max {
76 return AccessibleInput::Selection(n);
77 }
78 }
79
80 AccessibleInput::Empty
82 }
83
84 pub fn parse_multi_selection(input: &str, max: usize) -> Self {
86 let trimmed = input.trim().to_lowercase();
87
88 if trimmed.is_empty() {
89 return AccessibleInput::Empty;
90 }
91
92 if trimmed == "q" || trimmed == "quit" || trimmed == "cancel" {
93 return AccessibleInput::Cancel;
94 }
95
96 if trimmed == "done" || trimmed == "d" {
97 return AccessibleInput::MultiSelection(vec![]);
98 }
99
100 let mut selections = Vec::new();
102 for part in trimmed.split([',', ' ']) {
103 let part = part.trim();
104 if part.is_empty() {
105 continue;
106 }
107 if let Ok(n) = part.parse::<usize>() {
108 if n >= 1 && n <= max && !selections.contains(&n) {
109 selections.push(n);
110 }
111 }
112 }
113
114 if selections.is_empty() {
115 AccessibleInput::Empty
116 } else {
117 AccessibleInput::MultiSelection(selections)
118 }
119 }
120
121 pub fn parse_confirm(input: &str, default: Option<bool>) -> Self {
123 let trimmed = input.trim().to_lowercase();
124
125 if trimmed.is_empty() {
126 return match default {
127 Some(true) => AccessibleInput::Yes,
128 Some(false) => AccessibleInput::No,
129 None => AccessibleInput::Empty,
130 };
131 }
132
133 match trimmed.as_str() {
134 "y" | "yes" | "true" | "1" => AccessibleInput::Yes,
135 "n" | "no" | "false" | "0" => AccessibleInput::No,
136 "q" | "quit" | "cancel" => AccessibleInput::Cancel,
137 _ => AccessibleInput::Empty,
138 }
139 }
140}
141
142pub trait Accessible {
147 type Message;
149
150 fn accessible_prompt(&self) -> String;
155
156 fn parse_accessible_input(&self, input: &str) -> Option<Self::Message>;
161
162 fn is_accessible_complete(&self) -> bool;
164}
165
166pub fn read_line() -> io::Result<String> {
168 let stdin = io::stdin();
169 let mut line = String::new();
170 stdin.lock().read_line(&mut line)?;
171 Ok(line)
172}
173
174pub fn print_prompt(prompt: &str) -> io::Result<()> {
176 let mut stdout = io::stdout();
177 write!(stdout, "{}", prompt)?;
178 stdout.flush()
179}
180
181pub fn println_accessible(line: &str) -> io::Result<()> {
183 println!("{}", line);
184 Ok(())
185}
186
187pub fn strip_ansi(s: &str) -> String {
192 let mut result = String::with_capacity(s.len());
193 let mut chars = s.chars().peekable();
194
195 while let Some(c) = chars.next() {
196 if c == '\x1b' {
197 match chars.peek() {
198 Some('[') => {
199 chars.next();
201 while let Some(&next) = chars.peek() {
202 chars.next();
203 if next.is_ascii_alphabetic() {
204 break;
205 }
206 }
207 },
208 Some(']') => {
209 chars.next();
211 while let Some(next) = chars.next() {
212 if next == '\x07' {
213 break;
214 } else if next == '\x1b' && chars.peek() == Some(&'\\') {
215 chars.next();
216 break;
217 }
218 }
219 },
220 _ => {},
221 }
222 } else {
223 result.push(c);
224 }
225 }
226
227 result
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn test_parse_text() {
236 assert!(matches!(
237 AccessibleInput::parse_text("hello"),
238 AccessibleInput::Text(s) if s == "hello"
239 ));
240 assert!(matches!(
241 AccessibleInput::parse_text(" hello "),
242 AccessibleInput::Text(s) if s == "hello"
243 ));
244 assert!(matches!(AccessibleInput::parse_text(""), AccessibleInput::Empty));
245 }
246
247 #[test]
248 fn test_parse_selection() {
249 assert!(matches!(AccessibleInput::parse_selection("1", 3), AccessibleInput::Selection(1)));
250 assert!(matches!(AccessibleInput::parse_selection("3", 3), AccessibleInput::Selection(3)));
251 assert!(matches!(AccessibleInput::parse_selection("4", 3), AccessibleInput::Empty)); assert!(matches!(AccessibleInput::parse_selection("0", 3), AccessibleInput::Empty)); assert!(matches!(AccessibleInput::parse_selection("q", 3), AccessibleInput::Cancel));
254 }
255
256 #[test]
257 fn test_parse_multi_selection() {
258 match AccessibleInput::parse_multi_selection("1, 3", 5) {
259 AccessibleInput::MultiSelection(v) => assert_eq!(v, vec![1, 3]),
260 _ => panic!("Expected MultiSelection"),
261 }
262 match AccessibleInput::parse_multi_selection("1 2 3", 5) {
263 AccessibleInput::MultiSelection(v) => assert_eq!(v, vec![1, 2, 3]),
264 _ => panic!("Expected MultiSelection"),
265 }
266 assert!(matches!(
267 AccessibleInput::parse_multi_selection("done", 5),
268 AccessibleInput::MultiSelection(v) if v.is_empty()
269 ));
270 }
271
272 #[test]
273 fn test_parse_confirm() {
274 assert!(matches!(AccessibleInput::parse_confirm("yes", None), AccessibleInput::Yes));
275 assert!(matches!(AccessibleInput::parse_confirm("y", None), AccessibleInput::Yes));
276 assert!(matches!(AccessibleInput::parse_confirm("no", None), AccessibleInput::No));
277 assert!(matches!(AccessibleInput::parse_confirm("n", None), AccessibleInput::No));
278 assert!(matches!(AccessibleInput::parse_confirm("", Some(true)), AccessibleInput::Yes));
279 assert!(matches!(AccessibleInput::parse_confirm("", Some(false)), AccessibleInput::No));
280 assert!(matches!(AccessibleInput::parse_confirm("", None), AccessibleInput::Empty));
281 }
282
283 #[test]
284 fn test_strip_ansi() {
285 assert_eq!(strip_ansi("\x1b[31mRed\x1b[0m"), "Red");
287 assert_eq!(strip_ansi("\x1b[1;32mBold Green\x1b[0m"), "Bold Green");
288 assert_eq!(strip_ansi("No codes"), "No codes");
289 assert_eq!(strip_ansi("\x1b[38;2;255;0;0mTruecolor\x1b[0m"), "Truecolor");
290
291 assert_eq!(strip_ansi("\x1b]8;;https://example.com\x07link\x1b]8;;\x07"), "link");
293 }
294}