@tty-pt/ndc
Version:
ndc example project
215 lines (184 loc) • 5.19 kB
JavaScript
// import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { WebLinksAddon } from "@xterm/addon-web-links";
let term_max = 0;
// TODO change this to a class
export
function create(element, options = {}) {
const {
proto = location.protocol === "https:" ? "wss" : "ws",
port = window.location.port,
url = proto + "://" + window.location.hostname + ":" + port,
} = options;
const fitAddon = new FitAddon();
const resizeObserver = new ResizeObserver(() => fitAddon.fit());
const term = new globalThis.Terminal({
convertEol: true,
fontSize: 13,
fontFamily: 'Consolas,Liberation Mono,Menlo,Courier,monospace',
allowProposedApi: true,
});
let ws = new WebSocket(url, 'binary');
ws.binaryType = 'arraybuffer';
let sub = options.sub;
let subContents = {
...options, ws, term,
write: data => term.write(data),
};
const terminst = term_max;
term_max++;
function send(text) {
console.log("SEND!", terminst, text);
sub.ws.send(text);
}
if (sub) {
Object.assign(sub, {
...subContents,
onMessage: sub.onMessage,
onOpen: sub.onOpen,
onClose: sub.onClose,
send,
});
} else {
sub = {
...subContents,
onMessage: function (_ev, _arr) { return true; },
onOpen: function (_term, _ws) {},
onClose: function (_ws) {},
send,
};
}
const decoder = new TextDecoder('utf-8');
let resolveConnect = null;
const connected = new Promise(resolve => resolveConnect = resolve);
let will_echo = true;
let raw = false;
function onMessage(ev) {
const arr = new Uint8Array(ev.data);
if (!sub.onMessage(ev, arr))
return;
else if (arr[0] != 255) {
const data = decoder.decode(arr);
term.write(data);
// } else if (arr[1] == 254) { // DONT
// } else if (arr[1] == 253) { // DO
} else if (arr[1] == 252) { // WONT
switch (arr[2]) {
case 1: // TELOPT_ECHO
will_echo = false;
if (options.debug)
console.log("WONT ECHO");
break;
case 3: // TELOPT_SGA
raw = false;
if (options.debug)
console.log("WONT SGA (ICANON/not raw)");
break;
}
} else if (arr[1] == 251) { // WILL
switch (arr[2]) {
case 1: // TELOPT_ECHO
will_echo = true;
if (options.debug)
console.log("WILL ECHO");
break;
case 3: // TELOPT_SGA
raw = true;
if (options.debug)
console.log("WILL SGA (not ICANON/raw)");
break;
}
} else if (arr[1] == 250) { // SB
switch (arr[2]) {
case 31: // TELOPT_NAWS
}
}
}
function onOpen(term, ws) {
resolveConnect();
fitAddon.fit();
sub.onOpen(term, ws);
}
function onClose() {
sub.onClose(sub);
ws = ws.onclose = ws.onmessage = ws.onopen = null;
term.dispose();
// reconnect
const id = setInterval(() => {
create(element, { ...options, sub });
}, 3000);
const prevOnOpen = sub.onOpen;
sub.onOpen = (term, ws) => {
clearInterval(id);
prevOnOpen(term, ws);
sub.onOpen = prevOnOpen;
};
}
ws.onopen = onOpen;
ws.onmessage = onMessage;
ws.onclose = onClose;
function resize(cols, rows) {
const IAC = 255;
const SB = 250;
const NAWS = 31;
const SE = 240;
const colsHighByte = cols >> 8;
const colsLowByte = cols & 0xFF;
const rowsHighByte = rows >> 8;
const rowsLowByte = rows & 0xFF;
const nawsCommand = new Uint8Array([
IAC, SB, NAWS,
colsHighByte, colsLowByte,
rowsHighByte, rowsLowByte,
IAC, SE
]);
connected.then(() => ws.send(nawsCommand));
}
function open(parent) {
parent.scrollTop = parent.scrollHeight;
term.loadAddon(fitAddon);
term.loadAddon(new WebLinksAddon());
term.open(parent);
term.inputBuf = "";
term.perm = "";
term.onResize(({ cols, rows }) => resize(cols, rows));
resizeObserver.observe(parent);
term.element.addEventListener("focusin", () => {
term.focused = true;
});
term.element.addEventListener("focusout", () => {
term.focused = false;
});
term.onData(data => {
if (options.debug)
console.log("term.onData", data, data.charAt(0), raw, will_echo);
if (raw)
send(data === "\r" ? "\r\n" : data);
else if (data === "\r" || data === "\n") {
if (will_echo)
term.write("\b \b".repeat(term.inputBuf.length));
else
term.write("\n");
ws.send(term.inputBuf + "\r\n");
term.inputBuf = "";
} else if (data === "\u007f") {
if (raw)
send(data);
else {
term.write("\b \b");
term.inputBuf = term.inputBuf.length > 0 ? term.inputBuf.slice(0, term.inputBuf.length - 1) : "";
}
} else {
term.inputBuf += data;
if (will_echo)
term.write(data);
return;
}
term.lastInput = false;
});
return term;
}
open(element);
return sub;
}
export default { create };