bubbletea_widgets/textarea/
mod.rs

1//! Textarea component for Bubble Tea applications.
2//!
3//! This module provides a multi-line text input component with feature parity
4//! to the Go `bubbles/textarea` package. It supports soft-wrapping, line
5//! numbers, customizable prompts, clipboard integration, and rich theming via
6//! Lip Gloss styles.
7//!
8//! The component implements the crate's `Component` trait so it can be focused
9//! and blurred, and it exposes a `Model` with idiomatic methods for editing and
10//! navigation (insert, delete, move by character/word/line, etc.).
11//!
12//! ### Features
13//! - Soft-wrapped lines with correct column/character accounting for double-width runes
14//! - Optional line numbers and per-line prompts (static or via a prompt function)
15//! - Cursor movement by character, word, and line with deletion/edit helpers
16//! - Viewport-driven rendering for large inputs
17//! - Clipboard paste integration (platform dependent)
18//! - Theming via `TextareaStyle` for focused and blurred states
19//!
20//! ### Example
21//! ```rust
22//! use bubbletea_widgets::{textarea, Component};
23//!
24//! // Create a textarea with defaults
25//! let mut ta = textarea::new();
26//! ta.set_width(40);
27//! ta.set_height(6);
28//! ta.placeholder = "Type here…".into();
29//!
30//! // Focus to start receiving input
31//! let _ = ta.focus();
32//!
33//! // Programmatic edits
34//! ta.insert_string("Hello\nworld!");
35//! ta.word_left();
36//! ta.uppercase_right();
37//!
38//! // Render view (string with ANSI styling)
39//! let view = ta.view();
40//! println!("{}", view);
41//! ```
42//!
43//! See the `helpers` module for key bindings and styling utilities, and
44//! `memoization` for the internal soft-wrap cache.
45
46pub mod helpers;
47pub mod memoization;
48
49#[cfg(test)]
50mod tests;
51
52use helpers::*;
53use memoization::MemoizedWrap;
54
55use crate::{cursor, viewport, Component};
56use bubbletea_rs::{Cmd, Model as BubbleTeaModel};
57use lipgloss_extras::lipgloss;
58use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
59
60// Constants matching Go implementation
61const MIN_HEIGHT: usize = 1;
62const DEFAULT_HEIGHT: usize = 6;
63const DEFAULT_WIDTH: usize = 40;
64const DEFAULT_CHAR_LIMIT: usize = 0; // no limit
65const DEFAULT_MAX_HEIGHT: usize = 99;
66const DEFAULT_MAX_WIDTH: usize = 500;
67const MAX_LINES: usize = 10000;
68
69/// Internal messages for clipboard operations
70#[derive(Debug, Clone)]
71pub struct PasteMsg(pub String);
72
73/// Error message produced when a paste operation fails.
74#[derive(Debug, Clone)]
75pub struct PasteErrMsg(pub String);
76
77/// LineInfo helper for tracking line information regarding soft-wrapped lines
78/// Direct port from Go's LineInfo struct
79#[derive(Debug, Clone, Default)]
80pub struct LineInfo {
81    /// Width is the number of columns in the line
82    pub width: usize,
83    /// CharWidth is the number of characters in the line to account for double-width runes
84    pub char_width: usize,
85    /// Height is the number of rows in the line  
86    pub height: usize,
87    /// StartColumn is the index of the first column of the line
88    pub start_column: usize,
89    /// ColumnOffset is the number of columns that the cursor is offset from the start of the line
90    pub column_offset: usize,
91    /// RowOffset is the number of rows that the cursor is offset from the start of the line
92    pub row_offset: usize,
93    /// CharOffset is the number of characters that the cursor is offset from the start of the line
94    pub char_offset: usize,
95}
96
97/// Model is the Bubble Tea model for this text area element.
98/// Direct port from Go's Model struct with all fields preserved
99#[derive(Debug)]
100pub struct Model {
101    // Error state
102    /// Optional error string surfaced by the component.
103    pub err: Option<String>,
104
105    // General settings - memoization cache
106    cache: MemoizedWrap,
107
108    // Display settings
109    /// Prompt is printed at the beginning of each line
110    pub prompt: String,
111    /// Placeholder is the text displayed when the user hasn't entered anything yet
112    pub placeholder: String,
113    /// ShowLineNumbers, if enabled, causes line numbers to be printed after the prompt
114    pub show_line_numbers: bool,
115    /// EndOfBufferCharacter is displayed at the end of the input
116    pub end_of_buffer_character: char,
117
118    // KeyMap encodes the keybindings recognized by the widget
119    /// Key bindings recognized by the widget.
120    pub key_map: TextareaKeyMap,
121
122    // Styling. FocusedStyle and BlurredStyle are used to style the textarea in focused and blurred states
123    /// Style used when the textarea is focused.
124    pub focused_style: TextareaStyle,
125    /// Style used when the textarea is blurred.
126    pub blurred_style: TextareaStyle,
127    // style is the current styling to use - pointer equivalent in Rust
128    current_style: TextareaStyle,
129
130    // Cursor is the text area cursor
131    /// Embedded cursor model for caret rendering and blinking.
132    pub cursor: cursor::Model,
133
134    // Limits
135    /// CharLimit is the maximum number of characters this input element will accept
136    pub char_limit: usize,
137    /// MaxHeight is the maximum height of the text area in rows
138    pub max_height: usize,
139    /// MaxWidth is the maximum width of the text area in columns
140    pub max_width: usize,
141
142    // Dynamic prompt function - Option to handle nullable function pointer
143    prompt_func: Option<fn(usize) -> String>,
144    /// promptWidth is the width of the prompt
145    prompt_width: usize,
146
147    // Dimensions
148    /// width is the maximum number of characters that can be displayed at once
149    width: usize,
150    /// height is the maximum number of lines that can be displayed at once
151    height: usize,
152
153    // Content - using Vec<Vec<char>> to match Go's [][]rune
154    /// Underlying text value as runes (characters)
155    value: Vec<Vec<char>>,
156
157    // State
158    /// focus indicates whether user input focus should be on this input component
159    focus: bool,
160    /// Cursor column
161    col: usize,
162    /// Cursor row  
163    row: usize,
164    /// Last character offset, used to maintain state when cursor is moved vertically
165    last_char_offset: usize,
166
167    // Viewport is the vertically-scrollable viewport of the multi-line text input
168    viewport: viewport::Model,
169}
170
171impl Model {
172    /// Create a new textarea model with default settings - port of Go's New()
173    pub fn new() -> Self {
174        let vp = viewport::Model::new(0, 0);
175        // Disable viewport key handling to let textarea handle keys (no keymap field in viewport)
176
177        let cur = cursor::Model::new();
178
179        let (focused_style, blurred_style) = default_styles();
180
181        let mut model = Self {
182            err: None,
183            cache: MemoizedWrap::new(),
184            prompt: format!("{} ", lipgloss::thick_border().left),
185            placeholder: String::new(),
186            show_line_numbers: true,
187            end_of_buffer_character: ' ',
188            key_map: TextareaKeyMap::default(),
189            focused_style: focused_style.clone(),
190            blurred_style: blurred_style.clone(),
191            current_style: blurred_style, // Start blurred
192            cursor: cur,
193            char_limit: DEFAULT_CHAR_LIMIT,
194            max_height: DEFAULT_MAX_HEIGHT,
195            max_width: DEFAULT_MAX_WIDTH,
196            prompt_func: None,
197            prompt_width: 0,
198            width: DEFAULT_WIDTH,
199            height: DEFAULT_HEIGHT,
200            value: vec![vec![]; MIN_HEIGHT],
201            focus: false,
202            col: 0,
203            row: 0,
204            last_char_offset: 0,
205            viewport: vp,
206        };
207
208        // Ensure value has minimum height and maxLines capacity
209        model.value.reserve(MAX_LINES);
210        model.set_height(DEFAULT_HEIGHT);
211        model.set_width(DEFAULT_WIDTH);
212
213        model
214    }
215
216    /// Set the value of the text input - port of Go's SetValue
217    pub fn set_value(&mut self, s: impl Into<String>) {
218        self.reset();
219        self.insert_string(s.into());
220        // After setting full value, position cursor at end of input (last line)
221        self.row = self.value.len().saturating_sub(1);
222        if let Some(line) = self.value.get(self.row) {
223            self.set_cursor(line.len());
224        }
225    }
226
227    /// Insert a string at the cursor position - port of Go's InsertString
228    pub fn insert_string(&mut self, s: impl Into<String>) {
229        let s = s.into();
230        let runes: Vec<char> = s.chars().collect();
231        self.insert_runes_from_user_input(runes);
232    }
233
234    /// Insert a rune at the cursor position - port of Go's InsertRune
235    pub fn insert_rune(&mut self, r: char) {
236        self.insert_runes_from_user_input(vec![r]);
237    }
238
239    /// Get the current value as a string - port of Go's Value()
240    pub fn value(&self) -> String {
241        if self.value.is_empty() {
242            return String::new();
243        }
244
245        let mut result = String::new();
246        for (i, line) in self.value.iter().enumerate() {
247            if i > 0 {
248                result.push('\n');
249            }
250            result.extend(line.iter());
251        }
252        result
253    }
254
255    /// Length returns the number of characters currently in the text input - port of Go's Length()
256    pub fn length(&self) -> usize {
257        let mut l = 0;
258        for row in &self.value {
259            l += row
260                .iter()
261                .map(|&ch| UnicodeWidthChar::width(ch).unwrap_or(0))
262                .sum::<usize>();
263        }
264        // Add newline characters count
265        l + self.value.len().saturating_sub(1)
266    }
267
268    /// LineCount returns the number of lines currently in the text input - port of Go's LineCount()
269    pub fn line_count(&self) -> usize {
270        self.value.len()
271    }
272
273    /// Line returns the line position - port of Go's Line()
274    pub fn line(&self) -> usize {
275        self.row
276    }
277
278    /// Focused returns the focus state on the model - port of Go's Focused()
279    pub fn focused(&self) -> bool {
280        self.focus
281    }
282
283    /// Reset sets the input to its default state with no input - port of Go's Reset()
284    pub fn reset(&mut self) {
285        self.value = vec![vec![]; MIN_HEIGHT];
286        self.value.reserve(MAX_LINES);
287        self.col = 0;
288        self.row = 0;
289        self.viewport.goto_top();
290        self.set_cursor(0);
291    }
292
293    /// Width returns the width of the textarea - port of Go's Width()
294    pub fn width(&self) -> usize {
295        self.width
296    }
297
298    /// Height returns the current height of the textarea - port of Go's Height()
299    pub fn height(&self) -> usize {
300        self.height
301    }
302
303    /// SetWidth sets the width of the textarea - port of Go's SetWidth()
304    pub fn set_width(&mut self, w: usize) {
305        // Update prompt width only if there is no prompt function
306        if self.prompt_func.is_none() {
307            self.prompt_width = self.prompt.width();
308        }
309
310        // Add base style borders and padding to reserved outer width
311        let reserved_outer = 0; // Simplified for now, lipgloss API differs
312
313        // Add prompt width to reserved inner width
314        let mut reserved_inner = self.prompt_width;
315
316        // Add line number width to reserved inner width
317        if self.show_line_numbers {
318            let ln_width = 4; // Up to 3 digits for line number plus 1 margin
319            reserved_inner += ln_width;
320        }
321
322        // Input width must be at least one more than the reserved inner and outer width
323        let min_width = reserved_inner + reserved_outer + 1;
324        let mut input_width = w.max(min_width);
325
326        // Input width must be no more than maximum width
327        if self.max_width > 0 {
328            input_width = input_width.min(self.max_width);
329        }
330
331        self.viewport.width = input_width.saturating_sub(reserved_outer);
332        self.width = input_width
333            .saturating_sub(reserved_outer)
334            .saturating_sub(reserved_inner);
335    }
336
337    /// SetHeight sets the height of the textarea - port of Go's SetHeight()
338    pub fn set_height(&mut self, h: usize) {
339        if self.max_height > 0 {
340            self.height = clamp(h, MIN_HEIGHT, self.max_height);
341            self.viewport.height = clamp(h, MIN_HEIGHT, self.max_height);
342        } else {
343            self.height = h.max(MIN_HEIGHT);
344            self.viewport.height = h.max(MIN_HEIGHT);
345        }
346    }
347
348    /// SetPromptFunc supersedes the Prompt field and sets a dynamic prompt instead
349    /// Port of Go's SetPromptFunc
350    pub fn set_prompt_func(&mut self, prompt_width: usize, func: fn(usize) -> String) {
351        self.prompt_func = Some(func);
352        self.prompt_width = prompt_width;
353    }
354
355    /// SetCursor moves the cursor to the given position - port of Go's SetCursor()
356    pub fn set_cursor(&mut self, col: usize) {
357        self.col = clamp(
358            col,
359            0,
360            self.value.get(self.row).map_or(0, |line| line.len()),
361        );
362        // Reset last char offset when moving cursor horizontally
363        self.last_char_offset = 0;
364    }
365
366    /// CursorStart moves the cursor to the start of the input field - port of Go's CursorStart()
367    pub fn cursor_start(&mut self) {
368        self.set_cursor(0);
369    }
370
371    /// CursorEnd moves the cursor to the end of the input field - port of Go's CursorEnd()
372    pub fn cursor_end(&mut self) {
373        if let Some(line) = self.value.get(self.row) {
374            self.set_cursor(line.len());
375        }
376    }
377
378    /// CursorDown moves the cursor down by one line - port of Go's CursorDown()
379    pub fn cursor_down(&mut self) {
380        let li = self.line_info();
381        let char_offset = self.last_char_offset.max(li.char_offset);
382        self.last_char_offset = char_offset;
383
384        if li.row_offset + 1 >= li.height && self.row < self.value.len().saturating_sub(1) {
385            self.row += 1;
386            self.col = 0;
387        } else {
388            // Move the cursor to the start of the next line
389            const TRAILING_SPACE: usize = 2;
390            if let Some(line) = self.value.get(self.row) {
391                self.col =
392                    (li.start_column + li.width + TRAILING_SPACE).min(line.len().saturating_sub(1));
393            }
394        }
395
396        let nli = self.line_info();
397        self.col = nli.start_column;
398
399        if nli.width == 0 {
400            return;
401        }
402
403        let mut offset = 0;
404        while offset < char_offset {
405            if self.row >= self.value.len()
406                || self.col >= self.value.get(self.row).map_or(0, |line| line.len())
407                || offset >= nli.char_width.saturating_sub(1)
408            {
409                break;
410            }
411            if let Some(line) = self.value.get(self.row) {
412                if let Some(&ch) = line.get(self.col) {
413                    offset += UnicodeWidthChar::width(ch).unwrap_or(0);
414                }
415            }
416            self.col += 1;
417        }
418    }
419
420    /// CursorUp moves the cursor up by one line - port of Go's CursorUp()
421    pub fn cursor_up(&mut self) {
422        let li = self.line_info();
423        let char_offset = self.last_char_offset.max(li.char_offset);
424        self.last_char_offset = char_offset;
425
426        if li.row_offset == 0 && self.row > 0 {
427            self.row -= 1;
428            if let Some(line) = self.value.get(self.row) {
429                self.col = line.len();
430            }
431        } else {
432            // Move the cursor to the end of the previous line
433            const TRAILING_SPACE: usize = 2;
434            self.col = li.start_column.saturating_sub(TRAILING_SPACE);
435        }
436
437        let nli = self.line_info();
438        self.col = nli.start_column;
439
440        if nli.width == 0 {
441            return;
442        }
443
444        let mut offset = 0;
445        while offset < char_offset {
446            if let Some(line) = self.value.get(self.row) {
447                if self.col >= line.len() || offset >= nli.char_width.saturating_sub(1) {
448                    break;
449                }
450                if let Some(&ch) = line.get(self.col) {
451                    offset += UnicodeWidthChar::width(ch).unwrap_or(0);
452                }
453                self.col += 1;
454            } else {
455                break;
456            }
457        }
458    }
459
460    /// Move to the beginning of input - port of Go's moveToBegin()
461    pub fn move_to_begin(&mut self) {
462        self.row = 0;
463        self.set_cursor(0);
464    }
465
466    /// Move to the end of input - port of Go's moveToEnd()
467    pub fn move_to_end(&mut self) {
468        self.row = self.value.len().saturating_sub(1);
469        if let Some(line) = self.value.get(self.row) {
470            self.set_cursor(line.len());
471        }
472    }
473
474    // Internal helper functions matching Go implementation structure
475
476    /// Port of Go's insertRunesFromUserInput
477    fn insert_runes_from_user_input(&mut self, mut runes: Vec<char>) {
478        // Clean up any special characters in the input
479        runes = self.sanitize_runes(runes);
480
481        if self.char_limit > 0 {
482            let avail_space = self.char_limit.saturating_sub(self.length());
483            if avail_space == 0 {
484                return;
485            }
486            if avail_space < runes.len() {
487                runes.truncate(avail_space);
488            }
489        }
490
491        // Split the input into lines
492        let mut lines = Vec::new();
493        let mut lstart = 0;
494
495        for (i, &r) in runes.iter().enumerate() {
496            if r == '\n' {
497                lines.push(runes[lstart..i].to_vec());
498                lstart = i + 1;
499            }
500        }
501
502        if lstart <= runes.len() {
503            lines.push(runes[lstart..].to_vec());
504        }
505
506        // Obey the maximum line limit
507        if MAX_LINES > 0 && self.value.len() + lines.len() - 1 > MAX_LINES {
508            let allowed_height = (MAX_LINES - self.value.len() + 1).max(0);
509            lines.truncate(allowed_height);
510        }
511
512        if lines.is_empty() {
513            return;
514        }
515
516        // Ensure current row exists
517        while self.row >= self.value.len() {
518            self.value.push(Vec::new());
519        }
520
521        // Save the remainder of the original line at the current cursor position
522        let tail = if self.col < self.value[self.row].len() {
523            self.value[self.row][self.col..].to_vec()
524        } else {
525            Vec::new()
526        };
527
528        // Paste the first line at the current cursor position
529        if self.col <= self.value[self.row].len() {
530            self.value[self.row].truncate(self.col);
531        }
532        self.value[self.row].extend_from_slice(&lines[0]);
533        self.col += lines[0].len();
534
535        if lines.len() > 1 {
536            // Add the new lines maintaining cursor on the first line's end
537            for (i, line) in lines[1..].iter().enumerate() {
538                self.value.insert(self.row + 1 + i, line.clone());
539            }
540            // Move cursor to end of the last inserted line (Go behavior on SetValue)
541            self.row += lines.len() - 1;
542            self.col = lines.last().map(|l| l.len()).unwrap_or(0);
543            // Append tail to current line
544            self.value[self.row].extend_from_slice(&tail);
545        } else {
546            // No newlines: append tail back to current line
547            self.value[self.row].extend_from_slice(&tail);
548        }
549
550        self.set_cursor(self.col);
551    }
552
553    /// Sanitize runes for input - simple version
554    fn sanitize_runes(&self, runes: Vec<char>) -> Vec<char> {
555        // For now, just return as-is. In Go this handles special characters
556        runes
557    }
558
559    /// LineInfo returns line information for the current cursor position
560    /// Port of Go's LineInfo() - enhanced with better wrapped line handling
561    pub fn line_info(&mut self) -> LineInfo {
562        if self.row >= self.value.len() {
563            return LineInfo::default();
564        }
565
566        // Clone the line to avoid borrowing issues
567        let current_line = self.value[self.row].clone();
568        let width = self.width;
569        let grid = self.cache.wrap(&current_line, width);
570
571        // Find out which visual wrap line we are currently on
572        let mut counter = 0;
573        for (i, line) in grid.iter().enumerate() {
574            // Check if cursor is exactly at the end of this wrapped line and should wrap to next
575            if counter + line.len() == self.col && i + 1 < grid.len() {
576                // We wrap around to the next visual line
577                let next_line = &grid[i + 1];
578                return LineInfo {
579                    char_offset: 0,
580                    column_offset: 0,
581                    height: grid.len(),
582                    row_offset: i + 1,
583                    start_column: self.col,
584                    width: next_line.len(),
585                    char_width: next_line
586                        .iter()
587                        .map(|&ch| UnicodeWidthChar::width(ch).unwrap_or(0))
588                        .sum(),
589                };
590            }
591
592            // Check if cursor falls within this wrapped line
593            if counter + line.len() >= self.col {
594                let col_in_line = self.col.saturating_sub(counter);
595                let char_off: usize = line
596                    .iter()
597                    .take(col_in_line.min(line.len()))
598                    .map(|&ch| UnicodeWidthChar::width(ch).unwrap_or(0))
599                    .sum();
600
601                return LineInfo {
602                    char_offset: char_off,
603                    column_offset: col_in_line,
604                    height: grid.len(),
605                    row_offset: i,
606                    start_column: counter,
607                    width: line.len(),
608                    char_width: line
609                        .iter()
610                        .map(|&ch| UnicodeWidthChar::width(ch).unwrap_or(0))
611                        .sum(),
612                };
613            }
614
615            counter += line.len();
616        }
617
618        // Cursor is past the end of all content - place at end of last line
619        if let Some(last_line) = grid.last() {
620            let last_counter = counter - last_line.len();
621            return LineInfo {
622                char_offset: last_line
623                    .iter()
624                    .map(|&ch| UnicodeWidthChar::width(ch).unwrap_or(0))
625                    .sum(),
626                column_offset: last_line.len(),
627                height: grid.len(),
628                row_offset: grid.len().saturating_sub(1),
629                start_column: last_counter,
630                width: last_line.len(),
631                char_width: last_line
632                    .iter()
633                    .map(|&ch| UnicodeWidthChar::width(ch).unwrap_or(0))
634                    .sum(),
635            };
636        }
637
638        LineInfo::default()
639    }
640
641    /// Delete before cursor - port of Go's deleteBeforeCursor()
642    pub fn delete_before_cursor(&mut self) {
643        if let Some(line) = self.value.get_mut(self.row) {
644            let tail = if self.col <= line.len() {
645                line[self.col..].to_vec()
646            } else {
647                Vec::new()
648            };
649            *line = tail;
650        }
651        self.set_cursor(0);
652    }
653
654    /// Delete after cursor - port of Go's deleteAfterCursor()
655    pub fn delete_after_cursor(&mut self) {
656        if let Some(line) = self.value.get_mut(self.row) {
657            line.truncate(self.col);
658            let line_len = line.len();
659            self.set_cursor(line_len);
660        }
661    }
662
663    /// Delete character backward - port of Go's deleteCharacterBackward()
664    pub fn delete_character_backward(&mut self) {
665        self.col = clamp(
666            self.col,
667            0,
668            self.value.get(self.row).map_or(0, |line| line.len()),
669        );
670        if self.col == 0 {
671            self.merge_line_above(self.row);
672            return;
673        }
674
675        if let Some(line) = self.value.get_mut(self.row) {
676            if !line.is_empty() && self.col > 0 {
677                line.remove(self.col - 1);
678                self.set_cursor(self.col - 1);
679            }
680        }
681    }
682
683    /// Delete character forward - port of Go's deleteCharacterForward()
684    pub fn delete_character_forward(&mut self) {
685        if let Some(line) = self.value.get_mut(self.row) {
686            if !line.is_empty() && self.col < line.len() {
687                line.remove(self.col);
688            }
689        }
690
691        if self.col >= self.value.get(self.row).map_or(0, |line| line.len()) {
692            self.merge_line_below(self.row);
693        }
694    }
695
696    /// Delete word backward - port of Go's deleteWordLeft()
697    pub fn delete_word_backward(&mut self) {
698        if self.col == 0 {
699            self.merge_line_above(self.row);
700            return;
701        }
702
703        let line = if let Some(line) = self.value.get(self.row) {
704            line.clone()
705        } else {
706            return;
707        };
708
709        if line.is_empty() {
710            return;
711        }
712
713        // Find word boundaries - Go bubbles deleteWordLeft behavior
714        let mut start = self.col;
715        let mut end = self.col;
716
717        // If we're not at the end of a word, find the end first
718        while end < line.len() && line.get(end).is_some_and(|&c| !c.is_whitespace()) {
719            end += 1;
720        }
721
722        // Find start of the word we're in or before
723        while start > 0 && line.get(start - 1).is_some_and(|&c| !c.is_whitespace()) {
724            start -= 1;
725        }
726
727        // Only include preceding space if cursor is not at end of word
728        if self.col < line.len() && line.get(self.col).is_some_and(|&c| !c.is_whitespace()) {
729            // Cursor is inside word, include preceding space
730            if start > 0 && line.get(start - 1).is_some_and(|&c| c.is_whitespace()) {
731                start -= 1;
732            }
733        }
734
735        if let Some(line_mut) = self.value.get_mut(self.row) {
736            let end_clamped = end.min(line_mut.len());
737            let start_clamped = start.min(end_clamped);
738            line_mut.drain(start_clamped..end_clamped);
739        }
740
741        self.set_cursor(start);
742    }
743
744    /// Delete word forward - port of Go's deleteWordRight()
745    pub fn delete_word_forward(&mut self) {
746        let line = if let Some(line) = self.value.get(self.row) {
747            line.clone()
748        } else {
749            return;
750        };
751
752        if self.col >= line.len() || line.is_empty() {
753            self.merge_line_below(self.row);
754            return;
755        }
756
757        let old_col = self.col;
758        let mut new_col = self.col;
759
760        // Skip whitespace
761        while new_col < line.len() {
762            if let Some(&ch) = line.get(new_col) {
763                if ch.is_whitespace() {
764                    new_col += 1;
765                } else {
766                    break;
767                }
768            } else {
769                break;
770            }
771        }
772
773        // Skip word characters
774        while new_col < line.len() {
775            if let Some(&ch) = line.get(new_col) {
776                if !ch.is_whitespace() {
777                    new_col += 1;
778                } else {
779                    break;
780                }
781            } else {
782                break;
783            }
784        }
785
786        // Delete the selected text
787        if let Some(line) = self.value.get_mut(self.row) {
788            if new_col > line.len() {
789                line.truncate(old_col);
790            } else {
791                line.drain(old_col..new_col);
792            }
793        }
794
795        self.set_cursor(old_col);
796    }
797
798    /// Merge line below - port of Go's mergeLineBelow()
799    fn merge_line_below(&mut self, row: usize) {
800        if row >= self.value.len().saturating_sub(1) {
801            return;
802        }
803
804        // Combine the two lines
805        if let Some(next_line) = self.value.get(row + 1).cloned() {
806            if let Some(current_line) = self.value.get_mut(row) {
807                current_line.extend_from_slice(&next_line);
808            }
809        }
810
811        // Remove the next line by shifting all lines up
812        self.value.remove(row + 1);
813    }
814
815    /// Merge line above - port of Go's mergeLineAbove()
816    fn merge_line_above(&mut self, row: usize) {
817        if row == 0 {
818            return;
819        }
820
821        if let Some(prev_line) = self.value.get(row - 1) {
822            self.col = prev_line.len();
823        }
824        self.row = row - 1;
825
826        // Combine the two lines
827        if let Some(current_line) = self.value.get(row).cloned() {
828            if let Some(prev_line) = self.value.get_mut(row - 1) {
829                prev_line.extend_from_slice(&current_line);
830            }
831        }
832
833        // Remove the current line
834        self.value.remove(row);
835    }
836
837    /// Split line - port of Go's splitLine()
838    fn split_line(&mut self, row: usize, col: usize) {
839        if let Some(line) = self.value.get(row) {
840            let head = line[..col].to_vec();
841            let tail = line[col..].to_vec();
842
843            // Replace current line with head
844            self.value[row] = head;
845
846            // Insert tail as new line
847            self.value.insert(row + 1, tail);
848
849            self.col = 0;
850            self.row += 1;
851        }
852    }
853
854    /// Insert newline - port of Go's InsertNewline()
855    pub fn insert_newline(&mut self) {
856        if self.max_height > 0 && self.value.len() >= self.max_height {
857            return;
858        }
859
860        self.col = clamp(
861            self.col,
862            0,
863            self.value.get(self.row).map_or(0, |line| line.len()),
864        );
865        self.split_line(self.row, self.col);
866    }
867
868    /// Move cursor one character left - port of Go's characterLeft()
869    pub fn character_left(&mut self, inside_line: bool) {
870        if self.col == 0 && self.row != 0 {
871            self.row -= 1;
872            if let Some(line) = self.value.get(self.row) {
873                self.col = line.len();
874                if !inside_line {
875                    return;
876                }
877            }
878        }
879        if self.col > 0 {
880            self.set_cursor(self.col - 1);
881        }
882    }
883
884    /// Move cursor one character right - port of Go's characterRight()
885    pub fn character_right(&mut self) {
886        if let Some(line) = self.value.get(self.row) {
887            if self.col < line.len() {
888                self.set_cursor(self.col + 1);
889            } else if self.row < self.value.len() - 1 {
890                self.row += 1;
891                self.cursor_start();
892            }
893        }
894    }
895
896    /// Move cursor one word left - port of Go's wordLeft()
897    pub fn word_left(&mut self) {
898        // Move left over any spaces
899        while self.col > 0 {
900            if let Some(line) = self.value.get(self.row) {
901                if line.get(self.col - 1).is_some_and(|c| c.is_whitespace()) {
902                    self.set_cursor(self.col - 1);
903                } else {
904                    break;
905                }
906            } else {
907                break;
908            }
909        }
910        // Then move left over the previous word
911        while self.col > 0 {
912            if let Some(line) = self.value.get(self.row) {
913                if line.get(self.col - 1).is_some_and(|c| !c.is_whitespace()) {
914                    self.set_cursor(self.col - 1);
915                } else {
916                    break;
917                }
918            } else {
919                break;
920            }
921        }
922    }
923
924    /// Move cursor one word right - port of Go's wordRight()
925    pub fn word_right(&mut self) {
926        self.do_word_right(|_, _| {});
927    }
928
929    /// Internal word right with callback - port of Go's doWordRight()  
930    fn do_word_right<F>(&mut self, mut func: F)
931    where
932        F: FnMut(usize, usize),
933    {
934        if self.row >= self.value.len() {
935            return;
936        }
937
938        let line = match self.value.get(self.row) {
939            Some(line) => line.clone(),
940            None => return,
941        };
942
943        if self.col >= line.len() {
944            return;
945        }
946
947        let mut pos = self.col;
948        let mut char_idx = 0;
949
950        // Skip any spaces at current position first
951        while pos < line.len() && line[pos].is_whitespace() {
952            pos += 1;
953        }
954
955        // Move through word characters until we reach whitespace or end
956        while pos < line.len() && !line[pos].is_whitespace() {
957            func(char_idx, pos);
958            pos += 1;
959            char_idx += 1;
960        }
961
962        // Update cursor position
963        self.set_cursor(pos);
964    }
965
966    /// Transform word to uppercase - port of Go's uppercaseRight()
967    pub fn uppercase_right(&mut self) {
968        let start_col = self.col;
969        let start_row = self.row;
970
971        // Find the word boundaries
972        self.word_right(); // Move to end of word
973        let end_col = self.col;
974
975        // Transform characters
976        if let Some(line) = self.value.get_mut(start_row) {
977            let end_idx = end_col.min(line.len());
978            if let Some(slice) = line.get_mut(start_col..end_idx) {
979                for ch in slice.iter_mut() {
980                    *ch = ch.to_uppercase().next().unwrap_or(*ch);
981                }
982            }
983        }
984    }
985
986    /// Transform word to lowercase - port of Go's lowercaseRight()  
987    pub fn lowercase_right(&mut self) {
988        let start_col = self.col;
989        let start_row = self.row;
990
991        // Find the word boundaries
992        self.word_right(); // Move to end of word
993        let end_col = self.col;
994
995        // Transform characters
996        if let Some(line) = self.value.get_mut(start_row) {
997            let end_idx = end_col.min(line.len());
998            if let Some(slice) = line.get_mut(start_col..end_idx) {
999                for ch in slice.iter_mut() {
1000                    *ch = ch.to_lowercase().next().unwrap_or(*ch);
1001                }
1002            }
1003        }
1004    }
1005
1006    /// Transform word to title case - port of Go's capitalizeRight()
1007    pub fn capitalize_right(&mut self) {
1008        let start_col = self.col;
1009        let start_row = self.row;
1010
1011        // Find the word boundaries
1012        self.word_right(); // Move to end of word
1013        let end_col = self.col;
1014
1015        // Transform characters
1016        if let Some(line) = self.value.get_mut(start_row) {
1017            let end_idx = end_col.min(line.len());
1018            if let Some(slice) = line.get_mut(start_col..end_idx) {
1019                for (i, ch) in slice.iter_mut().enumerate() {
1020                    if i == 0 {
1021                        *ch = ch.to_uppercase().next().unwrap_or(*ch);
1022                    }
1023                }
1024            }
1025        }
1026    }
1027
1028    /// Transpose characters - port of Go's transposeLeft()
1029    pub fn transpose_left(&mut self) {
1030        let row = self.row;
1031        let mut col = self.col;
1032
1033        if let Some(line) = self.value.get_mut(row) {
1034            if col == 0 || line.len() < 2 {
1035                return;
1036            }
1037
1038            if col >= line.len() {
1039                col -= 1;
1040                self.col = col;
1041            }
1042
1043            if col > 0 && col < line.len() {
1044                line.swap(col - 1, col);
1045                if col < line.len() {
1046                    self.col = col + 1;
1047                }
1048            }
1049        }
1050    }
1051
1052    /// View renders the text area - port of Go's View()
1053    pub fn view(&mut self) -> String {
1054        // Early return for empty placeholder case
1055        if self.value.is_empty() || (self.value.len() == 1 && self.value[0].is_empty()) {
1056            return self.placeholder_view();
1057        }
1058
1059        // Set cursor text style for rendering
1060        self.cursor.text_style = self.current_style.computed_cursor_line();
1061
1062        let mut s = String::new();
1063        let line_info = self.line_info();
1064        let style = &self.current_style;
1065
1066        // Track display lines and widest line number for padding
1067        let mut display_line = 0;
1068        let mut widest_line_number = 0;
1069
1070        // Process each document line
1071        for (doc_line_idx, line) in self.value.iter().enumerate() {
1072            let wrapped_lines = self.cache.wrap(line, self.width);
1073            let is_current_doc_line = doc_line_idx == self.row;
1074
1075            for (wrap_idx, wrapped_line) in wrapped_lines.iter().enumerate() {
1076                let prompt = self.get_prompt_string(display_line);
1077                s.push_str(&style.computed_prompt().render(&prompt));
1078                display_line += 1;
1079
1080                // Line numbers
1081                let mut ln = String::new();
1082                if self.show_line_numbers {
1083                    if wrap_idx == 0 {
1084                        if is_current_doc_line {
1085                            ln = style
1086                                .computed_cursor_line_number()
1087                                .render(&self.format_line_number(doc_line_idx + 1));
1088                        } else {
1089                            ln = style
1090                                .computed_line_number()
1091                                .render(&self.format_line_number(doc_line_idx + 1));
1092                        }
1093                    } else if is_current_doc_line {
1094                        ln = style
1095                            .computed_cursor_line_number()
1096                            .render(&self.format_line_number(""));
1097                    } else {
1098                        ln = style
1099                            .computed_line_number()
1100                            .render(&self.format_line_number(""));
1101                    }
1102                    s.push_str(&ln);
1103                }
1104
1105                // Track widest line number for padding
1106                let lnw = lipgloss::width(&ln);
1107                if lnw > widest_line_number {
1108                    widest_line_number = lnw;
1109                }
1110
1111                let strwidth = wrapped_line
1112                    .iter()
1113                    .map(|&ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0))
1114                    .sum::<usize>();
1115                let mut padding = self.width.saturating_sub(strwidth);
1116
1117                // Handle width overflow from trailing spaces
1118                if strwidth > self.width {
1119                    // Remove trailing space if it causes overflow
1120                    let content: String = wrapped_line
1121                        .iter()
1122                        .collect::<String>()
1123                        .trim_end()
1124                        .to_string();
1125                    let new_wrapped_line: Vec<char> = content.chars().collect();
1126                    let new_strwidth = new_wrapped_line
1127                        .iter()
1128                        .map(|&ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0))
1129                        .sum::<usize>();
1130                    padding = self.width.saturating_sub(new_strwidth);
1131                }
1132
1133                // Render cursor if on current line and wrap
1134                if is_current_doc_line && line_info.row_offset == wrap_idx {
1135                    let col_offset = line_info.column_offset;
1136
1137                    // Before cursor
1138                    let before: String = wrapped_line.iter().take(col_offset).collect();
1139                    s.push_str(&style.computed_cursor_line().render(&before));
1140
1141                    // Cursor
1142                    if self.col >= line.len() && line_info.char_offset >= self.width {
1143                        self.cursor.set_char(" ");
1144                        s.push_str(&self.cursor.view());
1145                    } else {
1146                        let cursor_char = wrapped_line.get(col_offset).unwrap_or(&' ');
1147                        self.cursor.set_char(&cursor_char.to_string());
1148                        s.push_str(&self.cursor.view());
1149
1150                        // After cursor
1151                        let after: String = wrapped_line.iter().skip(col_offset + 1).collect();
1152                        s.push_str(&style.computed_cursor_line().render(&after));
1153                    }
1154                } else {
1155                    // Regular line content
1156                    let content: String = wrapped_line.iter().collect();
1157                    let line_style = if is_current_doc_line {
1158                        style.computed_cursor_line()
1159                    } else {
1160                        style.computed_text()
1161                    };
1162                    s.push_str(&line_style.render(&content));
1163                }
1164
1165                // Add padding
1166                s.push_str(&style.computed_text().render(&" ".repeat(padding.max(0))));
1167                s.push('\n');
1168            }
1169        }
1170
1171        // Fill remaining height
1172        for _ in 0..(self.height.saturating_sub(display_line)) {
1173            let prompt = self.get_prompt_string(display_line);
1174            s.push_str(&style.computed_prompt().render(&prompt));
1175            display_line += 1;
1176
1177            let left_gutter = self.end_of_buffer_character.to_string();
1178            let right_gap_width =
1179                self.width().saturating_sub(lipgloss::width(&left_gutter)) + widest_line_number;
1180            let right_gap = " ".repeat(right_gap_width.max(0));
1181            s.push_str(
1182                &style
1183                    .computed_end_of_buffer()
1184                    .render(&(left_gutter + &right_gap)),
1185            );
1186            s.push('\n');
1187        }
1188
1189        s
1190    }
1191
1192    /// Get prompt string for a given display line - port of Go's getPromptString()
1193    fn get_prompt_string(&self, display_line: usize) -> String {
1194        if let Some(prompt_func) = self.prompt_func {
1195            let prompt = prompt_func(display_line);
1196            let pl = prompt.len();
1197            if pl < self.prompt_width {
1198                format!("{}{}", " ".repeat(self.prompt_width - pl), prompt)
1199            } else {
1200                prompt
1201            }
1202        } else {
1203            self.prompt.clone()
1204        }
1205    }
1206
1207    /// Format line number for display - port of Go's formatLineNumber()
1208    fn format_line_number(&self, x: impl std::fmt::Display) -> String {
1209        // Calculate digits based on max height to ensure consistent formatting
1210        let digits = if self.max_height > 0 {
1211            self.max_height.to_string().len()
1212        } else {
1213            3 // Default to 3 digits as in Go implementation
1214        };
1215        format!(" {:width$} ", x, width = digits)
1216    }
1217
1218    /// Render placeholder text - port of Go's placeholder view logic
1219    fn placeholder_view(&mut self) -> String {
1220        if self.placeholder.is_empty() {
1221            return String::new();
1222        }
1223
1224        let mut s = String::new();
1225
1226        // Split placeholder into lines
1227        let placeholder_lines: Vec<&str> = self.placeholder.lines().collect();
1228
1229        for i in 0..self.height {
1230            // Render prompt
1231            let prompt = self.get_prompt_string(i);
1232            s.push_str(&prompt);
1233
1234            // Render line numbers
1235            if self.show_line_numbers {
1236                let ln = if i == 0 {
1237                    self.format_line_number(1)
1238                } else {
1239                    self.format_line_number("")
1240                };
1241                s.push_str(&ln);
1242            }
1243
1244            // Render line content
1245            if i < placeholder_lines.len() {
1246                // Render placeholder line (format_line_number already includes spacing)
1247                s.push_str(placeholder_lines[i]);
1248            } else {
1249                // End of buffer character for empty lines
1250                if self.end_of_buffer_character != ' ' {
1251                    s.push(self.end_of_buffer_character);
1252                }
1253            }
1254
1255            s.push('\n');
1256        }
1257
1258        // Remove trailing newline to match expected format
1259        s.trim_end_matches('\n').to_string()
1260    }
1261
1262    /// Scroll viewport down by n lines - for testing viewport functionality
1263    pub fn scroll_down(&mut self, lines: usize) {
1264        self.viewport.set_y_offset(self.viewport.y_offset + lines);
1265    }
1266
1267    /// Scroll viewport up by n lines - for testing viewport functionality  
1268    pub fn scroll_up(&mut self, lines: usize) {
1269        self.viewport
1270            .set_y_offset(self.viewport.y_offset.saturating_sub(lines));
1271    }
1272
1273    /// Get cursor line number for display - port of Go's cursorLineNumber()
1274    pub fn cursor_line_number(&mut self) -> usize {
1275        if self.row >= self.value.len() {
1276            return 0;
1277        }
1278
1279        // Count visual lines from all preceding document lines
1280        let mut line_count = 0;
1281        for i in 0..self.row {
1282            if let Some(line) = self.value.get(i).cloned() {
1283                let wrapped_lines = self.cache.wrap(&line, self.width);
1284                line_count += wrapped_lines.len();
1285            }
1286        }
1287
1288        // Add the row offset within the current document line
1289        line_count += self.line_info().row_offset;
1290        line_count
1291    }
1292
1293    /// Reposition the viewport to keep the cursor in view - port of Go's repositionView()
1294    fn reposition_view(&mut self) {
1295        let cursor_line = self.cursor_line_number();
1296        let minimum = self.viewport.y_offset;
1297        let maximum = minimum + self.viewport.height.saturating_sub(1);
1298
1299        if cursor_line < minimum {
1300            // Cursor is above the visible area, scroll up
1301            self.viewport.set_y_offset(cursor_line);
1302        } else if cursor_line > maximum {
1303            // Cursor is below the visible area, scroll down
1304            let new_offset = cursor_line.saturating_sub(self.viewport.height.saturating_sub(1));
1305            self.viewport.set_y_offset(new_offset);
1306        }
1307    }
1308
1309    /// Update handles incoming messages and updates the textarea state - port of Go's Update()
1310    pub fn update(&mut self, msg: Option<bubbletea_rs::Msg>) -> Option<bubbletea_rs::Cmd> {
1311        if !self.focus {
1312            return None;
1313        }
1314
1315        if let Some(msg) = msg {
1316            // Handle clipboard messages first
1317            if let Some(paste_msg) = msg.downcast_ref::<PasteMsg>() {
1318                self.insert_string(paste_msg.0.clone());
1319                return None;
1320            }
1321
1322            if let Some(_paste_err) = msg.downcast_ref::<PasteErrMsg>() {
1323                // Handle paste error - could be logged or shown to user
1324                return None;
1325            }
1326
1327            // Handle key messages
1328            if let Some(key_msg) = msg.downcast_ref::<bubbletea_rs::KeyMsg>() {
1329                return self.handle_key_msg(key_msg);
1330            }
1331
1332            // Pass other messages to cursor and viewport
1333            let cursor_cmd = self.cursor.update(&msg);
1334            let viewport_cmd = self.viewport.update(msg);
1335
1336            // Return the first available command
1337            cursor_cmd.or(viewport_cmd)
1338        } else {
1339            None
1340        }
1341    }
1342
1343    /// Handle key messages - port of Go's key handling logic
1344    fn handle_key_msg(&mut self, key_msg: &bubbletea_rs::KeyMsg) -> Option<bubbletea_rs::Cmd> {
1345        // Store old cursor position to determine if cursor moved
1346        let old_row = self.row;
1347        let old_col = self.col;
1348
1349        // Handle different types of key operations
1350        if let Some(cmd) = self.handle_clipboard_keys(key_msg) {
1351            return Some(cmd);
1352        }
1353
1354        self.handle_movement_keys(key_msg);
1355        self.handle_deletion_keys(key_msg);
1356        self.handle_text_operations(key_msg);
1357        self.handle_text_insertion(key_msg);
1358        self.handle_character_input(key_msg);
1359
1360        // Reposition viewport if cursor moved or content changed
1361        if self.row != old_row || self.col != old_col {
1362            self.reposition_view();
1363        }
1364
1365        None
1366    }
1367
1368    /// Handle clipboard-related key bindings
1369    fn handle_clipboard_keys(
1370        &mut self,
1371        key_msg: &bubbletea_rs::KeyMsg,
1372    ) -> Option<bubbletea_rs::Cmd> {
1373        use crate::key::matches_binding;
1374
1375        if matches_binding(key_msg, &self.key_map.paste) {
1376            return Some(self.paste_command());
1377        }
1378
1379        None
1380    }
1381
1382    /// Handle movement-related key bindings
1383    fn handle_movement_keys(&mut self, key_msg: &bubbletea_rs::KeyMsg) {
1384        use crate::key::matches_binding;
1385
1386        // Character movement
1387        if matches_binding(key_msg, &self.key_map.character_forward) {
1388            self.character_right();
1389        } else if matches_binding(key_msg, &self.key_map.character_backward) {
1390            self.character_left(false);
1391        // Word movement
1392        } else if matches_binding(key_msg, &self.key_map.word_forward) {
1393            self.word_right();
1394        } else if matches_binding(key_msg, &self.key_map.word_backward) {
1395            self.word_left();
1396        // Line movement
1397        } else if matches_binding(key_msg, &self.key_map.line_next) {
1398            self.cursor_down();
1399        } else if matches_binding(key_msg, &self.key_map.line_previous) {
1400            self.cursor_up();
1401        } else if matches_binding(key_msg, &self.key_map.line_start) {
1402            self.cursor_start();
1403        } else if matches_binding(key_msg, &self.key_map.line_end) {
1404            self.cursor_end();
1405        // Input navigation
1406        } else if matches_binding(key_msg, &self.key_map.input_begin) {
1407            self.move_to_begin();
1408        } else if matches_binding(key_msg, &self.key_map.input_end) {
1409            self.move_to_end();
1410        }
1411    }
1412
1413    /// Handle deletion-related key bindings
1414    fn handle_deletion_keys(&mut self, key_msg: &bubbletea_rs::KeyMsg) {
1415        use crate::key::matches_binding;
1416
1417        if matches_binding(key_msg, &self.key_map.delete_character_backward) {
1418            self.delete_character_backward();
1419        } else if matches_binding(key_msg, &self.key_map.delete_character_forward) {
1420            self.delete_character_forward();
1421        } else if matches_binding(key_msg, &self.key_map.delete_word_backward) {
1422            self.delete_word_backward();
1423        } else if matches_binding(key_msg, &self.key_map.delete_word_forward) {
1424            self.delete_word_forward();
1425        } else if matches_binding(key_msg, &self.key_map.delete_after_cursor) {
1426            self.delete_after_cursor();
1427        } else if matches_binding(key_msg, &self.key_map.delete_before_cursor) {
1428            self.delete_before_cursor();
1429        }
1430    }
1431
1432    /// Handle text transformation operations
1433    fn handle_text_operations(&mut self, key_msg: &bubbletea_rs::KeyMsg) {
1434        use crate::key::matches_binding;
1435
1436        if matches_binding(key_msg, &self.key_map.uppercase_word_forward) {
1437            self.uppercase_right();
1438        } else if matches_binding(key_msg, &self.key_map.lowercase_word_forward) {
1439            self.lowercase_right();
1440        } else if matches_binding(key_msg, &self.key_map.capitalize_word_forward) {
1441            self.capitalize_right();
1442        } else if matches_binding(key_msg, &self.key_map.transpose_character_backward) {
1443            self.transpose_left();
1444        }
1445    }
1446
1447    /// Handle text insertion operations
1448    fn handle_text_insertion(&mut self, key_msg: &bubbletea_rs::KeyMsg) {
1449        use crate::key::matches_binding;
1450
1451        if matches_binding(key_msg, &self.key_map.insert_newline) {
1452            self.insert_newline();
1453        }
1454    }
1455
1456    /// Handle regular character input
1457    fn handle_character_input(&mut self, key_msg: &bubbletea_rs::KeyMsg) {
1458        // Handle regular character input
1459        if let Some(ch) = self.extract_character_from_key_msg(key_msg) {
1460            if ch.is_control() {
1461                // Ignore control characters that aren't handled above
1462                return;
1463            }
1464            self.insert_rune(ch);
1465        }
1466    }
1467
1468    /// Extract character from key message for regular text input
1469    fn extract_character_from_key_msg(&self, key_msg: &bubbletea_rs::KeyMsg) -> Option<char> {
1470        use crossterm::event::{KeyCode, KeyModifiers};
1471
1472        // Only extract printable characters without control modifiers
1473        // Skip if Control or Alt modifiers are pressed (these are for shortcuts)
1474        if key_msg.modifiers.contains(KeyModifiers::CONTROL)
1475            || key_msg.modifiers.contains(KeyModifiers::ALT)
1476        {
1477            return None;
1478        }
1479
1480        match key_msg.key {
1481            KeyCode::Char(c) => {
1482                // Accept all printable characters
1483                if c.is_control() {
1484                    None
1485                } else {
1486                    Some(c)
1487                }
1488            }
1489            KeyCode::Tab => Some('\t'),
1490            _ => None,
1491        }
1492    }
1493
1494    /// Create paste command for clipboard integration - port of Go's Paste()
1495    fn paste_command(&self) -> bubbletea_rs::Cmd {
1496        // Use tick with minimal duration to trigger clipboard read
1497        bubbletea_rs::tick(
1498            std::time::Duration::from_millis(1),
1499            |_| match Self::read_clipboard() {
1500                Ok(content) => Box::new(PasteMsg(content)) as bubbletea_rs::Msg,
1501                Err(err) => Box::new(PasteErrMsg(err)) as bubbletea_rs::Msg,
1502            },
1503        )
1504    }
1505
1506    /// Read from system clipboard - matches textinput implementation
1507    fn read_clipboard() -> Result<String, String> {
1508        #[cfg(feature = "clipboard-support")]
1509        {
1510            use clipboard::{ClipboardContext, ClipboardProvider};
1511
1512            let res: Result<String, String> = (|| {
1513                let mut ctx: ClipboardContext = ClipboardProvider::new()
1514                    .map_err(|e| format!("Failed to create clipboard context: {}", e))?;
1515                ctx.get_contents()
1516                    .map_err(|e| format!("Failed to read clipboard: {}", e))
1517            })();
1518            res
1519        }
1520        #[cfg(not(feature = "clipboard-support"))]
1521        {
1522            // Return empty string instead of error to avoid disrupting workflow
1523            Ok(String::new())
1524        }
1525    }
1526
1527    /// Copy text to system clipboard
1528    pub fn copy_to_clipboard(&self, text: &str) -> Result<(), String> {
1529        #[cfg(feature = "clipboard-support")]
1530        {
1531            use clipboard::{ClipboardContext, ClipboardProvider};
1532
1533            let mut ctx: ClipboardContext = ClipboardProvider::new()
1534                .map_err(|e| format!("Failed to create clipboard context: {}", e))?;
1535
1536            ctx.set_contents(text.to_string())
1537                .map_err(|e| format!("Failed to write to clipboard: {}", e))
1538        }
1539        #[cfg(not(feature = "clipboard-support"))]
1540        {
1541            let _ = text; // Suppress unused parameter warning
1542            Err("Clipboard support not enabled".to_string())
1543        }
1544    }
1545
1546    /// Copy current selection to clipboard (if selection is implemented)
1547    pub fn copy_selection(&self) -> Result<(), String> {
1548        // For now, copy entire content
1549        // In a full implementation, this would copy only selected text
1550        let content = self.value();
1551        self.copy_to_clipboard(&content)
1552    }
1553
1554    /// Cut current selection to clipboard (if selection is implemented)
1555    pub fn cut_selection(&mut self) -> Result<(), String> {
1556        // For now, cut entire content
1557        // In a full implementation, this would cut only selected text
1558        let content = self.value();
1559        self.copy_to_clipboard(&content)?;
1560        self.reset();
1561        Ok(())
1562    }
1563}
1564
1565impl Default for Model {
1566    fn default() -> Self {
1567        Self::new()
1568    }
1569}
1570
1571/// Focus sets the focus state on the model - port of Go's Focus()
1572impl Component for Model {
1573    fn focus(&mut self) -> Option<Cmd> {
1574        self.focus = true;
1575        self.current_style = self.focused_style.clone();
1576        self.cursor.focus()
1577    }
1578
1579    fn blur(&mut self) {
1580        self.focus = false;
1581        self.current_style = self.blurred_style.clone();
1582        self.cursor.blur();
1583    }
1584
1585    fn focused(&self) -> bool {
1586        self.focus
1587    }
1588}
1589
1590/// Default styles matching Go's DefaultStyles() function
1591pub fn default_styles() -> (TextareaStyle, TextareaStyle) {
1592    let focused = default_focused_style();
1593    let blurred = default_blurred_style();
1594    (focused, blurred)
1595}
1596
1597/// Create a new textarea model - convenience function
1598pub fn new() -> Model {
1599    Model::new()
1600}