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}