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::key::matches;
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 for suggestion acceptance first
463 if matches(key_msg, &[&self.key_map.accept_suggestion]) {
464 if self.can_accept_suggestion() {
465 let suggestion = &self.matched_suggestions[self.current_suggestion_index];
466 let remaining: Vec<char> = suggestion[self.value.len()..].to_vec();
467 self.value.extend(remaining);
468 self.cursor_end();
469 }
470 return std::option::Option::None;
471 }
472
473 // Handle other key bindings
474 if matches(key_msg, &[&self.key_map.delete_word_backward]) {
475 self.delete_word_backward();
476 } else if matches(key_msg, &[&self.key_map.delete_character_backward]) {
477 self.err = None;
478 if !self.value.is_empty() && self.pos > 0 {
479 self.value.remove(self.pos - 1);
480 self.pos -= 1;
481 self.err = self.validate_runes(&self.value);
482 }
483 } else if matches(key_msg, &[&self.key_map.word_backward]) {
484 self.word_backward();
485 } else if matches(key_msg, &[&self.key_map.character_backward]) {
486 if self.pos > 0 {
487 self.set_cursor(self.pos - 1);
488 }
489 } else if matches(key_msg, &[&self.key_map.word_forward]) {
490 self.word_forward();
491 } else if matches(key_msg, &[&self.key_map.character_forward]) {
492 if self.pos < self.value.len() {
493 self.set_cursor(self.pos + 1);
494 }
495 } else if matches(key_msg, &[&self.key_map.line_start]) {
496 self.cursor_start();
497 } else if matches(key_msg, &[&self.key_map.delete_character_forward]) {
498 if !self.value.is_empty() && self.pos < self.value.len() {
499 self.value.remove(self.pos);
500 self.err = self.validate_runes(&self.value);
501 }
502 } else if matches(key_msg, &[&self.key_map.line_end]) {
503 self.cursor_end();
504 } else if matches(key_msg, &[&self.key_map.delete_after_cursor]) {
505 self.delete_after_cursor();
506 } else if matches(key_msg, &[&self.key_map.delete_before_cursor]) {
507 self.delete_before_cursor();
508 } else if matches(key_msg, &[&self.key_map.paste]) {
509 return std::option::Option::Some(paste());
510 } else if matches(key_msg, &[&self.key_map.delete_word_forward]) {
511 self.delete_word_forward();
512 } else if matches(key_msg, &[&self.key_map.next_suggestion]) {
513 self.next_suggestion();
514 } else if matches(key_msg, &[&self.key_map.prev_suggestion]) {
515 self.previous_suggestion();
516 } else {
517 // Regular character input (no Ctrl/Alt modifiers)
518 if let KeyCode::Char(ch) = key_msg.key {
519 // Accept when no control/alt modifiers; allow shift (encoded in char case)
520 if !key_msg.modifiers.contains(KeyModifiers::CONTROL)
521 && !key_msg.modifiers.contains(KeyModifiers::ALT)
522 {
523 self.insert_runes_from_user_input(vec![ch]);
524 }
525 }
526 }
527
528 self.update_suggestions();
529 }
530
531 // Handle paste messages
532 if let Some(paste_msg) = msg.downcast_ref::<PasteMsg>() {
533 let chars: Vec<char> = paste_msg.0.chars().collect();
534 self.insert_runes_from_user_input(chars);
535 }
536
537 if let Some(paste_err) = msg.downcast_ref::<PasteErrMsg>() {
538 self.err = Some(paste_err.0.clone());
539 }
540
541 // Update cursor
542 let cursor_cmd = self.cursor.update(&msg);
543
544 self.handle_overflow();
545 cursor_cmd
546 }
547
548 /// Internal method to handle text insertion from user input
549 pub(super) fn insert_runes_from_user_input(&mut self, runes: Vec<char>) {
550 let mut avail_space = if self.char_limit > 0 {
551 let space = self.char_limit - self.value.len() as i32;
552 if space <= 0 {
553 return;
554 }
555 Some(space as usize)
556 } else {
557 None
558 };
559
560 // Stuff before and after the cursor
561 let mut head = self.value[..self.pos].to_vec();
562 let tail = self.value[self.pos..].to_vec();
563
564 // Insert pasted runes
565 for r in runes {
566 head.push(r);
567 self.pos += 1;
568
569 if let Some(ref mut space) = avail_space {
570 *space -= 1;
571 if *space == 0 {
572 break;
573 }
574 }
575 }
576
577 // Put it all back together
578 let mut new_value = head;
579 new_value.extend(tail);
580
581 let input_err = self.validate_runes(&new_value);
582 self.set_value_internal(new_value, input_err);
583 }
584
585 /// Validate the input against the validation function if set
586 pub(super) fn validate_runes(&self, runes: &[char]) -> Option<String> {
587 if let Some(ref validate) = self.validate {
588 let value: String = runes.iter().collect();
589 validate(&value).err()
590 } else {
591 None
592 }
593 }
594
595 /// Handle overflow for horizontal scrolling viewport
596 pub(super) fn handle_overflow(&mut self) {
597 if self.width <= 0 {
598 self.offset = 0;
599 self.offset_right = self.value.len();
600 return;
601 }
602
603 let value_width = self.value.len();
604 if value_width <= self.width as usize {
605 self.offset = 0;
606 self.offset_right = self.value.len();
607 return;
608 }
609
610 // Correct right offset if we've deleted characters
611 self.offset_right = self.offset_right.min(self.value.len());
612
613 if self.pos < self.offset {
614 self.offset = self.pos;
615 let mut w = 0;
616 let mut i = 0;
617 let runes = &self.value[self.offset..];
618
619 while i < runes.len() && w <= self.width as usize {
620 w += 1; // Simplified width calculation
621 i += 1;
622 }
623
624 self.offset_right = self.offset + i;
625 } else if self.pos >= self.offset_right {
626 self.offset_right = self.pos;
627
628 let mut w = 0;
629 let runes = &self.value[..self.offset_right];
630 let mut i = runes.len();
631 while i > 0 && w < self.width as usize {
632 w += 1; // Simplified width calculation
633 i = i.saturating_sub(1);
634 }
635 self.offset = i;
636 }
637 }
638}