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}