Skip to main content

rusty_bubbles/
textarea.rs

1//! Cleanroom Rust port of upstream Go source file: `textarea/textarea.go`
2//! Upstream Target Tag / Version: `v2.1.0`
3//!
4//! <public-docs>
5//! # TextArea
6//!
7//! A multi-line text input component for Bubble Tea applications.
8//!
9//! The placeholder word/hard wrapping is a char-based port of the
10//! `charmbracelet/x/ansi` `Wordwrap`/`Hardwrap` algorithms.
11//! </public-docs>
12
13use crate::cursor;
14use crate::internal::clipboard;
15use crate::internal::memoization;
16use crate::internal::runeutil::{self, Sanitizer};
17use crate::key::{self, Binding};
18use crate::viewport;
19use rusty_bubbletea::cursor::CursorShape;
20use rusty_bubbletea::key::KeyPressMsg;
21use rusty_bubbletea::model::{Cmd, Msg};
22use rusty_bubbletea::paste::PasteMsg;
23use rusty_lipgloss::{self, Color, Style};
24use std::fmt;
25use std::time::Duration;
26use unicode_width::UnicodeWidthChar;
27
28const MIN_HEIGHT: usize = 1;
29const DEFAULT_HEIGHT: usize = 6;
30const DEFAULT_WIDTH: usize = 40;
31const DEFAULT_CHAR_LIMIT: usize = 0; // no limit
32const DEFAULT_MAX_HEIGHT: usize = 99;
33const DEFAULT_MAX_WIDTH: usize = 500;
34
35// XXX: in v2, make max lines dynamic and default max lines configurable.
36const MAX_LINES: usize = 10000;
37
38/// Internal messages for clipboard operations.
39#[derive(Debug)]
40pub struct PasteMsgInternal(pub String);
41
42#[derive(Debug)]
43pub struct PasteErrMsg(pub String);
44
45/// KeyMap is the key bindings for different actions within the textarea.
46#[derive(Debug, Clone)]
47pub struct KeyMap {
48    /// CharacterBackward binding.
49    pub character_backward: Binding,
50    /// CharacterForward binding.
51    pub character_forward: Binding,
52    /// DeleteAfterCursor binding.
53    pub delete_after_cursor: Binding,
54    /// DeleteBeforeCursor binding.
55    pub delete_before_cursor: Binding,
56    /// DeleteCharacterBackward binding.
57    pub delete_character_backward: Binding,
58    /// DeleteCharacterForward binding.
59    pub delete_character_forward: Binding,
60    /// DeleteWordBackward binding.
61    pub delete_word_backward: Binding,
62    /// DeleteWordForward binding.
63    pub delete_word_forward: Binding,
64    /// InsertNewline binding.
65    pub insert_newline: Binding,
66    /// LineEnd binding.
67    pub line_end: Binding,
68    /// LineNext binding.
69    pub line_next: Binding,
70    /// LinePrevious binding.
71    pub line_previous: Binding,
72    /// LineStart binding.
73    pub line_start: Binding,
74    /// PageUp binding.
75    pub page_up: Binding,
76    /// PageDown binding.
77    pub page_down: Binding,
78    /// Paste binding.
79    pub paste: Binding,
80    /// WordBackward binding.
81    pub word_backward: Binding,
82    /// WordForward binding.
83    pub word_forward: Binding,
84    /// InputBegin binding.
85    pub input_begin: Binding,
86    /// InputEnd binding.
87    pub input_end: Binding,
88
89    /// UppercaseWordForward binding.
90    pub uppercase_word_forward: Binding,
91    /// LowercaseWordForward binding.
92    pub lowercase_word_forward: Binding,
93    /// CapitalizeWordForward binding.
94    pub capitalize_word_forward: Binding,
95
96    /// TransposeCharacterBackward binding.
97    pub transpose_character_backward: Binding,
98}
99
100/// DefaultKeyMap returns the default set of key bindings for navigating and
101/// acting upon the textarea.
102pub fn default_key_map() -> KeyMap {
103    KeyMap {
104        character_forward: key::new_binding(vec![
105            key::with_keys(&["right", "ctrl+f"]),
106            key::with_help("right", "character forward"),
107        ]),
108        character_backward: key::new_binding(vec![
109            key::with_keys(&["left", "ctrl+b"]),
110            key::with_help("left", "character backward"),
111        ]),
112        word_forward: key::new_binding(vec![
113            key::with_keys(&["alt+right", "alt+f"]),
114            key::with_help("alt+right", "word forward"),
115        ]),
116        word_backward: key::new_binding(vec![
117            key::with_keys(&["alt+left", "alt+b"]),
118            key::with_help("alt+left", "word backward"),
119        ]),
120        line_next: key::new_binding(vec![
121            key::with_keys(&["down", "ctrl+n"]),
122            key::with_help("down", "next line"),
123        ]),
124        line_previous: key::new_binding(vec![
125            key::with_keys(&["up", "ctrl+p"]),
126            key::with_help("up", "previous line"),
127        ]),
128        delete_word_backward: key::new_binding(vec![
129            key::with_keys(&["alt+backspace", "ctrl+w"]),
130            key::with_help("alt+backspace", "delete word backward"),
131        ]),
132        delete_word_forward: key::new_binding(vec![
133            key::with_keys(&["alt+delete", "alt+d"]),
134            key::with_help("alt+delete", "delete word forward"),
135        ]),
136        delete_after_cursor: key::new_binding(vec![
137            key::with_keys(&["ctrl+k"]),
138            key::with_help("ctrl+k", "delete after cursor"),
139        ]),
140        delete_before_cursor: key::new_binding(vec![
141            key::with_keys(&["ctrl+u"]),
142            key::with_help("ctrl+u", "delete before cursor"),
143        ]),
144        insert_newline: key::new_binding(vec![
145            key::with_keys(&["enter", "ctrl+m"]),
146            key::with_help("enter", "insert newline"),
147        ]),
148        delete_character_backward: key::new_binding(vec![
149            key::with_keys(&["backspace", "ctrl+h"]),
150            key::with_help("backspace", "delete character backward"),
151        ]),
152        delete_character_forward: key::new_binding(vec![
153            key::with_keys(&["delete", "ctrl+d"]),
154            key::with_help("delete", "delete character forward"),
155        ]),
156        line_start: key::new_binding(vec![
157            key::with_keys(&["home", "ctrl+a"]),
158            key::with_help("home", "line start"),
159        ]),
160        line_end: key::new_binding(vec![
161            key::with_keys(&["end", "ctrl+e"]),
162            key::with_help("end", "line end"),
163        ]),
164        page_up: key::new_binding(vec![
165            key::with_keys(&["pgup"]),
166            key::with_help("pgup", "page up"),
167        ]),
168        page_down: key::new_binding(vec![
169            key::with_keys(&["pgdown"]),
170            key::with_help("pgdown", "page down"),
171        ]),
172        paste: key::new_binding(vec![
173            key::with_keys(&["ctrl+v"]),
174            key::with_help("ctrl+v", "paste"),
175        ]),
176        input_begin: key::new_binding(vec![
177            key::with_keys(&["alt+<", "ctrl+home"]),
178            key::with_help("alt+<", "input begin"),
179        ]),
180        input_end: key::new_binding(vec![
181            key::with_keys(&["alt+>", "ctrl+end"]),
182            key::with_help("alt+>", "input end"),
183        ]),
184        capitalize_word_forward: key::new_binding(vec![
185            key::with_keys(&["alt+c"]),
186            key::with_help("alt+c", "capitalize word forward"),
187        ]),
188        lowercase_word_forward: key::new_binding(vec![
189            key::with_keys(&["alt+l"]),
190            key::with_help("alt+l", "lowercase word forward"),
191        ]),
192        uppercase_word_forward: key::new_binding(vec![
193            key::with_keys(&["alt+u"]),
194            key::with_help("alt+u", "uppercase word forward"),
195        ]),
196        transpose_character_backward: key::new_binding(vec![
197            key::with_keys(&["ctrl+t"]),
198            key::with_help("ctrl+t", "transpose character backward"),
199        ]),
200    }
201}
202
203/// LineInfo is a helper for keeping track of line information regarding
204/// soft-wrapped lines.
205#[derive(Debug, Clone, Copy)]
206pub struct LineInfo {
207    /// Width is the number of columns in the line.
208    pub width: usize,
209
210    /// CharWidth is the number of characters in the line to account for
211    /// double-width runes.
212    pub char_width: usize,
213
214    /// Height is the number of rows in the line.
215    pub height: usize,
216
217    /// StartColumn is the index of the first column of the line.
218    pub start_column: usize,
219
220    /// ColumnOffset is the number of columns that the cursor is offset from
221    /// the start of the line.
222    pub column_offset: usize,
223
224    /// RowOffset is the number of rows that the cursor is offset from the
225    /// start of the line.
226    pub row_offset: usize,
227
228    /// CharOffset is the number of characters that the cursor is offset
229    /// from the start of the line. This will generally be equivalent to
230    /// ColumnOffset, but will be different if there are double-width runes
231    /// before the cursor.
232    pub char_offset: usize,
233}
234
235/// PromptInfo is a struct that can be used to store information about the
236/// prompt.
237#[derive(Debug, Clone, Copy)]
238pub struct PromptInfo {
239    /// The line number of the prompt.
240    pub line_number: usize,
241    /// Whether the textarea is focused.
242    pub focused: bool,
243}
244
245/// CursorStyle is the style for real and virtual cursors.
246#[derive(Debug, Clone)]
247pub struct CursorStyle {
248    /// Style styles the cursor block. For real cursors, the foreground
249    /// color set here will be used as the cursor color.
250    pub color: Color,
251
252    /// Shape is the cursor shape. The following shapes are available:
253    ///
254    /// - [`CursorShape::CursorBlock`]
255    /// - [`CursorShape::CursorUnderline`]
256    /// - [`CursorShape::CursorBar`]
257    pub shape: CursorShape,
258
259    /// CursorBlink determines whether or not the cursor should blink.
260    pub blink: bool,
261
262    /// BlinkSpeed is the speed at which the virtual cursor blinks. This has
263    /// no effect on real cursors as well as no effect if the cursor is set
264    /// not to blink.
265    pub blink_speed: Duration,
266}
267
268/// Styles are the styles for the textarea, separated into focused and
269/// blurred states. The appropriate styles will be chosen based on the focus
270/// state of the textarea.
271#[derive(Debug, Clone)]
272pub struct Styles {
273    /// The styles used when focused.
274    pub focused: StyleState,
275    /// The styles used when blurred.
276    pub blurred: StyleState,
277    /// The cursor style.
278    pub cursor: CursorStyle,
279}
280
281/// StyleState that will be applied to the text area.
282///
283/// StyleState can be applied to focused and unfocused states to change the
284/// styles depending on the focus state.
285#[derive(Debug, Clone)]
286pub struct StyleState {
287    /// The base style.
288    pub base: Style,
289    /// The text style.
290    pub text: Style,
291    /// The line number style.
292    pub line_number: Style,
293    /// The cursor line number style.
294    pub cursor_line_number: Style,
295    /// The cursor line style.
296    pub cursor_line: Style,
297    /// The end-of-buffer style.
298    pub end_of_buffer: Style,
299    /// The placeholder style.
300    pub placeholder: Style,
301    /// The prompt style.
302    pub prompt: Style,
303}
304
305impl StyleState {
306    fn computed_cursor_line(&self) -> Style {
307        self.cursor_line.clone().inherit(&self.base).inline(true)
308    }
309
310    fn computed_cursor_line_number(&self) -> Style {
311        self.cursor_line_number
312            .clone()
313            .inherit(&self.cursor_line)
314            .inherit(&self.base)
315            .inline(true)
316    }
317
318    fn computed_end_of_buffer(&self) -> Style {
319        self.end_of_buffer.clone().inherit(&self.base).inline(true)
320    }
321
322    fn computed_line_number(&self) -> Style {
323        self.line_number.clone().inherit(&self.base).inline(true)
324    }
325
326    fn computed_placeholder(&self) -> Style {
327        self.placeholder.clone().inherit(&self.base).inline(true)
328    }
329
330    fn computed_prompt(&self) -> Style {
331        self.prompt.clone().inherit(&self.base).inline(true)
332    }
333
334    fn computed_text(&self) -> Style {
335        self.text.clone().inherit(&self.base).inline(true)
336    }
337}
338
339/// Model is the Bubble Tea model for this text area element.
340pub struct Model {
341    /// The validation error, if any.
342    pub err: Option<String>,
343
344    /// cache is the memoization cache for wrapped lines.
345    cache: memoization::MemoCache<Vec<Vec<char>>>,
346
347    /// Prompt is printed at the beginning of each line.
348    pub prompt: String,
349
350    /// Placeholder is the text displayed when the user hasn't entered
351    /// anything yet.
352    pub placeholder: String,
353
354    /// ShowLineNumbers, if enabled, causes line numbers to be printed after
355    /// the prompt.
356    pub show_line_numbers: bool,
357
358    /// EndOfBufferCharacter is displayed at the end of the input.
359    pub end_of_buffer_character: char,
360
361    /// KeyMap encodes the keybindings recognized by the widget.
362    pub key_map: KeyMap,
363
364    /// virtualCursor manages the virtual cursor.
365    pub virtual_cursor: cursor::Model,
366
367    /// CharLimit is the maximum number of characters this input element
368    /// will accept. If 0 or less, there's no limit.
369    pub char_limit: usize,
370
371    /// MaxHeight is the maximum height of the text area in rows. If 0 or
372    /// less, there's no limit.
373    pub max_height: usize,
374
375    /// MaxWidth is the maximum width of the text area in columns. If 0 or
376    /// less, there's no limit.
377    pub max_width: usize,
378
379    /// DynamicHeight, when true, causes the textarea to automatically grow
380    /// and shrink its height to fit the content. The height is clamped
381    /// between MinHeight and MaxHeight.
382    pub dynamic_height: bool,
383
384    /// MinHeight is the minimum height of the text area in rows when
385    /// DynamicHeight is enabled. If 0 or less, defaults to 1.
386    pub min_height: usize,
387
388    /// MaxContentHeight is the maximum content height in visual rows
389    /// (accounting for soft wraps). When 0, the content guard falls back to
390    /// the legacy MaxHeight behavior.
391    pub max_content_height: usize,
392
393    /// Styling. Styles are defined in [`Styles`].
394    pub styles: Styles,
395
396    /// useVirtualCursor determines whether or not to use the virtual cursor.
397    pub use_virtual_cursor: bool,
398
399    /// If prompt_func is set, it replaces Prompt as a generator for prompt
400    /// strings at the beginning of each line.
401    pub prompt_func: Option<Box<dyn Fn(PromptInfo) -> String + Send + Sync>>,
402
403    /// prompt_width is the width of the prompt.
404    pub prompt_width: usize,
405
406    /// width is the maximum number of characters that can be displayed at
407    /// once.
408    pub width: usize,
409
410    /// height is the maximum number of lines that can be displayed at once.
411    pub height: usize,
412
413    /// Underlying text value.
414    value: Vec<Vec<char>>,
415
416    /// focus indicates whether user input focus should be on this input
417    /// component.
418    pub focus: bool,
419
420    /// Cursor column.
421    col: usize,
422
423    /// Cursor row.
424    row: usize,
425
426    /// Last character offset, used to maintain state when the cursor is
427    /// moved vertically.
428    last_char_offset: usize,
429
430    /// viewport is the vertically-scrollable viewport of the multi-line
431    /// text input.
432    viewport: viewport::Model,
433
434    /// rune sanitizer for input.
435    rsan: Option<runeutil::Sanitizer_>,
436}
437
438impl fmt::Debug for Model {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        f.debug_struct("textarea::Model")
441            .field("focus", &self.focus)
442            .field("row", &self.row)
443            .field("col", &self.col)
444            .field("lines", &self.value.len())
445            .finish()
446    }
447}
448
449/// New creates a new model with default settings.
450pub fn new() -> Model {
451    // The upstream textarea disables the viewport's keymap so that typed
452    // characters (e.g. 'l', 'h', 'j', 'k') are not interpreted as viewport
453    // scrolling.
454    let vp = viewport::new(vec![viewport::with_key_map(viewport::KeyMap {
455        page_down: key::new_binding(vec![]),
456        page_up: key::new_binding(vec![]),
457        half_page_up: key::new_binding(vec![]),
458        half_page_down: key::new_binding(vec![]),
459        up: key::new_binding(vec![]),
460        down: key::new_binding(vec![]),
461        left: key::new_binding(vec![]),
462        right: key::new_binding(vec![]),
463    })]);
464
465    let cur = cursor::new();
466
467    let styles = default_dark_styles();
468
469    let mut m = Model {
470        char_limit: DEFAULT_CHAR_LIMIT,
471        max_height: DEFAULT_MAX_HEIGHT,
472        max_width: DEFAULT_MAX_WIDTH,
473        prompt: format!("{} ", rusty_lipgloss::border::thick_border().left),
474        styles,
475        cache: memoization::new_memo_cache(MAX_LINES),
476        end_of_buffer_character: ' ',
477        show_line_numbers: true,
478        use_virtual_cursor: true,
479        virtual_cursor: cur,
480        key_map: default_key_map(),
481
482        value: vec![vec![]; MIN_HEIGHT],
483        focus: false,
484        col: 0,
485        row: 0,
486
487        viewport: vp,
488        err: None,
489        placeholder: String::new(),
490        dynamic_height: false,
491        min_height: 0,
492        max_content_height: 0,
493        prompt_func: None,
494        prompt_width: 0,
495        width: 0,
496        height: 0,
497        last_char_offset: 0,
498        rsan: None,
499    };
500
501    m.set_height(DEFAULT_HEIGHT);
502    m.set_width(DEFAULT_WIDTH);
503
504    m
505}
506
507/// DefaultStyles returns the default styles for focused and blurred states
508/// for the textarea.
509pub fn default_styles(is_dark: bool) -> Styles {
510    let light_dark = rusty_lipgloss::color::light_dark(is_dark);
511
512    Styles {
513        focused: StyleState {
514            base: rusty_lipgloss::new_style(),
515            cursor_line: rusty_lipgloss::new_style()
516                .background_color(light_dark(Color::parse("255"), Color::parse("0"))),
517            cursor_line_number: rusty_lipgloss::new_style()
518                .foreground_color(light_dark(Color::parse("240"), Color::parse("240"))),
519            end_of_buffer: rusty_lipgloss::new_style()
520                .foreground_color(light_dark(Color::parse("254"), Color::parse("0"))),
521            line_number: rusty_lipgloss::new_style()
522                .foreground_color(light_dark(Color::parse("249"), Color::parse("7"))),
523            placeholder: rusty_lipgloss::new_style().foreground_color(Color::parse("240")),
524            prompt: rusty_lipgloss::new_style().foreground_color(Color::parse("7")),
525            text: rusty_lipgloss::new_style(),
526        },
527        blurred: StyleState {
528            base: rusty_lipgloss::new_style(),
529            cursor_line: rusty_lipgloss::new_style()
530                .foreground_color(light_dark(Color::parse("245"), Color::parse("7"))),
531            cursor_line_number: rusty_lipgloss::new_style()
532                .foreground_color(light_dark(Color::parse("249"), Color::parse("7"))),
533            end_of_buffer: rusty_lipgloss::new_style()
534                .foreground_color(light_dark(Color::parse("254"), Color::parse("0"))),
535            line_number: rusty_lipgloss::new_style()
536                .foreground_color(light_dark(Color::parse("249"), Color::parse("7"))),
537            placeholder: rusty_lipgloss::new_style().foreground_color(Color::parse("240")),
538            prompt: rusty_lipgloss::new_style().foreground_color(Color::parse("7")),
539            text: rusty_lipgloss::new_style()
540                .foreground_color(light_dark(Color::parse("245"), Color::parse("7"))),
541        },
542        cursor: CursorStyle {
543            color: Color::parse("7"),
544            shape: CursorShape::CursorBlock,
545            blink: true,
546            blink_speed: Duration::from_millis(530),
547        },
548    }
549}
550
551/// DefaultLightStyles returns the default styles for a light background.
552pub fn default_light_styles() -> Styles {
553    default_styles(false)
554}
555
556/// DefaultDarkStyles returns the default styles for a dark background.
557pub fn default_dark_styles() -> Styles {
558    default_styles(true)
559}
560
561impl Model {
562    /// Styles returns the current styles for the textarea.
563    pub fn styles(&self) -> &Styles {
564        &self.styles
565    }
566
567    /// SetStyles updates styling for the textarea.
568    pub fn set_styles(&mut self, s: Styles) {
569        self.styles = s;
570        self.update_virtual_cursor_style();
571    }
572
573    /// VirtualCursor returns whether or not the virtual cursor is enabled.
574    pub fn virtual_cursor(&self) -> bool {
575        self.use_virtual_cursor
576    }
577
578    /// SetVirtualCursor sets whether or not to use the virtual cursor.
579    pub fn set_virtual_cursor(&mut self, v: bool) {
580        self.use_virtual_cursor = v;
581        self.update_virtual_cursor_style();
582    }
583
584    /// updateVirtualCursorStyle sets styling on the virtual cursor based on
585    /// the textarea's style settings.
586    fn update_virtual_cursor_style(&mut self) {
587        if !self.use_virtual_cursor {
588            self.virtual_cursor.set_mode(cursor::Mode::Hide);
589            return;
590        }
591
592        self.virtual_cursor.style =
593            rusty_lipgloss::new_style().foreground_color(self.styles.cursor.color.clone());
594
595        // By default, the blink speed of the cursor is set to a default
596        // internally.
597        if self.styles.cursor.blink {
598            if self.styles.cursor.blink_speed > Duration::ZERO {
599                self.virtual_cursor.blink_speed = self.styles.cursor.blink_speed;
600            }
601            self.virtual_cursor.set_mode(cursor::Mode::Blink);
602            return;
603        }
604        self.virtual_cursor.set_mode(cursor::Mode::Static);
605    }
606
607    /// SetValue sets the value of the text input.
608    pub fn set_value(&mut self, s: &str) {
609        self.reset();
610        self.insert_string(s);
611        self.recalculate_height();
612    }
613
614    /// InsertString inserts a string at the cursor position.
615    pub fn insert_string(&mut self, s: &str) {
616        self.insert_runes_from_user_input(&s.chars().collect::<Vec<char>>());
617        self.recalculate_height();
618    }
619
620    /// InsertRune inserts a rune at the cursor position.
621    pub fn insert_rune(&mut self, r: char) {
622        self.insert_runes_from_user_input(&[r]);
623        self.recalculate_height();
624    }
625
626    /// insertRunesFromUserInput inserts runes at the current cursor
627    /// position.
628    fn insert_runes_from_user_input(&mut self, input: &[char]) {
629        // Clean up any special characters in the input provided by the
630        // clipboard. This avoids bugs due to e.g. tab characters and whatnot.
631        let mut runes = self.san().sanitize(input);
632
633        if self.char_limit > 0 {
634            let avail_space = self.char_limit - self.length();
635            // If the char limit's been reached, cancel.
636            if avail_space == 0 {
637                return;
638            }
639            // If there's not enough space to paste the whole thing cut the
640            // pasted runes down so they'll fit.
641            if avail_space < runes.len() {
642                runes.truncate(avail_space);
643            }
644        }
645
646        // Split the input into lines.
647        let mut lines: Vec<Vec<char>> = vec![];
648        let mut lstart = 0;
649        for (i, r) in runes.iter().enumerate() {
650            if *r == '\n' {
651                // Queue a line to become a new row in the text area below.
652                lines.push(runes[lstart..i].to_vec());
653                lstart = i + 1;
654            }
655        }
656        if lstart <= runes.len() {
657            // The last line did not end with a newline character. Take it
658            // now.
659            lines.push(runes[lstart..].to_vec());
660        }
661
662        // Obey the maximum line limit.
663        if MAX_LINES > 0 && self.value.len() + lines.len() - 1 > MAX_LINES {
664            let allowed_height = MAX_LINES - self.value.len() + 1;
665            lines.truncate(allowed_height);
666        }
667
668        // Obey MaxContentHeight in visual rows when set.
669        if self.max_content_height > 0 {
670            let budget = self.max_content_height - self.total_visual_lines();
671            // Trim lines from the end until we fit within the budget.
672            while lines.len() > 1 && self.visual_lines_for_insert(&lines) > budget {
673                lines.truncate(lines.len() - 1);
674            }
675            if self.visual_lines_for_insert(&lines) > budget {
676                return;
677            }
678        }
679
680        if lines.is_empty() {
681            // Nothing left to insert.
682            return;
683        }
684
685        // Save the remainder of the original line at the current cursor
686        // position.
687        let tail: Vec<char> = self.value[self.row][self.col..].to_vec();
688
689        // Paste the first line at the current cursor position.
690        let mut first = self.value[self.row][..self.col].to_vec();
691        first.extend_from_slice(&lines[0]);
692        self.value[self.row] = first;
693        self.col += lines[0].len();
694
695        let num_extra_lines = lines.len() - 1;
696        if num_extra_lines > 0 {
697            // Add the new lines.
698            let mut new_grid: Vec<Vec<char>> = self.value.clone();
699            new_grid.resize(self.value.len() + num_extra_lines, vec![]);
700            // Add all the rows that were after the cursor in the original
701            // grid at the end of the new grid.
702            let shift = self.row + 1 + num_extra_lines;
703            for (idx, src) in (self.row + 1..self.value.len()).enumerate() {
704                new_grid[shift + idx] = self.value[src].clone();
705            }
706            self.value = new_grid;
707            // Insert all the new lines in the middle.
708            for l in &lines[1..] {
709                self.row += 1;
710                self.value[self.row] = l.clone();
711                self.col = l.len();
712            }
713        }
714
715        // Finally add the tail at the end of the last line inserted.
716        self.value[self.row].extend_from_slice(&tail);
717
718        self.set_cursor_column(self.col);
719    }
720
721    /// Value returns the value of the text input.
722    /// Value returns the value of the textarea.
723    pub fn value(&self) -> String {
724        if self.value.is_empty() {
725            return String::new();
726        }
727
728        let mut v = String::new();
729        for l in &self.value {
730            v.push_str(&String::from_iter(l.iter()));
731            v.push('\n');
732        }
733
734        v.trim_end_matches('\n').to_string()
735    }
736
737    /// Length returns the number of characters currently in the text input.
738    pub fn length(&self) -> usize {
739        let mut l = 0;
740        for row in &self.value {
741            l += string_width(&String::from_iter(row.iter()));
742        }
743        // We add len(value) to include the newline characters.
744        l + self.value.len() - 1
745    }
746
747    /// LineCount returns the number of lines that are currently in the text
748    /// input.
749    pub fn line_count(&self) -> usize {
750        self.value.len()
751    }
752
753    /// Line returns the 0-indexed row position of the cursor.
754    pub fn line(&self) -> usize {
755        self.row
756    }
757
758    /// Column returns the 0-indexed column position of the cursor.
759    pub fn column(&self) -> usize {
760        self.col
761    }
762
763    /// ScrollYOffset returns the Y offset (top row) index of the current
764    /// view, which can be used to calculate the current scroll position.
765    pub fn scroll_y_offset(&self) -> usize {
766        self.viewport.y_offset()
767    }
768
769    /// SetScrollYOffset sets the Y offset (top row) index of the current
770    /// view, clamping to the viewport's scrollable range.
771    ///
772    /// This is exposed so integration tests can position the view the same
773    /// way the upstream in-package tests use `viewport.SetYOffset`.
774    pub fn set_scroll_y_offset(&mut self, offset: usize) {
775        self.viewport.set_y_offset(offset);
776    }
777
778    /// ScrollPercent returns the amount of the textarea that is currently
779    /// scrolled through, clamped between 0 and 1.
780    pub fn scroll_percent(&self) -> f64 {
781        self.viewport.scroll_percent()
782    }
783
784    /// setCursorLineRelative moves the cursor by the given number of lines.
785    /// Negative values move the cursor up, positive values move the cursor
786    /// down.
787    fn set_cursor_line_relative(&mut self, delta: isize) {
788        if delta == 0 {
789            return;
790        }
791
792        let mut li = self.line_info();
793        let char_offset = self.last_char_offset.max(li.char_offset);
794        self.last_char_offset = char_offset;
795
796        // 2 columns to account for the trailing space wrapping.
797        const TRAILING_SPACE: usize = 2;
798
799        if delta > 0 {
800            // Moving down.
801            for _ in 0..delta {
802                if li.row_offset + 1 >= li.height && self.row < self.value.len() - 1 {
803                    self.row += 1;
804                    self.col = 0;
805                } else {
806                    // Move the cursor to the start of the next virtual line.
807                    self.col = (li.start_column + li.width + TRAILING_SPACE)
808                        .min(self.value[self.row].len().saturating_sub(1));
809                }
810                li = self.line_info();
811            }
812        } else {
813            // Moving up.
814            for _ in 0..(-delta) {
815                if li.row_offset == 0 && self.row > 0 {
816                    self.row -= 1;
817                    self.col = self.value[self.row].len();
818                } else {
819                    // Move the cursor to the end of the previous line.
820                    self.col = li.start_column.saturating_sub(TRAILING_SPACE);
821                }
822                li = self.line_info();
823            }
824        }
825
826        let nli = self.line_info();
827        self.col = nli.start_column;
828
829        if nli.width == 0 {
830            self.reposition_view();
831            return;
832        }
833
834        let mut offset = 0;
835        while offset < char_offset {
836            if self.row >= self.value.len()
837                || self.col >= self.value[self.row].len()
838                || offset >= nli.char_width.saturating_sub(1)
839            {
840                break;
841            }
842            offset += char_width(self.value[self.row][self.col]);
843            self.col += 1;
844        }
845        self.reposition_view();
846    }
847
848    /// CursorDown moves the cursor down by one line.
849    pub fn cursor_down(&mut self) {
850        self.set_cursor_line_relative(1);
851    }
852
853    /// CursorUp moves the cursor up by one line.
854    pub fn cursor_up(&mut self) {
855        self.set_cursor_line_relative(-1);
856    }
857
858    /// CursorPosition returns the current (row, col) of the cursor within
859    /// the (soft-wrapped) value.
860    ///
861    /// This is exposed so integration tests can assert on cursor placement
862    /// the same way the upstream in-package tests do.
863    pub fn cursor_position(&self) -> (usize, usize) {
864        (self.row, self.col)
865    }
866
867    /// SetCursorPosition sets the raw (row, col) of the cursor.
868    ///
869    /// This is exposed so integration tests can position the cursor the
870    /// same way the upstream in-package tests do.
871    pub fn set_cursor_position(&mut self, row: usize, col: usize) {
872        if row >= self.value.len() {
873            return;
874        }
875        self.row = row;
876        self.col = clamp(col, 0, self.value[row].len());
877        self.last_char_offset = 0;
878    }
879
880    /// SetCursorColumn moves the cursor to the given position. If the
881    /// position is out of bounds the cursor will be moved to the start or
882    /// end accordingly.
883    pub fn set_cursor_column(&mut self, col: usize) {
884        self.col = clamp(col, 0, self.value[self.row].len());
885        // Any time that we move the cursor horizontally we need to reset the
886        // last offset so that the horizontal position when navigating is
887        // adjusted.
888        self.last_char_offset = 0;
889    }
890
891    /// CursorStart moves the cursor to the start of the input field.
892    pub fn cursor_start(&mut self) {
893        self.set_cursor_column(0);
894    }
895
896    /// CursorEnd moves the cursor to the end of the input field.
897    pub fn cursor_end(&mut self) {
898        self.set_cursor_column(self.value[self.row].len());
899    }
900
901    /// Focused returns the focus state on the model.
902    pub fn focused(&self) -> bool {
903        self.focus
904    }
905
906    /// activeStyle returns the appropriate set of styles to use depending
907    /// on whether the textarea is focused or blurred.
908    fn active_style(&self) -> &StyleState {
909        if self.focus {
910            &self.styles.focused
911        } else {
912            &self.styles.blurred
913        }
914    }
915
916    /// Focus sets the focus state on the model. When the model is in focus
917    /// it can receive keyboard input and the cursor will be hidden.
918    pub fn focus(&mut self) -> Cmd {
919        self.focus = true;
920        self.virtual_cursor.focus()
921    }
922
923    /// Blur removes the focus state on the model. When the model is blurred
924    /// it can not receive keyboard input and the cursor will be hidden.
925    pub fn blur(&mut self) {
926        self.focus = false;
927        self.virtual_cursor.blur();
928    }
929
930    /// Reset sets the input to its default state with no input.
931    pub fn reset(&mut self) {
932        self.value = vec![vec![]; MIN_HEIGHT];
933        self.col = 0;
934        self.row = 0;
935        self.viewport.goto_top();
936        self.set_cursor_column(0);
937        self.recalculate_height();
938    }
939
940    /// Word returns the word at the cursor position.
941    /// A word is delimited by spaces or line-breaks.
942    pub fn word(&self) -> String {
943        let line = &self.value[self.row];
944        let col = self.col.saturating_sub(1);
945
946        if self.col == 0 {
947            return String::new();
948        }
949
950        // If cursor is beyond the line, return empty string
951        if col >= line.len() {
952            return String::new();
953        }
954
955        // If cursor is on a space, return empty string
956        if line[col].is_whitespace() {
957            return String::new();
958        }
959
960        // Find the start of the word by moving left
961        let mut start = col;
962        while start > 0 && !line[start - 1].is_whitespace() {
963            start -= 1;
964        }
965
966        // Find the end of the word by moving right
967        let mut end = col;
968        while end < line.len() && !line[end].is_whitespace() {
969            end += 1;
970        }
971
972        String::from_iter(line[start..end].iter())
973    }
974
975    /// san initializes or retrieves the rune sanitizer.
976    fn san(&mut self) -> &runeutil::Sanitizer_ {
977        if self.rsan.is_none() {
978            self.rsan = Some(runeutil::new_sanitizer(vec![]));
979        }
980        self.rsan.as_ref().unwrap()
981    }
982
983    /// deleteBeforeCursor deletes all text before the cursor.
984    fn delete_before_cursor(&mut self) {
985        self.value[self.row] = self.value[self.row][self.col..].to_vec();
986        self.set_cursor_column(0);
987    }
988
989    /// deleteAfterCursor deletes all text after the cursor.
990    fn delete_after_cursor(&mut self) {
991        self.value[self.row] = self.value[self.row][..self.col].to_vec();
992        self.set_cursor_column(self.value[self.row].len());
993    }
994
995    /// transposeLeft exchanges the runes at the cursor and immediately
996    /// before. No-op if the cursor is at the beginning of the line.
997    fn transpose_left(&mut self) {
998        if self.col == 0 || self.value[self.row].len() < 2 {
999            return;
1000        }
1001        if self.col >= self.value[self.row].len() {
1002            self.set_cursor_column(self.col - 1);
1003        }
1004        self.value[self.row].swap(self.col - 1, self.col);
1005        if self.col < self.value[self.row].len() {
1006            self.set_cursor_column(self.col + 1);
1007        }
1008    }
1009
1010    /// deleteWordLeft deletes the word left to the cursor.
1011    fn delete_word_left(&mut self) {
1012        if self.col == 0 || self.value[self.row].is_empty() {
1013            return;
1014        }
1015
1016        // Linter note: it's critical that we acquire the initial cursor
1017        // position here prior to altering it via SetCursor() below.
1018        let old_col = self.col;
1019
1020        self.set_cursor_column(self.col - 1);
1021        loop {
1022            if self.col == 0 {
1023                break;
1024            }
1025            if !self.value[self.row][self.col].is_whitespace() {
1026                break;
1027            }
1028            // ignore series of whitespace before cursor
1029            self.set_cursor_column(self.col - 1);
1030        }
1031
1032        while self.col > 0 {
1033            if !self.value[self.row][self.col].is_whitespace() {
1034                self.set_cursor_column(self.col - 1);
1035            } else {
1036                if self.col > 0 {
1037                    // keep the previous space
1038                    self.set_cursor_column(self.col + 1);
1039                }
1040                break;
1041            }
1042        }
1043
1044        if old_col > self.value[self.row].len() {
1045            self.value[self.row] = self.value[self.row][..self.col].to_vec();
1046        } else {
1047            let mut v = self.value[self.row][..self.col].to_vec();
1048            v.extend_from_slice(&self.value[self.row][old_col..]);
1049            self.value[self.row] = v;
1050        }
1051    }
1052
1053    /// deleteWordRight deletes the word right to the cursor.
1054    fn delete_word_right(&mut self) {
1055        if self.col >= self.value[self.row].len() || self.value[self.row].is_empty() {
1056            return;
1057        }
1058
1059        let old_col = self.col;
1060
1061        while self.col < self.value[self.row].len()
1062            && self.value[self.row][self.col].is_whitespace()
1063        {
1064            // ignore series of whitespace after cursor
1065            self.set_cursor_column(self.col + 1);
1066        }
1067
1068        while self.col < self.value[self.row].len() {
1069            if !self.value[self.row][self.col].is_whitespace() {
1070                self.set_cursor_column(self.col + 1);
1071            } else {
1072                break;
1073            }
1074        }
1075
1076        if self.col > self.value[self.row].len() {
1077            self.value[self.row] = self.value[self.row][..old_col].to_vec();
1078        } else {
1079            let mut v = self.value[self.row][..old_col].to_vec();
1080            v.extend_from_slice(&self.value[self.row][self.col..]);
1081            self.value[self.row] = v;
1082        }
1083
1084        self.set_cursor_column(old_col);
1085    }
1086
1087    /// characterRight moves the cursor one character to the right.
1088    fn character_right(&mut self) {
1089        if self.col < self.value[self.row].len() {
1090            self.set_cursor_column(self.col + 1);
1091        } else if self.row < self.value.len() - 1 {
1092            self.row += 1;
1093            self.cursor_start();
1094        }
1095    }
1096
1097    /// characterLeft moves the cursor one character to the left.
1098    fn character_left(&mut self, inside_line: bool) {
1099        if self.col == 0 && self.row != 0 {
1100            self.row -= 1;
1101            self.cursor_end();
1102            if !inside_line {
1103                return;
1104            }
1105        }
1106        if self.col > 0 {
1107            self.set_cursor_column(self.col - 1);
1108        }
1109    }
1110
1111    /// wordLeft moves the cursor one word to the left.
1112    fn word_left(&mut self) {
1113        loop {
1114            self.character_left(true /* insideLine */);
1115            if self.col < self.value[self.row].len()
1116                && !self.value[self.row][self.col].is_whitespace()
1117            {
1118                break;
1119            }
1120        }
1121
1122        while self.col > 0 {
1123            if self.value[self.row][self.col - 1].is_whitespace() {
1124                break;
1125            }
1126            self.set_cursor_column(self.col - 1);
1127        }
1128    }
1129
1130    /// wordRight moves the cursor one word to the right.
1131    fn word_right(&mut self) {
1132        self.do_word_right(&mut |_, _| {});
1133    }
1134
1135    fn do_word_right(&mut self, f: &mut dyn FnMut(usize, usize)) {
1136        // Skip spaces forward.
1137        while self.col >= self.value[self.row].len()
1138            || self.value[self.row][self.col].is_whitespace()
1139        {
1140            if self.row == self.value.len() - 1 && self.col == self.value[self.row].len() {
1141                // End of text.
1142                break;
1143            }
1144            self.character_right();
1145        }
1146
1147        let mut char_idx = 0;
1148        while self.col < self.value[self.row].len() {
1149            if self.value[self.row][self.col].is_whitespace() {
1150                break;
1151            }
1152            f(char_idx, self.col);
1153            self.set_cursor_column(self.col + 1);
1154            char_idx += 1;
1155        }
1156    }
1157
1158    /// uppercaseRight changes the word to the right to uppercase.
1159    fn uppercase_right(&mut self) {
1160        let idxs: Vec<usize> = self.collect_word_right_indices();
1161        for i in idxs {
1162            self.value[self.row][i] = self.value[self.row][i].to_uppercase().next().unwrap();
1163        }
1164    }
1165
1166    /// lowercaseRight changes the word to the right to lowercase.
1167    fn lowercase_right(&mut self) {
1168        let idxs: Vec<usize> = self.collect_word_right_indices();
1169        for i in idxs {
1170            self.value[self.row][i] = self.value[self.row][i].to_lowercase().next().unwrap();
1171        }
1172    }
1173
1174    /// capitalizeRight changes the word to the right to title case.
1175    fn capitalize_right(&mut self) {
1176        let idxs: Vec<usize> = self.collect_word_right_indices();
1177        for (char_idx, i) in idxs.iter().enumerate() {
1178            if char_idx == 0 {
1179                self.value[self.row][*i] = self.value[self.row][*i].to_uppercase().next().unwrap();
1180            }
1181        }
1182    }
1183
1184    fn collect_word_right_indices(&mut self) -> Vec<usize> {
1185        let mut idxs = vec![];
1186        self.do_word_right(&mut |_, i| idxs.push(i));
1187        idxs
1188    }
1189
1190    /// LineInfo returns the number of characters from the start of the
1191    /// (soft-wrapped) line and the (soft-wrapped) line width.
1192    pub fn line_info(&self) -> LineInfo {
1193        let grid = self.memoized_wrap(&self.value[self.row], self.width);
1194
1195        // Find out which line we are currently on. This can be determined
1196        // by the m.col and counting the number of runes that we need to
1197        // skip.
1198        let mut counter = 0;
1199        for (i, line) in grid.iter().enumerate() {
1200            // We've found the line that we are on
1201            if counter + line.len() == self.col && i + 1 < grid.len() {
1202                // We wrap around to the next line if we are at the end of
1203                // the previous line so that we can be at the very beginning
1204                // of the row.
1205                return LineInfo {
1206                    char_offset: 0,
1207                    column_offset: 0,
1208                    height: grid.len(),
1209                    row_offset: i + 1,
1210                    start_column: self.col,
1211                    width: grid[i + 1].len(),
1212                    char_width: string_width(&String::from_iter(line.iter())),
1213                };
1214            }
1215
1216            if counter + line.len() >= self.col {
1217                return LineInfo {
1218                    char_offset: string_width(&String::from_iter(
1219                        line[..self.col.saturating_sub(counter)].iter(),
1220                    )),
1221                    column_offset: self.col - counter,
1222                    height: grid.len(),
1223                    row_offset: i,
1224                    start_column: counter,
1225                    width: line.len(),
1226                    char_width: string_width(&String::from_iter(line.iter())),
1227                };
1228            }
1229
1230            counter += line.len();
1231        }
1232        LineInfo {
1233            width: 0,
1234            char_width: 0,
1235            height: 0,
1236            start_column: 0,
1237            column_offset: 0,
1238            row_offset: 0,
1239            char_offset: 0,
1240        }
1241    }
1242
1243    /// repositionView repositions the view of the viewport based on the
1244    /// defined scrolling behavior.
1245    fn reposition_view(&mut self) {
1246        let minimum = self.viewport.y_offset();
1247        let maximum = minimum + self.viewport.height() - 1;
1248        let row = self.cursor_line_number();
1249        if row < minimum {
1250            self.viewport.scroll_up(minimum - row);
1251        } else if row > maximum {
1252            self.viewport.scroll_down(row - maximum);
1253        }
1254    }
1255
1256    /// Width returns the width of the textarea.
1257    pub fn width(&self) -> usize {
1258        self.width
1259    }
1260
1261    /// MoveToBegin moves the cursor to the beginning of the input.
1262    pub fn move_to_begin(&mut self) {
1263        self.row = 0;
1264        self.set_cursor_column(0);
1265        self.reposition_view();
1266    }
1267
1268    /// MoveToEnd moves the cursor to the end of the input.
1269    pub fn move_to_end(&mut self) {
1270        self.row = self.value.len() - 1;
1271        self.set_cursor_column(self.value[self.row].len());
1272        self.reposition_view();
1273    }
1274
1275    /// PageUp moves the cursor up by one page. First call snaps to the
1276    /// first visible line, subsequent calls move up by a full page.
1277    pub fn page_up(&mut self) {
1278        // If not on the first visible line, snap to it.
1279        let offset = self.viewport.y_offset() as isize - self.cursor_line_number() as isize;
1280        if offset < 0 {
1281            self.set_cursor_line_relative(offset);
1282            return;
1283        }
1284
1285        // Already on first visible line, move up by a full page.
1286        self.set_cursor_line_relative(-(self.height as isize));
1287    }
1288
1289    /// PageDown moves the cursor down by one page. First call snaps to the
1290    /// last visible line, subsequent calls move down by a full page.
1291    pub fn page_down(&mut self) {
1292        // If not on the last visible line, snap to it.
1293        let offset = self.cursor_line_number() as isize - self.viewport.y_offset() as isize;
1294        if offset < (self.height - 1) as isize {
1295            self.set_cursor_line_relative((self.height - 1) as isize - offset);
1296            return;
1297        }
1298
1299        // Already on last visible line, move down by a full page.
1300        self.set_cursor_line_relative(self.height as isize);
1301    }
1302
1303    /// SetWidth sets the width of the textarea to fit exactly within the
1304    /// given width.
1305    pub fn set_width(&mut self, w: usize) {
1306        // Update prompt width only if there is no prompt function.
1307        if self.prompt_func.is_none() {
1308            self.prompt_width = string_width(&self.prompt);
1309        }
1310
1311        // Add base style borders and padding to reserved outer width.
1312        let reserved_outer = self.active_style().base.get_horizontal_frame_size();
1313
1314        // Add prompt width to reserved inner width.
1315        let mut reserved_inner = self.prompt_width;
1316
1317        // Add line number width to reserved inner width.
1318        if self.show_line_numbers {
1319            // XXX: this was originally documented as needing "1 cell" but
1320            // was, in practice, effectively hardcoded to 2 cells.
1321            const GAP: usize = 2;
1322
1323            // Number of digits plus 1 cell for the margin.
1324            reserved_inner += num_digits(self.max_height) + GAP;
1325        }
1326
1327        // Input width must be at least one more than the reserved inner and
1328        // outer width. This gives us a minimum input width of 1.
1329        let min_width = reserved_inner + reserved_outer + 1;
1330        let mut input_width = w.max(min_width);
1331
1332        // Input width must be no more than maximum width.
1333        if self.max_width > 0 {
1334            input_width = input_width.min(self.max_width);
1335        }
1336
1337        // Since the width of the viewport and input area is dependent on the
1338        // width of borders, prompt and line numbers, we need to calculate it
1339        // by subtracting the reserved width from them.
1340        self.viewport.set_width(input_width - reserved_outer);
1341        self.width = input_width - reserved_outer - reserved_inner;
1342        self.recalculate_height();
1343    }
1344
1345    /// SetPromptFunc supersedes the Prompt field and sets a dynamic prompt
1346    /// instead.
1347    pub fn set_prompt_func(
1348        &mut self,
1349        prompt_width: usize,
1350        f: Box<dyn Fn(PromptInfo) -> String + Send + Sync>,
1351    ) {
1352        self.prompt_func = Some(f);
1353        self.prompt_width = prompt_width;
1354    }
1355
1356    /// Height returns the current height of the textarea.
1357    pub fn height(&self) -> usize {
1358        self.height
1359    }
1360
1361    /// Cursor returns a real cursor for rendering in a Bubble Tea program.
1362    /// This requires that [`use_virtual_cursor`](Self::use_virtual_cursor) is
1363    /// false and the textarea is focused.
1364    pub fn cursor(&self) -> Option<rusty_bubbletea::cursor::Cursor> {
1365        if self.use_virtual_cursor || !self.focus {
1366            return None;
1367        }
1368
1369        let li = self.line_info();
1370        let base_style = &self.active_style().base;
1371
1372        let x_offset = li.char_offset
1373            + self.prompt_width
1374            + self.line_number_width()
1375            + base_style.get_margin_left()
1376            + base_style.get_padding_left()
1377            + base_style.get_border_left_size();
1378
1379        let y_offset = self
1380            .cursor_line_number()
1381            .saturating_sub(self.viewport.y_offset())
1382            + base_style.get_margin_top()
1383            + base_style.get_padding_top()
1384            + base_style.get_border_top_size();
1385
1386        let style = &self.styles.cursor;
1387        let mut c = rusty_bubbletea::cursor::Cursor::new(x_offset, y_offset);
1388        c.blink = style.blink;
1389        // The cursor color: upstream stores a color.Color; the bubbletea
1390        // Cursor expects an RGBColor — convert from the style color.
1391        let (r, g, b, _) = style.color.rgba_bytes();
1392        c.color = Some(rusty_x_ansi::color::RGBColor { r, g, b });
1393        c.shape = style.shape;
1394        Some(c)
1395    }
1396
1397    /// lineNumberWidth returns the width reserved for the line numbers,
1398    /// mirroring the upstream `LineNumberView` width calculation.
1399    fn line_number_width(&self) -> usize {
1400        if !self.show_line_numbers {
1401            return 0;
1402        }
1403        // Number of digits plus one cell for each margin.
1404        num_digits(self.max_height) + 2
1405    }
1406
1407    /// SetHeight sets the height of the textarea.
1408    pub fn set_height(&mut self, h: usize) {
1409        if self.max_height > 0 {
1410            self.height = clamp(h, MIN_HEIGHT, self.max_height);
1411            self.viewport
1412                .set_height(clamp(h, MIN_HEIGHT, self.max_height));
1413        } else {
1414            self.height = h.max(MIN_HEIGHT);
1415            self.viewport.set_height(h.max(MIN_HEIGHT));
1416        }
1417
1418        self.reposition_view();
1419    }
1420
1421    /// Update is the Bubble Tea update loop.
1422    pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
1423        if !self.focus {
1424            self.virtual_cursor.blur();
1425            return None;
1426        }
1427
1428        // Used to determine if the cursor should blink.
1429        let (old_row, old_col) = (self.cursor_line_number(), self.col);
1430
1431        let mut cmds: Vec<Cmd> = Vec::new();
1432
1433        if self.value[self.row].is_empty() && self.value[self.row].is_empty() {
1434            // (no-op guard; value rows are always allocated)
1435        }
1436
1437        if self.max_height > 0 && self.max_height != self.cache.capacity() {
1438            self.cache = memoization::new_memo_cache(self.max_height);
1439        }
1440
1441        if let Some(pm) = msg.as_any().downcast_ref::<PasteMsg>() {
1442            self.insert_runes_from_user_input(&pm.content.chars().collect::<Vec<char>>());
1443        }
1444
1445        if let Some(m) = msg.as_any().downcast_ref::<KeyPressMsg>() {
1446            let k = &m.0;
1447            if key::matches(k, std::slice::from_ref(&self.key_map.delete_after_cursor)) {
1448                self.col = clamp(self.col, 0, self.value[self.row].len());
1449                if self.col >= self.value[self.row].len() {
1450                    self.merge_line_below(self.row);
1451                } else {
1452                    self.delete_after_cursor();
1453                }
1454            } else if key::matches(k, std::slice::from_ref(&self.key_map.delete_before_cursor)) {
1455                self.col = clamp(self.col, 0, self.value[self.row].len());
1456                if self.col == 0 {
1457                    self.merge_line_above(self.row);
1458                } else {
1459                    self.delete_before_cursor();
1460                }
1461            } else if key::matches(
1462                k,
1463                std::slice::from_ref(&self.key_map.delete_character_backward),
1464            ) {
1465                self.col = clamp(self.col, 0, self.value[self.row].len());
1466                if self.col == 0 {
1467                    self.merge_line_above(self.row);
1468                } else if !self.value[self.row].is_empty() {
1469                    let mut v = self.value[self.row][..self.col.max(1) - 1].to_vec();
1470                    v.extend_from_slice(&self.value[self.row][self.col..]);
1471                    self.value[self.row] = v;
1472                    if self.col > 0 {
1473                        self.set_cursor_column(self.col - 1);
1474                    }
1475                }
1476            } else if key::matches(
1477                k,
1478                std::slice::from_ref(&self.key_map.delete_character_forward),
1479            ) {
1480                if !self.value[self.row].is_empty() && self.col < self.value[self.row].len() {
1481                    self.value[self.row].remove(self.col);
1482                }
1483                if self.col >= self.value[self.row].len() {
1484                    self.merge_line_below(self.row);
1485                }
1486            } else if key::matches(k, std::slice::from_ref(&self.key_map.delete_word_backward)) {
1487                if self.col == 0 {
1488                    self.merge_line_above(self.row);
1489                } else {
1490                    self.delete_word_left();
1491                }
1492            } else if key::matches(k, std::slice::from_ref(&self.key_map.delete_word_forward)) {
1493                self.col = clamp(self.col, 0, self.value[self.row].len());
1494                if self.col >= self.value[self.row].len() {
1495                    self.merge_line_below(self.row);
1496                } else {
1497                    self.delete_word_right();
1498                }
1499            } else if key::matches(k, std::slice::from_ref(&self.key_map.insert_newline)) {
1500                if self.at_content_limit() {
1501                    return None;
1502                }
1503                self.col = clamp(self.col, 0, self.value[self.row].len());
1504                self.split_line(self.row, self.col);
1505            } else if key::matches(k, std::slice::from_ref(&self.key_map.line_end)) {
1506                self.cursor_end();
1507            } else if key::matches(k, std::slice::from_ref(&self.key_map.line_start)) {
1508                self.cursor_start();
1509            } else if key::matches(k, std::slice::from_ref(&self.key_map.character_forward)) {
1510                self.character_right();
1511            } else if key::matches(k, std::slice::from_ref(&self.key_map.line_next)) {
1512                self.cursor_down();
1513            } else if key::matches(k, std::slice::from_ref(&self.key_map.word_forward)) {
1514                self.word_right();
1515            } else if key::matches(k, std::slice::from_ref(&self.key_map.paste)) {
1516                return self.paste_cmd();
1517            } else if key::matches(k, std::slice::from_ref(&self.key_map.character_backward)) {
1518                self.character_left(false /* insideLine */);
1519            } else if key::matches(k, std::slice::from_ref(&self.key_map.line_previous)) {
1520                self.cursor_up();
1521            } else if key::matches(k, std::slice::from_ref(&self.key_map.word_backward)) {
1522                self.word_left();
1523            } else if key::matches(k, std::slice::from_ref(&self.key_map.input_begin)) {
1524                self.move_to_begin();
1525            } else if key::matches(k, std::slice::from_ref(&self.key_map.input_end)) {
1526                self.move_to_end();
1527            } else if key::matches(k, std::slice::from_ref(&self.key_map.page_up)) {
1528                self.page_up();
1529            } else if key::matches(k, std::slice::from_ref(&self.key_map.page_down)) {
1530                self.page_down();
1531            } else if key::matches(
1532                k,
1533                std::slice::from_ref(&self.key_map.lowercase_word_forward),
1534            ) {
1535                self.lowercase_right();
1536            } else if key::matches(
1537                k,
1538                std::slice::from_ref(&self.key_map.uppercase_word_forward),
1539            ) {
1540                self.uppercase_right();
1541            } else if key::matches(
1542                k,
1543                std::slice::from_ref(&self.key_map.capitalize_word_forward),
1544            ) {
1545                self.capitalize_right();
1546            } else if key::matches(
1547                k,
1548                std::slice::from_ref(&self.key_map.transpose_character_backward),
1549            ) {
1550                self.transpose_left();
1551            } else {
1552                self.insert_runes_from_user_input(&k.text.chars().collect::<Vec<char>>());
1553            }
1554        }
1555
1556        if let Some(pm) = msg.as_any().downcast_ref::<PasteMsgInternal>() {
1557            self.insert_runes_from_user_input(&pm.0.chars().collect::<Vec<char>>());
1558        }
1559
1560        if let Some(pm) = msg.as_any().downcast_ref::<PasteErrMsg>() {
1561            self.err = Some(pm.0.clone());
1562        }
1563
1564        self.recalculate_height();
1565
1566        // Make sure we set the content of the viewport before updating it.
1567        let view = self.view_inner();
1568        self.viewport.set_content(&view);
1569        let vp_cmd = self.viewport.update(msg);
1570        cmds.push(vp_cmd);
1571
1572        if self.use_virtual_cursor {
1573            let cmd = self.virtual_cursor.update(msg);
1574            let mut cmd = cmd;
1575
1576            // If the cursor has moved, reset the blink state. This is a
1577            // small UX nuance that makes cursor movement obvious and feel
1578            // snappy.
1579            let (new_row, new_col) = (self.cursor_line_number(), self.col);
1580            if (new_row != old_row || new_col != old_col)
1581                && self.virtual_cursor.mode() == cursor::Mode::Blink
1582            {
1583                self.virtual_cursor.is_blinked = false;
1584                cmd = self.virtual_cursor.blink();
1585            }
1586            cmds.push(cmd);
1587        }
1588
1589        self.reposition_view();
1590
1591        rusty_bubbletea::commands::batch(cmds)
1592    }
1593
1594    fn view_inner(&self) -> String {
1595        if self.value().is_empty() && self.row == 0 && self.col == 0 && !self.placeholder.is_empty()
1596        {
1597            return self.placeholder_view();
1598        }
1599        self.view_content()
1600    }
1601
1602    fn view_content(&self) -> String {
1603        let mut s = String::new();
1604        let styles = self.active_style();
1605        // Mirror the upstream `m.virtualCursor.TextStyle =
1606        // m.activeStyle().computedCursorLine()` at the top of `view()`: the
1607        // blink state of the virtual cursor renders with the cursor-line
1608        // style.
1609        let mut vc = self.virtual_cursor.clone();
1610        vc.text_style = styles.computed_cursor_line();
1611        let mut new_lines = 0usize;
1612        let mut widest_line_number = 0usize;
1613        let line_info = self.line_info();
1614        let mut display_line = 0usize;
1615        for (l, line) in self.value.iter().enumerate() {
1616            let wrapped_lines = self.memoized_wrap(line, self.width);
1617
1618            let style = if self.row == l {
1619                styles.computed_cursor_line()
1620            } else {
1621                styles.computed_text()
1622            };
1623
1624            for (wl, wrapped_line) in wrapped_lines.iter().enumerate() {
1625                let mut prompt = self.prompt_view(display_line);
1626                prompt = styles.computed_prompt().render(&prompt);
1627                s += &style.render(&prompt);
1628                display_line += 1;
1629
1630                let ln = String::new();
1631                if self.show_line_numbers {
1632                    if wl == 0 {
1633                        // normal line
1634                        let is_cursor_line = self.row == l;
1635                        s += &self.line_number_view((l + 1) as isize, is_cursor_line);
1636                    } else {
1637                        // soft wrapped line
1638                        let is_cursor_line = self.row == l;
1639                        s += &self.line_number_view(-1, is_cursor_line);
1640                    }
1641                }
1642
1643                // Note the widest line number for padding purposes later.
1644                // Upstream declares `var ln string` but never assigns it, so
1645                // the widest line number stays 0; mirror that.
1646                let lnw = string_width(&ln);
1647                if lnw > widest_line_number {
1648                    widest_line_number = lnw;
1649                }
1650
1651                let mut wrapped_line = wrapped_line.clone();
1652                let strwidth = string_width(&String::from_iter(wrapped_line.iter()));
1653                let mut padding = self.width - strwidth;
1654                // If the trailing space causes the line to be wider than the
1655                // width, we should not draw it to the screen.
1656                if strwidth > self.width {
1657                    // The character causing the line to be wider than the
1658                    // width is guaranteed to be a space.
1659                    while wrapped_line.last() == Some(&' ') {
1660                        wrapped_line.pop();
1661                    }
1662                    padding = padding.saturating_sub(self.width - strwidth);
1663                }
1664                if self.row == l && line_info.row_offset == wl {
1665                    s += &style.render(&String::from_iter(
1666                        wrapped_line[..line_info.column_offset.min(wrapped_line.len())].iter(),
1667                    ));
1668                    if self.col >= line.len() && line_info.char_offset >= self.width {
1669                        vc.set_char(" ");
1670                        s += &vc.view();
1671                    } else {
1672                        let col = line_info.column_offset.min(wrapped_line.len());
1673                        let ch = if col < wrapped_line.len() {
1674                            String::from_iter(wrapped_line[col..col + 1].iter())
1675                        } else {
1676                            String::new()
1677                        };
1678                        vc.set_char(&ch);
1679                        s += &style.render(&vc.view());
1680                        s += &style.render(&String::from_iter(wrapped_line[col + 1..].iter()));
1681                    }
1682                } else {
1683                    s += &style.render(&String::from_iter(wrapped_line.iter()));
1684                }
1685                s += &style.render(&" ".repeat(padding));
1686                s += "\n";
1687                new_lines += 1;
1688            }
1689        }
1690
1691        // Always show at least `m.height` lines at all times.
1692        // To do this we can simply pad out a few extra new lines in the
1693        // view.
1694        for _ in 0..self.height {
1695            let prompt = self.prompt_view(display_line);
1696            s += &prompt;
1697            display_line += 1;
1698
1699            // Write end of buffer content
1700            let left_gutter = self.end_of_buffer_character.to_string();
1701            let right_gap_width =
1702                self.width().saturating_sub(string_width(&left_gutter)) + widest_line_number;
1703            let right_gap = " ".repeat(right_gap_width);
1704            s += &styles
1705                .computed_end_of_buffer()
1706                .render(&(left_gutter + &right_gap));
1707            s += "\n";
1708        }
1709
1710        let _ = new_lines;
1711        s
1712    }
1713
1714    /// View renders the text area in its current state.
1715    pub fn view(&self) -> String {
1716        // XXX: This is a workaround for the case where the viewport hasn't
1717        // been initialized yet like during the initial render.
1718        let mut viewport = self.viewport.clone();
1719        viewport.set_content(&self.view_inner());
1720        let view = viewport.view();
1721        let styles = self.active_style();
1722        styles.base.clone().render(&view)
1723    }
1724
1725    /// promptView renders a single line of the prompt.
1726    pub fn prompt_view(&self, display_line: usize) -> String {
1727        let mut prompt = self.prompt.clone();
1728        if let Some(f) = &self.prompt_func {
1729            prompt = f(PromptInfo {
1730                line_number: display_line,
1731                focused: self.focus,
1732            });
1733            let width = rusty_lipgloss::size::width(&prompt);
1734            if width < self.prompt_width {
1735                prompt = format!("{}{}", " ".repeat(self.prompt_width - width), prompt);
1736            }
1737        }
1738
1739        prompt
1740    }
1741
1742    /// lineNumberView renders the line number.
1743    fn line_number_view(&self, n: isize, is_cursor_line: bool) -> String {
1744        if !self.show_line_numbers {
1745            return String::new();
1746        }
1747
1748        let mut str_: String;
1749        if n <= 0 {
1750            str_ = " ".to_string();
1751        } else {
1752            str_ = n.to_string();
1753        }
1754
1755        // XXX: is textStyle really necessary here?
1756        let mut text_style = self.active_style().computed_text();
1757        let mut line_number_style = self.active_style().computed_line_number();
1758        if is_cursor_line {
1759            text_style = self.active_style().computed_cursor_line();
1760            line_number_style = self.active_style().computed_cursor_line_number();
1761        }
1762
1763        // Format line number dynamically based on the maximum number of
1764        // lines.
1765        let digits = num_digits(self.max_height);
1766        str_ = format!(" {:>width$} ", str_, width = digits);
1767
1768        text_style.render(&line_number_style.render(&str_))
1769    }
1770
1771    /// placeholderView returns the prompt and placeholder, if any.
1772    fn placeholder_view(&self) -> String {
1773        let mut s = String::new();
1774        let p = self.placeholder.clone();
1775        let styles = self.active_style();
1776        // word wrap lines
1777        let pwordwrap = wordwrap(&p, self.width, "");
1778        // hard wrap lines (handles lines that could not be word wrapped)
1779        let pwrap = hardwrap(&pwordwrap, self.width, true);
1780        // split string by new lines
1781        let plines: Vec<String> = pwrap.trim().split('\n').map(|x| x.to_string()).collect();
1782
1783        for i in 0..self.height {
1784            let is_line_number = plines.len() > i;
1785
1786            let mut line_style = styles.computed_placeholder();
1787            if plines.len() > i {
1788                line_style = styles.computed_cursor_line();
1789            }
1790
1791            // render prompt
1792            let prompt = self.prompt_view(i);
1793            let prompt = styles.computed_prompt().render(&prompt);
1794            s += &line_style.render(&prompt);
1795
1796            // when show line numbers enabled: render line number for only
1797            // the cursor line; indent other placeholder lines.
1798            if self.show_line_numbers {
1799                let mut ln = 0isize;
1800
1801                match i {
1802                    0 => {
1803                        ln = (i + 1) as isize;
1804                        if plines.len() > i {
1805                            s += &self.line_number_view(ln, is_line_number);
1806                        }
1807                    }
1808                    _ => {
1809                        if plines.len() > i {
1810                            s += &self.line_number_view(ln, is_line_number);
1811                        }
1812                    }
1813                }
1814            }
1815
1816            match i {
1817                // first line
1818                0 => {
1819                    // first character of first line as cursor with character
1820                    let mut vc = self.virtual_cursor.clone();
1821                    vc.text_style = styles.computed_placeholder();
1822
1823                    let ch = plines[0].chars().next().unwrap_or(' ');
1824                    let rest: String = plines[0].chars().skip(1).collect();
1825                    vc.set_char(&ch.to_string());
1826                    s += &line_style.render(&vc.view());
1827
1828                    // the rest of the first line
1829                    s += &line_style.render(&styles.computed_placeholder().render(&rest));
1830
1831                    // extend the first line with spaces to fill the width
1832                    let gap = " ".repeat(
1833                        self.width
1834                            .saturating_sub(rusty_lipgloss::size::width(&plines[0])),
1835                    );
1836                    s += &line_style.render(&gap);
1837                }
1838                // remaining lines
1839                _ => {
1840                    if plines.len() > i {
1841                        // current line placeholder text
1842                        let placeholder_line = &plines[i];
1843                        let gap = " ".repeat(self.width.saturating_sub(string_width(&plines[i])));
1844                        s += &line_style.render(&(placeholder_line.clone() + &gap));
1845                    } else {
1846                        // end of line buffer character
1847                        let eob = styles
1848                            .computed_end_of_buffer()
1849                            .render(&self.end_of_buffer_character.to_string());
1850                        s += &eob;
1851                    }
1852                }
1853            }
1854
1855            // terminate with new line
1856            s += "\n";
1857        }
1858
1859        let mut viewport = self.viewport.clone();
1860        viewport.set_content(&s);
1861        let v = viewport.view();
1862        styles.base.clone().render(&v)
1863    }
1864
1865    fn memoized_wrap(&self, runes: &[char], width: usize) -> Vec<Vec<char>> {
1866        // The cache is keyed by content hash; only used when the model is
1867        // mutable. For &self access we compute directly.
1868        let _ = runes;
1869        let _ = width;
1870        // Note: upstream memoizes via a mutable cache; this port computes
1871        // the wrap on demand to keep LineInfo usable through &self.
1872        let _ = &self.cache;
1873        wrap(runes, width)
1874    }
1875
1876    /// cursorLineNumber returns the line number that the cursor is on.
1877    /// This accounts for soft wrapped lines.
1878    pub fn cursor_line_number(&self) -> usize {
1879        let mut line = 0;
1880        for i in 0..self.row {
1881            // Calculate the number of lines that the current line will be
1882            // split into.
1883            line += self.memoized_wrap(&self.value[i], self.width).len();
1884        }
1885        line + self.line_info().row_offset
1886    }
1887
1888    /// TotalVisualLines returns the total number of display lines across
1889    /// all logical lines, accounting for soft wraps.
1890    pub fn total_visual_lines(&self) -> usize {
1891        let mut n = 0;
1892        for line in &self.value {
1893            n += self.memoized_wrap(line, self.width).len();
1894        }
1895        n
1896    }
1897
1898    /// recalculateHeight recomputes and applies the textarea height based
1899    /// on content when DynamicHeight is enabled. It is a no-op otherwise.
1900    fn recalculate_height(&mut self) {
1901        if !self.dynamic_height {
1902            return;
1903        }
1904        let min_h = self.min_height.max(MIN_HEIGHT);
1905        let total = self.total_visual_lines();
1906        let mut h = total.max(min_h);
1907        if self.max_height > 0 {
1908            h = h.min(self.max_height);
1909        }
1910        let max_offset = total.saturating_sub(h);
1911        if self.viewport.y_offset() > max_offset {
1912            self.viewport.set_y_offset(max_offset);
1913        }
1914        self.set_height(h);
1915    }
1916
1917    /// atContentLimit reports whether the textarea has reached its content
1918    /// limit.
1919    fn at_content_limit(&self) -> bool {
1920        if self.max_content_height > 0 {
1921            return self.total_visual_lines() >= self.max_content_height;
1922        }
1923        self.max_height > 0 && self.value.len() >= self.max_height
1924    }
1925
1926    /// visualLinesForInsert estimates how many additional visual lines
1927    /// would result from inserting the given lines at the current cursor
1928    /// position.
1929    fn visual_lines_for_insert(&self, lines: &[Vec<char>]) -> usize {
1930        if lines.is_empty() {
1931            return 0;
1932        }
1933
1934        // The current row's visual line count before insertion.
1935        let current_row_visual = self.memoized_wrap(&self.value[self.row], self.width).len();
1936
1937        // Simulate merging the first paste line into the current row.
1938        let mut merged: Vec<char> = self.value[self.row][..self.col].to_vec();
1939        merged.extend_from_slice(&lines[0]);
1940        if lines.len() == 1 {
1941            merged.extend_from_slice(&self.value[self.row][self.col..]);
1942        }
1943        let delta = self.memoized_wrap(&merged, self.width).len() - current_row_visual;
1944
1945        // Each additional line is a new logical line.
1946        let mut delta = delta;
1947        for (i, content) in lines.iter().enumerate() {
1948            let mut content = content.clone();
1949            if i == lines.len() - 1 {
1950                content.extend_from_slice(&self.value[self.row][self.col..]);
1951            }
1952            delta += self.memoized_wrap(&content, self.width).len();
1953        }
1954
1955        delta
1956    }
1957
1958    /// mergeLineBelow merges the current line the cursor is on with the
1959    /// line below.
1960    fn merge_line_below(&mut self, row: usize) {
1961        if row >= self.value.len() - 1 {
1962            return;
1963        }
1964
1965        // To perform a merge, we will need to combine the two lines.
1966        let mut merged = self.value[row].clone();
1967        merged.extend_from_slice(&self.value[row + 1]);
1968        self.value[row] = merged;
1969
1970        // Shift all lines up by one.
1971        for i in row + 1..self.value.len() - 1 {
1972            self.value[i] = self.value[i + 1].clone();
1973        }
1974
1975        // And, remove the last line.
1976        if !self.value.is_empty() {
1977            self.value.pop();
1978        }
1979    }
1980
1981    /// mergeLineAbove merges the current line the cursor is on with the
1982    /// line above.
1983    fn merge_line_above(&mut self, row: usize) {
1984        if row == 0 {
1985            return;
1986        }
1987
1988        self.col = self.value[row - 1].len();
1989        self.row -= 1;
1990
1991        // To perform a merge, we will need to combine the two lines.
1992        let mut merged = self.value[row - 1].clone();
1993        merged.extend_from_slice(&self.value[row]);
1994        self.value[row - 1] = merged;
1995
1996        // Shift all lines up by one.
1997        for i in row..self.value.len() - 1 {
1998            self.value[i] = self.value[i + 1].clone();
1999        }
2000
2001        // And, remove the last line.
2002        if !self.value.is_empty() {
2003            self.value.pop();
2004        }
2005    }
2006
2007    fn split_line(&mut self, row: usize, col: usize) {
2008        // To perform a split, take the current line and keep the content
2009        // before the cursor, take the content after the cursor and make it
2010        // the content of the line underneath, and shift the remaining lines
2011        // down by one.
2012        let head: Vec<char> = self.value[row][..col].to_vec();
2013        let tail: Vec<char> = self.value[row][col..].to_vec();
2014
2015        self.value.insert(row + 1, tail);
2016
2017        self.value[row] = head;
2018
2019        self.col = 0;
2020        self.row += 1;
2021    }
2022
2023    /// Paste is a command for pasting from the clipboard into the text
2024    /// input.
2025    fn paste_cmd(&self) -> Cmd {
2026        Some(Box::new(|| match clipboard::read_all() {
2027            Ok(str) => Some(Box::new(PasteMsgInternal(str))),
2028            Err(err) => Some(Box::new(PasteErrMsg(err))),
2029        }))
2030    }
2031}
2032
2033/// Blink returns the blink command for the virtual cursor.
2034pub fn blink() -> Box<dyn Msg> {
2035    crate::cursor::blink()
2036}
2037
2038fn wrap(runes: &[char], width: usize) -> Vec<Vec<char>> {
2039    let mut lines: Vec<Vec<char>> = vec![vec![]];
2040    let mut word: Vec<char> = vec![];
2041    let mut row = 0usize;
2042    let mut spaces = 0usize;
2043
2044    // Word wrap the runes
2045    for r in runes {
2046        if r.is_whitespace() {
2047            spaces += 1;
2048        } else {
2049            word.push(*r);
2050        }
2051
2052        if spaces > 0 {
2053            if string_width(&String::from_iter(lines[row].iter()))
2054                + string_width(&String::from_iter(word.iter()))
2055                + spaces
2056                > width
2057            {
2058                row += 1;
2059                lines.push(vec![]);
2060                lines[row].extend_from_slice(&word);
2061                lines[row].extend_from_slice(&repeat_spaces(spaces));
2062                spaces = 0;
2063                word.clear();
2064            } else {
2065                lines[row].extend_from_slice(&word);
2066                lines[row].extend_from_slice(&repeat_spaces(spaces));
2067                spaces = 0;
2068                word.clear();
2069            }
2070        } else if !word.is_empty() {
2071            // If the last character is a double-width rune, then we may not
2072            // be able to add it to this line as it might cause us to go past
2073            // the width.
2074            let last_char_len = char_width(*word.last().unwrap());
2075            if string_width(&String::from_iter(word.iter())) + last_char_len > width {
2076                // If the current line has any content, let's move to the
2077                // next line because the current word fills up the entire
2078                // line.
2079                if !lines[row].is_empty() {
2080                    row += 1;
2081                    lines.push(vec![]);
2082                }
2083                lines[row].extend_from_slice(&word);
2084                word.clear();
2085            }
2086        }
2087    }
2088
2089    if string_width(&String::from_iter(lines[row].iter()))
2090        + string_width(&String::from_iter(word.iter()))
2091        + spaces
2092        >= width
2093    {
2094        lines.push(vec![]);
2095        lines[row + 1].extend_from_slice(&word);
2096        // We add an extra space at the end of the line to account for the
2097        // trailing space at the end of the previous soft-wrapped lines so
2098        // that behaviour when navigating is consistent.
2099        spaces += 1;
2100        lines[row + 1].extend_from_slice(&repeat_spaces(spaces));
2101    } else {
2102        lines[row].extend_from_slice(&word);
2103        spaces += 1;
2104        lines[row].extend_from_slice(&repeat_spaces(spaces));
2105    }
2106
2107    lines
2108}
2109
2110fn repeat_spaces(n: usize) -> Vec<char> {
2111    vec![' '; n]
2112}
2113
2114/// numDigits returns the number of digits in an integer.
2115fn num_digits(n: usize) -> usize {
2116    if n == 0 {
2117        return 1;
2118    }
2119    let mut count = 0;
2120    let mut num = n;
2121    while num > 0 {
2122        count += 1;
2123        num /= 10;
2124    }
2125    count
2126}
2127
2128fn clamp(v: usize, low: usize, high: usize) -> usize {
2129    if high < low {
2130        return low;
2131    }
2132    v.max(low).min(high)
2133}
2134
2135fn char_width(c: char) -> usize {
2136    UnicodeWidthChar::width(c).unwrap_or(0)
2137}
2138
2139fn string_width(s: &str) -> usize {
2140    s.chars().map(char_width).sum()
2141}
2142
2143/// wordwrap wraps a string to a given line length without breaking
2144/// word boundaries (char-based port of `charmbracelet/x/ansi`'s `Wordwrap`).
2145fn wordwrap(s: &str, limit: usize, breakpoints: &str) -> String {
2146    if limit < 1 {
2147        return s.to_string();
2148    }
2149
2150    let mut buf = String::new();
2151    let mut word = String::new();
2152    let mut space = String::new();
2153    let mut cur_width = 0usize;
2154    let mut word_len = 0usize;
2155
2156    // addSpace mirrors the upstream helper: the pending space run is
2157    // written into the buffer.
2158    let add_space = |buf: &mut String, space: &mut String, cur_width: &mut usize| {
2159        *cur_width += space.len();
2160        buf.push_str(space);
2161        space.clear();
2162    };
2163    // addWord mirrors the upstream helper: flush the pending space run,
2164    // then the current word.
2165    let add_word = |buf: &mut String,
2166                    space: &mut String,
2167                    word: &mut String,
2168                    cur_width: &mut usize,
2169                    word_len: &mut usize| {
2170        if word.is_empty() {
2171            return;
2172        }
2173        add_space(buf, space, cur_width);
2174        *cur_width += *word_len;
2175        buf.push_str(word);
2176        word.clear();
2177        *word_len = 0;
2178    };
2179    let add_newline = |buf: &mut String, space: &mut String, cur_width: &mut usize| {
2180        buf.push('\n');
2181        *cur_width = 0;
2182        space.clear();
2183    };
2184
2185    for c in s.chars() {
2186        if c == '\n' {
2187            if word_len == 0 {
2188                if cur_width + space.len() > limit {
2189                    cur_width = 0;
2190                } else {
2191                    buf.push_str(&space);
2192                }
2193                space.clear();
2194            }
2195            add_word(
2196                &mut buf,
2197                &mut space,
2198                &mut word,
2199                &mut cur_width,
2200                &mut word_len,
2201            );
2202            add_newline(&mut buf, &mut space, &mut cur_width);
2203        } else if c.is_whitespace() && c != '\u{00A0}' {
2204            add_word(
2205                &mut buf,
2206                &mut space,
2207                &mut word,
2208                &mut cur_width,
2209                &mut word_len,
2210            );
2211            space.push(c);
2212        } else if c == '-' || breakpoints.contains(c) {
2213            add_space(&mut buf, &mut space, &mut cur_width);
2214            add_word(
2215                &mut buf,
2216                &mut space,
2217                &mut word,
2218                &mut cur_width,
2219                &mut word_len,
2220            );
2221            buf.push(c);
2222            cur_width += 1;
2223        } else {
2224            word.push(c);
2225            word_len += char_width(c);
2226            if cur_width + space.len() + word_len > limit && word_len < limit {
2227                add_newline(&mut buf, &mut space, &mut cur_width);
2228            }
2229        }
2230    }
2231
2232    add_word(
2233        &mut buf,
2234        &mut space,
2235        &mut word,
2236        &mut cur_width,
2237        &mut word_len,
2238    );
2239    buf
2240}
2241
2242/// hardwrap wraps a string to a given line length, breaking word boundaries
2243/// (char-based port of `charmbracelet/x/ansi`'s `Hardwrap`).
2244fn hardwrap(s: &str, limit: usize, preserve_space: bool) -> String {
2245    if limit < 1 {
2246        return s.to_string();
2247    }
2248
2249    let mut buf = String::new();
2250    let mut cur_width = 0usize;
2251    let mut force_newline = false;
2252
2253    for c in s.chars() {
2254        if c == '\n' {
2255            buf.push('\n');
2256            cur_width = 0;
2257            force_newline = false;
2258            continue;
2259        }
2260
2261        let w = char_width(c);
2262        if cur_width + w > limit {
2263            buf.push('\n');
2264            cur_width = 0;
2265            force_newline = true;
2266        }
2267
2268        // Skip spaces at the beginning of a line.
2269        if cur_width == 0 {
2270            if !preserve_space && force_newline && c.is_whitespace() {
2271                continue;
2272            }
2273            force_newline = false;
2274        }
2275
2276        buf.push(c);
2277        cur_width += w;
2278    }
2279
2280    buf
2281}