bubbletea_widgets/textarea/
helpers.rs

1//! Helper functions and types for the textarea component.
2//!
3//! This module exposes:
4//! - **`TextareaKeyMap`**: default key bindings (movement, deletion, clipboard, advanced word ops).
5//! - **`TextareaStyle`**: theming knobs for base/text, prompt, line numbers, cursor line, etc.
6//! - Utility helpers used by `textarea::Model` (word boundaries, clamping, spacing).
7//!
8//! ### Styling
9//! Use `default_focused_style()` and `default_blurred_style()` for presets that
10//! mimic the upstream Go defaults, or construct a custom `TextareaStyle` and
11//! assign it to the model.
12//!
13//! ```rust
14//! use bubbletea_widgets::textarea::{helpers::TextareaStyle, new};
15//! use lipgloss_extras::prelude::*;
16//!
17//! let mut model = new();
18//! let custom = TextareaStyle {
19//!     base: Style::new(),
20//!     text: Style::new(),
21//!     prompt: Style::new().foreground("#04b575"),
22//!     line_number: Style::new().foreground("#666666"),
23//!     cursor_line: Style::new().background("#2a2a2a"),
24//!     cursor_line_number: Style::new().foreground("#666666"),
25//!     end_of_buffer: Style::new().foreground("#3c3c3c"),
26//!     placeholder: Style::new().foreground("#666666"),
27//! };
28//! model.focused_style = custom.clone();
29//! model.blurred_style = custom;
30//! ```
31
32use crate::key::{self, KeyPress};
33use crossterm::event::{KeyCode, KeyModifiers};
34use lipgloss_extras::prelude::*;
35
36/// Complete KeyMap for textarea component - direct port from Go
37#[derive(Debug, Clone)]
38pub struct TextareaKeyMap {
39    /// Move cursor one character left.
40    pub character_backward: key::Binding,
41    /// Move cursor one character right.
42    pub character_forward: key::Binding,
43    /// Delete from cursor to end of line.
44    pub delete_after_cursor: key::Binding,
45    /// Delete from start of line to cursor.
46    pub delete_before_cursor: key::Binding,
47    /// Delete one character backward.
48    pub delete_character_backward: key::Binding,
49    /// Delete one character forward.
50    pub delete_character_forward: key::Binding,
51    /// Delete previous word.
52    pub delete_word_backward: key::Binding,
53    /// Delete next word.
54    pub delete_word_forward: key::Binding,
55    /// Insert newline.
56    pub insert_newline: key::Binding,
57    /// Move cursor to end of line.
58    pub line_end: key::Binding,
59    /// Move cursor to next visual line.
60    pub line_next: key::Binding,
61    /// Move cursor to previous visual line.
62    pub line_previous: key::Binding,
63    /// Move cursor to start of line.
64    pub line_start: key::Binding,
65    /// Paste from clipboard.
66    pub paste: key::Binding,
67    /// Move one word left.
68    pub word_backward: key::Binding,
69    /// Move one word right.
70    pub word_forward: key::Binding,
71    /// Move to beginning of input.
72    pub input_begin: key::Binding,
73    /// Move to end of input.
74    pub input_end: key::Binding,
75    // Advanced bindings from Go
76    /// Uppercase the word to the right of the cursor.
77    pub uppercase_word_forward: key::Binding,
78    /// Lowercase the word to the right of the cursor.
79    pub lowercase_word_forward: key::Binding,
80    /// Capitalize the word to the right of the cursor.
81    pub capitalize_word_forward: key::Binding,
82    /// Transpose the character to the left with the current one.
83    pub transpose_character_backward: key::Binding,
84}
85
86/// Implementation of KeyMap trait for help integration
87impl crate::key::KeyMap for TextareaKeyMap {
88    fn short_help(&self) -> Vec<&key::Binding> {
89        vec![
90            &self.character_backward,
91            &self.character_forward,
92            &self.line_next,
93            &self.line_previous,
94            &self.insert_newline,
95            &self.delete_character_backward,
96        ]
97    }
98
99    fn full_help(&self) -> Vec<Vec<&key::Binding>> {
100        vec![
101            vec![
102                &self.character_backward,
103                &self.character_forward,
104                &self.word_backward,
105                &self.word_forward,
106            ],
107            vec![
108                &self.line_next,
109                &self.line_previous,
110                &self.line_start,
111                &self.line_end,
112            ],
113            vec![
114                &self.insert_newline,
115                &self.delete_character_backward,
116                &self.delete_character_forward,
117                &self.paste,
118            ],
119            vec![
120                &self.delete_word_backward,
121                &self.delete_word_forward,
122                &self.delete_after_cursor,
123                &self.delete_before_cursor,
124            ],
125        ]
126    }
127}
128
129impl Default for TextareaKeyMap {
130    fn default() -> Self {
131        Self {
132            character_forward: key::Binding::new(vec![
133                KeyPress::from(KeyCode::Right),
134                KeyPress::from((KeyCode::Char('f'), KeyModifiers::CONTROL)),
135            ])
136            .with_help("→/ctrl+f", "character forward"),
137
138            character_backward: key::Binding::new(vec![
139                KeyPress::from(KeyCode::Left),
140                KeyPress::from((KeyCode::Char('b'), KeyModifiers::CONTROL)),
141            ])
142            .with_help("←/ctrl+b", "character backward"),
143
144            word_forward: key::Binding::new(vec![
145                KeyPress::from((KeyCode::Right, KeyModifiers::ALT)),
146                KeyPress::from((KeyCode::Char('f'), KeyModifiers::ALT)),
147            ])
148            .with_help("alt+→/alt+f", "word forward"),
149
150            word_backward: key::Binding::new(vec![
151                KeyPress::from((KeyCode::Left, KeyModifiers::ALT)),
152                KeyPress::from((KeyCode::Char('b'), KeyModifiers::ALT)),
153            ])
154            .with_help("alt+←/alt+b", "word backward"),
155
156            line_next: key::Binding::new(vec![
157                KeyPress::from(KeyCode::Down),
158                KeyPress::from((KeyCode::Char('n'), KeyModifiers::CONTROL)),
159            ])
160            .with_help("↓/ctrl+n", "next line"),
161
162            line_previous: key::Binding::new(vec![
163                KeyPress::from(KeyCode::Up),
164                KeyPress::from((KeyCode::Char('p'), KeyModifiers::CONTROL)),
165            ])
166            .with_help("↑/ctrl+p", "previous line"),
167
168            delete_word_backward: key::Binding::new(vec![
169                KeyPress::from((KeyCode::Backspace, KeyModifiers::ALT)),
170                KeyPress::from((KeyCode::Char('w'), KeyModifiers::CONTROL)),
171            ])
172            .with_help("alt+backspace/ctrl+w", "delete word backward"),
173
174            delete_word_forward: key::Binding::new(vec![
175                KeyPress::from((KeyCode::Delete, KeyModifiers::ALT)),
176                KeyPress::from((KeyCode::Char('d'), KeyModifiers::ALT)),
177            ])
178            .with_help("alt+delete/alt+d", "delete word forward"),
179
180            delete_after_cursor: key::Binding::new(vec![KeyPress::from((
181                KeyCode::Char('k'),
182                KeyModifiers::CONTROL,
183            ))])
184            .with_help("ctrl+k", "delete after cursor"),
185
186            delete_before_cursor: key::Binding::new(vec![KeyPress::from((
187                KeyCode::Char('u'),
188                KeyModifiers::CONTROL,
189            ))])
190            .with_help("ctrl+u", "delete before cursor"),
191
192            insert_newline: key::Binding::new(vec![
193                KeyPress::from(KeyCode::Enter),
194                KeyPress::from((KeyCode::Char('m'), KeyModifiers::CONTROL)),
195            ])
196            .with_help("enter/ctrl+m", "insert newline"),
197
198            delete_character_backward: key::Binding::new(vec![
199                KeyPress::from(KeyCode::Backspace),
200                KeyPress::from((KeyCode::Char('h'), KeyModifiers::CONTROL)),
201            ])
202            .with_help("backspace/ctrl+h", "delete character backward"),
203
204            delete_character_forward: key::Binding::new(vec![
205                KeyPress::from(KeyCode::Delete),
206                KeyPress::from((KeyCode::Char('d'), KeyModifiers::CONTROL)),
207            ])
208            .with_help("delete/ctrl+d", "delete character forward"),
209
210            line_start: key::Binding::new(vec![
211                KeyPress::from(KeyCode::Home),
212                KeyPress::from((KeyCode::Char('a'), KeyModifiers::CONTROL)),
213            ])
214            .with_help("home/ctrl+a", "line start"),
215
216            line_end: key::Binding::new(vec![
217                KeyPress::from(KeyCode::End),
218                KeyPress::from((KeyCode::Char('e'), KeyModifiers::CONTROL)),
219            ])
220            .with_help("end/ctrl+e", "line end"),
221
222            paste: key::Binding::new(vec![KeyPress::from((
223                KeyCode::Char('v'),
224                KeyModifiers::CONTROL,
225            ))])
226            .with_help("ctrl+v", "paste"),
227
228            input_begin: key::Binding::new(vec![
229                KeyPress::from((KeyCode::Char('<'), KeyModifiers::ALT)),
230                KeyPress::from((KeyCode::Home, KeyModifiers::CONTROL)),
231            ])
232            .with_help("alt+</ctrl+home", "input begin"),
233
234            input_end: key::Binding::new(vec![
235                KeyPress::from((KeyCode::Char('>'), KeyModifiers::ALT)),
236                KeyPress::from((KeyCode::End, KeyModifiers::CONTROL)),
237            ])
238            .with_help("alt+>/ctrl+end", "input end"),
239
240            capitalize_word_forward: key::Binding::new(vec![KeyPress::from((
241                KeyCode::Char('c'),
242                KeyModifiers::ALT,
243            ))])
244            .with_help("alt+c", "capitalize word forward"),
245
246            lowercase_word_forward: key::Binding::new(vec![KeyPress::from((
247                KeyCode::Char('l'),
248                KeyModifiers::ALT,
249            ))])
250            .with_help("alt+l", "lowercase word forward"),
251
252            uppercase_word_forward: key::Binding::new(vec![KeyPress::from((
253                KeyCode::Char('u'),
254                KeyModifiers::ALT,
255            ))])
256            .with_help("alt+u", "uppercase word forward"),
257
258            transpose_character_backward: key::Binding::new(vec![KeyPress::from((
259                KeyCode::Char('t'),
260                KeyModifiers::CONTROL,
261            ))])
262            .with_help("ctrl+t", "transpose character backward"),
263        }
264    }
265}
266
267/// Style that will be applied to the text area - direct port from Go
268#[derive(Debug, Clone)]
269pub struct TextareaStyle {
270    /// Base style applied to the entire textarea view.
271    pub base: Style,
272    /// Style for the current cursor line background.
273    pub cursor_line: Style,
274    /// Style for the current line number.
275    pub cursor_line_number: Style,
276    /// Style for the end-of-buffer character.
277    pub end_of_buffer: Style,
278    /// Style for line numbers generally.
279    pub line_number: Style,
280    /// Style for placeholder text.
281    pub placeholder: Style,
282    /// Style for the prompt prefix.
283    pub prompt: Style,
284    /// Style for regular text content.
285    pub text: Style,
286}
287
288impl TextareaStyle {
289    /// Computed cursor line style
290    pub fn computed_cursor_line(&self) -> Style {
291        self.cursor_line
292            .clone()
293            .inherit(self.base.clone())
294            .inline(true)
295    }
296
297    /// Computed cursor line number style  
298    pub fn computed_cursor_line_number(&self) -> Style {
299        self.cursor_line_number
300            .clone()
301            .inherit(self.cursor_line.clone())
302            .inherit(self.base.clone())
303            .inline(true)
304    }
305
306    /// Computed end of buffer style
307    pub fn computed_end_of_buffer(&self) -> Style {
308        self.end_of_buffer
309            .clone()
310            .inherit(self.base.clone())
311            .inline(true)
312    }
313
314    /// Computed line number style
315    pub fn computed_line_number(&self) -> Style {
316        self.line_number
317            .clone()
318            .inherit(self.base.clone())
319            .inline(true)
320    }
321
322    /// Computed placeholder style
323    pub fn computed_placeholder(&self) -> Style {
324        self.placeholder
325            .clone()
326            .inherit(self.base.clone())
327            .inline(true)
328    }
329
330    /// Computed prompt style
331    pub fn computed_prompt(&self) -> Style {
332        self.prompt.clone().inherit(self.base.clone()).inline(true)
333    }
334
335    /// Computed text style
336    pub fn computed_text(&self) -> Style {
337        self.text.clone().inherit(self.base.clone()).inline(true)
338    }
339}
340
341/// Create default focused style - matching Go DefaultStyles with adaptive colors
342pub fn default_focused_style() -> TextareaStyle {
343    use lipgloss::AdaptiveColor;
344
345    TextareaStyle {
346        base: Style::new(),
347        cursor_line: Style::new().background(AdaptiveColor {
348            Light: "255",
349            Dark: "0",
350        }),
351        cursor_line_number: Style::new().foreground(AdaptiveColor {
352            Light: "240",
353            Dark: "",
354        }),
355        end_of_buffer: Style::new().foreground(AdaptiveColor {
356            Light: "254",
357            Dark: "0",
358        }),
359        line_number: Style::new().foreground(AdaptiveColor {
360            Light: "249",
361            Dark: "7",
362        }),
363        placeholder: Style::new().foreground(Color::from("240")),
364        prompt: Style::new().foreground(Color::from("7")),
365        text: Style::new(),
366    }
367}
368
369/// Create default blurred style - matching Go DefaultStyles with adaptive colors
370pub fn default_blurred_style() -> TextareaStyle {
371    use lipgloss::AdaptiveColor;
372
373    TextareaStyle {
374        base: Style::new(),
375        cursor_line: Style::new().foreground(AdaptiveColor {
376            Light: "245",
377            Dark: "7",
378        }),
379        cursor_line_number: Style::new().foreground(AdaptiveColor {
380            Light: "249",
381            Dark: "7",
382        }),
383        end_of_buffer: Style::new().foreground(AdaptiveColor {
384            Light: "254",
385            Dark: "0",
386        }),
387        line_number: Style::new().foreground(AdaptiveColor {
388            Light: "249",
389            Dark: "7",
390        }),
391        placeholder: Style::new().foreground(Color::from("240")),
392        prompt: Style::new().foreground(Color::from("7")),
393        text: Style::new().foreground(AdaptiveColor {
394            Light: "245",
395            Dark: "7",
396        }),
397    }
398}
399
400/// Create default key map for textarea - function version
401pub fn default_key_map() -> TextareaKeyMap {
402    TextareaKeyMap::default()
403}
404
405/// Check if a character is a word boundary
406pub fn is_word_boundary(ch: char) -> bool {
407    ch.is_whitespace() || ch.is_ascii_punctuation()
408}
409
410/// Find the start of the current word
411pub fn word_start(text: &str, pos: usize) -> usize {
412    if pos == 0 {
413        return 0;
414    }
415
416    let chars: Vec<char> = text.chars().collect();
417    let mut i = pos.saturating_sub(1);
418
419    while i > 0 && !is_word_boundary(chars[i]) {
420        i -= 1;
421    }
422
423    if i > 0 && is_word_boundary(chars[i]) {
424        i + 1
425    } else {
426        i
427    }
428}
429
430/// Find the end of the current word
431pub fn word_end(text: &str, pos: usize) -> usize {
432    let chars: Vec<char> = text.chars().collect();
433    let mut i = pos;
434
435    while i < chars.len() && !is_word_boundary(chars[i]) {
436        i += 1;
437    }
438
439    i
440}
441
442/// Utility function to clamp a value between bounds
443pub fn clamp<T: Ord>(value: T, min: T, max: T) -> T {
444    if value < min {
445        min
446    } else if value > max {
447        max
448    } else {
449        value
450    }
451}
452
453/// Repeat spaces as characters
454pub fn repeat_spaces(n: usize) -> Vec<char> {
455    std::iter::repeat_n(' ', n).collect()
456}