From 4c9cff811233ace6ee3dd8eeb3fbfc2ea4d3e3aa Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 3 Apr 2026 18:02:22 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20add=20row=20offset=20to=20createTer?= =?UTF-8?q?m=20for=20region-mode=20rendering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/clayterm.c | 17 ++++---- src/clayterm.h | 2 +- src/input.c | 106 ++++++++++++++++++++++++++++++---------------- src/input.h | 6 +-- term-native.ts | 10 +++-- term.ts | 5 ++- test/term.test.ts | 58 +++++++++++++++++++++++++ 7 files changed, 151 insertions(+), 53 deletions(-) diff --git a/src/clayterm.c b/src/clayterm.c index a2da8ec..6fd50c0 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -35,7 +35,7 @@ /* ── Instance state ───────────────────────────────────────────────── */ struct Clayterm { - int w, h; + int w, h, row; Cell *front; Cell *back; Buffer out; @@ -131,7 +131,7 @@ static void emit_attr(struct Clayterm *ct, uint32_t fg, uint32_t bg) { static void emit_cursor(struct Clayterm *ct, int x, int y) { buf_str(&ct->out, "\x1b["); - buf_num(&ct->out, y + 1); + buf_num(&ct->out, y + 1 + ct->row); buf_put(&ct->out, ";", 1); buf_num(&ct->out, x + 1); buf_put(&ct->out, "H", 1); @@ -352,7 +352,7 @@ int clayterm_size(int w, int h) { static void clay_error(Clay_ErrorData err) { (void)err; } -struct Clayterm *init(void *mem, int w, int h) { +struct Clayterm *init(void *mem, int w, int h, int row) { struct Clayterm *ct = (struct Clayterm *)mem; int cell_count = w * h; int cell_bytes = align8(cell_count * (int)sizeof(Cell)); @@ -369,6 +369,7 @@ struct Clayterm *init(void *mem, int w, int h) { *ct = (struct Clayterm){ .w = w, .h = h, + .row = row, .front = (Cell *)base, .back = (Cell *)(base + cell_bytes), .out = {base + cell_bytes * 2, 0, cell_count * OUT_BYTES_PER_CELL}, @@ -557,19 +558,19 @@ char *output(struct Clayterm *ct) { return ct->out.data; } int length(struct Clayterm *ct) { return ct->out.length; } -int pointer_over_count(void) { - return Clay_GetPointerOverIds().length; -} +int pointer_over_count(void) { return Clay_GetPointerOverIds().length; } int pointer_over_id_string_length(int index) { Clay_ElementIdArray ids = Clay_GetPointerOverIds(); - if (index >= ids.length) return 0; + if (index >= ids.length) + return 0; return ids.internalArray[index].stringId.length; } int pointer_over_id_string_ptr(int index) { Clay_ElementIdArray ids = Clay_GetPointerOverIds(); - if (index >= ids.length) return 0; + if (index >= ids.length) + return 0; return (int)ids.internalArray[index].stringId.chars; } diff --git a/src/clayterm.h b/src/clayterm.h index 918757c..f6bcbd7 100644 --- a/src/clayterm.h +++ b/src/clayterm.h @@ -11,7 +11,7 @@ struct Clayterm; /* WASM exports */ int clayterm_size(int w, int h); -struct Clayterm *init(void *mem, int w, int h); +struct Clayterm *init(void *mem, int w, int h, int row); void reduce(struct Clayterm *ct, uint32_t *buf, int len); char *output(struct Clayterm *ct); int length(struct Clayterm *ct); diff --git a/src/input.c b/src/input.c index af96707..c1ed25f 100644 --- a/src/input.c +++ b/src/input.c @@ -63,10 +63,14 @@ static struct InputEvent *emit(struct InputState *st) { static uint8_t mouse_mods(int b) { uint8_t m = 0; - if (b & 4) m |= MOD_SHIFT; - if (b & 8) m |= MOD_ALT; - if (b & 16) m |= MOD_CTRL; - if (b & 32) m |= MOD_MOTION; + if (b & 4) + m |= MOD_SHIFT; + if (b & 8) + m |= MOD_ALT; + if (b & 16) + m |= MOD_CTRL; + if (b & 32) + m |= MOD_MOTION; return m; } @@ -485,7 +489,8 @@ static int parse_csi_u(struct InputState *st, struct InputEvent *ev) { for (int j = off; j < i - 1 && tc < MAX_TEXT_CODEPOINTS; j++) { char c = st->buf[j]; if (c >= '0' && c <= '9') { - if (val == -1) val = 0; + if (val == -1) + val = 0; val = val * 10 + (c - '0'); } else if (c == ':') { if (val >= 0) @@ -504,38 +509,67 @@ static int parse_csi_u(struct InputState *st, struct InputEvent *ev) { static uint16_t csi_legacy_key(char term, int number) { switch (term) { - case 'A': return KEY_ARROW_UP; - case 'B': return KEY_ARROW_DOWN; - case 'C': return KEY_ARROW_RIGHT; - case 'D': return KEY_ARROW_LEFT; - case 'H': return KEY_HOME; - case 'F': return KEY_END; - case 'P': return KEY_F1; - case 'Q': return KEY_F2; - case 'S': return KEY_F4; + case 'A': + return KEY_ARROW_UP; + case 'B': + return KEY_ARROW_DOWN; + case 'C': + return KEY_ARROW_RIGHT; + case 'D': + return KEY_ARROW_LEFT; + case 'H': + return KEY_HOME; + case 'F': + return KEY_END; + case 'P': + return KEY_F1; + case 'Q': + return KEY_F2; + case 'S': + return KEY_F4; case '~': switch (number) { - case 2: return KEY_INSERT; - case 3: return KEY_DELETE; - case 5: return KEY_PGUP; - case 6: return KEY_PGDN; - case 7: return KEY_HOME; - case 8: return KEY_END; - case 11: return KEY_F1; - case 12: return KEY_F2; - case 13: return KEY_F3; - case 14: return KEY_F4; - case 15: return KEY_F5; - case 17: return KEY_F6; - case 18: return KEY_F7; - case 19: return KEY_F8; - case 20: return KEY_F9; - case 21: return KEY_F10; - case 23: return KEY_F11; - case 24: return KEY_F12; - default: return 0; + case 2: + return KEY_INSERT; + case 3: + return KEY_DELETE; + case 5: + return KEY_PGUP; + case 6: + return KEY_PGDN; + case 7: + return KEY_HOME; + case 8: + return KEY_END; + case 11: + return KEY_F1; + case 12: + return KEY_F2; + case 13: + return KEY_F3; + case 14: + return KEY_F4; + case 15: + return KEY_F5; + case 17: + return KEY_F6; + case 18: + return KEY_F7; + case 19: + return KEY_F8; + case 20: + return KEY_F9; + case 21: + return KEY_F10; + case 23: + return KEY_F11; + case 24: + return KEY_F12; + default: + return 0; } - default: return 0; + default: + return 0; } } @@ -578,8 +612,8 @@ static int parse_csi_legacy(struct InputState *st, struct InputEvent *ev) { mod = cur; cur = -1; sub++; - } else if ((c >= 'A' && c <= 'D') || c == 'F' || c == 'H' || - c == 'P' || c == 'Q' || c == 'S' || c == '~') { + } else if ((c >= 'A' && c <= 'D') || c == 'F' || c == 'H' || c == 'P' || + c == 'Q' || c == 'S' || c == '~') { if (param == 0) number = cur; else if (param == 1 && sub == 0) diff --git a/src/input.h b/src/input.h index 3254da3..cdc0845 100644 --- a/src/input.h +++ b/src/input.h @@ -69,9 +69,9 @@ /* ── Kitty action types ──────────────────────────────────────────── */ -#define ACTION_NONE 0 -#define ACTION_PRESS 1 -#define ACTION_REPEAT 2 +#define ACTION_NONE 0 +#define ACTION_PRESS 1 +#define ACTION_REPEAT 2 #define ACTION_RELEASE 3 /* ── Key codes ────────────────────────────────────────────────────── */ diff --git a/term-native.ts b/term-native.ts index d6cd67a..d17834a 100644 --- a/term-native.ts +++ b/term-native.ts @@ -11,7 +11,11 @@ export interface Native { import { compiled } from "./wasm.ts"; -export async function createTermNative(w: number, h: number): Promise { +export async function createTermNative( + w: number, + h: number, + row: number = 0, +): Promise { let memory = new WebAssembly.Memory({ initial: 2 }); let exports: Record = {}; @@ -43,7 +47,7 @@ export async function createTermNative(w: number, h: number): Promise { let ct = exports as unknown as { __heap_base: WebAssembly.Global; clayterm_size(w: number, h: number): number; - init(mem: number, w: number, h: number): number; + init(mem: number, w: number, h: number, row: number): number; reduce(ct: number, buf: number, len: number): void; output(ct: number): number; length(ct: number): number; @@ -64,7 +68,7 @@ export async function createTermNative(w: number, h: number): Promise { memory.grow(pages - current); } - let statePtr = ct.init(heap, w, h); + let statePtr = ct.init(heap, w, h, row); let opsBuf = (heap + size + 3) & ~3; return { diff --git a/term.ts b/term.ts index 739fbeb..676e650 100644 --- a/term.ts +++ b/term.ts @@ -4,6 +4,7 @@ import { createTermNative } from "./term-native.ts"; export interface TermOptions { height: number; width: number; + row?: number; } export interface RenderOptions { @@ -29,8 +30,8 @@ export interface Term { } export async function createTerm(options: TermOptions): Promise { - let { width, height } = options; - let native = await createTermNative(width, height); + let { width, height, row = 0 } = options; + let native = await createTermNative(width, height, row); let { memory, statePtr, opsBuf } = native; let prev = new Set(); diff --git a/test/term.test.ts b/test/term.test.ts index 9bcdddd..7994419 100644 --- a/test/term.test.ts +++ b/test/term.test.ts @@ -88,4 +88,62 @@ describe("term", () => { │ │ ╰──────────────────────────────────────╯`.trim()); }); + + describe("row offset", () => { + it("renders two frames at the offset position", async () => { + let term = await createTerm({ width: 20, height: 5, row: 5 }); + let box = (msg: string) => [ + open("root", { + layout: { width: grow(), height: grow(), direction: "ttb" }, + }), + open("box", { + layout: { + width: grow(), + height: grow(), + direction: "ttb", + padding: { left: 1, top: 1 }, + }, + border: { + color: rgba(255, 255, 255), + left: 1, + right: 1, + top: 1, + bottom: 1, + }, + }), + text(msg), + close(), + close(), + ]; + + let header = await createTerm({ width: 20, height: 5 }); + let banner = decode(header.render(box("hello")).output); + + let first = decode(term.render(box("world")).output); + expect(print(banner + first, 20, 10)).toEqual(`\ +┌──────────────────┐ +│hello │ +│ │ +│ │ +└──────────────────┘ +┌──────────────────┐ +│world │ +│ │ +│ │ +└──────────────────┘`); + + let second = decode(term.render(box("universe")).output); + expect(print(banner + first + second, 20, 10)).toEqual(`\ +┌──────────────────┐ +│hello │ +│ │ +│ │ +└──────────────────┘ +┌──────────────────┐ +│universe │ +│ │ +│ │ +└──────────────────┘`); + }); + }); }); -- 2.51.2