const shell = document.querySelector("[data-shell]"); if (shell) { const area = shell.closest("[data-scroll-area]"); const history = shell.querySelector("#shell-history"); const input = shell.querySelector("[data-shell-input]"); const caret = shell.querySelector("[data-shell-caret]"); let busy = false; const updateCaret = () => { const pos = input.selectionStart ?? input.value.length; caret.style.left = `calc(${pos}ch - ${input.scrollLeft}px)`; caret.textContent = input.value[pos] ?? "\u00a0"; caret.style.animation = "none"; // restart the blink so the caret is solid while typing void caret.offsetWidth; caret.style.animation = ""; }; const snapToBottom = () => { area.scrollTop = area.scrollHeight; }; const run = async (cmd) => { busy = true; try { const res = await fetch("/shell/run", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ cmd }), }); const data = await res.json(); if (data.clear) history.replaceChildren(); if (data.html) history.insertAdjacentHTML("beforeend", data.html); if (data.vars) { for (const [key, value] of Object.entries(data.vars)) { document.documentElement.style.setProperty(`--${key}`, value); } } } catch { const el = document.createElement("p"); el.textContent = "request failed"; history.append(el); } finally { busy = false; snapToBottom(); } }; input.addEventListener("keydown", (e) => { if (e.key !== "Enter") return; e.preventDefault(); const cmd = input.value.trim(); if (!cmd || busy) return; input.value = ""; updateCaret(); run(cmd); }); for (const event of ["input", "keyup", "click", "focus"]) { input.addEventListener(event, updateCaret); } document.addEventListener("selectionchange", () => { if (document.activeElement === input) updateCaret(); }); area.addEventListener("click", () => { if (!getSelection().toString()) input.focus({ preventScroll: true }); }); input.focus({ preventScroll: true }); updateCaret(); snapToBottom(); }