xacli-components 0.2.1

Interactive components for XaCLI
Documentation
use std::io::Write;

use crossterm::{
    cursor,
    event::{Event, KeyCode, KeyModifiers},
    queue,
    style::Print,
    terminal::{self, ClearType},
};
use xacli_core::{Context, Error, InputValue, Result};

pub struct Select {
    prompt: String,
    options: Vec<(String, InputValue)>,
}

impl Select {
    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
    }

    /// Run with custom terminal (for testing with MockTerminal)
    pub fn run(mut 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(&mut self, ctx: &mut dyn Context) -> Result<InputValue> {
        if self.options.is_empty() {
            return Err(Error::InvalidInput("No options provided".into()));
        }

        let mut selected = 0;
        let mut first_render = true;

        let stdout = &mut ctx.stdout();

        self.render(stdout, selected, first_render)?;
        first_render = false;

        loop {
            let e = ctx.read_event()?;
            if let Event::Key(key) = e {
                match key.code {
                    KeyCode::Enter => {
                        // 清除渲染
                        self.clear_render(stdout)?;
                        // Use \r\n to properly handle raw mode
                        queue!(
                            stdout,
                            cursor::MoveToColumn(0),
                            Print(""),
                            Print(&self.options[selected].0),
                            Print("\r\n")
                        )?;
                        stdout.flush()?;
                        return Ok(self.options.remove(selected).1);
                    }
                    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::Up | KeyCode::Char('k') => {
                        if selected > 0 {
                            selected -= 1;
                            self.render(stdout, selected, first_render)?;
                        }
                    }
                    KeyCode::Down | KeyCode::Char('j') => {
                        if selected < self.options.len() - 1 {
                            selected += 1;
                            self.render(stdout, selected, first_render)?;
                        }
                    }
                    KeyCode::Home => {
                        selected = 0;
                        self.render(stdout, selected, first_render)?;
                    }
                    KeyCode::End => {
                        selected = self.options.len() - 1;
                        self.render(stdout, selected, first_render)?;
                    }
                    _ => {}
                }
            }
        }
    }

    fn render(&self, stdout: &mut impl Write, selected: usize, first_render: bool) -> Result<()> {
        if !first_render {
            // Clear previous render
            for _ in 0..self.options.len() + 1 {
                queue!(
                    stdout,
                    cursor::MoveUp(1),
                    crossterm::terminal::Clear(ClearType::CurrentLine),
                )?;
            }
        }

        queue!(stdout, cursor::MoveToColumn(0))?;

        // Prompt
        queue!(stdout, Print(&self.prompt), Print("\r\n"))?;

        // Options
        for (i, (label, _)) in self.options.iter().enumerate() {
            let marker = if i == selected { ">" } else { " " };
            queue!(
                stdout,
                cursor::MoveToColumn(0),
                Print(marker),
                Print(" "),
                Print(label),
                Print("\r\n"),
            )?;
        }

        stdout.flush()?;
        Ok(())
    }

    fn clear_render(&self, stdout: &mut impl Write) -> Result<()> {
        for _ in 0..self.options.len() + 1 {
            queue!(
                stdout,
                cursor::MoveUp(1),
                crossterm::terminal::Clear(ClearType::CurrentLine),
            )?;
        }
        queue!(stdout, cursor::MoveToColumn(0))?;
        stdout.flush()?;
        Ok(())
    }
}