xacli-components 0.2.1

Interactive components for XaCLI
Documentation
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> {
        // Enable raw mode for keyboard input
        terminal::enable_raw_mode()?;
        let result = self.run_inner(ctx);
        // Always disable raw mode when done
        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)?;

                        // Collect selected values
                        let mut result = Vec::new();
                        for (i, (label, _)) in self.options.iter().enumerate() {
                            if selected[i] {
                                // Use \r\n to properly handle raw mode
                                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) => {
                        // Select all
                        for s in selected.iter_mut() {
                            *s = true;
                        }
                        self.render(stdout, cursor, &selected, first_render)?;
                    }
                    KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                        // Deselect all
                        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 {
            // Clear previous render
            for _ in 0..self.options.len() + 2 {
                queue!(
                    stdout,
                    cursor::MoveUp(1),
                    terminal::Clear(terminal::ClearType::CurrentLine),
                )?;
            }
        }

        // Prompt
        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")
        )?;

        // Options
        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(())
    }
}