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

Merge pull request #23 from thefrontside/feat/render-errors

Surface Clay layout errors on RenderResult

authored by

Charles Lowell and committed by
GitHub
(Apr 17, 2026, 5:05 PM -0500) cef9c8b7 a1ae74cf

+110 -4
+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
+35 -2
src/clayterm.c
··· 7 7 * output — pointer to output byte buffer 8 8 * length — length of output byte buffer 9 9 * measure — Clay text measurement callback 10 + * error_count, error_type, error_message_length, error_message_ptr 11 + * — per-render Clay error accessors 10 12 */ 11 13 12 14 #include "clayterm.h" ··· 34 36 35 37 /* ── Instance state ───────────────────────────────────────────────── */ 36 38 39 + #define MAX_ERRORS 32 40 + 37 41 struct Clayterm { 38 42 int w, h; 39 43 Cell *front; ··· 44 48 /* clip region */ 45 49 int clipx, clipy, clipw, cliph; 46 50 int clipping; 51 + /* error collection */ 52 + Clay_ErrorData errors[MAX_ERRORS]; 53 + int error_count; 47 54 }; 48 55 49 56 /* Memory layout inside the arena provided by the host: ··· 399 406 + align64(clay_bytes); /* Clay arena */ 400 407 } 401 408 402 - static void clay_error(Clay_ErrorData err) { (void)err; } 409 + static void clay_error(Clay_ErrorData err) { 410 + struct Clayterm *ct = (struct Clayterm *)err.userData; 411 + if (ct->error_count < MAX_ERRORS) { 412 + ct->errors[ct->error_count++] = err; 413 + } 414 + } 415 + 416 + int error_count(struct Clayterm *ct) { return ct->error_count; } 417 + 418 + int error_type(struct Clayterm *ct, int index) { 419 + if (index < 0 || index >= ct->error_count) 420 + return -1; 421 + return (int)ct->errors[index].errorType; 422 + } 423 + 424 + int error_message_length(struct Clayterm *ct, int index) { 425 + if (index < 0 || index >= ct->error_count) 426 + return 0; 427 + return ct->errors[index].errorText.length; 428 + } 429 + 430 + int error_message_ptr(struct Clayterm *ct, int index) { 431 + if (index < 0 || index >= ct->error_count) 432 + return 0; 433 + return (int)ct->errors[index].errorText.chars; 434 + } 403 435 404 436 struct Clayterm *init(void *mem, int w, int h) { 405 437 struct Clayterm *ct = (struct Clayterm *)mem; ··· 413 445 Clay_Arena arena = 414 446 Clay_CreateArenaWithCapacityAndMemory(clay_bytes, clay_mem); 415 447 Clay_Initialize(arena, (Clay_Dimensions){(float)w, (float)h}, 416 - (Clay_ErrorHandler){clay_error, 0}); 448 + (Clay_ErrorHandler){clay_error, ct}); 417 449 418 450 *ct = (struct Clayterm){ 419 451 .w = w, ··· 437 469 438 470 void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) { 439 471 int i = 0; 472 + ct->error_count = 0; 440 473 441 474 Clay_BeginLayout(); 442 475
+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 }