cartographer_rs/menu/
interact.rs1use crate::Menu;
2use crate::MenuItem;
3use crate::MenuOptions;
4use console::Key;
5use console::Term;
6use rust_fuzzy_search::fuzzy_compare;
7use std::io::Write;
8
9struct MenuItemKeepTrack {
10 menu_item: MenuItem,
11 is_visible: bool,
12 is_selected: bool,
13}
14
15struct MenuState {
17 prompt: String,
19 inputed: String,
20 cursor_row: usize,
21
22 rows: Vec<MenuItemKeepTrack>,
24
25 term: Term,
27
28 lines_written: usize,
30}
31
32impl MenuState {
33 fn search_from_inputed(&mut self, opts: &MenuOptions) {
36 let mut num_results = 0;
38
39 for i in 0..self.rows.len() {
44 let mut score = 0.0;
46
47 if self.rows[i].is_selected && opts.show_select_in_search {
50 self.rows[i].is_visible = true;
51 num_results += 1;
52 } else {
53 score += fuzzy_compare(
54 &self.rows[i].menu_item.visible_name.to_lowercase(),
55 &self.inputed.to_lowercase(),
56 );
57 if self.rows[i].menu_item.alternative_matches.is_some() {
58 for i in self.rows[i].menu_item.alternative_matches.clone().unwrap() {
59 score += fuzzy_compare(&i.to_lowercase(), &self.inputed.to_lowercase());
60 score /= 2.0;
61 }
62 }
63 if score > opts.min_search_threshold {
64 num_results += 1;
65 self.rows[i].is_visible = true;
66 } else {
67 self.rows[i].is_visible = false;
68 }
69 }
70 }
71
72 if num_results == 0 {
74 for i in 0..self.rows.len() {
75 if self.rows[i].menu_item.visible_at_rest {
76 self.rows[i].is_visible = true;
77 }
78 self.cursor_row = 0;
81 }
82 } else {
83 if (self.lines_written as i32 - 3) <= 0 {
86 self.cursor_row = 0;
87 } else {
88 self.cursor_row = (self.cursor_row / (self.lines_written - 3)) * num_results;
89 }
90 }
91 }
92
93 fn mark_selected(&mut self) {
95 let mut counter = 0;
100 for i in 0..self.rows.len() {
101 if self.rows[i].is_visible {
102 if counter == self.cursor_row {
103 self.rows[i].is_selected = !self.rows[i].is_selected;
104 }
105 counter += 1;
106 }
107 }
108 }
109
110 fn get_row(
112 &self,
113 item: &MenuItemKeepTrack,
114 cur_redraw_row: usize,
115 opts: &MenuOptions,
116 ) -> String {
117 let cursor = if self.cursor_row == cur_redraw_row {
120 opts.cursor.to_string()
121 } else {
122 " ".repeat(opts.cursor_width)
123 };
124
125 let sel_indicator = match item.is_selected {
127 true => opts.selected_indicator.to_string() + " ",
128 false => " ".repeat(opts.selected_indicator_width),
129 };
130
131 return cursor + sel_indicator.as_str() + item.menu_item.visible_name.as_str();
132 }
133
134 fn get_menu_string(&mut self, opts: &MenuOptions) -> Result<String, std::io::Error> {
135 let mut next_screen_num_lines = 0;
137
138 let next_screen = {
140 let mut output = String::new();
141
142 for i in 0..self.rows.len() {
145 let item = self.rows.get(i).unwrap();
146
147 if next_screen_num_lines + 1 > opts.max_lines_visible {
150 break;
151 }
152
153 if item.is_visible {
154 let x = self.get_row(item, next_screen_num_lines, opts) + "\n";
155 output += x.as_str();
156
157 next_screen_num_lines += 1;
158 }
159 }
160 output
161 };
162 Ok(next_screen)
163 }
164
165 fn redraw(&mut self, opts: &MenuOptions) -> Result<(), std::io::Error> {
167 let mut next_screen: String;
168 let mut next_screen_num_lines: usize;
169
170 loop {
171 next_screen = self.get_menu_string(opts)?;
172
173 next_screen_num_lines = next_screen.matches('\n').count();
174 if (next_screen_num_lines as i32 - 1) < self.cursor_row as i32 {
175 self.cursor_row -= 1;
176 continue;
177 } else {
178 next_screen += self.prompt.as_str();
180 next_screen += self.inputed.as_str();
181 next_screen_num_lines = next_screen.matches('\n').count() + 1;
182 break;
183 }
184 }
185
186 if self.lines_written != 0 {
188 self.term.clear_line()?;
191 self.term.clear_last_lines(self.lines_written - 1)?;
192 self.lines_written = 0;
193 }
194 self.term.write_all(next_screen.as_bytes())?;
196 self.term.flush()?;
197 self.lines_written = next_screen_num_lines;
198
199 Ok(())
200 }
201}
202
203impl Menu {
204 pub fn serve(&self) -> Result<Option<Vec<String>>, std::io::Error> {
207 let term = Term::stdout();
208
209 let mut state = MenuState {
210 prompt: self.prompt.clone(),
211 lines_written: 0,
212 cursor_row: 1,
213 inputed: String::new(),
214 rows: Vec::<MenuItemKeepTrack>::new(),
215 term,
216 };
217
218 for i in 0..self.items.len() {
220 let item = self.items[i].clone();
221
222 let mut is_visible = false;
223 if self.items[i].visible_at_rest {
224 is_visible = true;
225 }
226
227 state.rows.push(MenuItemKeepTrack {
228 menu_item: item.clone(),
229 is_visible,
230 is_selected: false,
231 });
232 }
233
234 loop {
235 state.redraw(&self.configuration)?;
236 let usr_key = state.term.read_key()?;
237
238 match usr_key {
239 Key::Char(c) => {
240 if Key::Char(c) == self.configuration.select_key {
241 state.mark_selected();
242 } else {
243 state.inputed.push(c);
244 state.search_from_inputed(&self.configuration);
245 }
246 }
247 Key::Backspace => {
248 state.inputed.pop();
249 state.search_from_inputed(&self.configuration);
250 }
251 Key::ArrowUp | Key::ArrowLeft => {
252 if state.cursor_row != 0 {
253 state.cursor_row -= 1;
254 }
255 }
256 Key::Tab => {
257 if (state.cursor_row as i32) <= (state.lines_written as i32) - 3 {
258 state.cursor_row += 1;
259 } else {
260 state.cursor_row = 0;
261 }
262 }
263 Key::ArrowDown | Key::ArrowRight => {
264 if (state.cursor_row as i32) <= (state.lines_written as i32) - 3 {
265 state.cursor_row += 1;
266 }
267 }
268 Key::Enter => {
269 break;
270 }
271 _ => {
272 continue;
277 }
278 }
279 }
280
281 let mut output: Vec<String> = Vec::new();
282 for i in state.rows {
283 if i.is_selected {
284 output.push(i.menu_item.visible_name);
285 }
286 }
287
288 if output.is_empty() {
289 Ok(None)
290 } else {
291 Ok(Some(output))
292 }
293 }
294}