Skip to main content

rusty_rich/
text.rs

1//! Text with spans — equivalent to Rich's `text.py`.
2//!
3//! `Text` is a styled string: plain text plus a collection of `Span`s that
4//! mark regions with specific styles.
5
6use std::fmt;
7
8use regex::Regex;
9use unicode_width::UnicodeWidthStr;
10
11use crate::align::AlignMethod;
12use crate::style::Style;
13
14// ---------------------------------------------------------------------------
15// Span
16// ---------------------------------------------------------------------------
17
18/// A marked-up region in some text.
19#[derive(Debug, Clone, PartialEq)]
20pub struct Span {
21    pub start: usize,
22    pub end: usize,
23    pub style: Style,
24}
25
26impl Span {
27    /// Create a new `Span` covering byte range `[start, end)` with a given style.
28    pub fn new(start: usize, end: usize, style: Style) -> Self {
29        Self { start, end, style }
30    }
31
32    /// Check if this span is non-empty.
33    pub fn is_empty(&self) -> bool {
34        self.end <= self.start
35    }
36
37    /// Split a span at the given offset, producing two spans.
38    /// The first covers [start, offset), the second [offset, end).
39    pub fn split(&self, offset: usize) -> (Self, Option<Self>) {
40        if offset <= self.start || offset >= self.end {
41            return (self.clone(), None);
42        }
43        let span1 = Self::new(self.start, self.end.min(offset), self.style.clone());
44        let span2 = Self::new(span1.end, self.end, self.style.clone());
45        (span1, Some(span2))
46    }
47
48    /// Move start and end by a given offset.
49    pub fn move_by(&self, offset: isize) -> Self {
50        let start = (self.start as isize + offset).max(0) as usize;
51        let end = (self.end as isize + offset).max(0) as usize;
52        Self::new(start, end, self.style.clone())
53    }
54
55    /// Crop the span end to not exceed `offset`.
56    pub fn right_crop(&self, offset: usize) -> Self {
57        if offset >= self.end {
58            self.clone()
59        } else {
60            Self::new(self.start, self.end.min(offset), self.style.clone())
61        }
62    }
63}
64
65// ---------------------------------------------------------------------------
66// Text
67// ---------------------------------------------------------------------------
68
69/// A renderable piece of text with optional style spans.
70#[derive(Debug, Clone)]
71pub struct Text {
72    /// The plain text content.
73    pub plain: String,
74    /// Style spans applied over the text.
75    pub spans: Vec<Span>,
76    /// Default style for the entire text.
77    pub style: Style,
78    /// Justification method.
79    pub justify: JustifyMethod,
80    /// End string (appended during rendering).
81    pub end: String,
82    /// Overflow method.
83    pub overflow: OverflowMethod,
84    /// If true, don't wrap.
85    pub no_wrap: bool,
86    /// Tab width for expand_tabs (default 8).
87    pub tab_size: usize,
88    /// Whether to show indent guide lines.
89    pub indent_guides: bool,
90}
91
92pub type JustifyMethod = crate::align::AlignMethod;
93pub type OverflowMethod = crate::console::OverflowMethod;
94
95impl Text {
96    /// Create a new Text with the given plain content.
97    pub fn new(plain: impl Into<String>) -> Self {
98        Self {
99            plain: plain.into(),
100            spans: Vec::new(),
101            style: Style::new(),
102            justify: JustifyMethod::Left,
103            end: "\n".to_string(),
104            overflow: OverflowMethod::Fold,
105            no_wrap: false,
106            tab_size: 8,
107            indent_guides: false,
108        }
109    }
110
111    /// Create a styled Text with a default style but no content.
112    pub fn styled(style: Style) -> Self {
113        Self {
114            plain: String::new(),
115            spans: Vec::new(),
116            style,
117            justify: JustifyMethod::Left,
118            end: "\n".to_string(),
119            overflow: OverflowMethod::Fold,
120            no_wrap: false,
121            tab_size: 8,
122            indent_guides: false,
123        }
124    }
125
126    /// Parse an ANSI-escaped string into a styled Text.
127    ///
128    /// Extracts SGR (Select Graphic Rendition) codes as style spans and strips
129    /// all other ANSI escape sequences.
130    pub fn from_ansi(text: &str) -> Self {
131        let re = Regex::new(r"\x1b\[([\d;]*)([a-zA-Z])").unwrap();
132        let mut plain = String::new();
133        let mut spans: Vec<Span> = Vec::new();
134        let mut current_style = Style::new();
135        let mut last_end = 0usize;
136
137        for cap in re.captures_iter(text) {
138            let m = cap.get(0).unwrap();
139            let match_start = m.start();
140            let match_end = m.end();
141            let cmd = cap.get(2).map_or("", |m| m.as_str());
142
143            // Text before this escape sequence
144            if match_start > last_end {
145                let segment = &text[last_end..match_start];
146                let start = plain.len();
147                plain.push_str(segment);
148                let end = plain.len();
149                if !current_style.is_plain() && start < end {
150                    spans.push(Span::new(start, end, current_style.clone()));
151                }
152            }
153
154            // Only parse SGR (m command), strip everything else
155            if cmd == "m" {
156                let params_str = cap.get(1).map_or("", |m| m.as_str());
157                apply_sgr(&mut current_style, params_str);
158            }
159
160            last_end = match_end;
161        }
162
163        // Remaining text after last escape
164        if last_end < text.len() {
165            let segment = &text[last_end..];
166            let start = plain.len();
167            plain.push_str(segment);
168            let end = plain.len();
169            if !current_style.is_plain() && start < end {
170                spans.push(Span::new(start, end, current_style.clone()));
171            }
172        }
173
174        Self {
175            plain,
176            spans,
177            style: Style::new(),
178            justify: JustifyMethod::Left,
179            end: "\n".to_string(),
180            overflow: OverflowMethod::Fold,
181            no_wrap: false,
182            tab_size: 8,
183            indent_guides: false,
184        }
185    }
186
187    /// Parse a BBCode-like markup string into a styled Text.
188    /// Delegates to [`crate::markup::render()`].
189    pub fn from_markup(markup: &str) -> Self {
190        crate::markup::render(markup)
191    }
192
193    /// Get a reference to the default style.
194    pub fn get_style(&self) -> &Style {
195        &self.style
196    }
197
198    /// Get a mutable reference to the default style.
199    pub fn get_style_mut(&mut self) -> &mut Style {
200        &mut self.style
201    }
202
203    /// Get a reference to the spans slice.
204    pub fn spans(&self) -> &[Span] {
205        &self.spans
206    }
207
208    /// Builder: set the default style.
209    pub fn style(mut self, style: Style) -> Self {
210        self.style = style;
211        self
212    }
213
214    /// Builder: set the justification.
215    pub fn justify(mut self, justify: JustifyMethod) -> Self {
216        self.justify = justify;
217        self
218    }
219
220    /// Builder: set the end string.
221    pub fn end(mut self, end: impl Into<String>) -> Self {
222        self.end = end.into();
223        self
224    }
225
226    /// Builder: set overflow method.
227    pub fn overflow(mut self, overflow: OverflowMethod) -> Self {
228        self.overflow = overflow;
229        self
230    }
231
232    /// Builder: set no_wrap flag.
233    pub fn no_wrap(mut self, value: bool) -> Self {
234        self.no_wrap = value;
235        self
236    }
237
238    /// Builder: set tab size for expand_tabs.
239    pub fn tab_size(mut self, size: usize) -> Self {
240        self.tab_size = size;
241        self
242    }
243
244    /// Builder: show indent guides.
245    pub fn with_indent_guides(mut self, show: bool) -> Self {
246        self.indent_guides = show;
247        self
248    }
249
250    /// Append another Text or string to this one, with an optional style.
251    pub fn append(&mut self, text: impl Into<Text>, style: Option<Style>) {
252        let text: Text = text.into();
253        let offset = self.plain.len();
254        self.plain.push_str(&text.plain);
255
256        // Shift and add spans from the appended text
257        for span in &text.spans {
258            let mut s = span.clone();
259            s.start += offset;
260            s.end += offset;
261            self.spans.push(s);
262        }
263
264        // If a style is given, add a span for the appended text
265        if let Some(st) = style {
266            self.spans.push(Span::new(
267                offset,
268                offset + text.plain.len(),
269                st,
270            ));
271        }
272    }
273
274    /// Append a sequence of (text, style) tokens.
275    pub fn append_tokens(&mut self, tokens: Vec<(String, Style)>) {
276        for (text, style) in tokens {
277            self.append_styled(text, style);
278        }
279    }
280
281    /// Append a plain string with a style.
282    pub fn append_styled(&mut self, text: impl Into<String>, style: Style) {
283        let text = text.into();
284        let offset = self.plain.len();
285        self.plain.push_str(&text);
286        self.spans.push(Span::new(offset, offset + text.len(), style));
287    }
288
289    /// Get the cell length of the plain text.
290    pub fn cell_len(&self) -> usize {
291        UnicodeWidthStr::width(self.plain.as_str())
292    }
293
294    /// Get the style at a given position, combining spans.
295    pub fn style_at(&self, position: usize) -> Style {
296        let mut style = self.style.clone();
297        for span in &self.spans {
298            if position >= span.start && position < span.end {
299                style = style.combine(&span.style);
300            }
301        }
302        style
303    }
304
305    /// Get the combined style at a specific byte offset.
306    /// Iterates through spans that cover the offset and combines them.
307    pub fn get_style_at_offset(&self, offset: usize) -> Style {
308        self.style_at(offset)
309    }
310
311    /// Truncate the text to the given maximum width.
312    pub fn truncate(&mut self, max_width: usize, overflow: OverflowMethod) {
313        let w = self.cell_len();
314        if w <= max_width {
315            return;
316        }
317
318        match overflow {
319            OverflowMethod::Ellipsis => {
320                let ellipsis = "\u{2026}";
321                let ellip_w = UnicodeWidthStr::width(ellipsis);
322                if max_width <= ellip_w {
323                    self.plain = ellipsis[..max_width].to_string();
324                    self.spans.clear();
325                    return;
326                }
327                // Walk chars from the left until we reach max_width - ellipsis width
328                let target = max_width - ellip_w;
329                let mut byte_pos = 0usize;
330                let mut w_count = 0usize;
331                for (i, ch) in self.plain.char_indices() {
332                    let cw = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
333                    if w_count + cw > target {
334                        break;
335                    }
336                    w_count += cw;
337                    byte_pos = i + ch.len_utf8();
338                }
339                self.plain.truncate(byte_pos);
340                self.plain.push_str(ellipsis);
341                // Crop spans
342                let crop_at = byte_pos;
343                self.spans.retain(|s| s.start < crop_at);
344                for s in &mut self.spans {
345                    if s.end > crop_at {
346                        s.end = crop_at;
347                    }
348                }
349            }
350            OverflowMethod::Crop => {
351                // Walk chars up to max_width
352                let mut w_count = 0usize;
353                let mut byte_pos = 0usize;
354                for (i, ch) in self.plain.char_indices() {
355                    let cw = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
356                    if w_count + cw > max_width {
357                        break;
358                    }
359                    w_count += cw;
360                    byte_pos = i + ch.len_utf8();
361                }
362                self.plain.truncate(byte_pos);
363                let crop_at = byte_pos;
364                self.spans.retain(|s| s.start < crop_at);
365                for s in &mut self.spans {
366                    if s.end > crop_at {
367                        s.end = crop_at;
368                    }
369                }
370            }
371            _ => {} // Fold / Ignore: don't truncate
372        }
373    }
374
375    /// Expand tab characters to spaces.
376    pub fn expand_tabs(&mut self) {
377        let tab_width = self.tab_size;
378        let mut result = String::new();
379        let mut col = 0usize;
380        for ch in self.plain.chars() {
381            if ch == '\t' {
382                let spaces = tab_width - (col % tab_width);
383                result.push_str(&" ".repeat(spaces));
384                col += spaces;
385            } else {
386                result.push(ch);
387                col += unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
388            }
389        }
390        self.plain = result;
391    }
392
393    /// Split the text into lines.
394    pub fn split_lines(&self) -> Vec<Text> {
395        self.plain
396            .split('\n')
397            .map(|line| Text::new(line.to_string()))
398            .collect()
399    }
400
401    /// Apply a style to a range of text.
402    /// Equivalent to Python's `Text.stylize()`.
403    pub fn stylize(&mut self, style: Style, start: usize, end: Option<usize>) {
404        let end = end.unwrap_or(self.plain.len());
405        // Clamp end to plain length to prevent out-of-bounds spans
406        let end = end.min(self.plain.len());
407        if start < end && start < self.plain.len() {
408            self.spans.push(Span::new(start, end, style));
409        }
410    }
411
412    /// Like [`stylize`] but inserts the span at the beginning of the spans
413    /// list, giving it lower priority than existing spans.
414    pub fn stylize_before(&mut self, style: Style, start: usize, end: Option<usize>) {
415        let end = end.unwrap_or(self.plain.len());
416        if start < end && start < self.plain.len() {
417            self.spans.insert(0, Span::new(start, end, style));
418        }
419    }
420
421    /// Apply metadata to the text at the given spans.
422    /// Sets the style.meta field on overlapping spans.
423    pub fn apply_meta(&mut self, meta: Vec<u8>, spans: &[Span]) {
424        for span in spans {
425            let start = span.start;
426            let end = span.end;
427            // Apply meta to existing spans that overlap
428            for existing in &mut self.spans {
429                if existing.start < end && existing.end > start {
430                    existing.style.meta = Some(meta.clone());
431                }
432            }
433            // Add a span for this region with the meta
434            self.spans.push(Span::new(start, end, {
435                let mut s = Style::new();
436                s.meta = Some(meta.clone());
437                s
438            }));
439        }
440    }
441
442    /// Highlight all regex matches with the given style. Returns count of
443    /// matches. Equivalent to Python's `Text.highlight_regex()`.
444    pub fn highlight_regex(&mut self, pattern: &str, style: Style) -> usize {
445        let re = regex::Regex::new(pattern);
446        let re = match re {
447            Ok(r) => r,
448            Err(_) => return 0,
449        };
450
451        let mut count = 0usize;
452        // Find all matches in the plain text
453        let matches: Vec<(usize, usize)> = re
454            .find_iter(&self.plain)
455            .map(|m| (m.start(), m.end()))
456            .collect();
457
458        for (start, end) in matches {
459            self.spans.push(Span::new(start, end, style.clone()));
460            count += 1;
461        }
462        count
463    }
464
465    /// Return a new Text with the same style settings but empty content.
466    pub fn blank_copy(&self) -> Self {
467        Self {
468            plain: String::new(),
469            spans: Vec::new(),
470            style: self.style.clone(),
471            justify: self.justify,
472            end: self.end.clone(),
473            overflow: self.overflow,
474            no_wrap: self.no_wrap,
475            tab_size: self.tab_size,
476            indent_guides: self.indent_guides,
477        }
478    }
479
480    /// Return a copy with only the plain text (no spans, default style).
481    pub fn copy_styles(&self) -> Self {
482        Self::new(self.plain.clone())
483    }
484
485    /// Detect the leading whitespace indentation.
486    /// Returns (indent_string, indent_count) where indent_count is the number
487    /// of occurrences of the first indent character.
488    pub fn detect_indentation(&self) -> (String, usize) {
489        let trimmed_start = self.plain.len() - self.plain.trim_start().len();
490        if trimmed_start == 0 {
491            return (String::new(), 0);
492        }
493        let indent_str = self.plain[..trimmed_start].to_string();
494        let first_char = indent_str.chars().next().unwrap_or(' ');
495        let indent_count = indent_str.chars().filter(|&c| c == first_char).count();
496        (indent_str, indent_count)
497    }
498
499    /// Split the text at the given byte offsets into multiple pieces,
500    /// preserving style spans.
501    pub fn divide(&self, offsets: &[usize]) -> Vec<Text> {
502        let mut result: Vec<Text> = Vec::new();
503        let mut prev = 0usize;
504        for &offset in offsets {
505            if offset <= prev || offset > self.plain.len() {
506                continue;
507            }
508            result.push(self.slice(prev, offset));
509            prev = offset;
510        }
511        if prev < self.plain.len() {
512            result.push(self.slice(prev, self.plain.len()));
513        }
514        result
515    }
516
517    /// Extract a sub-text covering byte range [start, end), preserving spans.
518    fn slice(&self, start: usize, end: usize) -> Text {
519        let piece = self.plain[start..end].to_string();
520        let mut text = Text::new(piece);
521        text.style = self.style.clone();
522        for span in &self.spans {
523            if span.start < end && span.end > start {
524                let s_start = if span.start > start { span.start - start } else { 0 };
525                let s_end = if span.end < end { span.end - start } else { end - start };
526                if s_start < s_end {
527                    text.spans.push(Span::new(s_start, s_end, span.style.clone()));
528                }
529            }
530        }
531        text
532    }
533
534    /// Apply a style to the entire text by adding a full-length span.
535    pub fn extend_style(&mut self, style: Style) {
536        if !self.plain.is_empty() {
537            self.spans.push(Span::new(0, self.plain.len(), style));
538        }
539    }
540
541    /// Truncate or pad to exactly fit `width` cells.
542    /// Uses existing truncate + align methods.
543    pub fn fit(&self, width: usize) -> Text {
544        let mut copy = self.clone();
545        let cell_len = copy.cell_len();
546        if cell_len > width {
547            copy.truncate(width, OverflowMethod::Crop);
548        } else if cell_len < width {
549            copy.align(AlignMethod::Left, width);
550        }
551        copy
552    }
553
554    /// Pad with spaces or truncate to exact `length` cells.
555    pub fn set_length(&mut self, length: usize) {
556        let cell_len = self.cell_len();
557        if cell_len > length {
558            self.truncate(length, OverflowMethod::Crop);
559        } else if cell_len < length {
560            self.pad_right(length - cell_len, ' ');
561        }
562    }
563
564    /// Remove a suffix from the text, returning true if it was present.
565    pub fn remove_suffix(&mut self, suffix: &str) -> bool {
566        if self.plain.ends_with(suffix) {
567            let end = self.plain.len() - suffix.len();
568            self.plain.truncate(end);
569            self.spans.retain(|s| s.start < end);
570            for s in &mut self.spans {
571                if s.end > end {
572                    s.end = end;
573                }
574            }
575            true
576        } else {
577            false
578        }
579    }
580
581    /// Remove a trailing end string, returning true if it was present.
582    pub fn rstrip_end(&mut self, end: &str) -> bool {
583        self.remove_suffix(end)
584    }
585
586    /// Crop all text beyond `offset`. Returns the cropped (removed) portion.
587    pub fn right_crop(&mut self, offset: usize) -> Text {
588        if offset >= self.plain.len() {
589            // Nothing to crop
590            return Text::new("");
591        }
592
593        let cropped_text = self.plain[offset..].to_string();
594        let mut cropped = Text::new(&*cropped_text);
595        cropped.style = self.style.clone();
596
597        self.plain.truncate(offset);
598
599        // Split spans at the boundary
600        let mut kept_spans: Vec<Span> = Vec::new();
601        let mut cropped_spans: Vec<Span> = Vec::new();
602        for span in &self.spans {
603            if span.start < offset {
604                let mut s = span.clone();
605                if s.end > offset {
606                    // Span crosses the boundary
607                    cropped_spans.push(Span::new(0, s.end - offset, span.style.clone()));
608                    s.end = offset;
609                }
610                kept_spans.push(s);
611            } else {
612                // Entirely in the cropped portion
613                cropped_spans.push(Span::new(
614                    span.start - offset,
615                    span.end - offset,
616                    span.style.clone(),
617                ));
618            }
619        }
620        self.spans = kept_spans;
621        cropped.spans = cropped_spans;
622        cropped
623    }
624
625    /// Remove trailing whitespace.
626    pub fn rstrip(&mut self) -> &mut Self {
627        let trimmed_end = self.plain.len() - self.plain.trim_end().len();
628        if trimmed_end > 0 {
629            let new_len = self.plain.len() - trimmed_end;
630            self.plain.truncate(new_len);
631            self.spans.retain(|s| s.start < new_len);
632            for s in &mut self.spans {
633                if s.end > new_len {
634                    s.end = new_len;
635                }
636            }
637        }
638        self
639    }
640
641    /// Split at every character boundary, producing per-character Text pieces.
642    pub fn split(&self) -> Vec<Text> {
643        let mut result: Vec<Text> = Vec::new();
644        let mut byte_pos = 0usize;
645        for ch in self.plain.chars() {
646            let ch_len = ch.len_utf8();
647            let ch_str = &self.plain[byte_pos..byte_pos + ch_len];
648            let mut text = Text::new(ch_str.to_string());
649            text.style = self.style_at(byte_pos);
650            result.push(text);
651            byte_pos += ch_len;
652        }
653        result
654    }
655
656    /// Simple word-wrap: split text into lines, each not exceeding `width`
657    /// cells. Returns a `Vec<Text>`, one per wrapped line.
658    /// Equivalent to Python's `Text.wrap()`.
659    pub fn wrap(&self, width: usize) -> Vec<Text> {
660        let mut lines: Vec<Text> = Vec::new();
661        let mut current = Text::new("");
662
663        for word in self.plain.split_whitespace() {
664            let word_w = unicode_width::UnicodeWidthStr::width(word);
665            let cur_w = current.cell_len();
666
667            if cur_w == 0 {
668                // First word
669                current = Text::new(word);
670            } else if cur_w + 1 + word_w <= width {
671                // Fits on current line
672                current.plain.push(' ');
673                current.plain.push_str(word);
674            } else {
675                // Start new line
676                if !current.plain.is_empty() {
677                    lines.push(current);
678                }
679                current = Text::new(word);
680            }
681        }
682
683        if !current.plain.is_empty() {
684            lines.push(current);
685        }
686
687        lines
688    }
689
690    /// Render the text, applying styles and returning a styled string.
691    pub fn render(&self) -> String {
692        // Simple: apply spans as ANSI codes
693        if self.spans.is_empty() && self.style.is_plain() {
694            return self.plain.clone();
695        }
696
697        let mut out = String::new();
698        let chars: Vec<(usize, char)> = self.plain.char_indices().collect();
699        let default_ansi = self.style.to_ansi();
700        let reset = if default_ansi.is_empty() { "" } else { "\x1b[0m" };
701
702        if !default_ansi.is_empty() {
703            out.push_str(&default_ansi);
704        }
705
706        for (byte_pos, ch) in &chars {
707            // Apply any spans that start at this position
708            let mut applied = String::new();
709            for span in &self.spans {
710                if span.start == *byte_pos {
711                    applied.push_str(&span.style.to_ansi());
712                }
713            }
714            out.push_str(&applied);
715
716            // Output the character
717            out.push(*ch);
718
719            // End any spans that finish after this character
720            let char_end = byte_pos + ch.len_utf8();
721            let mut ended = false;
722            for span in &self.spans {
723                if span.end == char_end {
724                    out.push_str("\x1b[0m");
725                    ended = true;
726                }
727            }
728            // If we ended spans but the default style needs re-applying
729            if ended && !default_ansi.is_empty() {
730                out.push_str(&default_ansi);
731            }
732        }
733
734        if !reset.is_empty() {
735            out.push_str(reset);
736        }
737
738        out
739    }
740
741    /// Serialize the text to a BBCode-like markup string.
742    /// Each span is wrapped in `[style]...[/]` tags.
743    pub fn markup(&self) -> String {
744        if self.spans.is_empty() {
745            return crate::markup::escape(&self.plain);
746        }
747
748        let mut sorted: Vec<&Span> = self.spans.iter().collect();
749        sorted.sort_by_key(|s| (s.start, s.end));
750
751        let mut result = String::new();
752        let mut pos = 0usize;
753
754        for span in &sorted {
755            if span.start > pos {
756                result.push_str(&crate::markup::escape(&self.plain[pos..span.start]));
757            }
758            if span.start < span.end {
759                let style_str = span.style.to_string();
760                if style_str != "none" && !style_str.is_empty() {
761                    result.push_str(&format!("[{}]", style_str));
762                    result.push_str(&crate::markup::escape(&self.plain[span.start..span.end]));
763                    result.push_str("[/]");
764                } else {
765                    result.push_str(&crate::markup::escape(&self.plain[span.start..span.end]));
766                }
767            }
768            pos = pos.max(span.end);
769        }
770
771        if pos < self.plain.len() {
772            result.push_str(&crate::markup::escape(&self.plain[pos..]));
773        }
774
775        result
776    }
777
778    /// Pad the text on both sides with `count` copies of `character`.
779    pub fn pad(&mut self, count: usize, character: char) {
780        self.plain = format!(
781            "{}{}{}",
782            character.to_string().repeat(count),
783            self.plain,
784            character.to_string().repeat(count)
785        );
786        // Shift spans by `count`
787        for span in &mut self.spans {
788            span.start += count;
789            span.end += count;
790        }
791    }
792
793    /// Pad the left side only.
794    pub fn pad_left(&mut self, count: usize, character: char) {
795        self.plain = format!("{}{}", character.to_string().repeat(count), self.plain);
796        for span in &mut self.spans {
797            span.start += count;
798            span.end += count;
799        }
800    }
801
802    /// Pad the right side only.
803    pub fn pad_right(&mut self, count: usize, character: char) {
804        self.plain = format!("{}{}", self.plain, character.to_string().repeat(count));
805    }
806
807    /// Align the text within a given width using the specified method.
808    pub fn align(&mut self, method: AlignMethod, width: usize) {
809        let current = self.cell_len();
810        if current >= width {
811            return;
812        }
813        let padding = width - current;
814        match method {
815            AlignMethod::Left => self.pad_right(padding, ' '),
816            AlignMethod::Right => self.pad_left(padding, ' '),
817            AlignMethod::Center => {
818                let left = padding / 2;
819                self.pad_left(left, ' ');
820                self.pad_right(padding - left, ' ');
821            }
822            AlignMethod::Full => {} // not applicable
823        }
824    }
825}
826
827impl Default for Text {
828    fn default() -> Self {
829        Self::new("")
830    }
831}
832
833impl fmt::Display for Text {
834    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
835        write!(f, "{}", self.render())
836    }
837}
838
839impl From<&str> for Text {
840    fn from(s: &str) -> Self {
841        Self::new(s)
842    }
843}
844
845impl From<String> for Text {
846    fn from(s: String) -> Self {
847        Self::new(s)
848    }
849}
850
851// ---------------------------------------------------------------------------
852// TextType — either a &str or a Text
853// ---------------------------------------------------------------------------
854
855/// Represents something that can be used as text: a plain `&str` or a `Text`.
856#[derive(Debug, Clone)]
857pub enum TextType {
858    Plain(String),
859    Rich(Text),
860}
861
862impl TextType {
863    pub fn render(&self) -> String {
864        match self {
865            Self::Plain(s) => s.clone(),
866            Self::Rich(t) => t.render(),
867        }
868    }
869}
870
871impl From<&str> for TextType {
872    fn from(s: &str) -> Self {
873        Self::Plain(s.to_string())
874    }
875}
876
877impl From<String> for TextType {
878    fn from(s: String) -> Self {
879        Self::Plain(s)
880    }
881}
882
883impl From<Text> for TextType {
884    fn from(t: Text) -> Self {
885        Self::Rich(t)
886    }
887}
888
889impl fmt::Display for TextType {
890    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
891        match self {
892            Self::Plain(s) => write!(f, "{s}"),
893            Self::Rich(t) => write!(f, "{t}"),
894        }
895    }
896}
897
898// ---------------------------------------------------------------------------
899// Internal: parse an SGR parameter string and apply to a Style
900// ---------------------------------------------------------------------------
901
902/// Apply ANSI SGR parameters to a style in-place.
903/// `params` is the semicolon-separated string after `\x1b[` and before `m`.
904fn apply_sgr(style: &mut Style, params: &str) {
905    if params.is_empty() || params == "0" {
906        // Full reset
907        *style = Style::new();
908        return;
909    }
910
911    let parts: Vec<&str> = params.split(';').collect();
912    let mut i = 0usize;
913    while i < parts.len() {
914        match parts[i] {
915            "1" => { *style = style.clone().bold(true); }
916            "2" => { *style = style.clone().dim(true); }
917            "3" => { *style = style.clone().italic(true); }
918            "4" => { *style = style.clone().underline(true); }
919            "5" => { *style = style.clone().blink(true); }
920            "6" => { *style = style.clone().blink2(true); }
921            "7" => { *style = style.clone().reverse(true); }
922            "8" => { *style = style.clone().conceal(true); }
923            "9" => { *style = style.clone().strike(true); }
924            "21" => { *style = style.clone().underline2(true); }
925            "22" => { *style = style.clone().bold(false).dim(false); }
926            "23" => { *style = style.clone().italic(false); }
927            "24" => { *style = style.clone().underline(false); }
928            "25" => { *style = style.clone().blink(false).blink2(false); }
929            "27" => { *style = style.clone().reverse(false); }
930            "28" => { *style = style.clone().conceal(false); }
931            "29" => { *style = style.clone().strike(false); }
932            "51" => { *style = style.clone().frame(true); }
933            "52" => { *style = style.clone().encircle(true); }
934            "53" => { *style = style.clone().overline(true); }
935            "54" => { *style = style.clone().frame(false).encircle(false); }
936            "55" => { *style = style.clone().overline(false); }
937            "38" => {
938                // Extended foreground color
939                if i + 1 < parts.len() {
940                    match parts[i + 1] {
941                        "5" => {
942                            if i + 2 < parts.len() {
943                                if let Ok(n) = parts[i + 2].parse::<u8>() {
944                                    let c = crate::color::Color::from_8bit(n);
945                                    *style = style.clone().color(c);
946                                }
947                                i += 2;
948                            }
949                        }
950                        "2" => {
951                            if i + 4 < parts.len() {
952                                let r = parts[i + 2].parse::<u8>().unwrap_or(0);
953                                let g = parts[i + 3].parse::<u8>().unwrap_or(0);
954                                let b = parts[i + 4].parse::<u8>().unwrap_or(0);
955                                *style = style.clone().color(crate::color::Color::from_rgb(r, g, b));
956                                i += 4;
957                            }
958                        }
959                        _ => {}
960                    }
961                }
962            }
963            "48" => {
964                // Extended background color
965                if i + 1 < parts.len() {
966                    match parts[i + 1] {
967                        "5" => {
968                            if i + 2 < parts.len() {
969                                if let Ok(n) = parts[i + 2].parse::<u8>() {
970                                    let c = crate::color::Color::from_8bit(n);
971                                    *style = style.clone().bgcolor(c);
972                                }
973                                i += 2;
974                            }
975                        }
976                        "2" => {
977                            if i + 4 < parts.len() {
978                                let r = parts[i + 2].parse::<u8>().unwrap_or(0);
979                                let g = parts[i + 3].parse::<u8>().unwrap_or(0);
980                                let b = parts[i + 4].parse::<u8>().unwrap_or(0);
981                                *style = style.clone().bgcolor(crate::color::Color::from_rgb(r, g, b));
982                                i += 4;
983                            }
984                        }
985                        _ => {}
986                    }
987                }
988            }
989            "39" => { style.color = None; }
990            "49" => { style.bgcolor = None; }
991            n => {
992                // Standard colors (30-37, 40-47, 90-97, 100-107)
993                if let Ok(num) = n.parse::<u8>() {
994                    match num {
995                        30..=37 => {
996                            let c = crate::color::Color::from_8bit(num - 30);
997                            *style = style.clone().color(c);
998                        }
999                        40..=47 => {
1000                            let c = crate::color::Color::from_8bit(num - 40);
1001                            *style = style.clone().bgcolor(c);
1002                        }
1003                        90..=97 => {
1004                            let c = crate::color::Color::from_8bit(num - 82);
1005                            *style = style.clone().color(c);
1006                        }
1007                        100..=107 => {
1008                            let c = crate::color::Color::from_8bit(num - 92);
1009                            *style = style.clone().bgcolor(c);
1010                        }
1011                        _ => {}
1012                    }
1013                }
1014            }
1015        }
1016        i += 1;
1017    }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022    use super::*;
1023    use crate::color::Color;
1024
1025    #[test]
1026    fn test_text_append() {
1027        let mut t = Text::new("Hello");
1028        t.append_styled(" World", Style::new().bold(true));
1029        assert_eq!(t.plain, "Hello World");
1030        assert_eq!(t.spans.len(), 1);
1031        assert_eq!(t.spans[0].start, 5);
1032        assert_eq!(t.spans[0].end, 11);
1033    }
1034
1035    #[test]
1036    fn test_text_truncate() {
1037        let mut t = Text::new("Hello World");
1038        t.truncate(5, OverflowMethod::Ellipsis);
1039        assert!(t.plain.contains('\u{2026}'));
1040    }
1041
1042    #[test]
1043    fn test_styled_constructor() {
1044        let style = Style::new().bold(true).color(Color::parse("red").unwrap());
1045        let t = Text::styled(style.clone());
1046        assert_eq!(t.plain, "");
1047        assert_eq!(t.style, style);
1048        assert!(t.spans.is_empty());
1049    }
1050
1051    #[test]
1052    fn test_from_ansi_empty() {
1053        let t = Text::from_ansi("");
1054        assert_eq!(t.plain, "");
1055        assert!(t.spans.is_empty());
1056    }
1057
1058    #[test]
1059    fn test_from_ansi_no_escapes() {
1060        let t = Text::from_ansi("hello world");
1061        assert_eq!(t.plain, "hello world");
1062        assert!(t.spans.is_empty());
1063    }
1064
1065    #[test]
1066    fn test_from_ansi_bold() {
1067        let t = Text::from_ansi("\x1b[1mbold\x1b[0m");
1068        assert_eq!(t.plain, "bold");
1069        assert!(!t.spans.is_empty());
1070    }
1071
1072    #[test]
1073    fn test_style_getter() {
1074        let style = Style::new().bold(true);
1075        let t = Text::styled(style.clone());
1076        assert_eq!(t.get_style(), &style);
1077    }
1078
1079    #[test]
1080    fn test_style_mut_getter() {
1081        let mut t = Text::new("hello");
1082        t.style = Style::new().bold(true);
1083        assert_eq!(t.get_style().get_bold(), Some(true));
1084    }
1085
1086    #[test]
1087    fn test_spans_getter() {
1088        let mut t = Text::new("hello");
1089        t.stylize(Style::new().bold(true), 0, Some(3));
1090        assert_eq!(t.spans().len(), 1);
1091    }
1092
1093    #[test]
1094    fn test_from_markup() {
1095        let t = Text::from_markup("[bold]hello[/bold]");
1096        assert_eq!(t.plain, "hello");
1097        assert!(!t.spans.is_empty());
1098    }
1099
1100    #[test]
1101    fn test_append_tokens() {
1102        let mut t = Text::new("");
1103        let tokens = vec![
1104            ("Hello ".to_string(), Style::new().bold(true)),
1105            ("World".to_string(), Style::new().italic(true)),
1106        ];
1107        t.append_tokens(tokens);
1108        assert_eq!(t.plain, "Hello World");
1109        assert_eq!(t.spans.len(), 2);
1110    }
1111
1112    #[test]
1113    fn test_stylize_before() {
1114        let mut t = Text::new("hello");
1115        t.stylize(Style::new().bold(true), 0, Some(5));
1116        t.stylize_before(Style::new().italic(true), 0, Some(5));
1117        // stylize_before inserts at the beginning (index 0)
1118        assert_eq!(t.spans.len(), 2);
1119        assert_eq!(t.spans[0].style.get_italic(), Some(true));
1120    }
1121
1122    #[test]
1123    fn test_blank_copy() {
1124        let mut t = Text::new("hello");
1125        t.stylize(Style::new().bold(true), 0, Some(3));
1126        t.justify = JustifyMethod::Center;
1127        let blank = t.blank_copy();
1128        assert_eq!(blank.plain, "");
1129        assert!(blank.spans.is_empty());
1130        assert_eq!(blank.justify, JustifyMethod::Center);
1131    }
1132
1133    #[test]
1134    fn test_copy_styles() {
1135        let mut t = Text::new("hello");
1136        t.stylize(Style::new().bold(true), 0, Some(3));
1137        let copy = t.copy_styles();
1138        assert_eq!(copy.plain, "hello");
1139        assert!(copy.spans.is_empty());
1140    }
1141
1142    #[test]
1143    fn test_detect_indentation() {
1144        let t = Text::new("  hello");
1145        let (indent, count) = t.detect_indentation();
1146        assert_eq!(indent, "  ");
1147        assert_eq!(count, 2);
1148    }
1149
1150    #[test]
1151    fn test_detect_indentation_none() {
1152        let t = Text::new("hello");
1153        let (indent, count) = t.detect_indentation();
1154        assert_eq!(indent, "");
1155        assert_eq!(count, 0);
1156    }
1157
1158    #[test]
1159    fn test_get_style_at_offset() {
1160        let mut t = Text::new("hello world");
1161        t.stylize(Style::new().bold(true), 0, Some(5));
1162        let s0 = t.get_style_at_offset(0);
1163        assert_eq!(s0.get_bold(), Some(true));
1164        let s6 = t.get_style_at_offset(6);
1165        assert_eq!(s6.get_bold(), None); // not in span, default style has no bold
1166    }
1167
1168    #[test]
1169    fn test_divide() {
1170        let mut t = Text::new("abcdef");
1171        t.stylize(Style::new().bold(true), 0, Some(3));
1172        let parts = t.divide(&[2, 4]);
1173        assert_eq!(parts.len(), 3);
1174        assert_eq!(parts[0].plain, "ab");
1175        assert_eq!(parts[1].plain, "cd");
1176        assert_eq!(parts[2].plain, "ef");
1177    }
1178
1179    #[test]
1180    fn test_extend_style() {
1181        let mut t = Text::new("hello");
1182        t.extend_style(Style::new().bold(true));
1183        assert_eq!(t.spans.len(), 1);
1184        assert_eq!(t.spans[0].start, 0);
1185        assert_eq!(t.spans[0].end, 5);
1186    }
1187
1188    #[test]
1189    fn test_fit_truncate() {
1190        let t = Text::new("hello world");
1191        let fitted = t.fit(5);
1192        assert_eq!(fitted.cell_len(), 5);
1193    }
1194
1195    #[test]
1196    fn test_fit_pad() {
1197        let t = Text::new("hi");
1198        let fitted = t.fit(10);
1199        assert_eq!(fitted.cell_len(), 10);
1200    }
1201
1202    #[test]
1203    fn test_set_length_truncate() {
1204        let mut t = Text::new("hello world");
1205        t.set_length(5);
1206        assert_eq!(t.cell_len(), 5);
1207    }
1208
1209    #[test]
1210    fn test_set_length_pad() {
1211        let mut t = Text::new("hi");
1212        t.set_length(10);
1213        assert_eq!(t.cell_len(), 10);
1214    }
1215
1216    #[test]
1217    fn test_remove_suffix() {
1218        let mut t = Text::new("hello.txt");
1219        assert!(t.remove_suffix(".txt"));
1220        assert_eq!(t.plain, "hello");
1221    }
1222
1223    #[test]
1224    fn test_remove_suffix_not_found() {
1225        let mut t = Text::new("hello");
1226        assert!(!t.remove_suffix(".txt"));
1227        assert_eq!(t.plain, "hello");
1228    }
1229
1230    #[test]
1231    fn test_right_crop() {
1232        let mut t = Text::new("hello world");
1233        let cropped = t.right_crop(5);
1234        assert_eq!(t.plain, "hello");
1235        assert_eq!(cropped.plain, " world");
1236    }
1237
1238    #[test]
1239    fn test_rstrip() {
1240        let mut t = Text::new("hello   ");
1241        t.rstrip();
1242        assert_eq!(t.plain, "hello");
1243    }
1244
1245    #[test]
1246    fn test_rstrip_end() {
1247        let mut t = Text::new("hello\n");
1248        assert!(t.rstrip_end("\n"));
1249        assert_eq!(t.plain, "hello");
1250    }
1251
1252    #[test]
1253    fn test_split_chars() {
1254        let t = Text::new("abc");
1255        let chars = t.split();
1256        assert_eq!(chars.len(), 3);
1257        assert_eq!(chars[0].plain, "a");
1258        assert_eq!(chars[1].plain, "b");
1259        assert_eq!(chars[2].plain, "c");
1260    }
1261
1262    #[test]
1263    fn test_markup() {
1264        let mut t = Text::new("hello world");
1265        t.stylize(Style::new().bold(true), 0, Some(5));
1266        let markup = t.markup();
1267        assert!(markup.contains("[bold]"));
1268        assert!(markup.contains("[/]"));
1269        assert!(markup.contains("hello"));
1270        assert!(markup.contains(" world"));
1271    }
1272
1273    #[test]
1274    fn test_markup_no_spans() {
1275        let t = Text::new("hello");
1276        assert_eq!(t.markup(), "hello");
1277    }
1278
1279    #[test]
1280    fn test_no_wrap_builder() {
1281        let t = Text::new("hello").no_wrap(true);
1282        assert!(t.no_wrap);
1283    }
1284
1285    #[test]
1286    fn test_tab_size_builder() {
1287        let t = Text::new("hello").tab_size(4);
1288        assert_eq!(t.tab_size, 4);
1289    }
1290
1291    #[test]
1292    fn test_with_indent_guides_builder() {
1293        let t = Text::new("hello").with_indent_guides(true);
1294        assert!(t.indent_guides);
1295    }
1296
1297    #[test]
1298    fn test_expand_tabs_with_custom_size() {
1299        let mut t = Text::new("\thello").tab_size(4);
1300        t.expand_tabs();
1301        assert_eq!(t.plain, "    hello");
1302    }
1303
1304    #[test]
1305    fn test_apply_meta() {
1306        let mut t = Text::new("hello world");
1307        let meta = vec![1u8, 2u8, 3u8];
1308        let spans = vec![Span::new(0, 5, Style::new())];
1309        t.apply_meta(meta, &spans);
1310        // Should have added a span with meta
1311        assert!(t.spans.iter().any(|s| s.style.meta.is_some()));
1312    }
1313
1314    #[test]
1315    fn test_default_impl() {
1316        let t: Text = Default::default();
1317        assert_eq!(t.plain, "");
1318        assert_eq!(t.tab_size, 8);
1319    }
1320}