" + 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()
};
function cacheCommands(cmds) {
commands.length = 0;
commands.push(...cmds);
commands.push(...Object.keys(browserCommands));
commands.sort();
}
// src/js/completion.ts
function initCompletion() {
cmdInput.addEventListener("keydown", handleCompletion);
}
function handleCompletion(e) {
if (e.key !== "Tab")
return;
e.preventDefault();
const input = cmdInput.value;
for (const command of commands) {
if (command.startsWith(input)) {
cmdInput.value = command;
return;
}
}
}
// src/js/cursor.ts
var cursor = "Û";
var cmdCursor;
var enabled = true;
function initCursor() {
cmdCursor = $("command-cursor");
cmdInput.addEventListener("keydown", showCursor);
document.addEventListener("focus", cursorEnablerHandler, true);
showCursor();
}
function showCursor(e = {}) {
if (!enabled) {
cmdCursor.value = "";
return;
}
if (e.key === "Enter" && !e.shiftKey) {
cmdCursor.value = cursor;
return;
}
requestAnimationFrame(() => cmdCursor.value = buildBlankCursorLine() + cursor);
}
function cursorEnablerHandler(e) {
if (!e.target)
return;
const target = e.target;
enabled = target.id === "command-textbox";
showCursor();
}
function buildBlankCursorLine() {
let line = "";
for (const char of cmdInput.value.slice(0, cmdInput.selectionEnd)) {
line += char === `
` ? char : " ";
}
return line;
}
// src/js/drop.ts
function initDrop() {
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
document.body.addEventListener(eventName, preventDefaults, false);
});
document.body.addEventListener("drop", handleDrop);
}
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
function handleDrop(e) {
const fileInput = document.querySelector("input[type=file]");
const files = e.dataTransfer?.files ?? [];
if (files.length > 0) {
const dt = new DataTransfer;
Array.from(files).forEach((f) => dt.items.add(f));
fileInput.files = dt.files;
fileInput.dispatchEvent(new Event("change", { bubbles: true }));
}
}
// src/js/editor.ts
var INDENT_SIZE = 2;
function initEditor() {
document.addEventListener("input", handleAdjustHeight);
focusTextareaOnCreation();
}
function handleAdjustHeight(e) {
const target = e.target;
if (target?.matches(".editor"))
adjustHeight(target);
}
function adjustHeight(editor) {
editor.style.height = "auto";
editor.style.height = editor.scrollHeight + "px";
}
function focusTextareaOnCreation() {
const observer = new MutationObserver((mutations) => {
for (const m of mutations)
for (const node of Array.from(m.addedNodes))
if (node instanceof HTMLElement && node.childNodes[0] instanceof HTMLElement && node.childNodes[0].matches("textarea")) {
const editor = node.childNodes[0];
editor.focus();
editor.addEventListener("keydown", keydownHandler);
return;
}
});
observer.observe(scrollback, { childList: true });
}
function keydownHandler(e) {
const editor = e.target;
if (e.key === "Tab") {
e.preventDefault();
if (e.shiftKey)
removeTab(editor);
else
insertTab(editor);
} else if (e.ctrlKey && e.key === "c") {
focusInput();
} else if (e.ctrlKey && e.key === "s" || e.ctrlKey && e.key === "Enter") {
e.preventDefault();
send({
id: editor.dataset.path || "/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/history.ts
var history = ["one", "two", "three"];
var idx = -1;
var savedInput = "";
function initHistory() {
cmdInput.addEventListener("keydown", navigateHistory);
}
function addToHistory(input) {
if (history.length === 0 || history[0] === input)
return;
history.unshift(input);
resetHistory();
}
function resetHistory() {
idx = -1;
savedInput = "";
}
function navigateHistory(e) {
if (cmdLine.dataset.extended)
return;
if (e.key === "ArrowUp" || e.ctrlKey && e.key === "p") {
e.preventDefault();
if (idx >= history.length - 1)
return;
if (idx === -1)
savedInput = cmdInput.value;
cmdInput.value = history[++idx] || "";
if (idx >= history.length)
idx = history.length - 1;
} else if (e.key === "ArrowDown" || e.ctrlKey && e.key === "n") {
e.preventDefault();
if (idx <= 0) {
cmdInput.value = savedInput;
idx = -1;
return;
}
cmdInput.value = history[--idx] || "";
if (idx < -1)
idx = -1;
} else if (idx !== -1) {
resetHistory();
}
}
// src/js/shell.ts
async function runCommand(input) {
if (!input.trim())
return;
if (input.includes(";")) {
input.split(";").forEach(async (cmd2) => await runCommand(cmd2.trim()));
return;
}
const id = randomId();
addToHistory(input);
addInput(id, input);
const [cmd = "", ...args] = input.split(" ");
if (browserCommands[cmd]) {
const result = await browserCommands[cmd](...args);
if (result)
addOutput(id, result);
setStatus(id, "ok");
} else {
send({ id, type: "input", data: input });
}
}
// src/js/hyperlink.ts
function initHyperlink() {
window.addEventListener("click", 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/vram.ts
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.ts
initCompletion();
initCursor();
initDrop();
initFocus();
initForm();
initEditor();
initGamepad();
initHistory();
initHyperlink();
initInput();
initResize();
initScrollback();
initSession();
startConnection();
startVramCounter();