rustubble/
input.rs

1use crossterm::{
2    cursor::MoveTo,
3    event::{read, Event, KeyCode, KeyEvent, KeyModifiers},
4    execute,
5    style::{Color, Print, SetForegroundColor},
6    terminal::{Clear, ClearType},
7};
8
9use crate::helper::Helper;
10
11pub struct TextInput {
12    text: String,
13    cursor_position: usize,
14    placeholder: Option<String>,
15    padding: usize,
16    label: String,
17    helper: Option<Helper>,
18    prefix: String,
19}
20
21impl TextInput {
22    pub fn new(
23        placeholder: Option<&str>,
24        padding: usize,
25        initial_text: &str,
26        label: &str,
27        helper_text: Option<&str>,
28        prefix: &str,
29    ) -> Self {
30        TextInput {
31            text: initial_text.to_string(),
32            cursor_position: initial_text.len(),
33            placeholder: placeholder.map(String::from),
34            padding,
35            label: label.to_string(),
36            helper: helper_text.map(|text| Helper::new(text)), // Initialize helper if provided
37            prefix: prefix.to_string(),
38        }
39    }
40
41    pub fn insert_char(&mut self, c: char) {
42        if self.text == self.placeholder.as_ref().map_or("", String::as_str) || self.text.is_empty()
43        {
44            self.text.clear(); // Clear the initial or placeholder text
45            self.cursor_position = 0; // Reset the cursor position
46        }
47        self.text.insert(self.cursor_position, c);
48        self.cursor_position += 1;
49    }
50
51    pub fn delete_char(&mut self) {
52        if self.cursor_position > 0 {
53            self.text.remove(self.cursor_position - 1);
54            self.cursor_position -= 1;
55        }
56    }
57
58    pub fn move_cursor_left(&mut self) {
59        if self.cursor_position > 0 {
60            self.cursor_position -= 1;
61        }
62    }
63
64    pub fn move_cursor_right(&mut self) {
65        if self.cursor_position < self.text.len() {
66            self.cursor_position += 1;
67        }
68    }
69
70    pub fn render(&self, x: u16, y: u16) {
71        // Move to the position and clear the line for the label
72        execute!(
73            std::io::stdout(),
74            MoveTo(x + self.padding as u16, y),
75            Clear(ClearType::CurrentLine),
76            Print(&self.label),
77        )
78        .unwrap();
79
80        execute!(
81            std::io::stdout(),
82            MoveTo(x, y + 2),
83            Clear(ClearType::CurrentLine)
84        )
85        .unwrap();
86        execute!(
87            std::io::stdout(),
88            MoveTo(x, y + 2),
89            Clear(ClearType::CurrentLine),
90            SetForegroundColor(Color::White),
91            Print(" ".repeat(self.padding)), // Left padding
92            Print(format!(
93                "{} ",
94                if self.prefix.is_empty() {
95                    ""
96                } else {
97                    self.prefix.as_str()
98                }
99            )), // Render the prefix
100            SetForegroundColor(Color::Grey),
101            Print(if self.text.is_empty() {
102                self.placeholder.as_deref().unwrap_or("")
103            } else {
104                &self.text
105            })
106        )
107        .unwrap();
108
109        if let Some(ref helper) = self.helper {
110            helper.render(x + self.padding as u16, y + 5);
111            // Render helper text below the input field
112        }
113
114        // Reset cursor position and color
115        execute!(
116            std::io::stdout(),
117            MoveTo(
118                x + self.padding as u16 + self.prefix.len() as u16 + self.cursor_position as u16,
119                y + 2
120            ),
121            SetForegroundColor(Color::Reset)
122        )
123        .unwrap();
124        // Reset cursor position to after the text input
125        execute!(
126            std::io::stdout(),
127            MoveTo(
128                x + self.padding as u16
129                    + self.prefix.len() as u16
130                    + self.cursor_position as u16
131                    + 1,
132                y + 2
133            )
134        )
135        .unwrap();
136        // Reset the color to default
137        execute!(std::io::stdout(), SetForegroundColor(Color::Reset)).unwrap();
138    }
139}
140
141pub fn handle_input(input: &mut TextInput, x: u16, y: u16) -> Option<String> {
142    input.render(x, y);
143    loop {
144        match read().unwrap() {
145            Event::Key(KeyEvent {
146                code: KeyCode::Char(c),
147                modifiers,
148                ..
149            }) => {
150                if modifiers.contains(KeyModifiers::CONTROL) && c == 'c' {
151                    return None;
152                }
153
154                input.insert_char(c);
155                input.render(x, y);
156            }
157            Event::Key(KeyEvent {
158                code: KeyCode::Enter,
159                ..
160            }) => {
161                // Check if text is not just the placeholder
162                if !input.text.is_empty()
163                    && input.text != input.placeholder.as_ref().map_or("", String::as_str)
164                {
165                    return Some(input.text.clone());
166                }
167            }
168            Event::Key(KeyEvent {
169                code: KeyCode::Backspace,
170                ..
171            }) => {
172                input.delete_char();
173                input.render(x, y);
174            }
175            Event::Key(KeyEvent {
176                code: KeyCode::Left,
177                ..
178            }) => {
179                input.move_cursor_left();
180                input.render(x, y);
181            }
182            Event::Key(KeyEvent {
183                code: KeyCode::Right,
184                ..
185            }) => {
186                input.move_cursor_right();
187                input.render(x, y);
188            }
189            Event::Key(KeyEvent {
190                code: KeyCode::Esc, ..
191            }) => return None,
192            _ => {}
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*; // Import everything from the outer module
200
201    #[test]
202    fn test_insert_char() {
203        let mut text_input = TextInput::new(None, 0, "", "Label", None, "");
204        text_input.insert_char('a');
205        assert_eq!(text_input.text, "a");
206        assert_eq!(text_input.cursor_position, 1);
207    }
208
209    #[test]
210    fn test_delete_char() {
211        let mut text_input = TextInput::new(None, 0, "a", "Label", None, "");
212        text_input.delete_char();
213        assert_eq!(text_input.text, "");
214        assert_eq!(text_input.cursor_position, 0);
215    }
216
217    #[test]
218    fn test_move_cursor_left() {
219        let mut text_input = TextInput::new(None, 0, "ab", "Label", None, "");
220        text_input.move_cursor_right(); // Move cursor to end
221        text_input.move_cursor_left();
222        assert_eq!(text_input.cursor_position, 1);
223    }
224
225    #[test]
226    fn test_move_cursor_right() {
227        let mut text_input = TextInput::new(None, 0, "abc", "Label", None, "");
228        text_input.move_cursor_right();
229        text_input.move_cursor_right();
230        text_input.move_cursor_left();
231        text_input.move_cursor_right(); // Should be at the end now
232        assert_eq!(text_input.cursor_position, 3);
233    }
234}