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

Fix build for CI

Charles Lowell (Mar 16, 2026, 6:47 PM -0500) 650f4b84 53ea63c2

+245 -255
+2 -1
Makefile
··· 10 10 -Wl,--import-memory \ 11 11 -Wl,--stack-first \ 12 12 -Wl,--export-all \ 13 - -Wl,--allow-undefined 13 + -Wl,--undefined=Clay__MeasureText \ 14 + -Wl,--undefined=Clay__QueryScrollOffset 14 15 15 16 all: $(TARGET) 16 17 @echo "Built $(TARGET) ($$(wc -c < $(TARGET)) bytes)"
+5 -1
src/buffer.c
··· 5 5 6 6 typedef __SIZE_TYPE__ size_t; 7 7 8 - static void *memcpy(void *dst, const void *src, size_t n) { 8 + void *memcpy(void *dst, const void *src, size_t n) { 9 9 return __builtin_memcpy(dst, src, n); 10 + } 11 + 12 + void *memset(void *dst, int c, size_t n) { 13 + return __builtin_memset(dst, c, n); 10 14 } 11 15 12 16 static size_t strlen(const char *s) {
+233 -35
src/clayterm.c
··· 1 - /* clayterm.c — cell buffer, diff engine, escape gen, Clay render backend */ 1 + /* clayterm.c — WASM terminal rendering engine for Clay UI 2 + * 3 + * Public API (exported to WASM): 4 + * clayterm_size — compute arena size for given dimensions 5 + * init — initialize a Clayterm instance in provided memory 6 + * reduce — decode command buffer, run Clay layout, render to ANSI 7 + * output — pointer to output byte buffer 8 + * length — length of output byte buffer 9 + * measure — Clay text measurement callback 10 + */ 2 11 12 + #include "../clay/clay.h" 3 13 #include "clayterm.h" 4 14 #include "buffer.h" 5 15 #include "cell.h" 6 - #include "clay/clay.h" 7 16 #include "utf8.h" 8 17 #include "wcwidth.h" 18 + 19 + /* ── Command buffer protocol ──────────────────────────────────────── */ 20 + 21 + #define OP_BEGIN_LAYOUT 0x01 22 + #define OP_OPEN_ELEMENT 0x02 23 + #define OP_TEXT 0x03 24 + #define OP_CLOSE_ELEMENT 0x04 25 + #define OP_END_LAYOUT 0x05 26 + 27 + #define PROP_LAYOUT 0x01 28 + #define PROP_BG_COLOR 0x02 29 + #define PROP_CORNER_RADIUS 0x04 30 + #define PROP_BORDER 0x08 31 + #define PROP_CLIP 0x10 32 + #define PROP_FLOATING 0x20 9 33 10 34 /* ── Instance state ───────────────────────────────────────────────── */ 11 35 ··· 255 279 setcell(ct, x1 - 1, y, 0x2502, fg, bg); 256 280 } 257 281 258 - static void walk(struct Clayterm *ct, Clay_RenderCommandArray cmds) { 259 - for (int32_t i = 0; i < cmds.length; i++) { 260 - Clay_RenderCommand *cmd = Clay_RenderCommandArray_Get(&cmds, i); 261 - Clay_BoundingBox box = cmd->boundingBox; 262 - int x0 = (int)box.x; 263 - int y0 = (int)box.y; 264 - int x1 = (int)(box.x + box.width); 265 - int y1 = (int)(box.y + box.height); 282 + /* ── Command buffer helpers ───────────────────────────────────────── */ 283 + 284 + static uint32_t rd(uint32_t *buf, int len, int *i) { 285 + if (*i < len) 286 + return buf[(*i)++]; 287 + return 0; 288 + } 289 + 290 + static float rdf(uint32_t *buf, int len, int *i) { 291 + uint32_t v = rd(buf, len, i); 292 + float f; 293 + __builtin_memcpy(&f, &v, 4); 294 + return f; 295 + } 266 296 267 - switch (cmd->commandType) { 268 - case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: 269 - render_rect(ct, x0, y0, x1, y1, &cmd->renderData.rectangle); 270 - break; 271 - case CLAY_RENDER_COMMAND_TYPE_TEXT: 272 - render_text(ct, x0, y0, &cmd->renderData.text); 273 - break; 274 - case CLAY_RENDER_COMMAND_TYPE_BORDER: 275 - render_border(ct, x0, y0, x1, y1, &cmd->renderData.border); 276 - break; 277 - case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: 278 - ct->clipping = 1; 279 - ct->clipx = x0; 280 - ct->clipy = y0; 281 - ct->clipw = x1 - x0; 282 - ct->cliph = y1 - y0; 283 - break; 284 - case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: 285 - ct->clipping = 0; 286 - break; 287 - default: 288 - break; 289 - } 297 + static Clay_Color unpack_color(uint32_t c) { 298 + return (Clay_Color){ 299 + .r = (float)((c >> 16) & 0xff), 300 + .g = (float)((c >> 8) & 0xff), 301 + .b = (float)(c & 0xff), 302 + .a = (float)((c >> 24) & 0xff), 303 + }; 304 + } 305 + 306 + static Clay_SizingAxis decode_axis(uint32_t *buf, int len, int *i) { 307 + uint32_t type = rd(buf, len, i); 308 + float a = rdf(buf, len, i); 309 + float b = rdf(buf, len, i); 310 + Clay_SizingAxis axis = {0}; 311 + switch (type) { 312 + case 0: /* FIT */ 313 + axis.type = CLAY__SIZING_TYPE_FIT; 314 + axis.size.minMax.min = a; 315 + axis.size.minMax.max = b; 316 + break; 317 + case 1: /* GROW */ 318 + axis.type = CLAY__SIZING_TYPE_GROW; 319 + axis.size.minMax.min = a; 320 + axis.size.minMax.max = b; 321 + break; 322 + case 2: /* PERCENT */ 323 + axis.type = CLAY__SIZING_TYPE_PERCENT; 324 + axis.size.percent = a; 325 + break; 326 + case 3: /* FIXED */ 327 + axis.type = CLAY__SIZING_TYPE_FIXED; 328 + axis.size.minMax.min = a; 329 + axis.size.minMax.max = a; 330 + break; 290 331 } 332 + return axis; 291 333 } 292 334 293 335 /* ── Public API ───────────────────────────────────────────────────── */ ··· 340 382 return ct; 341 383 } 342 384 343 - void render(struct Clayterm *ct, Clay_RenderCommandArray cmds) { 385 + void reduce(struct Clayterm *ct, uint32_t *buf, int len) { 386 + int i = 0; 387 + uint32_t idx = 0; 388 + 389 + Clay_BeginLayout(); 390 + 391 + while (i < len) { 392 + uint32_t op = rd(buf, len, &i); 393 + 394 + switch (op) { 395 + case OP_OPEN_ELEMENT: { 396 + /* read id string */ 397 + uint32_t id_len = rd(buf, len, &i); 398 + int id_words = (id_len + 3) / 4; 399 + char *id_chars = (char *)&buf[i]; 400 + i += id_words; 401 + 402 + if (id_len > 0) { 403 + Clay_String str = {.length = (int32_t)id_len, .chars = id_chars}; 404 + Clay_ElementId eid = Clay__HashString(str, idx++); 405 + Clay__OpenElementWithId(eid); 406 + } else { 407 + Clay__OpenElement(); 408 + } 409 + 410 + /* read property mask */ 411 + uint32_t mask = rd(buf, len, &i); 412 + Clay_ElementDeclaration decl = {0}; 413 + 414 + if (mask & PROP_LAYOUT) { 415 + decl.layout.sizing.width = decode_axis(buf, len, &i); 416 + decl.layout.sizing.height = decode_axis(buf, len, &i); 417 + 418 + uint32_t pad = rd(buf, len, &i); 419 + decl.layout.padding.left = pad & 0xff; 420 + decl.layout.padding.right = (pad >> 8) & 0xff; 421 + decl.layout.padding.top = (pad >> 16) & 0xff; 422 + decl.layout.padding.bottom = (pad >> 24) & 0xff; 423 + 424 + uint32_t gd = rd(buf, len, &i); 425 + decl.layout.childGap = gd & 0xffff; 426 + decl.layout.layoutDirection = (gd >> 16) & 0xff; 427 + 428 + uint32_t al = rd(buf, len, &i); 429 + decl.layout.childAlignment.x = al & 0xff; 430 + decl.layout.childAlignment.y = (al >> 8) & 0xff; 431 + } 432 + 433 + if (mask & PROP_BG_COLOR) { 434 + decl.backgroundColor = unpack_color(rd(buf, len, &i)); 435 + } 436 + 437 + if (mask & PROP_CORNER_RADIUS) { 438 + uint32_t cr = rd(buf, len, &i); 439 + decl.cornerRadius.topLeft = (float)(cr & 0xff); 440 + decl.cornerRadius.topRight = (float)((cr >> 8) & 0xff); 441 + decl.cornerRadius.bottomLeft = (float)((cr >> 16) & 0xff); 442 + decl.cornerRadius.bottomRight = (float)((cr >> 24) & 0xff); 443 + } 444 + 445 + if (mask & PROP_BORDER) { 446 + decl.border.color = unpack_color(rd(buf, len, &i)); 447 + 448 + uint32_t bw = rd(buf, len, &i); 449 + decl.border.width.left = bw & 0xff; 450 + decl.border.width.right = (bw >> 8) & 0xff; 451 + decl.border.width.top = (bw >> 16) & 0xff; 452 + decl.border.width.bottom = (bw >> 24) & 0xff; 453 + } 454 + 455 + if (mask & PROP_CLIP) { 456 + uint32_t cl = rd(buf, len, &i); 457 + decl.clip.horizontal = cl & 0xff; 458 + decl.clip.vertical = (cl >> 8) & 0xff; 459 + } 460 + 461 + if (mask & PROP_FLOATING) { 462 + decl.floating.offset.x = rdf(buf, len, &i); 463 + decl.floating.offset.y = rdf(buf, len, &i); 464 + decl.floating.parentId = rd(buf, len, &i); 465 + 466 + uint32_t fc = rd(buf, len, &i); 467 + decl.floating.attachTo = fc & 0xff; 468 + decl.floating.attachPoints.element = (fc >> 8) & 0xff; 469 + decl.floating.attachPoints.parent = (fc >> 16) & 0xff; 470 + decl.floating.zIndex = (int16_t)((fc >> 24) & 0xff); 471 + } 472 + 473 + Clay__ConfigureOpenElement(decl); 474 + break; 475 + } 476 + 477 + case OP_TEXT: { 478 + uint32_t col = rd(buf, len, &i); 479 + uint32_t cfg = rd(buf, len, &i); 480 + uint32_t str_len = rd(buf, len, &i); 481 + int str_words = (str_len + 3) / 4; 482 + char *str_chars = (char *)&buf[i]; 483 + i += str_words; 484 + 485 + Clay_String text = {.length = (int32_t)str_len, .chars = str_chars}; 486 + 487 + Clay_TextElementConfig config = {0}; 488 + config.textColor = unpack_color(col); 489 + config.fontSize = cfg & 0xff; 490 + config.fontId = (cfg >> 8) & 0xff; 491 + config.wrapMode = (cfg >> 16) & 0xff; 492 + /* attrs byte -> alpha channel for render_text to extract */ 493 + config.textColor.a = (float)((cfg >> 24) & 0xff); 494 + 495 + Clay__OpenTextElement(text, Clay__StoreTextElementConfig(config)); 496 + break; 497 + } 498 + 499 + case OP_CLOSE_ELEMENT: 500 + Clay__CloseElement(); 501 + break; 502 + 503 + default: 504 + break; 505 + } 506 + } 507 + 508 + Clay_RenderCommandArray cmds = Clay_EndLayout(); 509 + 510 + /* reset output state */ 344 511 ct->out.length = 0; 345 512 ct->lastfg = ct->lastbg = 0xffffffff; 346 513 ct->lastx = ct->lasty = -1; ··· 348 515 cells_clear(ct->back, ct->w, ct->h); 349 516 350 517 /* walk Clay render commands into back buffer */ 351 - walk(ct, cmds); 518 + for (int32_t j = 0; j < cmds.length; j++) { 519 + Clay_RenderCommand *cmd = Clay_RenderCommandArray_Get(&cmds, j); 520 + Clay_BoundingBox box = cmd->boundingBox; 521 + int x0 = (int)box.x; 522 + int y0 = (int)box.y; 523 + int x1 = (int)(box.x + box.width); 524 + int y1 = (int)(box.y + box.height); 525 + 526 + switch (cmd->commandType) { 527 + case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: 528 + render_rect(ct, x0, y0, x1, y1, &cmd->renderData.rectangle); 529 + break; 530 + case CLAY_RENDER_COMMAND_TYPE_TEXT: 531 + render_text(ct, x0, y0, &cmd->renderData.text); 532 + break; 533 + case CLAY_RENDER_COMMAND_TYPE_BORDER: 534 + render_border(ct, x0, y0, x1, y1, &cmd->renderData.border); 535 + break; 536 + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: 537 + ct->clipping = 1; 538 + ct->clipx = x0; 539 + ct->clipy = y0; 540 + ct->clipw = x1 - x0; 541 + ct->cliph = y1 - y0; 542 + break; 543 + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: 544 + ct->clipping = 0; 545 + break; 546 + default: 547 + break; 548 + } 549 + } 352 550 353 551 /* diff front vs back, emit escape sequences */ 354 552 present(ct);
+3 -1
src/clayterm.h
··· 3 3 #ifndef CLAYTERM_H 4 4 #define CLAYTERM_H 5 5 6 + #include <stdint.h> 7 + 6 8 #include "cell.h" 7 9 8 10 struct Clayterm; ··· 10 12 /* WASM exports */ 11 13 int clayterm_size(int w, int h); 12 14 struct Clayterm *init(void *mem, int w, int h); 13 - void render(struct Clayterm *ct, Clay_RenderCommandArray cmds); 15 + void reduce(struct Clayterm *ct, uint32_t *buf, int len); 14 16 char *output(struct Clayterm *ct); 15 17 int length(struct Clayterm *ct); 16 18 void measure(int ret, int txt);
+2 -3
src/module.c
··· 1 1 /* module.c — single compilation unit for clayterm WASM module */ 2 2 3 - #include "clay/clay.h" 3 + #include "../clay/clay.h" 4 4 5 5 #include "buffer.c" 6 6 #include "cell.c" 7 - #include "clayterm.c" 8 - #include "ops.c" 9 7 #include "utf8.c" 10 8 #include "wcwidth.c" 9 + #include "clayterm.c"
-187
src/ops.c
··· 1 - /* ops.c — command buffer reducer for Clay layout commands */ 2 - 3 - #include "ops.h" 4 - #include "clay/clay.h" 5 - #include "clayterm.h" 6 - 7 - /* ── Command buffer helpers ────────────────────────────────────────── */ 8 - 9 - static uint32_t rd(uint32_t *buf, int len, int *i) { 10 - if (*i < len) 11 - return buf[(*i)++]; 12 - return 0; 13 - } 14 - 15 - static float rdf(uint32_t *buf, int len, int *i) { 16 - uint32_t v = rd(buf, len, i); 17 - float f; 18 - __builtin_memcpy(&f, &v, 4); 19 - return f; 20 - } 21 - 22 - static Clay_Color unpack_color(uint32_t c) { 23 - return (Clay_Color){ 24 - .r = (float)((c >> 16) & 0xff), 25 - .g = (float)((c >> 8) & 0xff), 26 - .b = (float)(c & 0xff), 27 - .a = (float)((c >> 24) & 0xff), 28 - }; 29 - } 30 - 31 - static Clay_SizingAxis decode_axis(uint32_t *buf, int len, int *i) { 32 - uint32_t type = rd(buf, len, i); 33 - float a = rdf(buf, len, i); 34 - float b = rdf(buf, len, i); 35 - Clay_SizingAxis axis = {0}; 36 - switch (type) { 37 - case 0: /* FIT */ 38 - axis.type = CLAY__SIZING_TYPE_FIT; 39 - axis.size.minMax.min = a; 40 - axis.size.minMax.max = b; 41 - break; 42 - case 1: /* GROW */ 43 - axis.type = CLAY__SIZING_TYPE_GROW; 44 - axis.size.minMax.min = a; 45 - axis.size.minMax.max = b; 46 - break; 47 - case 2: /* PERCENT */ 48 - axis.type = CLAY__SIZING_TYPE_PERCENT; 49 - axis.size.percent = a; 50 - break; 51 - case 3: /* FIXED */ 52 - axis.type = CLAY__SIZING_TYPE_FIXED; 53 - axis.size.minMax.min = a; 54 - axis.size.minMax.max = a; 55 - break; 56 - } 57 - return axis; 58 - } 59 - 60 - /* ── Command buffer reducer ───────────────────────────────────────── */ 61 - 62 - void reduce(struct Clayterm *ct, uint32_t *buf, int len) { 63 - int i = 0; 64 - uint32_t idx = 0; 65 - 66 - Clay_BeginLayout(); 67 - 68 - while (i < len) { 69 - uint32_t op = rd(buf, len, &i); 70 - 71 - switch (op) { 72 - case OP_OPEN_ELEMENT: { 73 - /* read id string */ 74 - uint32_t id_len = rd(buf, len, &i); 75 - int id_words = (id_len + 3) / 4; 76 - char *id_chars = (char *)&buf[i]; 77 - i += id_words; 78 - 79 - if (id_len > 0) { 80 - Clay_String str = {.length = (int32_t)id_len, .chars = id_chars}; 81 - Clay_ElementId eid = Clay__HashString(str, idx++); 82 - Clay__OpenElementWithId(eid); 83 - } else { 84 - Clay__OpenElement(); 85 - } 86 - 87 - /* read property mask */ 88 - uint32_t mask = rd(buf, len, &i); 89 - Clay_ElementDeclaration decl = {0}; 90 - 91 - if (mask & PROP_LAYOUT) { 92 - decl.layout.sizing.width = decode_axis(buf, len, &i); 93 - decl.layout.sizing.height = decode_axis(buf, len, &i); 94 - 95 - uint32_t pad = rd(buf, len, &i); 96 - decl.layout.padding.left = pad & 0xff; 97 - decl.layout.padding.right = (pad >> 8) & 0xff; 98 - decl.layout.padding.top = (pad >> 16) & 0xff; 99 - decl.layout.padding.bottom = (pad >> 24) & 0xff; 100 - 101 - uint32_t gd = rd(buf, len, &i); 102 - decl.layout.childGap = gd & 0xffff; 103 - decl.layout.layoutDirection = (gd >> 16) & 0xff; 104 - 105 - uint32_t al = rd(buf, len, &i); 106 - decl.layout.childAlignment.x = al & 0xff; 107 - decl.layout.childAlignment.y = (al >> 8) & 0xff; 108 - } 109 - 110 - if (mask & PROP_BG_COLOR) { 111 - decl.backgroundColor = unpack_color(rd(buf, len, &i)); 112 - } 113 - 114 - if (mask & PROP_CORNER_RADIUS) { 115 - uint32_t cr = rd(buf, len, &i); 116 - decl.cornerRadius.topLeft = (float)(cr & 0xff); 117 - decl.cornerRadius.topRight = (float)((cr >> 8) & 0xff); 118 - decl.cornerRadius.bottomLeft = (float)((cr >> 16) & 0xff); 119 - decl.cornerRadius.bottomRight = (float)((cr >> 24) & 0xff); 120 - } 121 - 122 - if (mask & PROP_BORDER) { 123 - decl.border.color = unpack_color(rd(buf, len, &i)); 124 - 125 - uint32_t bw = rd(buf, len, &i); 126 - decl.border.width.left = bw & 0xff; 127 - decl.border.width.right = (bw >> 8) & 0xff; 128 - decl.border.width.top = (bw >> 16) & 0xff; 129 - decl.border.width.bottom = (bw >> 24) & 0xff; 130 - } 131 - 132 - if (mask & PROP_CLIP) { 133 - uint32_t cl = rd(buf, len, &i); 134 - decl.clip.horizontal = cl & 0xff; 135 - decl.clip.vertical = (cl >> 8) & 0xff; 136 - } 137 - 138 - if (mask & PROP_FLOATING) { 139 - decl.floating.offset.x = rdf(buf, len, &i); 140 - decl.floating.offset.y = rdf(buf, len, &i); 141 - decl.floating.parentId = rd(buf, len, &i); 142 - 143 - uint32_t fc = rd(buf, len, &i); 144 - decl.floating.attachTo = fc & 0xff; 145 - decl.floating.attachPoints.element = (fc >> 8) & 0xff; 146 - decl.floating.attachPoints.parent = (fc >> 16) & 0xff; 147 - decl.floating.zIndex = (int16_t)((fc >> 24) & 0xff); 148 - } 149 - 150 - Clay__ConfigureOpenElement(decl); 151 - break; 152 - } 153 - 154 - case OP_TEXT: { 155 - uint32_t col = rd(buf, len, &i); 156 - uint32_t cfg = rd(buf, len, &i); 157 - uint32_t str_len = rd(buf, len, &i); 158 - int str_words = (str_len + 3) / 4; 159 - char *str_chars = (char *)&buf[i]; 160 - i += str_words; 161 - 162 - Clay_String text = {.length = (int32_t)str_len, .chars = str_chars}; 163 - 164 - Clay_TextElementConfig config = {0}; 165 - config.textColor = unpack_color(col); 166 - config.fontSize = cfg & 0xff; 167 - config.fontId = (cfg >> 8) & 0xff; 168 - config.wrapMode = (cfg >> 16) & 0xff; 169 - /* attrs byte → alpha channel for render_text to extract */ 170 - config.textColor.a = (float)((cfg >> 24) & 0xff); 171 - 172 - Clay__OpenTextElement(text, Clay__StoreTextElementConfig(config)); 173 - break; 174 - } 175 - 176 - case OP_CLOSE_ELEMENT: 177 - Clay__CloseElement(); 178 - break; 179 - 180 - default: 181 - break; 182 - } 183 - } 184 - 185 - Clay_RenderCommandArray cmds = Clay_EndLayout(); 186 - render(ct, cmds); 187 - }
-27
src/ops.h
··· 1 - /* ops.h — command buffer protocol for Clay layout commands */ 2 - 3 - #ifndef OPS_H 4 - #define OPS_H 5 - 6 - #include <stdint.h> 7 - 8 - struct Clayterm; 9 - 10 - /* Command buffer opcodes */ 11 - #define OP_BEGIN_LAYOUT 0x01 12 - #define OP_OPEN_ELEMENT 0x02 13 - #define OP_TEXT 0x03 14 - #define OP_CLOSE_ELEMENT 0x04 15 - #define OP_END_LAYOUT 0x05 16 - 17 - /* Property group masks for OPEN_ELEMENT */ 18 - #define PROP_LAYOUT 0x01 19 - #define PROP_BG_COLOR 0x02 20 - #define PROP_CORNER_RADIUS 0x04 21 - #define PROP_BORDER 0x08 22 - #define PROP_CLIP 0x10 23 - #define PROP_FLOATING 0x20 24 - 25 - void reduce(struct Clayterm *ct, uint32_t *buf, int len); 26 - 27 - #endif