Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
21 changes: 20 additions & 1 deletion specs/renderer-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
37 changes: 35 additions & 2 deletions src/clayterm.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -34,6 +36,8 @@

/* ── Instance state ───────────────────────────────────────────────── */

#define MAX_ERRORS 32

struct Clayterm {
int w, h;
Cell *front;
Expand All @@ -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:
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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();

Expand Down
20 changes: 20 additions & 0 deletions term-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
},
};
}
30 changes: 29 additions & 1 deletion term.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -46,6 +63,7 @@ export interface RenderResult {
output: Uint8Array;
events: PointerEvent[];
info: RenderInfo;
errors: ClayError[];
}

export interface Term {
Expand Down Expand Up @@ -124,7 +142,17 @@ export async function createTerm(options: TermOptions): Promise<Term> {
},
};

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 };
},
};
}
Loading