Skip to main content

sericom_core/screen_buffer/
ui_command.rs

1use crate::screen_buffer::Position;
2
3use super::{Cursor, Line, ScreenBuffer};
4
5/// `UICommand` is used for communication between stdin and the [`ScreenBuffer`].
6#[non_exhaustive]
7#[derive(Clone, Debug)]
8pub enum UICommand {
9    /// Scrolls up by `usize` lines
10    ScrollUp(usize),
11    /// Scrolls down by `usize` lines
12    ScrollDown(usize),
13    /// Scrolls to the last line (most recent)
14    ScrollBottom,
15    /// Scrolls to the beginning of the scrollback buffer (oldest line)
16    ScrollTop,
17    /// Starts text-selection at [`Position`]
18    StartSelection(Position),
19    /// Updates text-selection to [`Position`]
20    UpdateSelection(Position),
21    /// Copies the underlying selected text to the user's clipboard
22    CopySelection,
23    /// Completely clears the lines in the scrollback buffer
24    ClearBuffer,
25}
26
27pub(crate) trait UIAction {
28    fn scroll_up(&mut self, lines: usize);
29    fn scroll_down(&mut self, lines: usize);
30    fn scroll_to_bottom(&mut self);
31    fn scroll_to_top(&mut self);
32    fn start_selection(&mut self, pos: Position);
33    fn update_selection(&mut self, pos: Position);
34    fn clear_selection(&mut self);
35    fn copy_to_clipboard(&mut self) -> std::io::Result<()>;
36    fn clear_buffer(&mut self);
37    fn clear_screen(&mut self);
38}
39
40impl UIAction for ScreenBuffer {
41    /// Called to scroll the terminal up by `lines`.
42    fn scroll_up(&mut self, lines: usize) {
43        if self.view_start >= lines {
44            self.view_start -= lines;
45        } else {
46            self.view_start = 0;
47        }
48        self.clear_selection();
49        self.needs_render = true;
50    }
51
52    /// Called to scroll the terminal down by `lines`.
53    fn scroll_down(&mut self, lines: usize) {
54        let max_view_start = self.lines.len().saturating_sub(self.height as usize);
55        self.view_start = (self.view_start + lines).min(max_view_start);
56        self.clear_selection();
57        self.needs_render = true;
58    }
59
60    /// Scrolls to the bottom of the screen. The bottom of the screen is
61    /// the same as the most recent lines received from the serial connection
62    fn scroll_to_bottom(&mut self) {
63        self.view_start = self.lines.len().saturating_sub(self.height as usize);
64        self.needs_render = true;
65    }
66
67    /// Scrolls to the top of the serial connection's history.
68    fn scroll_to_top(&mut self) {
69        self.view_start = 0;
70        self.needs_render = true;
71    }
72
73    /// Sets the position within the screen for the start of a selection.
74    /// Where `screen_x` is the x-position of the start of the selection,
75    /// and `screen_y` is the y-position (line) of the start of the selection.
76    fn start_selection(&mut self, pos: Position) {
77        let absolute_line = self.view_start + pos.y;
78        self.clear_selection();
79        self.selection_start = Some((pos.x, absolute_line));
80        self.needs_render = true;
81    }
82
83    /// Update's a selection to include the position passed to it.
84    /// Where `screen_x` is the x-position and `screen_y` is the y-position (line).
85    fn update_selection(&mut self, pos: Position) {
86        let absolute_line = self.view_start + pos.y;
87        self.selection_end = Some((pos.x, absolute_line));
88        self.update_selection_highlighting();
89        self.needs_render = true;
90    }
91
92    /// Clears the selection state.
93    fn clear_selection(&mut self) {
94        for line in &mut self.lines {
95            line.clear_selection();
96        }
97        self.selection_start = None;
98        self.selection_end = None;
99        self.needs_render = true;
100    }
101
102    /// Copy's the currently selected text to the user's clipboard.
103    fn copy_to_clipboard(&mut self) -> std::io::Result<()> {
104        use crossterm::{clipboard, execute};
105
106        let selected_text = self.get_selected_text();
107        if !selected_text.is_empty() {
108            execute!(
109                std::io::stdout(),
110                clipboard::CopyToClipboard::to_clipboard_from(selected_text)
111            )?;
112        }
113        self.clear_selection();
114        Ok(())
115    }
116
117    /// Clears the entire serial connection's history and reset's the screen.
118    /// Similar to <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>l</kbd> in a terminal,
119    /// except this will reset the connection's message history (on the user's side).
120    fn clear_buffer(&mut self) {
121        self.lines.clear();
122        self.view_start = 0;
123        self.set_cursor_pos((0_u16, 0_usize));
124        self.lines.push_back(Line::new(self.width as usize));
125        self.needs_render = true;
126    }
127
128    /// Clears the current *visible* screen while keeping the buffer's history
129    fn clear_screen(&mut self) {
130        for _ in 0..self.height {
131            self.lines.push_back(Line::new(self.width as usize));
132        }
133        self.view_start = self.lines.len().saturating_sub(self.height as usize);
134        self.needs_render = true;
135    }
136}
137
138impl ScreenBuffer {
139    fn update_selection_highlighting(&mut self) {
140        for line in &mut self.lines {
141            line.clear_selection();
142        }
143
144        if let (Some((start_x, start_line)), Some((end_x, end_line))) =
145            (self.selection_start, self.selection_end)
146        {
147            let (start_line, start_x, end_line, end_x) =
148                // If start < end or if start = end, start x has to be less than end x
149                if start_line < end_line || (start_line == end_line && start_x <= end_x) {
150                    (start_line, start_x, end_line, end_x)
151                } else {
152                    (end_line, end_x, start_line, start_x)
153                };
154
155            for line_idx in start_line..=end_line {
156                if let Some(line) = self.lines.get_mut(line_idx) {
157                    let line_start_x = if line_idx == start_line { start_x } else { 0 };
158                    let line_end_x = if line_idx == end_line {
159                        end_x
160                    } else {
161                        self.width - 1
162                    };
163
164                    for x in line_start_x..=line_end_x.min(self.width - 1) {
165                        if let Some(cell) = line.get_mut_cell(x as usize) {
166                            cell.is_selected = true;
167                        }
168                    }
169                }
170            }
171        }
172    }
173
174    fn get_selected_text(&self) -> String {
175        if let (Some((start_x, start_line)), Some((end_x, end_line))) =
176            (self.selection_start, self.selection_end)
177        {
178            let (start_line, start_x, end_line, end_x) =
179                if start_line < end_line || (start_line == end_line && start_x <= end_x) {
180                    (start_line, start_x, end_line, end_x)
181                } else {
182                    (end_line, end_x, start_line, start_x)
183                };
184
185            let mut result = String::new();
186
187            for line_idx in start_line..=end_line {
188                if let Some(line) = self.lines.get(line_idx) {
189                    let line_start_x = if line_idx == start_line { start_x } else { 0 };
190                    let line_end_x = if line_idx == end_line {
191                        end_x
192                    } else {
193                        self.width - 1
194                    };
195
196                    for x in line_start_x..=line_end_x.min(self.width - 1) {
197                        if let Some(cell) = line.get_cell(x as usize) {
198                            result.push(cell.character);
199                        }
200                    }
201
202                    if line_idx < end_line {
203                        result.push('\n');
204                    }
205                }
206            }
207
208            result.trim_end().to_string()
209        } else {
210            String::new()
211        }
212    }
213}