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

✨ surface Clay layout errors on RenderResult

Clay reports errors (duplicate IDs, capacity exceeded, etc.) via a
callback during layout. These were previously silently discarded.
Collect them per-render on the Clayterm struct and expose them as
`errors: ClayError[]` on RenderResult.

Charles Lowell (Apr 17, 2026, 4:59 PM -0500) ab1bd20b a1ae74cf

+118 -10
+6
AGENTS.md
··· 33 33 34 34 - Directives are plain objects. No classes, no methods, no prototype chains. The 35 35 flat array pattern is normative. 36 + 37 + ## C code conventions 38 + 39 + - No global mutable state. All state belongs on a struct instance (e.g. 40 + `Clayterm`). Use Clay's `userData` pointer or similar mechanisms to route 41 + callbacks back to the owning instance.
+20 -1
specs/renderer-spec.md
··· 631 631 ### 12.3 Render return type 632 632 633 633 The `render()` method currently returns a `RenderResult` object shaped as 634 - `{ output: Uint8Array, events: PointerEvent[], info: RenderInfo }`. 634 + `{ output: Uint8Array, events: PointerEvent[], info: RenderInfo, errors: ClayError[] }`. 635 635 636 636 The `output` field is the ANSI byte output specified normatively in Section 7.3 637 637 and Section 8.2. ··· 670 670 671 671 Querying an element with an empty-string id or an id not present in the frame 672 672 returns `undefined`. 673 + 674 + The `errors` field contains any errors reported by the Clay layout engine during 675 + the most recent `render()` call. Each error is a `ClayError` object with: 676 + 677 + - `type`: a string identifying the error category. The following types are 678 + defined, matching Clay's error taxonomy: 679 + - `"TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED"` 680 + - `"ARENA_CAPACITY_EXCEEDED"` 681 + - `"ELEMENTS_CAPACITY_EXCEEDED"` 682 + - `"TEXT_MEASUREMENT_CAPACITY_EXCEEDED"` 683 + - `"DUPLICATE_ID"` 684 + - `"FLOATING_CONTAINER_PARENT_NOT_FOUND"` 685 + - `"PERCENTAGE_OVER_1"` 686 + - `"INTERNAL_ERROR"` 687 + - `"UNBALANCED_OPEN_CLOSE"` 688 + - `message`: a human-readable string describing the error in detail. 689 + 690 + Errors are collected per-render; each call to `render()` returns only the errors 691 + from that invocation. The array is empty when no errors occurred. 673 692 674 693 The return type of `render()` has changed twice since the project's inception 675 694 (string, then `Uint8Array`, then `RenderResult`). While the ANSI bytes
+43 -8
src/clayterm.c
··· 1 1 /* clayterm.c — WASM terminal rendering engine for Clay UI 2 2 * 3 3 * Public API (exported to WASM): 4 - * clayterm_size — compute arena size for given dimensions 5 - * init — initialize a Clayterm instance in provided memory 6 - * reduce — decode command buffer, run Clay layout, render to ANSI 7 - * output — pointer to output byte buffer 8 - * length — length of output byte buffer 9 - * measure — Clay text measurement callback 4 + * clayterm_size — compute arena size for given dimensions 5 + * init — initialize a Clayterm instance in provided memory 6 + * reduce — decode command buffer, run Clay layout, render to ANSI 7 + * output — pointer to output byte buffer 8 + * length — length of output byte buffer 9 + * measure — Clay text measurement callback 10 + * error_count — number of errors from last render 11 + * error_type — Clay_ErrorType enum value for error at index 12 + * error_message_length — byte length of error message at index 13 + * error_message_ptr — pointer to error message string at index 10 14 */ 11 15 12 16 #include "clayterm.h" ··· 34 38 35 39 /* ── Instance state ───────────────────────────────────────────────── */ 36 40 41 + #define MAX_ERRORS 32 42 + 37 43 struct Clayterm { 38 44 int w, h; 39 45 Cell *front; ··· 44 50 /* clip region */ 45 51 int clipx, clipy, clipw, cliph; 46 52 int clipping; 53 + /* error collection */ 54 + Clay_ErrorData errors[MAX_ERRORS]; 55 + int error_count; 47 56 }; 48 57 49 58 /* Memory layout inside the arena provided by the host: ··· 399 408 + align64(clay_bytes); /* Clay arena */ 400 409 } 401 410 402 - static void clay_error(Clay_ErrorData err) { (void)err; } 411 + static void clay_error(Clay_ErrorData err) { 412 + struct Clayterm *ct = (struct Clayterm *)err.userData; 413 + if (ct->error_count < MAX_ERRORS) { 414 + ct->errors[ct->error_count++] = err; 415 + } 416 + } 417 + 418 + int error_count(struct Clayterm *ct) { return ct->error_count; } 419 + 420 + int error_type(struct Clayterm *ct, int index) { 421 + if (index < 0 || index >= ct->error_count) 422 + return -1; 423 + return (int)ct->errors[index].errorType; 424 + } 425 + 426 + int error_message_length(struct Clayterm *ct, int index) { 427 + if (index < 0 || index >= ct->error_count) 428 + return 0; 429 + return ct->errors[index].errorText.length; 430 + } 431 + 432 + int error_message_ptr(struct Clayterm *ct, int index) { 433 + if (index < 0 || index >= ct->error_count) 434 + return 0; 435 + return (int)ct->errors[index].errorText.chars; 436 + } 403 437 404 438 struct Clayterm *init(void *mem, int w, int h) { 405 439 struct Clayterm *ct = (struct Clayterm *)mem; ··· 413 447 Clay_Arena arena = 414 448 Clay_CreateArenaWithCapacityAndMemory(clay_bytes, clay_mem); 415 449 Clay_Initialize(arena, (Clay_Dimensions){(float)w, (float)h}, 416 - (Clay_ErrorHandler){clay_error, 0}); 450 + (Clay_ErrorHandler){clay_error, ct}); 417 451 418 452 *ct = (struct Clayterm){ 419 453 .w = w, ··· 437 471 438 472 void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) { 439 473 int i = 0; 474 + ct->error_count = 0; 440 475 441 476 Clay_BeginLayout(); 442 477
+20
term-native.ts
··· 26 26 setPointer(x: number, y: number, down: boolean): void; 27 27 getPointerOverIds(): string[]; 28 28 getElementBounds(id: string): BoundingBox | undefined; 29 + errorCount(ct: number): number; 30 + errorType(ct: number, index: number): number; 31 + errorMessage(ct: number, index: number): string; 29 32 } 30 33 31 34 import { compiled } from "./wasm.ts"; ··· 80 83 pointer_over_id_string_length(index: number): number; 81 84 pointer_over_id_string_ptr(index: number): number; 82 85 get_element_bounds(name: number, len: number, out: number): number; 86 + error_count(ct: number): number; 87 + error_type(ct: number, index: number): number; 88 + error_message_length(ct: number, index: number): number; 89 + error_message_ptr(ct: number, index: number): number; 83 90 }; 84 91 85 92 let heap = ct.__heap_base.value as number; ··· 137 144 width: view.getFloat32(out + BOUNDING_BOX.width, true), 138 145 height: view.getFloat32(out + BOUNDING_BOX.height, true), 139 146 }; 147 + }, 148 + errorCount(ptr: number): number { 149 + return ct.error_count(ptr); 150 + }, 151 + errorType(ptr: number, index: number): number { 152 + return ct.error_type(ptr, index); 153 + }, 154 + errorMessage(ptr: number, index: number): string { 155 + let len = ct.error_message_length(ptr, index); 156 + if (len === 0) return ""; 157 + let p = ct.error_message_ptr(ptr, index); 158 + let decoder = new TextDecoder(); 159 + return decoder.decode(new Uint8Array(memory.buffer, p, len)); 140 160 }, 141 161 }; 142 162 }
+29 -1
term.ts
··· 38 38 bounds: BoundingBox; 39 39 } 40 40 41 + const ERROR_TYPES = [ 42 + "TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED", 43 + "ARENA_CAPACITY_EXCEEDED", 44 + "ELEMENTS_CAPACITY_EXCEEDED", 45 + "TEXT_MEASUREMENT_CAPACITY_EXCEEDED", 46 + "DUPLICATE_ID", 47 + "FLOATING_CONTAINER_PARENT_NOT_FOUND", 48 + "PERCENTAGE_OVER_1", 49 + "INTERNAL_ERROR", 50 + "UNBALANCED_OPEN_CLOSE", 51 + ] as const; 52 + 53 + export interface ClayError { 54 + type: string; 55 + message: string; 56 + } 57 + 41 58 export interface RenderInfo { 42 59 get(id: string): ElementInfo | undefined; 43 60 } ··· 46 63 output: Uint8Array; 47 64 events: PointerEvent[]; 48 65 info: RenderInfo; 66 + errors: ClayError[]; 49 67 } 50 68 51 69 export interface Term { ··· 124 142 }, 125 143 }; 126 144 127 - return { output, events, info }; 145 + let errors: ClayError[] = []; 146 + let count = native.errorCount(statePtr); 147 + for (let i = 0; i < count; i++) { 148 + let code = native.errorType(statePtr, i); 149 + errors.push({ 150 + type: ERROR_TYPES[code] ?? `UNKNOWN_${code}`, 151 + message: native.errorMessage(statePtr, i), 152 + }); 153 + } 154 + 155 + return { output, events, info, errors }; 128 156 }, 129 157 }; 130 158 }