import { IDisposable, Terminal } from "xterm"; import { FitAddon } from 'xterm-addon-fit'; import { WebLinksAddon } from 'xterm-addon-web-links'; import { WebglAddon } from 'xterm-addon-webgl'; import { ZModemAddon } from "./zmodem"; export class OurXterm { // The HTMLElement that contains our terminal elem: HTMLElement; // The xtermjs.XTerm term: Terminal; resizeListener: () => void; message: HTMLElement; messageTimeout: number; messageTimer: NodeJS.Timeout; onResizeHandler: IDisposable; onDataHandler: IDisposable; fitAddOn: FitAddon; zmodemAddon: ZModemAddon; toServer: (data: string | Uint8Array) => void; encoder: TextEncoder constructor(elem: HTMLElement) { this.elem = elem; this.term = new Terminal(); this.fitAddOn = new FitAddon(); this.zmodemAddon = new ZModemAddon({ toTerminal: (x: Uint8Array) => this.term.write(x), toServer: (x: Uint8Array) => this.sendInput(x) }); this.term.loadAddon(new WebLinksAddon()); this.term.loadAddon(this.fitAddOn); this.term.loadAddon(this.zmodemAddon); this.message = elem.ownerDocument.createElement("div"); this.message.className = "xterm-overlay"; this.messageTimeout = 2000; this.resizeListener = () => { this.fitAddOn.fit(); this.term.scrollToBottom(); this.showMessage(String(this.term.cols) + "x" + String(this.term.rows), this.messageTimeout); }; this.term.open(elem); this.term.focus(); this.resizeListener(); window.addEventListener("resize", () => { this.resizeListener(); }); }; info(): { columns: number, rows: number } { return { columns: this.term.cols, rows: this.term.rows }; }; // This gets called from the Websocket's onReceive handler output(data: Uint8Array) { this.zmodemAddon.consume(data); }; getMessage(): HTMLElement { return this.message; } showMessage(message: string, timeout: number) { this.message.innerHTML = message; this.showMessageElem(timeout); } showMessageElem(timeout: number) { this.elem.appendChild(this.message); if (this.messageTimer) { clearTimeout(this.messageTimer); } if (timeout > 0) { this.messageTimer = setTimeout(() => { try { this.elem.removeChild(this.message); } catch (error) { console.error(error); } }, timeout); } }; removeMessage(): void { if (this.message.parentNode == this.elem) { this.elem.removeChild(this.message); } } setWindowTitle(title: string) { document.title = title; }; setPreferences(value: object) { Object.keys(value).forEach((key) => { if (key == "EnableWebGL" && key) { this.term.loadAddon(new WebglAddon()); } else if (key == "font-size") { this.term.options.fontSize = value[key] } else if (key == "font-family") { this.term.options.fontFamily = value[key] } }); }; sendInput(data: Uint8Array) { return this.toServer(data) } onInput(callback: (input: string) => void) { this.encoder = new TextEncoder() this.toServer = callback; // I *think* we're ok like this, but if not, we can dispose // of the previous handler and put the new one in place. if (this.onDataHandler !== undefined) { return } this.onDataHandler = this.term.onData((input) => { this.toServer(this.encoder.encode(input)); }); }; onResize(callback: (colmuns: number, rows: number) => void) { this.onResizeHandler = this.term.onResize(() => { callback(this.term.cols, this.term.rows); }); }; deactivate(): void { this.onDataHandler.dispose(); this.onResizeHandler.dispose(); this.term.blur(); } reset(): void { this.removeMessage(); this.term.clear(); } close(): void { window.removeEventListener("resize", this.resizeListener); this.term.dispose(); } disableStdin(): void { this.term.options.disableStdin = true; } enableStdin(): void { this.term.options.disableStdin = false; } focus(): void { this.term.focus(); } }