diff --git a/bun.lock b/bun.lock index c93da167..e4a43240 100644 --- a/bun.lock +++ b/bun.lock @@ -234,6 +234,7 @@ "astro": "^7.1.6", }, "devDependencies": { + "@opentui/qrcode": "workspace:*", "@types/bun": "1.3.14", "@xterm/addon-unicode11": "^0.8.0", "@xterm/headless": "^5.5.0", diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index 45ac3308..f4c38128 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -7,6 +7,15 @@ const copyButtonTransformer = { pre(node) { node.properties["data-code"] = this.source if (this.options?.lang) node.properties["data-language"] = this.options.lang + + if (this.options?.lang === "text") { + const metadata = this.options.meta?.__raw ?? "" + const visual = metadata.match(/(?:^|\s)terminal=([a-z0-9-]+)(?=\s|$)/) + if (visual) { + node.properties["data-terminal-visual"] = visual[1] + if (/(?:^|\s)surface(?=\s|$)/.test(metadata)) node.properties["data-terminal-surface"] = true + } + } }, } diff --git a/packages/web/package.json b/packages/web/package.json index 02f77f5f..cf3e0ef2 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -11,13 +11,14 @@ "optimize:video": "bash scripts/optimize-landing-video.sh", "recordings:record": "bash scripts/recordings/bin/record", "recordings:build": "bun scripts/recordings/bin/build", + "docs:visuals": "bun scripts/generate-doc-visuals.ts", "scaffold": "bun scripts/test-doc-examples.ts", "validate:docs:metadata": "bun scripts/validate-doc-metadata.ts", "validate:docs:links": "bun scripts/validate-doc-links.ts", "validate:docs:skill": "bun scripts/validate-skill-docs.ts", "validate:docs:examples": "bun scripts/verify-doc-examples.ts", "validate:docs": "bun run validate:docs:metadata && bun run validate:docs:links && bun run validate:docs:skill && bun run test:docs", - "test:docs": "bun test src/lib/docs-index.test.ts src/lib/docs-search.test.ts scripts/doc-validator.test.ts", + "test:docs": "bun test src/lib/docs-index.test.ts src/lib/docs-search.test.ts scripts/doc-validator.test.ts scripts/doc-visuals.test.ts scripts/terminal-illustration.test.ts", "validate:packages": "bun scripts/validate-packages.ts src/content/packages", "test:packages": "bun test src/lib/package-facts.test.ts src/lib/package-schema.test.ts src/lib/remote-package-facts.test.ts" }, @@ -29,6 +30,7 @@ "astro": "^7.1.6" }, "devDependencies": { + "@opentui/qrcode": "workspace:*", "@types/bun": "1.3.14", "@xterm/addon-unicode11": "^0.8.0", "@xterm/headless": "^5.5.0", diff --git a/packages/web/scripts/doc-visuals.test.ts b/packages/web/scripts/doc-visuals.test.ts new file mode 100644 index 00000000..488fa453 --- /dev/null +++ b/packages/web/scripts/doc-visuals.test.ts @@ -0,0 +1,287 @@ +import { readFile, readdir } from "node:fs/promises" +import { LineNumberRenderable, RGBA, rgbToHex } from "@opentui/core" +import { expect, test } from "bun:test" +import visuals from "../src/data/doc-visuals.json" +import { renderDocVisual } from "./doc-visuals/shared" +import { structuredVisuals } from "./doc-visuals/structured" +import { generateDocVisuals } from "./generate-doc-visuals" + +test("documentation visual preserves native terminal geometry and color intent", async () => { + const generated = await generateDocVisuals() + const frame = generated["box-borders"] + + expect(JSON.stringify(generated)).toBe(JSON.stringify(visuals)) + expect(frame.cols).toBe(38) + expect(frame.rows).toBe(7) + + for (const visual of Object.values(generated)) { + expect(visual.lines).toHaveLength(visual.rows) + + for (const line of visual.lines) { + expect(line.reduce((width, span) => width + span.width, 0)).toBe(visual.cols) + } + + if (visual.cursor) { + expect(visual.cursor.column).toBeGreaterThanOrEqual(0) + expect(visual.cursor.column).toBeLessThan(visual.cols) + expect(visual.cursor.row).toBeGreaterThanOrEqual(0) + expect(visual.cursor.row).toBeLessThan(visual.rows) + } + } + + const spans = frame.lines.flat() + expect(spans.some((span) => span.text.includes("single") && frame.colors[span.foreground].intent === "default")).toBe( + true, + ) + expect(spans.every((span) => frame.colors[span.background].intent === "default")).toBe(true) + expect(spans.some((span) => span.text === "single line" && frame.colors[span.foreground].slot === 244)).toBe(true) + + const embedded = generated["embedded-terminal-vt"] + const terminalSpans = embedded.lines.flat() + expect(terminalSpans.every((span) => embedded.colors[span.background].intent === "default")).toBe(true) + expect( + terminalSpans.some( + (span) => span.text.includes("bun test") && embedded.colors[span.foreground].intent === "default", + ), + ).toBe(true) + expect(terminalSpans.some((span) => span.text.includes("$") && embedded.colors[span.foreground].slot === 244)).toBe( + true, + ) + expect(generated["frame-buffer-draw"].label).toContain("42 MB/s") + expect(generated["frame-buffer-draw"].label).toContain("18 MB/s") + expect(generated["frame-buffer-progress"].label).toContain("70%") + expect(generated["interaction-selection-focus"].label).toContain("deploy --check") + expect(generated["text-cell-ruler"].label).toContain("three cells") + expect(generated["text-cell-ruler"].label).toContain("columns 1 and 2") + + for (const id of ["input-focused", "textarea-selection", "interaction-selection-focus"]) { + expect(generated[id].cursor).not.toBeNull() + } + + function backgroundAt(visual: typeof frame, x: number, y: number) { + let column = 0 + for (const span of visual.lines[y]) { + column += span.width + if (column > x) return visual.colors[span.background] + } + throw new Error(`Missing cell at ${x}, ${y}`) + } + + const created = generated["renderable-created"] + const mutated = generated["renderable-mutated"] + expect([created.cols, mutated.cols]).toEqual([18, 30]) + expect(created.lines[1].map((span) => span.text).join("")).toContain("Waiting") + expect(mutated.lines[1].map((span) => span.text).join("")).toContain("Ready") + + const beforeMove = generated["renderable-reparent-before"] + const afterMove = generated["renderable-reparent-after"] + expect(backgroundAt(beforeMove, 2, 1).slot).toBe(243) + expect(backgroundAt(beforeMove, 20, 1).intent).toBe("default") + expect(backgroundAt(afterMove, 2, 1).intent).toBe("default") + expect(backgroundAt(afterMove, 20, 1).slot).toBe(243) + expect(generated["renderable-visibility"].lines[5].map((span) => span.text).join("")).toContain( + "children: 2 children: 2 children: 1", + ) + + const chunks = generated["text-styled-chunks"].lines.flat() + expect(chunks.find((span) => span.text === "Status")?.attributes).toBe(1) + expect(chunks.find((span) => span.text === "Note")?.attributes).toBe(4) + expect(chunks.find((span) => span.text === "Next")?.attributes).toBe(9) + const ruler = generated["text-cell-ruler"] + expect(ruler.lines[1].map((span) => span.text).join("")).toContain("ABC| 3 cells") + expect(ruler.lines[2].map((span) => span.text).join("")).toContain("A\u754cB| 4 cells") + expect(ruler.lines[2].find((span) => span.text === "\u754c")?.width).toBe(2) + expect(ruler.lines[2].find((span) => span.text === "B")?.width).toBe(1) + expect(backgroundAt(ruler, 11, 2).slot).toBe(235) + expect(backgroundAt(ruler, 12, 2).intent).toBe("default") + const offsets = generated["text-line-offsets"] + expect(backgroundAt(offsets, 0, 3).slot).toBe(238) + expect(backgroundAt(offsets, 19, 3).slot).toBe(238) + expect(offsets.lines[4].map((span) => span.text).join("")).toContain("range [3, 4) range [4, 5)") + + const hits = generated["interaction-hit-bubbling"] + expect(hits.lines[7].map((span) => span.text).join("")).toContain("target: front") + expect(hits.lines[8].map((span) => span.text).join("")).toContain("bubble: front -> parent") + expect(hits.lines[9].map((span) => span.text).join("")).toContain("stopped: front") + const drag = generated["interaction-drag-selection"] + expect(drag.lines[3].map((span) => span.text).join("")).toContain('Selected: "the app\\nTest"') + expect(drag.lines[4].map((span) => span.text).join("")).toContain("Range: [6, 18)") + + const spacing = generated["layout-flex-columns"] + for (let x = 0; x < spacing.cols; x++) { + const shaded = (x >= 2 && x < 10) || (x >= 12 && x < 32) + expect(backgroundAt(spacing, x, 2).slot).toBe(shaded ? 238 : undefined) + } + + const alignment = generated["layout-alignment"] + for (const [x, y] of [ + [2, 3], + [7, 3], + [13, 2], + [18, 4], + [24, 5], + [29, 5], + ]) { + expect(backgroundAt(alignment, x, y).slot).toBe(238) + } + for (const [x, y] of [ + [2, 2], + [13, 1], + [24, 3], + ]) { + expect(backgroundAt(alignment, x, y).intent).toBe("default") + } + + const rounding = generated["layout-cell-rounding"] + for (const [row, widths] of [ + [2, [10, 10, 10]], + [6, [10, 11, 10]], + ] as const) { + let left = 1 + for (const [index, width] of widths.entries()) { + for (let x = left; x < left + width; x++) { + expect(backgroundAt(rounding, x, row).slot).toBe(index === 1 ? 235 : 238) + } + left += width + } + } + + const wide = generated["layout-wrap-wide"] + const narrow = generated["layout-wrap-narrow"] + expect([wide.cols, wide.rows]).toEqual([32, 4]) + expect([narrow.cols, narrow.rows]).toEqual([22, 6]) + expect(wide.lines[2].map((span) => span.text).join("")).toContain("C") + expect(narrow.lines[2].map((span) => span.text).join("")).not.toContain("C") + expect(narrow.lines[4].map((span) => span.text).join("")).toContain("C") + expect(backgroundAt(wide, 22, 2).slot).toBe(238) + expect(backgroundAt(narrow, 2, 4).slot).toBe(238) + + const palette = generated["color-palette"] + for (let index = 0; index < 256; index++) { + const red = Math.floor((index - 16) / 36) + const green = Math.floor((index - 16) / 6) % 6 + const blue = (index - 16) % 6 + const x = index < 16 ? index * 2 : index < 232 ? (red % 3) * 13 + blue * 2 : index - 232 + const y = index < 16 ? 1 : index < 232 ? 5 + Math.floor(red / 3) * 8 + green : 21 + const color = backgroundAt(palette, x, y) + + expect(color.intent).toBe("rgb") + expect(color.value).toBe(rgbToHex(RGBA.fromIndex(index))) + } + + const alpha = generated["color-alpha"] + for (const [index, [light, dark]] of [ + ["#e0e0e0", "#a0a0a0"], + ["#b0d9bf", "#80a98f"], + ["#81d29f", "#61b37f"], + ["#52cc7f", "#42bc6f"], + ["#22c55e", "#22c55e"], + ].entries()) { + expect(backgroundAt(alpha, index * 7, 0).value).toBe(light) + expect(backgroundAt(alpha, index * 7 + 2, 0).value).toBe(dark) + } + + const directory = new URL("../src/content/docs/", import.meta.url) + const files = (await readdir(directory, { recursive: true })).filter((path) => path.endsWith(".mdx")) + const referenced = new Set() + + for (const path of files) { + const source = await readFile(new URL(path, directory), "utf8") + + for (const match of source.matchAll(/```text terminal=([a-z0-9-]+)(?:[^\S\n]+[^\n]*)?\n([\s\S]*?)\n```/g)) { + const visual = generated[match[1]] + expect(visual).toBeDefined() + + const text = visual.lines + .map((line) => + line + .map((span) => span.text) + .join("") + .trimEnd(), + ) + .join("\n") + + expect(match[2]).toBe(text) + referenced.add(match[1]) + } + } + + expect([...referenced].toSorted()).toEqual(Object.keys(generated).toSorted()) +}) + +test("failed visual setup restores renderer globals and runs registered cleanup", async () => { + const originalWindow = globalThis.window + const originalRequestAnimationFrame = globalThis.requestAnimationFrame + const originalCancelAnimationFrame = globalThis.cancelAnimationFrame + let cleaned = false + + await expect( + renderDocVisual({ + id: "failed-visual", + label: "Failed visual", + width: 8, + height: 2, + render(setup, registerCleanup) { + registerCleanup(() => { + cleaned = setup.renderer.isDestroyed + }) + throw new Error("Fixture initialization failed") + }, + }), + ).rejects.toThrow("Fixture initialization failed") + + expect(cleaned).toBe(true) + expect(globalThis.window).toBe(originalWindow) + expect(globalThis.requestAnimationFrame).toBe(originalRequestAnimationFrame) + expect(globalThis.cancelAnimationFrame).toBe(originalCancelAnimationFrame) +}) + +test("every registered visual cleanup runs when another cleanup fails", async () => { + const calls: string[] = [] + + await expect( + renderDocVisual({ + id: "failed-cleanup", + label: "Failed cleanup", + width: 8, + height: 2, + render(setup, registerCleanup) { + registerCleanup(() => { + calls.push(setup.renderer.isDestroyed ? "first" : "renderer still active") + }) + registerCleanup(() => { + calls.push("second") + throw new Error("Cleanup failed") + }) + registerCleanup(() => { + calls.push("third") + }) + }, + }), + ).rejects.toThrow("Cleanup failed") + + expect(calls).toEqual(["third", "second", "first"]) +}) + +test("detached line-number visuals destroy their owned children after setup failure", async () => { + const setLineSign = LineNumberRenderable.prototype.setLineSign + let gutter: LineNumberRenderable | undefined + let children: Array<{ isDestroyed: boolean }> = [] + + LineNumberRenderable.prototype.setLineSign = function (this: LineNumberRenderable) { + gutter = this + children = this.getChildren() + throw new Error("Line sign initialization failed") + } + + try { + const fixture = structuredVisuals.find((visual) => visual.id === "line-number-editor")! + await expect(renderDocVisual(fixture)).rejects.toThrow("Line sign initialization failed") + } finally { + LineNumberRenderable.prototype.setLineSign = setLineSign + } + + expect(gutter?.isDestroyed).toBe(true) + expect(children).toHaveLength(2) + expect(children.every((child) => child.isDestroyed)).toBe(true) +}) diff --git a/packages/web/scripts/doc-visuals/forms.ts b/packages/web/scripts/doc-visuals/forms.ts new file mode 100644 index 00000000..8e6f2dd3 --- /dev/null +++ b/packages/web/scripts/doc-visuals/forms.ts @@ -0,0 +1,221 @@ +import { + BoxRenderable, + InputRenderable, + RGBA, + SelectRenderable, + TabSelectRenderable, + TextareaRenderable, + TextRenderable, +} from "@opentui/core" +import type { DocVisualFixture } from "./shared" + +const foreground = RGBA.defaultForeground() +const background = RGBA.defaultBackground() +const muted = RGBA.fromIndex(244) +const selection = RGBA.fromIndex(238) + +export const formVisuals: DocVisualFixture[] = [ + { + id: "input-placeholder", + label: "An unfocused name input displaying the placeholder Enter your name", + width: 28, + height: 2, + render({ renderer }) { + const field = new BoxRenderable(renderer, { width: 28, height: 2 }) + + field.add(new TextRenderable(renderer, { content: "Name", fg: muted, bg: background })) + field.add( + new InputRenderable(renderer, { + width: 28, + placeholder: "Enter your name", + placeholderColor: muted, + backgroundColor: background, + focusedBackgroundColor: selection, + textColor: foreground, + focusedTextColor: foreground, + cursorColor: foreground, + }), + ) + + renderer.root.add(field) + }, + }, + { + id: "input-focused", + label: "A focused name input containing Ada Lovelace with its cursor visible", + width: 28, + height: 2, + cursor: true, + render({ renderer, mockInput }) { + const field = new BoxRenderable(renderer, { width: 28, height: 2 }) + const input = new InputRenderable(renderer, { + width: 28, + placeholder: "Enter your name", + placeholderColor: muted, + backgroundColor: background, + focusedBackgroundColor: selection, + textColor: foreground, + focusedTextColor: foreground, + cursorColor: foreground, + }) + + field.add(new TextRenderable(renderer, { content: "Name", fg: muted, bg: background })) + field.add(input) + renderer.root.add(field) + input.focus() + mockInput.typeText("Ada Lovelace") + }, + }, + { + id: "textarea-wrap", + label: "A multiline textarea wrapping a long line at a word boundary", + width: 30, + height: 4, + render({ renderer }) { + const field = new BoxRenderable(renderer, { width: 30, height: 4 }) + + field.add(new TextRenderable(renderer, { content: "Notes", fg: muted, bg: background })) + field.add( + new TextareaRenderable(renderer, { + width: 30, + height: 3, + initialValue: "Long lines wrap at word boundaries.\nKeep paragraphs readable.", + wrapMode: "word", + backgroundColor: background, + focusedBackgroundColor: background, + textColor: foreground, + focusedTextColor: foreground, + cursorColor: foreground, + }), + ) + + renderer.root.add(field) + }, + }, + { + id: "textarea-selection", + label: "A focused multiline textarea with keyboard focus selected", + width: 30, + height: 4, + cursor: true, + render({ renderer }) { + const value = "Plan the release\nReview keyboard focus\nShip the update" + const field = new BoxRenderable(renderer, { width: 30, height: 4 }) + const textarea = new TextareaRenderable(renderer, { + width: 30, + height: 3, + initialValue: value, + backgroundColor: background, + focusedBackgroundColor: background, + textColor: foreground, + focusedTextColor: foreground, + selectionBg: selection, + selectionFg: foreground, + cursorColor: foreground, + }) + + field.add(new TextRenderable(renderer, { content: "Draft", fg: muted, bg: background })) + field.add(textarea) + renderer.root.add(field) + textarea.focus() + textarea.setCursor(1, "Review keyboard focus".length) + + const start = value.indexOf("keyboard focus") + textarea.setSelection(start, start + "keyboard focus".length) + }, + }, + { + id: "select-options", + label: "New file, Open file, and Save options with Open file selected", + width: 32, + height: 6, + render({ renderer, mockInput }) { + const select = new SelectRenderable(renderer, { + width: 32, + height: 6, + options: [ + { name: "New file", description: "Create a document" }, + { name: "Open file", description: "Browse existing files" }, + { name: "Save", description: "Write current changes" }, + ], + backgroundColor: background, + focusedBackgroundColor: background, + textColor: foreground, + focusedTextColor: foreground, + selectedBackgroundColor: selection, + selectedTextColor: foreground, + descriptionColor: muted, + selectedDescriptionColor: foreground, + }) + + renderer.root.add(select) + select.focus() + mockInput.pressArrow("down") + }, + }, + { + id: "tab-select-tabs", + label: "Home, Files, and Settings tabs with Files selected and underlined", + width: 36, + height: 3, + render({ renderer, mockInput }) { + const tabs = new TabSelectRenderable(renderer, { + width: 36, + tabWidth: 12, + options: [ + { name: "Home", description: "View project overview" }, + { name: "Files", description: "Browse project files" }, + { name: "Settings", description: "Configure the project" }, + ], + backgroundColor: background, + focusedBackgroundColor: background, + textColor: muted, + focusedTextColor: muted, + selectedBackgroundColor: selection, + selectedTextColor: foreground, + selectedDescriptionColor: muted, + }) + + renderer.root.add(tabs) + tabs.focus() + mockInput.pressArrow("right") + }, + }, + { + id: "interaction-selection-focus", + label: "The word text is selected above a focused command input containing deploy --check", + width: 32, + height: 4, + cursor: true, + async render({ renderer, mockInput, mockMouse, renderOnce }) { + const layout = new BoxRenderable(renderer, { width: 32, height: 4, gap: 1 }) + const text = new TextRenderable(renderer, { + width: 32, + content: "Select text, then focus input", + fg: foreground, + bg: background, + selectionBg: selection, + selectionFg: foreground, + }) + const field = new BoxRenderable(renderer, { width: 32, height: 2 }) + const input = new InputRenderable(renderer, { + width: 32, + backgroundColor: background, + focusedBackgroundColor: selection, + textColor: foreground, + focusedTextColor: foreground, + cursorColor: foreground, + }) + + field.add(new TextRenderable(renderer, { content: "Command", fg: muted, bg: background })) + field.add(input) + layout.add(text) + layout.add(field) + renderer.root.add(layout) + await renderOnce() + await mockMouse.drag(text.x + 7, text.y, text.x + 10, text.y, undefined, { delayMs: 0 }) + input.focus() + mockInput.typeText("deploy --check") + }, + }, +] diff --git a/packages/web/scripts/doc-visuals/foundation.ts b/packages/web/scripts/doc-visuals/foundation.ts new file mode 100644 index 00000000..fe8e5efc --- /dev/null +++ b/packages/web/scripts/doc-visuals/foundation.ts @@ -0,0 +1,128 @@ +import { + BoxRenderable, + FrameBufferRenderable, + RGBA, + TextRenderable, + bold, + fg, + italic, + t, + underline, +} from "@opentui/core" +import type { DocVisualFixture } from "./shared" + +const foreground = RGBA.defaultForeground() +const muted = RGBA.fromIndex(244) + +export const foundationVisuals: DocVisualFixture[] = [ + { + id: "text-attributes", + label: "Text attributes applied to bold, italic, and underlined content", + width: 30, + height: 3, + render({ renderer }) { + for (const content of [ + t`${fg(muted)("bold ")}${bold("Important message")}`, + t`${fg(muted)("italic ")}${italic("Additional context")}`, + t`${fg(muted)("underline ")}${underline("Documentation")}`, + ]) { + renderer.root.add(new TextRenderable(renderer, { content, fg: foreground })) + } + }, + }, + { + id: "box-title-alignment", + label: "Box with a centered top title and right-aligned bottom title", + width: 32, + height: 3, + render({ renderer }) { + const box = new BoxRenderable(renderer, { + width: 32, + height: 3, + border: true, + borderColor: foreground, + title: "settings", + titleAlignment: "center", + bottomTitle: "close", + bottomTitleAlignment: "right", + paddingX: 1, + }) + + box.add(new TextRenderable(renderer, { content: "Top and bottom titles", fg: muted })) + renderer.root.add(box) + }, + }, + { + id: "color-palette", + label: + "OpenTUI's 256-color fallback palette: 16 terminal colors, six red-level slices of the RGB cube, and 24 grays. Blue increases rightward and green downward within each slice.", + width: 38, + height: 22, + render({ renderer }) { + const canvas = new FrameBufferRenderable(renderer, { width: 38, height: 22 }) + renderer.root.add(canvas) + const buffer = canvas.frameBuffer + const background = RGBA.defaultBackground() + buffer.clear(background) + + buffer.drawText("0-15 terminal colors", 0, 0, foreground, background) + // Freeze the fallback snapshots instead of applying the page's semantic palette overrides. + for (let index = 0; index < 16; index++) { + const color = RGBA.fromInts(...RGBA.fromIndex(index).toInts()) + buffer.drawText("\u2588\u2588", index * 2, 1, color, color) + } + + buffer.drawText("16-231 RGB cube", 0, 3, foreground, background) + for (let red = 0; red < 6; red++) { + const left = (red % 3) * 13 + const top = 5 + Math.floor(red / 3) * 8 + const redLevel = RGBA.fromIndex(16 + red * 36).toInts()[0] + buffer.drawText(`R=${redLevel}`, left, top - 1, muted, background) + + for (let green = 0; green < 6; green++) { + for (let blue = 0; blue < 6; blue++) { + const index = 16 + red * 36 + green * 6 + blue + const color = RGBA.fromInts(...RGBA.fromIndex(index).toInts()) + buffer.drawText("\u2588\u2588", left + blue * 2, top + green, color, color) + } + } + } + + buffer.drawText("232-255 grayscale", 0, 20, foreground, background) + for (let index = 232; index < 256; index++) { + const color = RGBA.fromInts(...RGBA.fromIndex(index).toInts()) + buffer.drawText("\u2588", index - 232, 21, color, color) + } + }, + }, + { + id: "color-alpha", + label: + "Green #22c55e over a gray checkerboard at alpha 0, 0.25, 0.5, 0.75, and 1. The checkerboard remains visible through translucent tiles and disappears at alpha 1.", + width: 34, + height: 4, + render({ renderer }) { + const canvas = new FrameBufferRenderable(renderer, { width: 34, height: 4 }) + renderer.root.add(canvas) + const buffer = canvas.frameBuffer + const background = RGBA.defaultBackground() + const checks = [RGBA.fromHex("#e0e0e0"), RGBA.fromHex("#a0a0a0")] + buffer.clear(background) + + for (const [index, alpha] of [0, 0.25, 0.5, 0.75, 1].entries()) { + const left = index * 7 + + for (let y = 0; y < 3; y++) { + for (let x = 0; x < 6; x += 2) { + buffer.fillRect(left + x, y, 2, 1, checks[(x / 2 + y) % 2]) + } + } + + const overlay = RGBA.fromHex("#22c55e") + overlay.a = alpha + buffer.fillRect(left, 0, 6, 3, overlay) + buffer.drawText(alpha.toFixed(2), left + 1, 3, foreground, background) + } + }, + }, +] diff --git a/packages/web/scripts/doc-visuals/graphics.ts b/packages/web/scripts/doc-visuals/graphics.ts new file mode 100644 index 00000000..e59c718e --- /dev/null +++ b/packages/web/scripts/doc-visuals/graphics.ts @@ -0,0 +1,192 @@ +import { + ASCIIFontRenderable, + EmbeddedTerminalRenderable, + FrameBufferRenderable, + ImageRenderable, + NativeImage, + RGBA, +} from "@opentui/core" +import { QRCodeRenderable } from "@opentui/qrcode" +import type { DocVisualFixture } from "./shared" + +const foreground = RGBA.defaultForeground() +const background = RGBA.defaultBackground() +const muted = RGBA.fromIndex(244) + +export const graphicsVisuals: DocVisualFixture[] = [ + { + id: "ascii-font-tiny", + label: "OPEN drawn with OpenTUI's compact tiny ASCII font", + width: 16, + height: 2, + render({ renderer }) { + renderer.root.add( + new ASCIIFontRenderable(renderer, { + text: "OPEN", + font: "tiny", + color: foreground, + }), + ) + }, + }, + { + id: "ascii-font-block", + label: "OPEN drawn with OpenTUI's six-row block ASCII font", + width: 38, + height: 6, + render({ renderer }) { + renderer.root.add( + new ASCIIFontRenderable(renderer, { + text: "OPEN", + font: "block", + color: foreground, + }), + ) + }, + }, + { + id: "frame-buffer-draw", + label: "Network throughput chart showing receive at 42 MB/s and transmit at 18 MB/s", + width: 21, + height: 4, + render({ renderer }) { + const canvas = new FrameBufferRenderable(renderer, { width: 21, height: 4 }) + const buffer = canvas.frameBuffer + + buffer.clear(background) + buffer.drawText("network throughput", 0, 0, foreground, background) + buffer.drawText("rx", 0, 1, muted, background) + buffer.drawText("tx", 0, 2, muted, background) + buffer.drawText("42 MB/s", 14, 1, muted, background) + buffer.drawText("18 MB/s", 14, 2, muted, background) + buffer.drawText("8 seconds", 4, 3, muted, background) + buffer.drawText("now", 18, 3, muted, background) + + for (const [row, bars] of ["▂▄▆█▇▅▃▂", "▃▅▇█▆▄▂▁"].entries()) { + for (const [column, bar] of [...bars].entries()) { + buffer.setCell(column + 4, row + 1, bar, foreground, background) + } + } + + renderer.root.add(canvas) + }, + }, + { + id: "frame-buffer-progress", + label: "Package download progress at 70%, with 14 of 20 files complete", + width: 25, + height: 3, + render({ renderer }) { + const canvas = new FrameBufferRenderable(renderer, { width: 25, height: 3 }) + const buffer = canvas.frameBuffer + + buffer.clear(background) + buffer.drawText("Downloading package", 0, 0, foreground, background) + buffer.drawText("70%", 22, 1, foreground, background) + buffer.drawText("14 of 20 files", 0, 2, muted, background) + + for (let column = 0; column < 20; column++) { + buffer.setCell(column, 1, column < 14 ? "█" : "░", column < 14 ? foreground : muted, background) + } + + renderer.root.add(canvas) + }, + }, + { + id: "embedded-terminal-vt", + label: "Embedded terminal test run with two passed and zero failed", + width: 27, + height: 4, + inheritTerminalColors: true, + render({ renderer }) { + const terminal = new EmbeddedTerminalRenderable(renderer, { width: 27, height: 4 }) + const output = [ + "\x1b[38;5;244m$ \x1b[39mbun test", + "\x1b[38;5;244m✓\x1b[39m parser accepts UTF-8", + "\x1b[38;5;244m✓\x1b[39m renderer draws wide cells", + "\x1b[1m2 passed\x1b[22m, 0 failed", + ].join("\r\n") + + terminal.write(new TextEncoder().encode(output)) + renderer.root.add(terminal) + }, + }, + { + id: "image-blocks", + label: "Generated RGBA landscape displayed with the Unicode block image protocol", + width: 24, + height: 8, + async render({ renderer }) { + const width = 48 + const height = 32 + const pixels = new Uint8Array(width * height * 4) + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const ridge = Math.min(8 + Math.abs(x - 14) * 0.62, 11 + Math.abs(x - 33) * 0.5) + const nearRidge = 16 + Math.abs(x - 28) * 0.52 + const sun = (x - 39) ** 2 + (y - 7) ** 2 <= 16 + const color = + y >= 25 + ? [34, 197, 94] + : y >= nearRidge + ? [30, 64, 175] + : y >= ridge + ? [96, 165, 250] + : sun + ? [250, 204, 21] + : [15, 23, 42] + const offset = (y * width + x) * 4 + + pixels[offset] = color[0] + pixels[offset + 1] = color[1] + pixels[offset + 2] = color[2] + pixels[offset + 3] = 255 + } + } + + const image = NativeImage.fromRgba(pixels, width, height) + let renderable: ImageRenderable | undefined + + try { + renderable = new ImageRenderable(renderer, { + source: image, + protocol: "blocks", + fit: "fill", + width: 24, + height: 8, + }) + + renderer.root.add(renderable) + await renderable.loadPromise + return () => image.dispose() + } catch (error) { + renderable?.destroy() + image.dispose() + throw error + } + }, + }, + { + id: "qr-code-version-one", + label: "Scannable version-one QR code encoding OPENTUI with a four-module white quiet zone", + width: 29, + height: 15, + render({ renderer }) { + const qr = new QRCodeRenderable(renderer, { + content: "OPENTUI", + quietZone: 4, + scale: 1, + foregroundColor: "#000000", + backgroundColor: "#ffffff", + }) + + if (qr.version !== 1) { + qr.destroy() + throw new Error(`Expected a version-one QR code, received version ${qr.version}`) + } + + renderer.root.add(qr) + }, + }, +] diff --git a/packages/web/scripts/doc-visuals/interaction.ts b/packages/web/scripts/doc-visuals/interaction.ts new file mode 100644 index 00000000..4a026fed --- /dev/null +++ b/packages/web/scripts/doc-visuals/interaction.ts @@ -0,0 +1,103 @@ +import { BoxRenderable, RGBA, TextRenderable, type MouseEvent } from "@opentui/core" +import type { DocVisualFixture } from "./shared" + +const foreground = RGBA.defaultForeground() +const background = RGBA.defaultBackground() +const muted = RGBA.fromIndex(244) + +export const interactionVisuals: DocVisualFixture[] = [ + { + id: "interaction-hit-bubbling", + label: "The overlapping front box receives the hit, bubbles to its parent, then stops a second down event", + width: 38, + height: 10, + async render({ renderer, mockMouse, renderOnce }) { + const trace: string[] = [] + let target = "" + let stop = false + const record = (event: MouseEvent) => { + target = event.target?.id ?? "" + trace.push(event.currentTarget!.id) + if (stop && event.currentTarget!.id === "front") event.stopPropagation() + } + const parent = new BoxRenderable(renderer, { + id: "parent", + width: 38, + height: 7, + border: true, + borderColor: foreground, + title: "parent", + onMouseDown: record, + }) + renderer.root.add(parent) + parent.add( + new BoxRenderable(renderer, { + id: "back", + position: "absolute", + left: 1, + top: 1, + width: 24, + height: 3, + zIndex: 0, + border: true, + borderColor: muted, + backgroundColor: background, + title: "back z=0", + onMouseDown: record, + }), + ) + const front = new BoxRenderable(renderer, { + id: "front", + position: "absolute", + left: 10, + top: 2, + width: 25, + height: 3, + zIndex: 1, + border: true, + borderColor: foreground, + backgroundColor: background, + title: "front z=1", + onMouseDown: record, + }) + parent.add(front) + const output = new TextRenderable(renderer, { fg: foreground, selectable: false }) + renderer.root.add(output) + + await renderOnce() + await mockMouse.click(front.x + 2, front.y + 1, undefined, { delayMs: 0 }) + const bubbled = trace.join(" -> ") + trace.length = 0 + stop = true + await mockMouse.click(front.x + 2, front.y + 1, undefined, { delayMs: 0 }) + output.content = `target: ${target}\nbubble: ${bubbled}\nstopped: ${trace.join(" -> ")}` + }, + }, + { + id: "interaction-drag-selection", + label: "A drag selects the app on the first line and Test on the second, with text-buffer offsets [6, 18)", + width: 32, + height: 5, + async render({ renderer, mockMouse, renderOnce }) { + const layout = new BoxRenderable(renderer, { width: 32, height: 5, gap: 1 }) + renderer.root.add(layout) + const text = new TextRenderable(renderer, { + content: "Build the app\nTest the input", + height: 2, + fg: foreground, + bg: background, + selectionBg: RGBA.fromIndex(238), + selectionFg: foreground, + }) + layout.add(text) + const output = new TextRenderable(renderer, { height: 2, fg: muted, selectable: false }) + layout.add(output) + + await renderOnce() + await mockMouse.drag(text.x + 6, text.y, text.x + 3, text.y + 1, undefined, { delayMs: 0 }) + const selected = renderer.getSelection()?.getSelectedText() + const range = text.getSelection() + output.content = `Selected: ${JSON.stringify(selected)}\nRange: [${range?.start}, ${range?.end})` + }, + }, +] diff --git a/packages/web/scripts/doc-visuals/layout.ts b/packages/web/scripts/doc-visuals/layout.ts new file mode 100644 index 00000000..102b1996 --- /dev/null +++ b/packages/web/scripts/doc-visuals/layout.ts @@ -0,0 +1,167 @@ +import { BoxRenderable, RGBA, TextRenderable } from "@opentui/core" +import type { TestRendererSetup } from "@opentui/core/testing" +import type { DocVisualFixture } from "./shared" + +const foreground = RGBA.defaultForeground() +const fill = RGBA.fromIndex(238) + +export const layoutVisuals: DocVisualFixture[] = [ + { + id: "layout-flex-columns", + label: + "A 34-column container with a one-cell border and padding, an eight-column fixed child, a two-column gap, and a growing child filling the remaining 20 columns", + width: 34, + height: 5, + render({ renderer }) { + const row = new BoxRenderable(renderer, { + width: 34, + height: 5, + border: true, + borderColor: foreground, + padding: 1, + flexDirection: "row", + gap: 2, + }) + renderer.root.add(row) + + const fixed = new BoxRenderable(renderer, { width: 8, height: 1, backgroundColor: fill }) + row.add(fixed) + fixed.add(new TextRenderable(renderer, { content: "fixed 8", fg: foreground })) + + const growing = new BoxRenderable(renderer, { + flexGrow: 1, + flexBasis: 0, + height: 1, + backgroundColor: fill, + }) + row.add(growing) + growing.add(new TextRenderable(renderer, { content: "grow 20", fg: foreground })) + }, + }, + { + id: "layout-alignment", + label: + "Three children distributed horizontally with space-between: one-row A and three-row B share a vertical center, while C overrides alignment to sit at the bottom", + width: 32, + height: 7, + render({ renderer }) { + const row = new BoxRenderable(renderer, { + width: 32, + height: 7, + border: true, + borderColor: foreground, + paddingX: 1, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }) + renderer.root.add(row) + + for (const [label, height] of [ + ["A", 1], + ["B", 3], + ["C", 1], + ] as const) { + const child = new BoxRenderable(renderer, { + width: 6, + height, + alignSelf: label === "C" ? "flex-end" : "auto", + backgroundColor: fill, + }) + row.add(child) + child.add(new TextRenderable(renderer, { content: label, fg: foreground })) + } + }, + }, + { + id: "layout-cell-rounding", + label: + "Three equal grow factors divide 30 inner columns into 10, 10, and 10 cells; adding one terminal column changes the allocation to 10, 11, and 10 cells", + width: 33, + height: 8, + async render({ renderer, renderOnce }) { + const cells: Array<{ box: BoxRenderable; label: TextRenderable }> = [] + + for (const width of [32, 33]) { + renderer.root.add( + new TextRenderable(renderer, { content: `${width} columns: ${width - 2} inside`, fg: foreground }), + ) + const row = new BoxRenderable(renderer, { + width, + height: 3, + border: true, + borderColor: foreground, + flexDirection: "row", + }) + renderer.root.add(row) + + for (let index = 0; index < 3; index++) { + const box = new BoxRenderable(renderer, { + flexGrow: 1, + flexBasis: 0, + minWidth: 0, + height: 1, + alignItems: "center", + backgroundColor: index === 1 ? RGBA.fromIndex(235) : fill, + }) + row.add(box) + const label = new TextRenderable(renderer, { content: "", fg: foreground }) + box.add(label) + cells.push({ box, label }) + } + } + + await renderOnce() + for (const { box, label } of cells) label.content = String(box.width) + }, + }, + { + id: "layout-wrap-wide", + label: "At 32 terminal columns, three eight-column children A, B, and C fit in one row with two-column gaps", + width: 32, + height: 4, + render(setup) { + return renderWrapping(setup, 32) + }, + }, + { + id: "layout-wrap-narrow", + label: + "After resizing the same terminal to 22 columns, A and B stay on the first row while C wraps to a second row and the container grows taller", + width: 22, + height: 6, + render(setup) { + return renderWrapping(setup, 22) + }, + }, +] + +async function renderWrapping({ renderer, renderOnce, resize }: TestRendererSetup, columns: number) { + const height = columns === 32 ? 4 : 6 + resize(32, height) + const label = new TextRenderable(renderer, { content: "32 columns", fg: foreground }) + renderer.root.add(label) + + const row = new BoxRenderable(renderer, { + width: "100%", + border: true, + borderColor: foreground, + paddingX: 1, + flexDirection: "row", + flexWrap: "wrap", + alignItems: "flex-start", + columnGap: 2, + rowGap: 1, + }) + renderer.root.add(row) + + for (const content of ["A", "B", "C"]) { + const child = new BoxRenderable(renderer, { width: 8, height: 1, flexShrink: 0, backgroundColor: fill }) + row.add(child) + child.add(new TextRenderable(renderer, { content, fg: foreground })) + } + + await renderOnce() + resize(columns, height) + label.content = `${columns} columns` +} diff --git a/packages/web/scripts/doc-visuals/renderables.ts b/packages/web/scripts/doc-visuals/renderables.ts new file mode 100644 index 00000000..3f7f4089 --- /dev/null +++ b/packages/web/scripts/doc-visuals/renderables.ts @@ -0,0 +1,133 @@ +import { BoxRenderable, RGBA, TextRenderable } from "@opentui/core" +import type { TestRendererSetup } from "@opentui/core/testing" +import type { DocVisualFixture } from "./shared" + +const foreground = RGBA.defaultForeground() +const muted = RGBA.fromIndex(244) +const fill = RGBA.fromIndex(243) + +export const renderableVisuals: DocVisualFixture[] = [ + { + id: "renderable-created", + label: "An 18-column shaded panel containing a status that says Waiting", + width: 18, + height: 3, + render(setup) { + return renderMutation(setup, false) + }, + }, + { + id: "renderable-mutated", + label: + "After changing content and width, the same shaded panel is 30 columns wide and its retained status says Ready", + width: 30, + height: 3, + render(setup) { + return renderMutation(setup, true) + }, + }, + { + id: "renderable-reparent-before", + label: "Before reparenting, the shaded Message subtree is a child of first; second is empty", + width: 34, + height: 3, + render(setup) { + return renderReparenting(setup, false) + }, + }, + { + id: "renderable-reparent-after", + label: "After second.add(message), the same shaded Message subtree is a child of second; first is empty", + width: 34, + height: 3, + render(setup) { + return renderReparenting(setup, true) + }, + }, + { + id: "renderable-visibility", + label: + "Visible: the panel contains Detail above Next. Hidden: Next moves up, but Detail stays attached and the panel still has two children. Detached: Next moves up, Detail has no parent, and the panel has one child. Neither operation destroys Detail.", + width: 38, + height: 7, + async render({ renderer, renderOnce }, registerCleanup) { + const row = new BoxRenderable(renderer, { width: 38, flexDirection: "row", gap: 1 }) + renderer.root.add(row) + + for (const state of ["visible", "hidden", "detached"]) { + const column = new BoxRenderable(renderer, { width: 12 }) + row.add(column) + column.add(new TextRenderable(renderer, { content: state, fg: foreground })) + + const panel = new BoxRenderable(renderer, { + width: 12, + height: 4, + border: true, + borderColor: foreground, + }) + column.add(panel) + const detail = new BoxRenderable(renderer, { height: 1, backgroundColor: fill }) + registerCleanup(() => detail.destroyRecursively()) + panel.add(detail) + detail.add(new TextRenderable(renderer, { content: "Detail", fg: foreground })) + panel.add(new TextRenderable(renderer, { content: "Next", fg: foreground })) + + await renderOnce() + if (state !== "visible") detail.visible = false + if (state === "detached") { + detail.visible = true + panel.remove(detail) + } + + column.add(new TextRenderable(renderer, { content: `children: ${panel.getChildrenCount()}`, fg: muted })) + column.add(new TextRenderable(renderer, { content: `parent: ${detail.parent ? "yes" : "no"}`, fg: muted })) + } + }, + }, +] + +async function renderMutation({ renderer, renderOnce }: TestRendererSetup, updated: boolean) { + const panel = new BoxRenderable(renderer, { + id: "panel", + width: 18, + height: 3, + paddingX: 1, + border: true, + borderColor: foreground, + backgroundColor: fill, + }) + renderer.root.add(panel) + const status = new TextRenderable(renderer, { id: "status", content: "Waiting", fg: foreground }) + panel.add(status) + + await renderOnce() + if (updated) { + status.content = "Ready" + panel.width = 30 + } +} + +async function renderReparenting({ renderer, renderOnce }: TestRendererSetup, moved: boolean) { + const row = new BoxRenderable(renderer, { width: 34, flexDirection: "row", gap: 2 }) + renderer.root.add(row) + + const [first, second] = ["first", "second"].map((id) => { + const panel = new BoxRenderable(renderer, { + id, + title: id, + width: 16, + height: 3, + paddingX: 1, + border: true, + borderColor: foreground, + }) + row.add(panel) + return panel + }) + const message = new BoxRenderable(renderer, { id: "message", width: 10, height: 1, backgroundColor: fill }) + first.add(message) + message.add(new TextRenderable(renderer, { content: "Message", fg: foreground })) + + await renderOnce() + if (moved) second.add(message) +} diff --git a/packages/web/scripts/doc-visuals/scrolling.ts b/packages/web/scripts/doc-visuals/scrolling.ts new file mode 100644 index 00000000..c8c549a9 --- /dev/null +++ b/packages/web/scripts/doc-visuals/scrolling.ts @@ -0,0 +1,251 @@ +import { + BoxRenderable, + RGBA, + ScrollBarRenderable, + ScrollBoxRenderable, + SliderRenderable, + SlotRenderable, + TextRenderable, + createCoreSlotRegistry, + registerCorePlugin, +} from "@opentui/core" +import type { TestRendererSetup } from "@opentui/core/testing" +import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui" +import type { DocVisualFixture } from "./shared" + +const foreground = RGBA.defaultForeground() +const background = RGBA.defaultBackground() +const muted = RGBA.fromIndex(244) +const track = RGBA.fromIndex(235) + +const entries = [ + "01 src/index.ts", + "02 src/app.ts", + "03 src/layout.ts", + "04 src/theme.ts", + "05 src/events.ts", + "06 src/input.ts", + "07 src/scroll.ts", + "08 src/render.ts", + "09 src/state.ts", + "10 src/config.ts", + "11 src/logger.ts", + "12 src/cleanup.ts", +] + +async function renderScrollBox(setup: TestRendererSetup, position: number) { + const { renderer } = setup + const layout = new BoxRenderable(renderer, { width: 32, height: 8 }) + const status = new TextRenderable(renderer, { content: "", fg: muted, height: 1 }) + const scrollbox = new ScrollBoxRenderable(renderer, { + width: 32, + height: 7, + border: true, + borderColor: foreground, + scrollbarOptions: { + trackOptions: { backgroundColor: track, foregroundColor: foreground }, + arrowOptions: { foregroundColor: foreground, backgroundColor: background }, + }, + }) + + for (const entry of entries) { + scrollbox.add(new TextRenderable(renderer, { content: entry, fg: foreground, height: 1, flexShrink: 0 })) + } + + layout.add(status) + layout.add(scrollbox) + renderer.root.add(layout) + + await setup.renderOnce() + scrollbox.scrollTo(position) + status.content = `offset: ${scrollbox.scrollTop} / ${scrollbox.scrollHeight - scrollbox.viewport.height}` +} + +export const scrollingVisuals: DocVisualFixture[] = [ + { + id: "slider-horizontal", + label: "Horizontal slider with its thumb positioned at 25 out of 100", + width: 30, + height: 3, + render({ renderer }) { + const layout = new BoxRenderable(renderer, { width: 30, height: 3 }) + const limits = new BoxRenderable(renderer, { + width: 30, + height: 1, + flexDirection: "row", + justifyContent: "space-between", + }) + + layout.add(new TextRenderable(renderer, { content: "value: 25", fg: foreground })) + layout.add( + new SliderRenderable(renderer, { + orientation: "horizontal", + width: 30, + height: 1, + min: 0, + max: 100, + value: 25, + backgroundColor: track, + foregroundColor: foreground, + }), + ) + limits.add(new TextRenderable(renderer, { content: "0", fg: muted })) + limits.add(new TextRenderable(renderer, { content: "100", fg: muted })) + layout.add(limits) + renderer.root.add(layout) + }, + }, + { + id: "slider-vertical", + label: "Vertical slider with its thumb positioned at 0.5 out of 1", + width: 13, + height: 10, + render({ renderer }) { + const layout = new BoxRenderable(renderer, { width: 13, height: 10, flexDirection: "row", gap: 2 }) + const labels = new BoxRenderable(renderer, { width: 9, height: 10, justifyContent: "space-between" }) + + labels.add(new TextRenderable(renderer, { content: "min 0", fg: muted })) + labels.add(new TextRenderable(renderer, { content: "value 0.5", fg: foreground })) + labels.add(new TextRenderable(renderer, { content: "max 1", fg: muted })) + layout.add(labels) + layout.add( + new SliderRenderable(renderer, { + orientation: "vertical", + width: 2, + height: 10, + min: 0, + max: 1, + value: 0.5, + backgroundColor: track, + foregroundColor: foreground, + }), + ) + renderer.root.add(layout) + }, + }, + { + id: "scrollbar-arrows", + label: "Standalone vertical scrollbar with arrows at position 0, showing 20 of 200 rows", + width: 20, + height: 10, + render({ renderer }) { + const layout = new BoxRenderable(renderer, { width: 20, height: 10, flexDirection: "row", gap: 1 }) + const status = new BoxRenderable(renderer, { width: 18, height: 10, justifyContent: "space-between" }) + const scrollbar = new ScrollBarRenderable(renderer, { + orientation: "vertical", + width: 1, + height: 10, + showArrows: true, + arrowOptions: { foregroundColor: foreground, backgroundColor: background }, + trackOptions: { backgroundColor: track, foregroundColor: foreground }, + }) + + scrollbar.scrollSize = 200 + scrollbar.viewportSize = 20 + scrollbar.scrollPosition = 0 + + status.add( + new TextRenderable(renderer, { + content: `position: ${scrollbar.scrollPosition} / ${scrollbar.scrollSize - scrollbar.viewportSize}`, + fg: foreground, + }), + ) + status.add( + new TextRenderable(renderer, { + content: `viewport: ${scrollbar.viewportSize} / ${scrollbar.scrollSize}`, + fg: muted, + }), + ) + layout.add(status) + layout.add(scrollbar) + renderer.root.add(layout) + }, + }, + { + id: "scrollbox-top", + label: "ScrollBox at offset 0 showing index.ts, app.ts, layout.ts, theme.ts, and events.ts", + width: 32, + height: 8, + render(setup) { + return renderScrollBox(setup, 0) + }, + }, + { + id: "scrollbox-scrolled", + label: "ScrollBox at offset 5 showing input.ts, scroll.ts, render.ts, state.ts, and config.ts", + width: 32, + height: 8, + render(setup) { + return renderScrollBox(setup, 5) + }, + }, + { + id: "plugin-slot-modes", + label: "Plugin slots: append shows host, clock, and sync; replace shows clock and sync; single winner shows clock", + width: 32, + height: 3, + render({ renderer }) { + const registry = createCoreSlotRegistry<"statusbar">(renderer, {}) + const layout = new BoxRenderable(renderer, { width: 32, height: 3 }) + + for (const name of ["clock", "sync"]) { + registerCorePlugin(registry, { + id: name, + slots: { + statusbar: () => new TextRenderable(renderer, { content: name, fg: foreground, marginRight: 1 }), + }, + }) + } + + for (const mode of ["append", "replace", "single_winner"] as const) { + const row = new BoxRenderable(renderer, { width: 32, height: 1, flexDirection: "row", gap: 2 }) + + row.add(new TextRenderable(renderer, { content: mode, fg: muted, width: 13 })) + row.add( + new SlotRenderable(renderer, { + registry, + name: "statusbar", + mode, + flexDirection: "row", + fallback: () => new TextRenderable(renderer, { content: "host", fg: muted, marginRight: 1 }), + }), + ) + layout.add(row) + } + + renderer.root.add(layout) + }, + }, + { + id: "keymap-active-keys", + label: "Active key bindings: Ctrl+S saves the file; Q quits the application", + width: 18, + height: 2, + render({ renderer }) { + const keymap = createDefaultOpenTuiKeymap(renderer) + + keymap.registerLayer({ + commands: [ + { name: "file.save", run() {} }, + { name: "app.quit", run() {} }, + ], + bindings: [ + { key: "ctrl+s", cmd: "file.save" }, + { key: "q", cmd: "app.quit" }, + ], + }) + + const layout = new BoxRenderable(renderer, { width: 18, height: 2 }) + + for (const binding of keymap.getActiveKeys()) { + const row = new BoxRenderable(renderer, { width: 18, height: 1, flexDirection: "row", gap: 2 }) + + row.add(new TextRenderable(renderer, { content: binding.display, fg: foreground, width: 6 })) + row.add(new TextRenderable(renderer, { content: String(binding.command), fg: muted })) + layout.add(row) + } + + renderer.root.add(layout) + }, + }, +] diff --git a/packages/web/scripts/doc-visuals/shared.ts b/packages/web/scripts/doc-visuals/shared.ts new file mode 100644 index 00000000..8549831c --- /dev/null +++ b/packages/web/scripts/doc-visuals/shared.ts @@ -0,0 +1,128 @@ +import { RGBA, rgbToHex } from "@opentui/core" +import { createTestRenderer, type TestRendererSetup } from "@opentui/core/testing" + +type Cleanup = () => void | Promise + +export interface DocVisualFixture { + id: string + label: string + width: number + height: number + cursor?: boolean + inheritTerminalColors?: boolean + render( + setup: TestRendererSetup, + registerCleanup: (cleanup: Cleanup) => void, + ): void | Cleanup | Promise +} + +export async function renderDocVisual(fixture: DocVisualFixture) { + const properties: Array<{ target: object; name: string; descriptor: PropertyDescriptor | undefined }> = [ + "requestAnimationFrame", + "cancelAnimationFrame", + "window", + ].map((name) => ({ target: globalThis, name, descriptor: Object.getOwnPropertyDescriptor(globalThis, name) })) + + if (globalThis.window) { + properties.push({ + target: globalThis.window, + name: "requestAnimationFrame", + descriptor: Object.getOwnPropertyDescriptor(globalThis.window, "requestAnimationFrame"), + }) + } + + let setup: TestRendererSetup | undefined + const cleanups: Cleanup[] = [] + + try { + setup = await createTestRenderer({ + width: fixture.width, + height: fixture.height, + useThread: false, + remote: false, + forwardEnvKeys: [], + }) + + setup.renderer.setBackgroundColor(RGBA.defaultBackground()) + const cleanup = await fixture.render(setup, (callback) => cleanups.push(callback)) + if (cleanup) cleanups.push(cleanup) + await setup.renderOnce() + + const frame = setup.captureSpans() + const state = setup.renderer.getCursorState() + const colors: ReturnType[] = [] + const colorIndexes = new Map() + + function colorIndex(color: RGBA, channel: "foreground" | "background") { + if (fixture.inheritTerminalColors && color.intent === "rgb") { + const value = rgbToHex(color) + + if (channel === "background" && value === "#000000") color = RGBA.defaultBackground() + else if (channel === "foreground" && value === "#ffffff") color = RGBA.defaultForeground() + else if (channel === "foreground" && value === "#808080") color = RGBA.fromIndex(244) + } + + const value = serializeColor(color) + const key = `${value.intent}:${value.slot ?? ""}:${value.value}` + let index = colorIndexes.get(key) + + if (index === undefined) { + index = colors.push(value) - 1 + colorIndexes.set(key, index) + } + + return index + } + + const lines = frame.lines.map(({ spans }) => + spans.map(({ text, width, fg, bg, attributes }) => ({ + text, + width, + foreground: colorIndex(fg, "foreground"), + background: colorIndex(bg, "background"), + ...(attributes ? { attributes } : {}), + })), + ) + + return { + label: fixture.label, + cols: frame.cols, + rows: frame.rows, + colors, + cursor: fixture.cursor && state.visible ? { column: state.x - 1, row: state.y - 1, style: state.style } : null, + lines, + } + } finally { + try { + try { + setup?.renderer.destroy() + } finally { + const errors: unknown[] = [] + + for (const cleanup of cleanups.toReversed()) { + try { + await cleanup() + } catch (error) { + errors.push(error) + } + } + + if (errors.length === 1) throw errors[0] + if (errors.length > 1) throw new AggregateError(errors, "Documentation visual cleanup failed") + } + } finally { + for (const { target, name, descriptor } of properties.toReversed()) { + if (descriptor) Object.defineProperty(target, name, descriptor) + else Reflect.deleteProperty(target, name) + } + } + } +} + +function serializeColor(color: RGBA) { + return { + intent: color.intent, + value: rgbToHex(color), + ...(color.intent === "indexed" ? { slot: color.slot } : {}), + } +} diff --git a/packages/web/scripts/doc-visuals/structured.ts b/packages/web/scripts/doc-visuals/structured.ts new file mode 100644 index 00000000..029b9fd2 --- /dev/null +++ b/packages/web/scripts/doc-visuals/structured.ts @@ -0,0 +1,258 @@ +import { + CodeRenderable, + DiffRenderable, + LineNumberRenderable, + MarkdownRenderable, + RGBA, + SyntaxStyle, + TextTableRenderable, + TextareaRenderable, + bold, + fg, + type TextTableContent, +} from "@opentui/core" +import { MockTreeSitterClient } from "@opentui/core/testing" +import type { DocVisualFixture } from "./shared" + +const foreground = RGBA.defaultForeground() +const background = RGBA.defaultBackground() +const muted = RGBA.fromIndex(244) + +const markdownTable = "| Name | Status |\n| --- | --- |\n| api | ready |\n| worker | paused |" +const patch = + "diff --git a/app.ts b/app.ts\nindex 1111111..2222222 100644\n--- a/app.ts\n+++ b/app.ts\n@@ -1,3 +1,3 @@\n setup()\n-const a = 1\n+const a = 2\n ready(a)\n" + +export const structuredVisuals: DocVisualFixture[] = [ + { + id: "text-table-styled", + label: "Rounded table with Service, Status, and Notes columns: api OK, worker DEGRADED", + width: 35, + height: 7, + render({ renderer }) { + const content: TextTableContent = [ + [[bold("Service")], [bold("Status")], [bold("Notes")]], + [[fg(foreground)("api")], [fg(foreground)("OK")], [fg(foreground)("latency 28ms")]], + [[fg(foreground)("worker")], [fg(muted)("DEGRADED")], [fg(foreground)("queue depth: 124")]], + ] + + renderer.root.add( + new TextTableRenderable(renderer, { + content, + columnWidthMode: "content", + borderStyle: "rounded", + borderColor: muted, + fg: foreground, + bg: background, + backgroundColor: background, + borderBackgroundColor: background, + }), + ) + }, + }, + { + id: "line-number-editor", + label: "Three lines of numbered code with a sign beside serve(port)", + width: 24, + height: 3, + render({ renderer }, registerCleanup) { + const textarea = new TextareaRenderable(renderer, { + width: "100%", + height: 3, + initialValue: "const port = 3000\nserve(port)\nawait ready()", + backgroundColor: background, + textColor: foreground, + }) + registerCleanup(() => { + if (!textarea.isDestroyed) textarea.destroy() + }) + + const gutter = new LineNumberRenderable(renderer, { + target: textarea, + width: "100%", + minWidth: 3, + paddingRight: 1, + fg: muted, + bg: background, + }) + registerCleanup(() => { + if (!gutter.isDestroyed) gutter.destroyRecursively() + }) + + gutter.setLineSign(1, { before: ">", beforeColor: foreground }) + renderer.root.add(gutter) + }, + }, + { + id: "code-highlighted", + label: "JavaScript function hello with bold keywords, a muted string, and an italic comment", + width: 33, + height: 5, + async render({ renderer, renderOnce }, registerCleanup) { + const client = new MockTreeSitterClient() + registerCleanup(() => client.destroy()) + + const syntaxStyle = SyntaxStyle.fromStyles({ + default: { fg: foreground }, + keyword: { fg: foreground, bold: true }, + string: { fg: RGBA.fromIndex(247) }, + comment: { fg: muted, italic: true }, + }) + registerCleanup(() => syntaxStyle.destroy()) + + client.setMockResult({ + highlights: [ + [0, 8, "keyword"], + [21, 41, "comment"], + [44, 49, "keyword"], + [60, 75, "string"], + [78, 84, "keyword"], + ], + }) + + const code = new CodeRenderable(renderer, { + width: "100%", + height: 5, + content: 'function hello() {\n // This is a comment\n const message = "Hello, world!"\n return message\n}', + filetype: "javascript", + syntaxStyle, + treeSitterClient: client, + fg: foreground, + bg: background, + }) + + renderer.root.add(code) + await renderOnce() + client.resolveAllHighlightOnce() + await code.highlightingDone + }, + }, + { + id: "markdown-table-grid", + label: "Bordered Markdown table with Name and Status columns: api ready, worker paused", + width: 19, + height: 7, + render({ renderer }, registerCleanup) { + const client = new MockTreeSitterClient() + registerCleanup(() => client.destroy()) + + const syntaxStyle = SyntaxStyle.fromStyles({ + default: { fg: foreground }, + "markup.heading": { fg: foreground, bold: true }, + }) + registerCleanup(() => syntaxStyle.destroy()) + + renderer.root.add( + new MarkdownRenderable(renderer, { + width: "100%", + content: markdownTable, + syntaxStyle, + treeSitterClient: client, + fg: foreground, + bg: background, + tableOptions: { + style: "grid", + widthMode: "content", + cellPaddingX: 1, + borderColor: muted, + }, + }), + ) + }, + }, + { + id: "markdown-table-columns", + label: "Borderless Markdown table with Name and Status columns: api ready, worker paused", + width: 14, + height: 3, + render({ renderer }, registerCleanup) { + const client = new MockTreeSitterClient() + registerCleanup(() => client.destroy()) + + const syntaxStyle = SyntaxStyle.fromStyles({ + default: { fg: foreground }, + "markup.heading": { fg: foreground, bold: true }, + }) + registerCleanup(() => syntaxStyle.destroy()) + + renderer.root.add( + new MarkdownRenderable(renderer, { + width: "100%", + content: markdownTable, + syntaxStyle, + treeSitterClient: client, + fg: foreground, + bg: background, + tableOptions: { style: "columns" }, + }), + ) + }, + }, + { + id: "diff-unified", + label: "Unified diff replacing const a = 1 with const a = 2", + width: 16, + height: 4, + render({ renderer }, registerCleanup) { + const client = new MockTreeSitterClient() + registerCleanup(() => client.destroy()) + + const syntaxStyle = SyntaxStyle.fromStyles({ default: { fg: foreground } }) + registerCleanup(() => syntaxStyle.destroy()) + + renderer.root.add( + new DiffRenderable(renderer, { + width: "100%", + height: 4, + diff: patch, + view: "unified", + syntaxStyle, + treeSitterClient: client, + fg: foreground, + lineNumberFg: muted, + lineNumberBg: background, + addedBg: background, + removedBg: background, + contextBg: background, + addedLineNumberBg: background, + removedLineNumberBg: background, + addedSignColor: foreground, + removedSignColor: muted, + }), + ) + }, + }, + { + id: "diff-split", + label: "Split diff replacing const a = 1 with const a = 2", + width: 34, + height: 3, + render({ renderer }, registerCleanup) { + const client = new MockTreeSitterClient() + registerCleanup(() => client.destroy()) + + const syntaxStyle = SyntaxStyle.fromStyles({ default: { fg: foreground } }) + registerCleanup(() => syntaxStyle.destroy()) + + renderer.root.add( + new DiffRenderable(renderer, { + width: "100%", + height: 3, + diff: patch, + view: "split", + syntaxStyle, + treeSitterClient: client, + fg: foreground, + lineNumberFg: muted, + lineNumberBg: background, + addedBg: background, + removedBg: background, + contextBg: background, + addedLineNumberBg: background, + removedLineNumberBg: background, + addedSignColor: foreground, + removedSignColor: muted, + }), + ) + }, + }, +] diff --git a/packages/web/scripts/doc-visuals/text-cells.ts b/packages/web/scripts/doc-visuals/text-cells.ts new file mode 100644 index 00000000..cc56dbd7 --- /dev/null +++ b/packages/web/scripts/doc-visuals/text-cells.ts @@ -0,0 +1,134 @@ +import { + BoxRenderable, + FrameBufferRenderable, + RGBA, + TextBuffer, + TextBufferView, + TextRenderable, + bg, + bold, + italic, + t, + underline, +} from "@opentui/core" +import type { DocVisualFixture } from "./shared" + +const foreground = RGBA.defaultForeground() +const background = RGBA.defaultBackground() +const muted = RGBA.fromIndex(244) +const surface = RGBA.fromIndex(235) +const selection = RGBA.fromIndex(238) + +export const textCellVisuals: DocVisualFixture[] = [ + { + id: "text-styled-chunks", + label: "Styled chunks: Status is bold, Note is italic, and Next combines bold and underline", + width: 22, + height: 3, + render({ renderer }) { + const content = t`${bold("Status")}: ready\n${italic("Note")}: saved locally\n${bold(underline("Next"))}: review changes` + renderer.root.add(new TextRenderable(renderer, { content, fg: foreground, bg: background })) + }, + }, + { + id: "text-cell-ruler", + label: + "Zero-based display columns: ASCII ABC occupies three cells; A\u754cB occupies four, with \u754c shaded across columns 1 and 2. Each marker follows the text immediately.", + width: 22, + height: 3, + render({ renderer }) { + renderer.root.add(new TextRenderable(renderer, { content: "column 01234", fg: muted, bg: background })) + + for (const [label, content] of [ + ["ASCII", "ABC"], + ["wide", t`A${bg(RGBA.fromIndex(238))("\u754c")}B`], + ]) { + const row = new BoxRenderable(renderer, { flexDirection: "row", height: 1 }) + renderer.root.add(row) + row.add(new TextRenderable(renderer, { content: label, width: 8, fg: muted, bg: background })) + + const text = new TextRenderable(renderer, { content, wrapMode: "none", fg: foreground, bg: surface }) + row.add(text) + row.add( + new TextRenderable(renderer, { content: `| ${text.textLength} cells`, fg: foreground, bg: background }), + ) + } + }, + }, + { + id: "text-wide-wrap", + label: + "Character wrapping of A\u754cB at widths 2, 3, and 4: the two shaded cells occupied by \u754c move intact to the next row when only one cell remains", + width: 34, + height: 5, + render({ renderer }) { + const row = new BoxRenderable(renderer, { flexDirection: "row", gap: 2 }) + renderer.root.add(row) + + for (const width of [2, 3, 4]) { + const column = new BoxRenderable(renderer, { width: 10 }) + row.add(column) + column.add(new TextRenderable(renderer, { content: `${width} columns`, fg: foreground, bg: background })) + column.add(new TextRenderable(renderer, { content: "0123".slice(0, width), fg: muted, bg: background })) + + const viewport = new BoxRenderable(renderer, { width, height: 3, backgroundColor: surface }) + column.add(viewport) + viewport.add( + new TextRenderable(renderer, { + content: t`A${bg(RGBA.fromIndex(238))("\u754c")}B`, + width, + wrapMode: "char", + fg: foreground, + bg: surface, + }), + ) + } + }, + }, + { + id: "text-line-offsets", + label: + "Both three-column views select B: soft-wrapped A\u754cB uses display offsets [3, 4), while A\u754c followed by a newline and B uses [4, 5)", + width: 35, + height: 5, + render({ renderer }, registerCleanup) { + const row = new BoxRenderable(renderer, { flexDirection: "row", gap: 3 }) + renderer.root.add(row) + + for (const [label, content, start] of [ + ["soft wrap", "A\u754cB", 3], + ["newline", "A\u754c\nB", 4], + ] as const) { + const column = new BoxRenderable(renderer, { width: 16 }) + row.add(column) + column.add(new TextRenderable(renderer, { content: label, fg: foreground, bg: background })) + column.add(new TextRenderable(renderer, { content: "012", fg: muted, bg: background })) + + const text = TextBuffer.create(renderer.widthMethod) + registerCleanup(() => text.destroy()) + const view = TextBufferView.create(text) + registerCleanup(() => view.destroy()) + text.setText(content) + text.setDefaultFg(foreground) + text.setDefaultBg(surface) + view.setWrapMode("char") + view.setViewport(0, 0, 3, 2) + view.setSelection(start, start + 1, selection, foreground) + + const canvas = new FrameBufferRenderable(renderer, { width: 3, height: 2 }) + column.add(canvas) + canvas.frameBuffer.clear(surface) + canvas.frameBuffer.drawTextBuffer(view, 0, 0) + + const range = view.getSelection()! + column.add( + new TextRenderable(renderer, { + content: `range [${range.start}, ${range.end})`, + fg: muted, + bg: background, + }), + ) + } + }, + }, +] diff --git a/packages/web/scripts/generate-doc-visuals.ts b/packages/web/scripts/generate-doc-visuals.ts new file mode 100644 index 00000000..f0a81ade --- /dev/null +++ b/packages/web/scripts/generate-doc-visuals.ts @@ -0,0 +1,84 @@ +import { writeFile } from "node:fs/promises" +import { BoxRenderable, RGBA, TextRenderable } from "@opentui/core" +import { foundationVisuals } from "./doc-visuals/foundation" +import { formVisuals } from "./doc-visuals/forms" +import { graphicsVisuals } from "./doc-visuals/graphics" +import { interactionVisuals } from "./doc-visuals/interaction" +import { layoutVisuals } from "./doc-visuals/layout" +import { renderableVisuals } from "./doc-visuals/renderables" +import { scrollingVisuals } from "./doc-visuals/scrolling" +import { renderDocVisual, type DocVisualFixture } from "./doc-visuals/shared" +import { structuredVisuals } from "./doc-visuals/structured" +import { textCellVisuals } from "./doc-visuals/text-cells" + +const styles = [ + { border: "single", description: "single line" }, + { border: "double", description: "double lines" }, + { border: "rounded", description: "round corners" }, + { border: "heavy", description: "heavy strokes" }, +] as const + +const boxBorders: DocVisualFixture = { + id: "box-borders", + label: "Four OpenTUI box border styles: single, double, rounded, and heavy", + width: 38, + height: 7, + render({ renderer }) { + const foreground = RGBA.defaultForeground() + const muted = RGBA.fromIndex(244) + const layout = new BoxRenderable(renderer, { width: 38, height: 7, gap: 1 }) + + for (let index = 0; index < styles.length; index += 2) { + const row = new BoxRenderable(renderer, { width: 38, height: 3, flexDirection: "row", gap: 2 }) + + for (const style of styles.slice(index, index + 2)) { + const box = new BoxRenderable(renderer, { + width: 18, + height: 3, + border: true, + borderStyle: style.border, + borderColor: foreground, + title: style.border, + titleAlignment: "center", + paddingX: 1, + }) + + box.add(new TextRenderable(renderer, { content: style.description, fg: muted })) + row.add(box) + } + + layout.add(row) + } + + renderer.root.add(layout) + }, +} + +export async function generateDocVisuals() { + const fixtures = [ + boxBorders, + ...foundationVisuals, + ...layoutVisuals, + ...renderableVisuals, + ...textCellVisuals, + ...interactionVisuals, + ...formVisuals, + ...scrollingVisuals, + ...structuredVisuals, + ...graphicsVisuals, + ] + const visuals: Record>> = {} + + for (const fixture of fixtures) { + if (Object.hasOwn(visuals, fixture.id)) throw new Error(`Duplicate documentation visual "${fixture.id}"`) + visuals[fixture.id] = await renderDocVisual(fixture) + } + + return visuals +} + +if (import.meta.main) { + const output = new URL("../src/data/doc-visuals.json", import.meta.url) + await writeFile(output, `${JSON.stringify(await generateDocVisuals(), null, 2)}\n`) + console.log("Generated src/data/doc-visuals.json") +} diff --git a/packages/web/scripts/terminal-illustration.test.ts b/packages/web/scripts/terminal-illustration.test.ts new file mode 100644 index 00000000..1773059a --- /dev/null +++ b/packages/web/scripts/terminal-illustration.test.ts @@ -0,0 +1,142 @@ +import { readFile } from "node:fs/promises" +import { runInNewContext } from "node:vm" +import { expect, test } from "bun:test" + +const source = await readFile(new URL("../src/scripts/terminal-illustration.js", import.meta.url), "utf8") + +async function player({ reducedMotion = false } = {}) { + const animationFrames = new Map void>() + const clicks = new Map void>() + const attributes = new Map() + const observers: Array<(entries: Array<{ isIntersecting: boolean }>) => void> = [] + let time = 0 + let nextFrame = 0 + const screen = { innerHTML: "", style: { setProperty() {} } } + const viewport = { style: {}, hasAttribute: () => true, getBoundingClientRect: () => ({ width: 240, height: 30 }) } + const toggle = { + textContent: "Play", + dataset: {}, + addEventListener: (name: string, callback: () => void) => clicks.set(name, callback), + setAttribute: (name: string, value: string) => attributes.set(name, value), + } + const illustration = { + querySelector(selector: string) { + if (selector === "[data-illustration-screen]") return screen + if (selector === "[data-illustration-viewport]") return viewport + if (selector === "[data-illustration-toggle]") return toggle + return null + }, + getAttribute(name: string) { + return name === "data-terminal-illustration" + ? JSON.stringify({ stories: [{ src: "/test.json" }], defaults: { holdMs: 0 } }) + : "Test recording" + }, + setAttribute() {}, + } + const document = { + hidden: false, + fonts: { ready: Promise.resolve() }, + body: { appendChild() {} }, + querySelectorAll: () => [illustration], + addEventListener() {}, + createElement(tag: string) { + return tag === "canvas" + ? { + getContext: () => ({ + font: "", + measureText: () => ({ actualBoundingBoxAscent: 100, actualBoundingBoxDescent: 25 }), + }), + } + : { style: {}, getBoundingClientRect: () => ({ width: 60 }), remove() {} } + }, + } + class IntersectionObserver { + constructor( + private callback: (entries: Array<{ isIntersecting: boolean }>) => void, + private options: { rootMargin?: string }, + ) {} + observe() { + if (this.options.rootMargin) this.callback([{ isIntersecting: true }]) + else observers.push(this.callback) + } + disconnect() {} + } + const window = { + IntersectionObserver, + matchMedia: () => ({ matches: reducedMotion }), + addEventListener() {}, + clearTimeout() {}, + requestAnimationFrame(callback: (now: number) => void) { + const id = ++nextFrame + animationFrames.set(id, callback) + return id + }, + cancelAnimationFrame: (id: number) => animationFrames.delete(id), + } + + runInNewContext(source, { + document, + window, + IntersectionObserver, + performance: { now: () => time }, + getComputedStyle: () => ({ color: "rgb(0, 0, 0)", fontFamily: "monospace" }), + fetch: async () => ({ + ok: true, + json: async () => ({ + cols: 8, + rows: 1, + chapters: [ + { + durationMs: 200, + frames: [ + { at: 0, rows: [[{ t: "Start" }]] }, + { at: 100, rows: [[{ t: "End" }]] }, + ], + }, + ], + }), + }), + }) + await new Promise(setImmediate) + + return { + screen, + toggle, + attributes, + intersect(visible: boolean) { + for (const callback of observers) callback([{ isIntersecting: visible }]) + }, + click() { + clicks.get("click")!() + }, + advance() { + time += 100 + const callbacks = [...animationFrames.values()] + animationFrames.clear() + for (const callback of callbacks) callback(time) + }, + } +} + +test("a single recording repaints its first frame when looping", async () => { + const recording = await player() + recording.intersect(true) + recording.advance() + expect(recording.screen.innerHTML).toContain("End") + recording.advance() + expect(recording.screen.innerHTML).toContain("Start") +}) + +test("manual reduced-motion playback pauses when it leaves the viewport", async () => { + const recording = await player({ reducedMotion: true }) + recording.intersect(true) + expect(recording.toggle.textContent).toBe("Play") + expect(recording.attributes.get("aria-pressed")).toBe("false") + expect(recording.screen.innerHTML).toContain("End") + recording.click() + expect(recording.attributes.get("aria-pressed")).toBe("true") + expect(recording.screen.innerHTML).toContain("Start") + recording.intersect(false) + expect(recording.toggle.textContent).toBe("Play") + expect(recording.attributes.get("aria-pressed")).toBe("false") +}) diff --git a/packages/web/src/components/ProseCode.astro b/packages/web/src/components/ProseCode.astro index 9349f372..e69ef50a 100644 --- a/packages/web/src/components/ProseCode.astro +++ b/packages/web/src/components/ProseCode.astro @@ -1,4 +1,6 @@ --- +import visuals from "../data/doc-visuals.json" + // Wraps markdown code fences (mapped via ``). // The wrapper anchors the copy button outside the scrollable pre, and the // button reads the raw source from data-code (see astro.config.mjs). @@ -6,11 +8,55 @@ // typeface as the article, no code frame, no copy control. const props = Astro.props const source = String(props["data-code"] ?? "") +const visualId = props["data-terminal-visual"] +const visual = typeof visualId === "string" ? visuals[visualId as keyof typeof visuals] : undefined const isDiagram = /^[┌╭┏╔]/.test(source.trimStart()) + +if (visualId && !visual) throw new Error(`Unknown terminal visual "${visualId}"`) + +if (visual) { + const text = visual.lines.map((line) => line.map((span) => span.text).join("").trimEnd()).join("\n") + if (source.trimEnd() !== text.trimEnd()) throw new Error(`Terminal visual "${visualId}" does not match its text fence`) +} + +function color(value: { intent: string; value: string; slot?: number }, background = false) { + if (value.intent === "default") { + return background ? "var(--terminal-background, var(--page-background))" : "var(--page-color)" + } + if (value.intent === "indexed") return `var(--terminal-color-${value.slot}, ${value.value})` + return value.value +} --- { - isDiagram ? ( + visual ? ( +
+ +
+ ) : isDiagram ? (
" }) scrollbox.add(lineNumbers) renderer.root.add(scrollbox) ``` +```text terminal=line-number-editor surface + 1 const port = 3000 +> 2 serve(port) + 3 await ready() +``` + ## Line signs and colors Set a single background for a line, or split gutter and content colors separately with a `LineColorConfig` object: diff --git a/packages/web/src/content/docs/components/markdown.mdx b/packages/web/src/content/docs/components/markdown.mdx index 6e37f251..ec663902 100644 --- a/packages/web/src/content/docs/components/markdown.mdx +++ b/packages/web/src/content/docs/components/markdown.mdx @@ -169,6 +169,28 @@ const markdown = new MarkdownRenderable(renderer, { - **`"grid"`**: boxed table with visible borders. Defaults to `borders: true`, `widthMode: "full"`. This is the normal markdown-in-a-box rendering. - **`"columns"`**: borderless columns with a 2-column gap, defaults to `widthMode: "content"`. Useful for append-only output where a full-width grid feels heavy. +A separate Name/Status sample compares both presets: + +Grid: + +```text terminal=markdown-table-grid +┌────────┬────────┐ +│ Name │ Status │ +├────────┼────────┤ +│ api │ ready │ +├────────┼────────┤ +│ worker │ paused │ +└────────┴────────┘ +``` + +Columns: + +```text terminal=markdown-table-columns +Name Status +api ready +worker paused +``` + If you do not pass `style`, it defaults to `"columns"` when `internalBlockMode` is `"top-level"`, and `"grid"` otherwise. You can still override individual fields (for example, `borders: true`) to pull toward a different look. ## Custom node rendering diff --git a/packages/web/src/content/docs/components/qr-code.mdx b/packages/web/src/content/docs/components/qr-code.mdx index 52b02477..d0fd51d3 100644 --- a/packages/web/src/content/docs/components/qr-code.mdx +++ b/packages/web/src/content/docs/components/qr-code.mdx @@ -103,6 +103,26 @@ Use `fit: "none"` when you want the configured scale to be the only rendered siz `quietZone` must be at least 4 modules for a standard QR code. OpenTUI validates the matrix and quiet-zone geometry, but scanner reliability still depends on terminal font, cell aspect ratio, display contrast, and camera conditions. +The content `OPENTUI` and a four-module quiet zone fit a version-one symbol: + +```text terminal=qr-code-version-one + + + █▀▀▀▀▀█ ▀█▄▀█ █▀▀▀▀▀█ + █ ███ █ ▀ ▀▀█ █ ███ █ + █ ▀▀▀ █ ▀ ▀█ █ ▀▀▀ █ + ▀▀▀▀▀▀▀ █ ▀▄█ ▀▀▀▀▀▀▀ + ▄▄ ▄ ▀▀▄ █ ▄▀▄▀▄█▄█▄█ + ▄█▀▀█▄▀ █ ▀▀▀▄▄█ ▀█▄ + ▀ ▀ ▀▀█ ██▄ ▀▀ ▀ ▀ + █▀▀▀▀▀█ ▄▄▀ ▀▀▄ ▀█▄█▀ + █ ███ █ ▀███▀█ ▀▄▀ + █ ▀▀▀ █ █ ▀▀▀▀▀█▄▀▀█ + ▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀▀ + + +``` + ## Fallback content When a container is too small to render even a scale-1 QR code, show fallback text instead of an empty area: diff --git a/packages/web/src/content/docs/components/scrollbar.mdx b/packages/web/src/content/docs/components/scrollbar.mdx index 77647dbc..0ea08810 100644 --- a/packages/web/src/content/docs/components/scrollbar.mdx +++ b/packages/web/src/content/docs/components/scrollbar.mdx @@ -49,6 +49,19 @@ renderer.root.add(scrollbar) scrollbar.focus() ``` +```text terminal=scrollbar-arrows +position: 0 / 180 ▲ + ▀ + + + + + + + +viewport: 20 / 200 ▼ +``` + ## Keyboard controls When focused, the scrollbar responds to: diff --git a/packages/web/src/content/docs/components/scrollbox.mdx b/packages/web/src/content/docs/components/scrollbox.mdx index 6b167b12..672344ae 100644 --- a/packages/web/src/content/docs/components/scrollbox.mdx +++ b/packages/web/src/content/docs/components/scrollbox.mdx @@ -47,6 +47,19 @@ for (let i = 0; i < 100; i++) { renderer.root.add(scrollbox) ``` +A bordered list of source files initially shows its first five rows: + +```text terminal=scrollbox-top +offset: 0 / 7 +┌──────────────────────────────┐ +│01 src/index.ts ▀│ +│02 src/app.ts │ +│03 src/layout.ts │ +│04 src/theme.ts │ +│05 src/events.ts │ +└──────────────────────────────┘ +``` + ## Sticky scroll Enable sticky scroll to keep content pinned to an edge as new content arrives. Set both `stickyScroll` and `stickyStart` because `stickyStart` has no default. @@ -180,6 +193,19 @@ scrollbox.scrollTo(0) scrollbox.scrollTo({ x: 0, y: 100 }) ``` +The same list after `scrollTo(5)`: + +```text terminal=scrollbox-scrolled +offset: 5 / 7 +┌──────────────────────────────┐ +│06 src/input.ts │ +│07 src/scroll.ts │ +│08 src/render.ts │ +│09 src/state.ts ▀│ +│10 src/config.ts │ +└──────────────────────────────┘ +``` + ### scrollChildIntoView Scroll the minimum distance needed to show a nested child in the viewport. The method uses DOM-style "nearest" diff --git a/packages/web/src/content/docs/components/select.mdx b/packages/web/src/content/docs/components/select.mdx index 576a0023..eeac0f0d 100644 --- a/packages/web/src/content/docs/components/select.mdx +++ b/packages/web/src/content/docs/components/select.mdx @@ -30,13 +30,12 @@ const renderer = await createCliRenderer() const menu = new SelectRenderable(renderer, { id: "menu", - width: 30, - height: 8, + width: 32, + height: 6, options: [ - { name: "New File", description: "Create a new file" }, - { name: "Open File", description: "Open an existing file" }, - { name: "Save", description: "Save current file" }, - { name: "Exit", description: "Exit the application" }, + { name: "New file", description: "Create a document" }, + { name: "Open file", description: "Browse existing files" }, + { name: "Save", description: "Write current changes" }, ], }) @@ -48,6 +47,17 @@ menu.focus() renderer.root.add(menu) ``` +After pressing Down, the next option is selected: + +```text terminal=select-options surface + New file + Create a document + ▶ Open file + Browse existing files + Save + Write current changes +``` + ## Keyboard navigation When focused, the select responds to these keys: diff --git a/packages/web/src/content/docs/components/slider.mdx b/packages/web/src/content/docs/components/slider.mdx index 42621434..fcb92822 100644 --- a/packages/web/src/content/docs/components/slider.mdx +++ b/packages/web/src/content/docs/components/slider.mdx @@ -42,6 +42,12 @@ const slider = new SliderRenderable(renderer, { renderer.root.add(slider) ``` +```text terminal=slider-horizontal +value: 25 + ██▌ +0 100 +``` + ## Vertical slider ```typescript @@ -55,6 +61,19 @@ const slider = new SliderRenderable(renderer, { }) ``` +```text terminal=slider-vertical +min 0 + + ▄▄ + ██ +value 0.5 ██ + ██ + ██ + ▀▀ + +max 1 +``` + ## Properties | Property | Type | Default | Description | diff --git a/packages/web/src/content/docs/components/tab-select.mdx b/packages/web/src/content/docs/components/tab-select.mdx index e595cca7..611b1233 100644 --- a/packages/web/src/content/docs/components/tab-select.mdx +++ b/packages/web/src/content/docs/components/tab-select.mdx @@ -30,13 +30,13 @@ const renderer = await createCliRenderer() const tabs = new TabSelectRenderable(renderer, { id: "tabs", - width: 60, + width: 36, options: [ - { name: "Home", description: "Dashboard and overview" }, - { name: "Files", description: "File management" }, - { name: "Settings", description: "Application settings" }, + { name: "Home", description: "View project overview" }, + { name: "Files", description: "Browse project files" }, + { name: "Settings", description: "Configure the project" }, ], - tabWidth: 20, + tabWidth: 12, }) tabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index, option) => { @@ -47,6 +47,14 @@ tabs.focus() renderer.root.add(tabs) ``` +After pressing Right, Files is selected: + +```text terminal=tab-select-tabs surface + Home Files Settings + ▬▬▬▬▬▬▬▬▬▬▬▬ + Browse project files +``` + ## Keyboard navigation When focused, the tab select responds to these keys: diff --git a/packages/web/src/content/docs/components/text-table.mdx b/packages/web/src/content/docs/components/text-table.mdx index 9bc011ed..ecabb07d 100644 --- a/packages/web/src/content/docs/components/text-table.mdx +++ b/packages/web/src/content/docs/components/text-table.mdx @@ -45,6 +45,18 @@ const table = new TextTableRenderable(renderer, { renderer.root.add(table) ``` +Monochrome preview of the same table: + +```text terminal=text-table-styled +╭───────┬────────┬────────────────╮ +│Service│Status │Notes │ +├───────┼────────┼────────────────┤ +│api │OK │latency 28ms │ +├───────┼────────┼────────────────┤ +│worker │DEGRADED│queue depth: 124│ +╰───────┴────────┴────────────────╯ +``` + ## Content The public content types are: diff --git a/packages/web/src/content/docs/components/text.mdx b/packages/web/src/content/docs/components/text.mdx index 64b3cfb8..fbb454ef 100644 --- a/packages/web/src/content/docs/components/text.mdx +++ b/packages/web/src/content/docs/components/text.mdx @@ -53,6 +53,14 @@ const styledText = new TextRenderable(renderer, { }) ``` +Bold, italic, and underline can also be applied independently: + +```text terminal=text-attributes +bold Important message +italic Additional context +underline Documentation +``` + ### Available attributes | Attribute | Description | diff --git a/packages/web/src/content/docs/components/textarea.mdx b/packages/web/src/content/docs/components/textarea.mdx index c5e103a8..d75e5783 100644 --- a/packages/web/src/content/docs/components/textarea.mdx +++ b/packages/web/src/content/docs/components/textarea.mdx @@ -41,6 +41,20 @@ renderer.root.add(textarea) textarea.focus() ``` +At 30 columns, longer text wraps at word boundaries: + +```typescript +textarea.width = 30 +textarea.setText("Long lines wrap at word boundaries.\nKeep paragraphs readable.") +``` + +```text terminal=textarea-wrap surface +Notes +Long lines wrap at word +boundaries. +Keep paragraphs readable. +``` + ## Submit handling Bind a submit action and listen for `onSubmit`: @@ -151,6 +165,15 @@ textarea.clearSelection() textarea.deleteSelection() ``` +Selecting `keyboard focus` in a draft: + +```text terminal=textarea-selection surface +Draft +Plan the release +Review keyboard focus +Ship the update +``` + ### Editing ```typescript diff --git a/packages/web/src/content/docs/core-concepts/colors.mdx b/packages/web/src/content/docs/core-concepts/colors.mdx index 38db58b7..558cf7b7 100644 --- a/packages/web/src/content/docs/core-concepts/colors.mdx +++ b/packages/web/src/content/docs/core-concepts/colors.mdx @@ -62,6 +62,21 @@ Both constructors convert a non-finite channel to `0`. Channel setters apply the An alpha value of zero is transparent. Intermediate alpha values blend during supported buffer-composition operations. +```typescript +const overlay = RGBA.fromHex("#22c55e") +overlay.a = 0.5 +buffer.fillRect(x, y, width, height, overlay) +``` + +These tiles draw the same green over a checkerboard. Only alpha changes: + +```text terminal=color-alpha + + + + 0.00 0.25 0.50 0.75 1.00 +``` + After a blend, the result is a literal RGB color. It no longer identifies a terminal default or indexed palette slot. See [FrameBuffer](/docs/components/frame-buffer) and the [Buffer API](/docs/reference/buffer-api) for direct alpha drawing. @@ -89,6 +104,33 @@ console.log(foreground.intent, background.intent) `fromIndex()` requires an integer from `0` through `255` and throws `RangeError` otherwise. Its default RGB snapshot comes from the built-in ANSI 256-color table. +The fallback palette contains 16 terminal colors, a 216-color RGB cube, and 24 grays. Within each cube slice, blue increases to the right and green increases downward: + +```text terminal=color-palette +0-15 terminal colors +████████████████████████████████ + +16-231 RGB cube +R=0 R=95 R=135 +████████████ ████████████ ████████████ +████████████ ████████████ ████████████ +████████████ ████████████ ████████████ +████████████ ████████████ ████████████ +████████████ ████████████ ████████████ +████████████ ████████████ ████████████ + +R=175 R=215 R=255 +████████████ ████████████ ████████████ +████████████ ████████████ ████████████ +████████████ ████████████ ████████████ +████████████ ████████████ ████████████ +████████████ ████████████ ████████████ +████████████ ████████████ ████████████ + +232-255 grayscale +████████████████████████ +``` + Default foreground uses `[255, 255, 255]` as its snapshot. Default background uses `[0, 0, 0]`. An optional snapshot supplies RGBA data for blending and early frames. It does not change the indexed or default intent. diff --git a/packages/web/src/content/docs/core-concepts/interaction.mdx b/packages/web/src/content/docs/core-concepts/interaction.mdx index e0cbd78f..69ca65ea 100644 --- a/packages/web/src/content/docs/core-concepts/interaction.mdx +++ b/packages/web/src/content/docs/core-concepts/interaction.mdx @@ -71,6 +71,22 @@ The hit grid obeys clipping from `overflow: "hidden"` and `"scroll"`. A layout c Mouse events start at the hit target and bubble through `parent` links. `event.stopPropagation()` prevents later ancestors from receiving that event. +In this overlapping pair, the front box receives both mouse downs. The first bubbles to `parent`; the second calls +`stopPropagation()` in the front box's handler. + +```text terminal=interaction-hit-bubbling +┌─parent─────────────────────────────┐ +│ │ +│ ┌─back z=0─────────────┐ │ +│ │ ┌─front z=1─────────────┐ │ +│ └────────│ │ │ +│ └───────────────────────┘ │ +└────────────────────────────────────┘ +target: front +bubble: front -> parent +stopped: front +``` + `event.preventDefault()` has only defined renderer defaults. On left-button `down`, it prevents automatic focus and the post-dispatch selection clear. It does not stop propagation. It also does not undo a new selection that already started on selectable text. @@ -95,6 +111,20 @@ Each renderer tracks at most one focused renderable. Focusing another renderable Use `focus()` and `blur()` for explicit control. Listen for `RenderableEvents.FOCUSED` and `RenderableEvents.BLURRED` on the instance. +Focusing an input does not clear a selection in another renderable: + +```typescript +input.focus() +input.value = "deploy --check" +``` + +```text terminal=interaction-selection-focus surface +Select text, then focus input + +Command +deploy --check +``` + By default, a left-button down focuses the nearest focusable target or ancestor. Set renderer `autoFocus: false` to disable this behavior. OpenTUI Core has no automatic Tab traversal or focus-order property. Your application must choose the next renderable and call `focus()`. @@ -174,6 +204,16 @@ These offsets form a half-open range from the start of that text buffer. They co The offsets are not UTF-16 indexes. Selection boundaries snap around complete grapheme clusters, including a cluster that spans multiple cells. +A drag can span lines. Here, the selected text includes the line break after `app`, and the local range ends after `Test`. + +```text terminal=interaction-drag-selection surface +Build the app +Test the input + +Selected: "the app\nTest" +Range: [6, 18) +``` + Read [Text and terminal cells](/docs/core-concepts/text-and-cells) before you combine selection offsets with JavaScript string methods. ## Test interaction diff --git a/packages/web/src/content/docs/core-concepts/layout.mdx b/packages/web/src/content/docs/core-concepts/layout.mdx index 4b3df0fd..026c34b5 100644 --- a/packages/web/src/content/docs/core-concepts/layout.mdx +++ b/packages/web/src/content/docs/core-concepts/layout.mdx @@ -12,24 +12,51 @@ OpenTUI uses Yoga to compute a renderable tree on a grid of terminal cells. It s A horizontal size is a count of terminal columns. A vertical size is a count of terminal rows. These values are never character counts. +## Spacing and growth + ```typescript -import { BoxRenderable, TextRenderable, createCliRenderer } from "@opentui/core" +import { BoxRenderable, RGBA, TextRenderable, createCliRenderer } from "@opentui/core" const renderer = await createCliRenderer() +const foreground = RGBA.defaultForeground() +const fill = RGBA.fromIndex(238) const row = new BoxRenderable(renderer, { - width: "100%", + width: 34, height: 5, + border: true, + borderColor: foreground, + padding: 1, flexDirection: "row", - alignItems: "center", - gap: 1, + gap: 2, }) -row.add(new TextRenderable(renderer, { content: "Status" })) -row.add(new TextRenderable(renderer, { content: "Ready", flexGrow: 1 })) +const fixed = new BoxRenderable(renderer, { width: 8, height: 1, backgroundColor: fill }) +fixed.add(new TextRenderable(renderer, { content: "fixed 8", fg: foreground })) + +const growing = new BoxRenderable(renderer, { + flexGrow: 1, + flexBasis: 0, + height: 1, + backgroundColor: fill, +}) +growing.add(new TextRenderable(renderer, { content: "grow 20", fg: foreground })) + +row.add(fixed) +row.add(growing) renderer.root.add(row) ``` +The shaded children occupy 8 and 20 columns. The border and padding consume four columns; the gap consumes two more. + +```text terminal=layout-flex-columns +┌────────────────────────────────┐ +│ │ +│ fixed 8 grow 20 │ +│ │ +└────────────────────────────────┘ +``` + The same layout properties work across the built-in renderable classes. ## Supported options @@ -63,6 +90,51 @@ Yoga align values are `"auto"`, `"flex-start"`, `"center"`, `"flex-end"`, `"stre The public TypeScript interface includes `"auto"` for min and max dimensions. The current runtime ignores that value for those four options. +## Alignment + +In a row, `justifyContent` distributes horizontal space and `alignItems` positions children vertically. A child can override the vertical alignment with `alignSelf`. + +```typescript +const row = new BoxRenderable(renderer, { + width: 32, + height: 7, + border: true, + borderColor: foreground, + paddingX: 1, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", +}) + +for (const [label, height] of [ + ["A", 1], + ["B", 3], + ["C", 1], +] as const) { + const child = new BoxRenderable(renderer, { + width: 6, + height, + alignSelf: label === "C" ? "flex-end" : "auto", + backgroundColor: fill, + }) + child.add(new TextRenderable(renderer, { content: label, fg: foreground })) + row.add(child) +} +renderer.root.add(row) +``` + +A and B share a vertical center despite their different heights. C sits at the bottom. + +```text terminal=layout-alignment +┌──────────────────────────────┐ +│ │ +│ B │ +│ A │ +│ │ +│ C │ +└──────────────────────────────┘ +``` + ## Automatic and intrinsic size An `"auto"` dimension lets Yoga derive size from children or a renderable's measure function. Text and editor renderables supply native measure targets. @@ -91,12 +163,81 @@ Yoga uses a point scale factor of `1`. It rounds computed edges to whole termina Percentage and flex calculations can produce fractions before this step. Adjacent children can therefore receive different rounded widths. +These three children use the same grow factor and zero basis: + +```typescript +const child = new BoxRenderable(renderer, { + flexGrow: 1, + flexBasis: 0, + minWidth: 0, +}) +``` + +A 32-column bordered row leaves 30 inner columns. Adding one column gives the middle child an extra cell, without gaps or overlaps: + +```text terminal=layout-cell-rounding +32 columns: 30 inside +┌──────────────────────────────┐ +│ 10 10 10 │ +└──────────────────────────────┘ +33 columns: 31 inside +┌───────────────────────────────┐ +│ 10 11 10 │ +└───────────────────────────────┘ +``` + Renderable getters expose computed integer geometry after a layout pass. OpenTUI clamps exposed `width` and `height` to at least one cell. ## Resize behavior A terminal resize keeps the same renderable instances. The renderer resizes its root and runs Yoga again with the new column and row counts. +With `flexWrap: "wrap"`, whole children move to the next row when they no longer fit. The container's automatic height grows to include them: + +```typescript +const row = new BoxRenderable(renderer, { + width: "100%", + border: true, + borderColor: foreground, + paddingX: 1, + flexDirection: "row", + flexWrap: "wrap", + alignItems: "flex-start", + columnGap: 2, + rowGap: 1, +}) + +for (const content of ["A", "B", "C"]) { + const child = new BoxRenderable(renderer, { + width: 8, + height: 1, + flexShrink: 0, + backgroundColor: fill, + }) + child.add(new TextRenderable(renderer, { content, fg: foreground })) + row.add(child) +} +renderer.root.add(row) +``` + +The same children before and after narrowing the terminal: + +```text terminal=layout-wrap-wide +32 columns +┌──────────────────────────────┐ +│ A B C │ +└──────────────────────────────┘ +``` + +```text terminal=layout-wrap-narrow +22 columns +┌────────────────────┐ +│ A B │ +│ │ +│ C │ +└────────────────────┘ +``` + Computed `x`, `y`, `width`, and `height` can all change. Buffered renderables resize their frame buffers before `onResize(width, height)` runs. `FrameBufferRenderable` resizes its public `frameBuffer` in its `onResize()` override, then calls the base hook. diff --git a/packages/web/src/content/docs/core-concepts/renderables.mdx b/packages/web/src/content/docs/core-concepts/renderables.mdx index 34096849..3d9bdf82 100644 --- a/packages/web/src/content/docs/core-concepts/renderables.mdx +++ b/packages/web/src/content/docs/core-concepts/renderables.mdx @@ -7,28 +7,57 @@ description: Create, mutate, reparent, and destroy imperative renderable tree no A renderable is an imperative node in OpenTUI's retained tree. It stores layout, visual state, children, event handlers, and native resources. +Retained means the same objects stay in the tree between frames. Change their properties instead of rebuilding them for each update. + +## Create and update + Create a renderable with a render context. A `CliRenderer` implements that context. ```typescript -import { BoxRenderable, TextRenderable, createCliRenderer } from "@opentui/core" +import { BoxRenderable, RGBA, TextRenderable, createCliRenderer } from "@opentui/core" const renderer = await createCliRenderer() +const foreground = RGBA.defaultForeground() +const fill = RGBA.fromIndex(243) const panel = new BoxRenderable(renderer, { id: "panel", - width: 30, - padding: 1, + width: 18, + height: 3, + paddingX: 1, border: true, + borderColor: foreground, + backgroundColor: fill, }) const status = new TextRenderable(renderer, { id: "status", content: "Waiting", + fg: foreground, }) panel.add(status) renderer.root.add(panel) +``` +The shaded panel contains a text child: + +```text terminal=renderable-created +┌────────────────┐ +│ Waiting │ +└────────────────┘ +``` + +Update the existing nodes. The wider panel also gives its text child more space: + +```typescript status.content = "Ready" +panel.width = 30 +``` + +```text terminal=renderable-mutated +┌────────────────────────────┐ +│ Ready │ +└────────────────────────────┘ ``` Setters such as `content`, `width`, `visible`, and `zIndex` request another render when their state changes. @@ -38,17 +67,53 @@ Setters such as `content`, `width`, `visible`, and `zIndex` request another rend Each renderable has at most one parent. `add()` reparents an existing node when necessary. ```typescript -const first = new BoxRenderable(renderer, { id: "first" }) -const second = new BoxRenderable(renderer, { id: "second" }) -const child = new TextRenderable(renderer, { id: "message", content: "Moved" }) +const row = new BoxRenderable(renderer, { width: 34, flexDirection: "row", gap: 2 }) +renderer.root.add(row) -first.add(child) -second.add(child) +const [first, second] = ["first", "second"].map((id) => { + const parent = new BoxRenderable(renderer, { + id, + title: id, + width: 16, + height: 3, + paddingX: 1, + border: true, + borderColor: foreground, + }) + row.add(parent) + return parent +}) +const message = new BoxRenderable(renderer, { + id: "message", + width: 10, + height: 1, + backgroundColor: fill, +}) +message.add(new TextRenderable(renderer, { content: "Message", fg: foreground })) +first.add(message) +``` -console.log(child.parent === second) // true +```text terminal=renderable-reparent-before +┌─first────────┐ ┌─second───────┐ +│ Message │ │ │ +└──────────────┘ └──────────────┘ +``` + +Add the same subtree to `second`. You do not need to remove it from `first`: + +```typescript +second.add(message) + +console.log(message.parent === second) // true console.log(first.getChildrenCount()) // 0 ``` +```text terminal=renderable-reparent-after +┌─first────────┐ ┌─second───────┐ +│ │ │ Message │ +└──────────────┘ └──────────────┘ +``` + `add(child, index)` inserts at an index. `insertBefore(child, anchor)` inserts before a direct child. Both methods return the inserted index, or `-1` when they cannot add the value. Use these methods to inspect the tree: @@ -58,18 +123,60 @@ Use these methods to inspect the tree: - `getRenderable(id)` finds a direct child. - `findDescendantById(id)` searches descendants recursively. -`remove(child)` only detaches a direct child. It does not destroy that child, so you can add the child elsewhere. - Reparenting calls `remove()` on the old parent. As a result, `onRemove()` runs for a temporary detach and for a reparent. Do not release final owned resources in `onRemove()`. Release them in `destroySelf()`, which runs only during destruction. See [Lifecycle and cleanup](/docs/core-concepts/lifecycle). +## Hide or detach + +Hiding a child keeps it in the tree. `remove(child)` detaches a direct child without destroying it. + +Start with two children: + +```typescript +const panel = new BoxRenderable(renderer, { + width: 12, + height: 4, + border: true, + borderColor: foreground, +}) +const detail = new BoxRenderable(renderer, { height: 1, backgroundColor: fill }) +detail.add(new TextRenderable(renderer, { content: "Detail", fg: foreground })) +const next = new TextRenderable(renderer, { content: "Next", fg: foreground }) +panel.add(detail) +panel.add(next) +renderer.root.add(panel) +``` + +Both hiding and detaching move `Next` up. Only detaching changes tree membership: + +```typescript +detail.visible = false +console.log(panel.getChildrenCount(), detail.parent === panel) // 2, true + +detail.visible = true +panel.remove(detail) +console.log(panel.getChildrenCount(), detail.parent) // 1, null +``` + +```text terminal=renderable-visibility +visible hidden detached +┌──────────┐ ┌──────────┐ ┌──────────┐ +│Detail │ │Next │ │Next │ +│Next │ │ │ │ │ +└──────────┘ └──────────┘ └──────────┘ +children: 2 children: 2 children: 1 +parent: yes parent: yes parent: no +``` + +Neither operation destroys `detail`. Set `visible = true` to show a hidden child, or call `add(detail)` to reattach a detached child. Reattaching does not change its visibility. + +`visible = false` sets the Yoga node to `display: none`. The node does not receive layout or render work while hidden. Hiding a focused node also blurs it. + ## Layout properties Renderables participate in the [Yoga-based layout model](/docs/core-concepts/layout). Their computed `x`, `y`, `width`, and `height` can change after layout or terminal resize. -`visible = false` sets the Yoga node to `display: none`. The node does not receive layout or render work while hidden. Hiding a focused node also blurs it. - `zIndex` changes sibling render and hit-test order without changing layout order. `translateX` and `translateY` move drawing and hit bounds without changing the Yoga result. `opacity` applies to the node and its descendants. `overflow: "hidden"` or `"scroll"` clips rendering and mouse hit bounds to the node. @@ -83,10 +190,13 @@ Shared mouse, focus, and selection behavior belongs to [Interaction, focus, and `destroy()` detaches direct children but does not destroy them. Use `destroyRecursively()` when this node owns the complete subtree. ```typescript +detail.destroyRecursively() panel.destroyRecursively() ``` -The renderer destroys its root recursively during renderer cleanup. Do not add a destroyed renderable to another parent. +The renderer destroys its root recursively during renderer cleanup. Destroy detached subtrees yourself when you no longer need them. + +Do not add a destroyed renderable to another parent. Subclass render hooks, measurement, buffering, and resource examples belong to [Custom renderables](/docs/extend/custom-renderables). diff --git a/packages/web/src/content/docs/core-concepts/text-and-cells.mdx b/packages/web/src/content/docs/core-concepts/text-and-cells.mdx index c07f7e97..ec246a40 100644 --- a/packages/web/src/content/docs/core-concepts/text-and-cells.mdx +++ b/packages/web/src/content/docs/core-concepts/text-and-cells.mdx @@ -16,13 +16,21 @@ String length and terminal width are different values. Keep that distinction whe Use `t` as a template tag. Style helpers return chunks that you can insert into the template. ```typescript -import { TextRenderable, bold, fg, link, t, underline, createCliRenderer } from "@opentui/core" +import { RGBA, TextRenderable, bold, italic, t, underline, createCliRenderer } from "@opentui/core" const renderer = await createCliRenderer() -const content = t`${bold("Status")}: ${fg("#22c55e")("ready")} ${link("https://opentui.dev")(underline("docs"))}` +const content = t`${bold("Status")}: ready\n${italic("Note")}: saved locally\n${bold(underline("Next"))}: review changes` -renderer.root.add(new TextRenderable(renderer, { content })) +renderer.root.add(new TextRenderable(renderer, { content, fg: RGBA.defaultForeground() })) +``` + +`Status` is bold, `Note` is italic, and `Next` combines bold and underline. The rest of each line keeps its default attributes: + +```text terminal=text-styled-chunks surface +Status: ready +Note: saved locally +Next: review changes ``` The color helpers include normal, bright, and background named colors. The text helpers include `bold`, `italic`, `underline`, `strikethrough`, `dim`, `blink`, and `reverse`. @@ -68,11 +76,46 @@ The first occupied cell stores the grapheme reference and its extent. Remaining Continuation cells prevent later drawing and diffing from treating the tail of a wide grapheme as independent text. They contain no separate printable character. +Both `"ABC".length` and `"A界B".length` are `3`. On this zero-based cell ruler, `界` occupies columns 1 and 2. It pushes `B` to column 3 and the following `|` to column 4: + +```text terminal=text-cell-ruler +column 01234 +ASCII ABC| 3 cells +wide A界B| 4 cells +``` + +The stronger background shade marks the two cells occupied by `界`. + +The measured width is a column count, not a JavaScript string length. A string with length `2` can still occupy one or two terminal cells. + +## Wrap text + Wrapping and truncation operate on display width. They do not split a grapheme to make it fit at a line boundary. Text renderables support `wrapMode: "word"`, `"char"`, or `"none"`. The default is `"word"`. -The measured width is a column count. For example, a JavaScript string with length `2` can still occupy one or two terminal cells. +```typescript +const text = new TextRenderable(renderer, { + content: "A界B", + width: 2, + wrapMode: "char", + fg: RGBA.defaultForeground(), +}) + +renderer.root.add(text) +``` + +The same text at widths of two, three, and four columns: + +```text terminal=text-wide-wrap +2 columns 3 columns 4 columns +01 012 0123 +A A界 A界B +界 B +B +``` + +At two columns, `界` cannot fit beside `A`, so it moves intact to the next row. The shaded area marks each view's cell bounds. ## Use buffer text operations @@ -100,6 +143,18 @@ Text-buffer selection uses a half-open range `{ start, end }`. Both values are g Each logical line break adds one offset unit. Soft wrapping does not add a unit because it does not add text. +These three-column views look the same, but they select `B` at different offsets. `"A界B"` uses `[3, 4)`; `"A界\nB"` uses `[4, 5)` because the newline adds one unit: + +```text terminal=text-line-offsets +soft wrap newline +012 012 +A界 A界 +B B +range [3, 4) range [4, 5) +``` + +The highlighted cell marks the selection. Both views return `"B"` from `getSelectedText()`. + Selection extraction snaps boundaries to complete grapheme clusters. A boundary inside a wide grapheme moves to include or exclude the complete cluster. These offsets are not UTF-16 indexes. Do not pass them directly to `String.prototype.slice()` for text that can contain wide or combined graphemes. diff --git a/packages/web/src/content/docs/keymap/overview.mdx b/packages/web/src/content/docs/keymap/overview.mdx index faf0914c..d03353b3 100644 --- a/packages/web/src/content/docs/keymap/overview.mdx +++ b/packages/web/src/content/docs/keymap/overview.mdx @@ -59,6 +59,11 @@ keymap.registerLayer({ Each registration returns a disposer. Destroying the renderer also ends the host lifecycle and releases keymap resources. See [Lifecycle and cleanup](/docs/core-concepts/lifecycle) for application shutdown. +```text terminal=keymap-active-keys +ctrl+s file.save +q app.quit +``` + ## Register, dispatch, and query Keymap uses one short model: diff --git a/packages/web/src/content/docs/plugins/slots.mdx b/packages/web/src/content/docs/plugins/slots.mdx index 0d9f946c..102a82a8 100644 --- a/packages/web/src/content/docs/plugins/slots.mdx +++ b/packages/web/src/content/docs/plugins/slots.mdx @@ -162,6 +162,12 @@ Every slot mount or `` component accepts a mode. | `replace` | Show contribution output. Show the fallback when no contribution has output. | | `single_winner` | Show only the first contribution. Show the fallback when it has no output. | +```text terminal=plugin-slot-modes +append host clock sync +replace clock sync +single_winner clock +``` + ## Resolve contributions ```typescript diff --git a/packages/web/src/data/doc-visuals.json b/packages/web/src/data/doc-visuals.json new file mode 100644 index 00000000..d2156161 --- /dev/null +++ b/packages/web/src/data/doc-visuals.json @@ -0,0 +1,11208 @@ +{ + "box-borders": { + "label": "Four OpenTUI box border styles: single, double, rounded, and heavy", + "cols": 38, + "rows": 7, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "┌─────single─────┐", + "width": 18, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "╔═════double═════╗", + "width": 18, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "single line", + "width": 11, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "║", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "double lines", + "width": 12, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": "║", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└────────────────┘", + "width": 18, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "╚════════════════╝", + "width": 18, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 38, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "╭────rounded─────╮", + "width": 18, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "┏━━━━━heavy━━━━━━┓", + "width": 18, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "round corners", + "width": 13, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "┃", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "heavy strokes", + "width": 13, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "┃", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "╰────────────────╯", + "width": 18, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "┗━━━━━━━━━━━━━━━━┛", + "width": 18, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "text-attributes": { + "label": "Text attributes applied to bold, italic, and underlined content", + "cols": 30, + "rows": 3, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "rgb", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "bold ", + "width": 11, + "foreground": 0, + "background": 1 + }, + { + "text": "Important message", + "width": 17, + "foreground": 2, + "background": 1, + "attributes": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "italic ", + "width": 11, + "foreground": 0, + "background": 1 + }, + { + "text": "Additional context", + "width": 18, + "foreground": 2, + "background": 1, + "attributes": 4 + }, + { + "text": " ", + "width": 1, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "underline ", + "width": 11, + "foreground": 0, + "background": 1 + }, + { + "text": "Documentation", + "width": 13, + "foreground": 2, + "background": 1, + "attributes": 8 + }, + { + "text": " ", + "width": 6, + "foreground": 3, + "background": 1 + } + ] + ] + }, + "box-title-alignment": { + "label": "Box with a centered top title and right-aligned bottom title", + "cols": 32, + "rows": 3, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "┌───────────settings───────────┐", + "width": 32, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "Top and bottom titles", + "width": 21, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└────────────────────────close─┘", + "width": 32, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "color-palette": { + "label": "OpenTUI's 256-color fallback palette: 16 terminal colors, six red-level slices of the RGB cube, and 24 grays. Blue increases rightward and green downward within each slice.", + "cols": 38, + "rows": 22, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "rgb", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#800000" + }, + { + "intent": "rgb", + "value": "#008000" + }, + { + "intent": "rgb", + "value": "#808000" + }, + { + "intent": "rgb", + "value": "#000080" + }, + { + "intent": "rgb", + "value": "#800080" + }, + { + "intent": "rgb", + "value": "#008080" + }, + { + "intent": "rgb", + "value": "#c0c0c0" + }, + { + "intent": "rgb", + "value": "#808080" + }, + { + "intent": "rgb", + "value": "#ff0000" + }, + { + "intent": "rgb", + "value": "#00ff00" + }, + { + "intent": "rgb", + "value": "#ffff00" + }, + { + "intent": "rgb", + "value": "#0000ff" + }, + { + "intent": "rgb", + "value": "#ff00ff" + }, + { + "intent": "rgb", + "value": "#00ffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "rgb", + "value": "#00005f" + }, + { + "intent": "rgb", + "value": "#000087" + }, + { + "intent": "rgb", + "value": "#0000af" + }, + { + "intent": "rgb", + "value": "#0000d7" + }, + { + "intent": "rgb", + "value": "#5f0000" + }, + { + "intent": "rgb", + "value": "#5f005f" + }, + { + "intent": "rgb", + "value": "#5f0087" + }, + { + "intent": "rgb", + "value": "#5f00af" + }, + { + "intent": "rgb", + "value": "#5f00d7" + }, + { + "intent": "rgb", + "value": "#5f00ff" + }, + { + "intent": "rgb", + "value": "#870000" + }, + { + "intent": "rgb", + "value": "#87005f" + }, + { + "intent": "rgb", + "value": "#870087" + }, + { + "intent": "rgb", + "value": "#8700af" + }, + { + "intent": "rgb", + "value": "#8700d7" + }, + { + "intent": "rgb", + "value": "#8700ff" + }, + { + "intent": "rgb", + "value": "#005f00" + }, + { + "intent": "rgb", + "value": "#005f5f" + }, + { + "intent": "rgb", + "value": "#005f87" + }, + { + "intent": "rgb", + "value": "#005faf" + }, + { + "intent": "rgb", + "value": "#005fd7" + }, + { + "intent": "rgb", + "value": "#005fff" + }, + { + "intent": "rgb", + "value": "#5f5f00" + }, + { + "intent": "rgb", + "value": "#5f5f5f" + }, + { + "intent": "rgb", + "value": "#5f5f87" + }, + { + "intent": "rgb", + "value": "#5f5faf" + }, + { + "intent": "rgb", + "value": "#5f5fd7" + }, + { + "intent": "rgb", + "value": "#5f5fff" + }, + { + "intent": "rgb", + "value": "#875f00" + }, + { + "intent": "rgb", + "value": "#875f5f" + }, + { + "intent": "rgb", + "value": "#875f87" + }, + { + "intent": "rgb", + "value": "#875faf" + }, + { + "intent": "rgb", + "value": "#875fd7" + }, + { + "intent": "rgb", + "value": "#875fff" + }, + { + "intent": "rgb", + "value": "#008700" + }, + { + "intent": "rgb", + "value": "#00875f" + }, + { + "intent": "rgb", + "value": "#008787" + }, + { + "intent": "rgb", + "value": "#0087af" + }, + { + "intent": "rgb", + "value": "#0087d7" + }, + { + "intent": "rgb", + "value": "#0087ff" + }, + { + "intent": "rgb", + "value": "#5f8700" + }, + { + "intent": "rgb", + "value": "#5f875f" + }, + { + "intent": "rgb", + "value": "#5f8787" + }, + { + "intent": "rgb", + "value": "#5f87af" + }, + { + "intent": "rgb", + "value": "#5f87d7" + }, + { + "intent": "rgb", + "value": "#5f87ff" + }, + { + "intent": "rgb", + "value": "#878700" + }, + { + "intent": "rgb", + "value": "#87875f" + }, + { + "intent": "rgb", + "value": "#878787" + }, + { + "intent": "rgb", + "value": "#8787af" + }, + { + "intent": "rgb", + "value": "#8787d7" + }, + { + "intent": "rgb", + "value": "#8787ff" + }, + { + "intent": "rgb", + "value": "#00af00" + }, + { + "intent": "rgb", + "value": "#00af5f" + }, + { + "intent": "rgb", + "value": "#00af87" + }, + { + "intent": "rgb", + "value": "#00afaf" + }, + { + "intent": "rgb", + "value": "#00afd7" + }, + { + "intent": "rgb", + "value": "#00afff" + }, + { + "intent": "rgb", + "value": "#5faf00" + }, + { + "intent": "rgb", + "value": "#5faf5f" + }, + { + "intent": "rgb", + "value": "#5faf87" + }, + { + "intent": "rgb", + "value": "#5fafaf" + }, + { + "intent": "rgb", + "value": "#5fafd7" + }, + { + "intent": "rgb", + "value": "#5fafff" + }, + { + "intent": "rgb", + "value": "#87af00" + }, + { + "intent": "rgb", + "value": "#87af5f" + }, + { + "intent": "rgb", + "value": "#87af87" + }, + { + "intent": "rgb", + "value": "#87afaf" + }, + { + "intent": "rgb", + "value": "#87afd7" + }, + { + "intent": "rgb", + "value": "#87afff" + }, + { + "intent": "rgb", + "value": "#00d700" + }, + { + "intent": "rgb", + "value": "#00d75f" + }, + { + "intent": "rgb", + "value": "#00d787" + }, + { + "intent": "rgb", + "value": "#00d7af" + }, + { + "intent": "rgb", + "value": "#00d7d7" + }, + { + "intent": "rgb", + "value": "#00d7ff" + }, + { + "intent": "rgb", + "value": "#5fd700" + }, + { + "intent": "rgb", + "value": "#5fd75f" + }, + { + "intent": "rgb", + "value": "#5fd787" + }, + { + "intent": "rgb", + "value": "#5fd7af" + }, + { + "intent": "rgb", + "value": "#5fd7d7" + }, + { + "intent": "rgb", + "value": "#5fd7ff" + }, + { + "intent": "rgb", + "value": "#87d700" + }, + { + "intent": "rgb", + "value": "#87d75f" + }, + { + "intent": "rgb", + "value": "#87d787" + }, + { + "intent": "rgb", + "value": "#87d7af" + }, + { + "intent": "rgb", + "value": "#87d7d7" + }, + { + "intent": "rgb", + "value": "#87d7ff" + }, + { + "intent": "rgb", + "value": "#00ff5f" + }, + { + "intent": "rgb", + "value": "#00ff87" + }, + { + "intent": "rgb", + "value": "#00ffaf" + }, + { + "intent": "rgb", + "value": "#00ffd7" + }, + { + "intent": "rgb", + "value": "#5fff00" + }, + { + "intent": "rgb", + "value": "#5fff5f" + }, + { + "intent": "rgb", + "value": "#5fff87" + }, + { + "intent": "rgb", + "value": "#5fffaf" + }, + { + "intent": "rgb", + "value": "#5fffd7" + }, + { + "intent": "rgb", + "value": "#5fffff" + }, + { + "intent": "rgb", + "value": "#87ff00" + }, + { + "intent": "rgb", + "value": "#87ff5f" + }, + { + "intent": "rgb", + "value": "#87ff87" + }, + { + "intent": "rgb", + "value": "#87ffaf" + }, + { + "intent": "rgb", + "value": "#87ffd7" + }, + { + "intent": "rgb", + "value": "#87ffff" + }, + { + "intent": "rgb", + "value": "#af0000" + }, + { + "intent": "rgb", + "value": "#af005f" + }, + { + "intent": "rgb", + "value": "#af0087" + }, + { + "intent": "rgb", + "value": "#af00af" + }, + { + "intent": "rgb", + "value": "#af00d7" + }, + { + "intent": "rgb", + "value": "#af00ff" + }, + { + "intent": "rgb", + "value": "#d70000" + }, + { + "intent": "rgb", + "value": "#d7005f" + }, + { + "intent": "rgb", + "value": "#d70087" + }, + { + "intent": "rgb", + "value": "#d700af" + }, + { + "intent": "rgb", + "value": "#d700d7" + }, + { + "intent": "rgb", + "value": "#d700ff" + }, + { + "intent": "rgb", + "value": "#ff005f" + }, + { + "intent": "rgb", + "value": "#ff0087" + }, + { + "intent": "rgb", + "value": "#ff00af" + }, + { + "intent": "rgb", + "value": "#ff00d7" + }, + { + "intent": "rgb", + "value": "#af5f00" + }, + { + "intent": "rgb", + "value": "#af5f5f" + }, + { + "intent": "rgb", + "value": "#af5f87" + }, + { + "intent": "rgb", + "value": "#af5faf" + }, + { + "intent": "rgb", + "value": "#af5fd7" + }, + { + "intent": "rgb", + "value": "#af5fff" + }, + { + "intent": "rgb", + "value": "#d75f00" + }, + { + "intent": "rgb", + "value": "#d75f5f" + }, + { + "intent": "rgb", + "value": "#d75f87" + }, + { + "intent": "rgb", + "value": "#d75faf" + }, + { + "intent": "rgb", + "value": "#d75fd7" + }, + { + "intent": "rgb", + "value": "#d75fff" + }, + { + "intent": "rgb", + "value": "#ff5f00" + }, + { + "intent": "rgb", + "value": "#ff5f5f" + }, + { + "intent": "rgb", + "value": "#ff5f87" + }, + { + "intent": "rgb", + "value": "#ff5faf" + }, + { + "intent": "rgb", + "value": "#ff5fd7" + }, + { + "intent": "rgb", + "value": "#ff5fff" + }, + { + "intent": "rgb", + "value": "#af8700" + }, + { + "intent": "rgb", + "value": "#af875f" + }, + { + "intent": "rgb", + "value": "#af8787" + }, + { + "intent": "rgb", + "value": "#af87af" + }, + { + "intent": "rgb", + "value": "#af87d7" + }, + { + "intent": "rgb", + "value": "#af87ff" + }, + { + "intent": "rgb", + "value": "#d78700" + }, + { + "intent": "rgb", + "value": "#d7875f" + }, + { + "intent": "rgb", + "value": "#d78787" + }, + { + "intent": "rgb", + "value": "#d787af" + }, + { + "intent": "rgb", + "value": "#d787d7" + }, + { + "intent": "rgb", + "value": "#d787ff" + }, + { + "intent": "rgb", + "value": "#ff8700" + }, + { + "intent": "rgb", + "value": "#ff875f" + }, + { + "intent": "rgb", + "value": "#ff8787" + }, + { + "intent": "rgb", + "value": "#ff87af" + }, + { + "intent": "rgb", + "value": "#ff87d7" + }, + { + "intent": "rgb", + "value": "#ff87ff" + }, + { + "intent": "rgb", + "value": "#afaf00" + }, + { + "intent": "rgb", + "value": "#afaf5f" + }, + { + "intent": "rgb", + "value": "#afaf87" + }, + { + "intent": "rgb", + "value": "#afafaf" + }, + { + "intent": "rgb", + "value": "#afafd7" + }, + { + "intent": "rgb", + "value": "#afafff" + }, + { + "intent": "rgb", + "value": "#d7af00" + }, + { + "intent": "rgb", + "value": "#d7af5f" + }, + { + "intent": "rgb", + "value": "#d7af87" + }, + { + "intent": "rgb", + "value": "#d7afaf" + }, + { + "intent": "rgb", + "value": "#d7afd7" + }, + { + "intent": "rgb", + "value": "#d7afff" + }, + { + "intent": "rgb", + "value": "#ffaf00" + }, + { + "intent": "rgb", + "value": "#ffaf5f" + }, + { + "intent": "rgb", + "value": "#ffaf87" + }, + { + "intent": "rgb", + "value": "#ffafaf" + }, + { + "intent": "rgb", + "value": "#ffafd7" + }, + { + "intent": "rgb", + "value": "#ffafff" + }, + { + "intent": "rgb", + "value": "#afd700" + }, + { + "intent": "rgb", + "value": "#afd75f" + }, + { + "intent": "rgb", + "value": "#afd787" + }, + { + "intent": "rgb", + "value": "#afd7af" + }, + { + "intent": "rgb", + "value": "#afd7d7" + }, + { + "intent": "rgb", + "value": "#afd7ff" + }, + { + "intent": "rgb", + "value": "#d7d700" + }, + { + "intent": "rgb", + "value": "#d7d75f" + }, + { + "intent": "rgb", + "value": "#d7d787" + }, + { + "intent": "rgb", + "value": "#d7d7af" + }, + { + "intent": "rgb", + "value": "#d7d7d7" + }, + { + "intent": "rgb", + "value": "#d7d7ff" + }, + { + "intent": "rgb", + "value": "#ffd700" + }, + { + "intent": "rgb", + "value": "#ffd75f" + }, + { + "intent": "rgb", + "value": "#ffd787" + }, + { + "intent": "rgb", + "value": "#ffd7af" + }, + { + "intent": "rgb", + "value": "#ffd7d7" + }, + { + "intent": "rgb", + "value": "#ffd7ff" + }, + { + "intent": "rgb", + "value": "#afff00" + }, + { + "intent": "rgb", + "value": "#afff5f" + }, + { + "intent": "rgb", + "value": "#afff87" + }, + { + "intent": "rgb", + "value": "#afffaf" + }, + { + "intent": "rgb", + "value": "#afffd7" + }, + { + "intent": "rgb", + "value": "#afffff" + }, + { + "intent": "rgb", + "value": "#d7ff00" + }, + { + "intent": "rgb", + "value": "#d7ff5f" + }, + { + "intent": "rgb", + "value": "#d7ff87" + }, + { + "intent": "rgb", + "value": "#d7ffaf" + }, + { + "intent": "rgb", + "value": "#d7ffd7" + }, + { + "intent": "rgb", + "value": "#d7ffff" + }, + { + "intent": "rgb", + "value": "#ffff5f" + }, + { + "intent": "rgb", + "value": "#ffff87" + }, + { + "intent": "rgb", + "value": "#ffffaf" + }, + { + "intent": "rgb", + "value": "#ffffd7" + }, + { + "intent": "rgb", + "value": "#080808" + }, + { + "intent": "rgb", + "value": "#121212" + }, + { + "intent": "rgb", + "value": "#1c1c1c" + }, + { + "intent": "rgb", + "value": "#262626" + }, + { + "intent": "rgb", + "value": "#303030" + }, + { + "intent": "rgb", + "value": "#3a3a3a" + }, + { + "intent": "rgb", + "value": "#444444" + }, + { + "intent": "rgb", + "value": "#4e4e4e" + }, + { + "intent": "rgb", + "value": "#585858" + }, + { + "intent": "rgb", + "value": "#626262" + }, + { + "intent": "rgb", + "value": "#6c6c6c" + }, + { + "intent": "rgb", + "value": "#767676" + }, + { + "intent": "rgb", + "value": "#8a8a8a" + }, + { + "intent": "rgb", + "value": "#949494" + }, + { + "intent": "rgb", + "value": "#9e9e9e" + }, + { + "intent": "rgb", + "value": "#a8a8a8" + }, + { + "intent": "rgb", + "value": "#b2b2b2" + }, + { + "intent": "rgb", + "value": "#bcbcbc" + }, + { + "intent": "rgb", + "value": "#c6c6c6" + }, + { + "intent": "rgb", + "value": "#d0d0d0" + }, + { + "intent": "rgb", + "value": "#dadada" + }, + { + "intent": "rgb", + "value": "#e4e4e4" + }, + { + "intent": "rgb", + "value": "#eeeeee" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "0-15 terminal colors", + "width": 23, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 15, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 3, + "background": 3 + }, + { + "text": "██", + "width": 2, + "foreground": 4, + "background": 4 + }, + { + "text": "██", + "width": 2, + "foreground": 5, + "background": 5 + }, + { + "text": "██", + "width": 2, + "foreground": 6, + "background": 6 + }, + { + "text": "██", + "width": 2, + "foreground": 7, + "background": 7 + }, + { + "text": "██", + "width": 2, + "foreground": 8, + "background": 8 + }, + { + "text": "██", + "width": 2, + "foreground": 9, + "background": 9 + }, + { + "text": "██", + "width": 2, + "foreground": 10, + "background": 10 + }, + { + "text": "██", + "width": 2, + "foreground": 11, + "background": 11 + }, + { + "text": "██", + "width": 2, + "foreground": 12, + "background": 12 + }, + { + "text": "██", + "width": 2, + "foreground": 13, + "background": 13 + }, + { + "text": "██", + "width": 2, + "foreground": 14, + "background": 14 + }, + { + "text": "██", + "width": 2, + "foreground": 15, + "background": 15 + }, + { + "text": "██", + "width": 2, + "foreground": 16, + "background": 16 + }, + { + "text": "██", + "width": 2, + "foreground": 17, + "background": 17 + }, + { + "text": "██", + "width": 2, + "foreground": 2, + "background": 2 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 38, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "16-231 RGB cube", + "width": 16, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 22, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "R=0", + "width": 3, + "foreground": 18, + "background": 1 + }, + { + "text": " ", + "width": 10, + "foreground": 2, + "background": 1 + }, + { + "text": "R=95", + "width": 4, + "foreground": 18, + "background": 1 + }, + { + "text": " ", + "width": 9, + "foreground": 2, + "background": 1 + }, + { + "text": "R=135", + "width": 5, + "foreground": 18, + "background": 1 + }, + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 3, + "background": 3 + }, + { + "text": "██", + "width": 2, + "foreground": 19, + "background": 19 + }, + { + "text": "██", + "width": 2, + "foreground": 20, + "background": 20 + }, + { + "text": "██", + "width": 2, + "foreground": 21, + "background": 21 + }, + { + "text": "██", + "width": 2, + "foreground": 22, + "background": 22 + }, + { + "text": "██", + "width": 2, + "foreground": 15, + "background": 15 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 23, + "background": 23 + }, + { + "text": "██", + "width": 2, + "foreground": 24, + "background": 24 + }, + { + "text": "██", + "width": 2, + "foreground": 25, + "background": 25 + }, + { + "text": "██", + "width": 2, + "foreground": 26, + "background": 26 + }, + { + "text": "██", + "width": 2, + "foreground": 27, + "background": 27 + }, + { + "text": "██", + "width": 2, + "foreground": 28, + "background": 28 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 29, + "background": 29 + }, + { + "text": "██", + "width": 2, + "foreground": 30, + "background": 30 + }, + { + "text": "██", + "width": 2, + "foreground": 31, + "background": 31 + }, + { + "text": "██", + "width": 2, + "foreground": 32, + "background": 32 + }, + { + "text": "██", + "width": 2, + "foreground": 33, + "background": 33 + }, + { + "text": "██", + "width": 2, + "foreground": 34, + "background": 34 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 35, + "background": 35 + }, + { + "text": "██", + "width": 2, + "foreground": 36, + "background": 36 + }, + { + "text": "██", + "width": 2, + "foreground": 37, + "background": 37 + }, + { + "text": "██", + "width": 2, + "foreground": 38, + "background": 38 + }, + { + "text": "██", + "width": 2, + "foreground": 39, + "background": 39 + }, + { + "text": "██", + "width": 2, + "foreground": 40, + "background": 40 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 41, + "background": 41 + }, + { + "text": "██", + "width": 2, + "foreground": 42, + "background": 42 + }, + { + "text": "██", + "width": 2, + "foreground": 43, + "background": 43 + }, + { + "text": "██", + "width": 2, + "foreground": 44, + "background": 44 + }, + { + "text": "██", + "width": 2, + "foreground": 45, + "background": 45 + }, + { + "text": "██", + "width": 2, + "foreground": 46, + "background": 46 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 47, + "background": 47 + }, + { + "text": "██", + "width": 2, + "foreground": 48, + "background": 48 + }, + { + "text": "██", + "width": 2, + "foreground": 49, + "background": 49 + }, + { + "text": "██", + "width": 2, + "foreground": 50, + "background": 50 + }, + { + "text": "██", + "width": 2, + "foreground": 51, + "background": 51 + }, + { + "text": "██", + "width": 2, + "foreground": 52, + "background": 52 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 53, + "background": 53 + }, + { + "text": "██", + "width": 2, + "foreground": 54, + "background": 54 + }, + { + "text": "██", + "width": 2, + "foreground": 55, + "background": 55 + }, + { + "text": "██", + "width": 2, + "foreground": 56, + "background": 56 + }, + { + "text": "██", + "width": 2, + "foreground": 57, + "background": 57 + }, + { + "text": "██", + "width": 2, + "foreground": 58, + "background": 58 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 59, + "background": 59 + }, + { + "text": "██", + "width": 2, + "foreground": 60, + "background": 60 + }, + { + "text": "██", + "width": 2, + "foreground": 61, + "background": 61 + }, + { + "text": "██", + "width": 2, + "foreground": 62, + "background": 62 + }, + { + "text": "██", + "width": 2, + "foreground": 63, + "background": 63 + }, + { + "text": "██", + "width": 2, + "foreground": 64, + "background": 64 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 65, + "background": 65 + }, + { + "text": "██", + "width": 2, + "foreground": 66, + "background": 66 + }, + { + "text": "██", + "width": 2, + "foreground": 67, + "background": 67 + }, + { + "text": "██", + "width": 2, + "foreground": 68, + "background": 68 + }, + { + "text": "██", + "width": 2, + "foreground": 69, + "background": 69 + }, + { + "text": "██", + "width": 2, + "foreground": 70, + "background": 70 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 71, + "background": 71 + }, + { + "text": "██", + "width": 2, + "foreground": 72, + "background": 72 + }, + { + "text": "██", + "width": 2, + "foreground": 73, + "background": 73 + }, + { + "text": "██", + "width": 2, + "foreground": 74, + "background": 74 + }, + { + "text": "██", + "width": 2, + "foreground": 75, + "background": 75 + }, + { + "text": "██", + "width": 2, + "foreground": 76, + "background": 76 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 77, + "background": 77 + }, + { + "text": "██", + "width": 2, + "foreground": 78, + "background": 78 + }, + { + "text": "██", + "width": 2, + "foreground": 79, + "background": 79 + }, + { + "text": "██", + "width": 2, + "foreground": 80, + "background": 80 + }, + { + "text": "██", + "width": 2, + "foreground": 81, + "background": 81 + }, + { + "text": "██", + "width": 2, + "foreground": 82, + "background": 82 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 83, + "background": 83 + }, + { + "text": "██", + "width": 2, + "foreground": 84, + "background": 84 + }, + { + "text": "██", + "width": 2, + "foreground": 85, + "background": 85 + }, + { + "text": "██", + "width": 2, + "foreground": 86, + "background": 86 + }, + { + "text": "██", + "width": 2, + "foreground": 87, + "background": 87 + }, + { + "text": "██", + "width": 2, + "foreground": 88, + "background": 88 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 89, + "background": 89 + }, + { + "text": "██", + "width": 2, + "foreground": 90, + "background": 90 + }, + { + "text": "██", + "width": 2, + "foreground": 91, + "background": 91 + }, + { + "text": "██", + "width": 2, + "foreground": 92, + "background": 92 + }, + { + "text": "██", + "width": 2, + "foreground": 93, + "background": 93 + }, + { + "text": "██", + "width": 2, + "foreground": 94, + "background": 94 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 95, + "background": 95 + }, + { + "text": "██", + "width": 2, + "foreground": 96, + "background": 96 + }, + { + "text": "██", + "width": 2, + "foreground": 97, + "background": 97 + }, + { + "text": "██", + "width": 2, + "foreground": 98, + "background": 98 + }, + { + "text": "██", + "width": 2, + "foreground": 99, + "background": 99 + }, + { + "text": "██", + "width": 2, + "foreground": 100, + "background": 100 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 101, + "background": 101 + }, + { + "text": "██", + "width": 2, + "foreground": 102, + "background": 102 + }, + { + "text": "██", + "width": 2, + "foreground": 103, + "background": 103 + }, + { + "text": "██", + "width": 2, + "foreground": 104, + "background": 104 + }, + { + "text": "██", + "width": 2, + "foreground": 105, + "background": 105 + }, + { + "text": "██", + "width": 2, + "foreground": 106, + "background": 106 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 13, + "background": 13 + }, + { + "text": "██", + "width": 2, + "foreground": 107, + "background": 107 + }, + { + "text": "██", + "width": 2, + "foreground": 108, + "background": 108 + }, + { + "text": "██", + "width": 2, + "foreground": 109, + "background": 109 + }, + { + "text": "██", + "width": 2, + "foreground": 110, + "background": 110 + }, + { + "text": "██", + "width": 2, + "foreground": 17, + "background": 17 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 111, + "background": 111 + }, + { + "text": "██", + "width": 2, + "foreground": 112, + "background": 112 + }, + { + "text": "██", + "width": 2, + "foreground": 113, + "background": 113 + }, + { + "text": "██", + "width": 2, + "foreground": 114, + "background": 114 + }, + { + "text": "██", + "width": 2, + "foreground": 115, + "background": 115 + }, + { + "text": "██", + "width": 2, + "foreground": 116, + "background": 116 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 117, + "background": 117 + }, + { + "text": "██", + "width": 2, + "foreground": 118, + "background": 118 + }, + { + "text": "██", + "width": 2, + "foreground": 119, + "background": 119 + }, + { + "text": "██", + "width": 2, + "foreground": 120, + "background": 120 + }, + { + "text": "██", + "width": 2, + "foreground": 121, + "background": 121 + }, + { + "text": "██", + "width": 2, + "foreground": 122, + "background": 122 + } + ], + [ + { + "text": " ", + "width": 38, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "R=175", + "width": 5, + "foreground": 18, + "background": 1 + }, + { + "text": " ", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": "R=215", + "width": 5, + "foreground": 18, + "background": 1 + }, + { + "text": " ", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": "R=255", + "width": 5, + "foreground": 18, + "background": 1 + }, + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 123, + "background": 123 + }, + { + "text": "██", + "width": 2, + "foreground": 124, + "background": 124 + }, + { + "text": "██", + "width": 2, + "foreground": 125, + "background": 125 + }, + { + "text": "██", + "width": 2, + "foreground": 126, + "background": 126 + }, + { + "text": "██", + "width": 2, + "foreground": 127, + "background": 127 + }, + { + "text": "██", + "width": 2, + "foreground": 128, + "background": 128 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 129, + "background": 129 + }, + { + "text": "██", + "width": 2, + "foreground": 130, + "background": 130 + }, + { + "text": "██", + "width": 2, + "foreground": 131, + "background": 131 + }, + { + "text": "██", + "width": 2, + "foreground": 132, + "background": 132 + }, + { + "text": "██", + "width": 2, + "foreground": 133, + "background": 133 + }, + { + "text": "██", + "width": 2, + "foreground": 134, + "background": 134 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 12, + "background": 12 + }, + { + "text": "██", + "width": 2, + "foreground": 135, + "background": 135 + }, + { + "text": "██", + "width": 2, + "foreground": 136, + "background": 136 + }, + { + "text": "██", + "width": 2, + "foreground": 137, + "background": 137 + }, + { + "text": "██", + "width": 2, + "foreground": 138, + "background": 138 + }, + { + "text": "██", + "width": 2, + "foreground": 16, + "background": 16 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 139, + "background": 139 + }, + { + "text": "██", + "width": 2, + "foreground": 140, + "background": 140 + }, + { + "text": "██", + "width": 2, + "foreground": 141, + "background": 141 + }, + { + "text": "██", + "width": 2, + "foreground": 142, + "background": 142 + }, + { + "text": "██", + "width": 2, + "foreground": 143, + "background": 143 + }, + { + "text": "██", + "width": 2, + "foreground": 144, + "background": 144 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 145, + "background": 145 + }, + { + "text": "██", + "width": 2, + "foreground": 146, + "background": 146 + }, + { + "text": "██", + "width": 2, + "foreground": 147, + "background": 147 + }, + { + "text": "██", + "width": 2, + "foreground": 148, + "background": 148 + }, + { + "text": "██", + "width": 2, + "foreground": 149, + "background": 149 + }, + { + "text": "██", + "width": 2, + "foreground": 150, + "background": 150 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 151, + "background": 151 + }, + { + "text": "██", + "width": 2, + "foreground": 152, + "background": 152 + }, + { + "text": "██", + "width": 2, + "foreground": 153, + "background": 153 + }, + { + "text": "██", + "width": 2, + "foreground": 154, + "background": 154 + }, + { + "text": "██", + "width": 2, + "foreground": 155, + "background": 155 + }, + { + "text": "██", + "width": 2, + "foreground": 156, + "background": 156 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 157, + "background": 157 + }, + { + "text": "██", + "width": 2, + "foreground": 158, + "background": 158 + }, + { + "text": "██", + "width": 2, + "foreground": 159, + "background": 159 + }, + { + "text": "██", + "width": 2, + "foreground": 160, + "background": 160 + }, + { + "text": "██", + "width": 2, + "foreground": 161, + "background": 161 + }, + { + "text": "██", + "width": 2, + "foreground": 162, + "background": 162 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 163, + "background": 163 + }, + { + "text": "██", + "width": 2, + "foreground": 164, + "background": 164 + }, + { + "text": "██", + "width": 2, + "foreground": 165, + "background": 165 + }, + { + "text": "██", + "width": 2, + "foreground": 166, + "background": 166 + }, + { + "text": "██", + "width": 2, + "foreground": 167, + "background": 167 + }, + { + "text": "██", + "width": 2, + "foreground": 168, + "background": 168 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 169, + "background": 169 + }, + { + "text": "██", + "width": 2, + "foreground": 170, + "background": 170 + }, + { + "text": "██", + "width": 2, + "foreground": 171, + "background": 171 + }, + { + "text": "██", + "width": 2, + "foreground": 172, + "background": 172 + }, + { + "text": "██", + "width": 2, + "foreground": 173, + "background": 173 + }, + { + "text": "██", + "width": 2, + "foreground": 174, + "background": 174 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 175, + "background": 175 + }, + { + "text": "██", + "width": 2, + "foreground": 176, + "background": 176 + }, + { + "text": "██", + "width": 2, + "foreground": 177, + "background": 177 + }, + { + "text": "██", + "width": 2, + "foreground": 178, + "background": 178 + }, + { + "text": "██", + "width": 2, + "foreground": 179, + "background": 179 + }, + { + "text": "██", + "width": 2, + "foreground": 180, + "background": 180 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 181, + "background": 181 + }, + { + "text": "██", + "width": 2, + "foreground": 182, + "background": 182 + }, + { + "text": "██", + "width": 2, + "foreground": 183, + "background": 183 + }, + { + "text": "██", + "width": 2, + "foreground": 184, + "background": 184 + }, + { + "text": "██", + "width": 2, + "foreground": 185, + "background": 185 + }, + { + "text": "██", + "width": 2, + "foreground": 186, + "background": 186 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 187, + "background": 187 + }, + { + "text": "██", + "width": 2, + "foreground": 188, + "background": 188 + }, + { + "text": "██", + "width": 2, + "foreground": 189, + "background": 189 + }, + { + "text": "██", + "width": 2, + "foreground": 190, + "background": 190 + }, + { + "text": "██", + "width": 2, + "foreground": 191, + "background": 191 + }, + { + "text": "██", + "width": 2, + "foreground": 192, + "background": 192 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 193, + "background": 193 + }, + { + "text": "██", + "width": 2, + "foreground": 194, + "background": 194 + }, + { + "text": "██", + "width": 2, + "foreground": 195, + "background": 195 + }, + { + "text": "██", + "width": 2, + "foreground": 196, + "background": 196 + }, + { + "text": "██", + "width": 2, + "foreground": 197, + "background": 197 + }, + { + "text": "██", + "width": 2, + "foreground": 198, + "background": 198 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 199, + "background": 199 + }, + { + "text": "██", + "width": 2, + "foreground": 200, + "background": 200 + }, + { + "text": "██", + "width": 2, + "foreground": 201, + "background": 201 + }, + { + "text": "██", + "width": 2, + "foreground": 202, + "background": 202 + }, + { + "text": "██", + "width": 2, + "foreground": 203, + "background": 203 + }, + { + "text": "██", + "width": 2, + "foreground": 204, + "background": 204 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 205, + "background": 205 + }, + { + "text": "██", + "width": 2, + "foreground": 206, + "background": 206 + }, + { + "text": "██", + "width": 2, + "foreground": 207, + "background": 207 + }, + { + "text": "██", + "width": 2, + "foreground": 208, + "background": 208 + }, + { + "text": "██", + "width": 2, + "foreground": 209, + "background": 209 + }, + { + "text": "██", + "width": 2, + "foreground": 210, + "background": 210 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 211, + "background": 211 + }, + { + "text": "██", + "width": 2, + "foreground": 212, + "background": 212 + }, + { + "text": "██", + "width": 2, + "foreground": 213, + "background": 213 + }, + { + "text": "██", + "width": 2, + "foreground": 214, + "background": 214 + }, + { + "text": "██", + "width": 2, + "foreground": 215, + "background": 215 + }, + { + "text": "██", + "width": 2, + "foreground": 216, + "background": 216 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 217, + "background": 217 + }, + { + "text": "██", + "width": 2, + "foreground": 218, + "background": 218 + }, + { + "text": "██", + "width": 2, + "foreground": 219, + "background": 219 + }, + { + "text": "██", + "width": 2, + "foreground": 220, + "background": 220 + }, + { + "text": "██", + "width": 2, + "foreground": 221, + "background": 221 + }, + { + "text": "██", + "width": 2, + "foreground": 222, + "background": 222 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 14, + "background": 14 + }, + { + "text": "██", + "width": 2, + "foreground": 223, + "background": 223 + }, + { + "text": "██", + "width": 2, + "foreground": 224, + "background": 224 + }, + { + "text": "██", + "width": 2, + "foreground": 225, + "background": 225 + }, + { + "text": "██", + "width": 2, + "foreground": 226, + "background": 226 + }, + { + "text": "██", + "width": 2, + "foreground": 2, + "background": 2 + } + ], + [ + { + "text": " ", + "width": 38, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "232-255 grayscale", + "width": 17, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 21, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "█", + "width": 1, + "foreground": 227, + "background": 227 + }, + { + "text": "█", + "width": 1, + "foreground": 228, + "background": 228 + }, + { + "text": "█", + "width": 1, + "foreground": 229, + "background": 229 + }, + { + "text": "█", + "width": 1, + "foreground": 230, + "background": 230 + }, + { + "text": "█", + "width": 1, + "foreground": 231, + "background": 231 + }, + { + "text": "█", + "width": 1, + "foreground": 232, + "background": 232 + }, + { + "text": "█", + "width": 1, + "foreground": 233, + "background": 233 + }, + { + "text": "█", + "width": 1, + "foreground": 234, + "background": 234 + }, + { + "text": "█", + "width": 1, + "foreground": 235, + "background": 235 + }, + { + "text": "█", + "width": 1, + "foreground": 236, + "background": 236 + }, + { + "text": "█", + "width": 1, + "foreground": 237, + "background": 237 + }, + { + "text": "█", + "width": 1, + "foreground": 238, + "background": 238 + }, + { + "text": "█", + "width": 1, + "foreground": 11, + "background": 11 + }, + { + "text": "█", + "width": 1, + "foreground": 239, + "background": 239 + }, + { + "text": "█", + "width": 1, + "foreground": 240, + "background": 240 + }, + { + "text": "█", + "width": 1, + "foreground": 241, + "background": 241 + }, + { + "text": "█", + "width": 1, + "foreground": 242, + "background": 242 + }, + { + "text": "█", + "width": 1, + "foreground": 243, + "background": 243 + }, + { + "text": "█", + "width": 1, + "foreground": 244, + "background": 244 + }, + { + "text": "█", + "width": 1, + "foreground": 245, + "background": 245 + }, + { + "text": "█", + "width": 1, + "foreground": 246, + "background": 246 + }, + { + "text": "█", + "width": 1, + "foreground": 247, + "background": 247 + }, + { + "text": "█", + "width": 1, + "foreground": 248, + "background": 248 + }, + { + "text": "█", + "width": 1, + "foreground": 249, + "background": 249 + }, + { + "text": " ", + "width": 14, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "color-alpha": { + "label": "Green #22c55e over a gray checkerboard at alpha 0, 0.25, 0.5, 0.75, and 1. The checkerboard remains visible through translucent tiles and disappears at alpha 1.", + "cols": 34, + "rows": 4, + "colors": [ + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "rgb", + "value": "#e0e0e0" + }, + { + "intent": "rgb", + "value": "#a0a0a0" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#b0d9bf" + }, + { + "intent": "rgb", + "value": "#80a98f" + }, + { + "intent": "rgb", + "value": "#81d29f" + }, + { + "intent": "rgb", + "value": "#61b37f" + }, + { + "intent": "rgb", + "value": "#52cc7f" + }, + { + "intent": "rgb", + "value": "#42bc6f" + }, + { + "intent": "rgb", + "value": "#22c55e" + }, + { + "intent": "default", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 2 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 5 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 6 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 7 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 6 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 8 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 9 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 8 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 6, + "foreground": 0, + "background": 10 + } + ], + [ + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 2 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 2 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 5 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 5 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 7 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 6 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 7 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 9 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 8 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 9 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 6, + "foreground": 0, + "background": 10 + } + ], + [ + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 2 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 5 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 6 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 7 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 6 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 8 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 9 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 8 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 6, + "foreground": 0, + "background": 10 + } + ], + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": "0.00", + "width": 4, + "foreground": 11, + "background": 3 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 3 + }, + { + "text": "0.25", + "width": 4, + "foreground": 11, + "background": 3 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 3 + }, + { + "text": "0.50", + "width": 4, + "foreground": 11, + "background": 3 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 3 + }, + { + "text": "0.75", + "width": 4, + "foreground": 11, + "background": 3 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 3 + }, + { + "text": "1.00", + "width": 4, + "foreground": 11, + "background": 3 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + } + ] + ] + }, + "layout-flex-columns": { + "label": "A 34-column container with a one-cell border and padding, an eight-column fixed child, a two-column gap, and a growing child filling the remaining 20 columns", + "cols": 34, + "rows": 5, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "┌────────────────────────────────┐", + "width": 34, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 32, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "fixed 8", + "width": 7, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "grow 20", + "width": 7, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 13, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 32, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└────────────────────────────────┘", + "width": 34, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "layout-alignment": { + "label": "Three children distributed horizontally with space-between: one-row A and three-row B share a vertical center, while C overrides alignment to sit at the bottom", + "cols": 32, + "rows": 7, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "┌──────────────────────────────┐", + "width": 32, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 30, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + }, + { + "text": "B", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 5, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "A", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 5, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 5, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 23, + "foreground": 2, + "background": 1 + }, + { + "text": "C", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 5, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└──────────────────────────────┘", + "width": 32, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "layout-cell-rounding": { + "label": "Three equal grow factors divide 30 inner columns into 10, 10, and 10 cells; adding one terminal column changes the allocation to 10, 11, and 10 cells", + "cols": 33, + "rows": 8, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + }, + { + "intent": "indexed", + "value": "#262626", + "slot": 235 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "32 columns: 30 inside", + "width": 21, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "┌──────────────────────────────┐", + "width": 32, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 3 + }, + { + "text": "10", + "width": 2, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 4 + }, + { + "text": "10", + "width": 2, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 4 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 3 + }, + { + "text": "10", + "width": 2, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 3 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "└──────────────────────────────┘", + "width": 32, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "33 columns: 31 inside", + "width": 21, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "┌───────────────────────────────┐", + "width": 33, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 3 + }, + { + "text": "10", + "width": 2, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 4 + }, + { + "text": "11", + "width": 2, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 5, + "foreground": 2, + "background": 4 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 3 + }, + { + "text": "10", + "width": 2, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 3 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└───────────────────────────────┘", + "width": 33, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "layout-wrap-wide": { + "label": "At 32 terminal columns, three eight-column children A, B, and C fit in one row with two-column gaps", + "cols": 32, + "rows": 4, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "32 columns", + "width": 10, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 22, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "┌──────────────────────────────┐", + "width": 32, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "A", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "B", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "C", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└──────────────────────────────┘", + "width": 32, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "layout-wrap-narrow": { + "label": "After resizing the same terminal to 22 columns, A and B stay on the first row while C wraps to a second row and the container grows taller", + "cols": 22, + "rows": 6, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "22 columns", + "width": 10, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "┌────────────────────┐", + "width": 22, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "A", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "B", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 20, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "C", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 11, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└────────────────────┘", + "width": 22, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "renderable-created": { + "label": "An 18-column shaded panel containing a status that says Waiting", + "cols": 18, + "rows": 3, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#767676", + "slot": 243 + }, + { + "intent": "rgb", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "┌────────────────┐", + "width": 18, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "Waiting", + "width": 7, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└────────────────┘", + "width": 18, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "renderable-mutated": { + "label": "After changing content and width, the same shaded panel is 30 columns wide and its retained status says Ready", + "cols": 30, + "rows": 3, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#767676", + "slot": 243 + }, + { + "intent": "rgb", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "┌────────────────────────────┐", + "width": 30, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "Ready", + "width": 5, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 22, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└────────────────────────────┘", + "width": 30, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "renderable-reparent-before": { + "label": "Before reparenting, the shaded Message subtree is a child of first; second is empty", + "cols": 34, + "rows": 3, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#767676", + "slot": 243 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "┌─first────────┐", + "width": 16, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "┌─second───────┐", + "width": 16, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "Message", + "width": 7, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 3, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 14, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└──────────────┘", + "width": 16, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "└──────────────┘", + "width": 16, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "renderable-reparent-after": { + "label": "After second.add(message), the same shaded Message subtree is a child of second; first is empty", + "cols": 34, + "rows": 3, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#767676", + "slot": 243 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "┌─first────────┐", + "width": 16, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "┌─second───────┐", + "width": 16, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 14, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "Message", + "width": 7, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 3, + "foreground": 2, + "background": 3 + }, + { + "text": " ", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└──────────────┘", + "width": 16, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "└──────────────┘", + "width": 16, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "renderable-visibility": { + "label": "Visible: the panel contains Detail above Next. Hidden: Next moves up, but Detail stays attached and the panel still has two children. Detached: Next moves up, Detail has no parent, and the panel has one child. Neither operation destroys Detail.", + "cols": 38, + "rows": 7, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#767676", + "slot": 243 + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "visible", + "width": 7, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + }, + { + "text": "hidden", + "width": 6, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 1 + }, + { + "text": "detached", + "width": 8, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "┌──────────┐", + "width": 12, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "┌──────────┐", + "width": 12, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "┌──────────┐", + "width": 12, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "Detail", + "width": 6, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 3 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│Next", + "width": 5, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│Next", + "width": 5, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│Next", + "width": 5, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 10, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 10, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└──────────┘", + "width": 12, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "└──────────┘", + "width": 12, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "└──────────┘", + "width": 12, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "children: 2", + "width": 11, + "foreground": 4, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "children: 2", + "width": 11, + "foreground": 4, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "children: 1", + "width": 11, + "foreground": 4, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "parent: yes", + "width": 11, + "foreground": 4, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "parent: yes", + "width": 11, + "foreground": 4, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "parent: no", + "width": 10, + "foreground": 4, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "text-styled-chunks": { + "label": "Styled chunks: Status is bold, Note is italic, and Next combines bold and underline", + "cols": 22, + "rows": 3, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "Status", + "width": 6, + "foreground": 0, + "background": 1, + "attributes": 1 + }, + { + "text": ": ready", + "width": 7, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 9, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "Note", + "width": 4, + "foreground": 0, + "background": 1, + "attributes": 4 + }, + { + "text": ": saved locally", + "width": 15, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "Next", + "width": 4, + "foreground": 0, + "background": 1, + "attributes": 9 + }, + { + "text": ": review changes", + "width": 16, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "text-cell-ruler": { + "label": "Zero-based display columns: ASCII ABC occupies three cells; A界B occupies four, with 界 shaded across columns 1 and 2. Each marker follows the text immediately.", + "cols": 22, + "rows": 3, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#262626", + "slot": 235 + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "column 01234", + "width": 13, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 9, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "ASCII", + "width": 5, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": "ABC", + "width": 3, + "foreground": 3, + "background": 4 + }, + { + "text": "| 3 cells", + "width": 10, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "wide", + "width": 4, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 1 + }, + { + "text": "A", + "width": 1, + "foreground": 3, + "background": 4 + }, + { + "text": "界", + "width": 2, + "foreground": 3, + "background": 5 + }, + { + "text": "B", + "width": 1, + "foreground": 3, + "background": 4 + }, + { + "text": "| 4 cells", + "width": 10, + "foreground": 3, + "background": 1 + } + ] + ] + }, + "text-wide-wrap": { + "label": "Character wrapping of A界B at widths 2, 3, and 4: the two shaded cells occupied by 界 move intact to the next row when only one cell remains", + "cols": 34, + "rows": 5, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "indexed", + "value": "#262626", + "slot": 235 + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "2 columns", + "width": 9, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": "3 columns", + "width": 9, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": "4 columns", + "width": 9, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "01", + "width": 2, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 10, + "foreground": 2, + "background": 1 + }, + { + "text": "012", + "width": 3, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 9, + "foreground": 2, + "background": 1 + }, + { + "text": "0123", + "width": 4, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "A", + "width": 1, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 4 + }, + { + "text": " ", + "width": 10, + "foreground": 2, + "background": 1 + }, + { + "text": "A", + "width": 1, + "foreground": 0, + "background": 4 + }, + { + "text": "界", + "width": 2, + "foreground": 0, + "background": 5 + }, + { + "text": " ", + "width": 9, + "foreground": 2, + "background": 1 + }, + { + "text": "A", + "width": 1, + "foreground": 0, + "background": 4 + }, + { + "text": "界", + "width": 2, + "foreground": 0, + "background": 5 + }, + { + "text": "B", + "width": 1, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "界", + "width": 2, + "foreground": 0, + "background": 5 + }, + { + "text": " ", + "width": 10, + "foreground": 2, + "background": 1 + }, + { + "text": "B", + "width": 1, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 4 + }, + { + "text": " ", + "width": 9, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 4 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "B", + "width": 1, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 4 + }, + { + "text": " ", + "width": 10, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 2, + "background": 4 + }, + { + "text": " ", + "width": 9, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 4 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "text-line-offsets": { + "label": "Both three-column views select B: soft-wrapped A界B uses display offsets [3, 4), while A界 followed by a newline and B uses [4, 5)", + "cols": 35, + "rows": 5, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "indexed", + "value": "#262626", + "slot": 235 + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "soft wrap", + "width": 9, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 10, + "foreground": 2, + "background": 1 + }, + { + "text": "newline", + "width": 7, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 9, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "012", + "width": 3, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 16, + "foreground": 2, + "background": 1 + }, + { + "text": "012", + "width": 3, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 13, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "A界", + "width": 3, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 16, + "foreground": 2, + "background": 1 + }, + { + "text": "A界", + "width": 3, + "foreground": 0, + "background": 4 + }, + { + "text": " ", + "width": 13, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "B", + "width": 1, + "foreground": 0, + "background": 5 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 4 + }, + { + "text": " ", + "width": 16, + "foreground": 2, + "background": 1 + }, + { + "text": "B", + "width": 1, + "foreground": 0, + "background": 5 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 4 + }, + { + "text": " ", + "width": 13, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "range [3, 4)", + "width": 12, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 1 + }, + { + "text": "range [4, 5)", + "width": 12, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "interaction-hit-bubbling": { + "label": "The overlapping front box receives the hit, bubbles to its parent, then stops a second down event", + "cols": 38, + "rows": 10, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "┌─parent─────────────────────────────┐", + "width": 38, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 36, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "┌─back z=0─────────────┐", + "width": 24, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 11, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": "┌─front z=1─────────────┐", + "width": 25, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "└────────", + "width": 9, + "foreground": 3, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 23, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 10, + "foreground": 2, + "background": 1 + }, + { + "text": "└───────────────────────┘", + "width": 25, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└────────────────────────────────────┘", + "width": 38, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "target: front", + "width": 13, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 25, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "bubble: front -> parent", + "width": 23, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 15, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "stopped: front", + "width": 14, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 24, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "interaction-drag-selection": { + "label": "A drag selects the app on the first line and Test on the second, with text-buffer offsets [6, 18)", + "cols": 32, + "rows": 5, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "Build ", + "width": 6, + "foreground": 0, + "background": 1 + }, + { + "text": "the app", + "width": 7, + "foreground": 0, + "background": 2 + }, + { + "text": " ", + "width": 19, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "Test", + "width": 4, + "foreground": 0, + "background": 2 + }, + { + "text": " the input", + "width": 10, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 18, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 32, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "Selected: \"the app\\nTest\"", + "width": 25, + "foreground": 4, + "background": 1 + }, + { + "text": " ", + "width": 7, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "Range: [6, 18)", + "width": 14, + "foreground": 4, + "background": 1 + }, + { + "text": " ", + "width": 18, + "foreground": 3, + "background": 1 + } + ] + ] + }, + "input-placeholder": { + "label": "An unfocused name input displaying the placeholder Enter your name", + "cols": 28, + "rows": 2, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "Name", + "width": 4, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 24, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "Enter your name", + "width": 15, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 13, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "input-focused": { + "label": "A focused name input containing Ada Lovelace with its cursor visible", + "cols": 28, + "rows": 2, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + } + ], + "cursor": { + "column": 12, + "row": 1, + "style": "block" + }, + "lines": [ + [ + { + "text": "Name", + "width": 4, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 24, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "Ada Lovelace", + "width": 12, + "foreground": 3, + "background": 4 + }, + { + "text": " ", + "width": 16, + "foreground": 2, + "background": 4 + } + ] + ] + }, + "textarea-wrap": { + "label": "A multiline textarea wrapping a long line at a word boundary", + "cols": 30, + "rows": 4, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "Notes", + "width": 5, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 25, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "Long lines wrap at word ", + "width": 24, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "boundaries.", + "width": 11, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 19, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "Keep paragraphs readable.", + "width": 25, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 5, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "textarea-selection": { + "label": "A focused multiline textarea with keyboard focus selected", + "cols": 30, + "rows": 4, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + } + ], + "cursor": { + "column": 21, + "row": 2, + "style": "block" + }, + "lines": [ + [ + { + "text": "Draft", + "width": 5, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 25, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "Plan the release", + "width": 16, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 14, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "Review ", + "width": 7, + "foreground": 3, + "background": 1 + }, + { + "text": "keyboard focus", + "width": 14, + "foreground": 3, + "background": 4 + }, + { + "text": " ", + "width": 9, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "Ship the update", + "width": 15, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 15, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "select-options": { + "label": "New file, Open file, and Save options with Open file selected", + "cols": 32, + "rows": 6, + "colors": [ + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " New file", + "width": 10, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 21, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "Create a document", + "width": 17, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 12, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 4 + }, + { + "text": "▶ Open file", + "width": 11, + "foreground": 2, + "background": 4 + }, + { + "text": " ", + "width": 20, + "foreground": 0, + "background": 4 + } + ], + [ + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 4 + }, + { + "text": "Browse existing files", + "width": 21, + "foreground": 2, + "background": 4 + }, + { + "text": " ", + "width": 8, + "foreground": 0, + "background": 4 + } + ], + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " Save", + "width": 6, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 25, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "Write current changes", + "width": 21, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 8, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "tab-select-tabs": { + "label": "Home, Files, and Settings tabs with Files selected and underlined", + "cols": 36, + "rows": 3, + "colors": [ + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + }, + { + "intent": "default", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "Home", + "width": 4, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 7, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 3 + }, + { + "text": "Files", + "width": 5, + "foreground": 4, + "background": 3 + }, + { + "text": " ", + "width": 6, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "Settings", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 12, + "foreground": 0, + "background": 1 + }, + { + "text": "▬▬▬▬▬▬▬▬▬▬▬▬", + "width": 12, + "foreground": 4, + "background": 3 + }, + { + "text": " ", + "width": 12, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "Browse project files", + "width": 20, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 15, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "interaction-selection-focus": { + "label": "The word text is selected above a focused command input containing deploy --check", + "cols": 32, + "rows": 4, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "indexed", + "value": "#444444", + "slot": 238 + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + } + ], + "cursor": { + "column": 14, + "row": 3, + "style": "block" + }, + "lines": [ + [ + { + "text": "Select ", + "width": 7, + "foreground": 0, + "background": 1 + }, + { + "text": "text", + "width": 4, + "foreground": 0, + "background": 2 + }, + { + "text": ", then focus input", + "width": 18, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 32, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "Command", + "width": 7, + "foreground": 4, + "background": 1 + }, + { + "text": " ", + "width": 25, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "deploy --check", + "width": 14, + "foreground": 0, + "background": 2 + }, + { + "text": " ", + "width": 18, + "foreground": 3, + "background": 2 + } + ] + ] + }, + "slider-horizontal": { + "label": "Horizontal slider with its thumb positioned at 25 out of 100", + "cols": 30, + "rows": 3, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#262626", + "slot": 235 + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "value: 25", + "width": 9, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 21, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 3 + }, + { + "text": "██▌", + "width": 3, + "foreground": 0, + "background": 3 + }, + { + "text": " ", + "width": 20, + "foreground": 2, + "background": 3 + } + ], + [ + { + "text": "0", + "width": 1, + "foreground": 4, + "background": 1 + }, + { + "text": " ", + "width": 26, + "foreground": 2, + "background": 1 + }, + { + "text": "100", + "width": 3, + "foreground": 4, + "background": 1 + } + ] + ] + }, + "slider-vertical": { + "label": "Vertical slider with its thumb positioned at 0.5 out of 1", + "cols": 13, + "rows": 10, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#262626", + "slot": 235 + }, + { + "intent": "default", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "min 0", + "width": 5, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 11, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 11, + "foreground": 2, + "background": 1 + }, + { + "text": "▄▄", + "width": 2, + "foreground": 4, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 11, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 4, + "background": 3 + } + ], + [ + { + "text": "value 0.5", + "width": 9, + "foreground": 4, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 4, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 11, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 4, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 11, + "foreground": 2, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 4, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 11, + "foreground": 2, + "background": 1 + }, + { + "text": "▀▀", + "width": 2, + "foreground": 4, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 11, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 3 + } + ], + [ + { + "text": "max 1", + "width": 5, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 3 + } + ] + ] + }, + "scrollbar-arrows": { + "label": "Standalone vertical scrollbar with arrows at position 0, showing 20 of 200 rows", + "cols": 20, + "rows": 10, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#262626", + "slot": 235 + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "position: 0 / 180", + "width": 17, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "▲", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 19, + "foreground": 2, + "background": 1 + }, + { + "text": "▀", + "width": 1, + "foreground": 0, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 19, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 19, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 19, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 19, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 19, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 19, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 3 + } + ], + [ + { + "text": " ", + "width": 19, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 3 + } + ], + [ + { + "text": "viewport: 20 / 200", + "width": 18, + "foreground": 4, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "▼", + "width": 1, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "scrollbox-top": { + "label": "ScrollBox at offset 0 showing index.ts, app.ts, layout.ts, theme.ts, and events.ts", + "cols": 32, + "rows": 8, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#262626", + "slot": 235 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "offset: 0 / 7", + "width": 13, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 19, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "┌──────────────────────────────┐", + "width": 32, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "│01 src/index.ts", + "width": 17, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 13, + "foreground": 2, + "background": 1 + }, + { + "text": "▀", + "width": 1, + "foreground": 3, + "background": 4 + }, + { + "text": "│", + "width": 1, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "│02 src/app.ts", + "width": 15, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 15, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 4 + }, + { + "text": "│", + "width": 1, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "│03 src/layout.ts", + "width": 18, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 4 + }, + { + "text": "│", + "width": 1, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "│04 src/theme.ts", + "width": 17, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 13, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 4 + }, + { + "text": "│", + "width": 1, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "│05 src/events.ts", + "width": 18, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 4 + }, + { + "text": "│", + "width": 1, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "└──────────────────────────────┘", + "width": 32, + "foreground": 3, + "background": 1 + } + ] + ] + }, + "scrollbox-scrolled": { + "label": "ScrollBox at offset 5 showing input.ts, scroll.ts, render.ts, state.ts, and config.ts", + "cols": 32, + "rows": 8, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#262626", + "slot": 235 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "offset: 5 / 7", + "width": 13, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 19, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "┌──────────────────────────────┐", + "width": 32, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "│06 src/input.ts", + "width": 17, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 13, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 4 + }, + { + "text": "│", + "width": 1, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "│07 src/scroll.ts", + "width": 18, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 4 + }, + { + "text": "│", + "width": 1, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "│08 src/render.ts", + "width": 18, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 4 + }, + { + "text": "│", + "width": 1, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "│09 src/state.ts", + "width": 17, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 13, + "foreground": 2, + "background": 1 + }, + { + "text": "▀", + "width": 1, + "foreground": 3, + "background": 4 + }, + { + "text": "│", + "width": 1, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "│10 src/config.ts", + "width": 18, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 4 + }, + { + "text": "│", + "width": 1, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "└──────────────────────────────┘", + "width": 32, + "foreground": 3, + "background": 1 + } + ] + ] + }, + "plugin-slot-modes": { + "label": "Plugin slots: append shows host, clock, and sync; replace shows clock and sync; single winner shows clock", + "cols": 32, + "rows": 3, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "append", + "width": 6, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 9, + "foreground": 2, + "background": 1 + }, + { + "text": "host", + "width": 4, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "clock", + "width": 5, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "sync", + "width": 4, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "replace", + "width": 7, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": "clock", + "width": 5, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "sync", + "width": 4, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "single_winner", + "width": 13, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "clock", + "width": 5, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 12, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "keymap-active-keys": { + "label": "Active key bindings: Ctrl+S saves the file; Q quits the application", + "cols": 18, + "rows": 2, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "ctrl+s", + "width": 6, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "file.save", + "width": 9, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "q", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 7, + "foreground": 2, + "background": 1 + }, + { + "text": "app.quit", + "width": 8, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "text-table-styled": { + "label": "Rounded table with Service, Status, and Notes columns: api OK, worker DEGRADED", + "cols": 35, + "rows": 7, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "rgb", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "╭───────┬────────┬────────────────╮", + "width": 35, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "Service", + "width": 7, + "foreground": 2, + "background": 1, + "attributes": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "Status", + "width": 6, + "foreground": 2, + "background": 1, + "attributes": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 3, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "Notes", + "width": 5, + "foreground": 2, + "background": 1, + "attributes": 1 + }, + { + "text": " ", + "width": 11, + "foreground": 3, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "├───────┼────────┼────────────────┤", + "width": 35, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "api", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 3, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "OK", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 3, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "latency 28ms", + "width": 12, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 3, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "├───────┼────────┼────────────────┤", + "width": 35, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "worker", + "width": 6, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 3, + "background": 1 + }, + { + "text": "│DEGRADED│", + "width": 10, + "foreground": 0, + "background": 1 + }, + { + "text": "queue depth: 124", + "width": 16, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "╰───────┴────────┴────────────────╯", + "width": 35, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "line-number-editor": { + "label": "Three lines of numbered code with a sign beside serve(port)", + "cols": 24, + "rows": 3, + "colors": [ + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "1", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "const port = 3000", + "width": 17, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": ">", + "width": 1, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "2", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "serve(port)", + "width": 11, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 9, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "3", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "await ready()", + "width": 13, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 7, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "code-highlighted": { + "label": "JavaScript function hello with bold keywords, a muted string, and an italic comment", + "cols": 33, + "rows": 5, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "indexed", + "value": "#9e9e9e", + "slot": 247 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "function", + "width": 8, + "foreground": 0, + "background": 1, + "attributes": 1 + }, + { + "text": " hello() {", + "width": 10, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 15, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "// This is a comment", + "width": 20, + "foreground": 3, + "background": 1, + "attributes": 4 + }, + { + "text": " ", + "width": 11, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "const", + "width": 5, + "foreground": 0, + "background": 1, + "attributes": 1 + }, + { + "text": " message = ", + "width": 11, + "foreground": 0, + "background": 1 + }, + { + "text": "\"Hello, world!\"", + "width": 15, + "foreground": 4, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "return", + "width": 6, + "foreground": 0, + "background": 1, + "attributes": 1 + }, + { + "text": " message", + "width": 8, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 17, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "}", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 32, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "markdown-table-grid": { + "label": "Bordered Markdown table with Name and Status columns: api ready, worker paused", + "cols": 19, + "rows": 7, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "┌────────┬────────┐", + "width": 19, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "Name", + "width": 4, + "foreground": 3, + "background": 1, + "attributes": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "Status", + "width": 6, + "foreground": 3, + "background": 1, + "attributes": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "├────────┼────────┤", + "width": 19, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "api", + "width": 3, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "ready", + "width": 5, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "├────────┼────────┤", + "width": 19, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "worker", + "width": 6, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "paused", + "width": 6, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "│", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "└────────┴────────┘", + "width": 19, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "markdown-table-columns": { + "label": "Borderless Markdown table with Name and Status columns: api ready, worker paused", + "cols": 14, + "rows": 3, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "Name", + "width": 4, + "foreground": 0, + "background": 1, + "attributes": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 1 + }, + { + "text": "Status", + "width": 6, + "foreground": 0, + "background": 1, + "attributes": 1 + } + ], + [ + { + "text": "api", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 5, + "foreground": 2, + "background": 1 + }, + { + "text": "ready", + "width": 5, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "worker", + "width": 6, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "paused", + "width": 6, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "diff-unified": { + "label": "Unified diff replacing const a = 1 with const a = 2", + "cols": 16, + "rows": 4, + "colors": [ + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "1", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "setup()", + "width": 7, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "2 -", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "const a = 1", + "width": 11, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "2", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": " +", + "width": 2, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "const a = 2", + "width": 11, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "3", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "ready(a)", + "width": 8, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "diff-split": { + "label": "Split diff replacing const a = 1 with const a = 2", + "cols": 34, + "rows": 3, + "colors": [ + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "1", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "setup()", + "width": 7, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 0, + "background": 1 + }, + { + "text": "1", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "setup()", + "width": 7, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 5, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "2 -", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "const a = 1", + "width": 11, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "2", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": " +", + "width": 2, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "const a = 2", + "width": 11, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "3", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "ready(a)", + "width": 8, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 5, + "foreground": 0, + "background": 1 + }, + { + "text": "3", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "ready(a)", + "width": 8, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "ascii-font-tiny": { + "label": "OPEN drawn with OpenTUI's compact tiny ASCII font", + "cols": 16, + "rows": 2, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "█▀█", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "█▀█", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "█▀▀", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "█▄", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "█", + "width": 1, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "█▄█", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "█▀▀", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "██▄", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "█", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 2, + "background": 1 + }, + { + "text": "▀█", + "width": 2, + "foreground": 0, + "background": 1 + } + ] + ] + }, + "ascii-font-block": { + "label": "OPEN drawn with OpenTUI's six-row block ASCII font", + "cols": 38, + "rows": 6, + "colors": [ + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "default", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "██████╗", + "width": 7, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "██████╗", + "width": 7, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "███████╗", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "███╗", + "width": 4, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "██╗", + "width": 3, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "██╔═══██╗", + "width": 9, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "██╔══██╗", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "██╔════╝", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "████╗", + "width": 5, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "██║", + "width": 3, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "██║", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "██║", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "██████╔╝", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "█████╗", + "width": 6, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "██╔██╗", + "width": 6, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "██║", + "width": 3, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "██║", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "██║", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "██╔═══╝", + "width": 7, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "██╔══╝", + "width": 6, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 1 + }, + { + "text": "██║╚██╗██║", + "width": 10, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "╚██████╔╝", + "width": 9, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "██║", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 0, + "background": 1 + }, + { + "text": "███████╗", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "██║", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "╚████║", + "width": 6, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "╚═════╝", + "width": 7, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "╚═╝", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 0, + "background": 1 + }, + { + "text": "╚══════╝", + "width": 8, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "╚═╝", + "width": 3, + "foreground": 2, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "╚═══╝", + "width": 5, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "frame-buffer-draw": { + "label": "Network throughput chart showing receive at 42 MB/s and transmit at 18 MB/s", + "cols": 21, + "rows": 4, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "network throughput", + "width": 18, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 3, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "rx", + "width": 2, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "▂▄▆█▇▅▃▂", + "width": 8, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "42 MB/s", + "width": 7, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": "tx", + "width": 2, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "▃▅▇█▆▄▂▁", + "width": 8, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "18 MB/s", + "width": 7, + "foreground": 3, + "background": 1 + } + ], + [ + { + "text": " ", + "width": 4, + "foreground": 2, + "background": 1 + }, + { + "text": "8 seconds", + "width": 9, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 5, + "foreground": 2, + "background": 1 + }, + { + "text": "now", + "width": 3, + "foreground": 3, + "background": 1 + } + ] + ] + }, + "frame-buffer-progress": { + "label": "Package download progress at 70%, with 14 of 20 files complete", + "cols": 25, + "rows": 3, + "colors": [ + { + "intent": "default", + "value": "#ffffff" + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "Downloading package", + "width": 19, + "foreground": 0, + "background": 1 + }, + { + "text": " ", + "width": 6, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "██████████████", + "width": 14, + "foreground": 0, + "background": 1 + }, + { + "text": "░░░░░░", + "width": 6, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 2, + "foreground": 2, + "background": 1 + }, + { + "text": "70%", + "width": 3, + "foreground": 0, + "background": 1 + } + ], + [ + { + "text": "14 of 20 files", + "width": 14, + "foreground": 3, + "background": 1 + }, + { + "text": " ", + "width": 11, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "embedded-terminal-vt": { + "label": "Embedded terminal test run with two passed and zero failed", + "cols": 27, + "rows": 4, + "colors": [ + { + "intent": "indexed", + "value": "#808080", + "slot": 244 + }, + { + "intent": "default", + "value": "#000000" + }, + { + "intent": "default", + "value": "#ffffff" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "$ ", + "width": 2, + "foreground": 0, + "background": 1 + }, + { + "text": "bun test ", + "width": 25, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "✓", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " parser accepts UTF-8 ", + "width": 26, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "✓", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": " renderer draws wide cells", + "width": 26, + "foreground": 2, + "background": 1 + } + ], + [ + { + "text": "2 passed", + "width": 8, + "foreground": 2, + "background": 1, + "attributes": 1 + }, + { + "text": ", 0 failed ", + "width": 19, + "foreground": 2, + "background": 1 + } + ] + ] + }, + "image-blocks": { + "label": "Generated RGBA landscape displayed with the Unicode block image protocol", + "cols": 24, + "rows": 8, + "colors": [ + { + "intent": "rgb", + "value": "#0f172a" + }, + { + "intent": "rgb", + "value": "#facc15" + }, + { + "intent": "rgb", + "value": "#60a5fa" + }, + { + "intent": "rgb", + "value": "#1e40af" + }, + { + "intent": "rgb", + "value": "#22c55e" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": "████████████████████████", + "width": 24, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": "██████████████████", + "width": 18, + "foreground": 0, + "background": 0 + }, + { + "text": "▘", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 1, + "background": 1 + }, + { + "text": "▜", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": "█████", + "width": 5, + "foreground": 0, + "background": 0 + }, + { + "text": "▛▀▝▀", + "width": 4, + "foreground": 0, + "background": 2 + }, + { + "text": "█████████", + "width": 9, + "foreground": 0, + "background": 0 + }, + { + "text": "▖", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 1, + "background": 1 + }, + { + "text": "▟", + "width": 1, + "foreground": 0, + "background": 1 + }, + { + "text": "██", + "width": 2, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": "██", + "width": 2, + "foreground": 0, + "background": 0 + }, + { + "text": "▛▀", + "width": 2, + "foreground": 0, + "background": 2 + }, + { + "text": "██████", + "width": 6, + "foreground": 2, + "background": 2 + }, + { + "text": "▝▀", + "width": 2, + "foreground": 0, + "background": 2 + }, + { + "text": "█", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▛▀▘", + "width": 3, + "foreground": 0, + "background": 2 + }, + { + "text": "██", + "width": 2, + "foreground": 2, + "background": 2 + }, + { + "text": "▀▀", + "width": 2, + "foreground": 0, + "background": 2 + }, + { + "text": "████", + "width": 4, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": "▀", + "width": 1, + "foreground": 0, + "background": 2 + }, + { + "text": "███████████", + "width": 11, + "foreground": 2, + "background": 2 + }, + { + "text": "▗▄▙▄", + "width": 4, + "foreground": 3, + "background": 2 + }, + { + "text": "██████", + "width": 6, + "foreground": 2, + "background": 2 + }, + { + "text": "▀▀", + "width": 2, + "foreground": 0, + "background": 2 + } + ], + [ + { + "text": "████████", + "width": 8, + "foreground": 2, + "background": 2 + }, + { + "text": "▗▄▟", + "width": 3, + "foreground": 3, + "background": 2 + }, + { + "text": "███████", + "width": 7, + "foreground": 3, + "background": 3 + }, + { + "text": "▄▄", + "width": 2, + "foreground": 3, + "background": 2 + }, + { + "text": "████", + "width": 4, + "foreground": 2, + "background": 2 + } + ], + [ + { + "text": "▄▄▄▄▄▄▄", + "width": 7, + "foreground": 4, + "background": 2 + }, + { + "text": "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀", + "width": 15, + "foreground": 3, + "background": 4 + }, + { + "text": "▄▄", + "width": 2, + "foreground": 4, + "background": 2 + } + ], + [ + { + "text": "████████████████████████", + "width": 24, + "foreground": 4, + "background": 4 + } + ] + ] + }, + "qr-code-version-one": { + "label": "Scannable version-one QR code encoding OPENTUI with a four-module white quiet zone", + "cols": 29, + "rows": 15, + "colors": [ + { + "intent": "rgb", + "value": "#ffffff" + }, + { + "intent": "rgb", + "value": "#000000" + } + ], + "cursor": null, + "lines": [ + [ + { + "text": " ", + "width": 29, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 29, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + }, + { + "text": "█▀▀▀▀▀█", + "width": 7, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀█▄▀█", + "width": 5, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "█▀▀▀▀▀█", + "width": 7, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "███", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀█", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "███", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀▀", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀█", + "width": 2, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀▀", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀▀▀▀▀▀", + "width": 7, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▄█", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀▀▀▀▀▀", + "width": 7, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + }, + { + "text": "▄▄", + "width": 2, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▄", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀▄", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▄▀▄▀▄█▄█▄█", + "width": 10, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + }, + { + "text": "▄█▀▀█▄▀", + "width": 7, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀▀▄▄█", + "width": 6, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀█▄", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 5, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 5, + "foreground": 0, + "background": 0 + }, + { + "text": "▀", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 0 + }, + { + "text": "▀", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀█", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 0 + }, + { + "text": "██▄", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀", + "width": 2, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + }, + { + "text": "█▀▀▀▀▀█", + "width": 7, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▄▄▀", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀▄", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀█▄█▀", + "width": 5, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "███", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 0 + }, + { + "text": "▀███▀█", + "width": 6, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▄▀", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 6, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀▀", + "width": 3, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 0 + }, + { + "text": "█", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀▀▀▀█▄▀▀█", + "width": 10, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 4, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀▀▀▀▀▀", + "width": 7, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 2, + "foreground": 0, + "background": 0 + }, + { + "text": "▀", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 3, + "foreground": 0, + "background": 0 + }, + { + "text": "▀", + "width": 1, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 1, + "foreground": 0, + "background": 0 + }, + { + "text": "▀▀", + "width": 2, + "foreground": 1, + "background": 0 + }, + { + "text": " ", + "width": 6, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 29, + "foreground": 0, + "background": 0 + } + ], + [ + { + "text": " ", + "width": 29, + "foreground": 0, + "background": 0 + } + ] + ] + } +} diff --git a/packages/web/src/scripts/terminal-illustration.js b/packages/web/src/scripts/terminal-illustration.js index 17a2253c..84b67031 100644 --- a/packages/web/src/scripts/terminal-illustration.js +++ b/packages/web/src/scripts/terminal-illustration.js @@ -541,10 +541,6 @@ updateToggle() function advanceStory() { - if (stories.length < 2) { - setupStory(storyIndex) - return - } setupStory((storyIndex + 1) % stories.length) paint() renderCaption() @@ -642,8 +638,6 @@ } }) - if (reduceMotion) return - if ("IntersectionObserver" in window) { new IntersectionObserver( function (entries) { @@ -651,7 +645,7 @@ return entry.isIntersecting }) if (isIntersecting) { - if (!hasStarted || resumeAfterIntersection) { + if ((!hasStarted && !reduceMotion) || resumeAfterIntersection) { resumeAfterIntersection = false play() } @@ -664,7 +658,7 @@ ).observe(illustration) } else { isIntersecting = true - play() + if (!reduceMotion) play() } } diff --git a/packages/web/src/styles/prose.css b/packages/web/src/styles/prose.css index 6a380b40..916b3f08 100644 --- a/packages/web/src/styles/prose.css +++ b/packages/web/src/styles/prose.css @@ -156,6 +156,7 @@ min-width: min(100%, var(--content-width)); max-width: 100%; margin-block: 0 1.5rem; + margin-inline: 0; overflow-x: auto; } @@ -171,6 +172,57 @@ font-variant-ligatures: none; } +.prose .terminal-frame { + /* Keep ambiguous-width symbols in the same single cells as the terminal. */ + font-feature-settings: "NWID"; + --terminal-color-235: color-mix(in srgb, var(--page-color) 7%, var(--page-background)); + --terminal-color-238: color-mix(in srgb, var(--page-color) 16%, var(--page-background)); + --terminal-color-243: var(--terminal-color-238); + --terminal-color-244: color-mix(in srgb, var(--page-color) 65%, var(--page-background)); + --terminal-color-247: color-mix(in srgb, var(--page-color) 82%, var(--page-background)); +} + +:root[data-theme="blue"] .prose .terminal-frame { + --terminal-color-244: color-mix(in srgb, var(--page-color) 80%, var(--page-background)); + --terminal-color-247: color-mix(in srgb, var(--page-color) 90%, var(--page-background)); +} + +.prose .box-figure pre.terminal-frame--surface { + --terminal-background: var(--terminal-color-235); + padding: 1.25em 2ch; + background: var(--terminal-background); +} + +.prose .terminal-frame__row { + position: relative; + display: block; + height: 1.25em; + line-height: 1.25em; +} + +.prose .terminal-frame__span { + display: inline-block; + height: 100%; + line-height: inherit; + vertical-align: top; +} + +.prose .terminal-frame__cursor { + position: absolute; + inset-block: 0; + width: 1ch; + background: var(--page-color); +} + +.prose .terminal-frame__cursor--line { + width: 1px; +} + +.prose .terminal-frame__cursor--underline { + inset-block-start: auto; + height: 2px; +} + .prose .code-block pre { width: 100%; min-width: 0;