[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

Select the types of activity you want to include in your feed.

test(render): cover REP CSI Ps b

Nate Moore (Jun 26, 2026, 11:54 PM EDT) 7f87619a 3dfb5f53

+114
+11
test/print.ts
··· 15 15 let x = 0; 16 16 let y = 0; 17 17 let i = 0; 18 + let lastCh = " "; // preceding graphic character, for REP (CSI b) 18 19 19 20 while (i < ansi.length) { 20 21 if (ansi[i] === "\x1b" && ansi[i + 1] === "[") { ··· 34 35 let parts = params.split(";"); 35 36 y = (parseInt(parts[0]) || 1) - 1; 36 37 x = (parseInt(parts[1]) || 1) - 1; 38 + } else if (cmd === "b") { 39 + // REP: repeat the preceding graphic character `params` times 40 + let n = parseInt(params) || 0; 41 + for (let r = 0; r < n; r++) { 42 + if (x >= 0 && x < w && y >= 0 && y < h) { 43 + grid[y][x] = lastCh; 44 + } 45 + x++; 46 + } 37 47 } else if (cmd === "m") { 38 48 // SGR — ignore 39 49 } ··· 46 56 // regular character — could be multi-byte UTF-8 47 57 let cp = ansi.codePointAt(i)!; 48 58 let ch = String.fromCodePoint(cp); 59 + lastCh = ch; 49 60 if (x >= 0 && x < w && y >= 0 && y < h) { 50 61 grid[y][x] = ch; 51 62 }
+103
test/rep.test.ts
··· 1 + import { close, fixed, open, rgba, text } from "../ops.ts"; 2 + import { createTerm } from "../term.ts"; 3 + import { describe, expect, it } from "./suite.ts"; 4 + import { print } from "./print.ts"; 5 + 6 + const decode = (b: Uint8Array) => new TextDecoder().decode(b); 7 + const trim = (s: string) => s.split("\n").map((l) => l.trimEnd()).join("\n"); 8 + 9 + // REP is "\x1b[<n>b" — repeat the preceding graphic character n times. 10 + function hasRep(ansi: string): boolean { 11 + for ( 12 + let i = ansi.indexOf("\x1b["); 13 + i !== -1; 14 + i = ansi.indexOf("\x1b[", i + 1) 15 + ) { 16 + let j = i + 2; 17 + while (j < ansi.length && ansi[j] >= "0" && ansi[j] <= "9") j++; 18 + if (j > i + 2 && ansi[j] === "b") return true; 19 + } 20 + return false; 21 + } 22 + 23 + describe("REP (CSI b) coalescing", () => { 24 + it("coalesces a horizontal run of identical cells into a REP sequence", async () => { 25 + let term = await createTerm({ width: 40, height: 3 }); 26 + 27 + // A bordered box: the top edge is ┌ + 36×─ + ┐ — a long run of the 28 + // identical box-drawing cell that REP can collapse. 29 + let ansi = decode( 30 + term.render([ 31 + open("box", { 32 + layout: { width: fixed(38), height: fixed(3), direction: "ttb" }, 33 + border: { 34 + color: rgba(255, 255, 255), 35 + left: 1, 36 + right: 1, 37 + top: 1, 38 + bottom: 1, 39 + }, 40 + }), 41 + text("hi"), 42 + close(), 43 + ]).output, 44 + ); 45 + 46 + expect(hasRep(ansi)).toBe(true); 47 + }); 48 + 49 + it("renders the same grid the repeated cells would have produced", async () => { 50 + let term = await createTerm({ width: 40, height: 3 }); 51 + 52 + let out = print( 53 + decode( 54 + term.render([ 55 + open("box", { 56 + layout: { width: fixed(38), height: fixed(3), direction: "ttb" }, 57 + border: { 58 + color: rgba(255, 255, 255), 59 + left: 1, 60 + right: 1, 61 + top: 1, 62 + bottom: 1, 63 + }, 64 + }), 65 + text("hi"), 66 + close(), 67 + ]).output, 68 + ), 69 + 40, 70 + 3, 71 + ); 72 + 73 + expect(trim(out).split("\n")[0]).toBe("┌" + "─".repeat(36) + "┐"); 74 + }); 75 + 76 + it("does not use REP for a short single-byte run that would not save bytes", async () => { 77 + let term = await createTerm({ width: 40, height: 1 }); 78 + 79 + // Frame 1: a full row of 'a'. Frame 2: change the first 5 cells to 'b'. 80 + // The diff is an isolated 5-cell run — REP (\x1b[4b) is break-even, so 81 + // emitting "bbbbb" inline is preferred. 82 + term.render([text("a".repeat(40))]); 83 + let ansi = decode( 84 + term.render([text("b".repeat(5) + "a".repeat(35))]).output, 85 + ); 86 + 87 + expect(ansi).toContain("bbbbb"); 88 + expect(hasRep(ansi)).toBe(false); 89 + }); 90 + 91 + it("uses REP once a single-byte run is long enough to save bytes", async () => { 92 + let term = await createTerm({ width: 40, height: 1 }); 93 + 94 + // Frame 2 changes the first 6 cells to 'b'. run*1=6 > 1+3+1=5, so the 95 + // run collapses to one 'b' + \x1b[5b. 96 + term.render([text("a".repeat(40))]); 97 + let ansi = decode( 98 + term.render([text("b".repeat(6) + "a".repeat(34))]).output, 99 + ); 100 + 101 + expect(ansi).toContain("\x1b[5b"); 102 + }); 103 + });