Skip to content

Commit cef9c8b

Browse files
authored
Merge pull request #23 from thefrontside/feat/render-errors
Surface Clay layout errors on RenderResult
2 parents a1ae74c + 994adca commit cef9c8b

5 files changed

Lines changed: 110 additions & 4 deletions

File tree

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,9 @@
3333

3434
- Directives are plain objects. No classes, no methods, no prototype chains. The
3535
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.

specs/renderer-spec.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ prevent overlap.
631631
### 12.3 Render return type
632632

633633
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[] }`.
635635

636636
The `output` field is the ANSI byte output specified normatively in Section 7.3
637637
and Section 8.2.
@@ -671,6 +671,25 @@ the top-left corner of the layout root.
671671
Querying an element with an empty-string id or an id not present in the frame
672672
returns `undefined`.
673673

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.
692+
674693
The return type of `render()` has changed twice since the project's inception
675694
(string, then `Uint8Array`, then `RenderResult`). While the ANSI bytes
676695
commitment (Section 7.3) is stable, the wrapper shape around those bytes is not.

src/clayterm.c

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* output — pointer to output byte buffer
88
* length — length of output byte buffer
99
* measure — Clay text measurement callback
10+
* error_count, error_type, error_message_length, error_message_ptr
11+
* — per-render Clay error accessors
1012
*/
1113

1214
#include "clayterm.h"
@@ -34,6 +36,8 @@
3436

3537
/* ── Instance state ───────────────────────────────────────────────── */
3638

39+
#define MAX_ERRORS 32
40+
3741
struct Clayterm {
3842
int w, h;
3943
Cell *front;
@@ -44,6 +48,9 @@ struct Clayterm {
4448
/* clip region */
4549
int clipx, clipy, clipw, cliph;
4650
int clipping;
51+
/* error collection */
52+
Clay_ErrorData errors[MAX_ERRORS];
53+
int error_count;
4754
};
4855

4956
/* Memory layout inside the arena provided by the host:
@@ -399,7 +406,32 @@ int clayterm_size(int w, int h) {
399406
+ align64(clay_bytes); /* Clay arena */
400407
}
401408

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+
}
403435

404436
struct Clayterm *init(void *mem, int w, int h) {
405437
struct Clayterm *ct = (struct Clayterm *)mem;
@@ -413,7 +445,7 @@ struct Clayterm *init(void *mem, int w, int h) {
413445
Clay_Arena arena =
414446
Clay_CreateArenaWithCapacityAndMemory(clay_bytes, clay_mem);
415447
Clay_Initialize(arena, (Clay_Dimensions){(float)w, (float)h},
416-
(Clay_ErrorHandler){clay_error, 0});
448+
(Clay_ErrorHandler){clay_error, ct});
417449

418450
*ct = (struct Clayterm){
419451
.w = w,
@@ -437,6 +469,7 @@ struct Clayterm *init(void *mem, int w, int h) {
437469

438470
void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) {
439471
int i = 0;
472+
ct->error_count = 0;
440473

441474
Clay_BeginLayout();
442475

term-native.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export interface Native {
2626
setPointer(x: number, y: number, down: boolean): void;
2727
getPointerOverIds(): string[];
2828
getElementBounds(id: string): BoundingBox | undefined;
29+
errorCount(ct: number): number;
30+
errorType(ct: number, index: number): number;
31+
errorMessage(ct: number, index: number): string;
2932
}
3033

3134
import { compiled } from "./wasm.ts";
@@ -80,6 +83,10 @@ export async function createTermNative(
8083
pointer_over_id_string_length(index: number): number;
8184
pointer_over_id_string_ptr(index: number): number;
8285
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;
8390
};
8491

8592
let heap = ct.__heap_base.value as number;
@@ -138,5 +145,18 @@ export async function createTermNative(
138145
height: view.getFloat32(out + BOUNDING_BOX.height, true),
139146
};
140147
},
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));
160+
},
141161
};
142162
}

term.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,23 @@ export interface ElementInfo {
3838
bounds: BoundingBox;
3939
}
4040

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+
4158
export interface RenderInfo {
4259
get(id: string): ElementInfo | undefined;
4360
}
@@ -46,6 +63,7 @@ export interface RenderResult {
4663
output: Uint8Array;
4764
events: PointerEvent[];
4865
info: RenderInfo;
66+
errors: ClayError[];
4967
}
5068

5169
export interface Term {
@@ -124,7 +142,17 @@ export async function createTerm(options: TermOptions): Promise<Term> {
124142
},
125143
};
126144

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 };
128156
},
129157
};
130158
}

0 commit comments

Comments
 (0)