sericom_core/screen_buffer/
render.rs

1use crossterm::style::Color;
2use std::io::BufWriter;
3
4use super::{Cursor, EscapeState, Line, ScreenBuffer, UIAction};
5use crate::configs::get_config;
6
7const MIN_RENDER_INTERVAL: tokio::time::Duration = tokio::time::Duration::from_millis(33);
8
9impl ScreenBuffer {
10    /// Takes incoming data (bytes (`u8`) from a serial connection) and
11    /// processes them accordingly, handling ascii escape sequences, to
12    /// render as characters/strings in the terminal.
13    pub fn add_data(&mut self, data: &[u8]) {
14        let text = String::from_utf8_lossy(data);
15        let mut chars = text.chars().peekable();
16
17        while let Some(ch) = chars.next() {
18            match self.escape_state {
19                EscapeState::Normal => {
20                    match ch {
21                        '\r' => {
22                            self.cursor_pos.x = 0;
23                            if chars.peek() == Some(&'\n') {
24                                chars.next();
25                                self.new_line();
26                            }
27                        }
28                        '\n' => {
29                            self.new_line();
30                        }
31                        '\x07' => {}
32                        '\x08' => {
33                            let mut temp_chars = chars.clone();
34                            // Matches the `\x08 ' ' \x08` deletion sequence
35                            if let (Some(' '), Some('\x08')) =
36                                (temp_chars.next(), temp_chars.next())
37                            {
38                                // Consume them - to remove from further processing
39                                chars.next();
40                                chars.next();
41                                self.move_cursor_left(1);
42                                self.set_char_at_cursor(' ');
43                            } else {
44                                // If not the deletion sequence, move cursor left
45                                // when receiving a single '\x08'
46                                self.move_cursor_left(1);
47                            }
48                        }
49                        '\x1B' => self.escape_state = EscapeState::Esc,
50                        c => {
51                            let mut batch = vec![c];
52                            while let Some(&next_ch) = chars.peek() {
53                                if next_ch.is_control()
54                                    || next_ch == '\x1B'
55                                    || self.cursor_pos.x + batch.len() as u16 >= self.width
56                                {
57                                    break;
58                                }
59                                batch.push(chars.next().unwrap());
60                            }
61                            self.add_char_batch(&batch);
62                        }
63                    }
64                }
65                EscapeState::Esc => match ch {
66                    '[' => self.escape_state = EscapeState::Csi,
67                    _ => self.escape_state = EscapeState::Normal,
68                },
69                EscapeState::Csi => match ch {
70                    ';' => self.escape_sequence.insert_separator(),
71                    c if ch.is_ascii_digit() => self.escape_sequence.push_num(c),
72                    c if c.is_ascii_alphabetic() => {
73                        // Reset because actions are the last members of a sequence
74                        self.escape_sequence.push_action(c);
75                        self.parse_sequence();
76                        self.escape_sequence.reset();
77                        self.escape_state = EscapeState::Normal;
78                    }
79                    // NOTE: May need to handle '?', ':', and '>'
80                    _ => self.escape_state = EscapeState::Normal,
81                },
82            }
83        }
84        self.scroll_to_bottom();
85        self.needs_render = true;
86    }
87
88    fn add_char_batch(&mut self, chars: &[char]) {
89        while self.cursor_pos.y >= self.lines.len() {
90            self.lines.push_back(Line::new(self.width as usize));
91        }
92
93        if let Some(line) = self.lines.get_mut(self.cursor_pos.y) {
94            for &ch in chars {
95                line.set_char(self.cursor_pos.x as usize, ch);
96                self.cursor_pos.x += 1;
97                if self.cursor_pos.x >= self.width {
98                    self.new_line();
99                    break;
100                }
101            }
102        }
103    }
104
105    /// A helper function to check whether the terminal's screen should be rendered.
106    pub fn should_render_now(&self) -> bool {
107        use tokio::time::Instant;
108
109        if !self.needs_render {
110            return false;
111        }
112
113        let now = Instant::now();
114        match self.last_render {
115            Some(last) => now.duration_since(last) >= MIN_RENDER_INTERVAL,
116            None => true,
117        }
118    }
119
120    /// Writes the lines/characters received from `add_data` to the terminal's screen.
121    /// As of now, `render` does not involve any diff-ing of previous renders.
122    ///
123    /// The nature of communicating to devices over a serial connection is similar
124    /// that of a terminal; lines get printed to a screen and with each new line,
125    /// all of the previously rendered characters must be re-rendered one cell higher.
126    ///
127    /// Because of this, the only diff-ing that would make sense would be
128    /// that of the cells within the screen that are simply blank.
129    pub fn render(&mut self) -> std::io::Result<()> {
130        use crossterm::{cursor, queue, style};
131        use std::io::{self, Write};
132        use tokio::time::Instant;
133
134        // let span = tracing::span!(Level::INFO, "Render");
135        // let _entered = span.enter();
136        if !self.needs_render {
137            return Ok(());
138        }
139
140        let mut writer = BufWriter::new(io::stdout());
141        queue!(writer, cursor::Hide)?;
142
143        for screen_y in 0..self.height {
144            let line_idx = self.view_start + screen_y as usize;
145            queue!(writer, cursor::MoveTo(0, screen_y))?;
146
147            if let Some(line) = self.lines.get(line_idx) {
148                let config = get_config();
149                let mut current_fg = Color::from(&config.appearance.fg);
150                let mut current_bg = Color::from(&config.appearance.bg);
151                queue!(writer, style::SetForegroundColor(current_fg))?;
152                queue!(writer, style::SetBackgroundColor(current_bg))?;
153
154                for cell in line {
155                    let fg = if cell.is_selected {
156                        Color::from(&config.appearance.hl_fg)
157                    } else {
158                        cell.fg_color
159                    };
160                    let bg = if cell.is_selected {
161                        Color::from(&config.appearance.hl_bg)
162                    } else {
163                        cell.bg_color
164                    };
165                    if fg != current_fg {
166                        queue!(writer, style::SetForegroundColor(fg))?;
167                        current_fg = fg;
168                    }
169                    if bg != current_bg {
170                        queue!(writer, style::SetBackgroundColor(bg))?;
171                        current_bg = bg;
172                    }
173                    queue!(writer, style::Print(cell.character))?;
174                }
175            } else {
176                queue!(writer, style::ResetColor)?;
177                queue!(writer, style::Print(" ".repeat(self.width as usize)))?;
178            }
179        }
180
181        // This is relative the the terminal's L x W, whereas self.cursor_pos.y
182        // is within the entire line buf; seems to only matter when self.lines.len() < self.height
183        // let screen_cursor_y = if self.cursor_pos.y >= self.view_start
184        //     && self.cursor_pos.y < self.view_start + self.height as usize
185        // {
186        //     (self.cursor_pos.y - self.view_start) as u16
187        // } else {
188        //     self.height - 1
189        // };
190        //
191        // event!(Level::INFO, screen_y = screen_cursor_y, self_y = self.cursor_pos.y);
192
193        queue!(
194            writer,
195            cursor::MoveTo(self.cursor_pos.x, self.cursor_pos.y as u16),
196            // cursor::MoveTo(self.cursor_pos.x, screen_cursor_y),
197            cursor::Show
198        )?;
199        writer.flush()?;
200
201        self.last_render = Some(Instant::now());
202        self.needs_render = false;
203        Ok(())
204    }
205}