From 17c12cd4f3e79b548cc9dcce6e23a6b4d8381da7 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 3 Apr 2026 20:01:39 -0500 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20add=20line=20rendering=20mode?= =?UTF-8?q?=20for=20pipe-friendly=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add { mode: "line" } option to render() that emits newline-separated rows instead of CUP positioning sequences. Line mode writes every cell and primes the front buffer, so subsequent diff renders work without a full redraw. --- src/clayterm.c | 46 ++++++++++++++++++++++++++++++++--- src/clayterm.h | 2 +- term-native.ts | 4 +-- term.ts | 4 ++- test/print.ts | 4 +++ test/term.test.ts | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+), 7 deletions(-) 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 }); From 0f964786d58a9b7242bca173d6c3df803d3d3c4b Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 8 Apr 2026 16:12:21 -0500 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=90=9B=20allow=20cell=20fill=20for=20?= =?UTF-8?q?different=20initialization=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This forces a render of all cells on the very first frame, which is perfectly acceptable. --- src/cell.c | 9 +++++---- src/cell.h | 2 +- src/clayterm.c | 28 +++++++++++++++++++++------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/cell.c b/src/cell.c index ddc3841..476a33b 100644 --- a/src/cell.c +++ b/src/cell.c @@ -2,11 +2,12 @@ #include "cell.h" -void cells_clear(Cell *buf, int w, int h) { +void cells_fill(Cell *buf, int w, int h, uint32_t ch, uint32_t fg, + uint32_t bg) { for (int i = 0; i < w * h; i++) { - buf[i].ch = ' '; - buf[i].fg = ATTR_DEFAULT; - buf[i].bg = ATTR_DEFAULT; + buf[i].ch = ch; + buf[i].fg = fg; + buf[i].bg = bg; } } diff --git a/src/cell.h b/src/cell.h index 7f1f320..5a1db90 100644 --- a/src/cell.h +++ b/src/cell.h @@ -24,7 +24,7 @@ typedef struct { #define ATTR_MASK 0xFF000000 #define COLOR_MASK 0x00FFFFFF -void cells_clear(Cell *buf, int w, int h); +void cells_fill(Cell *buf, int w, int h, uint32_t ch, uint32_t fg, uint32_t bg); int cell_cmp(Cell *a, Cell *b); #endif diff --git a/src/clayterm.c b/src/clayterm.c index 234e56c..494c9fb 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -149,9 +149,13 @@ static void emit_ch(struct Clayterm *ct, int x, int y, uint32_t ch) { buf_char(&ct->out, ch); } -/* ── Double-buffer diff (from termbox2 tb_present) ────────────────── */ - -static void present(struct Clayterm *ct) { +/** + * Diff back buffer against front buffer and emit only changed cells + * using absolute CUP positioning (\x1b[row;colH). Unchanged cells are + * skipped, making this efficient for subsequent frames where most of + * the screen is static. Derived from termbox2 tb_present. + */ +static void present_cups(struct Clayterm *ct) { ct->lastx = -1; ct->lasty = -1; @@ -191,6 +195,13 @@ static void present(struct Clayterm *ct) { } } +/** + * Emit back buffer as newline-separated rows without CUP positioning. + * Every cell is written (no diffing), and the front buffer is primed + * so that a subsequent present() call can diff efficiently. This is + * used for inline "region" rendering where the caller manages cursor + * positioning externally and the output must work in pipes. + */ static void present_lines(struct Clayterm *ct) { for (int y = 0; y < ct->h; y++) { if (y > 0) @@ -416,8 +427,11 @@ struct Clayterm *init(void *mem, int w, int h, int row) { .lasty = -1, }; - cells_clear(ct->front, w, h); - cells_clear(ct->back, w, h); + // initialize back buffer with spaces and default fg/bg + cells_fill(ct->back, w, h, ' ', ATTR_DEFAULT, ATTR_DEFAULT); + + // initialize front buffer with zeros. Every cell will be + cells_fill(ct->front, w, h, 0, 0, 0); return ct; } @@ -551,7 +565,7 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode) { ct->lastfg = ct->lastbg = 0xffffffff; ct->lastx = ct->lasty = -1; - cells_clear(ct->back, ct->w, ct->h); + cells_fill(ct->back, ct->w, ct->h, ' ', ATTR_DEFAULT, ATTR_DEFAULT); /* walk Clay render commands into back buffer */ for (int32_t j = 0; j < cmds.length; j++) { @@ -590,7 +604,7 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode) { if (mode == 1) { present_lines(ct); } else { - present(ct); + present_cups(ct); } } From c09a67edc4f90359a160b1fdf2c7896972e963b4 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Thu, 9 Apr 2026 11:52:42 -0500 Subject: [PATCH 3/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20make=20row=20a=201-bas?= =?UTF-8?q?ed=20render=20option=20matching=20DSR=20native=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the row offset from a constructor parameter to a render-time option. Row is now 1-based (matching ECMA-48 DSR/CPR format) so callers can pass the queried cursor position directly without conversion. Remove line mode from the inline region demo in favor of raw newline allocation followed by CUP rendering for all frames. --- demo/inline-region.out.txt | 16 ++ demo/inline-region.ts | 337 +++++++++++++++++++++++++++++++++++++ src/clayterm.c | 24 +-- src/clayterm.h | 4 +- term-native.ts | 15 +- term.ts | 20 ++- test/term.test.ts | 6 +- 7 files changed, 396 insertions(+), 26 deletions(-) create mode 100644 demo/inline-region.out.txt create mode 100644 demo/inline-region.ts diff --git a/demo/inline-region.out.txt b/demo/inline-region.out.txt new file mode 100644 index 0000000..1d15499 --- /dev/null +++ b/demo/inline-region.out.txt @@ -0,0 +1,16 @@ +[step 1] line mode render + ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠋ Compiling modules...","color":-7607811},{"id":4},{"id":4}] +[step 2] DSR query + cursor at top=52, region top=50 +[step 3] save cursor, start CUP renders +[step 4] CUP frame 1 + ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠙ Compiling modules...","color":-7607811},{"id":4},{"id":4}] +[step 5] CUP frame 2 + ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠹ Compiling modules...","color":-7607811},{"id":4},{"id":4}] +[step 6] CUP frame 3 + ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠸ Compiling modules...","color":-7607811},{"id":4},{"id":4}] +[step 7] CUP frame 4 + ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠼ Compiling modules...","color":-7607811},{"id":4},{"id":4}] +[step 8] CUP frame 5 + ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-11470213,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"✓ Compiling modules...","color":-11470213},{"id":4},{"id":4}] +[step 9] commit — restore cursor, advance diff --git a/demo/inline-region.ts b/demo/inline-region.ts new file mode 100644 index 0000000..48eb7d2 --- /dev/null +++ b/demo/inline-region.ts @@ -0,0 +1,337 @@ +/** + * Inline region demo — renders animated regions into normal scrollback. + * + * Shows the region lifecycle: + * 1. Allocate space with raw newlines + * 2. DSR — queries cursor position to compute `top` + * 3. CUP mode (all frames) — renders at `top` + * 4. Commit — restore cursor past region, advance with \n + */ + +import { main, type Operation, sleep, until } from "effection"; +import { + close, + createInput, + createTerm, + type CursorEvent, + fixed, + grow, + type Op, + open, + rgba, + text, +} from "../mod.ts"; +import { cursor, settings } from "../settings.ts"; +import { validated } from "../validate.ts"; + +let write = (b: Uint8Array) => Deno.stdout.writeSync(b); +let encode = (s: string) => new TextEncoder().encode(s); +let esc = (s: string) => write(encode(s)); + +let GREEN = rgba(80, 250, 123); +let GRAY = rgba(100, 100, 100); +let CYAN = rgba(139, 233, 253); + +let RED = rgba(255, 0, 0); +let ORANGE = rgba(255, 153, 0); +let YELLOW = rgba(255, 255, 0); +let NGREEN = rgba(51, 255, 0); +let BLUE = rgba(0, 153, 255); +let VIOLET = rgba(102, 0, 255); +let RAINBOW = [RED, ORANGE, YELLOW, NGREEN, BLUE, VIOLET]; + +let BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + +function* queryCursor(): Operation { + let parser = yield* until(createInput({ escLatency: 100 })); + esc("\x1b[6n"); + + let buf = new Uint8Array(32); + while (true) { + let n = Deno.stdin.readSync(buf); + if (n === null) continue; + let result = parser.scan(buf.subarray(0, n)); + for (let ev of result.events) { + if (ev.type === "cursor") { + return ev; + } + } + } +} + +function waitKey() { + let buf = new Uint8Array(32); + while (true) { + let n = Deno.stdin.readSync(buf); + if (n === null) continue; + for (let i = 0; i < n; i++) { + if (buf[i] === 0x03) { + Deno.stdin.setRaw(false); + esc("\x1b[?25h"); + Deno.exit(0); + } + } + return; + } +} + +function box(msg: string, fg: number, border: number): Op[] { + return [ + open("root", { + layout: { width: grow(), height: grow(), direction: "ttb" }, + }), + open("box", { + layout: { + width: grow(), + height: grow(), + direction: "ttb", + padding: { left: 1 }, + alignY: 2, + }, + border: { + color: border, + left: 1, + right: 1, + top: 1, + bottom: 1, + }, + cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 }, + }), + text(msg, { color: fg }), + close(), + close(), + ]; +} + +function* transaction( + height: number, + renderFrame: (frame: number) => Op[], + frames: number, + interval: number, +): Operation { + let { columns } = Deno.consoleSize(); + + esc("\n".repeat(height)); + + let pos = yield* queryCursor(); + /** 1-based terminal row where the region starts */ + let row = pos.top - height + 1; + + esc("\x1b[s"); + let tty = settings(cursor(false)); + write(tty.apply); + + let term = validated( + yield* until(createTerm({ width: columns, height })), + ); + for (let i = 0; i < frames; i++) { + let result = term.render(renderFrame(i), { row }); + write(new Uint8Array(result.output)); + yield* sleep(interval); + } + + write(tty.revert); + esc("\x1b[u"); + esc("\n"); +} + +function say(msg: string) { + esc(msg + "\n"); +} + +function pause() { + waitKey(); + esc("\n"); +} + +await main(function* () { + let { columns } = Deno.consoleSize(); + Deno.stdin.setRaw(true); + let tty = settings(cursor(false)); + write(tty.apply); + + // Introduction + say("Clayterm can render entire scenes, but it can also render"); + say('"inline" for a streaming UI. This is useful for semi-interactive'); + say("CLI commands that write output to the normal console screen."); + say(""); + + // Demo 1: Spinner box + esc("\n\n\n"); + + let pos = yield* queryCursor(); + /** 1-based terminal row where the region starts */ + let row = pos.top - 2; + + esc("\x1b[s"); + + let frames = 30; + let term = validated( + yield* until(createTerm({ width: columns, height: 3 })), + ); + + let first = term.render( + box("Press any key to compile modules.", CYAN, GRAY), + { row }, + ); + write(new Uint8Array(first.output)); + + waitKey(); + + for (let i = 0; i < frames; i++) { + let done = i === frames - 1; + let icon = done ? "✓" : BRAILLE[i % BRAILLE.length]; + let time = `${((i + 1) * 0.08).toFixed(1)}s`; + let label = done ? "Compiled modules" : "Compiling modules..."; + let result = term.render( + box( + `${icon} ${label} ${time}`, + done ? GREEN : CYAN, + done ? GREEN : GRAY, + ), + { row }, + ); + write(new Uint8Array(result.output)); + yield* sleep(80); + } + + esc("\x1b[u"); + esc("\x1b[0m"); + esc("\n"); + + yield* sleep(500); + + esc( + "\nRegions can be multi-line, but they can be a single line too. (continue...)", + ); + pause(); + + // Demo 2: Progress bar + let barWidth = Math.min(columns, 50); + let barFrames = 40; + yield* transaction( + 1, + (i) => { + let done = i === barFrames - 1; + if (done) { + return [ + open("root", { + layout: { + width: fixed(barWidth), + height: fixed(1), + direction: "ltr", + }, + }), + text("✓ Frobnicated", { color: GREEN }), + close(), + ]; + } + let progress = i / (barFrames - 1); + let label = "Frobnicating.. "; + let remaining = barWidth - label.length - 5; + let filled = Math.round(remaining * Math.min(progress, 1)); + let empty = remaining - filled; + let pct = `${Math.round(progress * 100)}%`; + let bar = "█".repeat(filled) + "░".repeat(empty); + return [ + open("root", { + layout: { + width: fixed(barWidth), + height: fixed(1), + direction: "ltr", + }, + }), + text(label, { color: CYAN }), + text(bar, { color: CYAN }), + text(` ${pct.padStart(4)}`, { color: GRAY }), + close(), + ]; + }, + barFrames, + 50, + ); + + esc("\x1b[0m"); + yield* sleep(500); + esc("\nGoodbye sadness with limitless sky. (continue...)"); + pause(); + + // Demo 3: Nyan cat + let nyanWidth = Math.min(columns, 120); + let nyanFrames = 50; + let cat = [ + "╭─────╮", + "│ ^.^ │", + "╰─────╯", + ]; + let catWidth = cat[0].length; + + yield* transaction( + 3, + (i) => { + let done = i === nyanFrames - 1; + let progress = i / (nyanFrames - 1); + let trail = Math.round((nyanWidth - catWidth) * Math.min(progress, 1)); + + if (done) { + // "IMAGINATION IS BEAUTIFUL WORLD!" in 3-row block font + let font: string[] = [ + "█ █▄█▄█ █▀█ █▀▀ █ █▀█ █▀█ ▀█▀ █ █▀█ █▀█ █ █▀▀ ██▄ █▀▀ █▀█ █ █ ▀█▀ █ █▀▀ █ █ █ █ █ █ █▀█ █▀█ █ █▀▄ █", + "█ █ ▀ █ █▀█ █ █ █ █ █ █▀█ █ █ █ █ █ █ █ ▀▀█ █▀█ █▀▀ █▀█ █ █ █ █ █▀ █ █ █ █▄█▄█ █ █ █▀▄ █ █ █ ▀", + "▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀▀ ▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀ █", + ]; + let ops: Op[] = [ + open("root", { + layout: { + width: fixed(nyanWidth), + height: fixed(3), + direction: "ttb", + }, + }), + ]; + for (let row = 0; row < 3; row++) { + let color = RAINBOW[(row * 2) % RAINBOW.length]; + ops.push(text(font[row], { color })); + } + ops.push(close()); + return ops; + } + + let ops: Op[] = [ + open("root", { + layout: { + width: fixed(nyanWidth), + height: fixed(3), + direction: "ttb", + }, + }), + ]; + + for (let row = 0; row < 3; row++) { + ops.push( + open(`row${row}`, { + layout: { width: grow(), height: fixed(1), direction: "ltr" }, + }), + ); + + if (trail > 0) { + let color = RAINBOW[(row * 2 + i) % RAINBOW.length]; + ops.push(text("█".repeat(trail), { color })); + } + + ops.push(text(cat[row], { color: CYAN })); + + ops.push(close()); + } + + ops.push(close()); + return ops; + }, + nyanFrames, + 60, + ); + + esc("\x1b[0m\n"); + write(tty.revert); + Deno.stdin.setRaw(false); +}); diff --git a/src/clayterm.c b/src/clayterm.c index 494c9fb..a9bedf6 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -35,7 +35,7 @@ /* ── Instance state ───────────────────────────────────────────────── */ struct Clayterm { - int w, h, row; + int w, h; Cell *front; Cell *back; Buffer out; @@ -129,17 +129,18 @@ static void emit_attr(struct Clayterm *ct, uint32_t fg, uint32_t bg) { ct->lastbg = bg; } -static void emit_cursor(struct Clayterm *ct, int x, int y) { +/** Emit CUP sequence. `row` is the 1-based terminal row of the region. */ +static void emit_cursor(struct Clayterm *ct, int x, int y, int row) { buf_str(&ct->out, "\x1b["); - buf_num(&ct->out, y + 1 + ct->row); + buf_num(&ct->out, y + row); buf_put(&ct->out, ";", 1); buf_num(&ct->out, x + 1); buf_put(&ct->out, "H", 1); } -static void emit_ch(struct Clayterm *ct, int x, int y, uint32_t ch) { +static void emit_ch(struct Clayterm *ct, int x, int y, int row, uint32_t ch) { if (ct->lastx != x - 1 || ct->lasty != y) { - emit_cursor(ct, x, y); + emit_cursor(ct, x, y, row); } ct->lastx = x; ct->lasty = y; @@ -155,7 +156,7 @@ static void emit_ch(struct Clayterm *ct, int x, int y, uint32_t ch) { * skipped, making this efficient for subsequent frames where most of * the screen is static. Derived from termbox2 tb_present. */ -static void present_cups(struct Clayterm *ct) { +static void present_cups(struct Clayterm *ct, int row) { ct->lastx = -1; ct->lasty = -1; @@ -177,9 +178,9 @@ static void present_cups(struct Clayterm *ct) { if (w > 1 && x >= ct->w - (w - 1)) { /* wide char doesn't fit, send spaces */ for (int i = x; i < ct->w; i++) - emit_ch(ct, i, y, ' '); + emit_ch(ct, i, y, row, ' '); } else { - emit_ch(ct, x, y, back->ch); + emit_ch(ct, x, y, row, back->ch); /* mark trailing cells of wide char as invalid in front * so they'll diff when overwritten by narrow chars */ for (int i = 1; i < w; i++) { @@ -400,7 +401,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, int row) { +struct Clayterm *init(void *mem, int w, int h) { struct Clayterm *ct = (struct Clayterm *)mem; int cell_count = w * h; int cell_bytes = align8(cell_count * (int)sizeof(Cell)); @@ -417,7 +418,6 @@ struct Clayterm *init(void *mem, int w, int h, int row) { *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}, @@ -435,7 +435,7 @@ struct Clayterm *init(void *mem, int w, int h, int row) { return ct; } -void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode) { +void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) { int i = 0; uint32_t idx = 0; @@ -604,7 +604,7 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode) { if (mode == 1) { present_lines(ct); } else { - present_cups(ct); + present_cups(ct, row); } } diff --git a/src/clayterm.h b/src/clayterm.h index 43997b3..c5d127a 100644 --- a/src/clayterm.h +++ b/src/clayterm.h @@ -11,8 +11,8 @@ 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, int mode); +struct Clayterm *init(void *mem, int w, int h); +void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row); 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 7a5e5e3..6e87598 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, mode: number): void; + reduce(ct: number, buf: number, len: number, mode: number, row: number): void; output(ct: number): number; length(ct: number): number; setPointer(x: number, y: number, down: boolean): void; @@ -14,7 +14,6 @@ import { compiled } from "./wasm.ts"; export async function createTermNative( w: number, h: number, - row: number = 0, ): Promise { let memory = new WebAssembly.Memory({ initial: 2 }); let exports: Record = {}; @@ -47,8 +46,14 @@ export async function createTermNative( let ct = exports as unknown as { __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, mode: number): void; + init(mem: number, w: number, h: number): number; + reduce( + ct: number, + buf: number, + len: number, + mode: number, + row: number, + ): void; output(ct: number): number; length(ct: number): number; Clay_SetPointerState(vec: number, down: number): void; @@ -68,7 +73,7 @@ export async function createTermNative( memory.grow(pages - current); } - let statePtr = ct.init(heap, w, h, row); + let statePtr = ct.init(heap, w, h); let opsBuf = (heap + size + 3) & ~3; return { diff --git a/term.ts b/term.ts index b4f0c93..2b6e9c1 100644 --- a/term.ts +++ b/term.ts @@ -4,11 +4,22 @@ import { createTermNative } from "./term-native.ts"; export interface TermOptions { height: number; width: number; - top?: number; } export interface RenderOptions { mode?: "line"; + + /** + * Row where to begin rendering. This should only be used when + * rendering into a region as part of the CLI main screen. For + * interfaces that use the entire screen, leave unset which will + * default to 0. This is 1-based which which is the DSR native + * format. + * + * https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ + */ + row?: number; + pointer?: { x: number; y: number; @@ -31,8 +42,8 @@ export interface Term { } export async function createTerm(options: TermOptions): Promise { - let { width, height, top = 0 } = options; - let native = await createTermNative(width, height, top); + let { width, height } = options; + let native = await createTermNative(width, height); let { memory, statePtr, opsBuf } = native; let prev = new Set(); @@ -43,7 +54,8 @@ export async function createTerm(options: TermOptions): Promise { render(ops: Op[], options?: RenderOptions): RenderResult { let len = pack(ops, memory.buffer, opsBuf, memory.buffer.byteLength); let mode = options?.mode === "line" ? 1 : 0; - native.reduce(statePtr, opsBuf, len, mode); + let row = options?.row ?? 1; + native.reduce(statePtr, opsBuf, len, mode, row); if (options?.pointer) { let { x, y, down } = options.pointer; diff --git a/test/term.test.ts b/test/term.test.ts index c45b1ea..00a4b4e 100644 --- a/test/term.test.ts +++ b/test/term.test.ts @@ -153,7 +153,7 @@ describe("term", () => { describe("row offset", () => { it("renders two frames at the offset position", async () => { - let term = await createTerm({ width: 20, height: 5, top: 5 }); + let term = await createTerm({ width: 20, height: 5 }); let box = (msg: string) => [ open("root", { layout: { width: grow(), height: grow(), direction: "ttb" }, @@ -181,7 +181,7 @@ describe("term", () => { let header = await createTerm({ width: 20, height: 5 }); let banner = decode(header.render(box("hello")).output); - let first = decode(term.render(box("world")).output); + let first = decode(term.render(box("world"), { row: 6 }).output); expect(print(banner + first, 20, 10)).toEqual(`\ ┌──────────────────┐ │hello │ @@ -194,7 +194,7 @@ describe("term", () => { │ │ └──────────────────┘`); - let second = decode(term.render(box("universe")).output); + let second = decode(term.render(box("universe"), { row: 6 }).output); expect(print(banner + first + second, 20, 10)).toEqual(`\ ┌──────────────────┐ │hello │ From 7b801bb8a3bd1a4bcd127885ed016cc14a013edb Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 10 Apr 2026 14:58:09 -0500 Subject: [PATCH 4/6] =?UTF-8?q?=E2=9C=A8=20add=20termcodes=20module=20and?= =?UTF-8?q?=20make=20CursorEvent=201-based?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename csi.ts to termcodes.ts and add ESC(), SHOWCURSOR(), HIDECURSOR(), ALTSCREEN(), and MAINSCREEN() helpers. Make CursorEvent.row/column 1-based to match DSR native format. Replace all raw escape sequences in the demo with termcodes. Use DECSC/DECRC (ESC 7/8) for cursor save/restore instead of SCO (CSI s/u). --- demo/inline-region.ts | 50 ++++++++++++++------------ input.ts | 10 +++--- mod.ts | 2 ++ settings.ts | 52 ++++++++++++++++----------- src/input.c | 4 +-- termcodes.ts | 84 +++++++++++++++++++++++++++++++++++++++++++ test/input.test.ts | 12 +++---- 7 files changed, 159 insertions(+), 55 deletions(-) create mode 100644 termcodes.ts diff --git a/demo/inline-region.ts b/demo/inline-region.ts index 48eb7d2..53720b9 100644 --- a/demo/inline-region.ts +++ b/demo/inline-region.ts @@ -13,20 +13,23 @@ import { close, createInput, createTerm, + CSI, type CursorEvent, + DSR, + ESC, fixed, grow, type Op, open, rgba, + SHOWCURSOR, text, } from "../mod.ts"; import { cursor, settings } from "../settings.ts"; import { validated } from "../validate.ts"; -let write = (b: Uint8Array) => Deno.stdout.writeSync(b); let encode = (s: string) => new TextEncoder().encode(s); -let esc = (s: string) => write(encode(s)); +let write = (b: Uint8Array) => Deno.stdout.writeSync(b); let GREEN = rgba(80, 250, 123); let GRAY = rgba(100, 100, 100); @@ -44,7 +47,7 @@ let BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", " function* queryCursor(): Operation { let parser = yield* until(createInput({ escLatency: 100 })); - esc("\x1b[6n"); + write(DSR()); let buf = new Uint8Array(32); while (true) { @@ -67,7 +70,7 @@ function waitKey() { for (let i = 0; i < n; i++) { if (buf[i] === 0x03) { Deno.stdin.setRaw(false); - esc("\x1b[?25h"); + write(SHOWCURSOR()); Deno.exit(0); } } @@ -111,13 +114,13 @@ function* transaction( ): Operation { let { columns } = Deno.consoleSize(); - esc("\n".repeat(height)); + write(encode("\n".repeat(height))); let pos = yield* queryCursor(); /** 1-based terminal row where the region starts */ - let row = pos.top - height + 1; + let row = pos.row - height + 1; - esc("\x1b[s"); + write(ESC("7")); let tty = settings(cursor(false)); write(tty.apply); @@ -131,17 +134,17 @@ function* transaction( } write(tty.revert); - esc("\x1b[u"); - esc("\n"); + write(ESC("8")); + write(encode("\n")); } function say(msg: string) { - esc(msg + "\n"); + write(encode(msg + "\n")); } function pause() { waitKey(); - esc("\n"); + write(encode("\n")); } await main(function* () { @@ -157,13 +160,13 @@ await main(function* () { say(""); // Demo 1: Spinner box - esc("\n\n\n"); + write(encode("\n\n\n")); let pos = yield* queryCursor(); /** 1-based terminal row where the region starts */ - let row = pos.top - 2; + let row = pos.row - 2; - esc("\x1b[s"); + write(ESC("7")); let frames = 30; let term = validated( @@ -195,14 +198,16 @@ await main(function* () { yield* sleep(80); } - esc("\x1b[u"); - esc("\x1b[0m"); - esc("\n"); + write(ESC("8")); + write(CSI("0m")); + write(encode("\n")); yield* sleep(500); - esc( - "\nRegions can be multi-line, but they can be a single line too. (continue...)", + write( + encode( + "\nRegions can be multi-line, but they can be a single line too. (continue...)", + ), ); pause(); @@ -251,9 +256,9 @@ await main(function* () { 50, ); - esc("\x1b[0m"); + write(CSI("0m")); yield* sleep(500); - esc("\nGoodbye sadness with limitless sky. (continue...)"); + write(encode("\nGoodbye sadness with limitless sky. (continue...)")); pause(); // Demo 3: Nyan cat @@ -331,7 +336,8 @@ await main(function* () { 60, ); - esc("\x1b[0m\n"); + write(CSI("0m")); + write(encode("\n")); write(tty.revert); Deno.stdin.setRaw(false); }); diff --git a/input.ts b/input.ts index 494f6fa..5163e3a 100644 --- a/input.ts +++ b/input.ts @@ -361,14 +361,14 @@ export interface CursorEvent { type: "cursor"; /** - * Cursor row (0-based). + * Cursor row (1-based). Matches ECMA-48 DSR native format. */ - top: number; + row: number; /** - * Cursor column (0-based). + * Cursor column (1-based). Matches ECMA-48 DSR native format. */ - left: number; + column: number; } import type { PointerEvent } from "./term.ts"; @@ -682,7 +682,7 @@ function mapEvent(native: NativeInputEvent): InputEvent { return { type: "resize", width: native.w, height: native.h }; } case EVENT_CURSOR: { - return { type: "cursor", top: native.y, left: native.x }; + return { type: "cursor", row: native.y, column: native.x }; } default: { return mapKeyEvent(native); diff --git a/mod.ts b/mod.ts index 8841400..8862d13 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,5 @@ export * from "./ops.ts"; export * from "./term.ts"; export * from "./input.ts"; +export * from "./settings.ts"; +export * from "./termcodes.ts"; diff --git a/settings.ts b/settings.ts index 0d9be06..48077e7 100644 --- a/settings.ts +++ b/settings.ts @@ -1,3 +1,12 @@ +import { + ALTSCREEN, + CSI, + ESC, + HIDECURSOR, + MAINSCREEN, + SHOWCURSOR, +} from "./termcodes.ts"; + export interface Setting { apply: Uint8Array; revert: Uint8Array; @@ -12,47 +21,50 @@ export function settings(...sequence: Setting[]): Setting { export function alternateBuffer(): Setting { return { - apply: csi("?1049h"), - revert: csi("?1049l"), + apply: ALTSCREEN(), + revert: MAINSCREEN(), }; } export function cursor(visible: boolean): Setting { if (visible) { return { - apply: csi("?25h"), - revert: csi("?25l"), + apply: SHOWCURSOR(), + revert: HIDECURSOR(), }; } else { return { - apply: csi("?25l"), - revert: csi("?25h"), + apply: HIDECURSOR(), + revert: SHOWCURSOR(), }; } } -export function progressiveInput(level: number): Setting { +/** + * Save and restore cursor position using DECSC (`ESC 7`) / DECRC (`ESC 8`). + * + * @see {@link https://vt100.net/docs/vt510-rm/DECSC.html | VT510 DECSC} + * @see {@link https://vt100.net/docs/vt510-rm/DECRC.html | VT510 DECRC} + */ +export function saveCursorPosition(): Setting { return { - apply: csi(`>${level}u`), - revert: csi("${level}u`), + revert: CSI("type = EVENT_CURSOR; - ev->y = row - 1; - ev->x = col - 1; + ev->y = row; + ev->x = col; shift(st, i); return PARSE_OK; } else { diff --git a/termcodes.ts b/termcodes.ts new file mode 100644 index 0000000..d7e0f01 --- /dev/null +++ b/termcodes.ts @@ -0,0 +1,84 @@ +/** + * Encode a plain escape sequence. + * + * Prepends `ESC` (`\x1b`) to the given string and returns the result as bytes. + * + * @see {@link https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ | ECMA-48} + */ +export function ESC(str: string): Uint8Array { + return encode(`\x1b${str}`); +} + +/** + * Encode a Control Sequence Introducer (CSI) command. + * + * Prepends `ESC[` to the given string and returns the result as bytes. + * + * @see {@link https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ | ECMA-48} + */ +export function CSI(str: string): Uint8Array { + return ESC(`[${str}`); +} + +/** + * Request the cursor position via Device Status Report (DSR). + * + * Sends `CSI 6n`. The terminal responds with a Cursor Position Report + * (`CSI row ; column R`) where row and column are 1-based. + * + * @see {@link https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ | ECMA-48} + */ +export function DSR(): Uint8Array { + return CSI("6n"); +} + +/** + * Show the cursor (DECTCEM set). + * + * DEC private mode 25. Not part of ECMA-48; originates from the VT220. + * + * @see {@link https://vt100.net/docs/vt510-rm/DECTCEM.html | VT510 DECTCEM} + */ +export function SHOWCURSOR(): Uint8Array { + return CSI("?25h"); +} + +/** + * Hide the cursor (DECTCEM reset). + * + * DEC private mode 25. Not part of ECMA-48; originates from the VT220. + * + * @see {@link https://vt100.net/docs/vt510-rm/DECTCEM.html | VT510 DECTCEM} + */ +export function HIDECURSOR(): Uint8Array { + return CSI("?25l"); +} + +/** + * Switch to the alternate screen buffer (xterm private mode 1049). + * + * Saves the cursor and switches to a clean alternate screen. Use + * {@link MAINSCREEN} to switch back. + * + * @see {@link https://invisible-island.net/xterm/ctlseqs/ctlseqs.html | xterm control sequences} + */ +export function ALTSCREEN(): Uint8Array { + return CSI("?1049h"); +} + +/** + * Switch back to the main screen buffer (xterm private mode 1049). + * + * Restores the cursor and returns to the main screen with scrollback intact. + * + * @see {@link https://invisible-island.net/xterm/ctlseqs/ctlseqs.html | xterm control sequences} + */ +export function MAINSCREEN(): Uint8Array { + return CSI("?1049l"); +} + +const encoder = new TextEncoder(); + +function encode(str: string): Uint8Array { + return encoder.encode(str); +} diff --git a/test/input.test.ts b/test/input.test.ts index 3eb81b8..38af941 100644 --- a/test/input.test.ts +++ b/test/input.test.ts @@ -687,8 +687,8 @@ describe("input", () => { expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ type: "cursor", - top: 23, - left: 79, + row: 24, + column: 80, }); }); @@ -697,8 +697,8 @@ describe("input", () => { expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ type: "cursor", - top: 0, - left: 0, + row: 1, + column: 1, }); }); @@ -708,8 +708,8 @@ describe("input", () => { expect(result.events[0]).toMatchObject({ type: "keydown", key: "a" }); expect(result.events[1]).toMatchObject({ type: "cursor", - top: 9, - left: 4, + row: 10, + column: 5, }); expect(result.events[2]).toMatchObject({ type: "keydown", key: "b" }); }); From feaa5c2714c9d0a3917d4c5a51eb491e8131f837 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 10 Apr 2026 15:04:11 -0500 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=94=A5=20remove=20accidentally=20comm?= =?UTF-8?q?itted=20demo=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/inline-region.out.txt | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 demo/inline-region.out.txt diff --git a/demo/inline-region.out.txt b/demo/inline-region.out.txt deleted file mode 100644 index 1d15499..0000000 --- a/demo/inline-region.out.txt +++ /dev/null @@ -1,16 +0,0 @@ -[step 1] line mode render - ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠋ Compiling modules...","color":-7607811},{"id":4},{"id":4}] -[step 2] DSR query - cursor at top=52, region top=50 -[step 3] save cursor, start CUP renders -[step 4] CUP frame 1 - ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠙ Compiling modules...","color":-7607811},{"id":4},{"id":4}] -[step 5] CUP frame 2 - ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠹ Compiling modules...","color":-7607811},{"id":4},{"id":4}] -[step 6] CUP frame 3 - ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠸ Compiling modules...","color":-7607811},{"id":4},{"id":4}] -[step 7] CUP frame 4 - ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-10197916,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"⠼ Compiling modules...","color":-7607811},{"id":4},{"id":4}] -[step 8] CUP frame 5 - ops: [{"id":2,"name":"root","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb"},"bg":-14144970},{"id":2,"name":"box","layout":{"width":{"type":"grow","min":0,"max":0},"height":{"type":"grow","min":0,"max":0},"direction":"ttb","padding":{"left":1},"alignY":2},"border":{"color":-11470213,"left":1,"right":1,"top":1,"bottom":1},"cornerRadius":{"tl":1,"tr":1,"bl":1,"br":1}},{"id":3,"content":"✓ Compiling modules...","color":-11470213},{"id":4},{"id":4}] -[step 9] commit — restore cursor, advance From e413755669bca75ac0dfa1c14759b3241546c909 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 10 Apr 2026 15:11:48 -0500 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=92=85=20use=20const=20for=20top-leve?= =?UTF-8?q?l=20constants=20in=20demos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/inline-region.ts | 32 ++++++++++++++++---------------- demo/keyboard.ts | 22 +++++++++++----------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/demo/inline-region.ts b/demo/inline-region.ts index 53720b9..75a5b92 100644 --- a/demo/inline-region.ts +++ b/demo/inline-region.ts @@ -28,22 +28,22 @@ import { import { cursor, settings } from "../settings.ts"; import { validated } from "../validate.ts"; -let encode = (s: string) => new TextEncoder().encode(s); -let write = (b: Uint8Array) => Deno.stdout.writeSync(b); - -let GREEN = rgba(80, 250, 123); -let GRAY = rgba(100, 100, 100); -let CYAN = rgba(139, 233, 253); - -let RED = rgba(255, 0, 0); -let ORANGE = rgba(255, 153, 0); -let YELLOW = rgba(255, 255, 0); -let NGREEN = rgba(51, 255, 0); -let BLUE = rgba(0, 153, 255); -let VIOLET = rgba(102, 0, 255); -let RAINBOW = [RED, ORANGE, YELLOW, NGREEN, BLUE, VIOLET]; - -let BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +const encode = (s: string) => new TextEncoder().encode(s); +const write = (b: Uint8Array) => Deno.stdout.writeSync(b); + +const GREEN = rgba(80, 250, 123); +const GRAY = rgba(100, 100, 100); +const CYAN = rgba(139, 233, 253); + +const RED = rgba(255, 0, 0); +const ORANGE = rgba(255, 153, 0); +const YELLOW = rgba(255, 255, 0); +const NGREEN = rgba(51, 255, 0); +const BLUE = rgba(0, 153, 255); +const VIOLET = rgba(102, 0, 255); +const RAINBOW = [RED, ORANGE, YELLOW, NGREEN, BLUE, VIOLET]; + +const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; function* queryCursor(): Operation { let parser = yield* until(createInput({ escLatency: 100 })); diff --git a/demo/keyboard.ts b/demo/keyboard.ts index 4c5bfd2..0dce0c3 100644 --- a/demo/keyboard.ts +++ b/demo/keyboard.ts @@ -33,15 +33,15 @@ import { import { useInput } from "./use-input.ts"; import { useStdin } from "./use-stdin.ts"; -let active = rgba(60, 120, 220); -let inactive = rgba(50, 50, 60); -let on = rgba(40, 180, 80); -let label = rgba(220, 220, 220); -let dim = rgba(100, 100, 120); -let highlight = rgba(255, 220, 80); +const active = rgba(60, 120, 220); +const inactive = rgba(50, 50, 60); +const on = rgba(40, 180, 80); +const label = rgba(220, 220, 220); +const dim = rgba(100, 100, 120); +const highlight = rgba(255, 220, 80); -let KEY_W = 5; -let GAP = 1; +const KEY_W = 5; +const GAP = 1; interface KeyDef { label: string; @@ -58,7 +58,7 @@ function matches(k: KeyDef, event: InputEvent | PointerEvent): boolean { event.code.toUpperCase() === k.code.toUpperCase(); } -let hovered = rgba(80, 80, 100); +const hovered = rgba(80, 80, 100); function key(ops: Op[], k: KeyDef, ctx: AppContext): void { let pressed = ctx.event && matches(k, ctx.event); @@ -328,7 +328,7 @@ function toggle(ops: Op[], enabled: boolean, name: string): void { ); } -let flagNames: +const flagNames: (keyof Omit)[] = [ "Disambiguate escape codes", @@ -338,7 +338,7 @@ let flagNames: "Report associated text", ]; -let logEntries: { key: string; name: keyof EventFilter }[] = [ +const logEntries: { key: string; name: keyof EventFilter }[] = [ { key: "a", name: "keydown" }, { key: "b", name: "keyup" }, { key: "c", name: "keyrepeat" },