[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay bomb.sh/docs/tty
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222import { close, fixed, grow, open, rgba, text } from "../ops.ts";import { createTerm } from "../term.ts";import { describe, expect, it } from "./suite.ts";
const decode = (b: Uint8Array) => new TextDecoder().decode(b);
type BgColor = { value: number; sgr: string;};
type Cell = { ch: string; bg?: string;};
function randomBgColor(): BgColor { let r = 0; let g = 0; let b = 0;
do { r = Math.floor(Math.random() * 256); g = Math.floor(Math.random() * 256); b = Math.floor(Math.random() * 256); } while ( (r === 255 && g === 0 && b === 0) || (r === 0 && g === 255 && b === 0) || (r === 0 && g === 0 && b === 255) );
return { value: rgba(r, g, b), sgr: `\x1b[48;2;${r};${g};${b}`, };}
function cells(ansi: string): Cell[] { let result: Cell[] = []; let bg: string | undefined;
for (let i = 0; i < ansi.length;) { if (ansi[i] === "\x1b" && ansi[i + 1] === "[") { let end = i + 2; while (end < ansi.length && !/[A-Za-z]/.test(ansi[end])) { end++; }
let seq = ansi.slice(i, end + 1); if (seq === "\x1b[0m") { bg = undefined; } else if (seq.startsWith("\x1b[48;2;") && seq.endsWith("m")) { bg = seq.slice(0, -1); }
i = end + 1; continue; }
result.push({ ch: ansi[i], bg }); i++; }
return result;}
function firstCell(cells: Cell[], ch: string): Cell { let cell = cells.find((c) => c.ch === ch); expect(cell).toBeDefined(); return cell!;}
describe("foreground", () => { it("emits uncolored text with no foreground", async () => { let term = await createTerm({ width: 12, height: 1 }); let ansi = decode(term.render([text("hi")]).output);
expect(ansi).toContain("hi"); expect(ansi).not.toContain("\x1b[38;2;255;255;255"); });});
describe("background", () => { it("fills border cells with the requested border-level bg", async () => { let term = await createTerm({ width: 12, height: 4 }); let bg = randomBgColor(); let ansi = decode( term.render([ open("box", { layout: { width: fixed(8), height: fixed(3), direction: "ttb" }, border: { color: rgba(255, 255, 255), bg: bg.value, left: 1, right: 1, top: 1, bottom: 1, }, }), text("Hi"), close(), ], { mode: "line" }).output, );
expect(ansi).toContain(`${bg.sgr}m┌`);
let rendered = cells(ansi); expect(firstCell(rendered, "┌").bg).toBe(bg.sgr); expect(firstCell(rendered, "─").bg).toBe(bg.sgr); expect(firstCell(rendered, "┐").bg).toBe(bg.sgr); expect(firstCell(rendered, "│").bg).toBe(bg.sgr); });
it("leaves existing border-cell bg unchanged when border bg is omitted", async () => { let term = await createTerm({ width: 12, height: 4 }); let bg = randomBgColor(); let ansi = decode( term.render([ open("box", { layout: { width: fixed(8), height: fixed(3), direction: "ttb" }, bg: bg.value, border: { color: rgba(255, 255, 255), left: 1, right: 1, top: 1, bottom: 1, }, }), text("Hi"), close(), ], { mode: "line" }).output, );
let rendered = cells(ansi); expect(firstCell(rendered, "┌").bg).toBe(bg.sgr); expect(firstCell(rendered, "─").bg).toBe(bg.sgr); expect(firstCell(rendered, "┐").bg).toBe(bg.sgr); expect(firstCell(rendered, "│").bg).toBe(bg.sgr); });
it("fills glyph cells with the requested text-level bg", async () => { let term = await createTerm({ width: 20, height: 1 }); let bg = randomBgColor(); let ansi = decode( term.render([ open("root", { layout: { width: grow(), height: grow() } }), text("Hi", { bg: bg.value }), close(), ]).output, );
let beforeH = ansi.slice(0, ansi.indexOf("H")); expect(beforeH).toContain(bg.sgr); });
it("resets border bg on subsequent frames without border bg", async () => { let term = await createTerm({ width: 12, height: 4 }); let bg = randomBgColor();
// Frame 1: border with bg term.render([ open("box", { layout: { width: fixed(8), height: fixed(3), direction: "ttb" }, border: { color: rgba(255, 255, 255), bg: bg.value, left: 1, right: 1, top: 1, bottom: 1, }, }), text("Hi"), close(), ]);
// Frame 2: same border, no bg let ansi = decode( term.render([ open("box", { layout: { width: fixed(8), height: fixed(3), direction: "ttb" }, border: { color: rgba(255, 255, 255), left: 1, right: 1, top: 1, bottom: 1, }, }), text("Hi"), close(), ]).output, );
expect(ansi).not.toContain(bg.sgr); expect(firstCell(cells(ansi), "┌").bg).toBeUndefined(); });
it("resets the background before writing trailing cells", async () => { let term = await createTerm({ width: 20, height: 1 }); let bg = randomBgColor(); let ansi = decode( term.render([ open("root", { layout: { width: grow(), height: grow() } }), text("Hi", { bg: bg.value }), close(), ]).output, );
let beforeH = ansi.slice(0, ansi.indexOf("H")); expect(beforeH).toContain(bg.sgr);
let hi = ansi.indexOf("Hi"); expect(hi).toBeGreaterThanOrEqual(0);
let afterHi = ansi.slice(hi + 2); expect(afterHi).not.toContain(bg.sgr); expect(afterHi.startsWith("\x1b[0m ")).toBe(true); });});