/** * Terminado WebSocket Addon for xterm.js 5.x * * This addon handles the Terminado protocol for xterm.js, allowing * bidirectional communication over WebSocket with a backend PTY process. * * The Terminado protocol uses JSON arrays: * - ["stdin", data] - send input to the terminal * - ["stdout", data] - receive output from the terminal * - ["set_size", rows, cols] - notify backend of terminal size changes */ class TerminadoAddon { constructor() { this._disposables = []; this._socket = null; this._bidirectional = true; this._buffered = true; this._attachSocketBuffer = ''; this._flushTimeout = null; } activate(terminal) { this._terminal = terminal; } dispose() { this._disposables.forEach(d => d.dispose()); this._disposables.length = 0; if (this._flushTimeout) { clearTimeout(this._flushTimeout); this._flushTimeout = null; } if (this._socket) { this.detach(); } } attach(socket, bidirectional = true, buffered = true) { if (this._socket) { this.detach(); } this._socket = socket; this._bidirectional = bidirectional !== false; this._buffered = buffered !== false; // Handle incoming messages from the websocket this._messageHandler = (ev) => { try { const data = JSON.parse(ev.data); if (Array.isArray(data) && data[0] === 'stdout') { const output = data[1]; if (this._buffered) { this._pushToBuffer(output); } else { this._terminal.write(output); } } } catch (err) { console.error('Error handling terminado message:', err); } }; // Handle terminal input (user typing) if (this._bidirectional) { const dataDisposable = this._terminal.onData((data) => { this._sendData(data); }); this._disposables.push(dataDisposable); } // Handle terminal resize events const resizeDisposable = this._terminal.onResize((size) => { this._setSize(size); }); this._disposables.push(resizeDisposable); // Handle socket close/error this._closeHandler = () => this.detach(); this._errorHandler = () => this.detach(); socket.addEventListener('message', this._messageHandler); socket.addEventListener('close', this._closeHandler); socket.addEventListener('error', this._errorHandler); } detach() { if (!this._socket) { return; } if (this._messageHandler) { this._socket.removeEventListener('message', this._messageHandler); this._messageHandler = null; } if (this._closeHandler) { this._socket.removeEventListener('close', this._closeHandler); this._closeHandler = null; } if (this._errorHandler) { this._socket.removeEventListener('error', this._errorHandler); this._errorHandler = null; } this._disposables.forEach(d => d.dispose()); this._disposables.length = 0; this._socket = null; } _sendData(data) { if (this._socket && this._socket.readyState === WebSocket.OPEN) { try { this._socket.send(JSON.stringify(['stdin', data])); } catch (err) { console.error('Error sending data to terminal:', err); } } } _setSize(size) { if (this._socket && this._socket.readyState === WebSocket.OPEN) { try { this._socket.send(JSON.stringify(['set_size', size.rows, size.cols])); } catch (err) { console.error('Error sending terminal size:', err); } } } _pushToBuffer(data) { if (this._attachSocketBuffer) { this._attachSocketBuffer += data; } else { this._attachSocketBuffer = data; if (this._flushTimeout) { clearTimeout(this._flushTimeout); } this._flushTimeout = setTimeout(() => this._flushBuffer(), 10); } } _flushBuffer() { if (this._attachSocketBuffer && this._terminal) { this._terminal.write(this._attachSocketBuffer); this._attachSocketBuffer = ''; } this._flushTimeout = null; } // Public method to manually send size sendSize(rows, cols) { if (this._socket && this._socket.readyState === WebSocket.OPEN) { try { this._socket.send(JSON.stringify(['set_size', rows, cols])); } catch (err) { console.error('Error sending manual terminal size:', err); } } } // Public method to manually send command sendCommand(command) { if (this._socket && this._socket.readyState === WebSocket.OPEN) { try { this._socket.send(JSON.stringify(['stdin', command])); } catch (err) { console.error('Error sending command:', err); } } } } // Export for use in browsers if (typeof module !== 'undefined' && module.exports) { module.exports = TerminadoAddon; }