1use crate::*;
2use std::{collections::{LinkedList, VecDeque}, ops::Deref};
3
4
5
6fn read_list<Data>(input_options: &[InputOption<Data>], prompt: Option<String>, default: Option<usize>) -> BoxResult<usize> {
8 if input_options.is_empty() {return Err(Box::new(ListConstraintError::EmptyList));}
9
10 let prompt = prompt.unwrap_or(String::from("Enter one of the following:"));
12 let display_strings =
13 input_options.iter().enumerate()
14 .map(|(i, option)| {
15 option.get_display_string(default.map(|default| i == default))
16 })
17 .collect::<Vec<_>>();
18
19 let (mut all_choose_strings, mut choose_name_mappings, mut choose_name_hidden_flags) = (vec!(), vec!(), vec!());
21 for (i, option) in input_options.iter().enumerate() {
22 if let Some(bulletin_string) = option.bulletin_string.as_deref() {
23 all_choose_strings.push(bulletin_string);
24 choose_name_mappings.push(i);
25 choose_name_hidden_flags.push(false);
26 }
27 all_choose_strings.push(option.get_name());
28 choose_name_mappings.push(i);
29 choose_name_hidden_flags.push(false);
30 for alt_name in &option.names[1..] {
31 all_choose_strings.push(alt_name);
32 choose_name_mappings.push(i);
33 choose_name_hidden_flags.push(true);
34 }
35 }
36
37 let print_prompt = || {
39 println!("{prompt}");
40 for option in display_strings.iter() {
41 println!("{option}");
42 }
43 println!();
44 };
45
46 if input_options.len() == 1 {
47 print_prompt();
48 println!();
49 println!("Automatically choosing the first option because it is the only option");
50 return Ok(0);
51 }
52
53 print_prompt();
54 let mut input = read_stdin()?;
55
56 loop {
58 if input.is_empty() && let Some(default) = default {
59 return Ok(default);
60 }
61
62 for (i, option) in all_choose_strings.iter().enumerate() {
64 if option.eq_ignore_ascii_case(&input) {
65 let chosen_index = choose_name_mappings[i];
66 return Ok(chosen_index);
67 }
68 }
69
70 println!();
71 println!("Invalid option.");
72
73 if let Some(possible_choose_string_index) = custom_fuzzy_search(&input, &all_choose_strings) {
75 let possible_option_index = choose_name_mappings[possible_choose_string_index];
76 let possible_option = &input_options[possible_option_index];
77 if choose_name_hidden_flags[possible_choose_string_index] {
78 print!("Did you mean to type \"{}\", for option \"{}\"? (enter nothing to confirm, or re-enter input) ", all_choose_strings[possible_choose_string_index], possible_option.get_name());
79 } else {
80 print!("Did you mean \"{}\"? (enter nothing to confirm, or re-enter input) ", all_choose_strings[possible_choose_string_index]);
81 }
82 let new_input = read_stdin()?;
83 if new_input.is_empty() {
84 let chosen_index = possible_option_index;
85 return Ok(chosen_index);
86 }
87 input = new_input;
88 } else {
89 print!("Invalid option, please re-enter input: ");
90 input = read_stdin()?;
91 }
92
93 }
94}
95
96
97
98impl<'a, Data> TryRead for &'a [InputOption<Data>] {
100 type Output = (usize, &'a InputOption<Data>);
101 type Default = usize;
102 fn try_read_line(self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
103 let chosen_index = read_list(self, prompt, default)?;
104 Ok((chosen_index, &self[chosen_index]))
105 }
106}
107
108impl<'a, Data, const LEN: usize> TryRead for &'a [InputOption<Data>; LEN] {
111 type Output = (usize, &'a InputOption<Data>);
112 type Default = usize;
113 fn try_read_line(self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
114 let chosen_index = read_list(self, prompt, default)?;
115 Ok((chosen_index, &self[chosen_index]))
116 }
117}
118
119impl<Data, const LEN: usize> TryRead for [InputOption<Data>; LEN] {
121 type Output = (usize, InputOption<Data>);
122 type Default = usize;
123 fn try_read_line(self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
124 let chosen_index = read_list(&self, prompt, default)?;
125 #[allow(clippy::expect_used)] Ok((chosen_index, self.into_iter().nth(chosen_index).expect("chosen index is out of bounds")))
127 }
128}
129
130
131
132#[derive(Debug)]
134pub enum ListConstraintError {
135 EmptyList,
137}
138
139impl Error for ListConstraintError {}
140
141impl Display for ListConstraintError {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 match self {
144 Self::EmptyList => write!(f, "List Constraint is empty"),
145 }
146 }
147}
148
149
150
151pub fn custom_fuzzy_search(pattern: &str, items: &[&str]) -> Option<usize> {
153 let (mut best_score, mut best_index) = (custom_fuzzy_match(pattern, items[0]), 0);
154 for (i, item) in items.iter().enumerate().skip(1) {
155 let score = custom_fuzzy_match(pattern, item);
156 if score > best_score {
157 best_score = score;
158 best_index = i;
159 }
160 }
161 if best_score > 0.0 {
162 Some(best_index)
163 } else {
164 None
165 }
166}
167
168pub fn custom_fuzzy_match(pattern: &str, item: &str) -> f32 {
170 let mut best_score = 0.0f32;
171 let offset_start = pattern.len() as isize * -1 + 1;
172 let offset_end = item.len() as isize - 1;
173 for offset in offset_start..=offset_end {
174 let item_slice = &item[offset.max(0) as usize .. (offset + pattern.len() as isize).min(item.len() as isize) as usize];
175 let pattern_slice = &pattern[(offset * -1).max(0) as usize .. (item.len() as isize - offset).min(pattern.len() as isize) as usize];
176 let mut slice_score = 0.0f32;
177 for (item_char, pattern_char) in item_slice.chars().zip(pattern_slice.chars()) {
178 if item_char.eq_ignore_ascii_case(&pattern_char) {
179 slice_score += 3.;
180 } else {
181 slice_score -= 1.;
182 }
183 }
184 slice_score *= 1. - offset as f32 / item.len() as f32 * 0.5; best_score = best_score.max(slice_score);
186 }
187 best_score
188}
189
190
191
192
193
194pub struct InputOption<Data> {
219 pub bulletin_string: Option<String>,
221 pub names: Vec<String>,
223 pub extra_data: Data,
225}
226
227impl<Data> InputOption<Data> {
228 pub fn new(bulletin: impl ToString, names: impl IntoVecString, data: Data) -> Self {
230 Self {
231 bulletin_string: Some(bulletin.to_string()),
232 names: names.into_vec_string(),
233 extra_data: data,
234 }
235 }
236 pub fn new_without_bulletin(names: impl IntoVecString, data: Data) -> Self {
238 Self {
239 bulletin_string: None,
240 names: names.into_vec_string(),
241 extra_data: data,
242 }
243 }
244 pub fn get_display_string(&self, is_default: Option<bool>) -> String {
246 let name = self.get_name();
247 match (self.bulletin_string.as_deref(), is_default) {
248 (Some(bulletin_string), Some(true )) => format!("[{bulletin_string}]: {name}",),
249 (Some(bulletin_string), Some(false)) => format!(" {bulletin_string}: {name}",),
250 (None , Some(true )) => format!("[{name}]",),
251 (None , Some(false)) => format!(" {name} ",),
252 (Some(bulletin_string), None ) => format!("{bulletin_string}: {name}",),
253 (None , None ) => name.to_string(),
254 }
255 }
256 pub fn get_name(&self) -> &str {
259 self.names.first().map(Deref::deref).unwrap_or("[unnamed]")
260 }
261}
262
263pub trait IntoVecString {
265 fn into_vec_string(self) -> Vec<String>;
267}
268
269impl IntoVecString for Vec<String> {
270 fn into_vec_string(self) -> Vec<String> {self}
271}
272
273impl IntoVecString for Vec<&str> {
274 fn into_vec_string(self) -> Vec<String> {
275 self.into_iter().map(str::to_string).collect()
276 }
277}
278
279
280
281
282
283impl<'a, T: Display> TryRead for &'a [T] {
284 type Output = (usize, &'a T);
285 type Default = usize;
286 fn try_read_line(self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
287 let options = self.iter().enumerate()
288 .map(|(i, option)| {
289 InputOption {
290 bulletin_string: Some((i + 1).to_string()),
291 names: vec!(option.to_string()),
292 extra_data: (),
293 }
294 })
295 .collect::<Vec<_>>();
296 let chosen_index = (options.deref()).try_read_line(prompt, default)?.0;
297 Ok((chosen_index, &self[chosen_index]))
298 }
299}
300
301impl<T: Display, const LEN: usize> TryRead for [T; LEN] {
322 type Output = (usize, T);
323 type Default = usize;
324 fn try_read_line(self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
325 let options = self.iter().enumerate()
326 .map(|(i, option)| {
327 InputOption {
328 bulletin_string: Some((i + 1).to_string()),
329 names: vec!(option.to_string()),
330 extra_data: (),
331 }
332 })
333 .collect::<Vec<_>>();
334 let chosen_index = (options.deref()).try_read_line(prompt, default)?.0;
335 #[allow(clippy::expect_used)] Ok((chosen_index, self.into_iter().nth(chosen_index).expect("chosen index is out of bounds")))
337 }
338}
339
340impl<T: Display> TryRead for Vec<T> {
341 type Output = (usize, T);
342 type Default = usize;
343 fn try_read_line(mut self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
344 let options = self.iter().enumerate()
345 .map(|(i, option)| {
346 InputOption {
347 bulletin_string: Some((i + 1).to_string()),
348 names: vec!(option.to_string()),
349 extra_data: (),
350 }
351 })
352 .collect::<Vec<_>>();
353 let chosen_index = (options.deref()).try_read_line(prompt, default)?.0;
354 Ok((chosen_index, self.swap_remove(chosen_index)))
355 }
356}
357
358impl<T: Display> TryRead for VecDeque<T> {
359 type Output = (usize, T);
360 type Default = usize;
361 fn try_read_line(mut self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
362 let options = self.iter().enumerate()
363 .map(|(i, option)| {
364 InputOption {
365 bulletin_string: Some((i + 1).to_string()),
366 names: vec!(option.to_string()),
367 extra_data: (),
368 }
369 })
370 .collect::<Vec<_>>();
371 let chosen_index = (options.deref()).try_read_line(prompt, default)?.0;
372 #[allow(clippy::expect_used)] Ok((chosen_index, self.swap_remove_back(chosen_index).expect("chosen index is out of bounds")))
374 }
375}
376
377impl<T: Display> TryRead for LinkedList<T> {
378 type Output = (usize, T);
379 type Default = usize;
380 fn try_read_line(self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
381 let options = self.iter().enumerate()
382 .map(|(i, option)| {
383 InputOption {
384 bulletin_string: Some((i + 1).to_string()),
385 names: vec!(option.to_string()),
386 extra_data: (),
387 }
388 })
389 .collect::<Vec<_>>();
390 let chosen_index = (options.deref()).try_read_line(prompt, default)?.0;
391 #[allow(clippy::expect_used)] Ok((chosen_index, self.into_iter().nth(chosen_index).expect("chosen index is out of bounds")))
393 }
394}