sericom_core/screen_buffer/mod.rs
1//! This module contains the code needed for the implementation of a
2//! stateful buffer that holds a history of the lines/data received
3//! from the serial connection and the rendering/updating of the buffer
4//! to the terminal screen (stdout).
5//!
6//! Simply writing the data received from the serial connection directly
7//! to stdout creates one main issue: there is no history of previous lines
8//! that were received from the serial connection. Without a screen buffer,
9//! lines would simply be wiped from existence as they exit the terminal's screen.
10//!
11//! As a result, there would be no way to implement features like scrolling,
12//! highlighting text (for UI purposes), and getting characters at specific
13//! locations within the screen for things like copying to a clipboard.
14//!
15//! The screen buffer solves these issues by storing each line received from the
16//! connection in a [`VecDeque`]. It is important to note that
17//! currently, the **capacity of the [`VecDeque`] is hardcoded with a value of 10,000
18//! lines with [`MAX_SCROLLBACK`]**.
19
20mod cell;
21mod cursor;
22mod escape;
23mod line;
24mod render;
25mod ui_command;
26pub use cell::*;
27use crossterm::style::Attributes;
28pub use cursor::*;
29use escape::{EscapeSequence, EscapeState};
30pub use line::*;
31pub use ui_command::*;
32
33use std::collections::VecDeque;
34
35/// The maximum number of lines stored in memory in [`ScreenBuffer`].
36pub const MAX_SCROLLBACK: usize = 10000;
37
38/// The `ScreenBuffer` holds rendering state for the entire terminal's window/frame.
39///
40/// It mainly serves to allow for user-interactions that require a history and location
41/// of the data displayed within the terminal i.e. copy/paste, scrolling, & highlighting.
42#[derive(Debug)]
43pub struct ScreenBuffer {
44 /// Terminal width
45 width: u16,
46 /// Terminal height
47 height: u16,
48 /// Scrollback buffer (all lines received from the serial connection).
49 /// Limited by memory.
50 lines: VecDeque<Line>,
51 /// Current view into the buffer.
52 /// Denotes which line is at the top of the screen.
53 view_start: usize,
54 /// Position of the cursor within the `ScreenBuffer`.
55 cursor_pos: Position,
56 /// Start of text selection. Used for highlighting and copying to clipboard.
57 selection_start: Option<(u16, usize)>,
58 /// End of text selection. Used for highlighting and copying to clipboard.
59 selection_end: Option<(u16, usize)>,
60 /// Configuration for the maximum amount of lines to keep in memory.
61 max_scrollback: usize,
62 /// Represents the current state for handling ansii escape sequences
63 /// as incoming data is being processed.
64 escape_state: EscapeState,
65 /// As ascii escape sequences are recieved, they are built in the
66 /// [`EscapeSequence`] to evaluate upon a completed escape sequence.
67 escape_sequence: EscapeSequence,
68 /// Represents the time since [`ScreenBuffer::render()`] was last called.
69 last_render: Option<tokio::time::Instant>,
70 display_attributes: Attributes,
71 /// Indicates that [`ScreenBuffer`] has new data and needs to render.
72 needs_render: bool,
73}
74
75impl ScreenBuffer {
76 /// Constructs a new `ScreenBuffer`.
77 ///
78 /// Takes the `width` and `height` of the terminal.
79 pub fn new(width: u16, height: u16) -> Self {
80 let mut buffer = Self {
81 width,
82 height,
83 lines: VecDeque::new(),
84 view_start: 0,
85 cursor_pos: Position::home(),
86 selection_start: None,
87 selection_end: None,
88 max_scrollback: MAX_SCROLLBACK,
89 last_render: None,
90 needs_render: false,
91 escape_state: EscapeState::Normal,
92 escape_sequence: EscapeSequence::new(),
93 display_attributes: Attributes::none(),
94 };
95 // Start with an empty line
96 buffer.lines.push_back(Line::new(width as usize));
97 buffer
98 }
99
100 fn set_char_at_cursor(&mut self, ch: char) {
101 while self.cursor_pos.y >= self.lines.len() {
102 self.lines.push_back(Line::new(self.width as usize));
103 }
104
105 if let Some(line) = self.lines.get_mut(self.cursor_pos.y)
106 && (self.cursor_pos.x as usize) < line.len()
107 {
108 line.set_char(self.cursor_pos.x as usize, ch);
109 }
110 }
111
112 fn clear_from_cursor_to_sol(&mut self) {
113 if let Some(line) = self.lines.get_mut(self.cursor_pos.y) {
114 line.reset_to(self.cursor_pos.x as usize);
115 }
116 }
117
118 fn clear_from_cursor_to_sos(&mut self) {
119 self.clear_from_cursor_to_sol();
120 for line in self
121 .lines
122 .range_mut(self.view_start..=self.cursor_pos.y - 1)
123 {
124 line.reset();
125 }
126 }
127
128 fn clear_from_cursor_to_eol(&mut self) {
129 if let Some(line) = self.lines.get_mut(self.cursor_pos.y) {
130 line.reset_from(self.cursor_pos.x as usize);
131 }
132 }
133
134 fn clear_from_cursor_to_eos(&mut self) {
135 self.clear_from_cursor_to_eol();
136 for line in self.lines.range_mut(self.cursor_pos.y + 1..) {
137 line.reset();
138 }
139 }
140
141 fn clear_whole_line(&mut self) {
142 if let Some(line) = self.lines.get_mut(self.cursor_pos.y) {
143 line.reset();
144 }
145 }
146
147 fn new_line(&mut self) {
148 self.set_cursor_pos((0, self.cursor_pos.y + 1));
149
150 if self.cursor_pos.y >= self.lines.len() {
151 self.lines.push_back(Line::new(self.width as usize));
152 }
153
154 // Remove old lines if exceeding `ScreenBuffer.max_scrollback`
155 while self.lines.len() > self.max_scrollback {
156 self.lines.pop_front();
157 // Update the view position
158 if self.cursor_pos.y > 0 {
159 self.cursor_pos.y -= 1;
160 }
161 if self.view_start > 0 {
162 self.view_start -= 1;
163 }
164 }
165 }
166}