Files
rohit267-champdev-comfyui-n…/web/champdev_terminal.js
2026-05-23 13:02:49 +05:30

200 lines
5.4 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
const VENDOR = new URL("./vendor/", import.meta.url).href;
function loadXterm() {
if (window.__champTermAssets) return window.__champTermAssets;
window.__champTermAssets = new Promise((resolve, reject) => {
if (!document.getElementById("champ-term-css")) {
const link = document.createElement("link");
link.id = "champ-term-css";
link.rel = "stylesheet";
link.href = VENDOR + "xterm.css";
document.head.append(link);
}
const s1 = document.createElement("script");
s1.src = VENDOR + "xterm.js";
s1.onerror = () => reject(new Error("failed to load xterm.js"));
s1.onload = () => {
const s2 = document.createElement("script");
s2.src = VENDOR + "addon-fit.js";
s2.onerror = () => reject(new Error("failed to load addon-fit.js"));
s2.onload = () => resolve();
document.head.append(s2);
};
document.head.append(s1);
});
return window.__champTermAssets;
}
function wsUrl(params) {
const u = new URL(api.apiURL("/champdev/terminal/ws"), window.location.origin);
u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
return u.href;
}
function widgetValue(node, name) {
return node.widgets?.find((w) => w.name === name)?.value ?? "";
}
async function setupTerminal(node) {
const root = document.createElement("div");
root.style.cssText =
"display:flex;flex-direction:column;width:100%;height:100%;background:#1e1e1e;border-radius:6px;overflow:hidden";
const bar = document.createElement("div");
bar.style.cssText =
"display:flex;gap:6px;padding:4px 6px;border-bottom:1px solid #333;flex:0 0 auto";
const restart = document.createElement("button");
restart.textContent = "⟳ Restart";
restart.style.cssText =
"background:#3a3a3a;color:#ddd;border:1px solid #555;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:12px";
const status = document.createElement("span");
status.style.cssText = "color:#888;font-size:12px;align-self:center";
bar.append(restart, status);
const host = document.createElement("div");
host.style.cssText = "flex:1 1 auto;min-height:0;padding:4px";
root.append(bar, host);
node.addDOMWidget("champ_term", "div", root, { serialize: false, hideOnZoom: false });
node.size = [620, 400];
await loadXterm();
const term = new Terminal({
cursorBlink: true,
fontSize: 13,
fontFamily: "monospace",
theme: { background: "#1e1e1e" },
});
const fit = new FitAddon.FitAddon();
term.loadAddon(fit);
term.open(host);
let ws = null;
const sendResize = () => {
try {
fit.fit();
} catch (e) {
/* host not measurable yet */
}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
}
};
const connect = () => {
try {
fit.fit();
} catch (e) {
/* ignore */
}
status.textContent = "connecting…";
ws = new WebSocket(
wsUrl({
cols: term.cols || 80,
rows: term.rows || 24,
shell: widgetValue(node, "shell"),
cwd: widgetValue(node, "start_dir"),
})
);
ws.binaryType = "arraybuffer";
ws.onopen = () => {
status.textContent = "connected";
term.focus();
sendResize();
};
ws.onmessage = (ev) => {
if (typeof ev.data === "string") {
try {
const m = JSON.parse(ev.data);
if (m.type === "exit") {
term.write(`\r\n\x1b[33m[process exited: ${m.code}${m.error ? " — " + m.error : ""}]\x1b[0m\r\n`);
status.textContent = "ended";
}
} catch (e) {
/* ignore */
}
} else {
term.write(new Uint8Array(ev.data));
}
};
ws.onerror = () => {
term.write("\r\n\x1b[31m[connection error]\x1b[0m\r\n");
status.textContent = "error";
};
ws.onclose = () => {
if (!["error", "ended"].includes(status.textContent)) {
status.textContent = "disconnected";
}
};
};
term.onData((d) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "input", data: d }));
}
});
const ro = new ResizeObserver(() => sendResize());
ro.observe(host);
restart.addEventListener("click", () => {
try {
ws?.close();
} catch (e) {
/* ignore */
}
term.reset();
connect();
});
connect();
node.__champTerm = {
dispose() {
try {
ro.disconnect();
} catch (e) {
/* ignore */
}
try {
ws?.close();
} catch (e) {
/* ignore */
}
try {
term.dispose();
} catch (e) {
/* ignore */
}
},
};
}
app.registerExtension({
name: "champdev.terminal",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "ChampdevTerminal") return;
const onCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onCreated?.apply(this, arguments);
setupTerminal(this).catch((err) =>
console.error("[champdev terminal] init failed:", err)
);
return r;
};
const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function () {
this.__champTerm?.dispose();
return onRemoved?.apply(this, arguments);
};
},
});