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

♻️ rename Op discriminant to `directive`, open() `name` to `id`

Restructures the Op type so the discriminant field is `directive`
instead of `id`, freeing `id` for the element identity string. The
`name` parameter on `open()` and `OpenElement` becomes `id`, aligning
with Clay's terminology.

Drops the `idx++` seed from Clay_HashString — element IDs are now
hashed with seed 0 and uniqueness is the caller's responsibility.
Duplicate IDs within a frame are undefined behavior.

Removes INV-7 (element identity disambiguation) from the spec and
updates Sections 8.3.1, 9.3, and 10.3 accordingly.

Closes #16

Charles Lowell (Apr 17, 2026, 4:12 PM -0500) ee7f5bcc 059190e6

+37 -56
+14 -24
ops.ts
··· 11 11 const PROP_CLIP = 0x10; 12 12 const PROP_FLOATING = 0x20; 13 13 14 - /* ── Packing ──────────────────────────────────────────────────────── */ 15 - 16 14 const encoder = new TextEncoder(); 17 15 18 16 function packAxis(view: DataView, offset: number, axis: SizingAxis): number { ··· 73 71 let o = offset; 74 72 75 73 for (let op of ops) { 76 - switch (op.id) { 74 + switch (op.directive) { 77 75 case OP_CLOSE_ELEMENT: 78 - view.setUint32(o, op.id, true); 76 + view.setUint32(o, op.directive, true); 79 77 o += 4; 80 78 break; 81 79 ··· 83 81 view.setUint32(o, OP_OPEN_ELEMENT, true); 84 82 o += 4; 85 83 86 - let id = encoder.encode(op.name); 87 - o = packString(view, id, o); 84 + let bytes = encoder.encode(op.id); 85 + o = packString(view, bytes, o); 88 86 89 87 let mask = 0; 90 88 if (op.layout) mask |= PROP_LAYOUT; ··· 210 208 return (o - offset) / 4; 211 209 } 212 210 213 - /* ── Color ────────────────────────────────────────────────────────── */ 214 - 215 211 export function rgba(r: number, g: number, b: number, a = 255): number { 216 212 return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | 217 213 (b & 0xFF); 218 214 } 219 - 220 - /* ── Sizing axis types ────────────────────────────────────────────── */ 221 215 222 216 export type SizingAxis = 223 217 | { type: "fit"; min?: number; max?: number } ··· 241 235 }); 242 236 export const fixed = (value: number): SizingAxis => ({ type: "fixed", value }); 243 237 244 - /* ── Op descriptors ───────────────────────────────────────────────── */ 245 - 246 238 export interface CloseElement { 247 - id: typeof OP_CLOSE_ELEMENT; 239 + directive: typeof OP_CLOSE_ELEMENT; 248 240 } 249 241 250 242 export interface OpenElement { 251 - id: typeof OP_OPEN_ELEMENT; 252 - name: string; 243 + directive: typeof OP_OPEN_ELEMENT; 244 + id: string; 253 245 layout?: { 254 246 width?: SizingAxis; 255 247 height?: SizingAxis; ··· 280 272 } 281 273 282 274 export interface Text { 283 - id: typeof OP_TEXT; 275 + directive: typeof OP_TEXT; 284 276 content: string; 285 277 color?: number; 286 278 fontSize?: number; ··· 291 283 292 284 export type Op = OpenElement | Text | CloseElement; 293 285 294 - /* ── Descriptor constructors ──────────────────────────────────────── */ 295 - 296 286 export function open( 297 - name: string, 298 - props: Omit<OpenElement, "id" | "name"> = {}, 287 + id: string, 288 + props: Omit<OpenElement, "directive" | "id"> = {}, 299 289 ): OpenElement { 300 - return { id: OP_OPEN_ELEMENT, name, ...props }; 290 + return { directive: OP_OPEN_ELEMENT, id, ...props }; 301 291 } 302 292 303 293 export function text( 304 294 content: string, 305 - props: Omit<Text, "id" | "content"> = {}, 295 + props: Omit<Text, "directive" | "content"> = {}, 306 296 ): Text { 307 - return { id: OP_TEXT, content, ...props }; 297 + return { directive: OP_TEXT, content, ...props }; 308 298 } 309 299 310 300 export function close(): CloseElement { 311 - return { id: OP_CLOSE_ELEMENT }; 301 + return { directive: OP_CLOSE_ELEMENT }; 312 302 }
+10 -18
specs/renderer-spec.md
··· 225 225 symmetric: both calls occur within the same render transaction, in the same 226 226 function scope. 227 227 228 - **INV-7. Element identity disambiguation.** When multiple elements within a 229 - frame share the same id, the renderer MUST disambiguate their identities so that 230 - the layout engine does not conflate them. The disambiguation mechanism is an 231 - implementation detail, but the guarantee is normative: identical ids MUST NOT 232 - cause layout corruption or element conflation. 233 - 234 - **INV-8. Separation of concerns.** The rendering concern and the input-parsing 228 + **INV-7. Separation of concerns.** The rendering concern and the input-parsing 235 229 concern MUST remain independent. Neither MUST depend on the other's state, 236 230 types, or API surface. They MAY share a compiled WASM binary for loading 237 231 efficiency, but this is an implementation convenience, not an architectural ··· 374 368 375 369 Creates an element-open directive. The `id` parameter provides an identity for 376 370 the element within the frame, used by the underlying layout engine for element 377 - tracking and hit-testing. The optional `props` parameter carries configuration 371 + tracking and hit-testing. IDs MUST be unique within a frame; passing duplicate 372 + IDs is undefined behavior. The optional `props` parameter carries configuration 378 373 for layout, styling, and behavior. 379 374 380 375 Elements opened with `open()` MUST be closed with a corresponding `close()` ··· 473 468 474 469 ### 9.3 Directive identity 475 470 476 - Each element directive is assigned an identity within the frame for use by the 477 - underlying layout engine. When multiple elements share the same id (the `id` 478 - parameter to `open()`), the renderer MUST disambiguate their identities 479 - automatically. The disambiguation mechanism is an implementation detail. The 480 - normative requirement is that the caller MUST NOT need to provide globally 481 - unique ids; the renderer handles uniqueness internally. 471 + Each element directive carries an `id` provided by the caller via `open()`. 472 + Element IDs MUST be unique within a frame. The renderer uses the ID directly as 473 + the element's identity for the layout engine. Passing duplicate IDs within a 474 + single frame is undefined behavior. 482 475 483 476 --- 484 477 ··· 501 494 502 495 ### 10.3 Element identity within a frame 503 496 504 - Within a single frame, each element MUST have an unambiguous identity for the 505 - layout engine. As specified in Section 9.3, the renderer handles disambiguation. 506 - Two elements with the same id in the same frame MUST NOT cause layout 507 - corruption, hash collision, or identity conflation. 497 + Within a single frame, each element MUST have a unique identity for the layout 498 + engine. As specified in Section 9.3, element IDs MUST be unique within a frame. 499 + Passing duplicate IDs is undefined behavior. 508 500 509 501 ### 10.4 No cross-frame identity 510 502
+1 -2
src/clayterm.c
··· 437 437 438 438 void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) { 439 439 int i = 0; 440 - uint32_t idx = 0; 441 440 442 441 Clay_BeginLayout(); 443 442 ··· 454 453 455 454 if (id_len > 0) { 456 455 Clay_String str = {.length = (int32_t)id_len, .chars = id_chars}; 457 - Clay_ElementId eid = Clay__HashString(str, idx++); 456 + Clay_ElementId eid = Clay__HashString(str, 0); 458 457 Clay__OpenElementWithId(eid); 459 458 } else { 460 459 Clay__OpenElement();
+7 -7
test/validate.test.ts
··· 19 19 expect(validate([])).toBe(true); 20 20 }); 21 21 22 - it("rejects ops with wrong id", () => { 23 - expect(validate([{ id: 0xff }])).toBe(false); 22 + it("rejects ops with wrong directive", () => { 23 + expect(validate([{ directive: 0xff }])).toBe(false); 24 24 }); 25 25 26 - it("rejects open element missing name", () => { 27 - expect(validate([{ id: 0x02 }])).toBe(false); 26 + it("rejects open element missing id", () => { 27 + expect(validate([{ directive: 0x02 }])).toBe(false); 28 28 }); 29 29 30 30 it("rejects text missing content", () => { 31 - expect(validate([{ id: 0x03 }])).toBe(false); 31 + expect(validate([{ directive: 0x03 }])).toBe(false); 32 32 }); 33 33 34 34 it("rejects non-array", () => { ··· 40 40 }); 41 41 42 42 it("assert throws TypeError on bad input", () => { 43 - expect(() => assert([{ id: 0x02 }])).toThrow(TypeError); 43 + expect(() => assert([{ directive: 0x02 }])).toThrow(TypeError); 44 44 }); 45 45 46 46 it("rejects padding > 255 (u8 overflow)", () => { ··· 106 106 107 107 it("throws on invalid ops", () => { 108 108 // deno-lint-ignore no-explicit-any 109 - expect(() => term.render([{ id: 0xff }] as any)).toThrow(TypeError); 109 + expect(() => term.render([{ directive: 0xff }] as any)).toThrow(TypeError); 110 110 }); 111 111 });
+5 -5
validate.ts
··· 89 89 zIndex: Type.Optional(u16), 90 90 }); 91 91 92 - /* ── Op types (discriminated on `id`) ─────────────────────────────── */ 92 + /* ── Op types (discriminated on `directive`) ──────────────────────── */ 93 93 94 - const CloseElement = Type.Object({ id: Type.Literal(0x04) }); 94 + const CloseElement = Type.Object({ directive: Type.Literal(0x04) }); 95 95 96 96 const OpenElement = Type.Object({ 97 - id: Type.Literal(0x02), 98 - name: Type.String(), 97 + directive: Type.Literal(0x02), 98 + id: Type.String(), 99 99 layout: Type.Optional(Layout), 100 100 bg: Type.Optional(rgba), 101 101 cornerRadius: Type.Optional(CornerRadius), ··· 105 105 }); 106 106 107 107 const TextOp = Type.Object({ 108 - id: Type.Literal(0x03), 108 + directive: Type.Literal(0x03), 109 109 content: Type.String(), 110 110 color: Type.Optional(rgba), 111 111 fontSize: Type.Optional(u8),