From de80e640e4ab56ebecf83b5693b46c5fe04fc0b1 Mon Sep 17 00:00:00 2001 From: Pavel Verigo Date: Tue, 4 Aug 2026 00:24:23 +0200 Subject: [PATCH 1/4] split vertically --- index.html | 21 ++++++++++++--------- src/editor.ts | 31 ++++++++++++++++--------------- style/style.css | 48 ++++++++++++++++++++++++------------------------ 3 files changed, 52 insertions(+), 48 deletions(-) diff --git a/index.html b/index.html index 232ac6e..22dd81e 100644 --- a/index.html +++ b/index.html @@ -9,22 +9,25 @@ - +
+
+

playground

+ | + A zigtools initiative. + | + Warning Zig's self-hosted WebAssembly backend is still experimental! +
+ +
-
+
- - - \ No newline at end of file + diff --git a/src/editor.ts b/src/editor.ts index dff936d..66a0635 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -40,9 +40,9 @@ const editor = new EditorView({ function revealOutputWindow() { const outputs = document.getElementById("output")!; outputs.scrollTo(0, outputs.scrollHeight!); - const editorHeightPercent = parseFloat(splitPane.style.getPropertyValue("--editor-height-percent")); - if (editorHeightPercent == 100) { - splitPane.style.setProperty("--editor-height-percent", `${resizeBarPreviousSize}%`); + const editorWidthPercent = parseFloat(splitPane.style.getPropertyValue("--editor-width-percent")); + if (editorWidthPercent == 100) { + splitPane.style.setProperty("--editor-width-percent", `${resizeBarPreviousSize}%`); } } @@ -84,11 +84,12 @@ zigWorker.onmessage = (ev: MessageEvent) => { const splitPane = document.getElementById("split-pane")! as HTMLDivElement; const resizeBar = document.getElementById("resize-bar")! as HTMLDivElement; -let resizeBarPreviousSize = 70; +let resizeBarPreviousSize = 60; function onResizeBarMove(event: MouseEvent) { - const percent = Math.min(Math.max(10, event.clientY / splitPane.getBoundingClientRect().height * 100), 100); - splitPane.style.setProperty("--editor-height-percent", `${percent}%`); + const bounds = splitPane.getBoundingClientRect(); + const percent = Math.min(Math.max(10, (event.clientX - bounds.left) / bounds.width * 100), 100); + splitPane.style.setProperty("--editor-width-percent", `${percent}%`); } function onResizeBarMouseUp(event: MouseEvent) { window.removeEventListener("mousemove", onResizeBarMove); @@ -98,9 +99,9 @@ function onResizeBarMouseUp(event: MouseEvent) { document.body.style.removeProperty("cursor"); // fully close the output window when it's almost closed - const editorHeightPercent = parseFloat(splitPane.style.getPropertyValue("--editor-height-percent")); - if (editorHeightPercent >= 90) { - splitPane.style.setProperty("--editor-height-percent", "100%"); + const editorWidthPercent = parseFloat(splitPane.style.getPropertyValue("--editor-width-percent")); + if (editorWidthPercent >= 90) { + splitPane.style.setProperty("--editor-width-percent", "100%"); } } @@ -109,16 +110,16 @@ resizeBar.addEventListener("mousedown", event => { window.addEventListener("mousemove", onResizeBarMove); window.addEventListener("mouseup", onResizeBarMouseUp); document.body.style.userSelect = "none"; - document.body.style.cursor = "row-resize"; + document.body.style.cursor = "col-resize"; } }); resizeBar.addEventListener("dblclick", event => { - const editorHeightPercent = parseFloat(splitPane.style.getPropertyValue("--editor-height-percent")); - if (editorHeightPercent == 100) { - splitPane.style.setProperty("--editor-height-percent", `${resizeBarPreviousSize}%`); + const editorWidthPercent = parseFloat(splitPane.style.getPropertyValue("--editor-width-percent")); + if (editorWidthPercent == 100) { + splitPane.style.setProperty("--editor-width-percent", `${resizeBarPreviousSize}%`); } else { - resizeBarPreviousSize = editorHeightPercent; - splitPane.style.setProperty("--editor-height-percent", `100%`); + resizeBarPreviousSize = editorWidthPercent; + splitPane.style.setProperty("--editor-width-percent", `100%`); } }); diff --git a/style/style.css b/style/style.css index b3a1483..5a14027 100644 --- a/style/style.css +++ b/style/style.css @@ -68,25 +68,27 @@ body { } main { - height: 100%; - - flex-grow: 1; - - height: calc(100% - 80px); + flex: 1 1 auto; + min-height: 0; } -footer { +header { display: flex; - padding: 0.5rem; - text-align: center; - align-items: center; justify-content: space-between; + padding: 0.5rem 0.75rem; + align-items: center; font-size: 0.85em; line-height: 1.25; background-color: var(--footer-background); color: var(--footer-text); - &>h1 { + .header-info { + display: flex; + gap: 0.65rem; + align-items: center; + } + + .header-info>h1 { font-size: 1em; font-weight: 900; letter-spacing: -1px; @@ -125,28 +127,29 @@ a { #split-pane { display: flex; - - flex-direction: column; + flex-direction: row; height: 100%; overflow: hidden; &>*:first-child { - height: var(--editor-height-percent); + flex: 0 0 var(--editor-width-percent); + min-width: 0; } &>#resize-bar { - height: 0.5rem; + flex: 0 0 0.5rem; } &>*:last-child { - height: calc(100% - 0.5rem - var(--editor-height-percent)); + flex: 1 1 auto; + min-width: 0; } } #resize-bar { position: relative; - width: 100%; - cursor: row-resize; + height: 100%; + cursor: col-resize; background-color: var(--resizer-color); } @@ -155,7 +158,7 @@ a { position: absolute; width: 100%; height: 100%; - transform: scale(3); + transform: scaleX(3); } #resize-bar::after { @@ -163,7 +166,7 @@ a { position: absolute; top: 50%; left: 50%; - width: 5rem; + height: 5rem; border: 2px dashed var(--resizer-pivot-color); border-radius: 100px; transform: translate(-50%,-50%); @@ -221,10 +224,6 @@ ul { } #run { - position: fixed; - top: 1rem; - right: 1rem; - z-index: 1; border: none; border-radius: 3px; padding: 0.5rem 1.85rem; @@ -236,8 +235,9 @@ ul { } #output { + min-width: 0; height: 100%; - overflow-y: auto; + overflow: auto; background-color: var(--output-background); color: var(--output-text); From 9b249a26ad70eb664d09e4509039e60057484396 Mon Sep 17 00:00:00 2001 From: Pavel Verigo Date: Tue, 4 Aug 2026 02:58:18 +0200 Subject: [PATCH 2/4] add xterm --- index.html | 5 ++- package-lock.json | 17 ++++++++ package.json | 2 + src/editor.ts | 77 +++++++++++++++++++++++------------ src/workers/runner.ts | 9 ++--- src/workers/zig.ts | 4 -- style/style.css | 93 +++++++++++++++---------------------------- 7 files changed, 110 insertions(+), 97 deletions(-) diff --git a/index.html b/index.html index 22dd81e..fba2d2b 100644 --- a/index.html +++ b/index.html @@ -24,7 +24,10 @@

playground

-
+
+
 
+
+
diff --git a/package-lock.json b/package-lock.json index 5c3550e..796428a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "@codemirror/view": "^6.9.3", "@ndim/codemirror-lang-zig": "^0.2.0", "@ndim/lezer-zig": "^0.2.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "codemirror": "^6.0.1", "vscode-languageserver-protocol": "^3.17.3" }, @@ -861,6 +863,21 @@ "node": ">=16.20.0" } }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, "node_modules/codemirror": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", diff --git a/package.json b/package.json index 2c168f2..8f2fd9a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@codemirror/view": "^6.9.3", "@ndim/codemirror-lang-zig": "^0.2.0", "@ndim/lezer-zig": "^0.2.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "codemirror": "^6.0.1", "vscode-languageserver-protocol": "^3.17.3" }, diff --git a/src/editor.ts b/src/editor.ts index 66a0635..bc9647a 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -5,6 +5,9 @@ import { formatDocument } from "@codemirror/lsp-client"; import { indentWithTab } from "@codemirror/commands"; import { indentUnit, syntaxHighlighting } from "@codemirror/language"; import { zigLanguage } from "@ndim/codemirror-lang-zig"; +import { Terminal } from "@xterm/xterm"; +import { FitAddon } from "@xterm/addon-fit"; +import "@xterm/xterm/css/xterm.css"; import { editorTheme, highlightStyle } from "./theme.ts"; import { lspClient } from "./lsp.ts"; // @ts-ignore @@ -37,9 +40,29 @@ const editor = new EditorView({ }), }); +const output = document.getElementById("output")!; +const executionStatus = document.getElementById("execution-status")!; +const outputStyle = getComputedStyle(document.documentElement); +const terminal = new Terminal({ + disableStdin: true, + convertEol: true, + cursorBlink: false, + fontSize: 16, + fontFamily: "monospace", + scrollback: 1000, + theme: { + background: outputStyle.getPropertyValue("--output-background").trim(), + foreground: outputStyle.getPropertyValue("--output-text").trim(), + }, +}); +const fitAddon = new FitAddon(); +terminal.loadAddon(fitAddon); +terminal.open(output); +terminal.write("\x1b[?25l"); // hide cursor +fitAddon.fit(); +new ResizeObserver(() => fitAddon.fit()).observe(output); + function revealOutputWindow() { - const outputs = document.getElementById("output")!; - outputs.scrollTo(0, outputs.scrollHeight!); const editorWidthPercent = parseFloat(splitPane.style.getPropertyValue("--editor-width-percent")); if (editorWidthPercent == 100) { splitPane.style.setProperty("--editor-width-percent", `${resizeBarPreviousSize}%`); @@ -47,38 +70,43 @@ function revealOutputWindow() { } let zigWorker = new ZigWorker(); +let compilationStartedAt = 0; +let isCompiling = false; +let executionId = 0; zigWorker.onmessage = (ev: MessageEvent) => { if (ev.data.stderr) { - document.querySelector(".zig-output:last-child")!.textContent += ev.data.stderr; + terminal.write(ev.data.stderr); revealOutputWindow(); return; - } else if (ev.data.failed) { - const outputSplit = document.createElement("div"); - outputSplit.classList.add("output-split"); - document.getElementById("output")!.appendChild(outputSplit); } else if (ev.data.compiled) { - let runnerWorker = new RunnerWorker(); + const compilationTime = Math.round(performance.now() - compilationStartedAt); + const currentExecutionId = executionId; + const runStartedAt = performance.now(); + isCompiling = false; + executionStatus.textContent = `Compiled in ${compilationTime} ms | Running...`; - const zigOutput = document.createElement("div"); - zigOutput.classList.add("runner-output"); - zigOutput.classList.add("latest"); - document.getElementById("output")!.appendChild(zigOutput); + let runnerWorker = new RunnerWorker(); runnerWorker.postMessage({ run: ev.data.compiled }); runnerWorker.onmessage = (rev: MessageEvent) => { if (rev.data.stderr) { - document.querySelector(".runner-output:last-child")!.textContent += rev.data.stderr; + terminal.write(rev.data.stderr); revealOutputWindow(); return; } else if (rev.data.done) { + if (currentExecutionId == executionId) { + const runTime = Math.round(performance.now() - runStartedAt); + executionStatus.textContent = `Compiled in ${compilationTime} ms | Ran in ${runTime} ms | Exit code ${rev.data.exitCode}`; + } runnerWorker.terminate(); - const outputSplit = document.createElement("div"); - outputSplit.classList.add("output-split"); - document.getElementById("output")!.appendChild(outputSplit); } } + } else if (ev.data.failed) { + const compilationTime = Math.round(performance.now() - compilationStartedAt); + isCompiling = false; + executionStatus.textContent = `Compilation failed in ${compilationTime} ms`; } } @@ -125,17 +153,14 @@ resizeBar.addEventListener("dblclick", event => { const outputsRun = document.getElementById("run")! as HTMLButtonElement; outputsRun.addEventListener("click", async () => { - for (const zo of document.querySelectorAll(".zig-output")) { - zo.classList.remove("latest"); - } - for (const ro of document.querySelectorAll(".runner-output")) { - ro.classList.remove("latest"); - } + if (isCompiling) return; - const zigOutput = document.createElement("div"); - zigOutput.classList.add("zig-output"); - zigOutput.classList.add("latest"); - document.getElementById("output")!.appendChild(zigOutput); + isCompiling = true; + executionId += 1; + compilationStartedAt = performance.now(); + executionStatus.textContent = "Compiling..."; + terminal.reset(); + terminal.write("\x1b[?25l"); // hide cursor revealOutputWindow(); zigWorker.postMessage({ diff --git a/src/workers/runner.ts b/src/workers/runner.ts index 559283f..f380205 100644 --- a/src/workers/runner.ts +++ b/src/workers/runner.ts @@ -18,19 +18,18 @@ async function run(wasmData: BufferSource) { "wasi_snapshot_preview1": wasi.wasiImport, });; + let exitCode = 0; try { // @ts-ignore - const exitCode = wasi.start(instance); - - postMessage({ - stderr: `\n\n---\nexit with exit code ${exitCode}\n---\n`, - }); + exitCode = wasi.start(instance); } catch (err) { postMessage({ stderr: `${err}` }); + exitCode = 1; } postMessage({ done: true, + exitCode, }); } diff --git a/src/workers/zig.ts b/src/workers/zig.ts index 58327bf..795e4df 100644 --- a/src/workers/zig.ts +++ b/src/workers/zig.ts @@ -36,10 +36,6 @@ async function run(source: string) { "wasi_snapshot_preview1": wasi.wasiImport, }); - postMessage({ - stderr: "Compiling...\n", - }); - try { // @ts-ignore const exitCode = wasi.start(instance); diff --git a/style/style.css b/style/style.css index 5a14027..28433e3 100644 --- a/style/style.css +++ b/style/style.css @@ -3,20 +3,13 @@ :root { --button-background: hsl(40, 75%, 40%); - --output-background: hsl(0, 0%, 90%); - --footer-background: hsl(0, 0%, 95%); - --footer-text: black; - --output-text: black; + --output-background: black; + --output-text: white; + --header-background: hsl(0, 0%, 95%); + --header-text: black; --resizer-color: hsl(0, 0%, 75%); --resizer-pivot-color: hsl(0, 0%, 30%); - --output-zig-outdated-background: hsl(37, 20%, 25%); - --output-zig-latest-background: hsl(37, 40%, 80%); - --output-runner-outdated-background: hsl(200, 15%, 25%); - --output-runner-latest-background: hsl(200, 30%, 80%); - --output-outdated-text: hsl(0, 0%, 60%); - --output-latest-text: hsl(0, 0%, 0%); - --tooltip-background: hsl(0, 0%, 100%); --tooltip-text: hsl(0, 0%, 0%); @@ -27,20 +20,11 @@ @media (prefers-color-scheme: dark) { :root { --button-background: hsl(40, 75%, 25%); - --output-background: hsl(0, 0%, 8%); - --footer-background: hsl(0, 0%, 10%); - --footer-text: hsl(0, 0%, 80%); - --output-text: hsl(0, 0%, 90%); + --header-background: hsl(0, 0%, 10%); + --header-text: hsl(0, 0%, 80%); --resizer-color: hsl(0, 0%, 18%); --resizer-pivot-color: hsl(0, 0%, 70%); - --output-zig-outdated-background: hsl(37, 93%, 5%); - --output-zig-latest-background: hsl(37, 93%, 10%); - --output-runner-outdated-background: hsl(200, 93%, 5%); - --output-runner-latest-background: hsl(200, 93%, 10%); - --output-outdated-text: hsl(0, 0%, 60%); - --output-latest-text: hsl(0, 0%, 100%); - --tooltip-background: hsl(0, 0%, 0%); --tooltip-text: hsl(0, 0%, 100%); } @@ -79,8 +63,8 @@ header { align-items: center; font-size: 0.85em; line-height: 1.25; - background-color: var(--footer-background); - color: var(--footer-text); + background-color: var(--header-background); + color: var(--header-text); .header-info { display: flex; @@ -178,6 +162,7 @@ code { .cm-editor { height: 100%; + font-size: 16px; } .cm-focused { @@ -234,49 +219,35 @@ ul { color: var(--button-text); } -#output { +#output-pane { min-width: 0; height: 100%; - overflow: auto; + display: flex; + flex-direction: column; + overflow: hidden; background-color: var(--output-background); color: var(--output-text); +} - &>div { - white-space: pre; - font-family: monospace; - } - - &>.zig-output { - padding: 1rem; - - &.latest { - background-color: var(--output-zig-latest-background); - color: var(--output-latest-text); - } - - &:not(.latest) { - background-color: var(--output-zig-outdated-background); - color: var(--output-outdated-text); - } - } - - &>.runner-output { - padding: 1rem; - - &.latest { - background-color: var(--output-runner-latest-background); - color: var(--output-latest-text); - } +#execution-status { + flex: 0 0 auto; + padding: 0.5rem 0.75rem; + border-bottom: 1px solid var(--resizer-color); + font-family: monospace; + font-size: 12px; + line-height: 1; + background-color: var(--header-background); + color: var(--header-text); +} - &:not(.latest) { - background-color: var(--output-runner-outdated-background); - color: var(--output-outdated-text); - } - } +#output { + flex: 1 1 auto; + min-width: 0; + min-height: 0; + padding: 0.75rem; + overflow: hidden; - &>.output-split { - width: 100%; - height: 1px; - background-color: white; + &>.xterm { + height: 100%; } } From 4abac1ae4c6e52889a51d2b29ee2fdea07aafb3a Mon Sep 17 00:00:00 2001 From: Pavel Verigo Date: Tue, 4 Aug 2026 05:10:22 +0200 Subject: [PATCH 3/4] xterm image, add examples --- index.html | 12 ++- package-lock.json | 31 +++++--- package.json | 5 +- src/editor.ts | 37 +++++++++- src/examples/ansi-text.zig | 17 +++++ src/examples/fibonacci.zig | 19 +++++ src/examples/fizz-buzz.zig | 21 ++++++ src/{main.zig => examples/hello-world.zig} | 0 src/examples/kitty-image.zig | 85 ++++++++++++++++++++++ src/examples/mandelbrot.zig | 43 +++++++++++ style/style.css | 17 +++++ 11 files changed, 273 insertions(+), 14 deletions(-) create mode 100644 src/examples/ansi-text.zig create mode 100644 src/examples/fibonacci.zig create mode 100644 src/examples/fizz-buzz.zig rename src/{main.zig => examples/hello-world.zig} (100%) create mode 100644 src/examples/kitty-image.zig create mode 100644 src/examples/mandelbrot.zig diff --git a/index.html b/index.html index fba2d2b..4121829 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,17 @@

playground

| Warning Zig's self-hosted WebAssembly backend is still experimental! - +
+ + +
diff --git a/package-lock.json b/package-lock.json index 796428a..1520099 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,9 @@ "@codemirror/view": "^6.9.3", "@ndim/codemirror-lang-zig": "^0.2.0", "@ndim/lezer-zig": "^0.2.0", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", + "@xterm/addon-fit": "^0.12.0-beta.292", + "@xterm/addon-image": "^0.10.0-beta.292", + "@xterm/xterm": "^6.1.0-beta.292", "codemirror": "^6.0.1", "vscode-languageserver-protocol": "^3.17.3" }, @@ -864,15 +865,27 @@ } }, "node_modules/@xterm/addon-fit": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", - "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", - "license": "MIT" + "version": "0.12.0-beta.292", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.12.0-beta.292.tgz", + "integrity": "sha512-SIuWR0KM2IpmumVqL4L43aOUeTEUeN82ItyrUFHGFtfj3veoAIbPapxJPj7O0tYngfiiHZqqm1+XsZp/Fc8LLA==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^6.1.0-beta.292" + } + }, + "node_modules/@xterm/addon-image": { + "version": "0.10.0-beta.292", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.292.tgz", + "integrity": "sha512-kFhxNiZ5eU1acMYvp/aS0wYs2wJ3j/ueq54FSNnWwnchirOuAVQxxdlFdxlZFHVLSas7rijqaRN1dniN+30HGQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^6.1.0-beta.292" + } }, "node_modules/@xterm/xterm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", - "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "version": "6.1.0-beta.292", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.292.tgz", + "integrity": "sha512-17zqK5tM/l6qeD7McF42OrEJ6w3XqJ2vFVKdWqu0cYLzdFqMWAHp2oFNc8Fj5DmqDSl1E1FZEg6IFflDllTvLA==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/package.json b/package.json index 8f2fd9a..c256610 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,9 @@ "@codemirror/view": "^6.9.3", "@ndim/codemirror-lang-zig": "^0.2.0", "@ndim/lezer-zig": "^0.2.0", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", + "@xterm/addon-fit": "^0.12.0-beta.292", + "@xterm/addon-image": "^0.10.0-beta.292", + "@xterm/xterm": "^6.1.0-beta.292", "codemirror": "^6.0.1", "vscode-languageserver-protocol": "^3.17.3" }, diff --git a/src/editor.ts b/src/editor.ts index bc9647a..2d29b34 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -7,6 +7,7 @@ import { indentUnit, syntaxHighlighting } from "@codemirror/language"; import { zigLanguage } from "@ndim/codemirror-lang-zig"; import { Terminal } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"; +import { ImageAddon } from "@xterm/addon-image"; import "@xterm/xterm/css/xterm.css"; import { editorTheme, highlightStyle } from "./theme.ts"; import { lspClient } from "./lsp.ts"; @@ -15,13 +16,23 @@ import ZigWorker from './workers/zig.ts?worker'; // @ts-ignore import RunnerWorker from './workers/runner.ts?worker'; // @ts-ignore -import zigMainSource from './main.zig?raw'; +import helloWorldSource from './examples/hello-world.zig?raw'; +// @ts-ignore +import ansiTextSource from './examples/ansi-text.zig?raw'; +// @ts-ignore +import fizzBuzzSource from './examples/fizz-buzz.zig?raw'; +// @ts-ignore +import fibonacciSource from './examples/fibonacci.zig?raw'; +// @ts-ignore +import mandelbrotSource from './examples/mandelbrot.zig?raw'; +// @ts-ignore +import kittyImageSource from './examples/kitty-image.zig?raw'; const editor = new EditorView({ extensions: [], parent: document.getElementById("editor")!, state: EditorState.create({ - doc: zigMainSource, + doc: helloWorldSource, extensions: [ basicSetup, editorTheme, @@ -57,6 +68,7 @@ const terminal = new Terminal({ }); const fitAddon = new FitAddon(); terminal.loadAddon(fitAddon); +terminal.loadAddon(new ImageAddon()); terminal.open(output); terminal.write("\x1b[?25l"); // hide cursor fitAddon.fit(); @@ -152,6 +164,27 @@ resizeBar.addEventListener("dblclick", event => { }); const outputsRun = document.getElementById("run")! as HTMLButtonElement; +const exampleSelect = document.getElementById("example-select")! as HTMLSelectElement; + +const examples: Record = { + "hello-world": helloWorldSource, + "ansi-text": ansiTextSource, + "fizz-buzz": fizzBuzzSource, + "fibonacci": fibonacciSource, + "mandelbrot": mandelbrotSource, + "kitty-image": kittyImageSource, +}; + +exampleSelect.addEventListener("change", () => { + editor.dispatch({ + changes: { + from: 0, + to: editor.state.doc.length, + insert: examples[exampleSelect.value], + }, + }); +}); + outputsRun.addEventListener("click", async () => { if (isCompiling) return; diff --git a/src/examples/ansi-text.zig b/src/examples/ansi-text.zig new file mode 100644 index 0000000..ec97d68 --- /dev/null +++ b/src/examples/ansi-text.zig @@ -0,0 +1,17 @@ +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + var buffer: [4096]u8 = undefined; + var file_writer = std.Io.File.stdout().writerStreaming(init.io, &buffer); + const stdout = &file_writer.interface; + + try stdout.writeAll("\x1b[31mred\n"); + try stdout.writeAll("\x1b[32mgreen\n"); + try stdout.writeAll("\x1b[33myellow\n"); + try stdout.writeAll("\x1b[34mblue\n"); + try stdout.writeAll("\x1b[35mmagenta\n"); + try stdout.writeAll("\x1b[36mcyan\n"); + try stdout.writeAll("\x1b[37mwhite\n"); + try stdout.writeAll("\x1b[0mreset\n"); + try stdout.flush(); +} diff --git a/src/examples/fibonacci.zig b/src/examples/fibonacci.zig new file mode 100644 index 0000000..52e3262 --- /dev/null +++ b/src/examples/fibonacci.zig @@ -0,0 +1,19 @@ +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + var buffer: [4096]u8 = undefined; + var file_writer = std.Io.File.stdout().writerStreaming(init.io, &buffer); + const stdout = &file_writer.interface; + + var a: u32 = 0; + var b: u32 = 1; + + for (0..12) |_| { + try stdout.print("{d}\n", .{a}); + const c = a + b; + a = b; + b = c; + } + + try stdout.flush(); +} diff --git a/src/examples/fizz-buzz.zig b/src/examples/fizz-buzz.zig new file mode 100644 index 0000000..bb6e5f2 --- /dev/null +++ b/src/examples/fizz-buzz.zig @@ -0,0 +1,21 @@ +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + var buffer: [4096]u8 = undefined; + var file_writer = std.Io.File.stdout().writerStreaming(init.io, &buffer); + const stdout = &file_writer.interface; + + for (1..21) |number| { + if (number % 15 == 0) { + try stdout.writeAll("FizzBuzz\n"); + } else if (number % 3 == 0) { + try stdout.writeAll("Fizz\n"); + } else if (number % 5 == 0) { + try stdout.writeAll("Buzz\n"); + } else { + try stdout.print("{d}\n", .{number}); + } + } + + try stdout.flush(); +} diff --git a/src/main.zig b/src/examples/hello-world.zig similarity index 100% rename from src/main.zig rename to src/examples/hello-world.zig diff --git a/src/examples/kitty-image.zig b/src/examples/kitty-image.zig new file mode 100644 index 0000000..d738cbd --- /dev/null +++ b/src/examples/kitty-image.zig @@ -0,0 +1,85 @@ +// Ported from https://www.shadertoy.com/view/MdlXz8 + +const std = @import("std"); +const pow = std.math.pow; +const clamp = std.math.clamp; +const tau = std.math.tau; + +fn color(x: f64, y: f64) [3]f64 { + const time = 23.0; + const p = [2]f64{ + @mod(x * tau, tau) - 250.0, + @mod(y * tau, tau) - 250.0, + }; + var i = p; + var c: f64 = 1.0; + const inten = 0.005; + + for (0..5) |n| { + const t = time * (1.0 - 3.5 / @as(f64, @floatFromInt(n + 1))); + const prev = i; + i = .{ + p[0] + @cos(t - prev[0]) + @sin(t + prev[1]), + p[1] + @sin(t - prev[1]) + @cos(t + prev[0]), + }; + + const dx = p[0] / (@sin(i[0] + t) / inten); + const dy = p[1] / (@cos(i[1] + t) / inten); + c += 1.0 / @sqrt(dx * dx + dy * dy); + } + + c /= 5.0; + c = 1.17 - pow(f64, c, 1.4); + const colour = pow(f64, @abs(c), 8.0); + + return .{ + clamp(colour, 0.0, 1.0), + clamp(colour + 0.35, 0.0, 1.0), + clamp(colour + 0.5, 0.0, 1.0), + }; +} + +pub fn main(init: std.process.Init) !void { + var buffer: [4096]u8 = undefined; + var file_writer = std.Io.File.stdout().writerStreaming(init.io, &buffer); + const stdout = &file_writer.interface; + + const width = 400; + const height = 400; + // https://sw.kovidgoyal.net/kitty/graphics-protocol/#remote-client + // from Kitty docs "The pixel data must first be base64 encoded then chunked up into chunks no larger than 4096 bytes." + // 1024 RGB pixels -> 3072 bytes -> 3072 / 3 * 4 = 4096 Base64 bytes + const pixels_per_chunk = 1024; + // TODO use std.base64 after compiler update + const base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + var pixel: usize = 0; + while (pixel < width * height) { + const chunk_end = @min(pixel + pixels_per_chunk, width * height); + const more: usize = @intFromBool(chunk_end < width * height); + + if (pixel == 0) { + try stdout.print("\x1b_Ga=T,f=24,s={d},v={d},m={d};", .{ width, height, more }); + } else { + try stdout.print("\x1b_Gm={d};", .{more}); + } + + while (pixel < chunk_end) : (pixel += 1) { + const x = @as(f64, @floatFromInt(pixel % width)) / (width - 1); + const y = @as(f64, @floatFromInt(pixel / width)) / (height - 1); + const rgb = color(x, y); + const red: u8 = @intFromFloat(rgb[0] * 255); + const green: u8 = @intFromFloat(rgb[1] * 255); + const blue: u8 = @intFromFloat(rgb[2] * 255); + + try stdout.writeByte(base64[red >> 2]); + try stdout.writeByte(base64[((red & 0x03) << 4) | (green >> 4)]); + try stdout.writeByte(base64[((green & 0x0f) << 2) | (blue >> 6)]); + try stdout.writeByte(base64[blue & 0x3f]); + } + + try stdout.writeAll("\x1b\\"); + } + + try stdout.flush(); +} diff --git a/src/examples/mandelbrot.zig b/src/examples/mandelbrot.zig new file mode 100644 index 0000000..9b73c0a --- /dev/null +++ b/src/examples/mandelbrot.zig @@ -0,0 +1,43 @@ +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + var buffer: [4096]u8 = undefined; + var file_writer = std.Io.File.stdout().writerStreaming(init.io, &buffer); + const stdout = &file_writer.interface; + + const width = 64; + const height = 24; + const iterations = 60; + const shades = " .:-=+*#%@"; + const real_min = -2.2; + const real_max = 1.0; + const imag_min = -1.2; + const imag_max = 1.2; + const real_ratio = (real_max - real_min) / @as(f64, @floatFromInt(width - 1)); + const imag_ratio = (imag_max - imag_min) / @as(f64, @floatFromInt(height - 1)); + + for (0..height) |y| { + const imag = imag_min + @as(f64, @floatFromInt(y)) * imag_ratio; + + for (0..width) |x| { + const real = real_min + @as(f64, @floatFromInt(x)) * real_ratio; + + var zx: f64 = 0; + var zy: f64 = 0; + var i: usize = 0; + + while (zx * zx + zy * zy <= 4 and i < iterations) : (i += 1) { + const next_zx = zx * zx - zy * zy + real; + zy = 2 * zx * zy + imag; + zx = next_zx; + } + + const shade = i * (shades.len - 1) / iterations; + try stdout.writeByte(shades[shade]); + } + + try stdout.writeByte('\n'); + } + + try stdout.flush(); +} diff --git a/style/style.css b/style/style.css index 28433e3..2611025 100644 --- a/style/style.css +++ b/style/style.css @@ -72,6 +72,12 @@ header { align-items: center; } + .header-controls { + display: flex; + gap: 0.5rem; + align-items: center; + } + .header-info>h1 { font-size: 1em; font-weight: 900; @@ -208,6 +214,17 @@ ul { line-height: 1.5; } +#example-select { + border: 1px solid var(--resizer-color); + border-radius: 3px; + padding: 0.5rem 0.75rem; + font-size: 1em; + font-weight: 700; + cursor: pointer; + background-color: var(--header-background); + color: var(--header-text); +} + #run { border: none; border-radius: 3px; From 406ba0427fe4628bc502cc259097024a91bfe499 Mon Sep 17 00:00:00 2001 From: Pavel Verigo Date: Tue, 4 Aug 2026 05:14:50 +0200 Subject: [PATCH 4/4] prevent white flash on page reload --- style/style.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/style/style.css b/style/style.css index 2611025..ca80517 100644 --- a/style/style.css +++ b/style/style.css @@ -166,6 +166,11 @@ code { font-family: monospace; } +/* prevent white flash on page reload */ +#editor { + background-color: var(--code-background-color); +} + .cm-editor { height: 100%; font-size: 16px;