1use crate::cursor::{Cursor, blink_cmd};
20use crate::key::{Binding, matches};
21use crate::runeutil::Sanitizer;
22use bubbletea::{Cmd, KeyMsg, Message, Model};
23use lipgloss::{Color, Style};
24use unicode_width::UnicodeWidthChar;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum EchoMode {
29 #[default]
31 Normal,
32 Password,
34 None,
36}
37
38pub type ValidateFn = Box<dyn Fn(&str) -> Option<String> + Send + Sync>;
40
41#[derive(Debug, Clone)]
43pub struct KeyMap {
44 pub character_forward: Binding,
46 pub character_backward: Binding,
48 pub word_forward: Binding,
50 pub word_backward: Binding,
52 pub delete_word_backward: Binding,
54 pub delete_word_forward: Binding,
56 pub delete_after_cursor: Binding,
58 pub delete_before_cursor: Binding,
60 pub delete_character_backward: Binding,
62 pub delete_character_forward: Binding,
64 pub line_start: Binding,
66 pub line_end: Binding,
68 pub paste: Binding,
70 pub accept_suggestion: Binding,
72 pub next_suggestion: Binding,
74 pub prev_suggestion: Binding,
76}
77
78impl Default for KeyMap {
79 fn default() -> Self {
80 Self {
81 character_forward: Binding::new().keys(&["right", "ctrl+f"]),
82 character_backward: Binding::new().keys(&["left", "ctrl+b"]),
83 word_forward: Binding::new().keys(&["alt+right", "ctrl+right", "alt+f"]),
84 word_backward: Binding::new().keys(&["alt+left", "ctrl+left", "alt+b"]),
85 delete_word_backward: Binding::new().keys(&["alt+backspace", "ctrl+w"]),
86 delete_word_forward: Binding::new().keys(&["alt+delete", "alt+d"]),
87 delete_after_cursor: Binding::new().keys(&["ctrl+k"]),
88 delete_before_cursor: Binding::new().keys(&["ctrl+u"]),
89 delete_character_backward: Binding::new().keys(&["backspace", "ctrl+h"]),
90 delete_character_forward: Binding::new().keys(&["delete", "ctrl+d"]),
91 line_start: Binding::new().keys(&["home", "ctrl+a"]),
92 line_end: Binding::new().keys(&["end", "ctrl+e"]),
93 paste: Binding::new().keys(&["ctrl+v"]),
94 accept_suggestion: Binding::new().keys(&["tab"]),
95 next_suggestion: Binding::new().keys(&["down", "ctrl+n"]),
96 prev_suggestion: Binding::new().keys(&["up", "ctrl+p"]),
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
103pub struct PasteMsg(pub String);
104
105#[derive(Debug, Clone)]
107pub struct PasteErrMsg(pub String);
108
109pub struct TextInput {
111 pub err: Option<String>,
113 pub prompt: String,
115 pub placeholder: String,
117 pub echo_mode: EchoMode,
119 pub echo_character: char,
121 pub cursor: Cursor,
123 pub prompt_style: Style,
125 pub text_style: Style,
127 pub placeholder_style: Style,
129 pub completion_style: Style,
131 pub char_limit: usize,
133 pub width: usize,
135 pub key_map: KeyMap,
137 pub show_suggestions: bool,
139 value: Vec<char>,
141 focus: bool,
143 pos: usize,
145 offset: usize,
147 offset_right: usize,
149 validate: Option<ValidateFn>,
151 sanitizer: Sanitizer,
153 suggestions: Vec<Vec<char>>,
155 matched_suggestions: Vec<Vec<char>>,
157 current_suggestion_index: usize,
159}
160
161impl Default for TextInput {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl std::fmt::Debug for TextInput {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 f.debug_struct("TextInput")
170 .field("err", &self.err)
171 .field("prompt", &self.prompt)
172 .field("placeholder", &self.placeholder)
173 .field("echo_mode", &self.echo_mode)
174 .field("cursor", &self.cursor)
175 .field("char_limit", &self.char_limit)
176 .field("width", &self.width)
177 .field("focus", &self.focus)
178 .field("pos", &self.pos)
179 .field("value_len", &self.value.len())
180 .field("validate", &self.validate.as_ref().map(|_| "<fn>"))
181 .finish()
182 }
183}
184
185impl Clone for TextInput {
186 fn clone(&self) -> Self {
187 Self {
188 err: self.err.clone(),
189 prompt: self.prompt.clone(),
190 placeholder: self.placeholder.clone(),
191 echo_mode: self.echo_mode,
192 echo_character: self.echo_character,
193 cursor: self.cursor.clone(),
194 prompt_style: self.prompt_style.clone(),
195 text_style: self.text_style.clone(),
196 placeholder_style: self.placeholder_style.clone(),
197 completion_style: self.completion_style.clone(),
198 char_limit: self.char_limit,
199 width: self.width,
200 key_map: self.key_map.clone(),
201 show_suggestions: self.show_suggestions,
202 value: self.value.clone(),
203 focus: self.focus,
204 pos: self.pos,
205 offset: self.offset,
206 offset_right: self.offset_right,
207 validate: None, sanitizer: self.sanitizer.clone(),
209 suggestions: self.suggestions.clone(),
210 matched_suggestions: self.matched_suggestions.clone(),
211 current_suggestion_index: self.current_suggestion_index,
212 }
213 }
214}
215
216impl TextInput {
217 #[must_use]
219 pub fn new() -> Self {
220 let sanitizer = Sanitizer::new()
221 .with_tab_replacement(" ")
222 .with_newline_replacement(" ");
223
224 Self {
225 err: None,
226 prompt: "> ".to_string(),
227 placeholder: String::new(),
228 echo_mode: EchoMode::Normal,
229 echo_character: '*',
230 cursor: Cursor::new(),
231 prompt_style: Style::new(),
232 text_style: Style::new(),
233 placeholder_style: Style::new().foreground_color(Color::from("240")),
234 completion_style: Style::new().foreground_color(Color::from("240")),
235 char_limit: 0,
236 width: 0,
237 key_map: KeyMap::default(),
238 show_suggestions: false,
239 value: Vec::new(),
240 focus: false,
241 pos: 0,
242 offset: 0,
243 offset_right: 0,
244 validate: None,
245 sanitizer,
246 suggestions: Vec::new(),
247 matched_suggestions: Vec::new(),
248 current_suggestion_index: 0,
249 }
250 }
251
252 pub fn set_prompt(&mut self, prompt: impl Into<String>) {
254 self.prompt = prompt.into();
255 }
256
257 pub fn set_placeholder(&mut self, placeholder: impl Into<String>) {
259 self.placeholder = placeholder.into();
260 }
261
262 pub fn set_echo_mode(&mut self, mode: EchoMode) {
264 self.echo_mode = mode;
265 }
266
267 pub fn set_value(&mut self, s: &str) {
269 let mut runes = self.sanitizer.sanitize(&s.chars().collect::<Vec<_>>());
270 if self.char_limit > 0 && runes.len() > self.char_limit {
271 runes.truncate(self.char_limit);
272 }
273 let err = self.do_validate(&runes);
274 self.set_value_internal(runes, err);
275 }
276
277 fn set_value_internal(&mut self, runes: Vec<char>, err: Option<String>) {
278 self.err = err;
279 let empty = self.value.is_empty();
280
281 if self.char_limit > 0 && runes.len() > self.char_limit {
282 self.value = runes[..self.char_limit].to_vec();
283 } else {
284 self.value = runes;
285 }
286
287 if (self.pos == 0 && empty) || self.pos > self.value.len() {
288 self.set_cursor(self.value.len());
289 }
290 self.handle_overflow();
291 self.update_suggestions();
292 }
293
294 #[must_use]
296 pub fn value(&self) -> String {
297 self.value.iter().collect()
298 }
299
300 #[must_use]
302 pub fn position(&self) -> usize {
303 self.pos
304 }
305
306 pub fn set_cursor(&mut self, pos: usize) {
308 self.pos = pos.min(self.value.len());
309 self.handle_overflow();
310 }
311
312 pub fn cursor_start(&mut self) {
314 self.set_cursor(0);
315 }
316
317 pub fn cursor_end(&mut self) {
319 self.set_cursor(self.value.len());
320 }
321
322 #[must_use]
324 pub fn focused(&self) -> bool {
325 self.focus
326 }
327
328 pub fn focus(&mut self) -> Option<Cmd> {
330 self.focus = true;
331 self.cursor.focus()
332 }
333
334 pub fn blur(&mut self) {
336 self.focus = false;
337 self.cursor.blur();
338 }
339
340 pub fn reset(&mut self) {
342 self.value.clear();
343 self.err = self.do_validate(&self.value);
344 self.set_cursor(0);
345 self.update_suggestions();
346 }
347
348 pub fn set_suggestions(&mut self, suggestions: &[&str]) {
350 self.suggestions = suggestions.iter().map(|s| s.chars().collect()).collect();
351 self.update_suggestions();
352 }
353
354 pub fn set_validate<F>(&mut self, f: F)
356 where
357 F: Fn(&str) -> Option<String> + Send + Sync + 'static,
358 {
359 self.validate = Some(Box::new(f));
360 }
361
362 #[must_use]
364 pub fn available_suggestions(&self) -> Vec<String> {
365 self.suggestions
366 .iter()
367 .map(|s| s.iter().collect())
368 .collect()
369 }
370
371 #[must_use]
373 pub fn matched_suggestions(&self) -> Vec<String> {
374 self.matched_suggestions
375 .iter()
376 .map(|s| s.iter().collect())
377 .collect()
378 }
379
380 #[must_use]
382 pub fn current_suggestion_index(&self) -> usize {
383 self.current_suggestion_index
384 }
385
386 #[must_use]
388 pub fn current_suggestion(&self) -> String {
389 self.matched_suggestions
390 .get(self.current_suggestion_index)
391 .map(|s| s.iter().collect())
392 .unwrap_or_default()
393 }
394
395 fn do_validate(&self, v: &[char]) -> Option<String> {
396 self.validate
397 .as_ref()
398 .and_then(|f| f(&v.iter().collect::<String>()))
399 }
400
401 fn insert_runes_from_user_input(&mut self, v: &[char]) {
402 let paste = self.sanitizer.sanitize(v);
403
404 let mut available = if self.char_limit > 0 {
405 let avail = self.char_limit.saturating_sub(self.value.len());
406 if avail == 0 {
407 return;
408 }
409 avail
410 } else {
411 usize::MAX
412 };
413
414 let paste = if paste.len() > available {
415 &paste[..available]
416 } else {
417 &paste
418 };
419
420 let head = &self.value[..self.pos];
422 let tail = &self.value[self.pos..];
423
424 let mut new_value = head.to_vec();
425 for &c in paste {
426 if available == 0 {
427 break;
428 }
429 new_value.push(c);
430 self.pos += 1;
431 available = available.saturating_sub(1);
432 }
433 new_value.extend_from_slice(tail);
434
435 let err = self.do_validate(&new_value);
436 self.set_value_internal(new_value, err);
437 }
438
439 fn handle_overflow(&mut self) {
440 let total_width: usize = self.value.iter().map(|c| c.width().unwrap_or(0)).sum();
441 if self.width == 0 || total_width <= self.width {
442 self.offset = 0;
443 self.offset_right = self.value.len();
444 return;
445 }
446
447 self.offset_right = self.offset_right.min(self.value.len());
448
449 if self.pos < self.offset {
450 self.offset = self.pos;
451 let mut w = 0;
452 let mut i = 0;
453 let runes = &self.value[self.offset..];
454
455 while i < runes.len() {
456 let cw = runes[i].width().unwrap_or(0);
457 if w + cw > self.width {
458 break;
459 }
460 w += cw;
461 i += 1;
462 }
463 self.offset_right = self.offset + i;
464 } else if self.pos >= self.offset_right {
465 self.offset_right = self.pos;
466 let mut w = 0;
467 let runes = &self.value[..self.offset_right];
468 let mut start_index = self.offset_right;
469
470 while start_index > 0 {
472 let prev = start_index - 1;
473 let cw = runes[prev].width().unwrap_or(0);
474 if w + cw > self.width {
475 break;
476 }
477 w += cw;
478 start_index = prev;
479 }
480 self.offset = start_index;
481 }
482 }
483
484 fn delete_before_cursor(&mut self) {
485 self.value = self.value[self.pos..].to_vec();
486 self.err = self.do_validate(&self.value);
487 self.offset = 0;
488 self.set_cursor(0);
489 }
490
491 fn delete_after_cursor(&mut self) {
492 self.value = self.value[..self.pos].to_vec();
493 self.err = self.do_validate(&self.value);
494 self.set_cursor(self.value.len());
495 }
496
497 fn delete_word_backward(&mut self) {
498 if self.pos == 0 || self.value.is_empty() {
499 return;
500 }
501
502 if self.echo_mode != EchoMode::Normal {
503 self.delete_before_cursor();
504 return;
505 }
506
507 let old_pos = self.pos;
508 self.set_cursor(self.pos.saturating_sub(1));
509
510 while self.pos > 0 {
512 let prev = self.pos - 1;
513 if let Some(c) = self.value.get(prev) {
514 if c.is_whitespace() {
515 self.set_cursor(prev);
516 } else {
517 break;
518 }
519 } else {
520 break;
521 }
522 }
523
524 while self.pos > 0 {
526 let prev = self.pos - 1;
527 if let Some(c) = self.value.get(prev) {
528 if !c.is_whitespace() {
529 self.set_cursor(prev);
530 } else {
531 break;
532 }
533 } else {
534 break;
535 }
536 }
537
538 if old_pos > self.value.len() {
539 self.value = self.value[..self.pos].to_vec();
540 } else {
541 let mut new_value = self.value[..self.pos].to_vec();
542 new_value.extend_from_slice(&self.value[old_pos..]);
543 self.value = new_value;
544 }
545 self.err = self.do_validate(&self.value);
546 self.handle_overflow();
547 }
548
549 fn delete_word_forward(&mut self) {
550 if self.pos >= self.value.len() || self.value.is_empty() {
551 return;
552 }
553
554 if self.echo_mode != EchoMode::Normal {
555 self.delete_after_cursor();
556 return;
557 }
558
559 let old_pos = self.pos;
560 self.set_cursor(self.pos + 1);
561
562 while self.pos < self.value.len()
564 && self.value.get(self.pos).is_some_and(|c| c.is_whitespace())
565 {
566 self.set_cursor(self.pos + 1);
567 }
568
569 while self.pos < self.value.len() {
571 if !self.value.get(self.pos).is_some_and(|c| c.is_whitespace()) {
572 self.set_cursor(self.pos + 1);
573 } else {
574 break;
575 }
576 }
577
578 if self.pos > self.value.len() {
579 self.value = self.value[..old_pos].to_vec();
580 } else {
581 let mut new_value = self.value[..old_pos].to_vec();
582 new_value.extend_from_slice(&self.value[self.pos..]);
583 self.value = new_value;
584 }
585 self.err = self.do_validate(&self.value);
586 self.set_cursor(old_pos);
587 }
588
589 fn word_backward(&mut self) {
590 if self.pos == 0 || self.value.is_empty() {
591 return;
592 }
593
594 if self.echo_mode != EchoMode::Normal {
595 self.cursor_start();
596 return;
597 }
598
599 while self.pos > 0 {
601 let prev = self.pos - 1;
602 if let Some(c) = self.value.get(prev) {
603 if c.is_whitespace() {
604 self.set_cursor(prev);
605 } else {
606 break;
607 }
608 } else {
609 break;
610 }
611 }
612
613 while self.pos > 0 {
615 let prev = self.pos - 1;
616 if let Some(c) = self.value.get(prev) {
617 if !c.is_whitespace() {
618 self.set_cursor(prev);
619 } else {
620 break;
621 }
622 } else {
623 break;
624 }
625 }
626 }
627
628 fn word_forward(&mut self) {
629 if self.pos >= self.value.len() || self.value.is_empty() {
630 return;
631 }
632
633 if self.echo_mode != EchoMode::Normal {
634 self.cursor_end();
635 return;
636 }
637
638 let mut i = self.pos;
639
640 while i < self.value.len() && self.value.get(i).is_some_and(|c| c.is_whitespace()) {
642 self.set_cursor(self.pos + 1);
643 i += 1;
644 }
645
646 while i < self.value.len() {
648 if !self.value.get(i).is_some_and(|c| c.is_whitespace()) {
649 self.set_cursor(self.pos + 1);
650 i += 1;
651 } else {
652 break;
653 }
654 }
655 }
656
657 fn echo_transform(&self, v: &str) -> String {
658 match self.echo_mode {
659 EchoMode::Normal => v.to_string(),
660 EchoMode::Password => self.echo_character.to_string().repeat(v.chars().count()),
661 EchoMode::None => String::new(),
662 }
663 }
664
665 fn can_accept_suggestion(&self) -> bool {
666 !self.matched_suggestions.is_empty()
667 }
668
669 fn update_suggestions(&mut self) {
670 if !self.show_suggestions {
671 return;
672 }
673
674 if self.value.is_empty() || self.suggestions.is_empty() {
675 self.matched_suggestions.clear();
676 return;
677 }
678
679 let value_str: String = self.value.iter().collect();
680 let value_lower = value_str.to_lowercase();
681
682 let matches: Vec<Vec<char>> = self
683 .suggestions
684 .iter()
685 .filter(|s| {
686 let suggestion: String = s.iter().collect();
687 suggestion.to_lowercase().starts_with(&value_lower)
688 })
689 .cloned()
690 .collect();
691
692 if matches != self.matched_suggestions {
693 self.current_suggestion_index = 0;
694 }
695
696 self.matched_suggestions = matches;
697 }
698
699 fn next_suggestion(&mut self) {
700 if self.matched_suggestions.is_empty() {
701 return;
702 }
703 self.current_suggestion_index =
704 (self.current_suggestion_index + 1) % self.matched_suggestions.len();
705 }
706
707 fn previous_suggestion(&mut self) {
708 if self.matched_suggestions.is_empty() {
709 return;
710 }
711 if self.current_suggestion_index == 0 {
712 self.current_suggestion_index = self.matched_suggestions.len().saturating_sub(1);
713 } else {
714 self.current_suggestion_index -= 1;
715 }
716 }
717
718 pub fn update(&mut self, msg: Message) -> Option<Cmd> {
720 if !self.focus {
721 return None;
722 }
723
724 if let Some(paste) = msg.downcast_ref::<PasteMsg>() {
726 self.insert_runes_from_user_input(&paste.0.chars().collect::<Vec<_>>());
727 return None;
728 }
729
730 if let Some(paste_err) = msg.downcast_ref::<PasteErrMsg>() {
731 self.err = Some(paste_err.0.clone());
732 return None;
733 }
734
735 let old_pos = self.pos;
736
737 if let Some(key) = msg.downcast_ref::<KeyMsg>() {
738 let key_str = key.to_string();
739
740 if matches(&key_str, &[&self.key_map.accept_suggestion])
742 && self.can_accept_suggestion()
743 && let Some(suggestion) =
744 self.matched_suggestions.get(self.current_suggestion_index)
745 && self.value.len() < suggestion.len()
746 {
747 self.value
748 .extend_from_slice(&suggestion[self.value.len()..]);
749 self.cursor_end();
750 }
751
752 if matches(&key_str, &[&self.key_map.delete_word_backward]) {
753 self.delete_word_backward();
754 } else if matches(&key_str, &[&self.key_map.delete_character_backward]) {
755 self.err = None;
756 if !self.value.is_empty() && self.pos > 0 {
757 self.value.remove(self.pos - 1);
758 self.err = self.do_validate(&self.value);
759 self.set_cursor(self.pos.saturating_sub(1));
760 }
761 } else if matches(&key_str, &[&self.key_map.word_backward]) {
762 self.word_backward();
763 } else if matches(&key_str, &[&self.key_map.character_backward]) {
764 if self.pos > 0 {
765 self.set_cursor(self.pos - 1);
766 }
767 } else if matches(&key_str, &[&self.key_map.word_forward]) {
768 self.word_forward();
769 } else if matches(&key_str, &[&self.key_map.character_forward]) {
770 if self.pos < self.value.len() {
771 self.set_cursor(self.pos + 1);
772 }
773 } else if matches(&key_str, &[&self.key_map.line_start]) {
774 self.cursor_start();
775 } else if matches(&key_str, &[&self.key_map.delete_character_forward]) {
776 if !self.value.is_empty() && self.pos < self.value.len() {
777 self.value.remove(self.pos);
778 self.err = self.do_validate(&self.value);
779 }
780 } else if matches(&key_str, &[&self.key_map.line_end]) {
781 self.cursor_end();
782 } else if matches(&key_str, &[&self.key_map.delete_after_cursor]) {
783 self.delete_after_cursor();
784 } else if matches(&key_str, &[&self.key_map.delete_before_cursor]) {
785 self.delete_before_cursor();
786 } else if matches(&key_str, &[&self.key_map.delete_word_forward]) {
787 self.delete_word_forward();
788 } else if matches(&key_str, &[&self.key_map.next_suggestion]) {
789 self.next_suggestion();
790 } else if matches(&key_str, &[&self.key_map.prev_suggestion]) {
791 self.previous_suggestion();
792 } else if !matches(
793 &key_str,
794 &[&self.key_map.paste, &self.key_map.accept_suggestion],
795 ) {
796 let runes: Vec<char> = key.runes.clone();
798 if !runes.is_empty() {
799 self.insert_runes_from_user_input(&runes);
800 }
801 }
802
803 self.update_suggestions();
804 }
805
806 let mut cmds: Vec<Option<Cmd>> = Vec::new();
807
808 if let Some(cmd) = self.cursor.update(msg) {
809 cmds.push(Some(cmd));
810 }
811
812 if old_pos != self.pos && self.cursor.mode() == crate::cursor::Mode::Blink {
813 cmds.push(Some(blink_cmd()));
815 }
816
817 self.handle_overflow();
818
819 bubbletea::batch(cmds)
820 }
821
822 #[must_use]
824 pub fn view(&self) -> String {
825 if self.value.is_empty() && !self.placeholder.is_empty() {
826 return self.placeholder_view();
827 }
828
829 let value: Vec<char> = self.value[self.offset..self.offset_right].to_vec();
830 let pos = self.pos.saturating_sub(self.offset);
831
832 let before: String = value[..pos.min(value.len())].iter().collect();
833 let mut v = self
834 .text_style
835 .clone()
836 .inline()
837 .render(&self.echo_transform(&before));
838
839 if pos < value.len() {
840 let char_at_cursor: String = value[pos..pos + 1].iter().collect();
841 let char_display = self.echo_transform(&char_at_cursor);
842
843 let mut cursor = self.cursor.clone();
844 cursor.set_char(&char_display);
845 v.push_str(&cursor.view());
846
847 let after: String = value[pos + 1..].iter().collect();
848 v.push_str(
849 &self
850 .text_style
851 .clone()
852 .inline()
853 .render(&self.echo_transform(&after)),
854 );
855 v.push_str(&self.completion_view(0));
856 } else if self.focus && self.can_accept_suggestion() {
857 if let Some(suggestion) = self.matched_suggestions.get(self.current_suggestion_index) {
858 if self.value.len() < suggestion.len() && self.pos < suggestion.len() {
859 let mut cursor = self.cursor.clone();
860 cursor.text_style = self.completion_style.clone();
861 let char_display: String = suggestion[self.pos..self.pos + 1].iter().collect();
862 cursor.set_char(&self.echo_transform(&char_display));
863 v.push_str(&cursor.view());
864 v.push_str(&self.completion_view(1));
865 } else {
866 let mut cursor = self.cursor.clone();
867 cursor.set_char(" ");
868 v.push_str(&cursor.view());
869 }
870 }
871 } else {
872 let mut cursor = self.cursor.clone();
873 cursor.set_char(" ");
874 v.push_str(&cursor.view());
875 }
876
877 if self.width > 0 {
879 let val_width: usize = value.iter().map(|c| c.width().unwrap_or(0)).sum();
880 if val_width <= self.width {
881 let padding = self.width.saturating_sub(val_width);
882 v.push_str(
883 &self
884 .text_style
885 .clone()
886 .inline()
887 .render(&" ".repeat(padding)),
888 );
889 }
890 }
891
892 format!("{}{}", self.prompt_style.render(&self.prompt), v)
893 }
894
895 fn placeholder_view(&self) -> String {
896 let prompt = self.prompt_style.render(&self.prompt);
897
898 let mut cursor = self.cursor.clone();
899 cursor.text_style = self.placeholder_style.clone();
900
901 let first_char: String = self.placeholder.chars().take(1).collect();
902 let rest: String = self.placeholder.chars().skip(1).collect();
903
904 cursor.set_char(&first_char);
905 let v = cursor.view();
906
907 let styled_rest = self.placeholder_style.clone().inline().render(&rest);
908
909 format!("{}{}{}", prompt, v, styled_rest)
910 }
911
912 fn completion_view(&self, offset: usize) -> String {
913 if self.can_accept_suggestion()
914 && let Some(suggestion) = self.matched_suggestions.get(self.current_suggestion_index)
915 && self.value.len() + offset <= suggestion.len()
916 {
917 let completion: String = suggestion[self.value.len() + offset..].iter().collect();
918 return self.placeholder_style.clone().inline().render(&completion);
919 }
920 String::new()
921 }
922}
923
924impl Model for TextInput {
925 fn init(&self) -> Option<Cmd> {
929 if self.focus { Some(blink_cmd()) } else { None }
930 }
931
932 fn update(&mut self, msg: Message) -> Option<Cmd> {
939 TextInput::update(self, msg)
940 }
941
942 fn view(&self) -> String {
944 TextInput::view(self)
945 }
946}
947
948#[cfg(test)]
949mod tests {
950 use super::*;
951
952 #[test]
953 fn test_textinput_new() {
954 let input = TextInput::new();
955 assert_eq!(input.prompt, "> ");
956 assert_eq!(input.echo_character, '*');
957 assert!(!input.focused());
958 }
959
960 #[test]
961 fn test_textinput_set_value() {
962 let mut input = TextInput::new();
963 input.set_value("hello");
964 assert_eq!(input.value(), "hello");
965 }
966
967 #[test]
968 fn test_textinput_cursor_position() {
969 let mut input = TextInput::new();
970 input.set_value("hello");
971 assert_eq!(input.position(), 5);
972
973 input.set_cursor(2);
974 assert_eq!(input.position(), 2);
975
976 input.cursor_start();
977 assert_eq!(input.position(), 0);
978
979 input.cursor_end();
980 assert_eq!(input.position(), 5);
981 }
982
983 #[test]
984 fn test_textinput_focus_blur() {
985 let mut input = TextInput::new();
986 assert!(!input.focused());
987
988 input.focus();
989 assert!(input.focused());
990
991 input.blur();
992 assert!(!input.focused());
993 }
994
995 #[test]
996 fn test_textinput_reset() {
997 let mut input = TextInput::new();
998 input.set_value("hello");
999 assert!(!input.value.is_empty());
1000
1001 input.reset();
1002 assert!(input.value.is_empty());
1003 }
1004
1005 #[test]
1006 fn test_textinput_reset_clears_error_and_suggestions() {
1007 let mut input = TextInput::new();
1008 input.show_suggestions = true;
1009 input.set_suggestions(&["apple", "apricot"]);
1010 input.set_validate(|v| (!v.is_empty()).then(|| "err".to_string()));
1011
1012 input.set_value("ap");
1013 assert!(input.err.is_some());
1014 assert!(!input.matched_suggestions().is_empty());
1015
1016 input.reset();
1017 assert!(input.err.is_none());
1018 assert!(input.matched_suggestions().is_empty());
1019 }
1020
1021 #[test]
1022 fn test_textinput_char_limit() {
1023 let mut input = TextInput::new();
1024 input.char_limit = 5;
1025 input.set_value("hello world");
1026 assert_eq!(input.value(), "hello");
1027 }
1028
1029 #[test]
1030 fn test_textinput_echo_mode() {
1031 let mut input = TextInput::new();
1032 input.set_value("secret");
1033
1034 assert_eq!(input.echo_transform("secret"), "secret");
1035
1036 input.echo_mode = EchoMode::Password;
1037 assert_eq!(input.echo_transform("secret"), "******");
1038
1039 input.echo_mode = EchoMode::None;
1040 assert_eq!(input.echo_transform("secret"), "");
1041 }
1042
1043 #[test]
1044 fn test_textinput_placeholder() {
1045 let mut input = TextInput::new();
1046 input.set_placeholder("Enter text...");
1047 assert_eq!(input.placeholder, "Enter text...");
1048 }
1049
1050 #[test]
1051 fn test_textinput_suggestions() {
1052 let mut input = TextInput::new();
1053 input.show_suggestions = true;
1054 input.set_suggestions(&["apple", "apricot", "banana"]);
1055 input.set_value("ap");
1056 input.update_suggestions();
1057
1058 assert_eq!(input.matched_suggestions().len(), 2);
1059 assert!(input.matched_suggestions().contains(&"apple".to_string()));
1060 assert!(input.matched_suggestions().contains(&"apricot".to_string()));
1061 }
1062
1063 #[test]
1064 fn test_textinput_set_value_updates_suggestions() {
1065 let mut input = TextInput::new();
1066 input.show_suggestions = true;
1067 input.set_suggestions(&["apple", "banana"]);
1068
1069 input.set_value("ap");
1070
1071 assert_eq!(input.matched_suggestions().len(), 1);
1072 assert!(input.matched_suggestions().contains(&"apple".to_string()));
1073 }
1074
1075 #[test]
1076 fn test_textinput_suggestion_overflow_uses_global_position() {
1077 let mut input = TextInput::new();
1078 input.width = 5;
1079 input.show_suggestions = true;
1080 input.set_value("abcdefghij");
1081 input.set_suggestions(&["abcdefghijZ"]);
1082 input.focus();
1083 input.cursor_end();
1084
1085 let view = input.view();
1086 assert!(
1087 view.contains("Z"),
1088 "Expected suggestion character to render at cursor when scrolled"
1089 );
1090 }
1091
1092 #[test]
1093 fn test_textinput_validation() {
1094 let mut input = TextInput::new();
1095 input.set_validate(|s| {
1096 if s.contains("bad") {
1097 Some("Contains bad word".to_string())
1098 } else {
1099 None
1100 }
1101 });
1102
1103 input.set_value("good");
1104 assert!(input.err.is_none());
1105
1106 input.set_value("bad");
1107 assert!(input.err.is_some());
1108 }
1109
1110 #[test]
1111 fn test_textinput_view() {
1112 let mut input = TextInput::new();
1113 input.set_value("hello");
1114 let view = input.view();
1115 assert!(view.contains("> "));
1116 assert!(view.contains("hello"));
1117 }
1118
1119 #[test]
1120 fn test_textinput_placeholder_view() {
1121 let mut input = TextInput::new();
1122 input.set_placeholder("Type here...");
1123 let view = input.view();
1124 assert!(view.contains("> "));
1125 }
1126
1127 #[test]
1128 fn test_keymap_default() {
1129 let km = KeyMap::default();
1130 assert!(!km.character_forward.get_keys().is_empty());
1131 assert!(!km.delete_character_backward.get_keys().is_empty());
1132 }
1133
1134 #[test]
1136 fn test_model_init_unfocused() {
1137 let input = TextInput::new();
1138 let cmd = Model::init(&input);
1140 assert!(cmd.is_none());
1141 }
1142
1143 #[test]
1144 fn test_model_init_focused() {
1145 let mut input = TextInput::new();
1146 input.focus();
1147 let cmd = Model::init(&input);
1149 assert!(cmd.is_some());
1150 }
1151
1152 #[test]
1153 fn test_model_view() {
1154 let mut input = TextInput::new();
1155 input.set_value("test");
1156 let model_view = Model::view(&input);
1158 let textinput_view = TextInput::view(&input);
1159 assert_eq!(model_view, textinput_view);
1160 }
1161
1162 #[test]
1163 fn test_model_update_handles_paste_msg() {
1164 let mut input = TextInput::new();
1165 input.focus();
1166 assert_eq!(input.value(), "");
1167
1168 let paste_msg = Message::new(PasteMsg("hello world".to_string()));
1170 let _ = Model::update(&mut input, paste_msg);
1171
1172 assert_eq!(input.value(), "hello world");
1173 }
1174
1175 #[test]
1176 fn test_model_update_unfocused_ignores_input() {
1177 let mut input = TextInput::new();
1178 assert!(!input.focused());
1179 assert_eq!(input.value(), "");
1180
1181 let paste_msg = Message::new(PasteMsg("ignored".to_string()));
1183 let _ = Model::update(&mut input, paste_msg);
1184
1185 assert_eq!(input.value(), "", "Unfocused input should ignore messages");
1186 }
1187
1188 #[test]
1189 fn test_model_update_handles_key_input() {
1190 let mut input = TextInput::new();
1191 input.focus();
1192 input.set_value("hello");
1193 assert_eq!(input.position(), 5);
1194
1195 let key_msg = Message::new(KeyMsg {
1197 key_type: bubbletea::KeyType::Left,
1198 runes: vec![],
1199 alt: false,
1200 paste: false,
1201 });
1202 let _ = Model::update(&mut input, key_msg);
1203
1204 assert_eq!(input.position(), 4, "Cursor should have moved left");
1205 }
1206
1207 #[test]
1208 fn test_textinput_satisfies_model_bounds() {
1209 fn accepts_model<M: Model + Send + 'static>(_model: M) {}
1211 let input = TextInput::new();
1212 accepts_model(input);
1213 }
1214
1215 #[test]
1216 fn test_word_backward_boundary() {
1217 let mut input = TextInput::new();
1218 input.set_value("abc");
1219 input.set_cursor(1); input.word_backward();
1221 assert_eq!(input.position(), 0); }
1223
1224 #[test]
1225 fn test_delete_word_backward_boundary() {
1226 let mut input = TextInput::new();
1227 input.set_value("abc");
1228 input.set_cursor(1); input.delete_word_backward();
1230 assert_eq!(input.value(), "bc");
1231 assert_eq!(input.position(), 0);
1232 }
1233
1234 #[test]
1235 fn test_handle_overflow_wide_chars() {
1236 let mut input = TextInput::new();
1237 input.width = 3;
1238 input.set_value("aπb"); input.set_cursor(0);
1245 let view = input.view();
1250 assert!(view.contains("aπ"));
1256 assert!(!view.contains("b")); }
1258
1259 #[test]
1260 fn test_delete_word_backward_on_whitespace() {
1261 let mut input = TextInput::new();
1262 input.set_value("abc ");
1263 input.set_cursor(6); input.delete_word_backward();
1266
1267 assert_eq!(
1271 input.value(),
1272 "",
1273 "Aggressive deletion: deleted both whitespace and word"
1274 );
1275 }
1276
1277 #[test]
1282 fn test_bracketed_paste_basic() {
1283 let mut input = TextInput::new();
1284 input.focus();
1285
1286 let key_msg = Message::new(KeyMsg {
1288 key_type: bubbletea::KeyType::Runes,
1289 runes: vec!['h', 'e', 'l', 'l', 'o'],
1290 alt: false,
1291 paste: true,
1292 });
1293 let _ = Model::update(&mut input, key_msg);
1294
1295 assert_eq!(input.value(), "hello");
1296 }
1297
1298 #[test]
1299 fn test_bracketed_paste_multiline_converts_newlines() {
1300 let mut input = TextInput::new();
1301 input.focus();
1302
1303 let key_msg = Message::new(KeyMsg {
1305 key_type: bubbletea::KeyType::Runes,
1306 runes: "line1\nline2\nline3".chars().collect(),
1307 alt: false,
1308 paste: true,
1309 });
1310 let _ = Model::update(&mut input, key_msg);
1311
1312 assert_eq!(
1313 input.value(),
1314 "line1 line2 line3",
1315 "Newlines should be converted to spaces in single-line input"
1316 );
1317 }
1318
1319 #[test]
1320 fn test_bracketed_paste_crlf_converts_to_space() {
1321 let mut input = TextInput::new();
1322 input.focus();
1323
1324 let key_msg = Message::new(KeyMsg {
1326 key_type: bubbletea::KeyType::Runes,
1327 runes: "line1\r\nline2".chars().collect(),
1328 alt: false,
1329 paste: true,
1330 });
1331 let _ = Model::update(&mut input, key_msg);
1332
1333 assert_eq!(
1334 input.value(),
1335 "line1 line2",
1336 "CRLF should be converted to single space"
1337 );
1338 }
1339
1340 #[test]
1341 fn test_bracketed_paste_respects_char_limit() {
1342 let mut input = TextInput::new();
1343 input.focus();
1344 input.char_limit = 10;
1345
1346 let key_msg = Message::new(KeyMsg {
1348 key_type: bubbletea::KeyType::Runes,
1349 runes: "this is a very long paste that exceeds the limit"
1350 .chars()
1351 .collect(),
1352 alt: false,
1353 paste: true,
1354 });
1355 let _ = Model::update(&mut input, key_msg);
1356
1357 assert_eq!(
1358 input.value().len(),
1359 10,
1360 "Paste should be truncated at char_limit"
1361 );
1362 assert_eq!(input.value(), "this is a ");
1363 }
1364
1365 #[test]
1366 fn test_bracketed_paste_respects_remaining_capacity() {
1367 let mut input = TextInput::new();
1368 input.focus();
1369 input.char_limit = 15;
1370 input.set_value("hello ");
1371
1372 let key_msg = Message::new(KeyMsg {
1374 key_type: bubbletea::KeyType::Runes,
1375 runes: "world and more text".chars().collect(),
1376 alt: false,
1377 paste: true,
1378 });
1379 let _ = Model::update(&mut input, key_msg);
1380
1381 assert_eq!(input.value().len(), 15);
1382 assert_eq!(input.value(), "hello world and");
1383 }
1384
1385 #[test]
1386 fn test_bracketed_paste_at_full_capacity_ignored() {
1387 let mut input = TextInput::new();
1388 input.focus();
1389 input.char_limit = 5;
1390 input.set_value("hello");
1391
1392 let key_msg = Message::new(KeyMsg {
1394 key_type: bubbletea::KeyType::Runes,
1395 runes: "world".chars().collect(),
1396 alt: false,
1397 paste: true,
1398 });
1399 let _ = Model::update(&mut input, key_msg);
1400
1401 assert_eq!(
1402 input.value(),
1403 "hello",
1404 "Paste at full capacity should be ignored"
1405 );
1406 }
1407
1408 #[test]
1409 fn test_bracketed_paste_unfocused_ignored() {
1410 let mut input = TextInput::new();
1411 assert_eq!(input.value(), "");
1413
1414 let key_msg = Message::new(KeyMsg {
1415 key_type: bubbletea::KeyType::Runes,
1416 runes: "ignored".chars().collect(),
1417 alt: false,
1418 paste: true,
1419 });
1420 let _ = Model::update(&mut input, key_msg);
1421
1422 assert_eq!(input.value(), "", "Unfocused input should ignore paste");
1423 }
1424
1425 #[test]
1426 fn test_bracketed_paste_inserts_at_cursor() {
1427 let mut input = TextInput::new();
1428 input.focus();
1429 input.set_value("helloworld");
1430 input.set_cursor(5); let key_msg = Message::new(KeyMsg {
1433 key_type: bubbletea::KeyType::Runes,
1434 runes: " ".chars().collect(),
1435 alt: false,
1436 paste: true,
1437 });
1438 let _ = Model::update(&mut input, key_msg);
1439
1440 assert_eq!(input.value(), "hello world");
1441 assert_eq!(input.position(), 6, "Cursor should be after pasted content");
1442 }
1443
1444 #[test]
1445 fn test_bracketed_paste_strips_control_chars() {
1446 let mut input = TextInput::new();
1447 input.focus();
1448
1449 let key_msg = Message::new(KeyMsg {
1451 key_type: bubbletea::KeyType::Runes,
1452 runes: "hello\x01\x02world".chars().collect(),
1453 alt: false,
1454 paste: true,
1455 });
1456 let _ = Model::update(&mut input, key_msg);
1457
1458 assert_eq!(
1459 input.value(),
1460 "helloworld",
1461 "Control characters should be stripped"
1462 );
1463 }
1464
1465 #[test]
1466 fn test_bracketed_paste_preserves_unicode() {
1467 let mut input = TextInput::new();
1468 input.focus();
1469
1470 let key_msg = Message::new(KeyMsg {
1471 key_type: bubbletea::KeyType::Runes,
1472 runes: "hello δΈη π".chars().collect(),
1473 alt: false,
1474 paste: true,
1475 });
1476 let _ = Model::update(&mut input, key_msg);
1477
1478 assert_eq!(input.value(), "hello δΈη π");
1479 }
1480
1481 #[test]
1482 fn test_bracketed_paste_tabs_to_spaces() {
1483 let mut input = TextInput::new();
1484 input.focus();
1485
1486 let key_msg = Message::new(KeyMsg {
1488 key_type: bubbletea::KeyType::Runes,
1489 runes: "col1\tcol2".chars().collect(),
1490 alt: false,
1491 paste: true,
1492 });
1493 let _ = Model::update(&mut input, key_msg);
1494
1495 assert_eq!(
1496 input.value(),
1497 "col1 col2",
1498 "Tabs should be converted to single space"
1499 );
1500 }
1501
1502 #[test]
1503 fn test_set_value_validates_after_truncation() {
1504 let mut input = TextInput::new();
1505 input.char_limit = 3;
1506 input.set_validate(|s| {
1508 if s.len() > 3 {
1509 Some("Too long".to_string())
1510 } else {
1511 None
1512 }
1513 });
1514
1515 input.set_value("1234");
1518
1519 assert_eq!(input.value(), "123");
1520 assert!(
1521 input.err.is_none(),
1522 "Validation should run on truncated value"
1523 );
1524 }
1525}