From 16642b40d39ed49a086d7aaf5873785518364e58 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Sun, 24 May 2026 03:21:29 -0500 Subject: [PATCH 1/5] refactor: alignment to use string literals instead of magic numbers --- examples/inline-regions/index.ts | 102 +++++++++++++++++++++++++++++++ examples/keyboard/index.ts | 14 ++--- ops.ts | 18 +++++- validate.ts | 16 ++++- 4 files changed, 138 insertions(+), 12 deletions(-) diff --git a/examples/inline-regions/index.ts b/examples/inline-regions/index.ts index 6968ff4..242d49e 100644 --- a/examples/inline-regions/index.ts +++ b/examples/inline-regions/index.ts @@ -48,6 +48,108 @@ const RAINBOW = [RED, ORANGE, YELLOW, NGREEN, BLUE, VIOLET]; const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +function* queryCursor(): Operation { + let parser = yield* until(createInput({ escLatency: 100 })); + write(DSR()); + + 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); + write(SHOWCURSOR()); + 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: "center", + }, + 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(); + + write(encode("\n".repeat(height))); + + let pos = yield* queryCursor(); + /** 1-based terminal row where the region starts */ + let row = pos.row - height + 1; + + write(ESC("7")); + 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); + write(ESC("8")); + write(encode("\n")); +} + +function say(msg: string) { + write(encode(msg + "\n")); +} + +function pause() { + waitKey(); + write(encode("\n")); +} + await main(function* () { let { columns } = terminalSize(); setRawMode(true); diff --git a/examples/keyboard/index.ts b/examples/keyboard/index.ts index bf7a7c8..fffa499 100644 --- a/examples/keyboard/index.ts +++ b/examples/keyboard/index.ts @@ -225,8 +225,8 @@ function key(ops: Op[], k: KeyDef, ctx: AppContext): void { width: fixed(w), height: grow(), padding: { left: 1, right: 1 }, - alignX: 2, - alignY: 2, + alignX: "center", + alignY: "center", }, bg, border: hover @@ -565,8 +565,8 @@ function keyboard(ctx: AppContext): Op[] { width: grow(), height: grow(), direction: "ttb", - alignX: 2, - alignY: 2, + alignX: "center", + alignY: "center", padding: { left: 2, top: 1 }, }, }), @@ -583,7 +583,7 @@ function keyboard(ctx: AppContext): Op[] { layout: { width: grow(), direction: "ltr", - alignY: 0, + alignY: "top", padding: { bottom: 1 }, }, }), @@ -634,7 +634,7 @@ function keyboard(ctx: AppContext): Op[] { // config panel (right) ops.push( - open("", { layout: { width: grow(), direction: "ltr", alignX: 1 } }), + open("", { layout: { width: grow(), direction: "ltr", alignX: "right" } }), ); configPanel(ops, ctx); ops.push(close()); @@ -648,7 +648,7 @@ function keyboard(ctx: AppContext): Op[] { layout: { direction: "ltr", gap: 3, - alignY: 1, + alignY: "bottom", padding: { left: 1, right: 1, top: 1, bottom: 1 }, }, border: { color: kbColor, left: 1, right: 1, top: 1, bottom: 1 }, diff --git a/ops.ts b/ops.ts index 776bfed..67116d2 100644 --- a/ops.ts +++ b/ops.ts @@ -132,7 +132,19 @@ export function pack( ); o += 4; - view.setUint32(o, (l.alignX ?? 0) | ((l.alignY ?? 0) << 8), true); + const alignX = l.alignX === "right" + ? 1 + : l.alignX === "center" + ? 2 + : 0; + + const alignY = l.alignY === "bottom" + ? 1 + : l.alignY === "center" + ? 2 + : 0; + + view.setUint32(o, alignX | (alignY << 8), true); o += 4; } @@ -275,8 +287,8 @@ export interface OpenElement { padding?: { left?: number; right?: number; top?: number; bottom?: number }; gap?: number; direction?: "ltr" | "ttb"; - alignX?: number; - alignY?: number; + alignX?: "left" | "center" | "right"; + alignY?: "top" | "center" | "bottom"; }; bg?: number; cornerRadius?: { tl?: number; tr?: number; bl?: number; br?: number }; diff --git a/validate.ts b/validate.ts index 248ea48..ef96b99 100644 --- a/validate.ts +++ b/validate.ts @@ -56,8 +56,20 @@ const Layout = Type.Object({ direction: Type.Optional( Type.Union([Type.Literal("ltr"), Type.Literal("ttb")]), ), - alignX: Type.Optional(u8), - alignY: Type.Optional(u8), + alignX: Type.Optional( + Type.Union([ + Type.Literal("left"), + Type.Literal("center"), + Type.Literal("right"), + ]), + ), + alignY: Type.Optional( + Type.Union([ + Type.Literal("top"), + Type.Literal("center"), + Type.Literal("bottom"), + ]), + ), }); const CornerRadius = Type.Object({ From 99bbd66a3b2c80e86f4639f7adfa2ab7de3619a9 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Sun, 24 May 2026 03:33:11 -0500 Subject: [PATCH 2/5] fix: linting issues --- ops.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ops.ts b/ops.ts index 67116d2..60e8353 100644 --- a/ops.ts +++ b/ops.ts @@ -132,13 +132,13 @@ export function pack( ); o += 4; - const alignX = l.alignX === "right" + let alignX = l.alignX === "right" ? 1 : l.alignX === "center" ? 2 : 0; - const alignY = l.alignY === "bottom" + let alignY = l.alignY === "bottom" ? 1 : l.alignY === "center" ? 2 From b8fa9e73d9ac336c85860f627a78de77a2c19ba7 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Sun, 24 May 2026 03:34:11 -0500 Subject: [PATCH 3/5] format --- ops.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ops.ts b/ops.ts index 60e8353..e747a5c 100644 --- a/ops.ts +++ b/ops.ts @@ -132,11 +132,7 @@ export function pack( ); o += 4; - let alignX = l.alignX === "right" - ? 1 - : l.alignX === "center" - ? 2 - : 0; + let alignX = l.alignX === "right" ? 1 : l.alignX === "center" ? 2 : 0; let alignY = l.alignY === "bottom" ? 1 From 764dd5a24e430941013268b66d4ecc5c2c0a6297 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Thu, 28 May 2026 20:20:29 -0500 Subject: [PATCH 4/5] docs: document string alignment in renderer spec --- specs/renderer-spec.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/specs/renderer-spec.md b/specs/renderer-spec.md index 6daced8..1bfa3c3 100644 --- a/specs/renderer-spec.md +++ b/specs/renderer-spec.md @@ -633,9 +633,9 @@ The `open()` constructor currently accepts the following property groups in its `props` parameter: - **`layout`** — sizing (width and height, specified via sizing helpers), - padding (per-side), alignment (currently numeric enum values, with a planned - transition to string literals), direction (top-to-bottom or left-to-right), - and gap + padding (per-side), alignment (`alignX`: `"left"` | `"center"` | `"right"`; + `alignY`: `"top"` | `"center"` | `"bottom"`, defaulting to left/top when + omitted), direction (top-to-bottom or left-to-right), and gap - **`border`** — per-side border widths and border color - **`cornerRadius`** — per-corner radius values, producing rounded box-drawing characters @@ -649,8 +649,7 @@ The `text()` constructor currently accepts: `color`, `fontSize`, `underline`, `strikethrough`). These property groups represent the current implementation surface. New groups -and fields have been added incrementally and more may follow. Alignment values -are expected to transition from numeric to string-literal form. +and fields have been added incrementally and more may follow. **Border width and layout interaction.** In the underlying layout engine (Clay), border configuration does not affect layout computation. This is Clay's intended From 7025bdcbb16b94ea5e1cfded1c1d346ed545fadb Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:08:56 -0600 Subject: [PATCH 5/5] refactor: remove unused functions and update alignment property to use string literal --- examples/inline-regions/index.ts | 104 +------------------------------ 1 file changed, 1 insertion(+), 103 deletions(-) diff --git a/examples/inline-regions/index.ts b/examples/inline-regions/index.ts index 242d49e..ca5c9aa 100644 --- a/examples/inline-regions/index.ts +++ b/examples/inline-regions/index.ts @@ -48,108 +48,6 @@ const RAINBOW = [RED, ORANGE, YELLOW, NGREEN, BLUE, VIOLET]; const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; -function* queryCursor(): Operation { - let parser = yield* until(createInput({ escLatency: 100 })); - write(DSR()); - - 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); - write(SHOWCURSOR()); - 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: "center", - }, - 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(); - - write(encode("\n".repeat(height))); - - let pos = yield* queryCursor(); - /** 1-based terminal row where the region starts */ - let row = pos.row - height + 1; - - write(ESC("7")); - 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); - write(ESC("8")); - write(encode("\n")); -} - -function say(msg: string) { - write(encode(msg + "\n")); -} - -function pause() { - waitKey(); - write(encode("\n")); -} - await main(function* () { let { columns } = terminalSize(); setRawMode(true); @@ -433,7 +331,7 @@ function box(msg: string, fg: number, border: number): Op[] { height: grow(), direction: "ttb", padding: { left: 1 }, - alignY: 2, + alignY: "center", }, border: { color: border,