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