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

Merge pull request #5 from thefrontside/docs/add-clayterm-spec

Add Clayterm current-state specification

authored by

Charles Lowell and committed by
GitHub
(Apr 17, 2026, 1:40 PM -0500) 6c786779 a6b1ee1e

+982
+168
specs/input-spec.md
··· 1 + # Clayterm Input Specification 2 + 3 + **Version:** 0.1 (draft) **Status:** Current-state specification. Descriptive 4 + for the input parsing surface. 5 + 6 + --- 7 + 8 + ## 1. Purpose 9 + 10 + This specification describes Clayterm's terminal input parsing surface: the API 11 + for decoding raw terminal byte sequences into structured events. 12 + 13 + Input parsing is architecturally independent from rendering (see 14 + [Renderer Specification](renderer-spec.md), INV-8). The two concerns share a 15 + compiled WASM binary for loading efficiency, but neither depends on the other's 16 + state, types, or API surface. 17 + 18 + This specification is currently non-normative. The input API has clear design 19 + intent but has undergone more revision than the rendering core and faces known 20 + upcoming forces that will reshape it (Kitty progressive enhancement field 21 + surfacing, terminfo binary parsing). It is written to document the current 22 + surface and guide future stabilization. 23 + 24 + --- 25 + 26 + ## 2. Scope 27 + 28 + ### In scope (descriptive) 29 + 30 + - The input parser creation and lifecycle 31 + - The scan API and its return type 32 + - The `InputEvent` discriminated union and its variants 33 + - The ESC timeout resolution model 34 + 35 + ### Out of scope 36 + 37 + - Rendering (see [Renderer Specification](renderer-spec.md)) 38 + - Pointer hit detection (owned by the render loop; see Renderer Specification, 39 + Section 12.4) 40 + - Higher-level event routing, focus management, or keybinding systems 41 + 42 + --- 43 + 44 + ## 3. Terminology 45 + 46 + **Input parser.** A WASM-backed instance that accepts raw terminal bytes and 47 + produces structured events. Each parser maintains its own internal state for 48 + multi-byte sequence buffering and ESC timeout tracking. 49 + 50 + **Scan.** A single invocation of the parser. The caller provides raw bytes (or 51 + no bytes, for timeout resolution), and the parser returns any events it can 52 + produce along with pending-timeout information. 53 + 54 + **InputEvent.** A discriminated union representing a single parsed terminal 55 + event. Discriminated on a `type` field. 56 + 57 + --- 58 + 59 + ## 4. Input Parser API 60 + 61 + ### 4.1 Parser creation 62 + 63 + ``` 64 + createInput(options?): Promise<Input> 65 + ``` 66 + 67 + Creates an input parser instance. The returned promise resolves when the WASM 68 + module is ready. 69 + 70 + Options: 71 + 72 + - **`escLatency`** — Milliseconds to wait before resolving a lone ESC byte as 73 + the Escape key. Default: 25ms. This controls the tradeoff between 74 + responsiveness (lower values) and correct disambiguation of ESC-prefixed 75 + sequences (higher values). 76 + 77 + - **`terminfo`** — A `Uint8Array` of raw terminfo binary. Accepted but C-side 78 + parsing is not yet implemented. 79 + 80 + ### 4.2 Scan 81 + 82 + ``` 83 + input.scan(bytes?: Uint8Array): ScanResult 84 + ``` 85 + 86 + Feeds raw terminal bytes into the parser and returns parsed events. The `bytes` 87 + parameter is optional; calling without arguments triggers a rescan for ESC 88 + timeout resolution. 89 + 90 + The parser is synchronous: it processes all provided bytes in a single call and 91 + returns immediately. 92 + 93 + ### 4.3 ScanResult 94 + 95 + ``` 96 + { events: InputEvent[], pending?: { delay: number, deadline: number } } 97 + ``` 98 + 99 + - **`events`** — An array of parsed events produced from the provided bytes (and 100 + any previously buffered bytes that could now be resolved). 101 + 102 + - **`pending`** — When present, indicates that an ambiguous ESC byte is buffered 103 + and the parser cannot yet determine whether it begins an escape sequence or is 104 + a standalone Escape keypress. The caller SHOULD schedule a rescan (calling 105 + `scan()` with no arguments) after the indicated delay. The `delay` field is a 106 + relative duration in milliseconds. The `deadline` field is an absolute 107 + timestamp (milliseconds since epoch) for the same point in time. 108 + 109 + --- 110 + 111 + ## 5. InputEvent Types 112 + 113 + The `InputEvent` discriminated union is discriminated on a `type` field. The 114 + current variants are: 115 + 116 + - **`KeyEvent`** (`type: "keydown" | "keyup" | "keyrepeat"`) — A keyboard event 117 + for special keys, control sequences, and modifier combinations. Fields include 118 + `key` (logical key name), `code` (physical key identifier), and modifier flags 119 + (`shift`, `ctrl`, `alt`, `meta`). 120 + 121 + - **`MouseEvent`** (`type: "mousedown" | "mouseup"`) — A mouse button press or 122 + release. Fields include `x`, `y` (cell coordinates), `button`, and modifier 123 + flags. 124 + 125 + - **`WheelEvent`** (`type: "wheel"`) — A scroll event. Fields include `x`, `y`, 126 + and scroll direction. 127 + 128 + - **`ResizeEvent`** (`type: "resize"`) — A terminal resize notification. Fields 129 + include `columns` and `rows`. 130 + 131 + The discriminant values and the type splits are deliberate design decisions. 132 + However, the field sets within each variant are expected to grow when Kitty 133 + progressive enhancement types are surfaced in the TypeScript layer (the C struct 134 + has already been extended with fields that are not yet mapped to the TS types). 135 + 136 + --- 137 + 138 + ## 6. Deferred / Future Areas 139 + 140 + _These topics are explicitly excluded from this specification. Their omission is 141 + intentional, not an oversight._ 142 + 143 + **Full Kitty progressive enhancement event types.** The C-side input parser 144 + struct has been extended for progressive enhancement fields. The TypeScript 145 + event types have not been updated to surface them. 146 + 147 + **Terminfo binary parsing.** The input API accepts a `terminfo` option, but 148 + C-side parsing is not implemented. 149 + 150 + **Whether input parsing should be a separate package.** Architecturally 151 + independent from the renderer but currently co-located. The distribution 152 + decision is open. 153 + 154 + --- 155 + 156 + ## Open Decisions 157 + 158 + 1. **What are the normative Kitty progressive enhancement event types?** The 159 + C-side struct has been extended. The TypeScript types have not been updated. 160 + This specification does not attempt to predict the final shapes. 161 + 162 + 2. **Should the input API be a separate package?** It is architecturally 163 + independent from the renderer (INV-8) but currently co-located in the same 164 + module. 165 + 166 + 3. **Is the input API ready for normative specification?** The API has clear 167 + design ownership but has undergone more revision than the rendering core. 168 + This specification documents the current surface without freezing it.
+814
specs/renderer-spec.md
··· 1 + # Clayterm Renderer Specification 2 + 3 + **Version:** 0.1 (draft) **Status:** Current-state specification. Normative for 4 + the rendering contract. Descriptive for settling surfaces. 5 + 6 + --- 7 + 8 + ## 1. Purpose 9 + 10 + Clayterm is a terminal rendering engine. It accepts a declarative description of 11 + a terminal UI layout, performs layout computation and cell-level diffing 12 + internally, and returns ANSI escape byte sequences suitable for direct write to 13 + a terminal output stream. 14 + 15 + This specification defines Clayterm's current-state rendering contract: its 16 + architectural model, its invariants, its stable public API surface, and its 17 + intentional boundaries. It is written to allow future feature work to extend the 18 + project without destabilizing the core. 19 + 20 + This specification does not attempt to define areas of Clayterm that are still 21 + settling. Where the project has working but evolving surfaces — including the 22 + pointer event model and certain wrapper types — those are described in Section 23 + 12 as current implementation rather than normative contract. 24 + 25 + Input parsing is specified separately in the 26 + [Clayterm Input Specification](input-spec.md). 27 + 28 + --- 29 + 30 + ## 2. Scope 31 + 32 + ### In scope (normative) 33 + 34 + - The rendering pipeline and its architectural commitments 35 + - The frame-snapshot rendering model 36 + - The stable public rendering API 37 + - The directive model and core helpers 38 + - Element identity and frame semantics 39 + - Boundary responsibilities (what Clayterm owns and what it does not) 40 + 41 + ### In scope (non-normative, descriptive) 42 + 43 + - Current implementation surfaces that are settling but not yet stable enough to 44 + freeze (Section 12) 45 + - Implementation notes that aid understanding but do not define contract 46 + (Section 13) 47 + 48 + ### Out of scope 49 + 50 + - Internal C code organization, function names, or file structure 51 + - WASM memory layout or compilation details beyond behavioral requirements 52 + - Performance targets or benchmark methodology 53 + - Packaging, CI, or distribution workflow details 54 + - Higher-level UI framework concerns (e.g., component lifecycle, reconciliation) 55 + - Demo applications 56 + - The crankterm project or any specific framework built on Clayterm 57 + - Input parsing (see [Clayterm Input Specification](input-spec.md)) 58 + 59 + --- 60 + 61 + ## 3. Terminology 62 + 63 + **Frame.** A single, complete rendering pass. Each frame begins with the caller 64 + providing directives and ends with the renderer returning ANSI bytes. Frames are 65 + independent; the renderer carries no UI tree state between them. 66 + 67 + **Directive (op).** A plain object that declares one element of the UI tree for 68 + a single frame. Directives are typed by an identifier field and carry layout, 69 + styling, and content properties. The set of directives for a frame is ordered 70 + and forms an implicit tree via open/close pairing. 71 + 72 + **Directive array.** An ordered array of directives constituting a complete 73 + frame description. The array is the input to the rendering transaction. 74 + 75 + **Render transaction.** The process of accepting a directive array, performing 76 + layout, walking render commands, diffing against the previous frame's cell 77 + buffer, and producing ANSI byte output. A render transaction is a single, 78 + synchronous operation from the caller's perspective. 79 + 80 + **ANSI bytes.** A byte sequence of UTF-8–encoded ANSI escape codes and text 81 + content that, when written to a terminal file descriptor, produces the visual 82 + output described by the frame's directives. ANSI bytes include 83 + cursor-positioning sequences, SGR (Select Graphic Rendition) attribute 84 + sequences, and UTF-8 text. 85 + 86 + **Renderer core.** The WASM module and its TS entry points that together 87 + implement the render transaction. The renderer core owns layout computation, 88 + render-command walking, cell-buffer diffing, and ANSI byte generation. 89 + 90 + **Caller.** Any code that invokes Clayterm's public API to produce terminal 91 + output. The caller owns terminal setup, IO, input handling, and application 92 + lifecycle. 93 + 94 + **Higher-level framework.** A component model, reconciler, or application 95 + framework built on top of Clayterm. Examples include crankterm. Clayterm has no 96 + dependency on any higher-level framework, and this specification does not 97 + constrain their design. 98 + 99 + **Term.** An instance of the Clayterm renderer, bound to specific terminal 100 + dimensions. A Term is the object through which the caller performs render 101 + transactions. 102 + 103 + --- 104 + 105 + ## 4. Architectural Model 106 + 107 + _This section is normative._ 108 + 109 + ### 4.1 Pipeline 110 + 111 + Clayterm implements a rendering pipeline with the following stages: 112 + 113 + 1. **Directive acceptance.** The caller provides a complete directive array 114 + representing the desired UI state for a single frame. 115 + 116 + 2. **Transfer.** The renderer transfers the frame description into the WASM 117 + module. The transfer mechanism is an implementation detail. The normative 118 + requirement is that the transfer occurs as part of a single render 119 + transaction; the caller does not interact with the transfer mechanism 120 + directly. 121 + 122 + 3. **Render transaction.** The WASM module processes the frame description. 123 + Internally, it drives a layout engine to compute element positions and sizes, 124 + walks the resulting render commands to populate a cell buffer, and diffs the 125 + cell buffer against the previous frame. 126 + 127 + 4. **Output generation.** For each cell that differs from the previous frame, 128 + the renderer emits ANSI escape sequences (cursor positioning, color 129 + attributes, and text) into an output buffer. 130 + 131 + 5. **Output retrieval.** The caller reads the ANSI byte output. 132 + 133 + ### 4.2 Single-transaction rendering 134 + 135 + A frame MUST be rendered in a single transaction that crosses the TS→WASM 136 + boundary once. The caller provides the complete directive array, invokes the 137 + render transaction, and reads the output. There are no intermediate callbacks, 138 + yields, or partial results. 139 + 140 + ### 4.3 Frame-snapshot model 141 + 142 + Each render transaction operates on a complete, self-contained snapshot of the 143 + UI. The renderer MUST NOT maintain an internal component tree or UI state across 144 + frames. The only state the renderer retains between frames is the cell buffer 145 + used for diffing, which is an implementation detail of output minimization and 146 + not observable to the caller except through reduced output size. 147 + 148 + ### 4.4 Double-buffered diffing 149 + 150 + The renderer maintains two cell buffers: a front buffer (the previously rendered 151 + frame) and a back buffer (the frame being rendered). After populating the back 152 + buffer from the current frame's render commands, the renderer compares it 153 + against the front buffer and emits ANSI bytes only for cells that differ. 154 + Changed cells are then copied from the back buffer to the front buffer so that 155 + both buffers are identical at the end of the transaction. This mechanism is 156 + internal to the renderer and not directly observable to the caller. 157 + 158 + --- 159 + 160 + ## 5. Contract Layer Boundary 161 + 162 + _This section is normative._ 163 + 164 + This specification defines the **architectural rendering contract**: the 165 + commitments that make Clayterm what it is and that callers and framework authors 166 + can depend on. 167 + 168 + This specification **does not** define the following as normative: 169 + 170 + - **The internal transfer encoding.** The mechanism by which directives are 171 + serialized for the WASM module — its byte format, opcode structure, and field 172 + encoding — is an implementation detail. The normative commitment is that the 173 + transfer happens within a single render transaction; the encoding is described 174 + in Section 12.1 as current implementation surface. 175 + 176 + - **Validation or error semantics.** How the renderer responds to invalid input 177 + (malformed directive arrays, unbalanced open/close pairs) is not yet specified 178 + as contract. Section 9.1 defines what constitutes valid input. Behavior for 179 + invalid input is currently unspecified. 180 + 181 + - **The complete set of directive properties.** The existence of the core 182 + directive constructors (`open`, `close`, `text`) and the core sizing helpers 183 + (`grow`, `fixed`, `fit`) is normative. The full set of properties accepted by 184 + these constructors — which layout fields, which styling options, which 185 + configuration groups are available — is current implementation surface 186 + described in Section 12.2. New property groups have been added over time and 187 + more may follow. 188 + 189 + - **The return type wrapper of `render()`.** The commitment that `render()` 190 + produces ANSI bytes accessible as a `Uint8Array` is normative. The wrapper 191 + type around those bytes is current implementation surface described in Section 192 + 12.3. 193 + 194 + Future readers should not treat current implementation surface as identical to 195 + the contract boundary. 196 + 197 + --- 198 + 199 + ## 6. Core Invariants 200 + 201 + _This section is normative._ 202 + 203 + **INV-1. Zero IO.** The renderer MUST NOT perform any terminal input or output. 204 + It MUST NOT write to stdout, read from stdin, open file descriptors, or interact 205 + with the terminal device. The renderer produces bytes; the caller writes them. 206 + 207 + **INV-2. Single transaction per frame.** Each frame MUST be rendered in a single 208 + transaction that crosses the TS→WASM boundary once. The caller provides the 209 + complete frame as a directive array and receives ANSI bytes in return. 210 + 211 + **INV-3. Frame-snapshot independence.** The renderer MUST NOT require the caller 212 + to maintain or provide state across frames beyond calling `render()` on the same 213 + Term instance. Each directive array fully describes its frame. 214 + 215 + **INV-4. ANSI byte output.** The output of a render transaction MUST be a byte 216 + sequence of valid UTF-8–encoded ANSI escape codes that is directly writable to a 217 + terminal output stream without further transformation or encoding. 218 + 219 + **INV-5. Layout/render/diff ownership.** The renderer owns the layout 220 + computation, render-command walk, cell-buffer diffing, and ANSI byte generation 221 + stages. The caller MUST NOT need to perform any of these operations. 222 + 223 + **INV-6. Internal lifecycle symmetry.** The renderer's internal layout lifecycle 224 + (begin-layout and end-layout calls to the underlying layout engine) MUST be 225 + symmetric: both calls occur within the same render transaction, in the same 226 + function scope. 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 235 + concern MUST remain independent. Neither MUST depend on the other's state, 236 + types, or API surface. They MAY share a compiled WASM binary for loading 237 + efficiency, but this is an implementation convenience, not an architectural 238 + coupling. 239 + 240 + --- 241 + 242 + ## 7. Rendering Contract 243 + 244 + _This section is normative._ 245 + 246 + ### 7.1 Inputs 247 + 248 + The rendering transaction accepts: 249 + 250 + - A **directive array**: an ordered array of directive objects constituting a 251 + complete frame. The array MUST contain balanced open/close pairs forming a 252 + valid tree structure. 253 + 254 + The directive array is the sole required input to a render transaction. 255 + 256 + ### 7.2 Rendering transaction 257 + 258 + When the caller invokes a render transaction: 259 + 260 + 1. The renderer accepts the directive array and transfers the frame description 261 + into the WASM module. 262 + 2. The WASM module processes the frame: it computes layout, walks render 263 + commands, populates the cell buffer, diffs against the previous frame, and 264 + writes ANSI bytes for changed cells. 265 + 3. Control returns to the caller with the ANSI byte output available. 266 + 267 + The render transaction is synchronous from the caller's perspective once 268 + invoked. It MUST NOT yield, suspend, or require callbacks during execution. 269 + 270 + ### 7.3 Output 271 + 272 + The render transaction produces ANSI bytes as a `Uint8Array`. These bytes: 273 + 274 + - MUST be valid UTF-8 275 + - MUST consist of ANSI escape sequences (CSI, SGR) and text content 276 + - MUST be directly writable to a terminal file descriptor to produce the 277 + described visual output 278 + - In cursor update mode, MUST represent only the cells that changed since the 279 + previous frame (on a Term instance that has rendered at least one prior frame) 280 + - In line mode, MUST represent all cells in the frame as newline-separated rows 281 + 282 + The output reflects the complete visual state of the frame. The caller SHOULD 283 + write the output to the terminal without modification. 284 + 285 + The output `Uint8Array` is a view over renderer-owned memory. It is valid until 286 + the next `render()` call on the same Term instance, at which point the buffer 287 + may be reused. Callers who need to retain the output beyond the next render MUST 288 + copy it. 289 + 290 + ### 7.4 Lifecycle 291 + 292 + A Term instance is created for specific terminal dimensions. The caller provides 293 + width and height at creation time. 294 + 295 + To handle terminal resize, the caller creates a new Term with the new 296 + dimensions. The previous Term instance becomes stale and SHOULD NOT be used for 297 + further rendering. 298 + 299 + Creation of a Term is asynchronous because it may involve WASM module 300 + preparation. A Term instance MAY be used for any number of render transactions. 301 + The Term retains its cell buffers across frames for diffing purposes. 302 + 303 + --- 304 + 305 + ## 8. Public Rendering API 306 + 307 + _This section is normative. Only items with high confidence of stability are 308 + included. See Section 5 for what this section does and does not freeze._ 309 + 310 + ### 8.1 Term creation 311 + 312 + ``` 313 + createTerm(options: { width: number; height: number }): Promise<Term> 314 + ``` 315 + 316 + Creates a new Term instance bound to the specified terminal dimensions. The 317 + returned promise resolves when the renderer is ready. The `width` and `height` 318 + parameters specify the terminal dimensions in character cells. 319 + 320 + ### 8.2 Render invocation 321 + 322 + ``` 323 + term.render(ops: Op[], options?: RenderOptions): <result containing ANSI bytes as Uint8Array> 324 + ``` 325 + 326 + Accepts an ordered array of directive objects and performs a render transaction 327 + as defined in Section 7. Returns the ANSI byte output as a `Uint8Array`. 328 + 329 + The optional `options` parameter controls the rendering mode. See Section 8.2.1 330 + and 8.2.2 for the two available modes. 331 + 332 + The return type is specified here only to the extent that the ANSI bytes MUST be 333 + accessible as a `Uint8Array`. The precise shape of the return value — whether it 334 + is a bare `Uint8Array`, a wrapper object, or a structure carrying additional 335 + data — is part of the current implementation surface described in Section 12.3 336 + and is not locked down by this specification. 337 + 338 + #### 8.2.1 Cursor update mode (default) 339 + 340 + When `mode` is omitted, the renderer operates in cursor update mode. Output 341 + consists of ANSI bytes with absolute CUP (`\x1b[row;colH`) cursor positioning 342 + for each changed cell. Only cells that differ from the previous frame are 343 + emitted, making this efficient for full-screen UIs where most of the screen is 344 + static between frames. 345 + 346 + The optional `row` parameter specifies a 1-based row offset for CUP positioning. 347 + This allows the caller to render into a region of the terminal starting at a row 348 + other than the top. The offset is applied to all emitted cursor positions. When 349 + omitted, it defaults to 1. 350 + 351 + #### 8.2.2 Line mode 352 + 353 + When `mode` is `"line"`, the renderer emits all cells as newline-separated rows 354 + without CUP positioning. Every cell is written regardless of whether it changed 355 + since the previous frame. The front buffer is updated so that a subsequent 356 + cursor update mode render can diff efficiently. 357 + 358 + Line mode is intended for inline region rendering where the caller manages 359 + cursor positioning externally and the output must work in pipes or non-alternate 360 + screen contexts. 361 + 362 + ### 8.3 Directive constructors 363 + 364 + Directives are created using constructor functions that return plain objects. 365 + The caller assembles these into an array. This pattern — functions returning 366 + plain directives, composed into arrays — is normative. A builder, fluent, or 367 + mutation-based API is explicitly rejected. 368 + 369 + #### 8.3.1 open 370 + 371 + ``` 372 + open(id: string, props?): OpenElement 373 + ``` 374 + 375 + Creates an element-open directive. The `id` parameter provides an identity for 376 + the element within the frame, used by the underlying layout engine for element 377 + tracking and hit-testing. The optional `props` parameter carries configuration 378 + for layout, styling, and behavior. 379 + 380 + Elements opened with `open()` MUST be closed with a corresponding `close()` 381 + directive later in the same directive array. 382 + 383 + The set of properties accepted by `props` is part of the current implementation 384 + surface described in Section 12.2. This specification defines the existence and 385 + signature of `open()` normatively but does not freeze the complete property 386 + surface, which has been extended incrementally and may continue to grow. 387 + 388 + #### 8.3.2 close 389 + 390 + ``` 391 + close(): CloseElement 392 + ``` 393 + 394 + Creates an element-close directive. Each `close()` MUST correspond to a 395 + preceding `open()`. 396 + 397 + #### 8.3.3 text 398 + 399 + ``` 400 + text(content: string, props?): Text 401 + ``` 402 + 403 + Creates a text directive. The `content` parameter provides the text string to 404 + render. The optional `props` parameter carries text styling configuration. 405 + 406 + Text directives MUST appear between a matching open/close pair. 407 + 408 + The set of styling properties accepted by `props` is part of the current 409 + implementation surface and may be extended. 410 + 411 + ### 8.4 Sizing helpers 412 + 413 + These functions produce sizing-axis values for use in element layout 414 + configuration: 415 + 416 + ``` 417 + grow(): SizingAxis 418 + ``` 419 + 420 + The element expands to fill available space in the parent along this axis. 421 + 422 + ``` 423 + fixed(value: number): SizingAxis 424 + ``` 425 + 426 + The element has a fixed size of `value` cells along this axis. 427 + 428 + ``` 429 + fit(min?: number, max?: number): SizingAxis 430 + ``` 431 + 432 + The element sizes to fit its content, optionally constrained by minimum and 433 + maximum bounds. 434 + 435 + ### 8.5 Color helper 436 + 437 + ``` 438 + rgba(r: number, g: number, b: number, a?: number): number 439 + ``` 440 + 441 + Packs color channel values (each 0–255) into a single 32-bit integer in ARGB 442 + format. Alpha defaults to 255 (fully opaque). The returned value is used 443 + wherever the directive model expects a color. 444 + 445 + --- 446 + 447 + ## 9. Directive Model 448 + 449 + _This section is normative._ 450 + 451 + ### 9.1 Directive-array pattern 452 + 453 + The rendering input is an ordered array of directive objects. Each directive is 454 + a plain JavaScript/TypeScript object created by a constructor function (Section 455 + 8.3). Directives are not classes, do not carry methods, and do not participate 456 + in a prototype chain. They MAY be spread, composed, stored, or inspected as 457 + ordinary objects. 458 + 459 + The array is processed in order. Open and close directives form an implicit 460 + tree. The renderer processes them sequentially. 461 + 462 + A directive array with unbalanced open/close pairs, or with close directives 463 + that do not match a preceding open, is invalid input. Callers SHOULD validate 464 + directive arrays before rendering. The renderer's behavior when given an invalid 465 + directive array is unspecified by this specification. 466 + 467 + ### 9.2 Transfer to the WASM module 468 + 469 + As part of the render transaction, the directive array is transferred into a 470 + form that the WASM module can process. This transfer is handled internally by 471 + the renderer and is not an operation the caller performs or observes. The 472 + transfer mechanism is an implementation detail described in Section 12.1. 473 + 474 + ### 9.3 Directive identity 475 + 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. 482 + 483 + --- 484 + 485 + ## 10. Identity and Frame Semantics 486 + 487 + _This section is normative._ 488 + 489 + ### 10.1 Frame completeness 490 + 491 + A directive array provided to `render()` MUST represent a complete frame. The 492 + renderer does not support incremental updates, partial frames, or delta 493 + descriptions. Every frame fully specifies the desired UI state. 494 + 495 + ### 10.2 Directive ordering 496 + 497 + Directives MUST be provided in depth-first tree order. An `open()` directive 498 + begins an element; its children (including nested open/close pairs and text 499 + directives) follow in order; a `close()` directive ends the element. The 500 + renderer processes directives in the order they appear in the array. 501 + 502 + ### 10.3 Element identity within a frame 503 + 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. 508 + 509 + ### 10.4 No cross-frame identity 510 + 511 + The renderer does not track element identity across frames. An element with id 512 + "sidebar" in frame N and an element with id "sidebar" in frame N+1 are not 513 + related from the renderer's perspective. Cross-frame identity, if needed, is the 514 + responsibility of a higher-level framework. 515 + 516 + --- 517 + 518 + ## 11. Boundaries and Non-Responsibilities 519 + 520 + _This section is normative._ 521 + 522 + ### 11.1 The renderer does not perform IO 523 + 524 + The renderer MUST NOT write to any output stream. The renderer MUST NOT read 525 + from any input stream. The renderer produces bytes; the caller decides when and 526 + how to write them. This enables the renderer to operate in any environment where 527 + WebAssembly is available, including browsers, server-side runtimes, and embedded 528 + contexts. 529 + 530 + ### 11.2 The renderer does not manage terminal state 531 + 532 + The renderer MUST NOT emit escape sequences for any of the following 533 + terminal-management operations: 534 + 535 + - Entering or leaving the alternate screen buffer 536 + - Hiding or showing the cursor 537 + - Setting the cursor shape or blink state 538 + - Enabling or disabling mouse reporting 539 + - Enabling or disabling keyboard protocol modes (e.g., Kitty progressive 540 + enhancement) 541 + - Enabling or disabling raw mode or similar terminal disciplines 542 + 543 + These are the caller's responsibility. The renderer's output contains only the 544 + escape sequences needed to render the frame content (cursor positioning for cell 545 + writes, SGR attributes for styling, and UTF-8 text). 546 + 547 + ### 11.3 The renderer does not own application lifecycle 548 + 549 + The renderer MUST NOT maintain a run loop, event loop, timer, or subscription 550 + mechanism. It does not schedule frames. It does not manage component state. It 551 + renders when asked and returns. The decision of when to render is entirely the 552 + caller's. 553 + 554 + ### 11.4 The renderer does not own input parsing 555 + 556 + Input parsing (keyboard events, mouse events, escape sequence decoding) is an 557 + independent concern specified separately in the 558 + [Clayterm Input Specification](input-spec.md). The renderer MUST NOT depend on 559 + input-parsing state, types, or API. 560 + 561 + However, pointer hit detection does require the render loop to participate. The 562 + caller may pass the current pointer position as part of render options, and the 563 + renderer returns the ids of every element the pointer is over. This is how the 564 + `PointerEvent[]` array in the render result is populated. See Section 12.4 for 565 + the current pointer event surface. 566 + 567 + ### 11.5 The renderer does not own higher-level framework concerns 568 + 569 + The renderer MUST NOT implement or depend on: 570 + 571 + - Component models or component lifecycles 572 + - Reconciliation or diffing of directive trees (the renderer diffs _cells_, not 573 + _trees_) 574 + - State management or reactivity 575 + - Event propagation through a component hierarchy 576 + 577 + These are the domain of higher-level frameworks built on Clayterm. 578 + 579 + --- 580 + 581 + ## 12. Current Surface That Remains Elastic 582 + 583 + _This entire section is non-normative. It describes the current implementation 584 + surface to aid consumers and future spec authors. The shapes described here are 585 + real, working, and in many cases deliberately designed, but they do not yet meet 586 + the stability threshold for normative specification. They MAY change in future 587 + versions without constituting a breaking change to the normative core defined 588 + above._ 589 + 590 + ### 12.1 Transfer encoding (command protocol) 591 + 592 + The renderer currently serializes directives into a flat byte buffer using a 593 + command protocol based on fixed-width `Uint32` words. Each directive is encoded 594 + as an opcode word followed by directive-specific data. Element-open directives 595 + use a property mask to indicate which optional field groups (layout, border, 596 + corner radius, clip, floating, scroll) are present, followed by the data for 597 + each indicated group. Strings are encoded as length-prefixed UTF-8 byte 598 + sequences within the word stream. Floats are stored as bit-reinterpreted 599 + `Uint32` values. 600 + 601 + This encoding has been extended incrementally (floating, clip, and scroll groups 602 + were added after the initial protocol) but has never been restructured. It is 603 + likely to remain stable in structure while continuing to grow. However, specific 604 + opcode values, mask definitions, and field layouts are implementation details 605 + and are not locked down by this specification. 606 + 607 + ### 12.2 Directive property groups 608 + 609 + The `open()` constructor currently accepts the following property groups in its 610 + `props` parameter: 611 + 612 + - **`layout`** — sizing (width and height, specified via sizing helpers), 613 + padding (per-side), alignment (currently numeric enum values, with a planned 614 + transition to string literals), direction (top-to-bottom or left-to-right), 615 + and gap 616 + - **`border`** — per-side border widths and border color 617 + - **`cornerRadius`** — per-corner radius values, producing rounded box-drawing 618 + characters 619 + - **`clip`** — clip region configuration for scroll containers 620 + - **`floating`** — floating-element configuration (offset, parent reference, 621 + attach points, z-index) 622 + - **`scroll`** — scroll container configuration 623 + 624 + The `text()` constructor currently accepts: `color`, `fontSize`, 625 + `letterSpacing`, `lineHeight`, and attribute flags (`bold`, `italic`, 626 + `underline`, `strikethrough`). 627 + 628 + These property groups represent the current implementation surface. New groups 629 + and fields have been added incrementally and more may follow. Alignment values 630 + are expected to transition from numeric to string-literal form. 631 + 632 + **Border width and layout interaction.** In the underlying layout engine (Clay), 633 + border configuration does not affect layout computation. This is Clay's intended 634 + behavior. Borders are drawn as visual overlays within the element's bounding 635 + box. A bordered element with zero padding will have its borders drawn over its 636 + content. Callers must add padding equal to or greater than the border width to 637 + prevent overlap. 638 + 639 + ### 12.3 Render return type 640 + 641 + The `render()` method currently returns a `RenderResult` object shaped as 642 + `{ output: Uint8Array, events: PointerEvent[] }`. 643 + 644 + The `output` field is the ANSI byte output specified normatively in Section 7.3 645 + and Section 8.2. 646 + 647 + The `events` field contains pointer events (enter, leave, click) derived from 648 + the underlying layout engine's element hit-testing. This field was added during 649 + a pointer-events feature implementation. The pointer event model is functional 650 + but has acknowledged gaps (no modifier keys on click events) and its interaction 651 + protocol (passing pointer state via render options, then reading events from the 652 + return value) was arrived at through iteration rather than upfront design. 653 + 654 + The return type of `render()` has changed twice since the project's inception 655 + (string, then `Uint8Array`, then `RenderResult`). While the ANSI bytes 656 + commitment (Section 7.3) is stable, the wrapper shape around those bytes is not. 657 + Future versions may restructure the return type. 658 + 659 + ### 12.4 Pointer event model 660 + 661 + Clayterm currently supports pointer hit-testing via the underlying layout 662 + engine's element-identification mechanism. The caller passes pointer state 663 + (`{ x, y, down }`) as part of render options, and the renderer returns pointer 664 + events as part of the render result: 665 + 666 + - `pointerenter` — the pointer has entered an element's bounding box 667 + - `pointerleave` — the pointer has left an element's bounding box 668 + - `pointerclick` — a pointer-up occurred over an element that was also under the 669 + pointer at pointer-down 670 + 671 + This surface is functional but should not be treated as stable contract. The 672 + calling convention was discovered through iteration, the event model has 673 + acknowledged gaps, and the approach may evolve. 674 + 675 + ### 12.5 Validation and packing 676 + 677 + **`validate(ops)`** — A public API function that checks a directive array for 678 + structural errors (unbalanced open/close pairs, invalid field types). Exported 679 + and used in tests. 680 + 681 + **`pack(ops, mem, offset)`** — An internal function that serializes a directive 682 + array into the transfer encoding described in Section 12.1. Currently exported 683 + but not public API; its exposure is incidental to the module structure. 684 + 685 + --- 686 + 687 + ## 13. Implementation Notes 688 + 689 + _This section is non-normative. These notes describe current implementation 690 + details that aid understanding but do not define contract. They may change 691 + without notice._ 692 + 693 + **WASM module structure.** The renderer is implemented in C and compiled to 694 + WebAssembly as a single module. The module contains both rendering and 695 + input-parsing functionality; they share a binary but maintain independent state. 696 + 697 + **WASM loading.** The WASM binary is inlined as a base64-encoded string in a 698 + generated module and instantiated per Term or Input with fresh memory. 699 + 700 + **Memory layout.** WASM linear memory is initialized with 256 pages (16MB). The 701 + renderer state struct and the transfer buffer are allocated in WASM linear 702 + memory. The specific layout is an implementation detail. 703 + 704 + **Layout engine.** The underlying layout engine is Clay, included as a 705 + dependency. Clay provides flexbox-like layout computation with support for 706 + fixed, grow, and fit sizing; padding; alignment; direction; gap; floating 707 + elements; clip regions; and scroll containers. 708 + 709 + **Text measurement.** Text width measurement uses `wcwidth`-based character 710 + width computation, supporting ASCII, CJK wide characters, and other Unicode 711 + codepoints. 712 + 713 + **Cell representation.** Each cell in the buffer stores a Unicode codepoint, a 714 + foreground color (packed ARGB with attribute flags in the high byte), and a 715 + background color. 716 + 717 + **Border junction resolution.** When bordered elements share edges, the renderer 718 + accumulates per-cell direction bitmasks and resolves them to correct box-drawing 719 + junction glyphs in a post-render pass. 720 + 721 + --- 722 + 723 + ## 14. Deferred / Future Areas 724 + 725 + _This section is non-normative. These topics are explicitly excluded from this 726 + specification. Their omission is intentional, not an oversight._ 727 + 728 + **Scroll container API.** The underlying layout engine supports scroll 729 + containers. No TypeScript-side API exists for providing scroll state to the 730 + renderer. 731 + 732 + **CSI helper for terminal setup.** A helper for generating paired apply/rollback 733 + byte arrays for terminal mode configuration was discussed but not implemented. 734 + 735 + **Browser-specific adapter.** The renderer's zero-IO architecture makes browser 736 + portability possible. No adapter exists. 737 + 738 + **`betweenChildren` border support.** The underlying layout engine supports 739 + this. It is not exposed in the directive model. 740 + 741 + --- 742 + 743 + ## Appendix A. Confidence Notes 744 + 745 + ### Why the rendering core is specified more aggressively than other surfaces 746 + 747 + The rendering architecture — `createTerm`, `render(ops)`, the directive 748 + constructors, the bytes-output commitment, and the core invariants — was 749 + designed at the project's inception and has been stable since. It has survived 750 + the addition of pointer events, border junction resolution, and the crankterm 751 + integration without revision to its fundamental shapes. Its key abstractions 752 + (flat directive arrays, single render transaction, ANSI byte output) were chosen 753 + over explicitly rejected alternatives (per-element FFI, protobuf, builder 754 + pattern, string output). This level of stability and intentionality justifies 755 + normative specification. 756 + 757 + The pointer event model and render return wrapper are the least settled of the 758 + currently shipping features. Both were introduced during feature implementation 759 + rather than designed as part of the core architecture. The return type of 760 + `render()` has changed twice. The pointer calling convention was discovered 761 + through iteration. These are working and useful, but they carry the lowest 762 + confidence of any feature currently in the codebase. 763 + 764 + ### How to interpret "currently exported" 765 + 766 + Several symbols are currently accessible from Clayterm's module exports — 767 + including `pack()`, `validate()`, and numerous input-related types — without 768 + clear evidence that they were intended as stable public contract. Being exported 769 + may mean "needed by internal modules" or "not yet audited for public/internal 770 + boundary." 771 + 772 + This specification does not treat the export list as a contract boundary. 773 + Instead, it uses stability over time, design ownership, survival of corrections, 774 + and absence of known reshaping forces as the criteria for normative inclusion. 775 + 776 + --- 777 + 778 + ## Open Decisions Intentionally Left Out of This Spec 779 + 780 + The following decisions are open. This specification omits them deliberately. 781 + Future readers should not interpret their absence as oversight or implicit 782 + resolution. 783 + 784 + 1. **What is the normative return type of `render()`?** This specification 785 + commits to ANSI bytes as `Uint8Array` but does not lock down the wrapper 786 + type. The current `RenderResult` shape may evolve. 787 + 788 + 2. **Is pointer event detection part of the rendering contract?** The current 789 + implementation returns pointer events from `render()`. This specification 790 + does not include pointer events in the normative core. Whether pointer 791 + detection is intrinsic to the renderer or should be a separate concern is 792 + unresolved. 793 + 794 + 3. **Is `pack()` public API?** `pack()` is currently exported but is an internal 795 + implementation detail, not public API. `validate()` is public API. 796 + 797 + 4. **How should border widths interact with layout?** The current behavior 798 + (borders do not affect layout) is inherited from the underlying layout 799 + engine. The project has questioned whether this is the right design. This 800 + specification describes the current behavior in Section 12.2 without 801 + committing to it. 802 + 803 + 5. **What are the specific transfer encoding details?** The encoding structure 804 + is described in Section 12.1 as current implementation surface. Locking down 805 + opcode values would constrain future extensions unnecessarily. 806 + 807 + 6. **What is the complete set of directive properties?** The property groups 808 + available in `open()` and `text()` are described in Section 12.2 as current 809 + implementation surface. They have been extended incrementally and will 810 + continue to grow. 811 + 812 + 7. **What are the validation and error semantics?** How the renderer responds to 813 + invalid input is unspecified. Callers SHOULD validate, but the validation 814 + model is not yet settled enough to define normatively.