justerm_core/term/logical.rs
1//! Viewport logical lines: the soft-wrap-joined text of everything touching the
2//! viewport, plus a per-char map back to the cell it came from.
3//!
4//! The returned shape and *why* this is core rather than consumer are stated in
5//! [`crate::logical`]. Read it there; this module is the `Term` half — the cell-aware
6//! assembly, which needs the whole buffer and so cannot live in a frame-mode consumer
7//! (ADR-0017).
8//!
9//! Two things are local to this site. It is the **first of the three alt-screen floor
10//! misses** (#113): the up-walk into scrollback stops at `abs_floor()`, because on the alt
11//! screen that scrollback belongs to the *primary* buffer — see
12//! `docs/map/invariant/alt-screen-buffer-floor.md`, where this is site one of the discovery
13//! history. And the walk deliberately reaches **past the viewport in both directions**, so a
14//! line wrapping in from above the top or out past the bottom still joins whole; the
15//! off-screen rows surface as an out-of-range `row` in `LogicalLine::cells` for the consumer
16//! to clip.
17//!
18//! Nothing here is `pub(super)`. The one entry point is public API, and every helper it
19//! walks with was already in `walk.rs` before this module existed — which is what made this
20//! the cheapest of #584's five slices rather than a measure of its importance.
21
22use crate::logical::LogicalLine;
23
24use super::Term;
25
26impl Term {
27 /// The viewport's logical lines (#113/ADR-0017): each line's text plus a
28 /// per-char map to its viewport `(row, col)`. Wide-char spacers are skipped
29 /// and trailing blanks trimmed (so the text is 1:1 with `cells`). Empty rows
30 /// are dropped. The cell-aware assembly the consumer can't do in frame mode.
31 pub fn viewport_logical_lines(&self) -> Vec<LogicalLine> {
32 let rows = self.grid.rows();
33 let total = self.scrollback.len() + rows;
34 let top = self.scrollback.len() - self.display_offset; // abs line of viewport row 0
35 let bottom = top + rows; // abs lines [top, bottom) are on screen
36
37 // If viewport row 0 is a wrap-continuation, walk up into scrollback to
38 // the logical line's true start so an edge-spanning URL still matches —
39 // floored, because on alt that scrollback is the *primary* buffer's.
40 let floor = self.abs_floor();
41 let mut start = top;
42 while start > floor && self.abs_row(start - 1).is_wrapped() {
43 start -= 1;
44 }
45
46 let mut out = Vec::new();
47 let mut line = start;
48 while line < bottom {
49 // Accumulate one logical line forward while each row soft-wraps; the
50 // tail may run past `bottom` (off-screen below) — included too.
51 let mut text = String::new();
52 let mut map: Vec<(i32, usize)> = Vec::new();
53 let mut cur = line;
54 loop {
55 let cells = self.abs_line(cur);
56 for (col, cell) in cells.iter().enumerate() {
57 if cell.is_spacer() {
58 continue;
59 }
60 // Signed viewport row: < 0 above the top, >= rows below.
61 let vrow = cur as i32 - top as i32;
62 text.push(cell.c());
63 map.push((vrow, col));
64 // Combining marks (#45) ride the same cell — append each and
65 // map it to that cell so `text` stays 1:1 with `cells`.
66 if let Some(marks) = self.combining_at(cur, col) {
67 for &m in marks {
68 text.push(m);
69 map.push((vrow, col));
70 }
71 }
72 }
73 let soft = self.abs_row(cur).is_wrapped();
74 if soft && cur + 1 < total {
75 cur += 1;
76 } else {
77 break;
78 }
79 }
80 // Trim trailing blanks (only the last row can have them), keeping
81 // `text` and `cells` in lockstep.
82 let trimmed = text.trim_end();
83 map.truncate(trimmed.chars().count());
84 text.truncate(trimmed.len());
85 if !text.is_empty() {
86 out.push(LogicalLine { text, cells: map });
87 }
88 line = cur + 1;
89 }
90 out
91 }
92}