Skip to main content

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}