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

✨ add row offset to createTerm for region-mode rendering

Add a `row` option to TermOptions that offsets all CUP sequences by
the given number of rows, enabling rendering into a region below
existing terminal content rather than always starting at row 1.

Charles Lowell (Apr 3, 2026, 6:02 PM -0500) 4c9cff81 dde5727e

+151 -53
+9 -8
src/clayterm.c
··· 35 35 /* ── Instance state ───────────────────────────────────────────────── */ 36 36 37 37 struct Clayterm { 38 - int w, h; 38 + int w, h, row; 39 39 Cell *front; 40 40 Cell *back; 41 41 Buffer out; ··· 131 131 132 132 static void emit_cursor(struct Clayterm *ct, int x, int y) { 133 133 buf_str(&ct->out, "\x1b["); 134 - buf_num(&ct->out, y + 1); 134 + buf_num(&ct->out, y + 1 + ct->row); 135 135 buf_put(&ct->out, ";", 1); 136 136 buf_num(&ct->out, x + 1); 137 137 buf_put(&ct->out, "H", 1); ··· 352 352 353 353 static void clay_error(Clay_ErrorData err) { (void)err; } 354 354 355 - struct Clayterm *init(void *mem, int w, int h) { 355 + struct Clayterm *init(void *mem, int w, int h, int row) { 356 356 struct Clayterm *ct = (struct Clayterm *)mem; 357 357 int cell_count = w * h; 358 358 int cell_bytes = align8(cell_count * (int)sizeof(Cell)); ··· 369 369 *ct = (struct Clayterm){ 370 370 .w = w, 371 371 .h = h, 372 + .row = row, 372 373 .front = (Cell *)base, 373 374 .back = (Cell *)(base + cell_bytes), 374 375 .out = {base + cell_bytes * 2, 0, cell_count * OUT_BYTES_PER_CELL}, ··· 557 558 558 559 int length(struct Clayterm *ct) { return ct->out.length; } 559 560 560 - int pointer_over_count(void) { 561 - return Clay_GetPointerOverIds().length; 562 - } 561 + int pointer_over_count(void) { return Clay_GetPointerOverIds().length; } 563 562 564 563 int pointer_over_id_string_length(int index) { 565 564 Clay_ElementIdArray ids = Clay_GetPointerOverIds(); 566 - if (index >= ids.length) return 0; 565 + if (index >= ids.length) 566 + return 0; 567 567 return ids.internalArray[index].stringId.length; 568 568 } 569 569 570 570 int pointer_over_id_string_ptr(int index) { 571 571 Clay_ElementIdArray ids = Clay_GetPointerOverIds(); 572 - if (index >= ids.length) return 0; 572 + if (index >= ids.length) 573 + return 0; 573 574 return (int)ids.internalArray[index].stringId.chars; 574 575 } 575 576
+1 -1
src/clayterm.h
··· 11 11 12 12 /* WASM exports */ 13 13 int clayterm_size(int w, int h); 14 - struct Clayterm *init(void *mem, int w, int h); 14 + struct Clayterm *init(void *mem, int w, int h, int row); 15 15 void reduce(struct Clayterm *ct, uint32_t *buf, int len); 16 16 char *output(struct Clayterm *ct); 17 17 int length(struct Clayterm *ct);
+70 -36
src/input.c
··· 63 63 64 64 static uint8_t mouse_mods(int b) { 65 65 uint8_t m = 0; 66 - if (b & 4) m |= MOD_SHIFT; 67 - if (b & 8) m |= MOD_ALT; 68 - if (b & 16) m |= MOD_CTRL; 69 - if (b & 32) m |= MOD_MOTION; 66 + if (b & 4) 67 + m |= MOD_SHIFT; 68 + if (b & 8) 69 + m |= MOD_ALT; 70 + if (b & 16) 71 + m |= MOD_CTRL; 72 + if (b & 32) 73 + m |= MOD_MOTION; 70 74 return m; 71 75 } 72 76 ··· 485 489 for (int j = off; j < i - 1 && tc < MAX_TEXT_CODEPOINTS; j++) { 486 490 char c = st->buf[j]; 487 491 if (c >= '0' && c <= '9') { 488 - if (val == -1) val = 0; 492 + if (val == -1) 493 + val = 0; 489 494 val = val * 10 + (c - '0'); 490 495 } else if (c == ':') { 491 496 if (val >= 0) ··· 504 509 505 510 static uint16_t csi_legacy_key(char term, int number) { 506 511 switch (term) { 507 - case 'A': return KEY_ARROW_UP; 508 - case 'B': return KEY_ARROW_DOWN; 509 - case 'C': return KEY_ARROW_RIGHT; 510 - case 'D': return KEY_ARROW_LEFT; 511 - case 'H': return KEY_HOME; 512 - case 'F': return KEY_END; 513 - case 'P': return KEY_F1; 514 - case 'Q': return KEY_F2; 515 - case 'S': return KEY_F4; 512 + case 'A': 513 + return KEY_ARROW_UP; 514 + case 'B': 515 + return KEY_ARROW_DOWN; 516 + case 'C': 517 + return KEY_ARROW_RIGHT; 518 + case 'D': 519 + return KEY_ARROW_LEFT; 520 + case 'H': 521 + return KEY_HOME; 522 + case 'F': 523 + return KEY_END; 524 + case 'P': 525 + return KEY_F1; 526 + case 'Q': 527 + return KEY_F2; 528 + case 'S': 529 + return KEY_F4; 516 530 case '~': 517 531 switch (number) { 518 - case 2: return KEY_INSERT; 519 - case 3: return KEY_DELETE; 520 - case 5: return KEY_PGUP; 521 - case 6: return KEY_PGDN; 522 - case 7: return KEY_HOME; 523 - case 8: return KEY_END; 524 - case 11: return KEY_F1; 525 - case 12: return KEY_F2; 526 - case 13: return KEY_F3; 527 - case 14: return KEY_F4; 528 - case 15: return KEY_F5; 529 - case 17: return KEY_F6; 530 - case 18: return KEY_F7; 531 - case 19: return KEY_F8; 532 - case 20: return KEY_F9; 533 - case 21: return KEY_F10; 534 - case 23: return KEY_F11; 535 - case 24: return KEY_F12; 536 - default: return 0; 532 + case 2: 533 + return KEY_INSERT; 534 + case 3: 535 + return KEY_DELETE; 536 + case 5: 537 + return KEY_PGUP; 538 + case 6: 539 + return KEY_PGDN; 540 + case 7: 541 + return KEY_HOME; 542 + case 8: 543 + return KEY_END; 544 + case 11: 545 + return KEY_F1; 546 + case 12: 547 + return KEY_F2; 548 + case 13: 549 + return KEY_F3; 550 + case 14: 551 + return KEY_F4; 552 + case 15: 553 + return KEY_F5; 554 + case 17: 555 + return KEY_F6; 556 + case 18: 557 + return KEY_F7; 558 + case 19: 559 + return KEY_F8; 560 + case 20: 561 + return KEY_F9; 562 + case 21: 563 + return KEY_F10; 564 + case 23: 565 + return KEY_F11; 566 + case 24: 567 + return KEY_F12; 568 + default: 569 + return 0; 537 570 } 538 - default: return 0; 571 + default: 572 + return 0; 539 573 } 540 574 } 541 575 ··· 578 612 mod = cur; 579 613 cur = -1; 580 614 sub++; 581 - } else if ((c >= 'A' && c <= 'D') || c == 'F' || c == 'H' || 582 - c == 'P' || c == 'Q' || c == 'S' || c == '~') { 615 + } else if ((c >= 'A' && c <= 'D') || c == 'F' || c == 'H' || c == 'P' || 616 + c == 'Q' || c == 'S' || c == '~') { 583 617 if (param == 0) 584 618 number = cur; 585 619 else if (param == 1 && sub == 0)
+3 -3
src/input.h
··· 69 69 70 70 /* ── Kitty action types ──────────────────────────────────────────── */ 71 71 72 - #define ACTION_NONE 0 73 - #define ACTION_PRESS 1 74 - #define ACTION_REPEAT 2 72 + #define ACTION_NONE 0 73 + #define ACTION_PRESS 1 74 + #define ACTION_REPEAT 2 75 75 #define ACTION_RELEASE 3 76 76 77 77 /* ── Key codes ────────────────────────────────────────────────────── */
+7 -3
term-native.ts
··· 11 11 12 12 import { compiled } from "./wasm.ts"; 13 13 14 - export async function createTermNative(w: number, h: number): Promise<Native> { 14 + export async function createTermNative( 15 + w: number, 16 + h: number, 17 + row: number = 0, 18 + ): Promise<Native> { 15 19 let memory = new WebAssembly.Memory({ initial: 2 }); 16 20 let exports: Record<string, CallableFunction> = {}; 17 21 ··· 43 47 let ct = exports as unknown as { 44 48 __heap_base: WebAssembly.Global; 45 49 clayterm_size(w: number, h: number): number; 46 - init(mem: number, w: number, h: number): number; 50 + init(mem: number, w: number, h: number, row: number): number; 47 51 reduce(ct: number, buf: number, len: number): void; 48 52 output(ct: number): number; 49 53 length(ct: number): number; ··· 64 68 memory.grow(pages - current); 65 69 } 66 70 67 - let statePtr = ct.init(heap, w, h); 71 + let statePtr = ct.init(heap, w, h, row); 68 72 let opsBuf = (heap + size + 3) & ~3; 69 73 70 74 return {
+3 -2
term.ts
··· 4 4 export interface TermOptions { 5 5 height: number; 6 6 width: number; 7 + row?: number; 7 8 } 8 9 9 10 export interface RenderOptions { ··· 29 30 } 30 31 31 32 export async function createTerm(options: TermOptions): Promise<Term> { 32 - let { width, height } = options; 33 - let native = await createTermNative(width, height); 33 + let { width, height, row = 0 } = options; 34 + let native = await createTermNative(width, height, row); 34 35 let { memory, statePtr, opsBuf } = native; 35 36 36 37 let prev = new Set<string>();
+58
test/term.test.ts
··· 88 88 │ │ 89 89 ╰──────────────────────────────────────╯`.trim()); 90 90 }); 91 + 92 + describe("row offset", () => { 93 + it("renders two frames at the offset position", async () => { 94 + let term = await createTerm({ width: 20, height: 5, row: 5 }); 95 + let box = (msg: string) => [ 96 + open("root", { 97 + layout: { width: grow(), height: grow(), direction: "ttb" }, 98 + }), 99 + open("box", { 100 + layout: { 101 + width: grow(), 102 + height: grow(), 103 + direction: "ttb", 104 + padding: { left: 1, top: 1 }, 105 + }, 106 + border: { 107 + color: rgba(255, 255, 255), 108 + left: 1, 109 + right: 1, 110 + top: 1, 111 + bottom: 1, 112 + }, 113 + }), 114 + text(msg), 115 + close(), 116 + close(), 117 + ]; 118 + 119 + let header = await createTerm({ width: 20, height: 5 }); 120 + let banner = decode(header.render(box("hello")).output); 121 + 122 + let first = decode(term.render(box("world")).output); 123 + expect(print(banner + first, 20, 10)).toEqual(`\ 124 + ┌──────────────────┐ 125 + │hello │ 126 + │ │ 127 + │ │ 128 + └──────────────────┘ 129 + ┌──────────────────┐ 130 + │world │ 131 + │ │ 132 + │ │ 133 + └──────────────────┘`); 134 + 135 + let second = decode(term.render(box("universe")).output); 136 + expect(print(banner + first + second, 20, 10)).toEqual(`\ 137 + ┌──────────────────┐ 138 + │hello │ 139 + │ │ 140 + │ │ 141 + └──────────────────┘ 142 + ┌──────────────────┐ 143 + │universe │ 144 + │ │ 145 + │ │ 146 + └──────────────────┘`); 147 + }); 148 + }); 91 149 });