bubbletea_widgets/textinput/
model.rs

1//! Core model implementation for the textinput component.
2
3use super::keymap::{default_key_map, KeyMap};
4#[cfg(feature = "clipboard-support")]
5use super::types::PasteMsg;
6use super::types::{EchoMode, PasteErrMsg, ValidateFunc};
7use crate::cursor::{new as cursor_new, Model as Cursor};
8use bubbletea_rs::{Cmd, Model as BubbleTeaModel, Msg};
9use lipgloss::{Color, Style};
10use std::time::Duration;
11
12/// The main text input component model for Bubble Tea applications.
13///
14/// This struct represents a single-line text input field with support for:
15/// - Cursor movement and text editing
16/// - Input validation
17/// - Auto-completion suggestions  
18/// - Different echo modes (normal, password, hidden)
19/// - Horizontal scrolling for long text
20/// - Customizable styling and key bindings
21///
22/// The model follows the Elm Architecture pattern used by Bubble Tea, with
23/// separate `Init()`, `Update()`, and `View()` methods for state management.
24///
25/// # Examples
26///
27/// ```rust
28/// use bubbletea_widgets::textinput::{new, EchoMode};
29/// use bubbletea_rs::Model;
30///
31/// // Create and configure a text input
32/// let mut input = new();
33/// input.focus();
34/// input.set_placeholder("Enter your name...");
35/// input.set_width(30);
36/// input.set_char_limit(50);
37///
38/// // For password input
39/// input.set_echo_mode(EchoMode::EchoPassword);
40///
41/// // With validation
42/// input.set_validate(Box::new(|s: &str| {
43///     if s.len() >= 3 {
44///         Ok(())
45///     } else {
46///         Err("Must be at least 3 characters".to_string())
47///     }
48/// }));
49/// ```
50///
51/// # Note
52///
53/// This struct matches the Go `Model` struct exactly for 1-1 compatibility.
54#[allow(dead_code)]
55pub struct Model {
56    /// Err is an error that was not caught by a validator.
57    pub err: Option<String>,
58
59    /// Prompt is the prompt to display before the text input.
60    pub prompt: String,
61    /// Style for the prompt prefix.
62    pub prompt_style: Style,
63
64    /// TextStyle is the style of the text as it's being typed.
65    pub text_style: Style,
66
67    /// Placeholder is the placeholder text to display when the input is empty.
68    pub placeholder: String,
69    /// Style for the placeholder text.
70    pub placeholder_style: Style,
71
72    /// Cursor is the cursor model.
73    pub cursor: Cursor,
74    /// Cursor rendering mode (blink/static/hidden).
75    pub cursor_mode: crate::cursor::Mode,
76
77    /// Value is the value of the text input.
78    pub(super) value: Vec<char>,
79
80    /// Focus indicates whether the input is focused.
81    pub(super) focus: bool,
82
83    /// Position is the cursor position.
84    pub(super) pos: usize,
85
86    /// Width is the maximum number of characters that can be displayed at once.
87    pub width: i32,
88
89    /// KeyMap encodes the keybindings.
90    pub key_map: KeyMap,
91
92    /// CharLimit is the maximum number of characters this input will accept.
93    /// 0 means no limit.
94    pub char_limit: i32,
95
96    /// EchoMode is the echo mode of the input.
97    pub echo_mode: EchoMode,
98
99    /// EchoCharacter is the character to use for password fields.
100    pub echo_character: char,
101
102    /// CompletionStyle is the style of the completion suggestion.
103    pub completion_style: Style,
104
105    /// Validate is a function that validates the input.
106    pub(super) validate: Option<ValidateFunc>,
107
108    /// Internal fields for managing overflow and suggestions
109    pub(super) offset: usize,
110    pub(super) offset_right: usize,
111    pub(super) suggestions: Vec<Vec<char>>,
112    pub(super) matched_suggestions: Vec<Vec<char>>,
113    pub(super) show_suggestions: bool,
114    pub(super) current_suggestion_index: usize,
115}
116
117/// Creates a new text input model with default settings.
118///
119/// The returned model is not focused by default. Call `focus()` to enable keyboard input.
120///
121/// # Returns
122///
123/// A new `Model` instance with default configuration:
124/// - Empty value and placeholder
125/// - Default prompt ("> ")
126/// - Normal echo mode
127/// - No character or width limits
128/// - Default key bindings
129///
130/// # Examples
131///
132/// ```rust
133/// use bubbletea_widgets::textinput::new;
134///
135/// let mut input = new();
136/// input.focus();
137/// input.set_placeholder("Enter text...");
138/// input.set_width(30);
139/// ```
140///
141/// # Note
142///
143/// This function matches Go's New function exactly for compatibility.
144pub fn new() -> Model {
145    let mut m = Model {
146        err: None,
147        prompt: "> ".to_string(),
148        prompt_style: Style::new(),
149        text_style: Style::new(),
150        placeholder: String::new(),
151        placeholder_style: Style::new().foreground(Color::from("240")),
152        cursor: cursor_new(),
153        cursor_mode: crate::cursor::Mode::Blink,
154        value: Vec::new(),
155        focus: false,
156        pos: 0,
157        width: 0,
158        key_map: default_key_map(),
159        char_limit: 0,
160        echo_mode: EchoMode::EchoNormal,
161        echo_character: '*',
162        completion_style: Style::new().foreground(Color::from("240")),
163        validate: None,
164        offset: 0,
165        offset_right: 0,
166        suggestions: Vec::new(),
167        matched_suggestions: Vec::new(),
168        show_suggestions: false,
169        current_suggestion_index: 0,
170    };
171
172    m.cursor.set_mode(crate::cursor::Mode::Blink);
173    m
174}
175
176/// Creates a new text input model (alias for `new()`).
177///
178/// This is provided for compatibility with the bubbletea pattern where both
179/// `New()` and `NewModel()` functions exist.
180///
181/// # Returns
182///
183/// A new `Model` instance with default settings
184///
185/// # Examples
186///
187/// ```rust
188/// use bubbletea_widgets::textinput::new_model;
189///
190/// let input = new_model();
191/// ```
192///
193/// # Note
194///
195/// The Go implementation has both New() and NewModel() functions for compatibility.
196pub fn new_model() -> Model {
197    new()
198}
199
200impl Default for Model {
201    fn default() -> Self {
202        new()
203    }
204}
205
206/// Creates a command that triggers cursor blinking.
207///
208/// This command should be returned from your application's `init()` method or
209/// when focusing the text input to start the cursor blinking animation.
210///
211/// # Returns
212///
213/// A `Cmd` that will periodically send blink messages to animate the cursor
214///
215/// # Examples
216///
217/// ```rust
218/// use bubbletea_widgets::textinput::blink;
219/// use bubbletea_rs::{Model, Cmd};
220///
221/// struct App {
222///     // ... other fields
223/// }
224///
225/// impl Model for App {
226///     fn init() -> (Self, Option<Cmd>) {
227///         // Return blink command to start cursor animation
228///         (App { /* ... */ }, Some(blink()))
229///     }
230/// #
231/// #   fn update(&mut self, _msg: bubbletea_rs::Msg) -> Option<Cmd> { None }
232/// #   fn view(&self) -> String { String::new() }
233/// }
234/// ```
235pub fn blink() -> Cmd {
236    use bubbletea_rs::tick as bubbletea_tick;
237    let id = 0usize;
238    let tag = 0usize;
239    bubbletea_tick(Duration::from_millis(500), move |_| {
240        Box::new(crate::cursor::BlinkMsg { id, tag }) as Msg
241    })
242}
243
244/// Creates a command that retrieves text from the system clipboard.
245///
246/// This command reads the current clipboard contents and sends a paste message
247/// that can be handled by the text input's `update()` method.
248///
249/// # Returns
250///
251/// A `Cmd` that will attempt to read from clipboard and send either:
252/// - `PasteMsg(String)` with the clipboard contents on success
253/// - `PasteErrMsg(String)` with an error message on failure
254///
255/// # Examples
256///
257/// ```rust
258/// use bubbletea_widgets::textinput::paste;
259///
260/// // This is typically called internally when Ctrl+V is pressed
261/// // but can be used manually:
262/// let paste_cmd = paste();
263/// ```
264///
265/// # Errors
266///
267/// The returned command may produce a `PasteErrMsg` if:
268/// - The clipboard is not accessible
269/// - The clipboard contains non-text data
270/// - System clipboard permissions are denied
271pub fn paste() -> Cmd {
272    use bubbletea_rs::tick as bubbletea_tick;
273    bubbletea_tick(Duration::from_nanos(1), |_| {
274        #[cfg(feature = "clipboard-support")]
275        {
276            use clipboard::{ClipboardContext, ClipboardProvider};
277            let res: Result<String, String> = (|| {
278                let mut ctx: ClipboardContext = ClipboardProvider::new()
279                    .map_err(|e| format!("Failed to create clipboard context: {}", e))?;
280                ctx.get_contents()
281                    .map_err(|e| format!("Failed to read clipboard: {}", e))
282            })();
283            match res {
284                Ok(s) => Box::new(PasteMsg(s)) as Msg,
285                Err(e) => Box::new(PasteErrMsg(e)) as Msg,
286            }
287        }
288        #[cfg(not(feature = "clipboard-support"))]
289        {
290            Box::new(PasteErrMsg("Clipboard support not enabled".to_string())) as Msg
291        }
292    })
293}
294
295impl BubbleTeaModel for Model {
296    fn init() -> (Self, std::option::Option<Cmd>) {
297        let model = new();
298        (model, std::option::Option::None)
299    }
300
301    fn update(&mut self, msg: Msg) -> std::option::Option<Cmd> {
302        self.update(msg)
303    }
304
305    fn view(&self) -> String {
306        self.view()
307    }
308}