xacli_components/components/
multiselect.rs1use std::io::{stdout, Write};
2
3use crossterm::{
4 cursor,
5 event::{Event, KeyCode, KeyModifiers},
6 queue,
7 style::Print,
8 terminal::{self, ClearType},
9};
10
11use super::read_event;
12use crate::{ComponentError, Result};
13
14pub struct MultiSelect<T> {
15 prompt: String,
16 options: Vec<(String, T)>,
17}
18
19impl<T> MultiSelect<T> {
20 pub fn new(prompt: impl Into<String>) -> Self {
21 Self {
22 prompt: prompt.into(),
23 options: Vec::new(),
24 }
25 }
26
27 pub fn option(mut self, label: impl Into<String>, value: T) -> Self {
28 self.options.push((label.into(), value));
29 self
30 }
31
32 pub fn options(mut self, options: Vec<(String, T)>) -> Self {
33 self.options = options;
34 self
35 }
36
37 pub fn run(self) -> Result<Vec<T>> {
38 if self.options.is_empty() {
39 return Err(ComponentError::InvalidInput("No options provided".into()));
40 }
41
42 let mut stdout = stdout();
43 terminal::enable_raw_mode()?;
44
45 let result = self.run_inner(&mut stdout);
46
47 terminal::disable_raw_mode()?;
48
49 result
50 }
51
52 #[cfg(test)]
54 pub fn run_with_output(self, output: &mut impl Write) -> Result<Vec<T>> {
55 if self.options.is_empty() {
56 return Err(ComponentError::InvalidInput("No options provided".into()));
57 }
58 self.run_inner(output)
59 }
60
61 fn run_inner(mut self, stdout: &mut impl Write) -> Result<Vec<T>> {
62 let mut cursor = 0;
63 let mut selected = vec![false; self.options.len()];
64 let mut first_render = true;
65
66 self.render(stdout, cursor, &selected, first_render)?;
67 first_render = false;
68
69 loop {
70 if let Event::Key(key) = read_event()? {
71 match key.code {
72 KeyCode::Enter => {
73 self.clear_render(stdout)?;
75
76 let mut result = Vec::new();
78 for (i, (label, _)) in self.options.iter().enumerate() {
79 if selected[i] {
80 queue!(
82 stdout,
83 cursor::MoveToColumn(0),
84 Print("✓ "),
85 Print(label),
86 Print("\r\n")
87 )?;
88 }
89 }
90 stdout.flush()?;
91
92 for (i, selected) in selected.iter().enumerate() {
93 if *selected {
94 result.push(self.options.remove(i - result.len()).1);
95 }
96 }
97
98 return Ok(result);
99 }
100 KeyCode::Esc => {
101 self.clear_render(stdout)?;
102 return Err(ComponentError::Interrupted);
103 }
104 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
105 self.clear_render(stdout)?;
106 return Err(ComponentError::Interrupted);
107 }
108 KeyCode::Char(' ') => {
109 selected[cursor] = !selected[cursor];
110 self.render(stdout, cursor, &selected, first_render)?;
111 }
112 KeyCode::Up | KeyCode::Char('k') => {
113 if cursor > 0 {
114 cursor -= 1;
115 self.render(stdout, cursor, &selected, first_render)?;
116 }
117 }
118 KeyCode::Down | KeyCode::Char('j') => {
119 if cursor < self.options.len() - 1 {
120 cursor += 1;
121 self.render(stdout, cursor, &selected, first_render)?;
122 }
123 }
124 KeyCode::Home => {
125 cursor = 0;
126 self.render(stdout, cursor, &selected, first_render)?;
127 }
128 KeyCode::End => {
129 cursor = self.options.len() - 1;
130 self.render(stdout, cursor, &selected, first_render)?;
131 }
132 KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
133 for s in selected.iter_mut() {
135 *s = true;
136 }
137 self.render(stdout, cursor, &selected, first_render)?;
138 }
139 KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
140 for s in selected.iter_mut() {
142 *s = false;
143 }
144 self.render(stdout, cursor, &selected, first_render)?;
145 }
146 _ => {}
147 }
148 }
149 }
150 }
151
152 fn render(
153 &self,
154 stdout: &mut impl Write,
155 cursor: usize,
156 selected: &[bool],
157 first_render: bool,
158 ) -> Result<()> {
159 if !first_render {
160 for _ in 0..self.options.len() + 2 {
162 queue!(
163 stdout,
164 cursor::MoveUp(1),
165 terminal::Clear(ClearType::CurrentLine),
166 )?;
167 }
168 }
169
170 queue!(stdout, cursor::MoveToColumn(0))?;
171
172 queue!(stdout, Print(&self.prompt), Print("\r\n"))?;
174 queue!(
175 stdout,
176 cursor::MoveToColumn(0),
177 Print("(Space to select, Enter to confirm, Ctrl+A/D to select/deselect all)"),
178 Print("\r\n")
179 )?;
180
181 for (i, (label, _)) in self.options.iter().enumerate() {
183 let marker = if i == cursor { ">" } else { " " };
184 let checkbox = if selected[i] { "[✓]" } else { "[ ]" };
185 queue!(
186 stdout,
187 cursor::MoveToColumn(0),
188 Print(marker),
189 Print(" "),
190 Print(checkbox),
191 Print(" "),
192 Print(label),
193 Print("\r\n"),
194 )?;
195 }
196
197 stdout.flush()?;
198 Ok(())
199 }
200
201 fn clear_render(&self, stdout: &mut impl Write) -> Result<()> {
202 for _ in 0..self.options.len() + 2 {
203 queue!(
204 stdout,
205 cursor::MoveUp(1),
206 terminal::Clear(ClearType::CurrentLine),
207 )?;
208 }
209 queue!(stdout, cursor::MoveToColumn(0))?;
210 stdout.flush()?;
211 Ok(())
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use std::sync::mpsc;
218
219 use xacli_testing::{EventBuilder, OutputCapture};
220
221 use super::*;
222 use crate::components::{clear_test_event_source, set_test_event_source};
223
224 fn setup_events(events: Vec<Event>) -> mpsc::Sender<Event> {
225 let (tx, rx) = mpsc::channel();
226 set_test_event_source(rx);
227 for event in events {
228 tx.send(event).unwrap();
229 }
230 tx
231 }
232
233 #[test]
234 fn test_multiselect_none_selected() {
235 let _tx = setup_events(vec![EventBuilder::enter()]);
236
237 let mut output = OutputCapture::new();
238 let result = MultiSelect::new("Choose:")
239 .option("Option A", 1)
240 .option("Option B", 2)
241 .option("Option C", 3)
242 .run_with_output(&mut output)
243 .unwrap();
244
245 assert!(result.is_empty());
246 clear_test_event_source();
247 }
248
249 #[test]
250 fn test_multiselect_select_first() {
251 let _tx = setup_events(vec![
252 EventBuilder::space(), EventBuilder::enter(),
254 ]);
255
256 let mut output = OutputCapture::new();
257 let result = MultiSelect::new("Choose:")
258 .option("Option A", 1)
259 .option("Option B", 2)
260 .option("Option C", 3)
261 .run_with_output(&mut output)
262 .unwrap();
263
264 assert_eq!(result, vec![1]);
265 clear_test_event_source();
266 }
267
268 #[test]
269 fn test_multiselect_select_multiple() {
270 let _tx = setup_events(vec![
271 EventBuilder::space(), EventBuilder::down(),
273 EventBuilder::down(),
274 EventBuilder::space(), EventBuilder::enter(),
276 ]);
277
278 let mut output = OutputCapture::new();
279 let result = MultiSelect::new("Choose:")
280 .option("Option A", 1)
281 .option("Option B", 2)
282 .option("Option C", 3)
283 .run_with_output(&mut output)
284 .unwrap();
285
286 assert_eq!(result, vec![1, 3]);
287 clear_test_event_source();
288 }
289
290 #[test]
291 fn test_multiselect_toggle_selection() {
292 let _tx = setup_events(vec![
293 EventBuilder::space(), EventBuilder::space(), EventBuilder::enter(),
296 ]);
297
298 let mut output = OutputCapture::new();
299 let result = MultiSelect::new("Choose:")
300 .option("Option A", 1)
301 .option("Option B", 2)
302 .run_with_output(&mut output)
303 .unwrap();
304
305 assert!(result.is_empty());
306 clear_test_event_source();
307 }
308
309 #[test]
310 fn test_multiselect_select_all() {
311 let _tx = setup_events(vec![
312 EventBuilder::ctrl('a'), EventBuilder::enter(),
314 ]);
315
316 let mut output = OutputCapture::new();
317 let result = MultiSelect::new("Choose:")
318 .option("Option A", 1)
319 .option("Option B", 2)
320 .option("Option C", 3)
321 .run_with_output(&mut output)
322 .unwrap();
323
324 assert_eq!(result, vec![1, 2, 3]);
325 clear_test_event_source();
326 }
327
328 #[test]
329 fn test_multiselect_deselect_all() {
330 let _tx = setup_events(vec![
331 EventBuilder::ctrl('a'), EventBuilder::ctrl('d'), EventBuilder::enter(),
334 ]);
335
336 let mut output = OutputCapture::new();
337 let result = MultiSelect::new("Choose:")
338 .option("Option A", 1)
339 .option("Option B", 2)
340 .run_with_output(&mut output)
341 .unwrap();
342
343 assert!(result.is_empty());
344 clear_test_event_source();
345 }
346
347 #[test]
348 fn test_multiselect_navigate_with_j_k() {
349 let _tx = setup_events(vec![
350 EventBuilder::char('j'), EventBuilder::char('j'), EventBuilder::space(), EventBuilder::char('k'), EventBuilder::space(), EventBuilder::enter(),
356 ]);
357
358 let mut output = OutputCapture::new();
359 let result = MultiSelect::new("Choose:")
360 .option("Option A", 1)
361 .option("Option B", 2)
362 .option("Option C", 3)
363 .run_with_output(&mut output)
364 .unwrap();
365
366 assert_eq!(result, vec![2, 3]);
367 clear_test_event_source();
368 }
369
370 #[test]
371 fn test_multiselect_escape_interrupts() {
372 let _tx = setup_events(vec![EventBuilder::escape()]);
373
374 let mut output = OutputCapture::new();
375 let result = MultiSelect::<i32>::new("Choose:")
376 .option("Option A", 1)
377 .option("Option B", 2)
378 .run_with_output(&mut output);
379
380 assert!(matches!(result, Err(ComponentError::Interrupted)));
381 clear_test_event_source();
382 }
383
384 #[test]
385 fn test_multiselect_no_options() {
386 let mut output = OutputCapture::new();
387 let result = MultiSelect::<i32>::new("Choose:").run_with_output(&mut output);
388
389 assert!(matches!(result, Err(ComponentError::InvalidInput(_))));
390 }
391}