1#![allow(clippy::type_complexity)]
2
3use crate::tui::component::Component;
4use crate::tui::focusable::{CURSOR_MARKER, Focusable};
5use crate::tui::keybindings::{
6 ACTION_EDITOR_CURSOR_LEFT, ACTION_EDITOR_CURSOR_LINE_END, ACTION_EDITOR_CURSOR_LINE_START,
7 ACTION_EDITOR_CURSOR_RIGHT, ACTION_EDITOR_CURSOR_WORD_LEFT, ACTION_EDITOR_CURSOR_WORD_RIGHT,
8 ACTION_EDITOR_DELETE_CHAR_BACKWARD, ACTION_EDITOR_DELETE_CHAR_FORWARD,
9 ACTION_EDITOR_DELETE_TO_LINE_END, ACTION_EDITOR_DELETE_TO_LINE_START,
10 ACTION_EDITOR_DELETE_WORD_BACKWARD, ACTION_EDITOR_DELETE_WORD_FORWARD, ACTION_EDITOR_UNDO,
11 ACTION_EDITOR_YANK, ACTION_EDITOR_YANK_POP, ACTION_INPUT_SUBMIT, ACTION_SELECT_CANCEL,
12 get_keybindings,
13};
14use crate::tui::keys::key_event_to_string;
15use crate::tui::kill_ring::KillRing;
16use crate::tui::undo_stack::UndoStack;
17use crate::tui::util::{slice_by_column, visible_width};
18use crate::tui::word_nav::{find_word_backward, find_word_forward};
19use crossterm::event::KeyEvent;
20use unicode_segmentation::UnicodeSegmentation;
21
22pub struct Input {
27 value: String,
28 cursor: usize,
29 prompt: String,
30 kill_ring: KillRing,
31 undo_stack: UndoStack<String>,
32 focused: bool,
33 on_submit: Option<Box<dyn FnMut(String)>>,
34 on_escape: Option<Box<dyn FnMut()>>,
35 on_change: Option<Box<dyn FnMut(&str)>>,
36
37 last_action: Option<&'static str>,
39
40 #[allow(dead_code)]
42 paste_buffer: String,
43 #[allow(dead_code)]
44 is_in_paste: bool,
45}
46
47impl Input {
48 pub fn new() -> Self {
49 Self {
50 value: String::new(),
51 cursor: 0,
52 prompt: "> ".to_string(),
53 kill_ring: KillRing::new(),
54 undo_stack: UndoStack::new(),
55 focused: false,
56 on_submit: None,
57 on_escape: None,
58 on_change: None,
59 last_action: None,
60 paste_buffer: String::new(),
61 is_in_paste: false,
62 }
63 }
64
65 pub fn with_prompt(mut self, prompt: impl Into<String>) -> Self {
66 self.prompt = prompt.into();
67 self
68 }
69
70 pub fn get_value(&self) -> &str {
71 &self.value
72 }
73
74 pub fn set_value(&mut self, value: &str) {
75 self.last_action = None;
76 self.save_undo();
77 self.value = value.to_string();
78 self.cursor = self.value.len();
79 if let Some(ref mut cb) = self.on_change {
80 cb(&self.value);
81 }
82 }
83
84 pub fn set_on_submit(&mut self, cb: Box<dyn FnMut(String)>) {
85 self.on_submit = Some(cb);
86 }
87
88 pub fn set_on_escape(&mut self, cb: Box<dyn FnMut()>) {
89 self.on_escape = Some(cb);
90 }
91
92 pub fn set_on_change(&mut self, cb: Box<dyn FnMut(&str)>) {
93 self.on_change = Some(cb);
94 }
95
96 fn save_undo(&mut self) {
97 self.undo_stack.push(&self.value);
98 }
99
100 fn maybe_push_undo(&mut self, char: &str) {
103 use crate::tui::util::is_whitespace_char;
104 if is_whitespace_char(char) || self.last_action != Some("type-word") {
107 self.save_undo();
108 }
109 self.last_action = Some("type-word");
110 }
111
112 fn insert_text(&mut self, text: &str) {
115 self.maybe_push_undo(text);
116 self.value.insert_str(self.cursor, text);
117 self.cursor += text.len();
118 }
119
120 #[allow(dead_code)]
123 fn handle_paste(&mut self, pasted_text: &str) {
124 self.last_action = None;
125 self.save_undo();
126
127 let clean = pasted_text.replace(['\r', '\n'], "").replace('\t', " ");
128
129 self.value = format!(
130 "{}{}{}",
131 &self.value[..self.cursor],
132 clean,
133 &self.value[self.cursor..]
134 );
135 self.cursor += clean.len();
136 }
137
138 fn delete_before_cursor(&mut self) {
141 if self.cursor == 0 {
142 return;
143 }
144 self.last_action = None;
145 self.save_undo();
146 let graphemes: Vec<(usize, &str)> = self.value.grapheme_indices(true).collect();
147 for &(idx, g) in graphemes.iter().rev() {
148 if idx < self.cursor {
149 let end = idx + g.len();
150 if end <= self.cursor {
151 self.value.drain(idx..end);
152 self.cursor = idx;
153 break;
154 }
155 }
156 }
157 }
158
159 fn delete_after_cursor(&mut self) {
160 if self.cursor >= self.value.len() {
161 return;
162 }
163 self.last_action = None;
164 self.save_undo();
165 let graphemes: Vec<(usize, &str)> = self.value.grapheme_indices(true).collect();
166 for &(idx, g) in &graphemes {
167 if idx >= self.cursor {
168 self.value.drain(idx..idx + g.len());
169 break;
170 }
171 }
172 }
173
174 fn move_cursor_left(&mut self) {
177 self.last_action = None;
178 if self.cursor == 0 {
179 return;
180 }
181 let graphemes: Vec<(usize, &str)> = self.value.grapheme_indices(true).collect();
182 for &(idx, g) in graphemes.iter().rev() {
183 if idx < self.cursor {
184 let end = idx + g.len();
185 if end <= self.cursor {
186 self.cursor = idx;
187 break;
188 }
189 }
190 }
191 }
192
193 fn move_cursor_right(&mut self) {
194 self.last_action = None;
195 if self.cursor >= self.value.len() {
196 return;
197 }
198 if let Some((idx, g)) = self.value[self.cursor..].grapheme_indices(true).next() {
199 self.cursor += idx + g.len();
200 }
201 }
202
203 fn move_to_start(&mut self) {
204 self.last_action = None;
205 self.cursor = 0;
206 }
207
208 fn move_to_end(&mut self) {
209 self.last_action = None;
210 self.cursor = self.value.len();
211 }
212
213 fn kill_word_backward(&mut self) {
216 let new_cursor = find_word_backward(&self.value, self.cursor);
217 if new_cursor < self.cursor {
218 self.save_undo();
219 let killed = self.value[new_cursor..self.cursor].to_string();
220 let accumulate = self.last_action == Some("kill");
221 self.kill_ring.push(&killed, true, accumulate);
222 self.value.drain(new_cursor..self.cursor);
223 self.cursor = new_cursor;
224 self.last_action = Some("kill");
225 }
226 }
227
228 fn kill_word_forward(&mut self) {
229 let new_cursor = find_word_forward(&self.value, self.cursor);
230 if new_cursor > self.cursor {
231 self.save_undo();
232 let killed = self.value[self.cursor..new_cursor].to_string();
233 let accumulate = self.last_action == Some("kill");
234 self.kill_ring.push(&killed, false, accumulate);
235 self.value.drain(self.cursor..new_cursor);
236 self.last_action = Some("kill");
237 }
238 }
239
240 fn kill_to_start(&mut self) {
241 if self.cursor > 0 {
242 self.save_undo();
243 let killed = self.value[..self.cursor].to_string();
244 let accumulate = self.last_action == Some("kill");
245 self.kill_ring.push(&killed, true, accumulate);
246 self.value.drain(..self.cursor);
247 self.cursor = 0;
248 self.last_action = Some("kill");
249 }
250 }
251
252 fn kill_to_end(&mut self) {
253 if self.cursor < self.value.len() {
254 self.save_undo();
255 let killed = self.value[self.cursor..].to_string();
256 let accumulate = self.last_action == Some("kill");
257 self.kill_ring.push(&killed, false, accumulate);
258 self.value.truncate(self.cursor);
259 self.last_action = Some("kill");
260 }
261 }
262
263 fn yank(&mut self) {
266 let text = self.kill_ring.peek().map(|s| s.to_string());
267 if let Some(text) = text {
268 self.save_undo();
269 self.cursor += text.len();
270 self.value.insert_str(self.cursor - text.len(), &text);
271 }
272 self.last_action = Some("yank");
273 }
274
275 fn yank_pop(&mut self) {
276 if self.kill_ring.len() <= 1 {
279 return;
280 }
281 let prev = self.kill_ring.peek().map(|s| s.to_string());
282 if let Some(ref prev_text) = prev {
283 self.save_undo();
284 if self.cursor >= prev_text.len() {
285 let before = self.value[..self.cursor - prev_text.len()].to_string();
286 let after = self.value[self.cursor..].to_string();
287 self.value = format!("{}{}", before, after);
288 self.cursor -= prev_text.len();
289 }
290 }
291 self.kill_ring.rotate();
292 let text = self.kill_ring.peek().map(|s| s.to_string());
293 if let Some(ref new_text) = text {
294 self.value.insert_str(self.cursor, new_text);
295 self.cursor += new_text.len();
296 }
297 }
298
299 fn undo(&mut self) {
300 if let Some(prev) = self.undo_stack.pop() {
301 self.value = prev;
302 self.cursor = self.value.len().min(self.cursor);
303 self.last_action = None;
304 }
305 }
306}
307
308impl Component for Input {
309 fn render(&self, width: usize) -> Vec<String> {
310 let prompt_width = visible_width(&self.prompt);
311 let avail = width.saturating_sub(prompt_width);
312
313 if avail == 0 {
314 return vec![self.prompt.clone()];
315 }
316
317 let total_width = visible_width(&self.value);
318 let cursor_text_width = visible_width(&self.value[..self.cursor]);
319
320 let scroll = if total_width < avail {
322 0
323 } else if self.cursor == self.value.len() {
324 total_width.saturating_sub(avail).saturating_sub(1)
326 } else {
327 let half = avail / 2;
329 if cursor_text_width < half {
330 0
331 } else if cursor_text_width > total_width.saturating_sub(half) {
332 total_width.saturating_sub(avail)
333 } else {
334 cursor_text_width.saturating_sub(half)
335 }
336 };
337
338 let visible = slice_by_column(&self.value, scroll, avail);
340 let vis_width = visible_width(&visible);
341 let cursor_visible_pos = cursor_text_width.saturating_sub(scroll);
342
343 let mut line = self.prompt.clone();
345
346 if self.focused && cursor_visible_pos < vis_width {
347 let before = slice_by_column(&visible, 0, cursor_visible_pos);
348 let at_cursor = slice_by_column(&visible, cursor_visible_pos, 1);
349 let after = slice_by_column(&visible, cursor_visible_pos + 1, avail);
350
351 line.push_str(CURSOR_MARKER);
352 line.push_str(&before);
353 line.push_str("\x1b[7m");
354 if at_cursor.is_empty() {
355 line.push(' ');
356 } else {
357 line.push_str(&at_cursor);
358 }
359 line.push_str("\x1b[27m");
360 line.push_str(&after);
361 } else if self.focused && cursor_visible_pos >= vis_width && vis_width < avail {
362 line.push_str(CURSOR_MARKER);
363 line.push_str(&visible);
364 line.push_str("\x1b[7m \x1b[27m");
365 } else {
366 line.push_str(&visible);
367 if self.focused {
368 line.push_str(CURSOR_MARKER);
369 }
370 }
371
372 let line_width = visible_width(&line);
374 if line_width < width {
375 line.push_str(&" ".repeat(width - line_width));
376 }
377
378 vec![line]
379 }
380
381 fn handle_input(&mut self, key: &KeyEvent) -> bool {
382 let kb = get_keybindings();
383
384 if crate::tui::keys::is_printable(key)
386 && let Some(s) = key_event_to_string(key)
387 {
388 self.insert_text(&s);
389 if let Some(ref mut cb) = self.on_change {
390 cb(&self.value);
391 }
392 return true;
393 }
394
395 if kb.matches(key, ACTION_INPUT_SUBMIT) {
396 if let Some(ref mut cb) = self.on_submit {
397 let value = std::mem::take(&mut self.value);
398 self.cursor = 0;
399 self.last_action = None;
400 cb(value);
401 }
402 return true;
403 }
404
405 if kb.matches(key, ACTION_SELECT_CANCEL) {
406 if let Some(ref mut cb) = self.on_escape {
407 cb();
408 }
409 return true;
410 }
411
412 if kb.matches(key, ACTION_EDITOR_DELETE_CHAR_BACKWARD) {
413 self.delete_before_cursor();
414 if let Some(ref mut cb) = self.on_change {
415 cb(&self.value);
416 }
417 return true;
418 }
419
420 if kb.matches(key, ACTION_EDITOR_DELETE_CHAR_FORWARD) {
421 self.delete_after_cursor();
422 if let Some(ref mut cb) = self.on_change {
423 cb(&self.value);
424 }
425 return true;
426 }
427
428 if kb.matches(key, ACTION_EDITOR_CURSOR_LEFT) {
429 self.move_cursor_left();
430 return true;
431 }
432
433 if kb.matches(key, ACTION_EDITOR_CURSOR_RIGHT) {
434 self.move_cursor_right();
435 return true;
436 }
437
438 if kb.matches(key, ACTION_EDITOR_CURSOR_LINE_START) {
439 self.move_to_start();
440 return true;
441 }
442
443 if kb.matches(key, ACTION_EDITOR_CURSOR_LINE_END) {
444 self.move_to_end();
445 return true;
446 }
447
448 if kb.matches(key, ACTION_EDITOR_DELETE_WORD_BACKWARD) {
449 self.kill_word_backward();
450 if let Some(ref mut cb) = self.on_change {
451 cb(&self.value);
452 }
453 return true;
454 }
455
456 if kb.matches(key, ACTION_EDITOR_DELETE_TO_LINE_START) {
457 self.kill_to_start();
458 if let Some(ref mut cb) = self.on_change {
459 cb(&self.value);
460 }
461 return true;
462 }
463
464 if kb.matches(key, ACTION_EDITOR_DELETE_TO_LINE_END) {
465 self.kill_to_end();
466 if let Some(ref mut cb) = self.on_change {
467 cb(&self.value);
468 }
469 return true;
470 }
471
472 if kb.matches(key, ACTION_EDITOR_YANK) {
473 self.yank();
474 if let Some(ref mut cb) = self.on_change {
475 cb(&self.value);
476 }
477 return true;
478 }
479
480 if kb.matches(key, ACTION_EDITOR_YANK_POP) {
481 self.yank_pop();
482 if let Some(ref mut cb) = self.on_change {
483 cb(&self.value);
484 }
485 return true;
486 }
487
488 if kb.matches(key, ACTION_EDITOR_UNDO) {
489 self.undo();
490 if let Some(ref mut cb) = self.on_change {
491 cb(&self.value);
492 }
493 return true;
494 }
495
496 if kb.matches(key, ACTION_EDITOR_DELETE_WORD_FORWARD) {
497 self.kill_word_forward();
498 if let Some(ref mut cb) = self.on_change {
499 cb(&self.value);
500 }
501 return true;
502 }
503
504 if kb.matches(key, ACTION_EDITOR_CURSOR_WORD_LEFT) {
505 self.cursor = find_word_backward(&self.value, self.cursor);
506 return true;
507 }
508
509 if kb.matches(key, ACTION_EDITOR_CURSOR_WORD_RIGHT) {
510 self.cursor = find_word_forward(&self.value, self.cursor);
511 return true;
512 }
513
514 false
515 }
516
517 fn handle_paste(&mut self, text: &str) {
518 self.handle_paste(text);
519 }
520
521 fn is_focusable(&self) -> bool {
522 true
523 }
524}
525
526impl Focusable for Input {
527 fn set_focused(&mut self, focused: bool) {
528 self.focused = focused;
529 }
530
531 fn focused(&self) -> bool {
532 self.focused
533 }
534}
535
536impl Default for Input {
537 fn default() -> Self {
538 Self::new()
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn test_new_input_is_empty() {
548 let input = Input::new();
549 assert_eq!(input.get_value(), "");
550 }
551
552 #[test]
553 fn test_insert_text() {
554 let mut input = Input::new();
555 input.insert_text("hello");
556 assert_eq!(input.get_value(), "hello");
557 assert_eq!(input.cursor, 5);
558 }
559
560 #[test]
561 fn test_backspace() {
562 let mut input = Input::new();
563 input.insert_text("hello");
564 input.delete_before_cursor();
565 assert_eq!(input.get_value(), "hell");
566 assert_eq!(input.cursor, 4);
567 }
568
569 #[test]
570 fn test_move_cursor() {
571 let mut input = Input::new();
572 input.insert_text("hello");
573 input.move_cursor_left();
574 assert_eq!(input.cursor, 4);
575 input.move_cursor_right();
576 assert_eq!(input.cursor, 5);
577 }
578
579 #[test]
580 fn test_set_value() {
581 let mut input = Input::new();
582 input.set_value("test");
583 assert_eq!(input.get_value(), "test");
584 assert_eq!(input.cursor, 4);
585 }
586
587 #[test]
588 fn test_kill_to_end() {
589 let mut input = Input::new();
590 input.insert_text("hello world");
591 for _ in 0..6 {
592 input.move_cursor_left();
593 }
594 input.kill_to_end();
595 assert_eq!(input.get_value(), "hello");
596 }
597
598 #[test]
599 fn test_undo() {
600 let mut input = Input::new();
601 input.insert_text("hello");
602 input.undo();
603 assert_eq!(input.get_value(), "");
604 }
605
606 #[test]
607 fn test_render_basic() {
608 let mut input = Input::new();
609 input.set_value("test");
610 let lines = input.render(20);
611 assert_eq!(lines.len(), 1);
612 assert!(lines[0].contains("test"));
613 }
614
615 #[test]
616 fn test_undo_coalescing() {
617 let mut input = Input::new();
618 input.insert_text("h");
619 input.insert_text("e");
620 input.insert_text(" ");
621 input.insert_text("w");
622 assert_eq!(input.get_value(), "he w");
623 input.undo();
625 assert_eq!(input.get_value(), "he");
626 input.undo();
628 assert_eq!(input.get_value(), "");
629 }
630
631 #[test]
632 fn test_paste_handling() {
633 let mut input = Input::new();
634 input.handle_paste("hello\nworld");
635 assert_eq!(input.get_value(), "helloworld");
637 assert_eq!(input.cursor, 10);
638 }
639}