fltk_term/
cell_performer.rs

1use crate::cells::{CellBuffer, Style};
2use crate::styles::*;
3use core::cmp::min;
4use fltk::enums::Color;
5use vte::{Params, Perform};
6
7pub struct CellsPerformer<'a> {
8    pub buf: &'a mut CellBuffer,
9    cur_style: Style,
10    fg_base: Option<u16>,
11    bg_base: Option<u16>,
12}
13
14impl<'a> CellsPerformer<'a> {
15    pub fn new(buf: &'a mut CellBuffer) -> Self {
16        let cur_style = Style::new(buf.default_bg, buf.default_fg);
17        Self {
18            buf,
19            cur_style,
20            fg_base: None,
21            bg_base: None,
22        }
23    }
24}
25
26fn fg_from_code(code: u16) -> Option<Color> {
27    match code {
28        30 => Some(BLACK),
29        31 => Some(RED),
30        32 => Some(GREEN),
31        33 => Some(YELLOW),
32        34 => Some(BLUE),
33        35 => Some(MAGENTA),
34        36 => Some(CYAN),
35        37 => Some(WHITE),
36        // Bright variants (90-97)
37        90 => Some(BRIGHT_BLACK),
38        91 => Some(BRIGHT_RED),
39        92 => Some(BRIGHT_GREEN),
40        93 => Some(BRIGHT_YELLOW),
41        94 => Some(BRIGHT_BLUE),
42        95 => Some(BRIGHT_MAGENTA),
43        96 => Some(BRIGHT_CYAN),
44        97 => Some(BRIGHT_WHITE),
45        _ => None,
46    }
47}
48
49fn bg_from_code(code: u16) -> Option<Color> {
50    match code {
51        40 => Some(BLACK),
52        41 => Some(RED),
53        42 => Some(GREEN),
54        43 => Some(YELLOW),
55        44 => Some(BLUE),
56        45 => Some(MAGENTA),
57        46 => Some(CYAN),
58        47 => Some(WHITE),
59        // Bright variants (100-107)
60        100 => Some(BRIGHT_BLACK),
61        101 => Some(BRIGHT_RED),
62        102 => Some(BRIGHT_GREEN),
63        103 => Some(BRIGHT_YELLOW),
64        104 => Some(BRIGHT_BLUE),
65        105 => Some(BRIGHT_MAGENTA),
66        106 => Some(BRIGHT_CYAN),
67        107 => Some(BRIGHT_WHITE),
68        _ => None,
69    }
70}
71
72fn xterm256_to_color(idx: u16) -> Color {
73    match idx {
74        0 => Color::from_rgb(0, 0, 0),
75        1 => Color::from_rgb(205, 0, 0),
76        2 => Color::from_rgb(0, 205, 0),
77        3 => Color::from_rgb(205, 205, 0),
78        4 => Color::from_rgb(0, 0, 238),
79        5 => Color::from_rgb(205, 0, 205),
80        6 => Color::from_rgb(0, 205, 205),
81        7 => Color::from_rgb(229, 229, 229),
82        8 => Color::from_rgb(127, 127, 127),
83        9 => Color::from_rgb(255, 0, 0),
84        10 => Color::from_rgb(0, 255, 0),
85        11 => Color::from_rgb(255, 255, 0),
86        12 => Color::from_rgb(92, 92, 255),
87        13 => Color::from_rgb(255, 0, 255),
88        14 => Color::from_rgb(0, 255, 255),
89        15 => Color::from_rgb(255, 255, 255),
90        16..=231 => {
91            let i = idx - 16;
92            let r = i / 36;
93            let g = (i % 36) / 6;
94            let b = i % 6;
95            let conv = |v: u16| -> u8 { [0, 95, 135, 175, 215, 255][min(v as usize, 5)] as u8 };
96            Color::from_rgb(conv(r), conv(g), conv(b))
97        }
98        232..=255 => {
99            let shade = 8 + 10 * (idx - 232);
100            let c = shade as u8;
101            Color::from_rgb(c, c, c)
102        }
103        _ => WHITE,
104    }
105}
106
107impl Perform for CellsPerformer<'_> {
108    fn print(&mut self, c: char) {
109        self.buf.write_char(c);
110    }
111
112    fn execute(&mut self, byte: u8) {
113        match byte {
114            b'\n' => {
115                // Line feed - move cursor down and to beginning of line
116                self.buf.line_feed();
117            }
118            b'\r' => {
119                // Carriage return - move cursor to beginning of current line
120                self.buf.carriage_return();
121            }
122            b'\t' => {
123                // Horizontal Tab - advance to next 8-col tab stop
124                self.buf.tab();
125            }
126            8 => {
127                // backspace
128                let (row, col) = self.buf.cursor();
129                if col > 0 {
130                    self.buf.move_cursor_abs(row, col - 1);
131                }
132            }
133            7 => {
134                // Bell character - ignore for now (could add system beep later)
135            }
136            _ => {}
137        }
138    }
139
140    fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, c: char) {
141        match c {
142            'm' => {
143                // SGR
144                if params.is_empty() {
145                    self.cur_style = Style::new(self.buf.default_bg, self.buf.default_fg);
146                    self.buf.set_style(self.cur_style);
147                    return;
148                }
149                let mut it = params.iter().peekable();
150                while let Some(p) = it.next() {
151                    let n = p[0];
152                    match n {
153                        0 => {
154                            self.cur_style = Style::new(self.buf.default_bg, self.buf.default_fg);
155                            self.fg_base = None;
156                            self.bg_base = None;
157                        }
158                        1 => {
159                            self.cur_style.bold = true;
160                        }
161                        2 => {
162                            self.cur_style.faint = true;
163                        }
164                        3 => {
165                            self.cur_style.italic = true;
166                        }
167                        4 => {
168                            self.cur_style.underline = true;
169                        }
170                        5 => {
171                            // blink (visual noop in current renderer)
172                        }
173                        7 => {
174                            self.cur_style.inverse = true;
175                        }
176                        8 => {
177                            // conceal; treat as faint for now
178                            self.cur_style.faint = true;
179                        }
180                        9 => {
181                            self.cur_style.strikethrough = true;
182                        }
183                        21 => {
184                            // double underline -> map to underline true
185                            self.cur_style.underline = true;
186                        }
187                        22 => {
188                            self.cur_style.bold = false;
189                            self.cur_style.faint = false;
190                        }
191                        23 => {
192                            self.cur_style.italic = false;
193                        }
194                        24 => {
195                            self.cur_style.underline = false;
196                        }
197                        25 => {
198                            // blink off
199                        }
200                        27 => {
201                            self.cur_style.inverse = false;
202                        }
203                        28 => {
204                            self.cur_style.faint = false;
205                        }
206                        29 => {
207                            self.cur_style.strikethrough = false;
208                        }
209                        53 => {
210                            self.cur_style.overline = true;
211                        }
212                        55 => {
213                            self.cur_style.overline = false;
214                        }
215                        30..=37 => {
216                            self.fg_base = Some(n);
217                            if let Some(c) = fg_from_code(n) {
218                                self.cur_style.fg = c;
219                            }
220                        }
221                        90..=97 => {
222                            self.fg_base = None;
223                            if let Some(c) = fg_from_code(n) {
224                                self.cur_style.fg = c;
225                            }
226                        }
227                        39 => {
228                            self.cur_style.fg = WHITE;
229                            self.fg_base = None;
230                        }
231                        40..=47 => {
232                            self.bg_base = Some(n);
233                            if let Some(c) = bg_from_code(n) {
234                                self.cur_style.bg = c;
235                            }
236                        }
237                        100..=107 => {
238                            self.bg_base = None;
239                            if let Some(c) = bg_from_code(n) {
240                                self.cur_style.bg = c;
241                            }
242                        }
243                        49 => {
244                            self.cur_style.bg = BLACK;
245                            self.bg_base = None;
246                        }
247                        38 => {
248                            if let Some(mode) = it.next() {
249                                match mode[0] {
250                                    5 => {
251                                        if let Some(col) = it.next() {
252                                            self.cur_style.fg = xterm256_to_color(col[0]);
253                                            self.fg_base = None;
254                                        }
255                                    }
256                                    2 => {
257                                        let r = it.next().map(|x| x[0] as u8).unwrap_or(255);
258                                        let g = it.next().map(|x| x[0] as u8).unwrap_or(255);
259                                        let b = it.next().map(|x| x[0] as u8).unwrap_or(255);
260                                        self.cur_style.fg = Color::from_rgb(r, g, b);
261                                        self.fg_base = None;
262                                    }
263                                    _ => {}
264                                }
265                            }
266                        }
267                        48 => {
268                            if let Some(mode) = it.next() {
269                                match mode[0] {
270                                    5 => {
271                                        if let Some(col) = it.next() {
272                                            self.cur_style.bg = xterm256_to_color(col[0]);
273                                            self.bg_base = None;
274                                        }
275                                    }
276                                    2 => {
277                                        let r = it.next().map(|x| x[0] as u8).unwrap_or(0);
278                                        let g = it.next().map(|x| x[0] as u8).unwrap_or(0);
279                                        let b = it.next().map(|x| x[0] as u8).unwrap_or(0);
280                                        self.cur_style.bg = Color::from_rgb(r, g, b);
281                                        self.bg_base = None;
282                                    }
283                                    _ => {}
284                                }
285                            }
286                        }
287                        _ => {}
288                    }
289                }
290                self.buf.set_style(self.cur_style);
291            }
292            'K' => {
293                // EL - Erase in Line: 0 right, 1 left, 2 entire
294                let mode = params.iter().next().map(|p| p[0]).unwrap_or(0);
295                match mode {
296                    0 => self.buf.clear_eol_0(),
297                    1 => self.buf.clear_eol_1(),
298                    2 => self.buf.clear_eol_2(),
299                    _ => {}
300                }
301            }
302            'H' | 'f' => {
303                // CUP: row;col (1-based)
304                let mut it = params.iter();
305                let row = it
306                    .next()
307                    .map(|p| p[0] as usize)
308                    .unwrap_or(1)
309                    .saturating_sub(1);
310                let col = it
311                    .next()
312                    .map(|p| p[0] as usize)
313                    .unwrap_or(1)
314                    .saturating_sub(1);
315                self.buf.move_cursor_abs(row, col);
316            }
317            'G' => {
318                // CHA: horizontal absolute column
319                let col = params
320                    .iter()
321                    .next()
322                    .map(|p| p[0] as usize)
323                    .unwrap_or(1)
324                    .saturating_sub(1);
325                let (row, _) = self.buf.cursor();
326                self.buf.move_cursor_abs(row, col);
327            }
328            'A' => {
329                // CUU: move up
330                let count = params.iter().next().map(|p| p[0] as isize).unwrap_or(1);
331                self.buf.move_cursor_rel(-{ count }, 0);
332            }
333            'B' => {
334                // CUD: move down
335                let count = params.iter().next().map(|p| p[0] as isize).unwrap_or(1);
336                self.buf.move_cursor_rel(count, 0);
337            }
338            'C' => {
339                // CUF: move forward
340                let count = params.iter().next().map(|p| p[0] as isize).unwrap_or(1);
341                self.buf.move_cursor_rel(0, count);
342            }
343            'D' => {
344                // CUB: move backward
345                let count = params.iter().next().map(|p| p[0] as isize).unwrap_or(1);
346                self.buf.move_cursor_rel(0, -{ count });
347            }
348            'J' => {
349                // ED - erase display: 0 to end, 1 from start, 2 entire, 3 clear scrollback
350                let mode = params.iter().next().map(|p| p[0]).unwrap_or(0);
351                match mode {
352                    0 => self.buf.clear_ed_0(),
353                    1 => self.buf.clear_ed_1(),
354                    2 => self.buf.clear_ed_2(),
355                    3 => self.buf.clear_scrollback(),
356                    _ => {}
357                }
358            }
359            '@' => {
360                // ICH - insert blanks
361                let count = params.iter().next().map(|p| p[0] as usize).unwrap_or(1);
362                self.buf.insert_blanks(count);
363            }
364            'P' => {
365                // DCH - delete characters
366                let count = params.iter().next().map(|p| p[0] as usize).unwrap_or(1);
367                self.buf.delete_chars(count);
368            }
369            'X' => {
370                // ECH - erase characters (replace with blanks)
371                let count = params.iter().next().map(|p| p[0] as usize).unwrap_or(1);
372                self.buf.erase_chars(count);
373            }
374            'L' => {
375                // IL - insert lines
376                let count = params.iter().next().map(|p| p[0] as usize).unwrap_or(1);
377                self.buf.insert_lines(count);
378            }
379            'M' => {
380                // DL - delete lines
381                let count = params.iter().next().map(|p| p[0] as usize).unwrap_or(1);
382                self.buf.delete_lines(count);
383            }
384            _ => {}
385        }
386    }
387
388    fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {}
389    fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _c: char) {}
390    fn put(&mut self, _byte: u8) {}
391    fn unhook(&mut self) {}
392    fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {}
393}