xacli_components/components/
input.rs1use 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!(); result
42 }
43
44 #[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 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); 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()); events.push(EventBuilder::right()); events.extend(type_string("e")); events.push(EventBuilder::end()); 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()); events.push(EventBuilder::left());
208 events.push(EventBuilder::delete()); 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}