···1515 let x = 0;
1616 let y = 0;
1717 let i = 0;
1818+ let lastCh = " "; // preceding graphic character, for REP (CSI b)
18191920 while (i < ansi.length) {
2021 if (ansi[i] === "\x1b" && ansi[i + 1] === "[") {
···3435 let parts = params.split(";");
3536 y = (parseInt(parts[0]) || 1) - 1;
3637 x = (parseInt(parts[1]) || 1) - 1;
3838+ } else if (cmd === "b") {
3939+ // REP: repeat the preceding graphic character `params` times
4040+ let n = parseInt(params) || 0;
4141+ for (let r = 0; r < n; r++) {
4242+ if (x >= 0 && x < w && y >= 0 && y < h) {
4343+ grid[y][x] = lastCh;
4444+ }
4545+ x++;
4646+ }
3747 } else if (cmd === "m") {
3848 // SGR — ignore
3949 }
···4656 // regular character — could be multi-byte UTF-8
4757 let cp = ansi.codePointAt(i)!;
4858 let ch = String.fromCodePoint(cp);
5959+ lastCh = ch;
4960 if (x >= 0 && x < w && y >= 0 && y < h) {
5061 grid[y][x] = ch;
5162 }
+103
test/rep.test.ts
···11+import { close, fixed, open, rgba, text } from "../ops.ts";
22+import { createTerm } from "../term.ts";
33+import { describe, expect, it } from "./suite.ts";
44+import { print } from "./print.ts";
55+66+const decode = (b: Uint8Array) => new TextDecoder().decode(b);
77+const trim = (s: string) => s.split("\n").map((l) => l.trimEnd()).join("\n");
88+99+// REP is "\x1b[<n>b" — repeat the preceding graphic character n times.
1010+function hasRep(ansi: string): boolean {
1111+ for (
1212+ let i = ansi.indexOf("\x1b[");
1313+ i !== -1;
1414+ i = ansi.indexOf("\x1b[", i + 1)
1515+ ) {
1616+ let j = i + 2;
1717+ while (j < ansi.length && ansi[j] >= "0" && ansi[j] <= "9") j++;
1818+ if (j > i + 2 && ansi[j] === "b") return true;
1919+ }
2020+ return false;
2121+}
2222+2323+describe("REP (CSI b) coalescing", () => {
2424+ it("coalesces a horizontal run of identical cells into a REP sequence", async () => {
2525+ let term = await createTerm({ width: 40, height: 3 });
2626+2727+ // A bordered box: the top edge is ┌ + 36×─ + ┐ — a long run of the
2828+ // identical box-drawing cell that REP can collapse.
2929+ let ansi = decode(
3030+ term.render([
3131+ open("box", {
3232+ layout: { width: fixed(38), height: fixed(3), direction: "ttb" },
3333+ border: {
3434+ color: rgba(255, 255, 255),
3535+ left: 1,
3636+ right: 1,
3737+ top: 1,
3838+ bottom: 1,
3939+ },
4040+ }),
4141+ text("hi"),
4242+ close(),
4343+ ]).output,
4444+ );
4545+4646+ expect(hasRep(ansi)).toBe(true);
4747+ });
4848+4949+ it("renders the same grid the repeated cells would have produced", async () => {
5050+ let term = await createTerm({ width: 40, height: 3 });
5151+5252+ let out = print(
5353+ decode(
5454+ term.render([
5555+ open("box", {
5656+ layout: { width: fixed(38), height: fixed(3), direction: "ttb" },
5757+ border: {
5858+ color: rgba(255, 255, 255),
5959+ left: 1,
6060+ right: 1,
6161+ top: 1,
6262+ bottom: 1,
6363+ },
6464+ }),
6565+ text("hi"),
6666+ close(),
6767+ ]).output,
6868+ ),
6969+ 40,
7070+ 3,
7171+ );
7272+7373+ expect(trim(out).split("\n")[0]).toBe("┌" + "─".repeat(36) + "┐");
7474+ });
7575+7676+ it("does not use REP for a short single-byte run that would not save bytes", async () => {
7777+ let term = await createTerm({ width: 40, height: 1 });
7878+7979+ // Frame 1: a full row of 'a'. Frame 2: change the first 5 cells to 'b'.
8080+ // The diff is an isolated 5-cell run — REP (\x1b[4b) is break-even, so
8181+ // emitting "bbbbb" inline is preferred.
8282+ term.render([text("a".repeat(40))]);
8383+ let ansi = decode(
8484+ term.render([text("b".repeat(5) + "a".repeat(35))]).output,
8585+ );
8686+8787+ expect(ansi).toContain("bbbbb");
8888+ expect(hasRep(ansi)).toBe(false);
8989+ });
9090+9191+ it("uses REP once a single-byte run is long enough to save bytes", async () => {
9292+ let term = await createTerm({ width: 40, height: 1 });
9393+9494+ // Frame 2 changes the first 6 cells to 'b'. run*1=6 > 1+3+1=5, so the
9595+ // run collapses to one 'b' + \x1b[5b.
9696+ term.render([text("a".repeat(40))]);
9797+ let ansi = decode(
9898+ term.render([text("b".repeat(6) + "a".repeat(34))]).output,
9999+ );
100100+101101+ expect(ansi).toContain("\x1b[5b");
102102+ });
103103+});