fltk_term/
cells.rs

1use crate::styles::*;
2use fltk::enums::Color;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub struct Style {
6    pub fg: Color,
7    pub bg: Color,
8    pub bold: bool,
9    pub faint: bool,
10    pub italic: bool,
11    pub underline: bool,
12    pub strikethrough: bool,
13    pub overline: bool,
14    pub inverse: bool,
15}
16
17impl Default for Style {
18    fn default() -> Self {
19        Self {
20            fg: WHITE,
21            bg: BLACK,
22            bold: false,
23            faint: false,
24            italic: false,
25            underline: false,
26            strikethrough: false,
27            overline: false,
28            inverse: false,
29        }
30    }
31}
32
33impl Style {
34    pub fn new(bg: Color, fg: Color) -> Self {
35        Self {
36            fg,
37            bg,
38            bold: false,
39            faint: false,
40            italic: false,
41            underline: false,
42            strikethrough: false,
43            overline: false,
44            inverse: false,
45        }
46    }
47}
48
49#[derive(Clone, Copy, Debug)]
50pub struct Cell {
51    pub ch: char,
52    pub style: Style,
53}
54
55impl Cell {
56    pub fn new(ch: char, style: Style) -> Self {
57        Self { ch, style }
58    }
59}
60
61pub struct CellBuffer {
62    lines: Vec<Vec<Cell>>, // simple grow-only buffer for now
63    pub max_lines: usize,
64    pub cur_style: Style,
65    pub default_bg: Color,
66    pub default_fg: Color,
67    dirty_start: Option<usize>,
68    dirty_end: Option<usize>,
69    dirty_cols: Vec<Option<(usize, usize)>>,
70    cursor_row: usize,
71    cursor_col: usize,
72    cols: usize,
73    rows: usize,
74}
75
76impl CellBuffer {
77    pub fn new(max_lines: usize, default_bg: Color, default_fg: Color) -> Self {
78        Self {
79            lines: vec![Vec::new()],
80            max_lines,
81            cur_style: Style {
82                fg: default_fg,
83                bg: default_bg,
84                ..Default::default()
85            },
86            default_bg,
87            default_fg,
88            dirty_start: None,
89            dirty_end: None,
90            dirty_cols: vec![None],
91            cursor_row: 0,
92            cursor_col: 0,
93            cols: 80,
94            rows: 24,
95        }
96    }
97
98    pub fn set_style(&mut self, style: Style) {
99        self.cur_style = style;
100    }
101
102    pub fn push_char(&mut self, ch: char) {
103        if let Some(line) = self.lines.last_mut() {
104            let col = line.len();
105            line.push(Cell::new(ch, self.cur_style));
106            let idx = self.lines.len().saturating_sub(1);
107            self.mark_dirty_line(idx);
108            self.mark_dirty_cols(idx, col, col);
109            self.cursor_row = idx;
110            self.cursor_col = col + 1;
111        }
112    }
113
114    pub fn newline(&mut self) {
115        self.lines.push(Vec::new());
116        self.dirty_cols.push(None);
117        if self.lines.len() > self.max_lines {
118            let overflow = self.lines.len() - self.max_lines;
119            self.lines.drain(0..overflow);
120            if overflow > 0 {
121                if self.dirty_cols.len() >= overflow {
122                    self.dirty_cols.drain(0..overflow);
123                }
124                self.cursor_row = self.cursor_row.saturating_sub(overflow);
125            }
126        }
127        let idx = self.lines.len().saturating_sub(1);
128        self.mark_dirty_line(idx);
129        self.mark_dirty_cols(idx, 0, 0);
130        self.cursor_row = idx;
131        self.cursor_col = 0;
132    }
133
134    pub fn clear_line_right(&mut self) {
135        if let Some(line) = self.lines.last_mut() {
136            let before = line.len();
137            line.clear();
138            let idx = self.lines.len().saturating_sub(1);
139            self.mark_dirty_line(idx);
140            if before > 0 {
141                self.mark_dirty_cols(idx, 0, before - 1);
142            }
143            self.cursor_row = idx;
144            self.cursor_col = 0;
145        }
146    }
147
148    pub fn snapshot(&self) -> Vec<Vec<Cell>> {
149        self.lines.clone()
150    }
151
152    pub fn set_dimensions(&mut self, cols: usize, rows: usize) {
153        self.cols = cols.max(1);
154        self.rows = rows.max(1);
155        // Keep cursor anchored to bottom region if it fell outside
156        let st = self.screen_top();
157        let sb = st + self.rows.saturating_sub(1);
158        if self.cursor_row < st {
159            self.cursor_row = st;
160            self.cursor_col = 0;
161        } else if self.cursor_row > sb {
162            self.cursor_row = sb;
163            self.cursor_col = 0;
164        }
165    }
166
167    fn screen_top(&self) -> usize {
168        self.lines.len().saturating_sub(self.rows)
169    }
170
171    fn screen_bottom(&self) -> usize {
172        self.lines.len().saturating_sub(1).max(
173            self.screen_top()
174                .saturating_add(self.rows.saturating_sub(1)),
175        )
176    }
177
178    fn mark_dirty_line(&mut self, idx: usize) {
179        self.dirty_start = Some(self.dirty_start.map(|s| s.min(idx)).unwrap_or(idx));
180        self.dirty_end = Some(self.dirty_end.map(|e| e.max(idx)).unwrap_or(idx));
181    }
182
183    fn mark_dirty_cols(&mut self, idx: usize, start: usize, end: usize) {
184        if idx >= self.dirty_cols.len() {
185            self.dirty_cols.resize(idx + 1, None);
186        }
187        self.dirty_cols[idx] = match self.dirty_cols[idx] {
188            Some((s, e)) => Some((s.min(start), e.max(end))),
189            None => Some((start, end)),
190        };
191    }
192
193    pub fn take_dirty(&mut self) -> Option<(usize, usize)> {
194        match (self.dirty_start.take(), self.dirty_end.take()) {
195            (Some(s), Some(e)) if s <= e => Some((s, e)),
196            _ => None,
197        }
198    }
199
200    pub fn take_dirty_areas(&mut self) -> Vec<(usize, usize, usize)> {
201        let mut areas = Vec::new();
202        for (i, rng) in self.dirty_cols.iter_mut().enumerate() {
203            if let Some((s, e)) = rng.take() {
204                areas.push((i, s, e));
205            }
206        }
207        areas
208    }
209
210    pub fn cursor(&self) -> (usize, usize) {
211        (self.cursor_row, self.cursor_col)
212    }
213    pub fn set_cursor(&mut self, row: usize, col: usize) {
214        self.cursor_row = row;
215        self.cursor_col = col;
216    }
217
218    fn ensure_row(&mut self, row: usize) {
219        while self.lines.len() <= row {
220            self.lines.push(Vec::new());
221            self.dirty_cols.push(None);
222        }
223    }
224
225    fn ensure_col(&mut self, row: usize, col: usize) {
226        self.ensure_row(row);
227        let line = &mut self.lines[row];
228        if line.len() <= col {
229            let pad = col + 1 - line.len();
230            for _ in 0..pad {
231                line.push(Cell::new(' ', self.cur_style));
232            }
233        }
234    }
235
236    pub fn write_char(&mut self, ch: char) {
237        // simple DECAWM-style wrap at cols
238        if self.cols > 0 && self.cursor_col >= self.cols {
239            self.line_feed();
240        }
241        let (row, col) = (self.cursor_row, self.cursor_col);
242        self.ensure_col(row, col);
243        if let Some(cell) = self.lines[row].get_mut(col) {
244            cell.ch = ch;
245            cell.style = self.cur_style;
246        }
247        self.mark_dirty_line(row);
248        self.mark_dirty_cols(row, col, col);
249        self.cursor_col = self.cursor_col.saturating_add(1);
250    }
251
252    pub fn carriage_return(&mut self) {
253        self.cursor_col = 0;
254    }
255
256    pub fn line_feed(&mut self) {
257        self.cursor_row = self.cursor_row.saturating_add(1);
258        self.cursor_col = 0;
259        self.ensure_row(self.cursor_row);
260        self.mark_dirty_line(self.cursor_row);
261    }
262
263    pub fn move_cursor_rel(&mut self, drow: isize, dcol: isize) {
264        let st = self.screen_top();
265        let sb = st + self.rows.saturating_sub(1);
266        let nr = (self.cursor_row as isize + drow).clamp(st as isize, sb as isize) as usize;
267        let mut nc = (self.cursor_col as isize + dcol).max(0) as usize;
268        // clamp col to screen cols if set
269        if self.cols > 0 {
270            nc = nc.min(self.cols.saturating_sub(1));
271        }
272        self.cursor_row = nr;
273        self.cursor_col = nc;
274        self.ensure_row(nr);
275    }
276
277    pub fn move_cursor_abs(&mut self, row1: usize, col1: usize) {
278        // Interpret row/col as screen-relative (0-based)
279        let st = self.screen_top();
280        let row = st + row1.min(self.rows.saturating_sub(1));
281        let col = if self.cols > 0 {
282            col1.min(self.cols.saturating_sub(1))
283        } else {
284            col1
285        };
286        self.cursor_row = row;
287        self.cursor_col = col;
288        self.ensure_row(row);
289    }
290
291    pub fn clear_eol(&mut self) {
292        self.ensure_row(self.cursor_row);
293        let col = self.cursor_col;
294        if let Some(line) = self.lines.get_mut(self.cursor_row) {
295            if col < line.len() {
296                let end = line.len() - 1;
297                line.truncate(col);
298                self.mark_dirty_line(self.cursor_row);
299                self.mark_dirty_cols(self.cursor_row, col, end);
300            }
301        }
302    }
303
304    // EL 0: erase from cursor to end of line (inclusive)
305    pub fn clear_eol_0(&mut self) {
306        self.ensure_row(self.cursor_row);
307        let col = self.cursor_col;
308        if let Some(line) = self.lines.get_mut(self.cursor_row) {
309            if col < line.len() {
310                let end = line.len().saturating_sub(1);
311                for i in col..=end {
312                    if let Some(cell) = line.get_mut(i) {
313                        cell.ch = ' ';
314                        cell.style = self.cur_style;
315                    }
316                }
317                self.mark_dirty_line(self.cursor_row);
318                self.mark_dirty_cols(self.cursor_row, col, end);
319            }
320        }
321    }
322
323    // EL 1: erase from start of line to cursor (inclusive of position before the cursor)
324    pub fn clear_eol_1(&mut self) {
325        let row = self.cursor_row;
326        self.ensure_row(row);
327        let col = self.cursor_col;
328        // Ensure cells exist up to cursor first to avoid borrow conflict
329        self.ensure_col(row, col);
330        if let Some(line) = self.lines.get_mut(row) {
331            if !line.is_empty() {
332                let end = col.min(line.len().saturating_sub(1));
333                for i in 0..=end {
334                    if let Some(cell) = line.get_mut(i) {
335                        cell.ch = ' ';
336                        cell.style = self.cur_style;
337                    }
338                }
339                self.mark_dirty_line(row);
340                self.mark_dirty_cols(row, 0, end);
341            }
342        }
343    }
344
345    // EL 2: erase entire line
346    pub fn clear_eol_2(&mut self) {
347        if let Some(line) = self.lines.get_mut(self.cursor_row) {
348            let len = line.len();
349            if len > 0 {
350                for c in line.iter_mut() {
351                    c.ch = ' ';
352                    c.style = self.cur_style;
353                }
354                self.mark_dirty_line(self.cursor_row);
355                self.mark_dirty_cols(self.cursor_row, 0, len.saturating_sub(1));
356            }
357        }
358    }
359
360    pub fn clear_ed_0(&mut self) {
361        // Clear from cursor to end of screen
362        self.clear_eol_0();
363        let row = self.cursor_row;
364        let sb = self.screen_top() + self.rows.saturating_sub(1);
365        let end_row = sb.min(self.lines.len().saturating_sub(1));
366        if row < end_row {
367            for r in row + 1..=end_row {
368                let len = self.lines[r].len();
369                if len > 0 {
370                    self.lines[r].clear();
371                    self.mark_dirty_line(r);
372                    self.mark_dirty_cols(r, 0, len.saturating_sub(1));
373                }
374            }
375        }
376    }
377
378    pub fn clear_ed_1(&mut self) {
379        // Clear from start of screen to cursor position (inclusive)
380        let row = self.cursor_row;
381        let st = self.screen_top();
382        // clear previous lines within the visible screen entirely
383        for r in st..row {
384            let len = self.lines.get(r).map(|l| l.len()).unwrap_or(0);
385            if len > 0 {
386                if let Some(line) = self.lines.get_mut(r) {
387                    line.clear();
388                }
389                self.mark_dirty_line(r);
390                self.mark_dirty_cols(r, 0, len.saturating_sub(1));
391            }
392        }
393        // clear current line from start to cursor col without shifting remainder
394        self.ensure_row(row);
395        let col = self.cursor_col;
396        // ensure cells exist up to cursor before borrowing line
397        self.ensure_col(row, col);
398        if let Some(line) = self.lines.get_mut(row) {
399            let end = col.min(line.len().saturating_sub(1));
400            for i in 0..=end {
401                if let Some(cell) = line.get_mut(i) {
402                    cell.ch = ' ';
403                    cell.style = self.cur_style;
404                }
405            }
406            self.mark_dirty_line(row);
407            if end < usize::MAX {
408                self.mark_dirty_cols(row, 0, end);
409            }
410        }
411    }
412
413    pub fn clear_ed_2(&mut self) {
414        // Clear entire screen (visible area only)
415        let st = self.screen_top();
416        let sb = st + self.rows.saturating_sub(1);
417        let end_row = sb.min(self.lines.len().saturating_sub(1));
418        for r in st..=end_row {
419            let len = self.lines.get(r).map(|l| l.len()).unwrap_or(0);
420            if let Some(line) = self.lines.get_mut(r) {
421                if len > 0 {
422                    line.clear();
423                }
424            }
425            if len > 0 {
426                self.mark_dirty_line(r);
427                self.mark_dirty_cols(r, 0, len.saturating_sub(1));
428            }
429        }
430        // place cursor at top-left of screen
431        self.cursor_row = st;
432        self.cursor_col = 0;
433    }
434
435    pub fn clear_scrollback(&mut self) {
436        // Clear scrollback: keep only the visible screen region
437        let st = self.screen_top();
438        if st > 0 {
439            let keep: Vec<Vec<Cell>> = self.lines.split_off(st);
440            self.lines = keep;
441            self.dirty_cols = vec![None; self.lines.len()];
442            self.cursor_row = self.cursor_row.saturating_sub(st);
443            self.mark_dirty_line(0);
444            if !self.lines.is_empty() {
445                let last_len = self.lines[0].len();
446                self.mark_dirty_cols(0, 0, last_len.saturating_sub(1));
447            }
448        }
449    }
450
451    pub fn insert_blanks(&mut self, count: usize) {
452        self.ensure_row(self.cursor_row);
453        let col = self.cursor_col;
454        if let Some(line) = self.lines.get_mut(self.cursor_row) {
455            let blanks = std::iter::repeat_n(Cell::new(' ', self.cur_style), count);
456            if col >= line.len() {
457                line.extend(blanks);
458            } else {
459                line.splice(col..col, blanks);
460            }
461            let end = col + count;
462            self.mark_dirty_line(self.cursor_row);
463            self.mark_dirty_cols(self.cursor_row, col, end);
464        }
465    }
466
467    pub fn delete_chars(&mut self, count: usize) {
468        self.ensure_row(self.cursor_row);
469        let col = self.cursor_col;
470        if let Some(line) = self.lines.get_mut(self.cursor_row) {
471            if col < line.len() {
472                let end = (col + count).min(line.len());
473                line.drain(col..end);
474                self.mark_dirty_line(self.cursor_row);
475                self.mark_dirty_cols(self.cursor_row, col, end.saturating_sub(1));
476            }
477        }
478    }
479
480    pub fn insert_lines(&mut self, count: usize) {
481        let row = self.cursor_row.min(self.lines.len());
482        for _ in 0..count {
483            self.lines.insert(row, Vec::new());
484            self.dirty_cols.insert(row, None);
485        }
486        // Trim to max_lines by removing from end
487        if self.lines.len() > self.max_lines {
488            let overflow = self.lines.len() - self.max_lines;
489            for _ in 0..overflow {
490                self.lines.pop();
491                self.dirty_cols.pop();
492            }
493        }
494        // Mark dirty affected range
495        let end = (row + count).min(self.lines.len().saturating_sub(1));
496        self.mark_dirty_line(row);
497        self.mark_dirty_line(end);
498    }
499
500    pub fn delete_lines(&mut self, count: usize) {
501        let row = self.cursor_row.min(self.lines.len());
502        let end = (row + count).min(self.lines.len());
503        if row < end {
504            self.lines.drain(row..end);
505            self.dirty_cols.drain(row..end);
506            // push one empty line at bottom to maintain capacity if desired
507            if self.lines.len() < self.max_lines {
508                self.lines.push(Vec::new());
509                self.dirty_cols.push(None);
510            }
511            self.mark_dirty_line(row);
512        }
513    }
514
515    // Horizontal Tab: advance to next 8-column stop
516    pub fn tab(&mut self) {
517        let next = ((self.cursor_col / 8) + 1) * 8;
518        if next > self.cursor_col {
519            // ensure cells until next-1 exist (filled with spaces)
520            self.ensure_col(self.cursor_row, next.saturating_sub(1));
521            // mark dirty area
522            self.mark_dirty_line(self.cursor_row);
523            self.mark_dirty_cols(self.cursor_row, self.cursor_col, next.saturating_sub(1));
524            self.cursor_col = next;
525        }
526    }
527
528    // ECH: erase N characters from cursor, cursor does not move
529    pub fn erase_chars(&mut self, count: usize) {
530        if count == 0 {
531            return;
532        }
533        let row = self.cursor_row;
534        let start = self.cursor_col;
535        let end = start.saturating_add(count).saturating_sub(1);
536        self.ensure_col(row, end);
537        if let Some(line) = self.lines.get_mut(row) {
538            let last = end.min(line.len().saturating_sub(1));
539            for i in start..=last {
540                if let Some(cell) = line.get_mut(i) {
541                    cell.ch = ' ';
542                    cell.style = self.cur_style;
543                }
544            }
545            self.mark_dirty_line(row);
546            self.mark_dirty_cols(row, start, last);
547        }
548    }
549}