//// // version: 1a97e37 // 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/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/shared/utils.ts function randomId() { return Math.random().toString(36).slice(7); } // 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 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/browser.ts var HEIGHT = 540; var WIDTH = 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(HEIGHT); iframe.width = String(WIDTH); 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() { sendNavCommand("reload"); } 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/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/js/webapp.ts var apps = []; function cacheApps(a) { apps.length = 0; apps.unshift(...a); apps.sort(); window.dispatchEvent(new CustomEvent("apps:change")); } // src/js/session.ts var sessionId = randomId(); var projectName = $("project-name"); var projectCwd = $("project-cwd"); var projectWww = $("project-www"); var sessionStore = new Map; function initSession() { window.addEventListener("apps:change", (e) => updateWww(sessionStore.get("project") || "root")); } function handleSessionStart(msg) { sessionStore.set("NOSE_DIR", msg.data.NOSE_DIR); sessionStore.set("hostname", msg.data.hostname); updateProjectName(msg.data.project); updateCwd(msg.data.cwd); browserCommands.mode?.(msg.data.mode); } function handleSessionUpdate(msg) { const data = msg.data; if (data.project) updateProjectName(data.project); if (data.cwd) updateCwd(data.cwd); } function updateProjectName(project) { sessionStore.set("project", project); projectName.textContent = project; updateWww(project); updateCwd("/"); } function updateCwd(cwd) { cwd = displayProjectPath(cwd); sessionStore.set("cwd", cwd); projectCwd.textContent = cwd; } function updateWww(project) { if (!apps.includes(project)) { 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/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/game.ts var FPS = 30; var HEIGHT2 = 540; var WIDTH2 = 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 import(`/source/${name}?session=${sessionId}`); } 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 = HEIGHT2; canvas2.width = WIDTH2; 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 = HEIGHT2 / 2 + "px"; canvas.style.width = WIDTH2 / 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 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/statusbar.ts var STATUS_MSG_LENGTH = 3000; var statusbar = $("statusbar"); var statusbarMsg = $("statusbar-msg"); var timer; 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 = ""; } // src/js/commands.ts var commands = []; var browserCommands = { browse: (url) => openBrowser(url, "command"), "browser-session": () => sessionId, clear: () => scrollback.innerHTML = "", commands: () => { return { html: "