//// // version: c1faf0e // src/js/dom.ts var content2 = $("content"); 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 content3 = 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); content3.style.transformOrigin = "top center"; content3.style.transform = `scaleX(${scale})`; } function resizeCinema() { const scale = Math.min(window.innerWidth / 960, window.innerHeight / 540); content3.style.transformOrigin = "center center"; content3.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 content4 = $$("span.content", input); parent.append(status, content4); 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, content4] = processOutput(output2); if (format === "html") item.innerHTML = content4; else item.textContent = content4; 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, content4] = processOutput(output2); if (format === "html") item.innerHTML += content4; else item.textContent += content4; 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, content4] = processOutput(output2); if (format === "html") item.innerHTML = content4; else item.textContent = content4; 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; } } function handleOutput(msg) { const result = msg.data; setStatus(msg.id, result.status); addOutput(msg.id, result.output); } // src/js/session.ts var sessionId = randomId(); // src/js/stream.ts 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/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/dispatch.ts async function dispatchMessage(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; case "ui:mode": browserCommands.mode?.(msg.data); break; default: console.error("unknown message type", 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?session=${sessionId}`, 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 dispatchMessage(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/commands.ts var commands = []; var browserCommands = { "browser-session": () => sessionId, clear: () => scrollback.innerHTML = "", commands: () => { return { html: "
" + commands.map((cmd) => `${cmd}`).join("") + "
" }; }, fullscreen: () => document.body.requestFullscreen(), mode: (mode) => { if (!mode) { mode = document.body.dataset.mode === "tall" ? "cinema" : "tall"; send({ type: "ui:mode", data: mode }); } content2.style.display = ""; document.body.dataset.mode = mode; resize(); autoScroll(); focusInput(); }, reload: () => window.location.reload() }; function cacheCommands(cmds) { commands.length = 0; commands.push(...cmds); commands.push(...Object.keys(browserCommands)); commands.sort(); console.log("CMDS", commands); } // 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/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/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/js/shell.ts async function runCommand(input) { if (!input.trim()) return; if (input.includes(";")) { input.split(";").forEach(async (cmd2) => await runCommand(cmd2.trim())); return; } const id = randomId(); addToHistory(input); addInput(id, input); const [cmd = "", ...args] = input.split(" "); if (browserCommands[cmd]) { const result = await browserCommands[cmd](...args); if (result) addOutput(id, result); setStatus(id, "ok"); } else { send({ id, type: "input", data: input }); } } // src/js/hyperlink.ts function initHyperlink() { window.addEventListener("click", handleClick); } async 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(); await runCommand(href.slice(1)); focusInput(); } } // src/js/input.ts function initInput() { cmdInput.addEventListener("keydown", inputHandler); cmdInput.addEventListener("paste", pasteHandler); } async 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(); await 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();