xacli_components/components/
multiselect.rs

1use 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    /// Run with custom output writer (for testing)
53    #[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                        // 清除渲染
74                        self.clear_render(stdout)?;
75
76                        // 收集选中的项并打印
77                        let mut result = Vec::new();
78                        for (i, (label, _)) in self.options.iter().enumerate() {
79                            if selected[i] {
80                                // Use \r\n to properly handle raw mode
81                                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                        // Select all
134                        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                        // Deselect all
141                        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            // 清除之前的渲染
161            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        // 提示
173        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        // 选项
182        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(), // Select first
253            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(), // Select first
272            EventBuilder::down(),
273            EventBuilder::down(),
274            EventBuilder::space(), // Select third
275            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(), // Select
294            EventBuilder::space(), // Deselect
295            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'), // Select all
313            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'), // Select all
332            EventBuilder::ctrl('d'), // Deselect all
333            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'), // Down
351            EventBuilder::char('j'), // Down
352            EventBuilder::space(),   // Select third
353            EventBuilder::char('k'), // Up
354            EventBuilder::space(),   // Select second
355            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}