use std::io::Write;
use crossterm::{
cursor,
event::{Event, KeyCode, KeyModifiers},
queue, style, terminal,
};
use xacli_core::{Context, Error, InputValue, Result};
pub struct MultiSelect {
prompt: String,
options: Vec<(String, InputValue)>,
}
impl MultiSelect {
pub fn new(prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
options: Vec::new(),
}
}
pub fn option(mut self, label: impl Into<String>, value: InputValue) -> Self {
self.options.push((label.into(), value));
self
}
pub fn options(mut self, options: Vec<(String, InputValue)>) -> Self {
self.options = options;
self
}
pub fn run(self, ctx: &mut dyn Context) -> Result<InputValue> {
terminal::enable_raw_mode()?;
let result = self.run_inner(ctx);
let _ = terminal::disable_raw_mode();
result
}
fn run_inner(self, ctx: &mut dyn Context) -> Result<InputValue> {
if self.options.is_empty() {
return Err(Error::InvalidInput("No options provided".to_string()));
}
let mut cursor = 0;
let mut selected = vec![false; self.options.len()];
let mut first_render = true;
let stdout = &mut ctx.stdout();
self.render(stdout, cursor, &selected, first_render)?;
first_render = false;
loop {
if let Event::Key(key) = ctx.read_event()? {
match key.code {
KeyCode::Enter => {
self.clear_render(stdout)?;
let mut result = Vec::new();
for (i, (label, _)) in self.options.iter().enumerate() {
if selected[i] {
queue!(
stdout,
cursor::MoveToColumn(0),
style::Print("✓ "),
style::Print(label),
style::Print("\r\n")
)?;
}
}
stdout.flush()?;
for (i, selected) in selected.iter().enumerate() {
if *selected {
let value = Box::new(self.options[i].1.clone());
result.push(value);
}
}
return Ok(InputValue::Array(result));
}
KeyCode::Esc => {
self.clear_render(stdout)?;
return Err(Error::InterruptError);
}
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.clear_render(stdout)?;
return Err(Error::InterruptError);
}
KeyCode::Char(' ') => {
selected[cursor] = !selected[cursor];
self.render(stdout, cursor, &selected, first_render)?;
}
KeyCode::Up | KeyCode::Char('k') => {
if cursor > 0 {
cursor -= 1;
self.render(stdout, cursor, &selected, first_render)?;
}
}
KeyCode::Down | KeyCode::Char('j') => {
if cursor < self.options.len() - 1 {
cursor += 1;
self.render(stdout, cursor, &selected, first_render)?;
}
}
KeyCode::Home => {
cursor = 0;
self.render(stdout, cursor, &selected, first_render)?;
}
KeyCode::End => {
cursor = self.options.len() - 1;
self.render(stdout, cursor, &selected, first_render)?;
}
KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
for s in selected.iter_mut() {
*s = true;
}
self.render(stdout, cursor, &selected, first_render)?;
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
for s in selected.iter_mut() {
*s = false;
}
self.render(stdout, cursor, &selected, first_render)?;
}
_ => {}
}
}
}
}
fn render(
&self,
stdout: &mut impl Write,
cursor: usize,
selected: &[bool],
first_render: bool,
) -> Result<()> {
if !first_render {
for _ in 0..self.options.len() + 2 {
queue!(
stdout,
cursor::MoveUp(1),
terminal::Clear(terminal::ClearType::CurrentLine),
)?;
}
}
queue!(
stdout,
cursor::MoveToColumn(0),
style::Print(&self.prompt),
style::Print("\r\n"),
cursor::MoveToColumn(0),
style::Print("(Space to select, Enter to confirm, Ctrl+A/D to select/deselect all)"),
style::Print("\r\n")
)?;
for (i, (label, _)) in self.options.iter().enumerate() {
let marker = if i == cursor { ">" } else { " " };
let checkbox = if selected[i] { "[✓]" } else { "[ ]" };
queue!(
stdout,
cursor::MoveToColumn(0),
style::Print(marker),
style::Print(" "),
style::Print(checkbox),
style::Print(" "),
style::Print(label),
style::Print("\r\n"),
)?;
}
stdout.flush()?;
Ok(())
}
fn clear_render(&self, stdout: &mut impl Write) -> Result<()> {
for _ in 0..self.options.len() + 2 {
queue!(
stdout,
cursor::MoveUp(1),
terminal::Clear(terminal::ClearType::CurrentLine),
)?;
}
queue!(stdout, cursor::MoveToColumn(0))?;
stdout.flush()?;
Ok(())
}
}