bubbletea-widgets 0.1.12

A collection of reusable TUI components for building terminal applications with bubbletea-rs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
//! Helper functions and types for the textarea component.
//!
//! This module exposes:
//! - **`TextareaKeyMap`**: default key bindings (movement, deletion, clipboard, advanced word ops).
//! - **`TextareaStyle`**: theming knobs for base/text, prompt, line numbers, cursor line, etc.
//! - Utility helpers used by `textarea::Model` (word boundaries, clamping, spacing).
//!
//! ### Styling
//! Use `default_focused_style()` and `default_blurred_style()` for presets that
//! mimic the upstream Go defaults, or construct a custom `TextareaStyle` and
//! assign it to the model.
//!
//! ```rust
//! use bubbletea_widgets::textarea::{helpers::TextareaStyle, new};
//! use lipgloss_extras::prelude::*;
//!
//! let mut model = new();
//! let custom = TextareaStyle {
//!     base: Style::new(),
//!     text: Style::new(),
//!     prompt: Style::new().foreground("#04b575"),
//!     line_number: Style::new().foreground("#666666"),
//!     cursor_line: Style::new().background("#2a2a2a"),
//!     cursor_line_number: Style::new().foreground("#666666"),
//!     end_of_buffer: Style::new().foreground("#3c3c3c"),
//!     placeholder: Style::new().foreground("#666666"),
//! };
//! model.focused_style = custom.clone();
//! model.blurred_style = custom;
//! ```

use crate::key::{self, KeyPress};
use crossterm::event::{KeyCode, KeyModifiers};
use lipgloss_extras::prelude::*;

/// Complete KeyMap for textarea component - direct port from Go
#[derive(Debug, Clone)]
pub struct TextareaKeyMap {
    /// Move cursor one character left.
    pub character_backward: key::Binding,
    /// Move cursor one character right.
    pub character_forward: key::Binding,
    /// Delete from cursor to end of line.
    pub delete_after_cursor: key::Binding,
    /// Delete from start of line to cursor.
    pub delete_before_cursor: key::Binding,
    /// Delete one character backward.
    pub delete_character_backward: key::Binding,
    /// Delete one character forward.
    pub delete_character_forward: key::Binding,
    /// Delete previous word.
    pub delete_word_backward: key::Binding,
    /// Delete next word.
    pub delete_word_forward: key::Binding,
    /// Insert newline.
    pub insert_newline: key::Binding,
    /// Move cursor to end of line.
    pub line_end: key::Binding,
    /// Move cursor to next visual line.
    pub line_next: key::Binding,
    /// Move cursor to previous visual line.
    pub line_previous: key::Binding,
    /// Move cursor to start of line.
    pub line_start: key::Binding,
    /// Paste from clipboard.
    pub paste: key::Binding,
    /// Move one word left.
    pub word_backward: key::Binding,
    /// Move one word right.
    pub word_forward: key::Binding,
    /// Move to beginning of input.
    pub input_begin: key::Binding,
    /// Move to end of input.
    pub input_end: key::Binding,
    // Advanced bindings from Go
    /// Uppercase the word to the right of the cursor.
    pub uppercase_word_forward: key::Binding,
    /// Lowercase the word to the right of the cursor.
    pub lowercase_word_forward: key::Binding,
    /// Capitalize the word to the right of the cursor.
    pub capitalize_word_forward: key::Binding,
    /// Transpose the character to the left with the current one.
    pub transpose_character_backward: key::Binding,
}

/// Implementation of KeyMap trait for help integration
impl crate::key::KeyMap for TextareaKeyMap {
    fn short_help(&self) -> Vec<&key::Binding> {
        vec![
            &self.character_backward,
            &self.character_forward,
            &self.line_next,
            &self.line_previous,
            &self.insert_newline,
            &self.delete_character_backward,
        ]
    }

    fn full_help(&self) -> Vec<Vec<&key::Binding>> {
        vec![
            vec![
                &self.character_backward,
                &self.character_forward,
                &self.word_backward,
                &self.word_forward,
            ],
            vec![
                &self.line_next,
                &self.line_previous,
                &self.line_start,
                &self.line_end,
            ],
            vec![
                &self.insert_newline,
                &self.delete_character_backward,
                &self.delete_character_forward,
                &self.paste,
            ],
            vec![
                &self.delete_word_backward,
                &self.delete_word_forward,
                &self.delete_after_cursor,
                &self.delete_before_cursor,
            ],
        ]
    }
}

