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