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..2af9afd 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -7,6 +7,8 @@ * 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" @@ -34,6 +36,8 @@ /* ── Instance state ───────────────────────────────────────────────── */ +#define MAX_ERRORS 32 + struct Clayterm { int w, h; Cell *front; @@ -44,6 +48,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 +406,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 +445,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 +469,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 }; }, }; }