impl Default for TextareaKeyMap {
    fn default() -> Self {
        Self {
            character_forward: key::Binding::new(vec![
                KeyPress::from(KeyCode::Right),
                KeyPress::from((KeyCode::Char('f'), KeyModifiers::CONTROL)),
            ])
            .with_help("→/ctrl+f", "character forward"),

            character_backward: key::Binding::new(vec![
                KeyPress::from(KeyCode::Left),
                KeyPress::from((KeyCode::Char('b'), KeyModifiers::CONTROL)),
            ])
            .with_help("←/ctrl+b", "character backward"),

            word_forward: key::Binding::new(vec![
                KeyPress::from((KeyCode::Right, KeyModifiers::ALT)),
                KeyPress::from((KeyCode::Char('f'), KeyModifiers::ALT)),
            ])
            .with_help("alt+→/alt+f", "word forward"),

            word_backward: key::Binding::new(vec![
                KeyPress::from((KeyCode::Left, KeyModifiers::ALT)),
                KeyPress::from((KeyCode::Char('b'), KeyModifiers::ALT)),
            ])
            .with_help("alt+←/alt+b", "word backward"),

            line_next: key::Binding::new(vec![
                KeyPress::from(KeyCode::Down),
                KeyPress::from((KeyCode::Char('n'), KeyModifiers::CONTROL)),
            ])
            .with_help("↓/ctrl+n", "next line"),

            line_previous: key::Binding::new(vec![
                KeyPress::from(KeyCode::Up),
                KeyPress::from((KeyCode::Char('p'), KeyModifiers::CONTROL)),
            ])
            .with_help("↑/ctrl+p", "previous line"),

            delete_word_backward: key::Binding::new(vec![
                KeyPress::from((KeyCode::Backspace, KeyModifiers::ALT)),
                KeyPress::from((KeyCode::Char('w'), KeyModifiers::CONTROL)),
            ])
            .with_help("alt+backspace/ctrl+w", "delete word backward"),

            delete_word_forward: key::Binding::new(vec![
                KeyPress::from((KeyCode::Delete, KeyModifiers::ALT)),
                KeyPress::from((KeyCode::Char('d'), KeyModifiers::ALT)),
            ])
            .with_help("alt+delete/alt+d", "delete word forward"),

            delete_after_cursor: key::Binding::new(vec![KeyPress::from((
                KeyCode::Char('k'),
                KeyModifiers::CONTROL,
            ))])
            .with_help("ctrl+k", "delete after cursor"),

            delete_before_cursor: key::Binding::new(vec![KeyPress::from((
                KeyCode::Char('u'),
                KeyModifiers::CONTROL,
            ))])
            .with_help("ctrl+u", "delete before cursor"),

            insert_newline: key::Binding::new(vec![
                KeyPress::from(KeyCode::Enter),
                KeyPress::from((KeyCode::Char('m'), KeyModifiers::CONTROL)),
            ])
            .with_help("enter/ctrl+m", "insert newline"),

            delete_character_backward: key::Binding::new(vec![
                KeyPress::from(KeyCode::Backspace),
                KeyPress::from((KeyCode::Char('h'), KeyModifiers::CONTROL)),
            ])
            .with_help("backspace/ctrl+h", "delete character backward"),

            delete_character_forward: key::Binding::new(vec![
                KeyPress::from(KeyCode::Delete),
                KeyPress::from((KeyCode::Char('d'), KeyModifiers::CONTROL)),
            ])
            .with_help("delete/ctrl+d", "delete character forward"),

            line_start: key::Binding::new(vec![
                KeyPress::from(KeyCode::Home),
                KeyPress::from((KeyCode::Char('a'), KeyModifiers::CONTROL)),
            ])
            .with_help("home/ctrl+a", "line start"),

            line_end: key::Binding::new(vec![
                KeyPress::from(KeyCode::End),
                KeyPress::from((KeyCode::Char('e'), KeyModifiers::CONTROL)),
            ])
            .with_help("end/ctrl+e", "line end"),

            paste: key::Binding::new(vec![KeyPress::from((
                KeyCode::Char('v'),
                KeyModifiers::CONTROL,
            ))])
            .with_help("ctrl+v", "paste"),

            input_begin: key::Binding::new(vec![
                KeyPress::from((KeyCode::Char('<'), KeyModifiers::ALT)),
                KeyPress::from((KeyCode::Home, KeyModifiers::CONTROL)),
            ])
            .with_help("alt+</ctrl+home", "input begin"),

            input_end: key::Binding::new(vec![
                KeyPress::from((KeyCode::Char('>'), KeyModifiers::ALT)),
                KeyPress::from((KeyCode::End, KeyModifiers::CONTROL)),
            ])
            .with_help("alt+>/ctrl+end", "input end"),

            capitalize_word_forward: key::Binding::new(vec![KeyPress::from((
                KeyCode::Char('c'),
                KeyModifiers::ALT,
            ))])
            .with_help("alt+c", "capitalize word forward"),

            lowercase_word_forward: key::Binding::new(vec![KeyPress::from((
                KeyCode::Char('l'),
                KeyModifiers::ALT,
            ))])
            .with_help("alt+l", "lowercase word forward"),

            uppercase_word_forward: key::Binding::new(vec![KeyPress::from((
                KeyCode::Char('u'),
                KeyModifiers::ALT,
            ))])
            .with_help("alt+u", "uppercase word forward"),

            transpose_character_backward: key::Binding::new(vec![KeyPress::from((
                KeyCode::Char('t'),
                KeyModifiers::CONTROL,
            ))])
            .with_help("ctrl+t", "transpose character backward"),
        }
    }
}

