Skip to main content

sivtr_core/buffer/
line.rs

1/// An ANSI color value.
2#[derive(Debug, Clone, PartialEq)]
3pub enum AnsiColor {
4    /// Standard/bright color (0-15).
5    Indexed(u8),
6    /// RGB true color.
7    Rgb(u8, u8, u8),
8}
9
10/// Style information for a span of text within a line.
11#[derive(Debug, Clone, PartialEq)]
12pub struct StyledSpan {
13    /// Start byte offset in the cleaned content.
14    pub start: usize,
15    /// End byte offset (exclusive) in the cleaned content.
16    pub end: usize,
17    /// Foreground color.
18    pub fg: Option<AnsiColor>,
19    /// Background color.
20    pub bg: Option<AnsiColor>,
21    pub bold: bool,
22    pub italic: bool,
23    pub underline: bool,
24    pub dim: bool,
25}
26
27/// A single line of terminal output.
28#[derive(Debug, Clone)]
29pub struct Line {
30    /// Plain text content (ANSI stripped).
31    pub content: String,
32    /// Display width of each character (0, 1, or 2 for wide chars).
33    pub display_widths: Vec<u8>,
34    /// Style spans for colored rendering.
35    pub styles: Vec<StyledSpan>,
36}
37
38impl Line {
39    /// Total display width of this line.
40    pub fn display_width(&self) -> usize {
41        self.display_widths.iter().map(|&w| w as usize).sum()
42    }
43
44    /// Number of characters in this line.
45    pub fn char_count(&self) -> usize {
46        self.content.chars().count()
47    }
48
49    /// Convert a display column to the corresponding character index.
50    pub fn char_index_for_display_col(&self, target_col: usize) -> usize {
51        let mut display_col = 0usize;
52        for (idx, width) in self.display_widths.iter().enumerate() {
53            let width = *width as usize;
54            if display_col + width > target_col {
55                return idx;
56            }
57            display_col += width;
58        }
59        self.display_widths.len()
60    }
61
62    /// Convert a character index to its starting display column.
63    pub fn display_col_for_char_index(&self, char_idx: usize) -> usize {
64        self.display_widths
65            .iter()
66            .take(char_idx.min(self.display_widths.len()))
67            .map(|&w| w as usize)
68            .sum()
69    }
70
71    /// Extract a substring by display column range [col_start, col_end).
72    /// Returns the extracted string. Short lines return what's available.
73    pub fn extract_by_display_cols(&self, col_start: usize, col_end: usize) -> String {
74        if col_start >= col_end {
75            return String::new();
76        }
77        let (char_start, char_end) =
78            crate::parse::unicode::display_col_to_char_range(&self.content, col_start, col_end);
79        self.content
80            .chars()
81            .skip(char_start)
82            .take(char_end - char_start)
83            .collect()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    fn make_line(s: &str) -> Line {
92        let content = s.to_string();
93        let display_widths = crate::parse::unicode::compute_display_widths(&content);
94        Line {
95            content,
96            display_widths,
97            styles: Vec::new(),
98        }
99    }
100
101    #[test]
102    fn test_display_width() {
103        let line = make_line("hello");
104        assert_eq!(line.display_width(), 5);
105    }
106
107    #[test]
108    fn test_display_width_cjk() {
109        let line = make_line("你好");
110        assert_eq!(line.display_width(), 4);
111    }
112
113    #[test]
114    fn test_extract_ascii() {
115        let line = make_line("hello world");
116        assert_eq!(line.extract_by_display_cols(0, 5), "hello");
117    }
118
119    #[test]
120    fn test_extract_cjk() {
121        let line = make_line("你好世界");
122        assert_eq!(line.extract_by_display_cols(0, 4), "你好");
123    }
124
125    #[test]
126    fn test_extract_beyond_line() {
127        let line = make_line("hi");
128        assert_eq!(line.extract_by_display_cols(0, 10), "hi");
129    }
130
131    #[test]
132    fn test_extract_empty_range() {
133        let line = make_line("hello");
134        assert_eq!(line.extract_by_display_cols(0, 0), "");
135        assert_eq!(line.extract_by_display_cols(2, 2), "");
136    }
137
138    #[test]
139    fn test_char_index_for_display_col() {
140        let line = make_line("a你好");
141        assert_eq!(line.char_index_for_display_col(0), 0);
142        assert_eq!(line.char_index_for_display_col(1), 1);
143        assert_eq!(line.char_index_for_display_col(2), 1);
144        assert_eq!(line.char_index_for_display_col(3), 2);
145        assert_eq!(line.char_index_for_display_col(4), 2);
146    }
147
148    #[test]
149    fn test_display_col_for_char_index() {
150        let line = make_line("a你好");
151        assert_eq!(line.display_col_for_char_index(0), 0);
152        assert_eq!(line.display_col_for_char_index(1), 1);
153        assert_eq!(line.display_col_for_char_index(2), 3);
154        assert_eq!(line.display_col_for_char_index(3), 5);
155    }
156}