nose-pluto/public/bundle.js

1364 lines
36 KiB
JavaScript

////
// version: 7f31112
// 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/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: `<span class="red">${message}</span>` });
}
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/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] || "");
window.dispatchEvent(new CustomEvent("session:update", { detail: msg.data }));
mode(msg.data.mode);
}
function handleSessionUpdate(msg) {
for (const key of Object.keys(msg.data))
sessionStore.set(key, msg.data[key] || "");
window.dispatchEvent(new CustomEvent("session:update", { detail: msg.data }));
}
// 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/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 = 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 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 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 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 || "/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
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/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 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() {
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
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.preventDefault();
openBrowser(href);
}
}
// 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/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/statusbar.ts
var projectName = $("project-name");
var projectCwd = $("project-cwd");
var projectWww = $("project-www");
var statusbar = $("statusbar");
var statusbarMsg = $("statusbar-msg");
function initStatusbar() {
registerEvents();
}
function registerEvents() {
window.addEventListener("apps:change", (e) => updateWww(sessionStore.get("project") || "root"));
window.addEventListener("session:update", (e) => {
const ev = e;
const data = ev.detail;
if (data.project)
updateProjectName(data.project);
if (data.cwd)
updateCwd(data.cwd);
});
}
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 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();
initSession();
initStatusbar();
startConnection();
startVramCounter();