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 and is theoretically allowed to grow forever**; limited by memory.
19
20mod cell;
21mod cursor;
22mod escape;
23mod line;
24mod render;
25mod ui_command;
26use cell::*;
27use cursor::*;
28use escape::{EscapeSequence, EscapeState};
29use line::*;
30pub use ui_command::*;
31
32use std::collections::VecDeque;
33
34/// The `ScreenBuffer` holds rendering state for the entire terminal's window/frame.
35/// It mainly serves to allow for user-interactions that require a history and location
36/// of the data displayed within the terminal i.e. copy/paste, scrolling, & highlighting.
37#[derive(Debug)]
38pub struct ScreenBuffer {
39    /// Terminal width
40    width: u16,
41    /// Terminal height
42    height: u16,
43    /// Scrollback buffer (all lines received from the serial connection).
44    /// Limited by memory.
45    lines: VecDeque<Line>,
46    /// Current view into the buffer.
47    /// Denotes which line is at the top of the screen.
48    view_start: usize,
49    /// Position of the cursor within the `ScreenBuffer`.
50    cursor_pos: Position,
51    /// Start of text selection. Used for highlighting and copying to clipboard.
52    selection_start: Option<(u16, usize)>,
53    /// End of text selection. Used for highlighting and copying to clipboard.
54    selection_end: Option<(u16, usize)>,
55    /// Configuration for the maximum amount of lines to keep in memory.
56    max_scrollback: usize,
57    /// Represents the current state for handling ansii escape sequences
58    /// as incoming data is being processed.
59    escape_state: EscapeState,
60    /// As ascii escape sequences are recieved, they are built in the
61    /// [`EscapeSequence`] to evaluate upon a completed escape sequence.
62    escape_sequence: EscapeSequence,
63    /// Represents the time since [`ScreenBuffer::render()`] was last called.
64    last_render: Option<tokio::time::Instant>,
65    /// Indicates that [`ScreenBuffer`] has new data and needs to render.
66    needs_render: bool,
67}
68
69impl ScreenBuffer {
70    /// Constructs a new `ScreenBuffer` to be used at the start of a
71    /// serial connection session.
72    pub fn new(width: u16, height: u16, max_scrollback: usize) -> Self {
73        let mut buffer = Self {
74            width,
75            height,
76            lines: VecDeque::new(),
77            view_start: 0,
78            cursor_pos: Position::home(),
79            selection_start: None,
80            selection_end: None,
81            max_scrollback,
82            last_render: None,
83            needs_render: false,
84            escape_state: EscapeState::Normal,
85            escape_sequence: EscapeSequence::new(),
86        };
87        // Start with an empty line
88        buffer.lines.push_back(Line::new(width as usize));
89        buffer
90    }
91
92    fn set_char_at_cursor(&mut self, ch: char) {
93        while self.cursor_pos.y >= self.lines.len() {
94            self.lines.push_back(Line::new(self.width as usize));
95        }
96
97        if let Some(line) = self.lines.get_mut(self.cursor_pos.y)
98            && (self.cursor_pos.x as usize) < line.len()
99        {
100            line.set_char(self.cursor_pos.x as usize, ch);
101        }
102    }
103
104    fn clear_from_cursor_to_sol(&mut self) {
105        if let Some(line) = self.lines.get_mut(self.cursor_pos.y) {
106            line.reset_to_idx(self.cursor_pos.x as usize);
107        }
108    }
109
110    fn clear_from_cursor_to_sos(&mut self) {
111        self.clear_from_cursor_to_sol();
112        for line in self
113            .lines
114            .range_mut(self.view_start..=self.cursor_pos.y - 1)
115        {
116            line.reset();
117        }
118    }
119
120    fn clear_from_cursor_to_eol(&mut self) {
121        if let Some(line) = self.lines.get_mut(self.cursor_pos.y) {
122            line.reset_from_idx(self.cursor_pos.x as usize);
123        }
124    }
125
126    fn clear_from_cursor_to_eos(&mut self) {
127        self.clear_from_cursor_to_eol();
128        for line in self.lines.range_mut(self.cursor_pos.y + 1..) {
129            line.reset();
130        }
131    }
132
133    fn clear_whole_line(&mut self) {
134        if let Some(line) = self.lines.get_mut(self.cursor_pos.y) {
135            line.reset();
136        }
137    }
138
139    fn new_line(&mut self) {
140        self.set_cursor_pos((0, self.cursor_pos.y + 1));
141
142        if self.cursor_pos.y >= self.lines.len() {
143            self.lines.push_back(Line::new(self.width as usize));
144        }
145
146        // Remove old lines if exceeding `ScreenBuffer.max_scrollback`
147        while self.lines.len() > self.max_scrollback {
148            self.lines.pop_front();
149            // Update the view position
150            if self.cursor_pos.y > 0 {
151                self.cursor_pos.y -= 1;
152            }
153            if self.view_start > 0 {
154                self.view_start -= 1;
155            }
156        }
157    }
158
159    #[allow(dead_code)]
160    pub fn get_stats(&self) -> BufferStats {
161        let total_lines = self.lines.len();
162        BufferStats {
163            total_lines,
164            view_start: self.view_start,
165            view_end: (self.view_start + self.height as usize).min(total_lines),
166            cursor_line: self.cursor_pos.y,
167            has_selection: self.selection_start.is_some() && self.selection_end.is_some(),
168        }
169    }
170}
171
172#[allow(dead_code)]
173#[derive(Debug)]
174pub struct BufferStats {
175    pub total_lines: usize,
176    pub view_start: usize,
177    pub view_end: usize,
178    pub cursor_line: usize,
179    pub has_selection: bool,
180}