diff options
Diffstat (limited to 'public/shell.js')
| -rw-r--r-- | public/shell.js | 73 |
1 files changed, 73 insertions, 0 deletions
diff --git a/public/shell.js b/public/shell.js new file mode 100644 index 0000000..9a70846 --- /dev/null +++ b/public/shell.js @@ -0,0 +1,73 @@ +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(); +} |
