From ab1bd20b4c31376d307825b593d5dc2da11a606a Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 17 Apr 2026 16:59:05 -0500 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20surface=20Clay=20layout=20error?= =?UTF-8?q?s=20on=20RenderResult?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- AGENTS.md | 6 +++++ specs/renderer-spec.md | 21 ++++++++++++++++- src/clayterm.c | 51 +++++++++++++++++++++++++++++++++++------- term-native.ts | 20 +++++++++++++++++ term.ts | 30 ++++++++++++++++++++++++- 5 files changed, 118 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5685f3a..01f0677 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,3 +33,9 @@ - Directives are plain objects. No classes, no methods, no prototype chains. The flat array pattern is normative. + +## C code conventions + +- No global mutable state. All state belongs on a struct instance (e.g. + `Clayterm`). Use Clay's `userData` pointer or similar mechanisms to route + callbacks back to the owning instance. diff --git a/specs/renderer-spec.md b/specs/renderer-spec.md index ac9c5f3..fa4276a 100644 --- a/specs/renderer-spec.md +++ b/specs/renderer-spec.md @@ -631,7 +631,7 @@ prevent overlap. ### 12.3 Render return type The `render()` method currently returns a `RenderResult` object shaped as -`{ output: Uint8Array, events: PointerEvent[], info: RenderInfo }`. +`{ output: Uint8Array, events: PointerEvent[], info: RenderInfo, errors: ClayError[] }`. The `output` field is the ANSI byte output specified normatively in Section 7.3 and Section 8.2. @@ -671,6 +671,25 @@ the top-left corner of the layout root. Querying an element with an empty-string id or an id not present in the frame returns `undefined`. +The `errors` field contains any errors reported by the Clay layout engine during +the most recent `render()` call. Each error is a `ClayError` object with: + +- `type`: a string identifying the error category. The following types are + defined, matching Clay's error taxonomy: + - `"TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED"` + - `"ARENA_CAPACITY_EXCEEDED"` + - `"ELEMENTS_CAPACITY_EXCEEDED"` + - `"TEXT_MEASUREMENT_CAPACITY_EXCEEDED"` + - `"DUPLICATE_ID"` + - `"FLOATING_CONTAINER_PARENT_NOT_FOUND"` + - `"PERCENTAGE_OVER_1"` + - `"INTERNAL_ERROR"` + - `"UNBALANCED_OPEN_CLOSE"` +- `message`: a human-readable string describing the error in detail. + +Errors are collected per-render; each call to `render()` returns only the errors +from that invocation. The array is empty when no errors occurred. + The return type of `render()` has changed twice since the project's inception (string, then `Uint8Array`, then `RenderResult`). While the ANSI bytes commitment (Section 7.3) is stable, the wrapper shape around those bytes is not. diff --git a/src/clayterm.c b/src/clayterm.c index fa1d4c0..eebeea7 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -1,12 +1,16 @@ /* clayterm.c — WASM terminal rendering engine for Clay UI * * Public API (exported to WASM): - * clayterm_size — compute arena size for given dimensions - * init — initialize a Clayterm instance in provided memory - * reduce — decode command buffer, run Clay layout, render to ANSI - * output — pointer to output byte buffer - * length — length of output byte buffer - * measure — Clay text measurement callback + * clayterm_size — compute arena size for given dimensions + * init — initialize a Clayterm instance in provided memory + * reduce — decode command buffer, run Clay layout, render to ANSI + * output — pointer to output byte buffer + * length — length of output byte buffer + * measure — Clay text measurement callback + * error_count — number of errors from last render + * error_type — Clay_ErrorType enum value for error at index + * error_message_length — byte length of error message at index + * error_message_ptr — pointer to error message string at index */ #include "clayterm.h" @@ -34,6 +38,8 @@ /* ── Instance state ───────────────────────────────────────────────── */ +#define MAX_ERRORS 32 + struct Clayterm { int w, h; Cell *front; @@ -44,6 +50,9 @@ struct Clayterm { /* clip region */ int clipx, clipy, clipw, cliph; int clipping; + /* error collection */ + Clay_ErrorData errors[MAX_ERRORS]; + int error_count; }; /* Memory layout inside the arena provided by the host: @@ -399,7 +408,32 @@ int clayterm_size(int w, int h) { + align64(clay_bytes); /* Clay arena */ } -static void clay_error(Clay_ErrorData err) { (void)err; } +static void clay_error(Clay_ErrorData err) { + struct Clayterm *ct = (struct Clayterm *)err.userData; + if (ct->error_count < MAX_ERRORS) { + ct->errors[ct->error_count++] = err; + } +} + +int error_count(struct Clayterm *ct) { return ct->error_count; } + +int error_type(struct Clayterm *ct, int index) { + if (index < 0 || index >= ct->error_count) + return -1; + return (int)ct->errors[index].errorType; +} + +int error_message_length(struct Clayterm *ct, int index) { + if (index < 0 || index >= ct->error_count) + return 0; + return ct->errors[index].errorText.length; +} + +int error_message_ptr(struct Clayterm *ct, int index) { + if (index < 0 || index >= ct->error_count) + return 0; + return (int)ct->errors[index].errorText.chars; +} struct Clayterm *init(void *mem, int w, int h) { struct Clayterm *ct = (struct Clayterm *)mem; @@ -413,7 +447,7 @@ struct Clayterm *init(void *mem, int w, int h) { Clay_Arena arena = Clay_CreateArenaWithCapacityAndMemory(clay_bytes, clay_mem); Clay_Initialize(arena, (Clay_Dimensions){(float)w, (float)h}, - (Clay_ErrorHandler){clay_error, 0}); + (Clay_ErrorHandler){clay_error, ct}); *ct = (struct Clayterm){ .w = w, @@ -437,6 +471,7 @@ struct Clayterm *init(void *mem, int w, int h) { void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) { int i = 0; + ct->error_count = 0; Clay_BeginLayout(); diff --git a/term-native.ts b/term-native.ts index 84349c9..40e646d 100644 --- a/term-native.ts +++ b/term-native.ts @@ -26,6 +26,9 @@ export interface Native { setPointer(x: number, y: number, down: boolean): void; getPointerOverIds(): string[]; getElementBounds(id: string): BoundingBox | undefined; + errorCount(ct: number): number; + errorType(ct: number, index: number): number; + errorMessage(ct: number, index: number): string; } import { compiled } from "./wasm.ts"; @@ -80,6 +83,10 @@ export async function createTermNative( pointer_over_id_string_length(index: number): number; pointer_over_id_string_ptr(index: number): number; get_element_bounds(name: number, len: number, out: number): number; + error_count(ct: number): number; + error_type(ct: number, index: number): number; + error_message_length(ct: number, index: number): number; + error_message_ptr(ct: number, index: number): number; }; let heap = ct.__heap_base.value as number; @@ -138,5 +145,18 @@ export async function createTermNative( height: view.getFloat32(out + BOUNDING_BOX.height, true), }; }, + errorCount(ptr: number): number { + return ct.error_count(ptr); + }, + errorType(ptr: number, index: number): number { + return ct.error_type(ptr, index); + }, + errorMessage(ptr: number, index: number): string { + let len = ct.error_message_length(ptr, index); + if (len === 0) return ""; + let p = ct.error_message_ptr(ptr, index); + let decoder = new TextDecoder(); + return decoder.decode(new Uint8Array(memory.buffer, p, len)); + }, }; } diff --git a/term.ts b/term.ts index 3e2193c..12517d0 100644 --- a/term.ts +++ b/term.ts @@ -38,6 +38,23 @@ export interface ElementInfo { bounds: BoundingBox; } +const ERROR_TYPES = [ + "TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED", + "ARENA_CAPACITY_EXCEEDED", + "ELEMENTS_CAPACITY_EXCEEDED", + "TEXT_MEASUREMENT_CAPACITY_EXCEEDED", + "DUPLICATE_ID", + "FLOATING_CONTAINER_PARENT_NOT_FOUND", + "PERCENTAGE_OVER_1", + "INTERNAL_ERROR", + "UNBALANCED_OPEN_CLOSE", +] as const; + +export interface ClayError { + type: string; + message: string; +} + export interface RenderInfo { get(id: string): ElementInfo | undefined; } @@ -46,6 +63,7 @@ export interface RenderResult { output: Uint8Array; events: PointerEvent[]; info: RenderInfo; + errors: ClayError[]; } export interface Term { @@ -124,7 +142,17 @@ export async function createTerm(options: TermOptions): Promise { }, }; - return { output, events, info }; + let errors: ClayError[] = []; + let count = native.errorCount(statePtr); + for (let i = 0; i < count; i++) { + let code = native.errorType(statePtr, i); + errors.push({ + type: ERROR_TYPES[code] ?? `UNKNOWN_${code}`, + message: native.errorMessage(statePtr, i), + }); + } + + return { output, events, info, errors }; }, }; } From 994adca383442bbf27cb98979b07c5ff82e2fb9b Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 17 Apr 2026 17:03:15 -0500 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8E=A8=20fix=20clang-format=20in=20AP?= =?UTF-8?q?I=20comment=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/clayterm.c | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/clayterm.c b/src/clayterm.c index eebeea7..2af9afd 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -1,16 +1,14 @@ /* clayterm.c — WASM terminal rendering engine for Clay UI * * Public API (exported to WASM): - * clayterm_size — compute arena size for given dimensions - * init — initialize a Clayterm instance in provided memory - * reduce — decode command buffer, run Clay layout, render to ANSI - * output — pointer to output byte buffer - * length — length of output byte buffer - * measure — Clay text measurement callback - * error_count — number of errors from last render - * error_type — Clay_ErrorType enum value for error at index - * error_message_length — byte length of error message at index - * error_message_ptr — pointer to error message string at index + * clayterm_size — compute arena size for given dimensions + * init — initialize a Clayterm instance in provided memory + * reduce — decode command buffer, run Clay layout, render to ANSI + * output — pointer to output byte buffer + * length — length of output byte buffer + * measure — Clay text measurement callback + * error_count, error_type, error_message_length, error_message_ptr + * — per-render Clay error accessors */ #include "clayterm.h"