xacli_components/components/select/
mod.rs

1use std::io::Write;
2
3use crossterm::{
4    cursor,
5    event::{Event, KeyCode, KeyModifiers},
6    queue,
7    style::Print,
8    terminal::{self, ClearType},
9};
10use xacli_core::{Context, Error, InputValue, Result};
11
12pub struct Select {
13    prompt: String,
14    options: Vec<(String, InputValue)>,
15}
16
17impl Select {
18    pub fn new(prompt: impl Into<String>) -> Self {
19        Self {
20            prompt: prompt.into(),
21            options: Vec::new(),
22        }
23    }
24
25    pub fn option(mut self, label: impl Into<String>, value: InputValue) -> Self {
26        self.options.push((label.into(), value));
27        self
28    }
29
30    pub fn options(mut self, options: Vec<(String, InputValue)>) -> Self {
31        self.options = options;
32        self
33    }
34
35    /// Run with custom terminal (for testing with MockTerminal)
36    pub fn run(mut self, ctx: &mut dyn Context) -> Result<InputValue> {
37        // Enable raw mode for keyboard input
38        terminal::enable_raw_mode()?;
39        let result = self.run_inner(ctx);
40        // Always disable raw mode when done
41        let _ = terminal::disable_raw_mode();
42        result
43    }
44
45    fn run_inner(&mut self, ctx: &mut dyn Context) -> Result<InputValue> {
46        if self.options.is_empty() {
47            return Err(Error::InvalidInput("No options provided".into()));
48        }
49
50        let mut selected = 0;
51        let mut first_render = true;
52
53        let stdout = &mut ctx.stdout();
54
55        self.render(stdout, selected, first_render)?;
56        first_render = false;
57
58        loop {
59            let e = ctx.read_event()?;
60            if let Event::Key(key) = e {
61                match key.code {
62                    KeyCode::Enter => {
63                        // 清除渲染
64                        self.clear_render(stdout)?;
65                        // Use \r\n to properly handle raw mode
66                        queue!(
67                            stdout,
68                            cursor::MoveToColumn(0),
69                            Print("✓ "),
70                            Print(&self.options[selected].0),
71                            Print("\r\n")
72                        )?;
73                        stdout.flush()?;
74                        return Ok(self.options.remove(selected).1);
75                    }
76                    KeyCode::Esc => {
77                        self.clear_render(stdout)?;
78                        return Err(Error::InterruptError);
79                    }
80                    KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
81                        self.clear_render(stdout)?;
82                        return Err(Error::InterruptError);
83                    }
84                    KeyCode::Up | KeyCode::Char('k') => {
85                        if selected > 0 {
86                            selected -= 1;
87                            self.render(stdout, selected, first_render)?;
88                        }
89                    }
90                    KeyCode::Down | KeyCode::Char('j') => {
91                        if selected < self.options.len() - 1 {
92                            selected += 1;
93                            self.render(stdout, selected, first_render)?;
94                        }
95                    }
96                    KeyCode::Home => {
97                        selected = 0;
98                        self.render(stdout, selected, first_render)?;
99                    }
100                    KeyCode::End => {
101                        selected = self.options.len() - 1;
102                        self.render(stdout, selected, first_render)?;
103                    }
104                    _ => {}
105                }
106            }
107        }
108    }
109
110    fn render(&self, stdout: &mut impl Write, selected: usize, first_render: bool) -> Result<()> {
111        if !first_render {
112            // Clear previous render
113            for _ in 0..self.options.len() + 1 {
114                queue!(
115                    stdout,
116                    cursor::MoveUp(1),
117                    crossterm::terminal::Clear(ClearType::CurrentLine),
118                )?;
119            }
120        }
121
122        queue!(stdout, cursor::MoveToColumn(0))?;
123
124        // Prompt
125        queue!(stdout, Print(&self.prompt), Print("\r\n"))?;
126
127        // Options
128        for (i, (label, _)) in self.options.iter().enumerate() {
129            let marker = if i == selected { ">" } else { " " };
130            queue!(
131                stdout,
132                cursor::MoveToColumn(0),
133                Print(marker),
134                Print(" "),
135                Print(label),
136                Print("\r\n"),
137            )?;
138        }
139
140        stdout.flush()?;
141        Ok(())
142    }
143
144    fn clear_render(&self, stdout: &mut impl Write) -> Result<()> {
145        for _ in 0..self.options.len() + 1 {
146            queue!(
147                stdout,
148                cursor::MoveUp(1),
149                crossterm::terminal::Clear(ClearType::CurrentLine),
150            )?;
151        }
152        queue!(stdout, cursor::MoveToColumn(0))?;
153        stdout.flush()?;
154        Ok(())
155    }
156}