diff --git a/src/clayterm.c b/src/clayterm.c index 6fd50c0..234e56c 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -191,6 +191,43 @@ static void present(struct Clayterm *ct) { } } +static void present_lines(struct Clayterm *ct) { + for (int y = 0; y < ct->h; y++) { + if (y > 0) + buf_put(&ct->out, "\n", 1); + for (int x = 0; x < ct->w;) { + Cell *back = cell_at(ct, ct->back, x, y); + Cell *front = cell_at(ct, ct->front, x, y); + + int w = wcwidth(back->ch); + if (w < 1) + w = 1; + + *front = *back; + + emit_attr(ct, back->fg, back->bg); + + if (w > 1 && x >= ct->w - (w - 1)) { + for (int i = x; i < ct->w; i++) { + buf_char(&ct->out, ' '); + } + } else { + uint32_t ch = back->ch; + if (!iswprint(ch)) + ch = 0xfffd; + buf_char(&ct->out, ch); + for (int i = 1; i < w; i++) { + Cell *fw = cell_at(ct, ct->front, x + i, y); + fw->ch = 0xffffffff; + fw->fg = 0xffffffff; + fw->bg = 0xffffffff; + } + } + x += w; + } + } +} + /* ── Color conversion ─────────────────────────────────────────────── */ static uint32_t color(Clay_Color c) { @@ -384,7 +421,7 @@ struct Clayterm *init(void *mem, int w, int h, int row) { return ct; } -void reduce(struct Clayterm *ct, uint32_t *buf, int len) { +void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode) { int i = 0; uint32_t idx = 0; @@ -550,8 +587,11 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len) { } } - /* diff front vs back, emit escape sequences */ - present(ct); + if (mode == 1) { + present_lines(ct); + } else { + present(ct); + } } char *output(struct Clayterm *ct) { return ct->out.data; } diff --git a/src/clayterm.h b/src/clayterm.h index f6bcbd7..43997b3 100644 --- a/src/clayterm.h +++ b/src/clayterm.h @@ -12,7 +12,7 @@ struct Clayterm; /* WASM exports */ int clayterm_size(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); +void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode); char *output(struct Clayterm *ct); int length(struct Clayterm *ct); void measure(int ret, int txt); diff --git a/term-native.ts b/term-native.ts index d17834a..7a5e5e3 100644 --- a/term-native.ts +++ b/term-native.ts @@ -2,7 +2,7 @@ export interface Native { memory: WebAssembly.Memory; statePtr: number; opsBuf: number; - reduce(ct: number, buf: number, len: number): void; + reduce(ct: number, buf: number, len: number, mode: number): void; output(ct: number): number; length(ct: number): number; setPointer(x: number, y: number, down: boolean): void; @@ -48,7 +48,7 @@ export async function createTermNative( __heap_base: WebAssembly.Global; clayterm_size(w: number, h: number): number; init(mem: number, w: number, h: number, row: number): number; - reduce(ct: number, buf: number, len: number): void; + reduce(ct: number, buf: number, len: number, mode: number): void; output(ct: number): number; length(ct: number): number; Clay_SetPointerState(vec: number, down: number): void; diff --git a/term.ts b/term.ts index 8f83c65..b4f0c93 100644 --- a/term.ts +++ b/term.ts @@ -8,6 +8,7 @@ export interface TermOptions { } export interface RenderOptions { + mode?: "line"; pointer?: { x: number; y: number; @@ -41,7 +42,8 @@ export async function createTerm(options: TermOptions): Promise { return { render(ops: Op[], options?: RenderOptions): RenderResult { let len = pack(ops, memory.buffer, opsBuf, memory.buffer.byteLength); - native.reduce(statePtr, opsBuf, len); + let mode = options?.mode === "line" ? 1 : 0; + native.reduce(statePtr, opsBuf, len, mode); if (options?.pointer) { let { x, y, down } = options.pointer; diff --git a/test/print.ts b/test/print.ts index bd32cf4..1801bb3 100644 --- a/test/print.ts +++ b/test/print.ts @@ -38,6 +38,10 @@ export function print(ansi: string, w: number, h: number): string { // SGR — ignore } // ignore all other CSI sequences (?25l, ?25h, etc.) + } else if (ansi[i] === "\n") { + y++; + x = 0; + i++; } else { // regular character — could be multi-byte UTF-8 let cp = ansi.codePointAt(i)!; diff --git a/test/term.test.ts b/test/term.test.ts index c1ac926..c45b1ea 100644 --- a/test/term.test.ts +++ b/test/term.test.ts @@ -4,6 +4,7 @@ import { close, grow, open, rgba, text } from "../ops.ts"; import { print } from "./print.ts"; const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes); +const trim = (s: string) => s.split("\n").map((l) => l.trimEnd()).join("\n"); describe("term", () => { let term: Term; @@ -89,6 +90,67 @@ describe("term", () => { ╰──────────────────────────────────────╯`.trim()); }); + describe("line mode", () => { + 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(), + ]; + + it("renders with newlines instead of CUP sequences", async () => { + let term = await createTerm({ width: 20, height: 5 }); + + let out = decode( + term.render(box("hello world"), { mode: "line" }).output, + ); + // deno-lint-ignore no-control-regex + expect(out).not.toMatch(/\x1b\[\d+;\d+H/); + expect(out.split("\n").length).toBe(5); + expect(trim(print(out, 20, 5))).toEqual(` +┌──────────────────┐ +│hello world │ +│ │ +│ │ +└──────────────────┘`.trim()); + }); + + it("primes front buffer for subsequent diff render", async () => { + let term = await createTerm({ width: 20, height: 5 }); + + let first = decode( + term.render(box("hello world"), { mode: "line" }).output, + ); + let second = decode(term.render(box("goodbye")).output); + + expect(trim(print(first + second, 20, 5))).toEqual(` +┌──────────────────┐ +│goodbye │ +│ │ +│ │ +└──────────────────┘`.trim()); + + expect(second.length).toBeLessThan(first.length); + }); + }); + describe("row offset", () => { it("renders two frames at the offset position", async () => { let term = await createTerm({ width: 20, height: 5, top: 5 });