Skip to main content

sericom_core/screen_buffer/
render.rs

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