xacli_components/components/
select.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 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 #[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 self.clear_render(stdout)?;
74 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 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 queue!(stdout, Print(&self.prompt), Print("\r\n"))?;
135
136 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'), EventBuilder::char('j'), EventBuilder::char('k'), 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(), EventBuilder::home(), 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}