xacli_components/components/
multiselect.rs1use 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 self.clear_render(stdout)?;
65
66 let mut result = Vec::new();
68 for (i, (label, _)) in self.options.iter().enumerate() {
69 if selected[i] {
70 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 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 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 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 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 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}