Skip to main content

kronos_tide/
terminal.rs

1use crate::unicode::char_width;
2
3pub struct TerminalBuffer {
4    cells: Vec<Vec<Cell>>,
5    cursor_row: usize,
6    cursor_col: usize,
7    rows: usize,
8    cols: usize,
9    scrollback: Vec<Vec<Cell>>,
10    max_scrollback: usize,
11    /// DECSTBM scroll region: (top_row, bottom_row) inclusive, 0-indexed.
12    scroll_top: usize,
13    scroll_bottom: usize,
14    /// Window title set by OSC 0/1/2.
15    pub window_title: String,
16}
17
18#[derive(Clone, Debug)]
19pub struct Cell {
20    pub ch: char,
21    pub fg: Color,
22    pub bg: Color,
23    pub bold: bool,
24    pub italic: bool,
25    pub underline: bool,
26    /// True when this cell is the right half of a wide (width-2) character.
27    /// The renderer should skip printing and the cell ch is set to ' '.
28    pub wide_continuation: bool,
29    /// Combining characters layered on top of this cell (e.g. accents).
30    pub combining: Vec<char>,
31}
32
33#[derive(Clone, Debug, Copy)]
34pub struct Color {
35    pub r: u8,
36    pub g: u8,
37    pub b: u8,
38}
39
40impl Color {
41    pub const WHITE: Self = Self {
42        r: 255,
43        g: 255,
44        b: 255,
45    };
46    pub const BLACK: Self = Self { r: 0, g: 0, b: 0 };
47    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
48        Self { r, g, b }
49    }
50
51    pub fn from_ansi(code: u8) -> Self {
52        match code {
53            0 => Self::rgb(0, 0, 0),
54            1 => Self::rgb(205, 49, 49),
55            2 => Self::rgb(13, 188, 121),
56            3 => Self::rgb(229, 229, 16),
57            4 => Self::rgb(36, 114, 200),
58            5 => Self::rgb(188, 63, 188),
59            6 => Self::rgb(17, 168, 205),
60            7 => Self::rgb(229, 229, 229),
61            8 => Self::rgb(102, 102, 102),
62            9 => Self::rgb(241, 76, 76),
63            10 => Self::rgb(35, 209, 139),
64            11 => Self::rgb(245, 245, 67),
65            12 => Self::rgb(59, 142, 234),
66            13 => Self::rgb(214, 112, 214),
67            14 => Self::rgb(41, 184, 219),
68            15 => Self::rgb(255, 255, 255),
69            16..=231 => {
70                let idx = code - 16;
71                let r = (idx / 36) * 51;
72                let g = ((idx % 36) / 6) * 51;
73                let b = (idx % 6) * 51;
74                Self::rgb(r, g, b)
75            }
76            232..=255 => {
77                let gray = 8 + (code - 232) * 10;
78                Self::rgb(gray, gray, gray)
79            }
80        }
81    }
82}
83
84impl Default for Cell {
85    fn default() -> Self {
86        Self {
87            ch: ' ',
88            fg: Color::WHITE,
89            bg: Color::BLACK,
90            bold: false,
91            italic: false,
92            underline: false,
93            wide_continuation: false,
94            combining: Vec::new(),
95        }
96    }
97}
98
99impl TerminalBuffer {
100    pub fn new(rows: usize, cols: usize, max_scrollback: usize) -> Self {
101        let scroll_bottom = rows.saturating_sub(1);
102        Self {
103            cells: vec![vec![Cell::default(); cols]; rows],
104            cursor_row: 0,
105            cursor_col: 0,
106            rows,
107            cols,
108            scrollback: Vec::new(),
109            max_scrollback,
110            scroll_top: 0,
111            scroll_bottom,
112            window_title: String::new(),
113        }
114    }
115
116    /// Write a single character, applying Unicode width rules.
117    ///
118    /// - Width-0 (combining): appended to the previous cell's combining stack.
119    /// - Width-1: normal single-column write.
120    /// - Width-2 (wide): occupies current column (left half) and marks column+1
121    ///   as a wide_continuation cell.  If only one column remains on the line the
122    ///   wide character is rendered in the last column and no continuation is written
123    ///   (truncation — simplest safe choice; the alternative is wrapping half-chars).
124    pub fn write_char(&mut self, ch: char) {
125        match ch {
126            '\n' => {
127                self.cursor_col = 0;
128                self.advance_row();
129            }
130            '\r' => {
131                self.cursor_col = 0;
132            }
133            '\x08' => {
134                if self.cursor_col > 0 {
135                    self.cursor_col -= 1;
136                }
137            }
138            '\t' => {
139                let next_tab = (self.cursor_col / 8 + 1) * 8;
140                self.cursor_col = next_tab.min(self.cols - 1);
141            }
142            _ => {
143                let w = char_width(ch);
144                match w {
145                    0 => self.write_combining(ch),
146                    1 => self.write_normal(ch),
147                    2 => self.write_wide(ch),
148                    _ => self.write_normal(ch), // unknown width → treat as 1
149                }
150            }
151        }
152    }
153
154    fn write_combining(&mut self, ch: char) {
155        // Append combining char to the cell immediately before the cursor.
156        // If cursor is at column 0 with no previous cell, silently drop it.
157        if self.cursor_col > 0 {
158            let col = self.cursor_col - 1;
159            let row = self.cursor_row;
160            if row < self.rows && col < self.cols {
161                self.cells[row][col].combining.push(ch);
162            }
163        }
164        // cursor does not advance for combining marks
165    }
166
167    fn write_normal(&mut self, ch: char) {
168        if self.cursor_col >= self.cols {
169            self.cursor_col = 0;
170            self.advance_row();
171        }
172        let (row, col) = (self.cursor_row, self.cursor_col);
173        if row < self.rows && col < self.cols {
174            self.cells[row][col].ch = ch;
175            self.cells[row][col].wide_continuation = false;
176            self.cells[row][col].combining.clear();
177        }
178        self.cursor_col += 1;
179    }
180
181    fn write_wide(&mut self, ch: char) {
182        // Need two columns.  If only one remains, truncate to last column.
183        if self.cursor_col >= self.cols {
184            self.cursor_col = 0;
185            self.advance_row();
186        }
187        let (row, col) = (self.cursor_row, self.cursor_col);
188        if row < self.rows && col < self.cols {
189            self.cells[row][col].ch = ch;
190            self.cells[row][col].wide_continuation = false;
191            self.cells[row][col].combining.clear();
192        }
193        if col + 1 < self.cols {
194            // Mark the right half as continuation so the renderer skips it.
195            self.cells[row][col + 1] = Cell {
196                ch: ' ',
197                wide_continuation: true,
198                ..Cell::default()
199            };
200            self.cursor_col += 2;
201        } else {
202            // Truncation: wide char placed in last column, no continuation cell.
203            self.cursor_col += 1;
204        }
205    }
206
207    /// Advance one row within the current scroll region, scrolling if needed.
208    fn advance_row(&mut self) {
209        if self.cursor_row >= self.scroll_bottom {
210            self.scroll_region_up(1);
211            // cursor stays at scroll_bottom after scroll
212            self.cursor_row = self.scroll_bottom;
213        } else {
214            self.cursor_row += 1;
215        }
216    }
217
218    /// Scroll the active scroll region up by `n` lines.
219    /// Displaced top lines move to the scrollback buffer.
220    pub fn scroll_up(&mut self) {
221        self.scroll_region_up(1);
222    }
223
224    /// Scroll the scroll region up by `n` lines (CSI Ps S).
225    pub fn scroll_region_up(&mut self, n: usize) {
226        let top = self.scroll_top;
227        let bot = self.scroll_bottom.min(self.rows - 1);
228        for _ in 0..n {
229            let removed = self.cells[top].clone();
230            self.cells.remove(top);
231            self.scrollback.push(removed);
232            if self.scrollback.len() > self.max_scrollback {
233                self.scrollback.remove(0);
234            }
235            // Insert blank row at bottom of region.
236            self.cells.insert(bot, vec![Cell::default(); self.cols]);
237        }
238    }
239
240    /// Scroll the scroll region down by `n` lines (CSI Ps T).
241    /// Top lines are blank-filled; bottom lines are discarded.
242    pub fn scroll_region_down(&mut self, n: usize) {
243        let top = self.scroll_top;
244        let bot = self.scroll_bottom.min(self.rows - 1);
245        for _ in 0..n {
246            if bot < self.rows {
247                self.cells.remove(bot);
248            }
249            self.cells.insert(top, vec![Cell::default(); self.cols]);
250        }
251    }
252
253    pub fn cursor(&self) -> (usize, usize) {
254        (self.cursor_row, self.cursor_col)
255    }
256
257    pub fn cell(&self, row: usize, col: usize) -> &Cell {
258        &self.cells[row][col]
259    }
260
261    pub fn resize(&mut self, rows: usize, cols: usize) {
262        self.rows = rows;
263        self.cols = cols;
264        self.cells.resize(rows, vec![Cell::default(); cols]);
265        for row in &mut self.cells {
266            row.resize(cols, Cell::default());
267        }
268        if self.cursor_row >= rows {
269            self.cursor_row = rows - 1;
270        }
271        if self.cursor_col >= cols {
272            self.cursor_col = cols - 1;
273        }
274        // Re-clamp scroll region to new size.
275        if self.scroll_top >= rows {
276            self.scroll_top = 0;
277        }
278        if self.scroll_bottom >= rows {
279            self.scroll_bottom = rows - 1;
280        }
281    }
282
283    pub fn clear(&mut self) {
284        for row in &mut self.cells {
285            for cell in row.iter_mut() {
286                *cell = Cell::default();
287            }
288        }
289        self.cursor_row = 0;
290        self.cursor_col = 0;
291    }
292
293    pub fn dimensions(&self) -> (usize, usize) {
294        (self.rows, self.cols)
295    }
296
297    pub fn scrollback_len(&self) -> usize {
298        self.scrollback.len()
299    }
300
301    pub fn set_cursor(&mut self, row: usize, col: usize) {
302        self.cursor_row = row.min(self.rows.saturating_sub(1));
303        self.cursor_col = col.min(self.cols.saturating_sub(1));
304    }
305
306    /// DECSTBM: set scroll region.  Both values are 1-indexed; we store 0-indexed.
307    /// Passes through silently on invalid ranges.
308    pub fn set_scroll_region(&mut self, top: usize, bottom: usize) {
309        if top < bottom && bottom < self.rows {
310            self.scroll_top = top;
311            self.scroll_bottom = bottom;
312        }
313        // Move cursor to home position after DECSTBM (per spec).
314        self.cursor_row = self.scroll_top;
315        self.cursor_col = 0;
316    }
317
318    pub fn scroll_top(&self) -> usize {
319        self.scroll_top
320    }
321    pub fn scroll_bottom(&self) -> usize {
322        self.scroll_bottom
323    }
324
325    pub fn set_cell_styled(
326        &mut self,
327        row: usize,
328        col: usize,
329        ch: char,
330        fg: Color,
331        bg: Color,
332        bold: bool,
333        italic: bool,
334        underline: bool,
335    ) {
336        if row < self.rows && col < self.cols {
337            self.cells[row][col] = Cell {
338                ch,
339                fg,
340                bg,
341                bold,
342                italic,
343                underline,
344                wide_continuation: false,
345                combining: Vec::new(),
346            };
347        }
348    }
349
350    pub fn clear_below(&mut self) {
351        for col in self.cursor_col..self.cols {
352            self.cells[self.cursor_row][col] = Cell::default();
353        }
354        for row in (self.cursor_row + 1)..self.rows {
355            for col in 0..self.cols {
356                self.cells[row][col] = Cell::default();
357            }
358        }
359    }
360
361    pub fn clear_above(&mut self) {
362        for col in 0..=self.cursor_col.min(self.cols - 1) {
363            self.cells[self.cursor_row][col] = Cell::default();
364        }
365        for row in 0..self.cursor_row {
366            for col in 0..self.cols {
367                self.cells[row][col] = Cell::default();
368            }
369        }
370    }
371
372    pub fn clear_line(&mut self) {
373        for col in 0..self.cols {
374            self.cells[self.cursor_row][col] = Cell::default();
375        }
376    }
377
378    pub fn clear_line_right(&mut self) {
379        for col in self.cursor_col..self.cols {
380            self.cells[self.cursor_row][col] = Cell::default();
381        }
382    }
383
384    pub fn clear_line_left(&mut self) {
385        for col in 0..=self.cursor_col.min(self.cols - 1) {
386            self.cells[self.cursor_row][col] = Cell::default();
387        }
388    }
389}