Skip to main content

max / shop

48.2 KB · 1212 lines History Blame Raw
1 //! Terminal grid + cursor + alt-screen for shop.
2 //!
3 //! Alt-screen support is here because vim without it makes a mess of
4 //! scrollback on every `:q`.
5 //!
6 //! Implements [`vte::Perform`], so the binary can pipe PTY bytes through a
7 //! `vte::Parser` straight into the grid.
8
9 mod cell;
10 mod edit;
11 mod history;
12 mod mouse;
13 pub mod oracle;
14 mod perform;
15 mod ring;
16 mod selection;
17 mod sgr;
18 mod text;
19
20 #[cfg(test)]
21 mod testutil;
22
23 pub use cell::{Attrs, Cell, Color};
24 pub use mouse::{MouseAction, MouseButton, MouseEncoding, MouseMods, MouseReport, MouseTracking};
25 pub use selection::{Point, Selection, SelectionMode, SelectionSpan};
26
27 use cell::{MAX_MARKS, MarkTable, char_cols, encode_attrs, encode_color};
28 use std::collections::VecDeque;
29
30 /// Shape hint from DECSCUSR (`CSI Ps SP q`). Blink flag is ignored — MVP
31 /// renders all as steady.
32 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
33 pub enum CursorShape {
34 Block,
35 Underline,
36 Bar,
37 }
38
39 /// State delta since the last [`Grid::take_damage`] — the renderer's cue for
40 /// which per-row instance caches to rotate, rebuild, or invalidate.
41 #[derive(Debug, Clone, Default)]
42 pub struct Damage {
43 /// Rows the full screen scrolled up (positive) or down (negative). Only
44 /// nonzero when the scroll region covered the entire screen — the
45 /// renderer can rotate its per-row cache by this amount and skip
46 /// re-emitting the shifted rows. Partial-region scrolls mark all affected
47 /// rows dirty instead.
48 pub scroll: i16,
49 /// Rows whose cell contents changed. Includes the blank rows exposed by
50 /// a scroll (i.e. after scroll-up-by-N the bottom N rows are dirty).
51 pub dirty_rows: Vec<u16>,
52 /// Alt-screen state toggled. The renderer should throw away its whole
53 /// per-row cache and rebuild from `dirty_rows`.
54 pub screen_swapped: bool,
55 /// Grid was resized. Same effect as `screen_swapped` on the renderer.
56 pub resized: bool,
57 /// The viewport moved into or within scrollback, or content arrived under
58 /// it. Same effect as `screen_swapped` on the renderer: the cache describes
59 /// rows that are no longer the rows on screen. `dirty_rows` carries the
60 /// whole screen when this is set, and `scroll` is zero — a cache rotation
61 /// would be wrong, since the visible rows did not shift by a knowable
62 /// amount.
63 pub view_moved: bool,
64 }
65
66 /// What the terminal answers about itself.
67 ///
68 /// Every field here is something the grid cannot work out and a program can
69 /// ask for: the renderer owns cell geometry, the theme owns the default
70 /// colours, and the binary owns its own name. The grid holds them only so the
71 /// query arms have something true to say, and the binary keeps them current
72 /// through [`Grid::set_identity`].
73 ///
74 /// The defaults are shop's own, so a grid nobody configured still answers
75 /// plausibly rather than answering zero.
76 #[derive(Clone, Debug, PartialEq, Eq)]
77 pub struct Identity {
78 /// Reported by XTVERSION, as `name(version)`.
79 pub name: String,
80 pub version: String,
81 /// One cell in physical pixels, width then height. Follows the output
82 /// scale, so it changes when the window moves between displays.
83 pub cell_px: (u16, u16),
84 /// Default foreground and background, sRGB, as OSC 10 and 11 report them.
85 pub fg: [u8; 3],
86 pub bg: [u8; 3],
87 }
88
89 impl Default for Identity {
90 /// A placeholder, and only ever that.
91 ///
92 /// `shop` overwrites all of it at startup and again on every resize, so
93 /// nothing a user sees comes from here. `cell_px` is deliberately not the
94 /// cell of any face, so a reply carrying it is obviously unconfigured
95 /// rather than plausibly stale. The real one is measured off the bundled
96 /// face by `shop_render::CellMetrics`.
97 fn default() -> Self {
98 Self {
99 name: "shop".into(),
100 version: "0".into(),
101 cell_px: (0, 0),
102 fg: [0xe6, 0xde, 0xd3],
103 bg: [0x25, 0x23, 0x1f],
104 }
105 }
106 }
107
108 /// Cursor state (position + deferred-wrap flag).
109 #[derive(Copy, Clone, Debug, Default)]
110 pub struct Cursor {
111 pub row: u16,
112 pub col: u16,
113 pub visible: bool,
114 /// Deferred wrap: after writing to the rightmost column, the next
115 /// printable char wraps to the next line. This mirrors DEC/xterm
116 /// behavior and matters for vim's line drawing.
117 pub wrap_next: bool,
118 }
119
120 /// One row that has scrolled off the top of the main screen.
121 ///
122 /// Always exactly the grid's current width: `resize` rewraps the whole buffer
123 /// to the new one — see [`Grid::history`].
124 #[derive(Clone, Debug)]
125 struct HistoryRow {
126 cells: Vec<Cell>,
127 /// Whether it ran off the right edge and continued on the row below, so a
128 /// copy spanning the two joins them without a newline.
129 ///
130 /// Also the wrap-point record [`Grid::rewrap_history`] reads: a maximal run
131 /// of these plus the row that ends it is one logical line.
132 wrapped: bool,
133 }
134
135 /// Rows of scrollback a grid keeps unless told otherwise.
136 ///
137 /// Ten thousand is the common default and costs `lines * cols * 12` bytes —
138 /// about 24 MB at 200 columns. [`Grid::set_history_limit`] is what a config
139 /// key drives.
140 pub const DEFAULT_HISTORY_LIMIT: usize = 10_000;
141
142 pub struct Grid {
143 cols: u16,
144 rows: u16,
145 // Row-major cells with a ring-buffer row layout: logical row `r` lives at
146 // physical row `(origin + r) mod rows`. Fullscreen scrolls advance the
147 // origin instead of memcpy'ing rows — a 2.4 GB/s cost on the vtebench
148 // scrolling workload with the naive layout, near-free with the ring.
149 // Partial-region scrolls fall back to memcpy through ring-mapped indices.
150 main: Vec<Cell>,
151 alt: Vec<Cell>,
152 // One flag per PHYSICAL row: "this row ran off the right edge and
153 // continues on the next one". Physical indexing is what makes the ring
154 // scroll carry the flags for free — an origin bump moves rows and their
155 // wrap state together, with no second pass.
156 //
157 // Only the deferred wrap in `place_char` sets a flag. A row that happens
158 // to fill exactly and is then ended with CR/LF is not wrapped, which is
159 // the distinction that decides whether copied text gets a newline here.
160 main_wrapped: Vec<bool>,
161 alt_wrapped: Vec<bool>,
162 /// Rows that have scrolled off the top of the main screen, oldest first.
163 ///
164 /// Main only: the alt screen is a fixed canvas an application repaints, so
165 /// a row leaving its top is overdraw rather than history, and every
166 /// terminal that keeps scrollback keeps none for it.
167 ///
168 /// Every row here is exactly `cols` wide. `resize` rewraps the whole buffer
169 /// to the new width and re-materializes it at that width, so the invariant
170 /// every reader depends on — `row()` yields `cols` cells — holds for history
171 /// rows as much as for live ones.
172 history: VecDeque<HistoryRow>,
173 /// How many rows `history` keeps before dropping its oldest.
174 history_limit: usize,
175 /// The combining marks that cells refer to by id.
176 ///
177 /// Grid-wide rather than per-screen, so a cell can be copied between the
178 /// live screen, the alt screen and history without its marks needing to
179 /// travel or be rewritten. That is the point of storing an id: a cell stays
180 /// a value, and every structural move of cells in this file stays a memcpy.
181 marks: MarkTable,
182 /// How far back the viewport sits, in rows. Zero is live. Never exceeds
183 /// `history.len()`, and forced to zero on the alt screen.
184 view_offset: u16,
185 /// The viewport moved, or moved under content, since the last damage
186 /// drain. Every cached row is suspect, so the renderer rebuilds.
187 view_dirty: bool,
188 on_alt: bool,
189 main_origin: u16,
190 alt_origin: u16,
191 /// Rotation within the current scroll region. Independent from
192 /// main/alt_origin. Non-zero only when the active screen is in a
193 /// non-fullscreen region (DECSTBM); enforced by unroll on transition.
194 region_origin: u16,
195 cursor: Cursor,
196 cursor_shape: CursorShape,
197 /// Where each screen's cursor was when the other took over, indexed by
198 /// `on_alt`. The alt-screen swap's own bookkeeping, and nothing else's.
199 swap_saved: [Cursor; 2],
200 /// DECSC's slot, one per screen, indexed by `on_alt`.
201 ///
202 /// Separate from `swap_saved` because they answer to different owners and
203 /// sharing one slot loses saves. They shared one until this was written:
204 /// `ESC 7` on the alt screen wrote the slot that leaving alt restored
205 /// from, so a full-screen program that saved its cursor moved the shell's
206 /// on the way out. Per-screen because that is what xterm does, and an
207 /// `ESC 7` in vim has nothing to say about where the shell was.
208 dec_saved: [Cursor; 2],
209 scroll_top: u16, // 0-indexed, inclusive
210 scroll_bottom: u16, // 0-indexed, inclusive
211 pending_fg: Color,
212 pending_bg: Color,
213 pending_attrs: Attrs,
214 // Pre-encoded pending style, ready to store into a Cell's words. Kept in
215 // sync with pending_{fg,bg,attrs} by [`Grid::recompute_style_words`],
216 // called from apply_sgr. Lets the hot `place_char` skip encode_color +
217 // encode_attrs on every glyph.
218 pending_fg_word: u32,
219 pending_bg_word: u32,
220 // Cached byte offset of the current cursor row's start in the active
221 // buffer, or `u32::MAX` when invalid. Fast `place_char` populates it on
222 // first use of a print run; any non-print Perform entry point (execute /
223 // csi / esc / osc), newline, screen-swap, DECSTBM, or resize invalidates.
224 cur_row_start: u32,
225 // DECSET 2026: true between `\e[?2026h` and `\e[?2026l`. Purely
226 // reflected via [`Grid::sync_update`] — the binary decides whether/how
227 // long to defer redraws (typical timeout is ~150ms).
228 sync_update: bool,
229 // DECSET 2004: the program has asked to be told that text arrived by
230 // paste rather than by typing, so it can decline to act on it. Reflected
231 // via [`Grid::bracketed_paste`]; wrapping the payload is the binary's job.
232 bracketed_paste: bool,
233 // DECCKM (`CSI ? 1 h`) and DECKPAM (`ESC =`). Both change what the
234 // KEYBOARD sends, not what the screen shows, so the grid only records
235 // them — shop-xkb is what reads them.
236 cursor_keys_application: bool,
237 keypad_application: bool,
238 // DECSET 1007: alternate scroll. On the alt screen there is no history to
239 // move through, so the wheel is translated into cursor keys instead —
240 // which is what makes `less` and `man` scroll. On by default, and a
241 // program that wants the wheel to mean something else clears it. Same
242 // division as 2004: the grid tracks the mode, the binary acts on it.
243 alternate_scroll: bool,
244 // DECSET 9/1000/1002/1003 and 1006: how much of the mouse the program
245 // wants, and in which encoding it wants it. Same division again — the grid
246 // records what was asked for and the binary, which is the only half that
247 // sees a pointer, does the sending.
248 mouse_tracking: MouseTracking,
249 mouse_encoding: MouseEncoding,
250 pending_title: Option<String>,
251 identity: Identity,
252 // Bytes the terminal owes the program, from queries it answered. The grid
253 // has no handle on the PTY, so it queues and the binary drains after every
254 // parse, the way it already does for the title.
255 //
256 // A query with no answer is not a no-op: the asking program waits out its
257 // timeout first. yazi gives DA1 three seconds before deciding the terminal
258 // cannot draw, so silence here costs three seconds on every launch of it.
259 pending_replies: Vec<u8>,
260 // Damage tracking — accumulated between take_damage() calls.
261 row_dirty: Vec<bool>,
262 pending_scroll: i16,
263 pending_screen_swap: bool,
264 pending_resize: bool,
265 }
266
267 const CUR_ROW_INVALID: u32 = u32::MAX;
268
269 impl Grid {
270 pub fn new(cols: u16, rows: u16) -> Self {
271 let cols = cols.max(1);
272 let rows = rows.max(1);
273 let cell_count = cols as usize * rows as usize;
274 Self {
275 cols,
276 rows,
277 main: vec![Cell::default(); cell_count],
278 alt: vec![Cell::default(); cell_count],
279 main_wrapped: vec![false; rows as usize],
280 alt_wrapped: vec![false; rows as usize],
281 history: VecDeque::new(),
282 history_limit: DEFAULT_HISTORY_LIMIT,
283 marks: MarkTable::default(),
284 view_offset: 0,
285 view_dirty: false,
286 on_alt: false,
287 main_origin: 0,
288 alt_origin: 0,
289 region_origin: 0,
290 cursor: Cursor {
291 visible: true,
292 ..Cursor::default()
293 },
294 cursor_shape: CursorShape::Block,
295 swap_saved: [Cursor::default(); 2],
296 dec_saved: [Cursor::default(); 2],
297 scroll_top: 0,
298 scroll_bottom: rows - 1,
299 pending_fg: Color::Default,
300 pending_bg: Color::Default,
301 pending_attrs: Attrs::default(),
302 pending_fg_word: 0,
303 pending_bg_word: 0,
304 cur_row_start: CUR_ROW_INVALID,
305 sync_update: false,
306 bracketed_paste: false,
307 cursor_keys_application: false,
308 keypad_application: false,
309 alternate_scroll: true,
310 mouse_tracking: MouseTracking::Off,
311 mouse_encoding: MouseEncoding::X10,
312 pending_title: None,
313 identity: Identity::default(),
314 pending_replies: Vec::new(),
315 // Initial state: everything dirty so first render populates the
316 // per-row cache.
317 row_dirty: vec![true; rows as usize],
318 pending_scroll: 0,
319 pending_screen_swap: false,
320 pending_resize: true,
321 }
322 }
323
324 /// Drain accumulated changes since the last call. The renderer applies
325 /// them (rotate cache, rebuild dirty rows, wipe on swap/resize) before
326 /// emitting the frame.
327 pub fn take_damage(&mut self) -> Damage {
328 let scroll = std::mem::take(&mut self.pending_scroll);
329 let screen_swapped = std::mem::take(&mut self.pending_screen_swap);
330 let resized = std::mem::take(&mut self.pending_resize);
331 let view_dirty = std::mem::take(&mut self.view_dirty);
332 let mut dirty_rows: Vec<u16> = Vec::new();
333 for (i, d) in self.row_dirty.iter_mut().enumerate() {
334 if *d {
335 dirty_rows.push(i as u16);
336 *d = false;
337 }
338 }
339 // A viewport back in history breaks every assumption the incremental
340 // path makes: `scroll` describes the live screen moving, and a dirty
341 // logical row is not the visible row it would be at offset zero. So
342 // once the user is reading history, any change at all rebuilds the
343 // screen. That is affordable precisely because it is not the hot path
344 // — the throughput case is a viewport pinned to the bottom.
345 let stale = view_dirty
346 || (self.view_offset > 0 && (scroll != 0 || !dirty_rows.is_empty() || screen_swapped));
347 if stale && !resized {
348 return Damage {
349 scroll: 0,
350 dirty_rows: (0..self.rows).collect(),
351 screen_swapped,
352 resized,
353 view_moved: true,
354 };
355 }
356 Damage {
357 scroll,
358 dirty_rows,
359 screen_swapped,
360 resized,
361 view_moved: false,
362 }
363 }
364
365 fn mark_row_dirty(&mut self, row: u16) {
366 if let Some(slot) = self.row_dirty.get_mut(row as usize) {
367 *slot = true;
368 }
369 }
370
371 fn mark_all_rows_dirty(&mut self) {
372 for d in &mut self.row_dirty {
373 *d = true;
374 }
375 }
376
377 /// Report every row as damaged, for a renderer that lost its own cache.
378 ///
379 /// Damage is a diff, and a diff only works while both ends agree on what
380 /// the other has. The renderer keeps a per-row glyph cache and rebuilds
381 /// only the rows damage names, so anything that throws that cache away —
382 /// rebuilding the renderer on a scale change is the live case — leaves the
383 /// two ends disagreeing: the grid has nothing new to report, the renderer
384 /// has nothing at all, and the window stays empty until the program on the
385 /// pty happens to print. A shell prints on the next keystroke and hides
386 /// this; a command that writes once and waits does not.
387 ///
388 /// So this is not a redraw request but the renderer saying it forgot, and
389 /// the grid answering with the whole screen.
390 pub fn invalidate_render(&mut self) {
391 self.mark_all_rows_dirty();
392 self.view_dirty = true;
393 }
394
395 pub fn cursor_shape(&self) -> CursorShape {
396 self.cursor_shape
397 }
398
399 /// True while a DECSET 2026 synchronized-update batch is open. The
400 /// renderer should hold off on presenting a frame until this clears
401 /// (matching `\e[?2026l`) or its own safety timeout expires.
402 pub fn sync_update(&self) -> bool {
403 self.sync_update
404 }
405
406 /// True while the program has DECSET 2004 on and wants pasted text
407 /// wrapped in `\e[200~` / `\e[201~`.
408 ///
409 /// A shell that knows the difference will not run a pasted command until
410 /// the user presses Enter, which is the whole point of the mode: pasting
411 /// something with a newline in it stops being an accidental execution.
412 pub fn bracketed_paste(&self) -> bool {
413 self.bracketed_paste
414 }
415
416 /// DECCKM: cursor keys send `SS3 A` rather than `CSI A`.
417 ///
418 /// vim and readline both set it, and it is the difference between an
419 /// arrow key moving the cursor and typing `[A` into the buffer.
420 pub fn cursor_keys_application(&self) -> bool {
421 self.cursor_keys_application
422 }
423
424 /// DECKPAM: the keypad sends function sequences rather than digits.
425 pub fn keypad_application(&self) -> bool {
426 self.keypad_application
427 }
428
429 /// DECSET 1007: the wheel may be translated into cursor keys on the alt
430 /// screen. On unless a program clears it, and only consulted there — off
431 /// the alt screen the wheel has real history to move through.
432 pub fn alternate_scroll(&self) -> bool {
433 self.alternate_scroll
434 }
435
436 /// Whether the alt screen is the active one.
437 ///
438 /// The alt screen keeps no history, which is why anything deciding what a
439 /// scroll means has to ask.
440 pub fn on_alt(&self) -> bool {
441 self.on_alt
442 }
443
444 /// Consume any window title set by the shell via OSC 0/2 since the last
445 /// call. Binary polls after each `parser.advance` and forwards to
446 /// `xdg_window.set_title`.
447 pub fn take_pending_title(&mut self) -> Option<String> {
448 self.pending_title.take()
449 }
450
451 /// Consume any bytes owed to the program in answer to its queries. Binary
452 /// polls after each `parser.advance` and writes them to the PTY.
453 ///
454 /// Empty on almost every call: programs ask once, at startup.
455 pub fn take_pending_replies(&mut self) -> Vec<u8> {
456 std::mem::take(&mut self.pending_replies)
457 }
458
459 fn reply(&mut self, bytes: &[u8]) {
460 self.pending_replies.extend_from_slice(bytes);
461 }
462
463 /// Tell the grid what to say about itself. Call at startup and whenever
464 /// any of it moves: cell size follows the output scale, and the colours
465 /// follow the theme.
466 pub fn set_identity(&mut self, identity: Identity) {
467 self.identity = identity;
468 }
469
470 pub fn cols(&self) -> u16 {
471 self.cols
472 }
473
474 pub fn rows(&self) -> u16 {
475 self.rows
476 }
477
478 pub fn cursor(&self) -> Cursor {
479 self.cursor
480 }
481
482 /// Where the cursor sits on screen, or `None` when the viewport is far
483 /// enough back that it has scrolled off the bottom.
484 ///
485 /// The cursor's own row is a live row. Drawing it at that row while the
486 /// user reads history would put a blinking block on unrelated text.
487 pub fn cursor_view_row(&self) -> Option<u16> {
488 let r = self.cursor.row.checked_add(self.view_offset)?;
489 (r < self.rows).then_some(r)
490 }
491
492 /// The cells of visible row `r`, always exactly `cols` of them.
493 ///
494 /// While the viewport sits back in history the top `view_offset` rows come
495 /// from [`Grid::history`] and the rest from the live screen, so every
496 /// reader — the renderer, selection, word boundaries — sees one flat
497 /// screen and needs to know nothing about where it came from.
498 pub fn row(&self, r: u16) -> &[Cell] {
499 if let Some(h) = self.history_row(r) {
500 return &h.cells;
501 }
502 let start = self.row_start(self.live_row(r));
503 let end = start + self.cols as usize;
504 &self.active_cells()[start..end]
505 }
506
507 /// Append a cell's text — base character then any combining marks — to
508 /// `out`.
509 ///
510 /// The seam between "what a cell looks like" and "what a cell is". The
511 /// renderer and word boundaries want the base and use [`Cell::c`]; anything
512 /// producing text a human or another program will read wants the cluster
513 /// and comes here, because an accent dropped on the way to the clipboard is
514 /// a path that no longer names the file it came from.
515 pub(crate) fn push_cell_text(&self, cell: &Cell, out: &mut String) {
516 out.push(match cell.c() {
517 '\0' => ' ',
518 c => c,
519 });
520 out.extend(self.marks.get(cell.marks_id()));
521 }
522
523 /// A cell's combining marks, in the order they arrived, empty for almost
524 /// every cell.
525 ///
526 /// The renderer needs these to draw a cluster; nothing else outside this
527 /// crate does, because [`Cell::c`] already answers what the cell is and how
528 /// wide it is.
529 pub fn marks(&self, cell: &Cell) -> &[char] {
530 self.marks.get(cell.marks_id())
531 }
532
533 /// The column a pointer at `col` was aiming at.
534 ///
535 /// Half a wide character is not a thing anyone can mean to click on, so a
536 /// click on the second column of one reads as a click on the character.
537 pub fn snap_col(&self, row: u16, col: u16) -> u16 {
538 let cells = self.row(row);
539 match cells.get(col as usize) {
540 Some(c) if c.is_spacer() && col > 0 => col - 1,
541 _ => col,
542 }
543 }
544
545 /// How many columns the cursor covers, so a block cursor over a wide
546 /// character is drawn the width of the character rather than half of it.
547 pub fn cursor_cols(&self) -> u16 {
548 self.cursor_view_row()
549 .and_then(|r| self.row(r).get(self.cursor.col as usize).map(Cell::cols))
550 .unwrap_or(1)
551 }
552
553 #[inline]
554 fn invalidate_cur_row(&mut self) {
555 self.cur_row_start = CUR_ROW_INVALID;
556 }
557
558 #[inline]
559 fn recompute_style_words(&mut self) {
560 self.pending_fg_word = encode_color(self.pending_fg) | encode_attrs(self.pending_attrs);
561 self.pending_bg_word = encode_color(self.pending_bg);
562 }
563
564 fn place_char(&mut self, c: char) {
565 // A wide character needs two columns, so on a one-column grid there is
566 // no such thing and the pair never forms.
567 let width = if self.cols < 2 { 1 } else { char_cols(c) };
568 if width == 0 {
569 // A combining mark is not placed anywhere. It amends the character
570 // already on screen and leaves the cursor where it was, which is
571 // what the application counting its own columns did too.
572 self.amend_with_mark(c);
573 return;
574 }
575 // Deferred wrap: if the previous print landed on the rightmost cell,
576 // the next visible char starts a new line.
577 if self.cursor.wrap_next {
578 // Record the continuation before newline() moves us off the row
579 // (or scrolls the ring out from under it).
580 self.set_row_wrapped(self.cursor.row, true);
581 self.newline();
582 self.cursor.col = 0;
583 self.cursor.wrap_next = false;
584 } else if width == 2 && self.cursor.col + 1 >= self.cols {
585 // A wide character with one column left does not fit, and is not
586 // split to make it fit: the application counted two columns and has
587 // already moved to the next line. The column it could not use is
588 // left as a spacer, so it holds the width it is owed on screen and
589 // is skipped by anything reading the buffer back as text.
590 let pad = self.cursor.col;
591 self.set_row_wrapped(self.cursor.row, true);
592 self.write_pad(pad);
593 self.newline();
594 self.cursor.col = 0;
595 }
596 // Cache the row start on first print of a run; non-print Perform
597 // entry points and newline invalidate it back to CUR_ROW_INVALID.
598 let start = if self.cur_row_start == CUR_ROW_INVALID {
599 let s = self.row_start(self.cursor.row) as u32;
600 self.cur_row_start = s;
601 s as usize
602 } else {
603 self.cur_row_start as usize
604 };
605 let col = self.cursor.col as usize;
606 let row_idx = self.cursor.row as usize;
607 // Landing on either half of a wide pair destroys it, and the other half
608 // has to go with it or it is left claiming a width it no longer has.
609 // Off the hot path: the test is one mask against a cell already in
610 // cache, and it fails for every character in ordinary output.
611 self.heal_pair(start, col, width as usize);
612 let cells: &mut [Cell] = if self.on_alt {
613 &mut self.alt
614 } else {
615 &mut self.main
616 };
617 if width == 2 {
618 // Two stores, bounds-checked. `col + 1 < cols` holds because the
619 // no-room case above moved to the next row, and a wide character is
620 // rare enough that the hot path below is the one worth the unsafe.
621 let (lead, spacer) = Cell::wide_pair(c, self.pending_fg_word, self.pending_bg_word);
622 cells[start + col] = lead;
623 cells[start + col + 1] = spacer;
624 self.row_dirty[row_idx] = true;
625 } else {
626 // Bounds are enforced structurally: start is a valid row start (a
627 // multiple of cols within cells.len()), col < cols, row < rows.
628 debug_assert!(start + col < cells.len());
629 debug_assert!(row_idx < self.row_dirty.len());
630 // SAFETY: the debug_asserts above encode the invariants — start is
631 // computed from row_start() (multiple of cols, < cells.len()), col is
632 // clamped to cols on entry, and row_dirty has one entry per row.
633 // Bounds-check elimination matters here: this is the innermost store
634 // for every printed glyph, called at PTY-drain rate on cell-dense
635 // workloads (millions/sec on `cat` of a wide buffer).
636 #[allow(unsafe_code)]
637 unsafe {
638 let cell = cells.get_unchecked_mut(start + col);
639 cell.c_raw = c as u32;
640 cell.fg_word = self.pending_fg_word;
641 // Clears both width flags along with the colour, so a cell
642 // taken over by a narrow character stops being half a pair.
643 cell.bg_word = self.pending_bg_word;
644 *self.row_dirty.get_unchecked_mut(row_idx) = true;
645 }
646 }
647 // The cursor ends on the last column the character covered, so the
648 // deferred wrap fires off the same test whatever the width was.
649 let last = self.cursor.col + width - 1;
650 if last + 1 >= self.cols {
651 self.cursor.col = last;
652 self.cursor.wrap_next = true;
653 } else {
654 self.cursor.col = last + 1;
655 }
656 }
657
658 /// Blank the far half of any wide pair that a write of `width` columns at
659 /// `col` lands on, so no cell is left as half of a character.
660 fn heal_pair(&mut self, start: usize, col: usize, width: usize) {
661 let cols = self.cols as usize;
662 let cells = self.active_cells_mut();
663 // The pair the write starts inside: either this cell is a spacer whose
664 // lead sits behind it, or it is a lead whose spacer the write does not
665 // reach.
666 //
667 // The `is_wide` test on the cell behind is what makes the first case
668 // safe. Not every spacer has a lead: `write_pad` leaves one at the
669 // right edge for a wide character that did not fit, and without the
670 // test a narrow write over that pad blanks whatever sits two columns
671 // back — a real character, and if it is itself a wide lead the row is
672 // left holding exactly the half pair this function exists to prevent.
673 // Found by the soak oracle, 2026-08-29.
674 if cells[start + col].is_spacer() && col > 0 && cells[start + col - 1].is_wide() {
675 cells[start + col - 1] = Cell::default();
676 } else if width == 1 && cells[start + col].is_wide() && col + 1 < cols {
677 cells[start + col + 1] = Cell::default();
678 }
679 // A two-column write also covers the cell after it, which may be the
680 // lead of the next pair along.
681 if width == 2 && col + 1 < cols && cells[start + col + 1].is_wide() && col + 2 < cols {
682 cells[start + col + 2] = Cell::default();
683 }
684 }
685
686 /// Attach a combining mark to the character the cursor last passed over.
687 ///
688 /// The base is found from the cursor rather than remembered, so nothing has
689 /// to survive a scroll between the base arriving and its mark: it is the
690 /// cell the cursor is about to move past, stepped back once more when that
691 /// lands on a wide character's second column.
692 fn amend_with_mark(&mut self, mark: char) {
693 // With the deferred wrap pending, the cursor is still ON the last
694 // character it wrote rather than after it.
695 let Some(col) = (if self.cursor.wrap_next {
696 Some(self.cursor.col)
697 } else {
698 self.cursor.col.checked_sub(1)
699 }) else {
700 // A mark with nothing before it on this row. Malformed — there is
701 // no base for it to change, and inventing a cell for it would be
702 // the column error this whole thing exists to remove.
703 return;
704 };
705 let start = self.row_start(self.cursor.row);
706 let cells = self.active_cells();
707 let col = if cells[start + col as usize].is_spacer() && col > 0 {
708 col - 1
709 } else {
710 col
711 };
712 let base = cells[start + col as usize];
713 let mut seq: Vec<char> = self.marks.get(base.marks_id()).to_vec();
714 if seq.len() >= MAX_MARKS {
715 return;
716 }
717 seq.push(mark);
718 let Some(id) = self.marks.intern(&seq) else {
719 // The id space is spent. Dropping the mark leaves the cell holding
720 // what it held, which is the only outcome here that is not a lie.
721 return;
722 };
723 let cells = self.active_cells_mut();
724 cells[start + col as usize] = base.with_marks(id);
725 self.mark_row_dirty(self.cursor.row);
726 }
727
728 /// Leave column `col` of the cursor's row as a blank the width of one cell
729 /// that is not a character: the column a wide character could not fit into.
730 fn write_pad(&mut self, col: u16) {
731 let start = self.row_start(self.cursor.row);
732 let cells = self.active_cells_mut();
733 cells[start + col as usize] = Cell::pad();
734 self.mark_row_dirty(self.cursor.row);
735 }
736 }
737
738 #[cfg(test)]
739 mod tests {
740 use crate::testutil::{assert_cursor, feed, history_text, row_str};
741 use crate::*;
742
743 /// A pad at the right edge is a spacer with no lead, and a narrow write
744 /// over it must not reach back for one.
745 ///
746 /// The row here ends `..日日<pad>`: the last wide character did not fit in
747 /// the final column, so `write_pad` left a spacer there and the character
748 /// went to the next row. Writing a narrow character over that pad must not
749 /// blank the cell two columns back, which is the spacer of a real pair:
750 /// that leaves its lead on screen claiming a width it does not have.
751 #[test]
752 fn a_pad_at_the_right_edge_is_not_half_a_pair() {
753 let mut g = Grid::new(10, 4);
754 // The first wide character takes columns 7 and 8. The second has only
755 // column 9 left, so it goes to the next row and leaves a pad behind.
756 feed(&mut g, "\x1b[1;8H日日".as_bytes());
757 assert!(g.row(0)[7].is_wide());
758 assert!(g.row(0)[8].is_spacer());
759 assert!(
760 g.row(0)[9].is_spacer(),
761 "the column the wide character could not use"
762 );
763 assert!(!g.row(0)[9].is_wide());
764
765 feed(&mut g, b"\x1b[1;10Hx");
766 assert_eq!(g.row(0)[9].c(), 'x');
767 assert_eq!(
768 g.row(0)[7].c(),
769 '',
770 "the pair two columns back was blanked"
771 );
772 assert!(g.row(0)[7].is_wide());
773 assert!(g.row(0)[8].is_spacer(), "its spacer went with it");
774 }
775
776 // ---- basics --------------------------------------------------------
777
778 #[test]
779 fn new_grid_is_all_spaces() {
780 let g = Grid::new(10, 3);
781 for r in 0..3 {
782 assert_eq!(row_str(&g, r), "");
783 }
784 assert_cursor(&g, 0, 0);
785 }
786
787 #[test]
788 fn print_advances_cursor() {
789 let mut g = Grid::new(20, 3);
790 feed(&mut g, b"hello");
791 assert_eq!(row_str(&g, 0), "hello");
792 assert_cursor(&g, 0, 5);
793 }
794
795 #[test]
796 fn cr_lf_move_to_next_row_col_zero() {
797 let mut g = Grid::new(20, 3);
798 feed(&mut g, b"one\r\ntwo");
799 assert_eq!(row_str(&g, 0), "one");
800 assert_eq!(row_str(&g, 1), "two");
801 assert_cursor(&g, 1, 3);
802 }
803
804 #[test]
805 fn backspace_moves_cursor_back() {
806 let mut g = Grid::new(20, 3);
807 feed(&mut g, b"abc\x08");
808 assert_cursor(&g, 0, 2);
809 // BS is non-destructive — 'c' still there.
810 assert_eq!(row_str(&g, 0), "abc");
811 }
812
813 #[test]
814 fn tab_advances_to_next_multiple_of_eight() {
815 let mut g = Grid::new(40, 3);
816 feed(&mut g, b"ab\t");
817 assert_cursor(&g, 0, 8);
818 feed(&mut g, b"c");
819 assert_cursor(&g, 0, 9);
820 }
821
822 // ---- wrap ----------------------------------------------------------
823
824 #[test]
825 fn deferred_wrap_on_rightmost_cell() {
826 let mut g = Grid::new(4, 3);
827 feed(&mut g, b"ABCD");
828 // After 4 chars in a 4-wide grid, cursor is at col 3 with wrap_next.
829 assert_eq!(row_str(&g, 0), "ABCD");
830 let c = g.cursor();
831 assert!(c.wrap_next, "should have wrap_next set");
832 feed(&mut g, b"E");
833 // Next print wraps to row 1 col 0.
834 assert_eq!(row_str(&g, 1), "E");
835 }
836
837 // ---- CSI cursor motion --------------------------------------------
838
839 #[test]
840 fn cup_moves_cursor_one_indexed() {
841 let mut g = Grid::new(20, 5);
842 feed(&mut g, b"\x1b[3;5H");
843 assert_cursor(&g, 2, 4);
844 }
845
846 #[test]
847 fn cup_defaults_to_top_left() {
848 let mut g = Grid::new(20, 5);
849 feed(&mut g, b"aaa\r\nbbb\r\nccc\x1b[H");
850 assert_cursor(&g, 0, 0);
851 }
852
853 #[test]
854 fn cuu_cud_cuf_cub_clamp_at_edges() {
855 let mut g = Grid::new(20, 5);
856 feed(&mut g, b"\x1b[100A"); // way up — should clamp at 0
857 assert_cursor(&g, 0, 0);
858 feed(&mut g, b"\x1b[100B"); // way down — clamp at rows-1
859 assert_cursor(&g, 4, 0);
860 feed(&mut g, b"\x1b[100C"); // way right — clamp at cols-1
861 assert_cursor(&g, 4, 19);
862 feed(&mut g, b"\x1b[100D"); // way left — clamp at 0
863 assert_cursor(&g, 4, 0);
864 }
865
866 #[test]
867 fn cha_and_vpa_position_absolutely() {
868 let mut g = Grid::new(20, 5);
869 feed(&mut g, b"\x1b[10G\x1b[3d");
870 assert_cursor(&g, 2, 9);
871 }
872
873 // -- character width ---------------------------------------------------
874 //
875 // How many columns a character takes is an agreement with the application,
876 // not a rendering choice: a program lays its own output out by the same
877 // table and computes its cursor moves from it. These pin the grid to that
878 // table, because the failure they guard is not a smudged glyph — it is the
879 // grid and the application disagreeing about which column the cursor is in
880 // and every absolute move after it landing somewhere else.
881
882 #[test]
883 fn a_wide_character_takes_two_columns() {
884 let mut g = Grid::new(8, 2);
885 feed(&mut g, "日x".as_bytes());
886 let cells = g.row(0);
887 assert!(
888 cells[0].is_wide(),
889 "the lead does not claim its second column"
890 );
891 assert!(cells[1].is_spacer(), "the second column is not held");
892 assert_eq!(cells[2].c(), 'x', "the next character overlapped the pair");
893 assert_eq!(
894 g.cursor().col,
895 3,
896 "the cursor is not where the program thinks"
897 );
898 }
899
900 #[test]
901 fn a_wide_character_reads_back_as_one_character() {
902 // The spacer holds a column, not a character. Emitting it would put a
903 // space inside every CJK word that reached the clipboard.
904 let mut g = Grid::new(8, 2);
905 feed(&mut g, "日本語".as_bytes());
906 assert_eq!(g.text_range(0, 1), "日本語\n");
907 }
908
909 #[test]
910 fn a_wide_character_that_does_not_fit_moves_to_the_next_row_whole() {
911 // One column left and a two-column character: it goes to the next row
912 // rather than being split, and the line still reads as one line.
913 let mut g = Grid::new(5, 3);
914 feed(&mut g, "abcd日".as_bytes());
915 assert_eq!(row_str(&g, 0), "abcd", "the odd column was written into");
916 assert!(
917 g.row(1)[0].is_wide(),
918 "the character did not move down whole"
919 );
920 assert!(g.row_wrapped(0), "the line stopped continuing");
921 assert_eq!(
922 g.text_range(0, 2),
923 "abcd日\n",
924 "the column it could not use came out as a space"
925 );
926 }
927
928 #[test]
929 fn overwriting_half_a_wide_character_takes_the_other_half_with_it() {
930 // vim redrawing one column of a line it previously drew CJK into. The
931 // orphaned half would keep claiming a width it no longer has.
932 let mut g = Grid::new(6, 2);
933 feed(&mut g, "日本".as_bytes());
934 feed(&mut g, b"\x1b[1;1Hx"); // onto the first lead
935 assert_eq!(row_str(&g, 0), "x 本", "the orphaned spacer survived");
936 assert!(!g.row(0)[1].is_spacer());
937
938 let mut g = Grid::new(6, 2);
939 feed(&mut g, "日本".as_bytes());
940 feed(&mut g, b"\x1b[1;2Hx"); // onto the first spacer
941 assert_eq!(row_str(&g, 0), " x本", "the orphaned lead survived");
942 assert!(!g.row(0)[0].is_wide());
943 }
944
945 #[test]
946 fn a_wide_character_written_over_a_pair_clears_the_pair_it_overlaps() {
947 let mut g = Grid::new(6, 2);
948 feed(&mut g, "日本".as_bytes());
949 // Starting one column in covers the first spacer and the second lead.
950 feed(&mut g, "\x1b[1;2H語".as_bytes());
951 assert_eq!(row_str(&g, 0), "", "a half of the old pair survived");
952 assert!(!g.row(0)[0].is_wide());
953 assert!(!g.row(0)[3].is_spacer());
954 }
955
956 #[test]
957 fn erasing_through_half_a_wide_character_takes_the_other_half() {
958 let mut g = Grid::new(6, 2);
959 feed(&mut g, "ab日x".as_bytes());
960 // Erase from column 3 (the spacer) to the end of the line.
961 feed(&mut g, b"\x1b[1;4H\x1b[K");
962 assert_eq!(
963 row_str(&g, 0),
964 "ab",
965 "the lead was left claiming two columns"
966 );
967 assert!(!g.row(0)[2].is_wide());
968 }
969
970 #[test]
971 fn a_one_column_grid_holds_a_wide_character_as_one_column() {
972 // Degenerate, and it must terminate rather than loop looking for room
973 // that a one-column grid can never have.
974 let mut g = Grid::new(1, 2);
975 feed(&mut g, "".as_bytes());
976 assert_eq!(g.row(0)[0].c(), '');
977 assert!(!g.row(0)[0].is_wide());
978 }
979
980 #[test]
981 fn narrowing_then_widening_gives_wide_characters_back() {
982 // The round-trip property the rewrap already had, over the characters
983 // it now has to keep together.
984 let mut g = Grid::new(10, 2);
985 feed(&mut g, "日本語です\r\nabc\r\nx".as_bytes());
986 let before = history_text(&g);
987 g.resize(4, 2);
988 g.resize(10, 2);
989 assert_eq!(history_text(&g), before);
990 }
991
992 #[test]
993 fn a_rewrap_does_not_split_a_wide_character() {
994 // Five columns of CJK re-split at four: the character straddling the
995 // boundary has to move down whole, leaving the odd column as a pad.
996 let mut g = Grid::new(10, 2);
997 feed(&mut g, "日本語です\r\nx\r\ny".as_bytes());
998 g.resize(5, 2);
999 for r in 0..g.history_len() as u16 {
1000 let cells = g.row(r);
1001 assert!(
1002 !cells[cells.len() - 1].is_wide(),
1003 "row {r} ends on half a character"
1004 );
1005 assert!(!cells[0].is_spacer(), "row {r} starts on half a character");
1006 }
1007 assert!(
1008 history_text(&g).starts_with("日本語です"),
1009 "the line did not survive the rewrap: {:?}",
1010 history_text(&g)
1011 );
1012 }
1013
1014 #[test]
1015 fn a_rewrap_does_not_carry_the_old_widths_pad_columns() {
1016 // The column a wide character could not fit into is a fact about the
1017 // old width. Carried through, it would wedge a blank into the middle of
1018 // the line at every width after this one.
1019 let mut g = Grid::new(5, 2);
1020 feed(&mut g, "abcd日本\r\nx\r\ny".as_bytes());
1021 g.resize(6, 2);
1022 assert!(
1023 history_text(&g).starts_with("abcd日本"),
1024 "a stale pad column survived: {:?}",
1025 history_text(&g)
1026 );
1027 }
1028
1029 #[test]
1030 fn narrowing_the_live_screen_does_not_leave_half_a_character() {
1031 // resize_buf clips the live screen rather than reflowing it, and the
1032 // clip can land between a lead and its spacer.
1033 let mut g = Grid::new(6, 2);
1034 feed(&mut g, "ab日".as_bytes());
1035 g.resize(3, 2);
1036 assert!(!g.row(0)[2].is_wide(), "a lead survived without its spacer");
1037 }
1038
1039 #[test]
1040 fn a_wide_character_is_one_word_for_a_double_click() {
1041 // The spacer holds a blank, and a blank is a word delimiter, so reading
1042 // it literally would end the word on the first CJK character.
1043 let g = {
1044 let mut g = Grid::new(10, 2);
1045 feed(&mut g, "日本語 x".as_bytes());
1046 g
1047 };
1048 let sel = Selection::new(SelectionMode::Word, Point::new(0, 0));
1049 assert_eq!(g.selection_text(&sel), "日本語");
1050 }
1051
1052 #[test]
1053 fn a_selection_stopping_on_half_a_character_still_copies_the_character() {
1054 let mut g = Grid::new(10, 2);
1055 feed(&mut g, "日本".as_bytes());
1056 // Columns 0..=2 — the second lead's spacer is outside the drag.
1057 let mut sel = Selection::new(SelectionMode::Char, Point::new(0, 0));
1058 sel.drag_to(Point::new(0, 2));
1059 assert_eq!(g.selection_text(&sel), "日本");
1060 }
1061
1062 // -- combining marks ---------------------------------------------------
1063 //
1064 // A mark modifies the character before it and occupies no column. The cell
1065 // keeps its base inline and refers to the marks by id, so everything that
1066 // only wants to know what a cell LOOKS like is unchanged and only the paths
1067 // producing text have to reassemble the cluster.
1068
1069 #[test]
1070 fn a_combining_mark_takes_no_column_of_its_own() {
1071 let mut g = Grid::new(8, 2);
1072 feed(&mut g, "e\u{301}x".as_bytes());
1073 assert_eq!(g.cursor().col, 2, "the mark consumed a column");
1074 assert_eq!(g.row(0)[0].c(), 'e', "the base is not inline any more");
1075 assert_eq!(
1076 g.row(0)[1].c(),
1077 'x',
1078 "the mark displaced the next character"
1079 );
1080 }
1081
1082 #[test]
1083 fn a_combining_mark_copies_back_with_its_base() {
1084 // The whole point: a path off a Mac-formatted volume has to paste back
1085 // as the path it came from, accents and all.
1086 let mut g = Grid::new(20, 2);
1087 feed(&mut g, "Jose\u{301}/".as_bytes());
1088 assert_eq!(g.text_range(0, 1), "Jose\u{301}/\n");
1089 }
1090
1091 #[test]
1092 fn several_marks_stack_on_one_base() {
1093 let mut g = Grid::new(8, 2);
1094 feed(&mut g, "o\u{323}\u{302}".as_bytes());
1095 assert_eq!(g.cursor().col, 1);
1096 assert_eq!(g.text_range(0, 1), "o\u{323}\u{302}\n");
1097 }
1098
1099 #[test]
1100 fn a_mark_after_a_wide_character_lands_on_the_character_not_its_spacer() {
1101 let mut g = Grid::new(8, 2);
1102 feed(&mut g, "\u{301}".as_bytes());
1103 assert_eq!(g.row(0)[0].marks_id(), 1, "the mark missed the lead");
1104 assert_eq!(g.row(0)[1].marks_id(), 0, "the spacer took the mark");
1105 assert_eq!(g.text_range(0, 1), "\u{301}\n");
1106 }
1107
1108 #[test]
1109 fn a_mark_at_the_right_edge_amends_the_character_still_under_the_cursor() {
1110 // The deferred wrap leaves the cursor ON the last character it wrote
1111 // rather than after it, so the base is found differently there.
1112 let mut g = Grid::new(4, 2);
1113 feed(&mut g, "abcd\u{301}".as_bytes());
1114 assert_eq!(g.text_range(0, 1), "abcd\u{301}\n");
1115 assert_eq!(g.cursor().col, 3, "the mark moved the cursor off the edge");
1116 }
1117
1118 #[test]
1119 fn a_mark_with_nothing_before_it_is_dropped() {
1120 // Malformed. Giving it a cell would be exactly the column error this
1121 // is here to remove.
1122 let mut g = Grid::new(8, 2);
1123 feed(&mut g, "\u{301}x".as_bytes());
1124 assert_eq!(g.cursor().col, 1);
1125 assert_eq!(g.text_range(0, 1), "x\n");
1126 }
1127
1128 #[test]
1129 fn marks_survive_a_rewrap() {
1130 // The id rides in the cell, so every structural move of cells in this
1131 // file carries the marks with no help. This is the assertion that says
1132 // so out loud.
1133 let mut g = Grid::new(10, 2);
1134 feed(&mut g, "abcde\u{301}fghij\r\nx\r\ny".as_bytes());
1135 let before = history_text(&g);
1136 assert!(before.contains('\u{301}'));
1137 g.resize(4, 2);
1138 g.resize(10, 2);
1139 assert_eq!(history_text(&g), before);
1140 }
1141
1142 #[test]
1143 fn overwriting_a_base_drops_the_marks_that_were_on_it() {
1144 // The marks belonged to the character that was there, not to the cell.
1145 let mut g = Grid::new(8, 2);
1146 feed(&mut g, "e\u{301}".as_bytes());
1147 feed(&mut g, b"\x1b[1;1Hx");
1148 assert_eq!(g.text_range(0, 1), "x\n");
1149 assert_eq!(g.row(0)[0].marks_id(), 0);
1150 }
1151
1152 #[test]
1153 fn the_same_mark_sequence_is_interned_once() {
1154 // What keeps the table in the dozens however much text goes past: it
1155 // is the bases that vary, not the sequences attached to them.
1156 let mut g = Grid::new(20, 2);
1157 feed(
1158 &mut g,
1159 "a\u{301}e\u{301}i\u{301}o\u{301}u\u{301}".as_bytes(),
1160 );
1161 let ids: Vec<u16> = (0..5).map(|c| g.row(0)[c].marks_id()).collect();
1162 assert_eq!(ids, vec![1; 5], "one sequence took five ids");
1163 }
1164
1165 #[test]
1166 fn a_cell_stops_taking_marks_at_the_cap() {
1167 // Unbounded input. Past the cap the mark is dropped and the cell keeps
1168 // what it had, rather than the cell being rewritten or the id space
1169 // being spent on a cluster nobody is reading.
1170 let mut g = Grid::new(8, 2);
1171 let mut bytes = String::from("e");
1172 for _ in 0..MAX_MARKS + 5 {
1173 bytes.push('\u{301}');
1174 }
1175 feed(&mut g, bytes.as_bytes());
1176 let text = g.text_range(0, 1);
1177 assert_eq!(
1178 text.chars().filter(|c| *c == '\u{301}').count(),
1179 MAX_MARKS,
1180 "the cap did not hold"
1181 );
1182 }
1183
1184 #[test]
1185 fn a_marked_character_is_one_word_for_a_double_click() {
1186 // Word boundaries read the base and never learn the table exists.
1187 let mut g = Grid::new(20, 2);
1188 feed(&mut g, "Jose\u{301} x".as_bytes());
1189 let sel = Selection::new(SelectionMode::Word, Point::new(0, 0));
1190 assert_eq!(g.selection_text(&sel), "Jose\u{301}");
1191 }
1192
1193 #[test]
1194 fn invalidate_render_reports_every_row_however_quiet_the_grid_is() {
1195 // The renderer rebuilt itself and lost its cache. Nothing about the
1196 // grid changed, which is exactly why it cannot be left to report the
1197 // difference: there isn't one, and the window would stay empty until
1198 // the program on the pty happened to print.
1199 let mut g = Grid::new(8, 4);
1200 feed(&mut g, b"one\r\ntwo");
1201 let _ = g.take_damage();
1202 assert!(
1203 g.take_damage().dirty_rows.is_empty(),
1204 "a quiet grid still had damage to report"
1205 );
1206 g.invalidate_render();
1207 let d = g.take_damage();
1208 assert_eq!(d.dirty_rows.len(), 4, "not every row came back");
1209 assert!(d.view_moved, "the viewport was not invalidated with them");
1210 }
1211 }
1212