diff --git a/public/bundle.js b/public/bundle.js index f7bb43d..60c6a56 100644 --- a/public/bundle.js +++ b/public/bundle.js @@ -1,5 +1,5 @@ //// -// version: 3e67d66 +// version: ea5b33c // src/js/dom.ts var content2 = $("content"); @@ -24,32 +24,26 @@ var $$ = (tag, innerHTML = "") => { 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); } +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 = { @@ -166,179 +160,6 @@ function handleOutput(msg) { 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) { @@ -347,14 +168,6 @@ function cacheApps(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/session.ts var sessionId = randomId(); @@ -370,7 +183,7 @@ function handleSessionStart(msg) { sessionStore.set("hostname", msg.data.hostname); updateProjectName(msg.data.project); updateCwd(msg.data.cwd); - browserCommands.mode?.(msg.data.mode); + mode(msg.data.mode); } function handleSessionUpdate(msg) { const data = msg.data; @@ -597,10 +410,32 @@ class GameContext { } } +// 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 HEIGHT2 = 540; -var WIDTH2 = 960; +var HEIGHT = 540; +var WIDTH = 960; var oldMode = "cinema"; var running = false; var canvas; @@ -620,14 +455,14 @@ async function handleGameStart(msg) { const name = msg.data; let game; try { - game = await import(`/source/${name}?session=${sessionId}`); + 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") { - browserCommands.mode?.(); + mode(); oldMode = "tall"; } canvas = createCanvas(); @@ -642,8 +477,8 @@ async function handleGameStart(msg) { function createCanvas() { const canvas2 = $$("canvas.game.active"); canvas2.id = randomId(); - canvas2.height = HEIGHT2; - canvas2.width = WIDTH2; + canvas2.height = HEIGHT; + canvas2.width = WIDTH; canvas2.tabIndex = 0; const main = document.querySelector("main"); main?.classList.add("game"); @@ -710,12 +545,12 @@ function endGame() { window.removeEventListener("keyup", handleKeyup); window.removeEventListener("resize", resizeCanvas); if (oldMode === "tall") - browserCommands.mode?.(); + 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"; + canvas.style.height = HEIGHT / 2 + "px"; + canvas.style.width = WIDTH / 2 + "px"; const output2 = $$("li.output"); output2.append(canvas); insert(output2); @@ -811,61 +646,88 @@ function retryConnection() { 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); +// src/js/history.ts +var history = ["one", "two", "three"]; +var idx = -1; +var savedInput = ""; +function initHistory() { + cmdInput.addEventListener("keydown", navigateHistory); } -function showStatusMsg() { - statusbar.classList.add("showing-msg"); +function addToHistory(input) { + if (history.length === 0 || history[0] === input) + return; + history.unshift(input); + resetHistory(); } -function hideStatusMsg() { - statusbar.className = ""; +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 = []; -var browserCommands = { - browse: (url) => { - const currentUrl = url ?? currentAppUrl(); - if (currentUrl) { - openBrowser(currentUrl, "command"); - } else { - setTimeout(() => setStatus(latestId(), "error"), 0); - return "usage: browse "; - } - }, - "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: "session:update", data: { "ui:mode": mode } }); - } - content2.style.display = ""; - document.body.dataset.mode = mode; - resize(); - focusInput(); - }, - status: (msg) => status(msg), - reload: () => window.location.reload() -}; +var commands = {}; function cacheCommands(cmds) { - commands.length = 0; - commands.push(...cmds); - commands.push(...Object.keys(browserCommands)); - commands.sort(); + 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 @@ -877,7 +739,7 @@ function handleCompletion(e) { return; e.preventDefault(); const input = cmdInput.value; - for (const command of commands) { + for (const command of Object.keys(commands)) { if (command.startsWith(input)) { cmdInput.value = command; return; @@ -1207,70 +1069,153 @@ function handleGamepad() { requestAnimationFrame(handleGamepad); } -// src/js/history.ts -var history = ["one", "two", "three"]; -var idx = -1; -var savedInput = ""; -function initHistory() { - cmdInput.addEventListener("keydown", navigateHistory); +// src/js/browser.ts +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 addToHistory(input) { - if (history.length === 0 || history[0] === input) - return; - history.unshift(input); - resetHistory(); +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 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; - } +function closeBrowser() { + window.removeEventListener("keydown", handleBrowserKeydown); + window.removeEventListener("message", handleAppMessage); + controls.removeEventListener("click", handleClick); + iframe.removeEventListener("load", handlePageLoad); 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 }); + 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/hyperlink.ts @@ -1348,10 +1293,34 @@ function clearInput() { delete cmdLine.dataset.extended; } +// 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/vram.ts var vramCounter = $("vram-size"); var startVramCounter = () => { - const timer2 = setInterval(() => { + const timer = setInterval(() => { const count = parseInt(vramCounter.textContent) + 1; let val = count + "KB"; if (count < 10) @@ -1359,7 +1328,7 @@ var startVramCounter = () => { vramCounter.textContent = val; if (count >= 64) { vramCounter.textContent += " OK"; - clearInterval(timer2); + clearInterval(timer); } }, 15); }; diff --git a/src/html/layout.tsx b/src/html/layout.tsx index 5401fbb..f771fcf 100644 --- a/src/html/layout.tsx +++ b/src/html/layout.tsx @@ -12,6 +12,12 @@ export const Layout: FC = async ({ children, title }) => ( + diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 0721446..1320ead 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -40,9 +40,27 @@ export function unique(array: T[]): T[] { return [...new Set(array)] } +// import a typescript module. caches in production, doesn't in dev. export async function importUrl(url: string) { url += url.includes("?") ? "&" : "?" - url += "t=" + Date.now() + url += "t=" + (nodeEnv() === "production" ? gitSHA() : Date.now()) + console.log("-> import", url) return import(url) +} + +// "production" or nuttin +function nodeEnv(): string | undefined { + if (typeof process !== 'undefined' && process.env?.NODE_ENV) + return process.env.NODE_ENV + + if (typeof globalThis !== 'undefined' && (globalThis as any).NODE_ENV) + return (globalThis as any).NODE_ENV + + return undefined +} + +// should always be set +function gitSHA(): string { + return (globalThis as any).GIT_SHA } \ No newline at end of file