xacli_components/components/
multiselect.rs

1use std::io::{stdout, Write};
2
3use crossterm::{
4    cursor,
5    event::{self, Event, KeyCode, KeyModifiers},
6    queue,
7    style::Print,
8    terminal::{self, ClearType},
9};
10
11use crate::{ComponentError, Result};
12
13pub struct MultiSelect<T> {
14    prompt: String,
15    options: Vec<(String, T)>,
16}
17
18impl<T> MultiSelect<T> {
19    pub fn new(prompt: impl Into<String>) -> Self {
20        Self {
21            prompt: prompt.into(),
22            options: Vec::new(),
23        }
24    }
25
26    pub fn option(mut self, label: impl Into<String>, value: T) -> Self {
27        self.options.push((label.into(), value));
28        self
29    }
30
31    pub fn options(mut self, options: Vec<(String, T)>) -> Self {
32        self.options = options;
33        self
34    }
35
36    pub fn run(self) -> Result<Vec<T>> {
37        if self.options.is_empty() {
38            return Err(ComponentError::InvalidInput("No options provided".into()));
39        }
40
41        let mut stdout = stdout();
42        terminal::enable_raw_mode()?;
43
44        let result = self.run_inner(&mut stdout);
45
46        terminal::disable_raw_mode()?;
47
48        result
49    }
50
51    fn run_inner(mut self, stdout: &mut impl Write) -> Result<Vec<T>> {
52        let mut cursor = 0;
53        let mut selected = vec![false; self.options.len()];
54        let mut first_render = true;
55
56        self.render(stdout, cursor, &selected, first_render)?;
57        first_render = false;
58
59        loop {
60            if let Event::Key(key) = event::read()? {
61                match key.code {
62                    KeyCode::Enter => {
63                        // 清除渲染
64                        self.clear_render(stdout)?;
65
66                        // 收集选中的项并打印
67                        let mut result = Vec::new();
68                        for (i, (label, _)) in self.options.iter().enumerate() {
69                            if selected[i] {
70                                // Use \r\n to properly handle raw mode
71                                queue!(
72                                    stdout,
73                                    cursor::MoveToColumn(0),
74                                    Print("✓ "),
75                                    Print(label),
76                                    Print("\r\n")
77                                )?;
78                            }
79                        }
80                        stdout.flush()?;
81
82                        for (i, selected) in selected.iter().enumerate() {
83                            if *selected {
84                                result.push(self.options.remove(i - result.len()).1);
85                            }
86                        }
87
88                        return Ok(result);
89                    }
90                    KeyCode::Esc => {
91                        self.clear_render(stdout)?;
92                        return Err(ComponentError::Interrupted);
93                    }
94                    KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
95                        self.clear_render(stdout)?;
96                        return Err(ComponentError::Interrupted);
97                    }
98                    KeyCode::Char(' ') => {
99                        selected[cursor] = !selected[cursor];
100                        self.render(stdout, cursor, &selected, first_render)?;
101                    }
102                    KeyCode::Up | KeyCode::Char('k') => {
103                        if cursor > 0 {
104                            cursor -= 1;
105                            self.render(stdout, cursor, &selected, first_render)?;
106                        }
107                    }
108                    KeyCode::Down | KeyCode::Char('j') => {
109                        if cursor < self.options.len() - 1 {
110                            cursor += 1;
111                            self.render(stdout, cursor, &selected, first_render)?;
112                        }
113                    }
114                    KeyCode::Home => {
115                        cursor = 0;
116                        self.render(stdout, cursor, &selected, first_render)?;
117                    }
118                    KeyCode::End => {
119                        cursor = self.options.len() - 1;
120                        self.render(stdout, cursor, &selected, first_render)?;
121                    }
122                    KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
123                        // Select all
124                        for s in selected.iter_mut() {
125                            *s = true;
126                        }
127                        self.render(stdout, cursor, &selected, first_render)?;
128                    }
129                    KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
130                        // Deselect all
131                        for s in selected.iter_mut() {
132                            *s = false;
133                        }
134                        self.render(stdout, cursor, &selected, first_render)?;
135                    }
136                    _ => {}
137                }
138            }
139        }
140    }
141
142    fn render(
143        &self,
144        stdout: &mut impl Write,
145        cursor: usize,
146        selected: &[bool],
147        first_render: bool,
148    ) -> Result<()> {
149        if !first_render {
150            // 清除之前的渲染
151            for _ in 0..self.options.len() + 2 {
152                queue!(
153                    stdout,
154                    cursor::MoveUp(1),
155                    terminal::Clear(ClearType::CurrentLine),
156                )?;
157            }
158        }
159
160        queue!(stdout, cursor::MoveToColumn(0))?;
161
162        // 提示
163        queue!(stdout, Print(&self.prompt), Print("\r\n"))?;
164        queue!(
165            stdout,
166            cursor::MoveToColumn(0),
167            Print("(Space to select, Enter to confirm, Ctrl+A/D to select/deselect all)"),
168            Print("\r\n")
169        )?;
170
171        // 选项
172        for (i, (label, _)) in self.options.iter().enumerate() {
173            let marker = if i == cursor { ">" } else { " " };
174            let checkbox = if selected[i] { "[✓]" } else { "[ ]" };
175            queue!(
176                stdout,
177                cursor::MoveToColumn(0),
178                Print(marker),
179                Print(" "),
180                Print(checkbox),
181                Print(" "),
182                Print(label),
183                Print("\r\n"),
184            )?;
185        }
186
187        stdout.flush()?;
188        Ok(())
189    }
190
191    fn clear_render(&self, stdout: &mut impl Write) -> Result<()> {
192        for _ in 0..self.options.len() + 2 {
193            queue!(
194                stdout,
195                cursor::MoveUp(1),
196                terminal::Clear(ClearType::CurrentLine),
197            )?;
198        }
199        queue!(stdout, cursor::MoveToColumn(0))?;
200        stdout.flush()?;
201        Ok(())
202    }
203}