socktop-webterm/static/terminado-addon.js

183 lines
4.7 KiB
JavaScript
Raw Normal View History

/**
* 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;
}