@tty-pt/ndc
Version:
ndc example project
200 lines (169 loc) • 4.74 kB
JavaScript
// import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { WebLinksAddon } from "@xterm/addon-web-links";
import "./ndc.css";
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;
let sub = options.sub ?? {
onMessage: function (_ev, _arr) { return true; },
onOpen: function (_term, _ws) {},
onClose: function (_ws) {},
cols: 80,
rows: 25,
};
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 = {};
const write = data => term.write(data);
const terminst = term_max;
term_max++;
function send(text) {
ws.send(text);
}
const decoder = new TextDecoder('utf-8');
let connected = false;
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 resize(cols, rows) {
sub.cols = cols;
sub.rows = rows;
if (!connected)
return;
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
]);
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);
function connect() {
ws = new WebSocket(url, 'binary');
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
connected = true;
resize(sub.cols, sub.rows);
sub.onOpen(term, ws);
};
ws.onmessage = onMessage;
ws.onclose = () => {
connected = false;
sub.onClose();
sub.timeout = setTimeout(() => {
clearTimeout(sub.timeout);
connect();
}, 3000);
};
};
connect();
return sub;
}
window.ttyNdc = { create };
export default { create };