tb_tui_common/
text_input.rs1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
9
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct TextInput {
15 buf: String,
16 cursor: usize,
17}
18
19impl TextInput {
20 pub fn new() -> Self {
22 Self::default()
23 }
24
25 pub fn with_text(text: impl Into<String>) -> Self {
28 let buf = text.into();
29 let cursor = buf.len();
30 Self { buf, cursor }
31 }
32
33 pub fn text(&self) -> &str {
35 &self.buf
36 }
37
38 pub fn trimmed(&self) -> &str {
41 self.buf.trim()
42 }
43
44 pub fn is_empty(&self) -> bool {
46 self.buf.is_empty()
47 }
48
49 pub fn clear(&mut self) {
51 self.buf.clear();
52 self.cursor = 0;
53 }
54
55 pub fn cursor_col(&self) -> usize {
59 self.buf[..self.cursor].chars().count()
60 }
61
62 pub fn insert(&mut self, c: char) {
64 self.buf.insert(self.cursor, c);
65 self.cursor += c.len_utf8();
66 }
67
68 pub fn insert_str(&mut self, s: &str) {
70 self.buf.insert_str(self.cursor, s);
71 self.cursor += s.len();
72 }
73
74 pub fn backspace(&mut self) {
76 if self.cursor == 0 {
77 return;
78 }
79 let prev = self.prev_char_len();
80 self.cursor -= prev;
81 self.buf.remove(self.cursor);
82 }
83
84 pub fn delete_forward(&mut self) {
86 if self.cursor < self.buf.len() {
87 self.buf.remove(self.cursor);
88 }
89 }
90
91 pub fn delete_word_back(&mut self) {
94 let head = &self.buf[..self.cursor];
95 let trimmed = head.trim_end_matches(' ');
96 let start = trimmed.rfind(' ').map(|i| i + 1).unwrap_or(0);
97 self.buf.replace_range(start..self.cursor, "");
98 self.cursor = start;
99 }
100
101 pub fn left(&mut self) {
103 if self.cursor > 0 {
104 self.cursor -= self.prev_char_len();
105 }
106 }
107
108 pub fn right(&mut self) {
110 if self.cursor < self.buf.len() {
111 let next = self.buf[self.cursor..]
112 .chars()
113 .next()
114 .map(char::len_utf8)
115 .unwrap_or(0);
116 self.cursor += next;
117 }
118 }
119
120 pub fn home(&mut self) {
122 self.cursor = 0;
123 }
124
125 pub fn end(&mut self) {
127 self.cursor = self.buf.len();
128 }
129
130 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
138 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
139 let alt = key.modifiers.contains(KeyModifiers::ALT);
140 let logo = key.modifiers.contains(KeyModifiers::SUPER);
141 match key.code {
142 KeyCode::Backspace => self.backspace(),
143 KeyCode::Delete => self.delete_forward(),
144 KeyCode::Left => self.left(),
145 KeyCode::Right => self.right(),
146 KeyCode::Home => self.home(),
147 KeyCode::End => self.end(),
148 KeyCode::Char('w') if ctrl => self.delete_word_back(),
149 KeyCode::Char('a') if ctrl => self.home(),
150 KeyCode::Char('e') if ctrl => self.end(),
151 KeyCode::Char(c) if !ctrl && !alt && !logo => self.insert(c),
152 _ => return false,
153 }
154 true
155 }
156
157 fn prev_char_len(&self) -> usize {
160 self.buf[..self.cursor]
161 .chars()
162 .next_back()
163 .map(char::len_utf8)
164 .unwrap_or(0)
165 }
166}
167
168impl From<&str> for TextInput {
169 fn from(s: &str) -> Self {
170 Self::with_text(s)
171 }
172}
173
174impl From<String> for TextInput {
175 fn from(s: String) -> Self {
176 Self::with_text(s)
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crossterm::event::KeyEvent;
184
185 fn key(code: KeyCode) -> KeyEvent {
186 KeyEvent::new(code, KeyModifiers::NONE)
187 }
188 fn ctrl(c: char) -> KeyEvent {
189 KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
190 }
191
192 #[test]
193 fn insert_and_backspace_at_end() {
194 let mut t = TextInput::new();
195 for c in "abc".chars() {
196 t.insert(c);
197 }
198 assert_eq!(t.text(), "abc");
199 assert_eq!(t.cursor_col(), 3);
200 t.backspace();
201 assert_eq!(t.text(), "ab");
202 assert_eq!(t.cursor_col(), 2);
203 }
204
205 #[test]
206 fn mid_string_insert_and_delete() {
207 let mut t = TextInput::with_text("ac");
208 t.left(); t.insert('b');
210 assert_eq!(t.text(), "abc");
211 assert_eq!(t.cursor_col(), 2);
212 t.home();
213 t.delete_forward(); assert_eq!(t.text(), "bc");
215 assert_eq!(t.cursor_col(), 0);
216 }
217
218 #[test]
219 fn backspace_and_left_are_noops_at_start() {
220 let mut t = TextInput::with_text("x");
221 t.home();
222 t.left();
223 assert_eq!(t.cursor_col(), 0);
224 t.backspace();
225 assert_eq!(t.text(), "x");
226 t.end();
228 t.delete_forward();
229 assert_eq!(t.text(), "x");
230 }
231
232 #[test]
233 fn home_end_and_cursor_col() {
234 let mut t = TextInput::with_text("hello");
235 assert_eq!(t.cursor_col(), 5);
236 t.home();
237 assert_eq!(t.cursor_col(), 0);
238 t.right();
239 t.right();
240 assert_eq!(t.cursor_col(), 2);
241 t.end();
242 assert_eq!(t.cursor_col(), 5);
243 }
244
245 #[test]
246 fn delete_word_back_eats_trailing_spaces_then_word() {
247 let mut t = TextInput::with_text("foo bar baz");
248 t.delete_word_back();
249 assert_eq!(t.text(), "foo bar ");
250 t.delete_word_back();
251 assert_eq!(t.text(), "foo ");
252 t.delete_word_back();
253 assert_eq!(t.text(), "");
254 assert_eq!(t.cursor_col(), 0);
255 }
256
257 #[test]
258 fn utf8_cursor_stays_on_char_boundaries() {
259 let mut t = TextInput::new();
260 t.insert('é');
261 t.insert('🦀');
262 assert_eq!(t.text(), "é🦀");
263 assert_eq!(t.cursor_col(), 2);
264 t.backspace(); assert_eq!(t.text(), "é");
266 assert_eq!(t.cursor_col(), 1);
267 t.left();
268 t.insert('x'); assert_eq!(t.text(), "xé");
270 }
271
272 #[test]
273 fn handle_key_consumes_edits_but_not_enter_or_esc() {
274 let mut t = TextInput::new();
275 assert!(t.handle_key(key(KeyCode::Char('h'))));
276 assert!(t.handle_key(key(KeyCode::Char('i'))));
277 assert_eq!(t.text(), "hi");
278 assert!(t.handle_key(ctrl('w'))); assert_eq!(t.text(), "");
280 assert!(!t.handle_key(key(KeyCode::Enter)));
282 assert!(!t.handle_key(key(KeyCode::Esc)));
283 assert!(!t.handle_key(ctrl('t')));
285 assert_eq!(t.text(), "");
286 }
287
288 #[test]
289 fn handle_key_ignores_super_modified_chars() {
290 let mut t = TextInput::new();
294 let cmd_v = KeyEvent::new(KeyCode::Char('v'), KeyModifiers::SUPER);
295 assert!(!t.handle_key(cmd_v), "Super+char should not be consumed");
296 assert_eq!(t.text(), "", "Super+char must not be inserted");
297 assert!(t.handle_key(key(KeyCode::Char('v'))));
299 assert_eq!(t.text(), "v");
300 }
301
302 #[test]
303 fn insert_str_pastes_at_cursor() {
304 let mut t = TextInput::with_text("ad");
305 t.left();
306 t.insert_str("bc");
307 assert_eq!(t.text(), "abcd");
308 assert_eq!(t.cursor_col(), 3);
309 }
310}