xacli_components/components/
input.rs

1use std::io::{stdout, Write};
2
3use crossterm::{
4    cursor,
5    event::{Event, KeyCode, KeyModifiers},
6    queue,
7    style::Print,
8    terminal::{self, ClearType},
9};
10
11use super::read_event;
12use crate::{ComponentError, Result};
13
14pub struct Input {
15    prompt: String,
16    default: Option<String>,
17}
18
19impl Input {
20    pub fn new(prompt: impl Into<String>) -> Self {
21        Self {
22            prompt: prompt.into(),
23            default: None,
24        }
25    }
26
27    pub fn default(mut self, default: impl Into<String>) -> Self {
28        self.default = Some(default.into());
29        self
30    }
31
32    pub fn run(self) -> Result<String> {
33        let mut stdout = stdout();
34        terminal::enable_raw_mode()?;
35
36        let result = self.run_inner(&mut stdout);
37
38        terminal::disable_raw_mode()?;
39        println!(); // 换行
40
41        result
42    }
43
44    /// Run with custom output writer (for testing)
45    #[cfg(test)]
46    pub fn run_with_output(self, output: &mut impl Write) -> Result<String> {
47        self.run_inner(output)
48    }
49
50    fn run_inner(&self, stdout: &mut impl Write) -> Result<String> {
51        let mut input = self.default.clone().unwrap_or_default();
52        let mut cursor_pos = input.len();
53
54        // 显示提示
55        self.render(stdout, &input, cursor_pos)?;
56
57        loop {
58            if let Event::Key(key) = read_event()? {
59                match key.code {
60                    KeyCode::Enter => {
61                        return Ok(input);
62                    }
63                    KeyCode::Esc => {
64                        return Err(ComponentError::Interrupted);
65                    }
66                    KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
67                        return Err(ComponentError::Interrupted);
68                    }
69                    KeyCode::Char(c) => {
70                        input.insert(cursor_pos, c);
71                        cursor_pos += 1;
72                    }
73                    KeyCode::Backspace => {
74                        if cursor_pos > 0 {
75                            cursor_pos -= 1;
76                            input.remove(cursor_pos);
77                        }
78                    }
79                    KeyCode::Delete => {
80                        if cursor_pos < input.len() {
81                            input.remove(cursor_pos);
82                        }
83                    }
84                    KeyCode::Left => {
85                        cursor_pos = cursor_pos.saturating_sub(1);
86                    }
87                    KeyCode::Right => {
88                        if cursor_pos < input.len() {
89                            cursor_pos += 1;
90                        }
91                    }
92                    KeyCode::Home => {
93                        cursor_pos = 0;
94                    }
95                    KeyCode::End => {
96                        cursor_pos = input.len();
97                    }
98                    _ => {}
99                }
100
101                self.render(stdout, &input, cursor_pos)?;
102            }
103        }
104    }
105
106    fn render(&self, stdout: &mut impl Write, input: &str, cursor_pos: usize) -> Result<()> {
107        queue!(
108            stdout,
109            cursor::MoveToColumn(0),
110            terminal::Clear(ClearType::CurrentLine),
111            Print(&self.prompt),
112            Print(" "),
113            Print(input),
114        )?;
115
116        let col = (self.prompt.len() + 1 + cursor_pos) as u16;
117        queue!(stdout, cursor::MoveToColumn(col))?;
118
119        stdout.flush()?;
120        Ok(())
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use std::sync::mpsc;
127
128    use xacli_testing::{backspaces, type_string, EventBuilder, OutputCapture};
129
130    use super::*;
131    use crate::components::{clear_test_event_source, set_test_event_source};
132
133    fn setup_events(events: Vec<Event>) -> mpsc::Sender<Event> {
134        let (tx, rx) = mpsc::channel();
135        set_test_event_source(rx);
136        for event in events {
137            tx.send(event).unwrap();
138        }
139        tx
140    }
141
142    #[test]
143    fn test_input_basic() {
144        let mut events = type_string("Hello");
145        events.push(EventBuilder::enter());
146        let _tx = setup_events(events);
147
148        let mut output = OutputCapture::new();
149        let result = Input::new("Name:").run_with_output(&mut output).unwrap();
150
151        assert_eq!(result, "Hello");
152        clear_test_event_source();
153    }
154
155    #[test]
156    fn test_input_with_default() {
157        let _tx = setup_events(vec![EventBuilder::enter()]);
158
159        let mut output = OutputCapture::new();
160        let result = Input::new("Name:")
161            .default("Alice")
162            .run_with_output(&mut output)
163            .unwrap();
164
165        assert_eq!(result, "Alice");
166        clear_test_event_source();
167    }
168
169    #[test]
170    fn test_input_replace_default() {
171        let mut events = backspaces(5); // Delete "Alice"
172        events.extend(type_string("Bob"));
173        events.push(EventBuilder::enter());
174        let _tx = setup_events(events);
175
176        let mut output = OutputCapture::new();
177        let result = Input::new("Name:")
178            .default("Alice")
179            .run_with_output(&mut output)
180            .unwrap();
181
182        assert_eq!(result, "Bob");
183        clear_test_event_source();
184    }
185
186    #[test]
187    fn test_input_cursor_movement() {
188        let mut events = type_string("Hllo");
189        events.push(EventBuilder::home()); // Go to start
190        events.push(EventBuilder::right()); // Move to position 1
191        events.extend(type_string("e")); // Insert 'e' -> "Hello"
192        events.push(EventBuilder::end()); // Go to end
193        events.push(EventBuilder::enter());
194        let _tx = setup_events(events);
195
196        let mut output = OutputCapture::new();
197        let result = Input::new("Text:").run_with_output(&mut output).unwrap();
198
199        assert_eq!(result, "Hello");
200        clear_test_event_source();
201    }
202
203    #[test]
204    fn test_input_delete() {
205        let mut events = type_string("Helllo");
206        events.push(EventBuilder::left()); // Move before last 'l'
207        events.push(EventBuilder::left());
208        events.push(EventBuilder::delete()); // Delete extra 'l'
209        events.push(EventBuilder::enter());
210        let _tx = setup_events(events);
211
212        let mut output = OutputCapture::new();
213        let result = Input::new("Text:").run_with_output(&mut output).unwrap();
214
215        assert_eq!(result, "Hello");
216        clear_test_event_source();
217    }
218
219    #[test]
220    fn test_input_escape_interrupts() {
221        let _tx = setup_events(vec![EventBuilder::escape()]);
222
223        let mut output = OutputCapture::new();
224        let result = Input::new("Name:").run_with_output(&mut output);
225
226        assert!(matches!(result, Err(ComponentError::Interrupted)));
227        clear_test_event_source();
228    }
229
230    #[test]
231    fn test_input_ctrl_c_interrupts() {
232        let _tx = setup_events(vec![EventBuilder::ctrl('c')]);
233
234        let mut output = OutputCapture::new();
235        let result = Input::new("Name:").run_with_output(&mut output);
236
237        assert!(matches!(result, Err(ComponentError::Interrupted)));
238        clear_test_event_source();
239    }
240}