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_extras::prelude::*;
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}