From 1d6a7bc0c14b0a6cb5f0f8f7768dd00f506cfe3e Mon Sep 17 00:00:00 2001 From: Chris Wanstrath <2+defunkt@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:12:05 -0700 Subject: [PATCH] bundle js --- package.json | 11 +- public/bundle.js | 1110 +++++++++++++++++++++++++++++++++++++++++++ src/html/layout.tsx | 2 +- src/server.tsx | 1 + 4 files changed, 1119 insertions(+), 5 deletions(-) create mode 100644 public/bundle.js diff --git a/package.json b/package.json index 2c08857..87d9b75 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,13 @@ "type": "module", "private": true, "scripts": { - "start": "bun src/server.tsx", - "runner": "env NODE_ENV=production bun run src/runner.ts", - "prod": "env NODE_ENV=production bun src/server.tsx", "dev": "env BUN_HOT=1 bun --hot src/server.tsx", + "start": "bun src/server.tsx", + "prod": "env NODE_ENV=production bun src/server.tsx", + "build": "./scripts/build.sh", + + "runner": "env NODE_ENV=production bun run src/runner.ts", + "deploy": "./scripts/deploy.sh", "push": "./scripts/deploy.sh", "remote:install": "./scripts/remote-install.sh", @@ -21,4 +24,4 @@ "dependencies": { "kleur": "^4.1.5" } -} \ No newline at end of file +} diff --git a/public/bundle.js b/public/bundle.js new file mode 100644 index 0000000..8d3bdca --- /dev/null +++ b/public/bundle.js @@ -0,0 +1,1110 @@ +// src/js/dom.ts +var cmdLine = $("command-line"); +var cmdInput = $("command-textbox"); +var scrollback = $("scrollback"); +function $(id) { + return document.getElementById(id); +} +var $$ = (tag, innerHTML = "") => { + let name = tag; + let classList = []; + if (tag.startsWith(".")) + tag = "div" + tag; + if (tag.includes(".")) + [name, ...classList] = tag.split(".").map((x) => x.trim()); + const el = document.createElement(name); + if (innerHTML) + el.innerHTML = innerHTML; + if (classList.length) + el.classList.add(...classList); + return el; +}; + +// src/js/resize.ts +var content2 = document.getElementById("content"); +function initResize() { + window.addEventListener("resize", resize); + resize(); +} +function resize() { + if (document.body.dataset.mode === "tall") { + resizeTall(); + } else { + resizeCinema(); + } +} +function resizeTall() { + const scale = Math.min(1, window.innerWidth / 960); + content2.style.transformOrigin = "top center"; + content2.style.transform = `scaleX(${scale})`; +} +function resizeCinema() { + const scale = Math.min(window.innerWidth / 960, window.innerHeight / 540); + content2.style.transformOrigin = "center center"; + content2.style.transform = `scale(${scale})`; +} + +// src/shared/utils.ts +function randomId() { + return Math.random().toString(36).slice(7); +} + +// src/js/scrollback.ts +function initScrollback() { + window.addEventListener("click", handleInputClick); +} +function autoScroll() {} +function insert(node) { + scrollback.append(node); +} +function addInput(id, input) { + const parent = $$("li.input"); + const status = $$("span.status.yellow", "•"); + const content3 = $$("span.content", input); + parent.append(status, content3); + parent.dataset.id = id; + insert(parent); + scrollback.scrollTop = scrollback.scrollHeight - scrollback.clientHeight; +} +function setStatus(id, status) { + const statusEl = document.querySelector(`[data-id="${id}"].input .status`); + if (!statusEl) + return; + const colors = { + waiting: "yellow", + streaming: "purple", + ok: "green", + error: "red" + }; + statusEl.classList.remove(...Object.values(colors)); + statusEl.classList.add(colors[status]); +} +function addOutput(id, output2) { + const item = $$("li"); + item.classList.add("output"); + item.dataset.id = id || randomId(); + const [format, content3] = processOutput(output2); + if (format === "html") + item.innerHTML = content3; + else + item.textContent = content3; + const input = document.querySelector(`[data-id="${id}"].input`); + if (input instanceof HTMLLIElement) { + input.parentNode.insertBefore(item, input.nextSibling); + } else { + insert(item); + } + autoScroll(); +} +function addErrorMessage(message) { + addOutput("", { html: `${message}` }); +} +function appendOutput(id, output2) { + const item = document.querySelector(`[data-id="${id}"].output`); + if (!item) { + console.error(`output id ${id} not found`); + return; + } + const [format, content3] = processOutput(output2); + if (format === "html") + item.innerHTML += content3; + else + item.textContent += content3; + autoScroll(); +} +function replaceOutput(id, output2) { + const item = document.querySelector(`[data-id="${id}"].output`); + if (!item) { + console.error(`output id ${id} not found`); + return; + } + const [format, content3] = processOutput(output2); + if (format === "html") + item.innerHTML = content3; + else + item.textContent = content3; + autoScroll(); +} +function processOutput(output) { + let content = ""; + let html = false; + if (typeof output === "string") { + content = output; + } else if (Array.isArray(output)) { + content = output.join(" "); + } else if (typeof output === "object" && "html" in output) { + html = true; + content = output.html; + if (output.script) + eval(output.script); + } else if (typeof output === "object" && "text" in output) { + content = output.text; + if (output.script) + eval(output.script); + } else if (typeof output === "object" && "script" in output) { + eval(output.script); + } else { + content = JSON.stringify(output); + } + return [html ? "html" : "text", content]; +} +function handleInputClick(e) { + const target = e.target; + if (!(target instanceof HTMLElement)) + return; + if (target.matches(".input .content")) { + cmdInput.value = target.textContent; + } +} + +// src/js/session.ts +var sessionId = randomId(); + +// src/js/commands.ts +var commands = []; +var browserCommands = { + "browser-session": () => sessionId, + clear: () => scrollback.innerHTML = "", + commands: () => commands.join(" "), + fullscreen: () => document.body.requestFullscreen(), + mode: () => { + document.body.dataset.mode = document.body.dataset.mode === "tall" ? "cinema" : "tall"; + resize(); + autoScroll(); + }, + reload: () => window.location.reload() +}; +function cacheCommands(cmds) { + commands.length = 0; + commands.push(...cmds); + commands.push(...Object.keys(browserCommands)); + commands.sort(); +} + +// src/js/completion.ts +function initCompletion() { + cmdInput.addEventListener("keydown", handleCompletion); +} +function handleCompletion(e) { + if (e.key !== "Tab") + return; + e.preventDefault(); + const input = cmdInput.value; + for (const command of commands) { + if (command.startsWith(input)) { + cmdInput.value = command; + return; + } + } +} + +// src/js/cursor.ts +var cursor = "Û"; +var cmdCursor; +var enabled = true; +function initCursor() { + cmdCursor = $("command-cursor"); + cmdInput.addEventListener("keydown", showCursor); + document.addEventListener("focus", cursorEnablerHandler, true); + showCursor(); +} +function showCursor(e = {}) { + if (!enabled) { + cmdCursor.value = ""; + return; + } + if (e.key === "Enter" && !e.shiftKey) { + cmdCursor.value = cursor; + return; + } + requestAnimationFrame(() => cmdCursor.value = buildBlankCursorLine() + cursor); +} +function cursorEnablerHandler(e) { + if (!e.target) + return; + const target = e.target; + enabled = target.id === "command-textbox"; + showCursor(); +} +function buildBlankCursorLine() { + let line = ""; + for (const char of cmdInput.value.slice(0, cmdInput.selectionEnd)) { + line += char === ` +` ? char : " "; + } + return line; +} + +// src/js/drop.ts +function initDrop() { + ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => { + document.body.addEventListener(eventName, preventDefaults, false); + }); + document.body.addEventListener("drop", handleDrop); +} +function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); +} +function handleDrop(e) { + const fileInput = document.querySelector("input[type=file]"); + const files = e.dataTransfer?.files ?? []; + if (files.length > 0) { + const dt = new DataTransfer; + Array.from(files).forEach((f) => dt.items.add(f)); + fileInput.files = dt.files; + fileInput.dispatchEvent(new Event("change", { bubbles: true })); + } +} + +// src/js/history.ts +var history = ["one", "two", "three"]; +var idx = -1; +var savedInput = ""; +function initHistory() { + cmdInput.addEventListener("keydown", navigateHistory); +} +function addToHistory(input) { + if (history.length === 0 || history[0] === input) + return; + history.unshift(input); + resetHistory(); +} +function resetHistory() { + idx = -1; + savedInput = ""; +} +function navigateHistory(e) { + if (cmdLine.dataset.extended) + return; + if (e.key === "ArrowUp" || e.ctrlKey && e.key === "p") { + e.preventDefault(); + if (idx >= history.length - 1) + return; + if (idx === -1) + savedInput = cmdInput.value; + cmdInput.value = history[++idx] || ""; + if (idx >= history.length) + idx = history.length - 1; + } else if (e.key === "ArrowDown" || e.ctrlKey && e.key === "n") { + e.preventDefault(); + if (idx <= 0) { + cmdInput.value = savedInput; + idx = -1; + return; + } + cmdInput.value = history[--idx] || ""; + if (idx < -1) + idx = -1; + } else if (idx !== -1) { + resetHistory(); + } +} + +// src/shared/game.ts +class GameContext { + ctx; + constructor(ctx) { + this.ctx = ctx; + } + width = 960; + height = 540; + clear(color) { + if (color) + this.rectfill(0, 0, this.width, this.height, color); + else + this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); + } + text(msg, x, y, color = "black", size = 16, font = "C64ProMono") { + const c = this.ctx; + c.save(); + c.fillStyle = color; + c.font = `${size}px ${font}`; + c.textBaseline = "top"; + c.fillText(msg, x, y); + c.restore(); + } + centerText(msg, color = "black", size = 16, font = "C64ProMono") { + const c = this.ctx; + c.save(); + c.fillStyle = color; + c.font = `${size}px ${font}`; + c.textBaseline = "middle"; + c.textAlign = "center"; + c.fillText(msg, this.width / 2, this.height / 2); + c.restore(); + } + centerTextX(msg, y, color = "black", size = 16, font = "C64ProMono") { + const c = this.ctx; + c.save(); + c.fillStyle = color; + c.font = `${size}px ${font}`; + c.textBaseline = "middle"; + c.textAlign = "center"; + c.fillText(msg, this.width / 2, y); + c.restore(); + } + centerTextY(msg, x, color = "black", size = 16, font = "C64ProMono") { + const c = this.ctx; + c.save(); + c.fillStyle = color; + c.font = `${size}px ${font}`; + c.textBaseline = "middle"; + c.textAlign = "center"; + c.fillText(msg, x, this.height / 2); + c.restore(); + } + circ(x, y, r, color = "black") { + const c = this.ctx; + c.save(); + c.beginPath(); + c.strokeStyle = color; + c.arc(x, y, r, 0, Math.PI * 2); + c.stroke(); + c.restore(); + } + circfill(x, y, r, color = "black") { + const c = this.ctx; + c.save(); + c.beginPath(); + c.fillStyle = color; + c.arc(x, y, r, 0, Math.PI * 2); + c.fill(); + c.restore(); + } + line(x0, y0, x1, y1, color = "black") { + const c = this.ctx; + c.save(); + c.beginPath(); + c.strokeStyle = color; + c.moveTo(x0, y0); + c.lineTo(x1, y1); + c.stroke(); + c.restore(); + } + oval(x0, y0, x1, y1, color = "black") { + const c = this.ctx; + const w = x1 - x0; + const h = y1 - y0; + c.save(); + c.beginPath(); + c.strokeStyle = color; + c.ellipse(x0 + w / 2, y0 + h / 2, Math.abs(w) / 2, Math.abs(h) / 2, 0, 0, Math.PI * 2); + c.stroke(); + c.restore(); + } + ovalfill(x0, y0, x1, y1, color = "black") { + const c = this.ctx; + const w = x1 - x0; + const h = y1 - y0; + c.save(); + c.beginPath(); + c.fillStyle = color; + c.ellipse(x0 + w / 2, y0 + h / 2, Math.abs(w) / 2, Math.abs(h) / 2, 0, 0, Math.PI * 2); + c.fill(); + c.restore(); + } + rect(x0, y0, x1, y1, color = "black") { + const c = this.ctx; + c.save(); + c.beginPath(); + c.strokeStyle = color; + c.rect(x0, y0, x1 - x0, y1 - y0); + c.stroke(); + c.restore(); + } + rectfill(x0, y0, x1, y1, color = "black") { + const c = this.ctx; + c.save(); + this.ctx.fillStyle = color; + this.ctx.fillRect(x0, y0, x1 - x0, y1 - y0); + c.restore(); + } + rrect(x, y, w, h, r, color = "black") { + const c = this.ctx; + c.save(); + c.beginPath(); + c.strokeStyle = color; + this.roundRectPath(x, y, w, h, r); + c.stroke(); + c.restore(); + } + rrectfill(x, y, w, h, r, color = "black") { + const c = this.ctx; + c.save(); + c.beginPath(); + c.fillStyle = color; + this.roundRectPath(x, y, w, h, r); + c.fill(); + c.restore(); + } + trianglefill(x0, y0, x1, y1, x2, y2, color = "black") { + return this.polygonfill([ + [x0, y0], + [x1, y1], + [x2, y2] + ], color); + } + polygonfill(points, color = "black") { + if (points.length < 3) + return; + const c = this.ctx; + c.save(); + c.beginPath(); + c.fillStyle = color; + c.moveTo(points[0][0], points[0][1]); + for (let i = 1;i < points.length; i++) { + c.lineTo(points[i][0], points[i][1]); + } + c.closePath(); + c.fill(); + c.restore(); + } + roundRectPath(x, y, w, h, r) { + const c = this.ctx; + c.moveTo(x + r, y); + c.lineTo(x + w - r, y); + c.quadraticCurveTo(x + w, y, x + w, y + r); + c.lineTo(x + w, y + h - r); + c.quadraticCurveTo(x + w, y + h, x + w - r, y + h); + c.lineTo(x + r, y + h); + c.quadraticCurveTo(x, y + h, x, y + h - r); + c.lineTo(x, y + r); + c.quadraticCurveTo(x, y, x + r, y); + } +} + +// src/js/focus.ts +function initFocus() { + window.addEventListener("click", focusHandler); + focusInput(); +} +function focusInput() { + cmdInput.focus(); +} +function focusHandler(e) { + const target = e.target; + if (!(target instanceof HTMLElement)) { + focusInput(); + return; + } + if (["INPUT", "TEXTAREA", "CANVAS", "A"].includes(target.tagName)) + return false; + const selection = window.getSelection() || ""; + if (selection.toString() === "") + focusInput(); + e.preventDefault(); +} + +// src/js/game.ts +var FPS = 30; +var HEIGHT = 540; +var WIDTH = 980; +var oldMode = "cinema"; +var running = false; +var canvas; +var pressedStack = new Set; +var pressed = { + key: "", + shift: false, + ctrl: false, + meta: false, + pressed: pressedStack, + prevPressed: new Set, + justPressed: new Set, + justReleased: new Set +}; +async function handleGameStart(msg) { + const msgId = msg.id; + const name = msg.data; + let game; + try { + game = await import(`/source/${name}`); + } catch (err) { + setStatus(msgId, "error"); + addOutput(msgId, `Error: ${err.message ? err.message : err}`); + return; + } + if (document.body.dataset.mode === "tall") { + browserCommands.mode?.(); + oldMode = "tall"; + } + canvas = createCanvas(); + canvas.focus(); + setStatus(msgId, "ok"); + window.addEventListener("keydown", handleKeydown); + window.addEventListener("keyup", handleKeyup); + window.addEventListener("resize", resizeCanvas); + resizeCanvas(); + gameLoop(new GameContext(canvas.getContext("2d")), game); +} +function createCanvas() { + const canvas2 = $$("canvas.game.active"); + canvas2.id = randomId(); + canvas2.height = HEIGHT; + canvas2.width = WIDTH; + canvas2.tabIndex = 0; + const main = document.querySelector("main"); + main?.classList.add("game"); + main?.parentNode?.insertBefore(canvas2, main); + return canvas2; +} +function handleKeydown(e) { + e.preventDefault(); + if (e.key === "Escape" || e.ctrlKey && e.key === "c") { + endGame(); + } else { + pressedStack.add(e.key); + pressed.key = e.key; + pressed.ctrl = e.ctrlKey; + pressed.shift = e.shiftKey; + pressed.meta = e.metaKey; + } +} +function handleKeyup(e) { + pressedStack.delete(e.key); + if (pressedStack.size === 0) { + pressed.key = ""; + pressed.ctrl = false; + pressed.shift = false; + pressed.meta = false; + } +} +function updateInputState() { + pressed.justPressed = new Set([...pressed.pressed].filter((k) => !pressed.prevPressed.has(k))); + pressed.justReleased = new Set([...pressed.prevPressed].filter((k) => !pressed.pressed.has(k))); + pressed.prevPressed = new Set(pressed.pressed); +} +function resizeCanvas() { + const scale = Math.min(window.innerWidth / 960, window.innerHeight / 540); + canvas.width = 960 * scale; + canvas.height = 540 * scale; + const ctx = canvas.getContext("2d"); + ctx.setTransform(scale, 0, 0, scale, 0, 0); +} +function gameLoop(ctx, game) { + running = true; + let last = 0; + if (game.init) + game.init(); + function loop(ts) { + if (!running) + return; + const delta = ts - last; + if (delta >= 1000 / FPS) { + updateInputState(); + if (game.update) + game.update(delta, pressed); + if (game.draw) + game.draw(ctx); + last = ts; + } + requestAnimationFrame(loop); + } + requestAnimationFrame(loop); +} +function endGame() { + running = false; + window.removeEventListener("keydown", handleKeydown); + window.removeEventListener("keyup", handleKeyup); + window.removeEventListener("resize", resizeCanvas); + if (oldMode === "tall") + browserCommands.mode?.(); + const main = document.querySelector("main"); + main?.classList.remove("game"); + canvas.classList.remove("active"); + canvas.style.height = HEIGHT / 2 + "px"; + canvas.style.width = WIDTH / 2 + "px"; + const output2 = $$("li.output"); + output2.append(canvas); + insert(output2); + focusInput(); +} + +// src/js/shell.ts +function runCommand(input) { + if (!input.trim()) + return; + if (input.includes(";")) { + input.split(";").forEach((cmd2) => runCommand(cmd2.trim())); + return; + } + const id = randomId(); + addToHistory(input); + addInput(id, input); + const [cmd = "", ..._args] = input.split(" "); + if (browserCommands[cmd]) { + const result = browserCommands[cmd](); + if (typeof result === "string") + addOutput(id, result); + setStatus(id, "ok"); + } else { + send({ id, type: "input", data: input }); + } +} +async function handleMessage(msg) { + switch (msg.type) { + case "output": + handleOutput(msg); + break; + case "commands": + cacheCommands(msg.data); + break; + case "error": + console.error(msg.data); + break; + case "stream:start": + handleStreamStart(msg); + break; + case "stream:end": + handleStreamEnd(msg); + break; + case "stream:append": + handleStreamAppend(msg); + break; + case "stream:replace": + handleStreamReplace(msg); + break; + case "game:start": + await handleGameStart(msg); + break; + default: + console.error("unknown message type", msg); + } +} +function handleOutput(msg) { + const result = msg.data; + setStatus(msg.id, result.status); + addOutput(msg.id, result.output); +} +function handleStreamStart(msg) { + const id = msg.id; + const status = document.querySelector(`[data-id="${id}"].input .status`); + if (!status) + return; + addOutput(id, msg.data); + status.classList.remove("yellow"); + status.classList.add("purple"); +} +function handleStreamAppend(msg) { + appendOutput(msg.id, msg.data); +} +function handleStreamReplace(msg) { + replaceOutput(msg.id, msg.data); +} +function handleStreamEnd(_msg) {} + +// src/js/websocket.ts +var MAX_RETRIES = 5; +var retries = 0; +var connected = false; +var msgQueue = []; +var ws = null; +function startConnection() { + const url = new URL("/ws", location.href); + url.protocol = url.protocol.replace("http", "ws"); + ws = new WebSocket(url); + ws.onmessage = receive; + ws.onclose = retryConnection; + ws.onerror = () => ws?.close(); + ws.onopen = () => { + connected = true; + msgQueue.forEach((msg) => send(msg)); + msgQueue.length = 0; + }; +} +function send(msg) { + if (!connected) { + msgQueue.push(msg); + startConnection(); + return; + } + if (!msg.session) + msg.session = sessionId; + ws?.readyState === 1 && ws.send(JSON.stringify(msg)); + console.log("-> send", msg); +} +async function receive(e) { + const data = JSON.parse(e.data); + console.log("<- receive", data); + await handleMessage(data); +} +function retryConnection() { + connected = false; + if (retries >= MAX_RETRIES) { + addErrorMessage(`Failed to reconnect ${retries} times. Server is down.`); + if (ws) + ws.onclose = () => {}; + return; + } + retries++; + addErrorMessage(`Connection lost. Retrying...`); + setTimeout(startConnection, 2000); +} + +// src/js/editor.ts +var INDENT_SIZE = 2; +function initEditor() { + document.addEventListener("input", handleAdjustHeight); + focusTextareaOnCreation(); +} +function handleAdjustHeight(e) { + const target = e.target; + if (target?.matches(".editor")) + adjustHeight(target); +} +function adjustHeight(editor) { + editor.style.height = "auto"; + editor.style.height = editor.scrollHeight + "px"; +} +function focusTextareaOnCreation() { + const observer = new MutationObserver((mutations) => { + for (const m of mutations) + for (const node of Array.from(m.addedNodes)) + if (node instanceof HTMLElement && node.childNodes[0] instanceof HTMLElement && node.childNodes[0].matches("textarea")) { + const editor = node.childNodes[0]; + editor.focus(); + editor.addEventListener("keydown", keydownHandler); + return; + } + }); + observer.observe(scrollback, { childList: true }); +} +function keydownHandler(e) { + const editor = e.target; + if (e.key === "Tab") { + e.preventDefault(); + if (e.shiftKey) + removeTab(editor); + else + insertTab(editor); + } else if (e.ctrlKey && e.key === "c") { + focusInput(); + } else if (e.ctrlKey && e.key === "s" || e.ctrlKey && e.key === "Enter") { + e.preventDefault(); + send({ + id: editor.dataset.path, + type: "save-file", + data: editor.value + }); + } else if (e.key === "{") { + if (editor.selectionStart !== editor.selectionEnd) { + insertAroundSelection(editor, "{", "}"); + e.preventDefault(); + } else { + setTimeout(() => insertAfterCaret(editor, "}"), 0); + } + } else if (e.key === "}" && isNextChar(editor, "}")) { + moveOneRight(editor); + e.preventDefault(); + } else if (e.key === "[") { + if (editor.selectionStart !== editor.selectionEnd) { + insertAroundSelection(editor, "[", "]"); + e.preventDefault(); + } else { + setTimeout(() => insertAfterCaret(editor, "]"), 0); + } + } else if (e.key === "]" && isNextChar(editor, "]")) { + moveOneRight(editor); + e.preventDefault(); + } else if (e.key === "(") { + if (editor.selectionStart !== editor.selectionEnd) { + insertAroundSelection(editor, "(", ")"); + e.preventDefault(); + } else { + setTimeout(() => insertAfterCaret(editor, ")"), 0); + } + } else if (e.key === ")" && isNextChar(editor, ")")) { + moveOneRight(editor); + e.preventDefault(); + } else if (e.key === '"') { + if (isNextChar(editor, '"')) { + moveOneRight(editor); + e.preventDefault(); + } else if (editor.selectionStart !== editor.selectionEnd) { + insertAroundSelection(editor, '"', '"'); + e.preventDefault(); + } else { + setTimeout(() => insertAfterCaret(editor, '"'), 0); + } + } else if (e.key === "Enter") { + indentNewlineForBraces(e, editor); + } +} +function moveOneRight(editor) { + const pos = editor.selectionStart; + editor.selectionStart = editor.selectionEnd = pos + 1; +} +function insertTab(editor) { + const start = editor.selectionStart; + const end = editor.selectionEnd; + const lineStart = editor.value.lastIndexOf(` +`, start - 1) + 1; + editor.value = editor.value.slice(0, lineStart) + " " + editor.value.slice(lineStart); + editor.selectionStart = start + INDENT_SIZE; + editor.selectionEnd = end + INDENT_SIZE; +} +function removeTab(editor) { + const start = editor.selectionStart; + const end = editor.selectionEnd; + const lineStart = editor.value.lastIndexOf(` +`, start - 1) + 1; + if (editor.value.slice(lineStart, lineStart + 2) === " ") { + editor.value = editor.value.slice(0, lineStart) + editor.value.slice(lineStart + 2); + editor.selectionStart = start - INDENT_SIZE; + editor.selectionEnd = end - INDENT_SIZE; + } +} +function insertAfterCaret(editor, char) { + const pos = editor.selectionStart; + editor.value = editor.value.slice(0, pos) + char + editor.value.slice(pos); + editor.selectionStart = editor.selectionEnd = pos; +} +function isNextChar(editor, char) { + return editor.value[editor.selectionStart] === char; +} +function indentNewlineForBraces(e, editor) { + e.preventDefault(); + if (isBetween(editor, "{", "}") || isBetween(editor, "[", "]")) { + setTimeout(() => insertMoreIndentedNewline(editor), 0); + } else { + setTimeout(() => insertIndentedNewline(editor), 0); + } +} +function isBetween(editor, start, end) { + const pos = editor.selectionStart; + const val = editor.value; + if (pos <= 0 || pos >= val.length) + return false; + return val[pos - 1] === start && val[pos] === end; +} +function insertIndentedNewline(editor) { + const pos = editor.selectionStart; + const before = editor.value.slice(0, pos); + const prevLineStart = before.lastIndexOf(` +`, pos - 1) + 1; + const prevLine = before.slice(prevLineStart); + let leading = 0; + while (prevLine[leading] === " ") + leading++; + const indent = " ".repeat(leading); + const insert2 = ` +` + indent; + editor.value = editor.value.slice(0, pos) + insert2 + editor.value.slice(pos); + const newPos = pos + insert2.length; + editor.selectionStart = editor.selectionEnd = newPos; + adjustHeight(editor); +} +function insertAroundSelection(editor, before, after) { + const start = editor.selectionStart; + const end = editor.selectionEnd; + editor.value = editor.value.slice(0, start) + before + editor.value.slice(start, end) + after + editor.value.slice(end); +} +function insertMoreIndentedNewline(editor) { + const pos = editor.selectionStart; + const before = editor.value.slice(0, pos); + const prevLineStart = before.lastIndexOf(` +`, pos - 1) + 1; + const prevLine = before.slice(prevLineStart); + let leading = 0; + while (prevLine[leading] === " ") + leading++; + const oldIndent = " ".repeat(leading); + const newIndent = " ".repeat(leading + INDENT_SIZE); + const insert2 = ` +` + newIndent + ` +` + oldIndent; + editor.value = editor.value.slice(0, pos) + insert2 + editor.value.slice(pos); + const newPos = pos + insert2.length; + editor.selectionStart = editor.selectionEnd = newPos - 1 - oldIndent.length; + adjustHeight(editor); +} + +// src/js/form.ts +function initForm() { + document.addEventListener("submit", submitHandler); +} +var submitHandler = async (e) => { + e.preventDefault(); + const form = e.target; + if (!(form instanceof HTMLFormElement)) + return; + const li = form.closest(".output"); + if (!(li instanceof HTMLLIElement)) + return; + const id = li.dataset.id; + if (!id) + return; + let output2; + let error = false; + try { + const fd = new FormData(form); + const data = await fetch("/cmd" + new URL(form.action).pathname, { + method: "POST", + headers: { "X-Session": sessionId }, + body: fd + }).then((r) => r.json()); + output2 = data.output; + error = data.status === "error"; + } catch (e2) { + output2 = e2.message || e2.toString(); + error = true; + } + if (error) + setStatus(id, "error"); + replaceOutput(id, output2); + focusInput(); +}; + +// src/js/gamepad.ts +var BUTTONS_TO_KEYS = { + 0: "z", + 1: "x", + 2: "c", + 3: "v", + 12: "ArrowUp", + 13: "ArrowDown", + 14: "ArrowLeft", + 15: "ArrowRight", + 9: "Enter", + 8: "Escape" +}; +var activePads = new Set; +var prevState = {}; +function initGamepad() { + window.addEventListener("gamepadconnected", (e) => { + console.log("Gamepad connected:", e.gamepad); + activePads.add(e.gamepad.index); + requestAnimationFrame(handleGamepad); + }); + window.addEventListener("gamepaddisconnected", (e) => { + console.log("Gamepad disconnected:", e.gamepad); + activePads.delete(e.gamepad.index); + }); +} +function handleGamepad() { + const pads = navigator.getGamepads(); + for (const gp of pads) { + if (!gp) + continue; + if (!prevState[gp.index]) + prevState[gp.index] = gp.buttons.map(() => false); + gp.buttons.forEach((btn, i) => { + const was = prevState[gp.index][i]; + const is = btn.pressed; + if (was !== is) { + const key = BUTTONS_TO_KEYS[i]; + if (key) { + const type = is ? "keydown" : "keyup"; + window.dispatchEvent(new KeyboardEvent(type, { code: key, key })); + } + prevState[gp.index][i] = is; + } + }); + } + requestAnimationFrame(handleGamepad); +} + +// src/js/hyperlink.ts +function initHyperlink() { + window.addEventListener("click", handleClick); +} +function handleClick(e) { + const target = e.target; + if (!(target instanceof HTMLElement)) + return; + const a = target.closest("a"); + if (!a) + return; + const href = a.getAttribute("href"); + if (!href) + return; + if (href.startsWith("#")) { + e.preventDefault(); + runCommand(href.slice(1)); + focusInput(); + } +} + +// src/js/input.ts +function initInput() { + cmdInput.addEventListener("keydown", inputHandler); + cmdInput.addEventListener("paste", pasteHandler); +} +function inputHandler(event) { + const target = event.target; + if (target?.id !== cmdInput.id) + return; + if (event.key === "Escape" || event.ctrlKey && event.key === "c") { + clearInput(); + resetHistory(); + } else if (event.key === "Tab") { + event.preventDefault(); + } else if (event.shiftKey && event.key === "Enter") { + cmdInput.rows += 1; + cmdLine.dataset.extended = "true"; + } else if (event.key === "Enter") { + event.preventDefault(); + runCommand(cmdInput.value); + clearInput(); + } + setTimeout(() => { + if (cmdLine.dataset.extended && !cmdInput.value.includes(` +`)) { + cmdInput.rows = 1; + cmdInput.style.height = "auto"; + delete cmdLine.dataset.extended; + } + }, 0); +} +function pasteHandler(event) { + const text = event.clipboardData?.getData("text") || ""; + if (!text.includes(` +`)) { + delete cmdLine.dataset.extended; + return; + } + setTimeout(() => { + cmdInput.style.height = "auto"; + cmdInput.style.height = cmdInput.scrollHeight + "px"; + cmdLine.dataset.extended = "true"; + }, 0); +} +function clearInput() { + cmdInput.value = ""; + cmdInput.rows = 1; + cmdInput.style.height = "auto"; + delete cmdLine.dataset.extended; +} + +// src/js/vram.ts +var vramCounter = $("vram-size"); +var startVramCounter = () => { + const timer = setInterval(() => { + const count = parseInt(vramCounter.textContent) + 1; + let val = count + "KB"; + if (count < 10) + val = "0" + val; + vramCounter.textContent = val; + if (count >= 64) { + vramCounter.textContent += " OK"; + clearInterval(timer); + } + }, 15); +}; + +// src/js/main.ts +initCompletion(); +initCursor(); +initDrop(); +initFocus(); +initForm(); +initEditor(); +initGamepad(); +initHistory(); +initHyperlink(); +initInput(); +initResize(); +initScrollback(); +startConnection(); +startVramCounter(); diff --git a/src/html/layout.tsx b/src/html/layout.tsx index 87d51c2..2778bed 100644 --- a/src/html/layout.tsx +++ b/src/html/layout.tsx @@ -12,7 +12,7 @@ export const Layout: FC = async ({ children, title }) => ( - +