xacli_components/components/multiselect/
mod.rs

1use 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        // Enable raw mode for keyboard input
35        terminal::enable_raw_mode()?;
36        let result = self.run_inner(ctx);
37        // Always disable raw mode when done
38        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                        // Collect selected values
63                        let mut result = Vec::new();
64                        for (i, (label, _)) in self.options.iter().enumerate() {
65                            if selected[i] {
66                                // Use \r\n to properly handle raw mode
67                                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                        // Select all
121                        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                        // Deselect all
128                        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            // Clear previous render
148            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        // Prompt
158        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        // Options
169        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}