bubbletea_widgets/textinput/methods.rs
1//! Core methods for the Model struct.
2
3use super::model::{paste, Model};
4use super::types::{EchoMode, PasteErrMsg, PasteMsg, ValidateFunc};
5use crate::Component;
6use bubbletea_rs::{Cmd, KeyMsg, Msg};
7use crossterm::event::{KeyCode, KeyModifiers};
8
9impl Model {
10 /// Sets the value of the text input.
11 ///
12 /// This method replaces the entire content of the text input with the provided string.
13 /// If a validation function is set, it will be applied to the new value.
14 ///
15 /// # Arguments
16 ///
17 /// * `s` - The new string value to set
18 ///
19 /// # Examples
20 ///
21 /// ```rust
22 /// use bubbletea_widgets::textinput::new;
23 ///
24 /// let mut input = new();
25 /// input.set_value("Hello, world!");
26 /// assert_eq!(input.value(), "Hello, world!");
27 /// ```
28 ///
29 /// # Note
30 ///
31 /// This method matches Go's SetValue method exactly for compatibility.
32 pub fn set_value(&mut self, s: &str) {
33 let runes: Vec<char> = s.chars().collect();
34 let err = self.validate_runes(&runes);
35 self.set_value_internal(runes, err);
36 }
37
38 /// Internal method to set value with validation
39 pub(super) fn set_value_internal(&mut self, runes: Vec<char>, err: Option<String>) {
40 self.err = err;
41
42 let empty = self.value.is_empty();
43
44 if self.char_limit > 0 && runes.len() > self.char_limit as usize {
45 self.value = runes[..self.char_limit as usize].to_vec();
46 } else {
47 self.value = runes;
48 }
49
50 if (self.pos == 0 && empty) || self.pos > self.value.len() {
51 self.set_cursor(self.value.len());
52 }
53
54 self.handle_overflow();
55 self.update_suggestions();
56 }
57
58 /// Returns the current value of the text input.
59 ///
60 /// # Returns
61 ///
62 /// A `String` containing the current text value
63 ///
64 /// # Examples
65 ///
66 /// ```rust
67 /// use bubbletea_widgets::textinput::new;
68 ///
69 /// let mut input = new();
70 /// input.set_value("test");
71 /// assert_eq!(input.value(), "test");
72 /// ```
73 ///
74 /// # Note
75 ///
76 /// This method matches Go's Value method exactly for compatibility.
77 pub fn value(&self) -> String {
78 self.value.iter().collect()
79 }
80
81 /// Returns the current cursor position as a character index.
82 ///
83 /// # Returns
84 ///
85 /// The cursor position as a `usize`, where 0 is the beginning of the text
86 ///
87 /// # Examples
88 ///
89 /// ```rust
90 /// use bubbletea_widgets::textinput::new;
91 ///
92 /// let mut input = new();
93 /// input.set_value("hello");
94 /// input.set_cursor(2);
95 /// assert_eq!(input.position(), 2);
96 /// ```
97 ///
98 /// # Note
99 ///
100 /// This method matches Go's Position method exactly for compatibility.
101 pub fn position(&self) -> usize {
102 self.pos
103 }
104
105 /// Moves the cursor to the specified position.
106 ///
107 /// If the position is beyond the end of the text, the cursor will be placed at the end.
108 /// This method also handles overflow for horizontal scrolling when the text is wider than the display width.
109 ///
110 /// # Arguments
111 ///
112 /// * `pos` - The new cursor position as a character index
113 ///
114 /// # Examples
115 ///
116 /// ```rust
117 /// use bubbletea_widgets::textinput::new;
118 ///
119 /// let mut input = new();
120 /// input.set_value("hello world");
121 /// input.set_cursor(6); // Position after "hello "
122 /// assert_eq!(input.position(), 6);
123 /// ```
124 ///
125 /// # Note
126 ///
127 /// This method matches Go's SetCursor method exactly for compatibility.
128 pub fn set_cursor(&mut self, pos: usize) {
129 self.pos = pos.min(self.value.len());
130 self.handle_overflow();
131 }
132
133 /// Moves the cursor to the beginning of the input field.
134 ///
135 /// # Examples
136 ///
137 /// ```rust
138 /// use bubbletea_widgets::textinput::new;
139 ///
140 /// let mut input = new();
141 /// input.set_value("hello");
142 /// input.cursor_end();
143 /// input.cursor_start();
144 /// assert_eq!(input.position(), 0);
145 /// ```
146 ///
147 /// # Note
148 ///
149 /// This method matches Go's CursorStart method exactly for compatibility.
150 pub fn cursor_start(&mut self) {
151 self.set_cursor(0);
152 }
153
154 /// Moves the cursor to the end of the input field.
155 ///
156 /// # Examples
157 ///
158 /// ```rust
159 /// use bubbletea_widgets::textinput::new;
160 ///
161 /// let mut input = new();
162 /// input.set_value("hello");
163 /// input.cursor_start();
164 /// input.cursor_end();
165 /// assert_eq!(input.position(), 5);
166 /// ```
167 ///
168 /// # Note
169 ///
170 /// This method matches Go's CursorEnd method exactly for compatibility.
171 pub fn cursor_end(&mut self) {
172 self.set_cursor(self.value.len());
173 }
174
175 /// Returns whether the text input currently has focus.
176 ///
177 /// # Returns
178 ///
179 /// `true` if the input is focused and will respond to key events, `false` otherwise
180 ///
181 /// # Examples
182 ///
183 /// ```rust
184 /// use bubbletea_widgets::textinput::new;
185 ///
186 /// let mut input = new();
187 /// assert!(!input.focused());
188 /// input.focus();
189 /// assert!(input.focused());
190 /// ```
191 ///
192 /// # Note
193 ///
194 /// This method matches Go's Focused method exactly for compatibility.
195 pub fn focused(&self) -> bool {
196 self.focus
197 }
198
199 /// Sets focus on the text input, enabling it to receive key events.
200 ///
201 /// When focused, the text input will display a cursor and respond to keyboard input.
202 /// This method also focuses the cursor component which may return a command for cursor blinking.
203 ///
204 /// # Returns
205 ///
206 /// A `Cmd` that may be used to start cursor blinking animation
207 ///
208 /// # Examples
209 ///
210 /// ```rust
211 /// use bubbletea_widgets::textinput::new;
212 ///
213 /// let mut input = new();
214 /// let cmd = input.focus();
215 /// assert!(input.focused());
216 /// ```
217 ///
218 /// # Note
219 ///
220 /// This method matches Go's Focus method exactly for compatibility.
221 pub fn focus(&mut self) -> Cmd {
222 self.focus = true;
223 self.cursor.focus().unwrap_or_else(|| {
224 // If cursor didn't produce a command, return a resolved no-op command
225 Box::pin(async { None })
226 })
227 }
228
229 /// Removes focus from the text input, disabling key event handling.
230 ///
231 /// When blurred, the text input will not respond to keyboard input and
232 /// the cursor will not be visible.
233 ///
234 /// # Examples
235 ///
236 /// ```rust
237 /// use bubbletea_widgets::textinput::new;
238 ///
239 /// let mut input = new();
240 /// input.focus();
241 /// assert!(input.focused());
242 /// input.blur();
243 /// assert!(!input.focused());
244 /// ```
245 ///
246 /// # Note
247 ///
248 /// This method matches Go's Blur method exactly for compatibility.
249 pub fn blur(&mut self) {
250 self.focus = false;
251 self.cursor.blur();
252 }
253
254 /// Clears all text and resets the cursor to the beginning.
255 ///
256 /// This method removes all text content and moves the cursor to position 0.
257 /// It does not change other settings like placeholder text, validation, or styling.
258 ///
259 /// # Examples
260 ///
261 /// ```rust
262 /// use bubbletea_widgets::textinput::new;
263 ///
264 /// let mut input = new();
265 /// input.set_value("some text");
266 /// input.reset();
267 /// assert_eq!(input.value(), "");
268 /// assert_eq!(input.position(), 0);
269 /// ```
270 ///
271 /// # Note
272 ///
273 /// This method matches Go's Reset method exactly for compatibility.
274 pub fn reset(&mut self) {
275 self.value.clear();
276 self.set_cursor(0);
277 }
278
279 /// Sets the list of available suggestions for auto-completion.
280 ///
281 /// Suggestions will be filtered based on the current input and can be navigated
282 /// using the configured key bindings (typically up/down arrows and tab to accept).
283 ///
284 /// # Arguments
285 ///
286 /// * `suggestions` - A vector of strings that can be suggested to the user
287 ///
288 /// # Examples
289 ///
290 /// ```rust
291 /// use bubbletea_widgets::textinput::new;
292 ///
293 /// let mut input = new();
294 /// input.set_suggestions(vec![
295 /// "apple".to_string(),
296 /// "application".to_string(),
297 /// "apply".to_string(),
298 /// ]);
299 /// input.set_value("app");
300 /// // Now suggestions starting with "app" will be available
301 /// ```
302 ///
303 /// # Note
304 ///
305 /// This method matches Go's SetSuggestions method exactly for compatibility.
306 pub fn set_suggestions(&mut self, suggestions: Vec<String>) {
307 self.suggestions = suggestions
308 .into_iter()
309 .map(|s| s.chars().collect())
310 .collect();
311 self.update_suggestions();
312 }
313
314 /// Sets the placeholder text displayed when the input is empty.
315 ///
316 /// # Arguments
317 ///
318 /// * `placeholder` - The placeholder text to display
319 ///
320 /// # Examples
321 ///
322 /// ```rust
323 /// use bubbletea_widgets::textinput::new;
324 ///
325 /// let mut input = new();
326 /// input.set_placeholder("Enter your name...");
327 /// // Placeholder will be visible when input is empty and focused
328 /// ```
329 pub fn set_placeholder(&mut self, placeholder: &str) {
330 self.placeholder = placeholder.to_string();
331 }
332
333 /// Sets the display width of the input field in characters.
334 ///
335 /// This controls how many characters are visible at once. If the text is longer
336 /// than the width, it will scroll horizontally as the user types or moves the cursor.
337 ///
338 /// # Arguments
339 ///
340 /// * `width` - The width in characters. Use 0 for no width limit.
341 ///
342 /// # Examples
343 ///
344 /// ```rust
345 /// use bubbletea_widgets::textinput::new;
346 ///
347 /// let mut input = new();
348 /// input.set_width(20); // Show up to 20 characters at once
349 /// ```
350 pub fn set_width(&mut self, width: i32) {
351 self.width = width;
352 }
353
354 /// Sets the echo mode for displaying typed characters.
355 ///
356 /// # Arguments
357 ///
358 /// * `mode` - The echo mode to use:
359 /// - `EchoNormal`: Display characters as typed (default)
360 /// - `EchoPassword`: Display asterisks instead of actual characters
361 /// - `EchoNone`: Don't display any characters
362 ///
363 /// # Examples
364 ///
365 /// ```rust
366 /// use bubbletea_widgets::textinput::{new, EchoMode};
367 ///
368 /// let mut input = new();
369 /// input.set_echo_mode(EchoMode::EchoPassword);
370 /// input.set_value("secret");
371 /// // Text will appear as asterisks: ******
372 /// ```
373 pub fn set_echo_mode(&mut self, mode: EchoMode) {
374 self.echo_mode = mode;
375 }
376
377 /// Sets the maximum number of characters allowed in the input.
378 ///
379 /// # Arguments
380 ///
381 /// * `limit` - Maximum number of characters. Use 0 for no limit.
382 ///
383 /// # Examples
384 ///
385 /// ```rust
386 /// use bubbletea_widgets::textinput::new;
387 ///
388 /// let mut input = new();
389 /// input.set_char_limit(10); // Allow up to 10 characters
390 /// input.set_value("This is a very long string");
391 /// assert_eq!(input.value().len(), 10); // Truncated to 10 characters
392 /// ```
393 pub fn set_char_limit(&mut self, limit: i32) {
394 self.char_limit = limit;
395 }
396
397 /// Sets a validation function that will be called whenever the input changes.
398 ///
399 /// The validation function receives the current input value and should return
400 /// `Ok(())` if the input is valid, or `Err(message)` if invalid.
401 ///
402 /// # Arguments
403 ///
404 /// * `validate` - A function that takes a `&str` and returns `Result<(), String>`
405 ///
406 /// # Examples
407 ///
408 /// ```rust
409 /// use bubbletea_widgets::textinput::new;
410 ///
411 /// let mut input = new();
412 /// input.set_validate(Box::new(|s: &str| {
413 /// if s.contains('@') {
414 /// Ok(())
415 /// } else {
416 /// Err("Must contain @ symbol".to_string())
417 /// }
418 /// }));
419 /// ```
420 pub fn set_validate(&mut self, validate: ValidateFunc) {
421 self.validate = Some(validate);
422 }
423
424 /// Processes a message and updates the text input state.
425 ///
426 /// This method handles keyboard input, cursor movement, text editing operations,
427 /// and clipboard operations. It should be called from your application's update loop.
428 ///
429 /// # Arguments
430 ///
431 /// * `msg` - The message to process (typically a key press or paste event)
432 ///
433 /// # Returns
434 ///
435 /// An optional `Cmd` that may need to be executed (e.g., for cursor blinking)
436 ///
437 /// # Examples
438 ///
439 /// ```rust
440 /// use bubbletea_widgets::textinput::new;
441 /// use bubbletea_rs::{KeyMsg, Model};
442 /// use crossterm::event::{KeyCode, KeyModifiers};
443 ///
444 /// let mut input = new();
445 /// input.focus();
446 ///
447 /// // Simulate typing 'h'
448 /// let key_msg = KeyMsg {
449 /// key: KeyCode::Char('h'),
450 /// modifiers: KeyModifiers::NONE,
451 /// };
452 /// input.update(Box::new(key_msg));
453 /// assert_eq!(input.value(), "h");
454 /// ```
455 pub fn update(&mut self, msg: Msg) -> std::option::Option<Cmd> {
456 if !self.focus {
457 return std::option::Option::None;
458 }
459
460 // Handle key messages
461 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
462 // Check key bindings in order of priority
463 if let Some(cmd) = self.handle_suggestion_keys(key_msg) {
464 return cmd;
465 }
466 if let Some(cmd) = self.handle_clipboard_keys(key_msg) {
467 return cmd;
468 }
469
470 self.handle_deletion_keys(key_msg);
471 self.handle_movement_keys(key_msg);
472 self.handle_character_input(key_msg);
473
474 self.update_suggestions();
475 }
476
477 // Handle paste messages
478 if let Some(paste_msg) = msg.downcast_ref::<PasteMsg>() {
479 let chars: Vec<char> = paste_msg.0.chars().collect();
480 self.insert_runes_from_user_input(chars);
481 }
482
483 if let Some(paste_err) = msg.downcast_ref::<PasteErrMsg>() {
484 self.err = Some(paste_err.0.clone());
485 }
486
487 // Update cursor
488 let cursor_cmd = self.cursor.update(&msg);
489
490 self.handle_overflow();
491 cursor_cmd
492 }
493
494 /// Handle suggestion-related key bindings
495 fn handle_suggestion_keys(&mut self, key_msg: &KeyMsg) -> Option<Option<Cmd>> {
496 use crate::key::matches_binding;
497
498 // Check for suggestion acceptance first
499 if matches_binding(key_msg, &self.key_map.accept_suggestion) {
500 if self.can_accept_suggestion() {
501 let suggestion = &self.matched_suggestions[self.current_suggestion_index];
502 let remaining: Vec<char> = suggestion[self.value.len()..].to_vec();
503 self.value.extend(remaining);
504 self.cursor_end();
505 }
506 return Some(None);
507 }
508
509 // Handle suggestion navigation
510 if matches_binding(key_msg, &self.key_map.next_suggestion) {
511 self.next_suggestion();
512 } else if matches_binding(key_msg, &self.key_map.prev_suggestion) {
513 self.previous_suggestion();
514 } else {
515 return None; // No suggestion key was matched
516 }
517
518 Some(None)
519 }
520
521 /// Handle clipboard-related key bindings
522 fn handle_clipboard_keys(&mut self, key_msg: &KeyMsg) -> Option<Option<Cmd>> {
523 use crate::key::matches_binding;
524
525 if matches_binding(key_msg, &self.key_map.paste) {
526 return Some(Some(paste()));
527 }
528
529 None
530 }
531
532 /// Handle deletion-related key bindings
533 fn handle_deletion_keys(&mut self, key_msg: &KeyMsg) {
534 use crate::key::matches_binding;
535
536 if matches_binding(key_msg, &self.key_map.delete_word_backward) {
537 self.delete_word_backward();
538 } else if matches_binding(key_msg, &self.key_map.delete_character_backward) {
539 self.err = None;
540 if !self.value.is_empty() && self.pos > 0 {
541 self.value.remove(self.pos - 1);
542 self.pos -= 1;
543 self.err = self.validate_runes(&self.value);
544 }
545 } else if matches_binding(key_msg, &self.key_map.delete_character_forward) {
546 if !self.value.is_empty() && self.pos < self.value.len() {
547 self.value.remove(self.pos);
548 self.err = self.validate_runes(&self.value);
549 }
550 } else if matches_binding(key_msg, &self.key_map.delete_after_cursor) {
551 self.delete_after_cursor();
552 } else if matches_binding(key_msg, &self.key_map.delete_before_cursor) {
553 self.delete_before_cursor();
554 } else if matches_binding(key_msg, &self.key_map.delete_word_forward) {
555 self.delete_word_forward();
556 }
557 }
558
559 /// Handle movement-related key bindings
560 fn handle_movement_keys(&mut self, key_msg: &KeyMsg) {
561 use crate::key::matches_binding;
562
563 if matches_binding(key_msg, &self.key_map.word_backward) {
564 self.word_backward();
565 } else if matches_binding(key_msg, &self.key_map.character_backward) {
566 if self.pos > 0 {
567 self.set_cursor(self.pos - 1);
568 }
569 } else if matches_binding(key_msg, &self.key_map.word_forward) {
570 self.word_forward();
571 } else if matches_binding(key_msg, &self.key_map.character_forward) {
572 if self.pos < self.value.len() {
573 self.set_cursor(self.pos + 1);
574 }
575 } else if matches_binding(key_msg, &self.key_map.line_start) {
576 self.cursor_start();
577 } else if matches_binding(key_msg, &self.key_map.line_end) {
578 self.cursor_end();
579 }
580 }
581
582 /// Handle regular character input
583 fn handle_character_input(&mut self, key_msg: &KeyMsg) {
584 // Regular character input (no Ctrl/Alt modifiers)
585 if let KeyCode::Char(ch) = key_msg.key {
586 // Accept when no control/alt modifiers; allow shift (encoded in char case)
587 if !key_msg.modifiers.contains(KeyModifiers::CONTROL)
588 && !key_msg.modifiers.contains(KeyModifiers::ALT)
589 {
590 self.insert_runes_from_user_input(vec![ch]);
591 }
592 }
593 }
594
595 /// Internal method to handle text insertion from user input
596 pub(super) fn insert_runes_from_user_input(&mut self, runes: Vec<char>) {
597 let mut avail_space = if self.char_limit > 0 {
598 let space = self.char_limit - self.value.len() as i32;
599 if space <= 0 {
600 return;
601 }
602 Some(space as usize)
603 } else {
604 None
605 };
606
607 // Stuff before and after the cursor
608 let mut head = self.value[..self.pos].to_vec();
609 let tail = self.value[self.pos..].to_vec();
610
611 // Insert pasted runes
612 for r in runes {
613 head.push(r);
614 self.pos += 1;
615
616 if let Some(ref mut space) = avail_space {
617 *space -= 1;
618 if *space == 0 {
619 break;
620 }
621 }
622 }
623
624 // Put it all back together
625 let mut new_value = head;
626 new_value.extend(tail);
627
628 let input_err = self.validate_runes(&new_value);
629 self.set_value_internal(new_value, input_err);
630 }
631
632 /// Validate the input against the validation function if set
633 pub(super) fn validate_runes(&self, runes: &[char]) -> Option<String> {
634 if let Some(ref validate) = self.validate {
635 let value: String = runes.iter().collect();
636 validate(&value).err()
637 } else {
638 None
639 }
640 }
641
642 /// Handle overflow for horizontal scrolling viewport
643 pub(super) fn handle_overflow(&mut self) {
644 if self.width <= 0 {
645 self.offset = 0;
646 self.offset_right = self.value.len();
647 return;
648 }
649
650 let value_width = self.value.len();
651 if value_width <= self.width as usize {
652 self.offset = 0;
653 self.offset_right = self.value.len();
654 return;
655 }
656
657 // Correct right offset if we've deleted characters
658 self.offset_right = self.offset_right.min(self.value.len());
659
660 if self.pos < self.offset {
661 self.offset = self.pos;
662 let mut w = 0;
663 let mut i = 0;
664 let runes = &self.value[self.offset..];
665
666 while i < runes.len() && w <= self.width as usize {
667 w += 1; // Simplified width calculation
668 i += 1;
669 }
670
671 self.offset_right = self.offset + i;
672 } else if self.pos >= self.offset_right {
673 self.offset_right = self.pos;
674
675 let mut w = 0;
676 let runes = &self.value[..self.offset_right];
677 let mut i = runes.len();
678 while i > 0 && w < self.width as usize {
679 w += 1; // Simplified width calculation
680 i = i.saturating_sub(1);
681 }
682 self.offset = i;
683 }
684 }
685}
686
687impl Component for Model {
688 /// Sets the component to focused state.
689 ///
690 /// This implementation wraps the existing focus() method to match the Component trait's
691 /// expected signature of returning Option<Cmd> instead of Cmd.
692 ///
693 /// # Returns
694 ///
695 /// Some(Cmd) containing a cursor blink command, or None if no command is needed.
696 fn focus(&mut self) -> Option<Cmd> {
697 Some(self.focus())
698 }
699
700 /// Sets the component to blurred (unfocused) state.
701 ///
702 /// This directly delegates to the existing blur() method which already matches
703 /// the Component trait signature.
704 fn blur(&mut self) {
705 self.blur()
706 }
707
708 /// Returns the current focus state of the component.
709 ///
710 /// This directly delegates to the existing focused() method which already matches
711 /// the Component trait signature.
712 fn focused(&self) -> bool {
713 self.focused()
714 }
715}