[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.

๐Ÿ”ง return Uint8Array from render, fix duplicate element IDs

- Change Term.render() to return Uint8Array instead of string, avoiding
unnecessary TextDecoder overhead on every frame
- Fix Clay element ID collisions by adding a monotonic index to the hash
- Remove cursor hide/show from the C render path (caller responsibility)
- Update README example and tests to match new return type

Charles Lowell (Mar 16, 2026, 3:13 PM -0500) 49b88549 d070a259

+17 -19
+2 -2
README.md
··· 25 25 | -> diff against (front) | 26 26 | -> escape bytes | 27 27 +---------------+ | | 28 - | | ANSI string | | 28 + | | ANSI byte array| | 29 29 | stdout.write | <============= | | 30 30 | | | | 31 31 +---------------+ +---------------------------+ ··· 55 55 close(), 56 56 ]); 57 57 58 - Deno.stdout.writeSync(new TextEncoder().encode(ansi)); 58 + Deno.stdout.writeSync(ansi); 59 59 ``` 60 60 61 61 ## Development
-6
src/clayterm.c
··· 347 347 348 348 cells_clear(ct->back, ct->w, ct->h); 349 349 350 - /* hide cursor */ 351 - buf_str(&ct->out, "\x1b[?25l"); 352 - 353 350 /* walk Clay render commands into back buffer */ 354 351 walk(ct, cmds); 355 352 356 353 /* diff front vs back, emit escape sequences */ 357 354 present(ct); 358 - 359 - /* show cursor */ 360 - buf_str(&ct->out, "\x1b[?25h"); 361 355 } 362 356 363 357 char *output(struct Clayterm *ct) { return ct->out.data; }
+2 -1
src/ops.c
··· 61 61 62 62 void reduce(struct Clayterm *ct, uint32_t *buf, int len) { 63 63 int i = 0; 64 + uint32_t idx = 0; 64 65 65 66 Clay_BeginLayout(); 66 67 ··· 77 78 78 79 if (id_len > 0) { 79 80 Clay_String str = {.length = (int32_t)id_len, .chars = id_chars}; 80 - Clay_ElementId eid = Clay__HashString(str, 0); 81 + Clay_ElementId eid = Clay__HashString(str, idx++); 81 82 Clay__OpenElementWithId(eid); 82 83 } else { 83 84 Clay__OpenElement();
+6 -6
term.ts
··· 7 7 } 8 8 9 9 export interface Term { 10 - render(ops: Op[]): string; 10 + render(ops: Op[]): Uint8Array; 11 11 } 12 - 13 - const decoder = new TextDecoder(); 14 12 15 13 export async function createTerm(options: TermOptions): Promise<Term> { 16 14 const { width, height } = options; ··· 20 18 ); 21 19 22 20 return { 23 - render(ops: Op[]): string { 21 + render(ops: Op[]): Uint8Array { 24 22 const len = pack(ops, memory.buffer, opsBuf); 25 23 reduce(statePtr, opsBuf, len); 26 - return decoder.decode( 27 - new Uint8Array(memory.buffer, output(statePtr), length(statePtr)), 24 + return new Uint8Array( 25 + memory.buffer, 26 + output(statePtr), 27 + length(statePtr), 28 28 ); 29 29 }, 30 30 };
+7 -4
test/term.test.ts
··· 3 3 import { close, grow, open, rgba, text } from "../ops.ts"; 4 4 import { print } from "./print.ts"; 5 5 6 + const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes); 7 + 6 8 describe("term", () => { 7 9 let term: Term; 8 10 ··· 11 13 }); 12 14 13 15 it("renders hello world", () => { 14 - const out = print(term.render([ 16 + const out = print(decode(term.render([ 15 17 open("root", { 16 18 layout: { width: grow(), height: grow(), direction: "ttb" }, 17 19 }), 18 20 text("Hello, World!"), 19 21 close(), 20 - ]), 40, 10); 22 + ])), 40, 10); 21 23 22 24 expect(out).toContain("Hello, World!"); 23 25 }); 24 26 25 27 it("renders borders and padding", () => { 26 - const out = print(term.render([ 28 + const out = print(decode(term.render([ 27 29 open("box", { 28 30 layout: { 29 31 width: grow(), ··· 42 44 }), 43 45 text("padded"), 44 46 close(), 45 - ]), 40, 10); 47 + ])), 40, 10); 46 48 47 49 expect(out).toEqual(` 48 50 โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ ··· 56 58 โ”‚ โ”‚ 57 59 โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ`.trim()); 58 60 }); 61 + 59 62 });