Skip to main content

rmux_core/screen/
view.rs

1use crate::grid::{GridLine, GridLineFlags};
2
3use super::Screen;
4
5/// Read-only copy of one rendered screen cell for copy-mode consumers.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct ScreenCellView {
8    pub(super) text: String,
9    pub(super) width: u8,
10    pub(super) padding: bool,
11    pub(super) attr: u16,
12    pub(super) fg: crate::input::Colour,
13    pub(super) bg: crate::input::Colour,
14    pub(super) us: crate::input::Colour,
15    pub(super) link: u32,
16}
17
18impl ScreenCellView {
19    /// Returns the stored cell text.
20    #[must_use]
21    pub fn text(&self) -> &str {
22        &self.text
23    }
24
25    /// Returns the display width of the cell.
26    #[must_use]
27    pub const fn width(&self) -> u8 {
28        self.width
29    }
30
31    /// Returns whether the cell is padding for a wide glyph.
32    #[must_use]
33    pub const fn is_padding(&self) -> bool {
34        self.padding
35    }
36
37    /// Returns the cell attributes.
38    #[must_use]
39    pub const fn attr(&self) -> u16 {
40        self.attr
41    }
42
43    /// Returns the cell foreground colour.
44    #[must_use]
45    pub const fn fg(&self) -> crate::input::Colour {
46        self.fg
47    }
48
49    /// Returns the cell background colour.
50    #[must_use]
51    pub const fn bg(&self) -> crate::input::Colour {
52        self.bg
53    }
54
55    /// Returns the cell underline colour.
56    #[must_use]
57    pub const fn us(&self) -> crate::input::Colour {
58        self.us
59    }
60
61    /// Returns the hyperlink inner ID for the cell.
62    #[must_use]
63    pub const fn link(&self) -> u32 {
64        self.link
65    }
66}
67
68/// Read-only copy of one absolute screen line for copy-mode consumers.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct ScreenLineView {
71    pub(super) cells: Vec<ScreenCellView>,
72    width: u32,
73    pub(super) wrapped: bool,
74    pub(super) start_prompt: bool,
75    pub(super) start_output: bool,
76    pub(super) time: i64,
77}
78
79impl ScreenLineView {
80    /// Returns the stored cells for the line.
81    #[must_use]
82    pub fn cells(&self) -> &[ScreenCellView] {
83        &self.cells
84    }
85
86    /// Returns the terminal-width column span represented by this line.
87    #[must_use]
88    pub const fn width(&self) -> u32 {
89        self.width
90    }
91
92    /// Returns one cell by column.
93    #[must_use]
94    pub fn cell(&self, x: u32) -> Option<&ScreenCellView> {
95        self.cells.get(x as usize)
96    }
97
98    /// Returns whether the line wraps onto the following row.
99    #[must_use]
100    pub const fn wrapped(&self) -> bool {
101        self.wrapped
102    }
103
104    /// Returns whether the line starts a shell prompt block.
105    #[must_use]
106    pub const fn start_prompt(&self) -> bool {
107        self.start_prompt
108    }
109
110    /// Returns whether the line starts a shell output block.
111    #[must_use]
112    pub const fn start_output(&self) -> bool {
113        self.start_output
114    }
115
116    /// Returns the line timestamp.
117    #[must_use]
118    pub const fn time(&self) -> i64 {
119        self.time
120    }
121
122    /// Resolves the owning non-padding cell for a column.
123    #[must_use]
124    pub fn owning_cell_x(&self, x: u32) -> Option<u32> {
125        if x >= self.width {
126            return None;
127        }
128        let Some(cell) = self.cell(x) else {
129            return Some(x);
130        };
131        if !cell.is_padding() {
132            return Some(x);
133        }
134
135        let mut owner = x;
136        while owner > 0 {
137            owner -= 1;
138            let candidate = self.cell(owner)?;
139            if !candidate.is_padding() {
140                let width = u32::from(candidate.width().max(1));
141                if owner.saturating_add(width) > x {
142                    return Some(owner);
143                }
144                return None;
145            }
146        }
147        None
148    }
149}
150
151impl Screen {
152    /// Returns a read-only copy of one absolute line.
153    #[must_use]
154    pub fn absolute_line_view(&self, absolute_y: usize) -> Option<ScreenLineView> {
155        let line = self.grid.absolute_line(absolute_y)?;
156        let width = u32::from(self.grid.size().cols.max(1));
157        let cells = if let Some(text) = line.plain_text() {
158            let mut cells = text
159                .bytes()
160                .map(|byte| ScreenCellView {
161                    text: char::from(byte).to_string(),
162                    width: 1,
163                    padding: false,
164                    attr: 0,
165                    fg: crate::input::COLOUR_DEFAULT,
166                    bg: crate::input::COLOUR_DEFAULT,
167                    us: crate::input::COLOUR_DEFAULT,
168                    link: 0,
169                })
170                .collect::<Vec<_>>();
171            cells.resize_with(width as usize, || ScreenCellView {
172                text: " ".to_owned(),
173                width: 1,
174                padding: false,
175                attr: 0,
176                fg: crate::input::COLOUR_DEFAULT,
177                bg: crate::input::COLOUR_DEFAULT,
178                us: crate::input::COLOUR_DEFAULT,
179                link: 0,
180            });
181            cells
182        } else {
183            line.cells()
184                .iter()
185                .map(|cell| ScreenCellView {
186                    text: cell.text().to_owned(),
187                    width: cell.width(),
188                    padding: cell.is_padding(),
189                    attr: cell.attr(),
190                    fg: cell.fg(),
191                    bg: cell.bg(),
192                    us: cell.us(),
193                    link: cell.link(),
194                })
195                .collect()
196        };
197        Some(ScreenLineView {
198            cells,
199            width,
200            wrapped: line.flags().contains(GridLineFlags::WRAPPED),
201            start_prompt: line.flags().contains(GridLineFlags::START_PROMPT),
202            start_output: line.flags().contains(GridLineFlags::START_OUTPUT),
203            time: line.time(),
204        })
205    }
206
207    /// Clones the screen as a standalone viewport over its absolute lines.
208    #[must_use]
209    pub fn clone_viewport(&self, top_line: usize, cursor_x: u32, cursor_absolute_y: usize) -> Self {
210        let size = self.grid.size();
211        let rows = usize::from(size.rows.max(1));
212        let cols = u32::from(size.cols.max(1));
213        let total_lines = self.absolute_line_count();
214        let top_line = top_line.min(total_lines.saturating_sub(rows));
215        let mut viewport = Self::new(size, 0);
216
217        viewport.mode = self.mode;
218        viewport.cursor_style = self.cursor_style;
219        viewport.title = self.title.clone();
220        viewport.window_name = self.window_name.clone();
221        viewport.path = self.path.clone();
222        viewport.title_stack = self.title_stack.clone();
223        viewport.hyperlinks = self.hyperlinks.clone();
224        viewport.active_hyperlink = self.active_hyperlink;
225        viewport.bell_count = 0;
226        viewport.utf8_config = self.utf8_config.clone();
227
228        let lines = (0..rows)
229            .map(|offset| {
230                self.grid
231                    .absolute_line(top_line + offset)
232                    .cloned()
233                    .unwrap_or_else(|| GridLine::new(cols))
234            })
235            .collect();
236        viewport.grid.replace_visible(lines);
237
238        viewport.cursor_x = cursor_x.min(viewport.max_cursor_x());
239        viewport.cursor_y = if (top_line..top_line + rows).contains(&cursor_absolute_y) {
240            (cursor_absolute_y - top_line) as u32
241        } else {
242            0
243        };
244        viewport.pending_wrap = false;
245        viewport.saved_cursor_x = None;
246        viewport.saved_cursor_y = None;
247        viewport.saved_cursor_pending_wrap = false;
248        viewport.saved_grid = None;
249        viewport.rupper = 0;
250        viewport.rlower = u32::from(size.rows.max(1)).saturating_sub(1);
251        viewport.reset_tabs();
252        viewport
253    }
254}