diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index f07781c9d..388ee4903 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -6,6 +6,10 @@ - Added `TuiAltScreen.getScreenLines()`, which returns the lines of the last rendered frame. +### Fixed + +- Fixed color bleeding past mouse selections and search highlights in fullscreen mode when a styled token ends at the highlight boundary ([#10169](https://github.com/earendil-works/pi/issues/10169)) + ## [0.99.2] - 2026-09-30 ## [0.99.1] - 2026-09-29 diff --git a/packages/tui/src/utils.ts b/packages/tui/src/utils.ts index f537ad7e3..c07945887 100644 --- a/packages/tui/src/utils.ts +++ b/packages/tui/src/utils.ts @@ -1273,8 +1273,11 @@ export function sliceWithWidth( while (i < line.length) { const ansi = extractAnsiCode(line, i); if (ansi) { - if (currentCol >= startCol && currentCol < endCol) result += ansi.code; - else if (currentCol < startCol) pendingAnsi += ansi.code; + if (currentCol >= startCol && currentCol < endCol) { + // Keep original order: codes from before the range must precede codes at the boundary + result += pendingAnsi + ansi.code; + pendingAnsi = ""; + } else if (currentCol < startCol) pendingAnsi += ansi.code; i += ansi.length; continue; } diff --git a/packages/tui/test/regression-slice-by-column-ansi-order.test.ts b/packages/tui/test/regression-slice-by-column-ansi-order.test.ts new file mode 100644 index 000000000..b742817f9 --- /dev/null +++ b/packages/tui/test/regression-slice-by-column-ansi-order.test.ts @@ -0,0 +1,17 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { sliceByColumn } from "../src/utils.ts"; + +// https://github.com/earendil-works/pi/issues/10169 +describe("sliceByColumn ANSI order regression", () => { + it("keeps a reset at the slice start after earlier style codes", () => { + const line = "\x1b[32mfoo\x1b[39m bar"; + assert.strictEqual(sliceByColumn(line, 3, 4, true), "\x1b[32m\x1b[39m bar"); + }); + + it("does not leak color into text after a highlighted token", () => { + const line = "Another \x1b[35malpha\x1b[39m line with \x1b[35mbeta\x1b[39m later."; + const after = sliceByColumn(line, 13, 100, true); + assert.strictEqual(after, "\x1b[35m\x1b[39m line with \x1b[35mbeta\x1b[39m later."); + }); +});