/// Style that will be applied to the text area - direct port from Go
#[derive(Debug, Clone)]
pub struct TextareaStyle {
    /// Base style applied to the entire textarea view.
    pub base: Style,
    /// Style for the current cursor line background.
    pub cursor_line: Style,
    /// Style for the current line number.
    pub cursor_line_number: Style,
    /// Style for the end-of-buffer character.
    pub end_of_buffer: Style,
    /// Style for line numbers generally.
    pub line_number: Style,
    /// Style for placeholder text.
    pub placeholder: Style,
    /// Style for the prompt prefix.
    pub prompt: Style,
    /// Style for regular text content.
    pub text: Style,
}

impl TextareaStyle {
    /// Computed cursor line style
    pub fn computed_cursor_line(&self) -> Style {
        self.cursor_line
            .clone()
            .inherit(self.base.clone())
            .inline(true)
    }

    /// Computed cursor line number style  
    pub fn computed_cursor_line_number(&self) -> Style {
        self.cursor_line_number
            .clone()
            .inherit(self.cursor_line.clone())
            .inherit(self.base.clone())
            .inline(true)
    }

    /// Computed end of buffer style
    pub fn computed_end_of_buffer(&self) -> Style {
        self.end_of_buffer
            .clone()
            .inherit(self.base.clone())
            .inline(true)
    }

    /// Computed line number style
    pub fn computed_line_number(&self) -> Style {
        self.line_number
            .clone()
            .inherit(self.base.clone())
            .inline(true)
    }

    /// Computed placeholder style
    pub fn computed_placeholder(&self) -> Style {
        self.placeholder
            .clone()
            .inherit(self.base.clone())
            .inline(true)
    }

    /// Computed prompt style
    pub fn computed_prompt(&self) -> Style {
        self.prompt.clone().inherit(self.base.clone()).inline(true)
    }

    /// Computed text style
    pub fn computed_text(&self) -> Style {
        self.text.clone().inherit(self.base.clone()).inline(true)
    }
}

/// Create default focused style - matching Go DefaultStyles with adaptive colors
pub fn default_focused_style() -> TextareaStyle {
    use lipgloss::AdaptiveColor;

    TextareaStyle {
        base: Style::new(),
        cursor_line: Style::new().background(AdaptiveColor {
            Light: "255",
            Dark: "0",
        }),
        cursor_line_number: Style::new().foreground(AdaptiveColor {
            Light: "240",
            Dark: "",
        }),
        end_of_buffer: Style::new().foreground(AdaptiveColor {
            Light: "254",
            Dark: "0",
        }),
        line_number: Style::new().foreground(AdaptiveColor {
            Light: "249",
            Dark: "7",
        }),
        placeholder: Style::new().foreground(Color::from("240")),
        prompt: Style::new().foreground(Color::from("7")),
        text: Style::new(),
    }
}

/// Create default blurred style - matching Go DefaultStyles with adaptive colors
pub fn default_blurred_style() -> TextareaStyle {
    use lipgloss::AdaptiveColor;

    TextareaStyle {
        base: Style::new(),
        cursor_line: Style::new().foreground(AdaptiveColor {
            Light: "245",
            Dark: "7",
        }),
        cursor_line_number: Style::new().foreground(AdaptiveColor {
            Light: "249",
            Dark: "7",
        }),
        end_of_buffer: Style::new().foreground(AdaptiveColor {
            Light: "254",
            Dark: "0",
        }),
        line_number: Style::new().foreground(AdaptiveColor {
            Light: "249",
            Dark: "7",
        }),
        placeholder: Style::new().foreground(Color::from("240")),
        prompt: Style::new().foreground(Color::from("7")),
        text: Style::new().foreground(AdaptiveColor {
            Light: "245",
            Dark: "7",
        }),
    }
}

/// Create default key map for textarea - function version
pub fn default_key_map() -> TextareaKeyMap {
    TextareaKeyMap::default()
}

/// Check if a character is a word boundary
pub fn is_word_boundary(ch: char) -> bool {
    ch.is_whitespace() || ch.is_ascii_punctuation()
}

/// Find the start of the current word
pub fn word_start(text: &str, pos: usize) -> usize {
    if pos == 0 {
        return 0;
    }

    let chars: Vec<char> = text.chars().collect();
    let mut i = pos.saturating_sub(1);

    while i > 0 && !is_word_boundary(chars[i]) {
        i -= 1;
    }

    if i > 0 && is_word_boundary(chars[i]) {
        i + 1
    } else {
        i
    }
}

/// Find the end of the current word
pub fn word_end(text: &str, pos: usize) -> usize {
    let chars: Vec<char> = text.chars().collect();
    let mut i = pos;

    while i < chars.len() && !is_word_boundary(chars[i]) {
        i += 1;
    }

    i
}

/// Utility function to clamp a value between bounds
pub fn clamp<T: Ord>(value: T, min: T, max: T) -> T {
    if value < min {
        min
    } else if value > max {
        max
    } else {
        value
    }
}

/// Repeat spaces as characters
pub fn repeat_spaces(n: usize) -> Vec<char> {
    std::iter::repeat_n(' ', n).collect()
}