1use alacritty_terminal::event::EventListener;
9use alacritty_terminal::grid::{Dimensions, Grid};
10use alacritty_terminal::index::Line;
11use alacritty_terminal::term::Term;
12use alacritty_terminal::term::cell::{Cell, Flags};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct Cursor {
17 pub col: u16,
18 pub row: u16,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct Screen {
24 pub text: String,
25 pub cursor: Cursor,
26 pub cols: u16,
27 pub rows: u16,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct ReadResult {
33 pub text: String,
34 pub cursor: u64,
36 pub cols: u16,
37 pub rows: u16,
38 pub truncated: bool,
39}
40
41pub fn extract<T: EventListener>(term: &Term<T>) -> Screen {
43 let grid = term.grid();
44 let rows = grid.screen_lines();
45 let cols = grid.columns();
46 let mut lines = Vec::with_capacity(rows);
47 for row in 0..rows {
48 lines.push(line_text(grid, Line(i32::try_from(row).unwrap_or(i32::MAX))));
49 }
50 let point = grid.cursor.point;
51 let cursor = Cursor {
52 col: u16::try_from(point.column.0).unwrap_or(u16::MAX),
53 row: u16::try_from(
54 point.line.0.clamp(0, i32::try_from(rows.saturating_sub(1)).unwrap_or(i32::MAX)),
55 )
56 .unwrap_or(0),
57 };
58 Screen {
59 text: lines.join("\n"),
60 cursor,
61 cols: u16::try_from(cols).unwrap_or(u16::MAX),
62 rows: u16::try_from(rows).unwrap_or(u16::MAX),
63 }
64}
65
66pub fn read<T: EventListener>(
79 term: &Term<T>,
80 appended: u64,
81 since: Option<u64>,
82 max_lines: usize,
83 max_bytes: usize,
84) -> ReadResult {
85 let grid = term.grid();
86 let top = grid.topmost_line().0;
87 let bottom = grid.bottommost_line().0;
88 let total = u64::try_from(bottom - top + 1).unwrap_or(0);
89 let first = appended.saturating_sub(total);
90 let tail = total.saturating_sub(u64::try_from(max_lines).unwrap_or(u64::MAX));
91 let (start, mut truncated) = match since {
95 Some(since) if since >= first => (since - first, false),
96 Some(_) => (0, true),
97 None => (tail, tail > 0),
98 };
99 let start = start.min(total);
100 let mut lines = Vec::new();
101 let mut bytes = 0usize;
102 for index in start..total {
103 let Ok(offset) = i32::try_from(index) else { break };
104 let text = line_text(grid, Line(top + offset));
105 let len = text.len() + 1;
106 if lines.len() >= max_lines || bytes + len > max_bytes {
107 truncated = true;
108 break;
109 }
110 bytes += len;
111 lines.push(text);
112 }
113 let end = start.saturating_add(u64::try_from(lines.len()).unwrap_or(u64::MAX));
114 let cursor = if end >= total && total > 0 { appended.saturating_sub(1) } else { first + end };
115 ReadResult {
116 text: lines.join("\n"),
117 cursor,
118 cols: u16::try_from(grid.columns()).unwrap_or(u16::MAX),
119 rows: u16::try_from(grid.screen_lines()).unwrap_or(u16::MAX),
120 truncated,
121 }
122}
123
124struct GridSize {
126 cols: usize,
127 rows: usize,
128}
129
130impl Dimensions for GridSize {
131 fn total_lines(&self) -> usize {
132 self.rows
133 }
134 fn screen_lines(&self) -> usize {
135 self.rows
136 }
137 fn columns(&self) -> usize {
138 self.cols
139 }
140}
141
142pub(crate) fn resize<T: EventListener>(
149 term: &mut Term<T>,
150 cols: usize,
151 rows: usize,
152 appended: &mut u64,
153) {
154 let before = term.grid().total_lines();
155 term.resize(GridSize { cols, rows });
156 let after = term.grid().total_lines();
157 if after >= before {
158 *appended = appended.saturating_add(u64::try_from(after - before).unwrap_or(u64::MAX));
159 } else {
160 *appended = appended.saturating_sub(u64::try_from(before - after).unwrap_or(u64::MAX));
161 }
162}
163
164fn line_text(grid: &Grid<Cell>, line: Line) -> String {
166 let row = &grid[line];
167 let mut text = String::with_capacity(row.len());
168 for cell in row {
169 if cell.flags.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER) {
170 continue;
171 }
172 text.push(cell.c);
173 }
174 let trimmed = text.trim_end();
175 trimmed.to_owned()
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use alacritty_terminal::event::VoidListener;
182 use alacritty_terminal::term::Config;
183 use alacritty_terminal::vte::ansi::Processor;
184
185 struct Size {
186 cols: usize,
187 rows: usize,
188 }
189
190 impl alacritty_terminal::grid::Dimensions for Size {
191 fn total_lines(&self) -> usize {
192 self.rows
193 }
194 fn screen_lines(&self) -> usize {
195 self.rows
196 }
197 fn columns(&self) -> usize {
198 self.cols
199 }
200 }
201
202 fn term_with(bytes: &[u8], cols: usize, rows: usize) -> Term<VoidListener> {
203 driven(bytes, cols, rows, 100).0
204 }
205
206 fn driven(bytes: &[u8], cols: usize, rows: usize, history: usize) -> (Term<VoidListener>, u64) {
209 let mut term = Term::new(
210 Config { scrolling_history: history, ..Default::default() },
211 &Size { cols, rows },
212 VoidListener,
213 );
214 let mut appended = u64::try_from(term.grid().total_lines()).unwrap_or(0);
215 let mut parser: Processor = Processor::new();
216 crate::parser::advance(&mut parser, &mut term, bytes, history, &mut appended);
217 (term, appended)
218 }
219
220 fn feed(
221 term: &mut Term<VoidListener>,
222 parser: &mut Processor,
223 bytes: &[u8],
224 history: usize,
225 appended: &mut u64,
226 ) {
227 crate::parser::advance(parser, term, bytes, history, appended);
228 }
229
230 #[test]
233 fn the_screen_is_the_text_a_person_sees() {
234 let term = term_with(b"hello\r\nworld", 20, 5);
235 let screen = extract(&term);
236 assert_eq!(screen.text.lines().next(), Some("hello"));
237 assert_eq!(screen.text.lines().nth(1), Some("world"));
238 assert_eq!(screen.cursor, Cursor { col: 5, row: 1 });
239 assert_eq!((screen.cols, screen.rows), (20, 5));
240 }
241
242 #[test]
245 fn a_read_from_a_cursor_returns_the_new_lines() {
246 let (term, appended) = driven(b"one\r\ntwo\r\nthree", 20, 5, 100);
247 let all = read(&term, appended, None, 200, 32 * 1024);
248 assert!(all.text.contains("one"));
249 assert!(all.text.contains("three"));
250 let after = read(&term, appended, Some(all.cursor), 200, 32 * 1024);
251 assert!(after.text.is_empty(), "nothing new after the cursor: {:?}", after.text);
252 assert!(!after.truncated, "caught up is not truncated");
253 }
254
255 #[test]
258 fn a_read_is_bounded_and_says_when_it_truncated() {
259 let (term, appended) = driven(b"a\r\nb\r\nc\r\nd\r\ne", 20, 5, 100);
260 let bounded = read(&term, appended, None, 2, 32 * 1024);
261 assert!(bounded.truncated, "two lines of five is truncated");
262 assert_eq!(bounded.text.lines().count(), 2);
263 }
264
265 #[test]
268 fn a_bounded_read_leaves_the_cursor_at_the_last_line_it_returned() {
269 let (term, appended) = driven(b"a\r\nb\r\nc\r\nd\r\ne", 20, 5, 100);
270 let first = read(&term, appended, Some(0), 2, 32 * 1024);
271 assert_eq!(first.text.lines().collect::<Vec<_>>(), ["a", "b"]);
272 assert!(first.truncated, "the bound cut the read");
273 let next = read(&term, appended, Some(first.cursor), 10, 32 * 1024);
274 assert_eq!(next.text.lines().collect::<Vec<_>>(), ["c", "d", "e"]);
275 assert!(!next.truncated, "the second read reached the end");
276 }
277
278 #[test]
282 fn a_read_cursor_advances_after_the_scrollback_is_full() {
283 let mut term = Term::new(
284 Config { scrolling_history: 2, ..Default::default() },
285 &Size { cols: 20, rows: 2 },
286 VoidListener,
287 );
288 let mut appended = u64::try_from(term.grid().total_lines()).unwrap_or(0);
289 let mut parser: Processor = Processor::new();
290 feed(&mut term, &mut parser, b"1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n", 2, &mut appended);
291 let caught = read(&term, appended, None, 10, 32 * 1024);
292 assert_eq!(caught.text.lines().collect::<Vec<_>>(), ["6", "7", "8"]);
293 let bound = caught.cursor;
294 feed(&mut term, &mut parser, b"9\r\n10\r\n", 2, &mut appended);
295 let next = read(&term, appended, Some(bound), 10, 32 * 1024);
296 assert_eq!(next.text.lines().collect::<Vec<_>>(), ["9", "10"]);
297 assert!(!next.truncated, "nothing fell out of the buffer behind the cursor");
298 }
299
300 #[test]
304 fn a_wrapped_line_reflows_when_the_terminal_narrows() {
305 let mut term = Term::new(
306 Config { scrolling_history: 100, ..Default::default() },
307 &Size { cols: 20, rows: 12 },
308 VoidListener,
309 );
310 let mut appended = 12u64;
311 let mut parser: Processor = Processor::new();
312 feed(
313 &mut term,
314 &mut parser,
315 b"\x1b[32mok\x1b[0m aaaaaaaaaaaaaaaaaaaaaaaa\r\n\x1b[1mbold\x1b[0m done",
316 100,
317 &mut appended,
318 );
319 let wide = read(&term, 10_000, None, 500, 1 << 20);
320 let wide_flat: String = wide.text.chars().filter(|c| !c.is_whitespace()).collect();
321 assert!(wide_flat.contains("okaaaaaaaaaaaaaaaaaaaaaaaa"), "{:?}", wide.text);
322 term.resize(Size { cols: 10, rows: 12 });
323 let narrow = read(&term, 10_000, None, 500, 1 << 20);
326 let flat: String = narrow.text.chars().filter(|c| !c.is_whitespace()).collect();
327 assert!(
328 flat.contains("okaaaaaaaaaaaaaaaaaaaaaaaa"),
329 "the wrapped run survives the reflow: {:?}",
330 narrow.text
331 );
332 assert!(flat.contains("bolddone"), "{:?}", narrow.text);
333 }
334
335 #[test]
340 fn a_reflow_keeps_the_read_windows_first_line() {
341 let mut term = Term::new(
342 Config { scrolling_history: 100, ..Default::default() },
343 &Size { cols: 10, rows: 4 },
344 VoidListener,
345 );
346 let mut appended = u64::try_from(term.grid().total_lines()).unwrap_or(0);
347 let mut parser: Processor = Processor::new();
348 feed(
349 &mut term,
350 &mut parser,
351 b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n",
352 100,
353 &mut appended,
354 );
355 let first = appended.saturating_sub(u64::try_from(term.grid().total_lines()).unwrap_or(0));
356 resize(&mut term, 40, 4, &mut appended);
359 let after = appended.saturating_sub(u64::try_from(term.grid().total_lines()).unwrap_or(0));
360 assert_eq!(after, first, "the reflow moved the buffer's first line under the cursor");
361 }
362
363 #[test]
366 fn a_wide_character_is_extracted_once() {
367 let term = term_with("日本".as_bytes(), 20, 5);
368 let screen = extract(&term);
369 assert_eq!(screen.text.lines().next(), Some("日本"));
370 }
371}