//// // version: 606bcc9 var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true, configurable: true, set: (newValue) => all[name] = () => newValue }); }; // src/js/completion.ts var exports_completion = {}; __export(exports_completion, { initCompletion: () => initCompletion }); // src/js/dom.ts var exports_dom = {}; __export(exports_dom, { scrollback: () => scrollback, content: () => content2, cmdLine: () => cmdLine, cmdInput: () => cmdInput, $$: () => $$, $: () => $ }); 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/commands.ts var exports_commands = {}; __export(exports_commands, { mode: () => mode, commands: () => commands, cacheCommands: () => cacheCommands }); // src/js/shell.ts var exports_shell = {}; __export(exports_shell, { runCommand: () => runCommand }); // src/js/scrollback.ts var exports_scrollback = {}; __export(exports_scrollback, { setStatus: () => setStatus, replaceOutput: () => replaceOutput, latestId: () => latestId, insert: () => insert, initScrollback: () => initScrollback, handleOutput: () => handleOutput, appendOutput: () => appendOutput, addOutput: () => addOutput, addInput: () => addInput, addErrorMessage: () => addErrorMessage }); // src/shared/utils.ts function randomId() { return Math.random().toString(36).slice(7); } async function importUrl(url) { url += url.includes("?") ? "&" : "?"; url += "t=" + (nodeEnv() === "production" ? gitSHA() : Date.now()); console.log("-> import", url); return import(url); } function nodeEnv() { if (typeof process !== "undefined" && process.env?.NODE_ENV) return "development"; if (typeof globalThis !== "undefined" && globalThis.NODE_ENV) return globalThis.NODE_ENV; return; } function gitSHA() { return globalThis.GIT_SHA; } // src/js/scrollback.ts var statusColors = { waiting: "yellow", streaming: "purple", ok: "green", error: "red" }; function initScrollback() { window.addEventListener("click", handleInputClick); } function insert(node) { scrollback.append(node); } function latestId() { const nodes = document.querySelectorAll("[data-id]"); if (nodes.length) return nodes[nodes.length - 1].dataset.id; } function addInput(id, input, status) { const parent = $$("li.input"); const statusSpan = $$(`span.status.${statusColors[status || "waiting"]}`, "•"); const content3 = $$("span.content", input); parent.append(statusSpan, 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; statusEl.classList.remove(...Object.values(statusColors)); statusEl.classList.add(statusColors[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); } } 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; } 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; } 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; const id = "id" in msg ? msg.id || "" : ""; setStatus(id, result.status); addOutput(id, result.output); } // src/js/websocket.ts var exports_websocket = {}; __export(exports_websocket, { startConnection: () => startConnection, send: () => send, close: () => close }); // src/js/session.ts var exports_session = {}; __export(exports_session, { sessionStore: () => sessionStore, sessionId: () => sessionId, initSession: () => initSession, handleSessionUpdate: () => handleSessionUpdate, handleSessionStart: () => handleSessionStart }); // src/js/event.ts var exports_event = {}; __export(exports_event, { once: () => once, on: () => on, off: () => off, fire: () => fire }); function fire(eventName, detail) { window.dispatchEvent(new CustomEvent(eventName, { detail })); } function on(eventName, handler) { const listener = handler; window.addEventListener(eventName, listener); return () => window.removeEventListener(eventName, listener); } function once(eventName, handler) { const listener = handler; window.addEventListener(eventName, listener, { once: true }); return () => window.removeEventListener(eventName, listener); } function off(eventName, handler) { window.removeEventListener(eventName, handler); } // src/js/session.ts var sessionId = randomId(); var sessionStore = new Map; function initSession() {} function handleSessionStart(msg) { for (const key of Object.keys(msg.data)) sessionStore.set(key, msg.data[key] || ""); fire("session:update", msg.data); mode(msg.data.mode); } function handleSessionUpdate(msg) { for (const key of Object.keys(msg.data)) sessionStore.set(key, msg.data[key] || ""); fire("session:update", msg.data); } // src/js/dispatch.ts var exports_dispatch = {}; __export(exports_dispatch, { dispatchMessage: () => dispatchMessage }); // src/js/webapp.ts var exports_webapp = {}; __export(exports_webapp, { currentAppUrl: () => currentAppUrl, cacheApps: () => cacheApps, apps: () => apps }); var apps = []; function cacheApps(a) { apps.length = 0; apps.unshift(...a); apps.sort(); window.dispatchEvent(new CustomEvent("apps:change")); } function currentAppUrl() { const project = sessionStore.get("project") || "root"; if (!apps.includes(project)) return; const hostname = sessionStore.get("hostname") || "localhost"; const s = hostname.startsWith("localhost") ? "" : "s"; return `http${s}://${project}.${hostname}`; } // src/js/stream.ts var exports_stream = {}; __export(exports_stream, { handleStreamStart: () => handleStreamStart, handleStreamReplace: () => handleStreamReplace, handleStreamEnd: () => handleStreamEnd, handleStreamAppend: () => handleStreamAppend }); 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/game.ts var exports_game = {}; __export(exports_game, { handleGameStart: () => handleGameStart }); // 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 var exports_focus = {}; __export(exports_focus, { initFocus: () => initFocus, focusInput: () => focusInput, focusHandler: () => focusHandler }); 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 = 960; 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 importUrl(`/source/${name}?session=${sessionId}}`); } catch (err) { setStatus(msgId, "error"); addOutput(msgId, `Error: ${err.message ? err.message : err}`); return; } if (document.body.dataset.mode === "tall") { 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") 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 "apps": cacheApps(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 "session:start": handleSessionStart(msg); break; case "session:update": handleSessionUpdate(msg); 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 close() { ws?.close(1000, "bye"); } 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/history.ts var exports_history = {}; __export(exports_history, { resetHistory: () => resetHistory, initHistory: () => initHistory, addToHistory: () => addToHistory }); 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, showInput = true) { if (!input.trim()) return; if (input.includes(";")) { input.split(";").forEach(async (cmd2) => await runCommand(cmd2.trim())); return; } const id = randomId(); addToHistory(input); if (showInput) addInput(id, input); const [cmd = "", ...args] = input.split(" "); if (commands[cmd]?.type === "browser") { const mod = await importUrl(`/source/${cmd}`); if (!mod.default) { addOutput(id, `no default export in ${cmd}`); setStatus(id, "error"); return; } const result = mod.default(...args); if (result) addOutput(id, result); setStatus(id, "ok"); } else { send({ id, type: "input", data: input }); } } // src/js/commands.ts var commands = {}; function cacheCommands(cmds) { for (const key in commands) delete commands[key]; Object.assign(commands, cmds); } async function mode(mode2) { await runCommand(mode2 ? `mode ${mode2}` : "mode", false); } // 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 Object.keys(commands)) { if (command.startsWith(input)) { cmdInput.value = command + " "; return; } } } // src/js/cursor.ts var exports_cursor = {}; __export(exports_cursor, { initCursor: () => initCursor }); 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 var exports_drop = {}; __export(exports_drop, { initDrop: () => initDrop }); 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 exports_editor = {}; __export(exports_editor, { initEditor: () => initEditor }); 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 || "/tmp.txt", 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 var exports_form = {}; __export(exports_form, { submitHandler: () => submitHandler, initForm: () => initForm }); 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 exports_gamepad = {}; __export(exports_gamepad, { initGamepad: () => initGamepad }); 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 var exports_hyperlink = {}; __export(exports_hyperlink, { initHyperlink: () => initHyperlink }); // src/js/browser.ts var exports_browser = {}; __export(exports_browser, { openBrowser: () => openBrowser, isBrowsing: () => isBrowsing }); var HEIGHT2 = 540; var WIDTH2 = 960; var controls = $("browser-controls"); var address = $("browser-address"); var iframe; var realUrl = ""; var showInput = true; function isBrowsing() { return document.querySelector("iframe.browser.active") !== null; } function openBrowser(url, openedVia = "click") { showInput = openedVia === "click"; iframe = $$("iframe.browser.active"); iframe.src = url; iframe.sandbox.add("allow-scripts", "allow-same-origin", "allow-forms"); iframe.height = String(HEIGHT2); iframe.width = String(WIDTH2); iframe.tabIndex = 0; window.addEventListener("message", handleAppMessage); window.addEventListener("keydown", handleBrowserKeydown); controls.addEventListener("click", handleClick); iframe.addEventListener("load", handlePageLoad); controls.style.display = ""; const main = document.querySelector("#content"); main?.prepend(iframe); setAddress(url); } function closeBrowser() { window.removeEventListener("keydown", handleBrowserKeydown); window.removeEventListener("message", handleAppMessage); controls.removeEventListener("click", handleClick); iframe.removeEventListener("load", handlePageLoad); const id = randomId(); if (showInput) addInput(id, "browse " + realUrl, "ok"); iframe.style.transformOrigin = "top left"; iframe.style.transform = "scale(0.5)"; iframe.style.pointerEvents = "none"; iframe.tabIndex = -1; iframe.classList.remove("fullscreen", "active"); scrollback.append(iframe); controls.style.display = "none"; focusInput(); } function handleAppMessage(event) { const origin = event.origin; if (!origin.includes("localhost") && !origin.match(/\.local$/)) { return; } const { type, data } = event.data; switch (type) { case "NAV_READY": break; case "URL_CHANGED": setAddress(data.url); break; case "TITLE_CHANGED": console.log("Page title:", data.title); break; case "NAV_BLOCKED": showNavigationError(data.url, data.reason); break; case "KEYDOWN": const keyEvent = new KeyboardEvent("keydown", { key: data.key, ctrlKey: data.ctrlKey, shiftKey: data.shiftKey, altKey: data.altKey, metaKey: data.metaKey, bubbles: true }); window.dispatchEvent(keyEvent); break; } } function handleBrowserKeydown(e) { if (e.key === "Escape" || e.ctrlKey && e.key === "c") { e.preventDefault(); closeBrowser(); } } function handleClick(e) { const target = e.target; if (!(target instanceof HTMLElement)) return; switch (target.id) { case "back-button": navigateBack(); break; case "forward-button": navigateForward(); break; case "stop-button": stopLoading(); break; case "reload-button": reloadBrowser(); break; case "fullscreen-button": fullscreenBrowser(); break; case "close-button": e.stopImmediatePropagation(); closeBrowser(); break; default: return; } e.preventDefault(); } function handlePageLoad() {} function setAddress(url) { realUrl = url; address.textContent = url.replace(/https?:\/\//, ""); } function navigateBack() { sendNavCommand("back"); } function navigateForward() { sendNavCommand("forward"); } function reloadBrowser() { iframe.src = realUrl; } function stopLoading() { sendNavCommand("stop"); } function fullscreenBrowser() { controls.style.display = "none"; iframe.classList.add("fullscreen"); document.body.append(iframe); } function sendNavCommand(action) { if (!iframe.contentWindow) return; iframe.contentWindow.postMessage({ type: "NAV_COMMAND", action }, "*"); } function showNavigationError(url, reason) { alert(`NAVIGATION BLOCKED ${url} ${reason}`); } // src/js/hyperlink.ts function initHyperlink() { window.addEventListener("click", handleClick2); } async function handleClick2(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("#") && href.length > 1) { e.preventDefault(); await runCommand(href.slice(1)); focusInput(); } else if (!isBrowsing() && !e.metaKey) { e.preventDefault(); openBrowser(href); } } // src/js/input.ts var exports_input = {}; __export(exports_input, { initInput: () => initInput }); 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/resize.ts var exports_resize = {}; __export(exports_resize, { resize: () => resize, initResize: () => initResize }); 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/js/statusbar.ts var exports_statusbar = {}; __export(exports_statusbar, { status: () => status, projectWww: () => projectWww, projectName: () => projectName, projectCwd: () => projectCwd, initStatusbar: () => initStatusbar }); var projectName = $("project-name"); var projectCwd = $("project-cwd"); var projectWww = $("project-www"); var statusbar = $("statusbar"); var statusbarMsg = $("statusbar-msg"); var STATUS_MSG_LENGTH = 3000; var timer; function initStatusbar() { registerEvents(); } function registerEvents() { on("apps:change", (e) => updateWww(sessionStore.get("project") || "root")); on("session:update", (e) => { const ev = e; const data = ev.detail; if (data.project) updateProjectName(data.project); if (data.cwd) updateCwd(data.cwd); }); } function status(msg) { showStatusMsg(); statusbarMsg.textContent = msg; if (timer) clearTimeout(timer); timer = setTimeout(hideStatusMsg, STATUS_MSG_LENGTH); } function showStatusMsg() { statusbar.classList.add("showing-msg"); } function hideStatusMsg() { statusbar.className = ""; } function updateProjectName(project) { projectName.textContent = project; updateWww(project); updateCwd("/"); } function updateCwd(cwd) { cwd = displayProjectPath(cwd); projectCwd.textContent = cwd; } function updateWww(project) { if (!apps.includes(project) || project === "root") { projectWww.style.display = "none"; return; } projectWww.style.display = ""; const hostname = sessionStore.get("hostname") || "localhost"; const s = hostname.startsWith("localhost") ? "" : "s"; projectWww.href = `http${s}://${project}.${hostname}`; } function displayProjectPath(path) { let prefix = sessionStore.get("NOSE_DIR") || ""; prefix += "/" + sessionStore.get("project"); return path.replace(prefix, "") || "/"; } // src/js/vram.ts var exports_vram = {}; __export(exports_vram, { startVramCounter: () => startVramCounter }); var vramCounter = $("vram-size"); var startVramCounter = () => { const timer2 = 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(timer2); } }, 15); }; // src/js/main.tmp.ts initCompletion(); initCursor(); initDrop(); initFocus(); initForm(); initEditor(); initGamepad(); initHistory(); initHyperlink(); initInput(); initResize(); initScrollback(); initSession(); initStatusbar(); startConnection(); startVramCounter(); Object.assign(window, { __pluto_modules: { browser: exports_browser, commands: exports_commands, completion: exports_completion, cursor: exports_cursor, dispatch: exports_dispatch, dom: exports_dom, drop: exports_drop, editor: exports_editor, event: exports_event, focus: exports_focus, form: exports_form, game: exports_game, gamepad: exports_gamepad, history: exports_history, hyperlink: exports_hyperlink, input: exports_input, resize: exports_resize, scrollback: exports_scrollback, session: exports_session, shell: exports_shell, statusbar: exports_statusbar, stream: exports_stream, vram: exports_vram, webapp: exports_webapp, websocket: exports_websocket } });