summaryrefslogtreecommitdiff
path: root/public/shell.js
diff options
context:
space:
mode:
authorKyren223 <contact@kyren.codes>2026-10-10 04:43:44 +0300
committerKyren223 <contact@kyren.codes>2026-10-10 04:43:44 +0300
commit332051138391a86325e4e780895aa4148f81c5d5 (patch)
treee80c35a1f76c383d8dc1ffbd13921e326638877b /public/shell.js
parent03403186339abb58c5d4a720a5da23ddde66014b (diff)
Website stuff, mostly done, needs finishing touches that I am too lazy
to do
Diffstat (limited to 'public/shell.js')
-rw-r--r--public/shell.js73
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();
+}