Something went wrong. Try again.
[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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354import { beforeEach, describe, expect, it } from "./suite.ts";import { createTerm, type Term } from "../term.ts";import { close, fixed, grow, type Op, open, rgba, snapshot, text,} from "../ops.ts";import { print } from "./print.ts";
const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes);const trim = (s: string) => s.split("\n").map((l) => l.trimEnd()).join("\n");
describe("term", () => { let term: Term;
beforeEach(async () => { term = await createTerm({ width: 40, height: 10 }); });
it("renders hello world", () => { let out = print( decode( term.render([ open("root", { layout: { width: grow(), height: grow(), direction: "ttb" }, }), text("Hello, World!"), close(), ]).output, ), 40, 10, );
expect(out).toContain("Hello, World!"); });
it("text inherits parent background", () => { let ansi = decode( term.render([ open("root", { layout: { width: grow(), height: grow(), direction: "ttb" }, bg: rgba(255, 0, 0), }), text("hi"), close(), ]).output, );
// the SGR active when "h" is emitted should include the // parent's red background (48;2;255;0;0), not terminal default let before = ansi.slice(0, ansi.indexOf("h")); expect(before).toContain("\x1b[48;2;255;0;0"); });
it("renders borders and padding", () => { let out = print( decode( term.render([ open("box", { layout: { width: grow(), height: grow(), padding: { left: 5, top: 5 }, direction: "ttb", }, border: { color: rgba(0, 255, 0), left: 1, right: 1, top: 1, bottom: 1, }, cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 }, }), text("padded"), close(), ]).output, ), 40, 10, );
expect(out).toEqual(`╭──────────────────────────────────────╮│ ││ ││ ││ ││ padded ││ ││ ││ │╰──────────────────────────────────────╯`.trim()); });
describe("line mode", () => { let box = (msg: string) => [ open("root", { layout: { width: grow(), height: grow(), direction: "ttb" }, }), open("box", { layout: { width: grow(), height: grow(), direction: "ttb", padding: { left: 1, top: 1 }, }, border: { color: rgba(255, 255, 255), left: 1, right: 1, top: 1, bottom: 1, }, }), text(msg), close(), close(), ];
it("renders with newlines instead of CUP sequences", async () => { let term = await createTerm({ width: 20, height: 5 });
let out = decode( term.render(box("hello world"), { mode: "line" }).output, ); // deno-lint-ignore no-control-regex expect(out).not.toMatch(/\x1b\[\d+;\d+H/); expect(out.split("\n").length).toBe(5); expect(trim(print(out, 20, 5))).toEqual(`┌──────────────────┐│hello world ││ ││ │└──────────────────┘`.trim()); });
it("primes front buffer for subsequent diff render", async () => { let term = await createTerm({ width: 20, height: 5 });
let first = decode( term.render(box("hello world"), { mode: "line" }).output, ); let second = decode(term.render(box("goodbye")).output);
expect(trim(print(first + second, 20, 5))).toEqual(`┌──────────────────┐│goodbye ││ ││ │└──────────────────┘`.trim());
expect(second.length).toBeLessThan(first.length); }); });
describe("info", () => { it("returns bounds for named elements", async () => { let term = await createTerm({ width: 40, height: 10 }); let result = term.render([ open("root", { layout: { width: grow(), height: grow(), direction: "ttb" }, }), open("child", { layout: { width: fixed(20), height: fixed(5) }, }), close(), close(), ]);
let root = result.info.get("root"); expect(root).toBeDefined(); expect(root!.bounds).toEqual({ x: 0, y: 0, width: 40, height: 10 });
let child = result.info.get("child"); expect(child).toBeDefined(); expect(child!.bounds).toEqual({ x: 0, y: 0, width: 20, height: 5 }); });
it("returns undefined for unknown ids", async () => { let term = await createTerm({ width: 20, height: 5 }); term.render([ open("root", { layout: { width: grow(), height: grow() } }), close(), ]);
let result = term.render([ open("root", { layout: { width: grow(), height: grow() } }), close(), ]);
expect(result.info.get("nonexistent")).toBeUndefined(); expect(result.info.get("")).toBeUndefined(); }); });
describe("snapshot", () => { it("produces identical output to direct ops", async () => { let ops = [ open("root", { layout: { width: grow(), height: grow(), direction: "ttb" }, bg: rgba(0, 0, 128), }), open("child", { layout: { width: grow(), padding: { left: 1 }, direction: "ttb", }, border: { color: rgba(255, 255, 255), left: 1, right: 1, top: 1, bottom: 1, }, }), text("snapshot test"), close(), close(), ];
let direct = await createTerm({ width: 40, height: 10 }); let snapped = await createTerm({ width: 40, height: 10 });
let expected = direct.render(ops, { mode: "line" }).output; let actual = snapped.render([snapshot(ops)], { mode: "line" }).output;
expect(decode(actual)).toEqual(decode(expected)); });
it("renders inside another element", async () => { let child = snapshot([ open("child", { layout: { width: grow(), direction: "ttb" }, }), text("inner"), close(), ]);
let direct = await createTerm({ width: 20, height: 5 }); let snapped = await createTerm({ width: 20, height: 5 });
let wrapper = (content: Op[]) => [ open("root", { layout: { width: grow(), height: grow(), direction: "ttb", padding: { left: 1, top: 1 }, }, border: { color: rgba(255, 255, 255), left: 1, right: 1, top: 1, bottom: 1, }, }), ...content, close(), ];
let expected = direct.render( wrapper([ open("child", { layout: { width: grow(), direction: "ttb" }, }), text("inner"), close(), ]), { mode: "line" }, ).output;
let actual = snapped.render( wrapper([child]), { mode: "line" }, ).output;
expect(decode(actual)).toEqual(decode(expected)); expect(trim(print(decode(actual), 20, 5))).toEqual(`┌──────────────────┐│inner ││ ││ │└──────────────────┘`.trim()); }); });
describe("row offset", () => { it("renders two frames at the offset position", async () => { let term = await createTerm({ width: 20, height: 5 }); let box = (msg: string) => [ open("root", { layout: { width: grow(), height: grow(), direction: "ttb" }, }), open("box", { layout: { width: grow(), height: grow(), direction: "ttb", padding: { left: 1, top: 1 }, }, border: { color: rgba(255, 255, 255), left: 1, right: 1, top: 1, bottom: 1, }, }), text(msg), close(), close(), ];
let header = await createTerm({ width: 20, height: 5 }); let banner = decode(header.render(box("hello")).output);
let first = decode(term.render(box("world"), { row: 6 }).output); expect(print(banner + first, 20, 10)).toEqual(`\┌──────────────────┐│hello ││ ││ │└──────────────────┘┌──────────────────┐│world ││ ││ │└──────────────────┘`);
let second = decode(term.render(box("universe"), { row: 6 }).output); expect(print(banner + first + second, 20, 10)).toEqual(`\┌──────────────────┐│hello ││ ││ │└──────────────────┘┌──────────────────┐│universe ││ ││ │└──────────────────┘`); }); });});