From 91a02a5074afe347c4f5edb71b2235ef38c5b3eb Mon Sep 17 00:00:00 2001 From: Scott Mohekey Date: Wed, 29 Jul 2026 20:30:59 +1200 Subject: [PATCH] fix: background-colour erase (bce) for ESC[K / ED past the written text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A BufferLine is only painted up to its `_length`, and `eraseRange` clamped the erase to `_length` — so an app that sets a background then emits `ESC[K` (erase to end of line) to fill the rest of a row got a row coloured only under the text it had written, not the full width. This is background-colour erase (bce), which real terminals and xterm.js implement. When the pen carries an explicit background, grow the line to the erase boundary so the erased cells exist and render with that background. Default-background erases stay trimmed, preserving the previous behaviour (and keeping trailing cells out of selection/copy). Reproduces with e.g. `ESC[42mAB ESC[K` — cells past "AB" now carry the green background. Full-screen TUIs that draw diffs this way (e.g. the Codex CLI) were rendering patchy backgrounds without it. --- lib/src/core/buffer/line.dart | 16 +++++++++++++++ test/src/core/buffer/bce_test.dart | 33 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 test/src/core/buffer/bce_test.dart diff --git a/lib/src/core/buffer/line.dart b/lib/src/core/buffer/line.dart index 6e72114d..f8de649f 100644 --- a/lib/src/core/buffer/line.dart +++ b/lib/src/core/buffer/line.dart @@ -145,6 +145,22 @@ class BufferLine with IndexedItem { eraseCell(end - 1, style); } + // Background-colour erase (bce): a line is only painted up to `_length`, so clamping the erase to + // `_length` would silently drop cells an app expects filled with the current background — e.g. codex + // sets a diff row's background then emits `ESC[K` to colour the rest of the row, leaving the row + // patchy instead of full-width. When the pen carries an explicit background, grow the line to the + // erase boundary so those cells exist and render. (Default-background erases stay trimmed, preserving + // the previous behaviour and keeping trailing cells out of selection/copy.) + if (style.background != 0 && end > _length) { + final gapStart = _length; + resize(end); + // A cursor parked past the written text leaves a gap before `start`; keep it default rather than + // whatever stale data the grown buffer held. + for (var i = gapStart; i < start && i < end; i++) { + resetCell(i); + } + } + end = min(end, _length); for (var i = start; i < end; i++) { eraseCell(i, style); diff --git a/test/src/core/buffer/bce_test.dart b/test/src/core/buffer/bce_test.dart new file mode 100644 index 00000000..3a1183c4 --- /dev/null +++ b/test/src/core/buffer/bce_test.dart @@ -0,0 +1,33 @@ +import 'package:test/test.dart'; +import 'package:xterm/core.dart'; + +void main() { + group('background-colour erase (bce)', () { + test('ESC[K after short text fills the rest of the row with the current background', () { + final terminal = Terminal(); + terminal.resize(20, 5); + + // Green background, two characters, then erase-to-end-of-line. + terminal.write('\x1b[42mAB\x1b[K'); + + final line = terminal.buffer.lines[0]; + final green = line.getBackground(0); // the background used for the written cells + expect(green, isNot(0), reason: 'sanity: an explicit background is set'); + + // Cells past the written text must carry the same background (bce), not the default. + expect(line.getBackground(2), green); + expect(line.getBackground(10), green); + expect(line.getBackground(19), green); + }); + + test('ESC[K with the default background does not extend the line', () { + final terminal = Terminal(); + terminal.resize(20, 5); + + terminal.write('AB\x1b[K'); + + // No explicit background → erased cells stay at the default (0); the line is not grown. + expect(terminal.buffer.lines[0].getBackground(10), 0); + }); + }); +}