xacli_components/components/
select.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 Select<T> {
15    prompt: String,
16    options: Vec<(String, T)>,
17}
18
19impl<T> Select<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<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<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<T> {
62        let mut selected = 0;
63        let mut first_render = true;
64
65        self.render(stdout, selected, first_render)?;
66        first_render = false;
67
68        loop {
69            if let Event::Key(key) = read_event()? {
70                match key.code {
71                    KeyCode::Enter => {
72                        // 清除渲染
73                        self.clear_render(stdout)?;
74                        // Use \r\n to properly handle raw mode
75                        queue!(
76                            stdout,
77                            cursor::MoveToColumn(0),
78                            Print("✓ "),
79                            Print(&self.options[selected].0),
80                            Print("\r\n")
81                        )?;
82                        stdout.flush()?;
83                        return Ok(self.options.remove(selected).1);
84                    }
85                    KeyCode::Esc => {
86                        self.clear_render(stdout)?;
87                        return Err(ComponentError::Interrupted);
88                    }
89                    KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
90                        self.clear_render(stdout)?;
91                        return Err(ComponentError::Interrupted);
92                    }
93                    KeyCode::Up | KeyCode::Char('k') => {
94                        if selected > 0 {
95                            selected -= 1;
96                            self.render(stdout, selected, first_render)?;
97                        }
98                    }
99                    KeyCode::Down | KeyCode::Char('j') => {
100                        if selected < self.options.len() - 1 {
101                            selected += 1;
102                            self.render(stdout, selected, first_render)?;
103                        }
104                    }
105                    KeyCode::Home => {
106                        selected = 0;
107                        self.render(stdout, selected, first_render)?;
108                    }
109                    KeyCode::End => {
110                        selected = self.options.len() - 1;
111                        self.render(stdout, selected, first_render)?;
112                    }
113                    _ => {}
114                }
115            }
116        }
117    }
118
119    fn render(&self, stdout: &mut impl Write, selected: usize, first_render: bool) -> Result<()> {
120        if !first_render {
121            // 清除之前的渲染
122            for _ in 0..self.options.len() + 1 {
123                queue!(
124                    stdout,
125                    cursor::MoveUp(1),
126                    terminal::Clear(ClearType::CurrentLine),
127                )?;
128            }
129        }
130
131        queue!(stdout, cursor::MoveToColumn(0))?;
132
133        // 提示
134        queue!(stdout, Print(&self.prompt), Print("\r\n"))?;
135
136        // 选项
137        for (i, (label, _)) in self.options.iter().enumerate() {
138            let marker = if i == selected { ">" } else { " " };
139            queue!(
140                stdout,
141                cursor::MoveToColumn(0),
142                Print(marker),
143                Print(" "),
144                Print(label),
145                Print("\r\n"),
146            )?;
147        }
148
149        stdout.flush()?;
150        Ok(())
151    }
152
153    fn clear_render(&self, stdout: &mut impl Write) -> Result<()> {
154        for _ in 0..self.options.len() + 1 {
155            queue!(
156                stdout,
157                cursor::MoveUp(1),
158                terminal::Clear(ClearType::CurrentLine),
159            )?;
160        }
161        queue!(stdout, cursor::MoveToColumn(0))?;
162        stdout.flush()?;
163        Ok(())
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use std::sync::mpsc;
170
171    use xacli_testing::{EventBuilder, OutputCapture};
172
173    use super::*;
174    use crate::components::{clear_test_event_source, set_test_event_source};
175
176    fn setup_events(events: Vec<Event>) -> mpsc::Sender<Event> {
177        let (tx, rx) = mpsc::channel();
178        set_test_event_source(rx);
179        for event in events {
180            tx.send(event).unwrap();
181        }
182        tx
183    }
184
185    #[test]
186    fn test_select_first_option() {
187        let _tx = setup_events(vec![EventBuilder::enter()]);
188
189        let mut output = OutputCapture::new();
190        let result = Select::new("Choose:")
191            .option("Option A", 1)
192            .option("Option B", 2)
193            .option("Option C", 3)
194            .run_with_output(&mut output)
195            .unwrap();
196
197        assert_eq!(result, 1);
198        clear_test_event_source();
199    }
200
201    #[test]
202    fn test_select_navigate_down() {
203        let _tx = setup_events(vec![
204            EventBuilder::down(),
205            EventBuilder::down(),
206            EventBuilder::enter(),
207        ]);
208
209        let mut output = OutputCapture::new();
210        let result = Select::new("Choose:")
211            .option("Option A", 1)
212            .option("Option B", 2)
213            .option("Option C", 3)
214            .run_with_output(&mut output)
215            .unwrap();
216
217        assert_eq!(result, 3);
218        clear_test_event_source();
219    }
220
221    #[test]
222    fn test_select_navigate_with_j_k() {
223        let _tx = setup_events(vec![
224            EventBuilder::char('j'), // Down
225            EventBuilder::char('j'), // Down
226            EventBuilder::char('k'), // Up
227            EventBuilder::enter(),
228        ]);
229
230        let mut output = OutputCapture::new();
231        let result = Select::new("Choose:")
232            .option("Option A", 1)
233            .option("Option B", 2)
234            .option("Option C", 3)
235            .run_with_output(&mut output)
236            .unwrap();
237
238        assert_eq!(result, 2);
239        clear_test_event_source();
240    }
241
242    #[test]
243    fn test_select_home_end() {
244        let _tx = setup_events(vec![
245            EventBuilder::end(),  // Go to last
246            EventBuilder::home(), // Go to first
247            EventBuilder::enter(),
248        ]);
249
250        let mut output = OutputCapture::new();
251        let result = Select::new("Choose:")
252            .option("Option A", 1)
253            .option("Option B", 2)
254            .option("Option C", 3)
255            .run_with_output(&mut output)
256            .unwrap();
257
258        assert_eq!(result, 1);
259        clear_test_event_source();
260    }
261
262    #[test]
263    fn test_select_escape_interrupts() {
264        let _tx = setup_events(vec![EventBuilder::escape()]);
265
266        let mut output = OutputCapture::new();
267        let result = Select::<i32>::new("Choose:")
268            .option("Option A", 1)
269            .option("Option B", 2)
270            .run_with_output(&mut output);
271
272        assert!(matches!(result, Err(ComponentError::Interrupted)));
273        clear_test_event_source();
274    }
275
276    #[test]
277    fn test_select_no_options() {
278        let mut output = OutputCapture::new();
279        let result = Select::<i32>::new("Choose:").run_with_output(&mut output);
280
281        assert!(matches!(result, Err(ComponentError::InvalidInput(_))));
282    }
283}