compass-bot/repl/remote-tty.mjs
2023-11-05 21:49:15 +08:00

198 lines
4.2 KiB
JavaScript

import { waitEvent } from 'compass-utils';
import { randomBytes } from 'node:crypto';
import { Duplex } from 'node:stream';
import { promisify } from 'node:util';
import debug from 'debug';
const logger = debug('compass-repl:RemoteTTY');
const kSource = Symbol('kSource');
export class RemoteTTY extends Duplex {
#rows
#cols
#color_depths
#tty_ready
#tty_ready_resolve
#event_callbacks
constructor(options, input, output) {
super(options);
this[kSource] = {
input: input,
output: output,
};
this.#tty_ready = new Promise(resolve => {
this.#tty_ready_resolve = resolve;
});
this.#event_callbacks = new Map();
this[kSource].input.on('close', () => this.push(null));
this[kSource].input.on('data', (data) => {
try {
if (!(data instanceof Buffer)) {
data = Buffer.from(data);
}
if (data[0] === 0x02) {
if (!this.push(data.slice(1))) {
this[kSource].input.pause();
}
return;
}
if (data[0] === 0x01) {
// Initialize data
this.#rows = data.readUInt16BE(1);
this.#cols = data.readUInt16BE(3);
this.#color_depths = data.readUInt8(5);
if (this.#tty_ready_resolve) {
this.#tty_ready_resolve();
this.#tty_ready_resolve = null;
}
} else if (data[0] == 0x03) {
// Resize event
this.#rows = data.readUInt16BE(1);
this.#cols = data.readUInt16BE(3);
this.emit('resize');
} else if (data[0] == 0x04) {
// Callback
let id = (data.readBigUInt64BE(1) << 64n) + data.readBigUInt64BE(9);
if (this.#event_callbacks.has(id)) {
logger(`Rescived callback ${data.toString('hex')}`);
this.#event_callbacks.get(id)();
this.#event_callbacks.delete(id);
}
}
} catch (err) {
//
}
});
}
get rows() {
return this.#rows;
}
get columns() {
return this.#cols;
}
getWindowSize() {
return [this.#rows, this.#cols];
}
getColorDepth() {
return this.#color_depths;
}
hasColors(count = 16) {
return (1 << this.#color_depths) >= count;
}
#registerCallback(data, callback) {
let cbid = randomBytes(16);
let cbid_v = (cbid.readBigUInt64BE(0) << 64n) + cbid.readBigUInt64BE(8);
if (callback) {
logger(`Registered callback ${cbid.toString('hex')}`);
this.#event_callbacks.set(cbid_v, callback);
}
cbid.copy(data, 1);
}
clearLine(dir, callback) {
logger('clearLine');
let data = Buffer.alloc(18);
data[0] = 0x10;
data.writeInt8(dir, 17);
this.#registerCallback(data, callback);
this.#sendData(data);
return this.writableNeedDrain;
}
clearScreenDown(callback) {
logger('clearScreenDown');
let data = Buffer.alloc(17);
data[0] = 0x11;
this.#registerCallback(data, callback);
this.#sendData(data);
return this.writableNeedDrain;
}
cursorTo(x, y, callback) {
logger('cursorTo');
let data = Buffer.alloc(22);
if (typeof y !== 'number') {
callback = y;
y = 0;
data[17] = 0x01;
} else {
data[17] = 0x00;
}
data[0] = 0x12;
data.writeUInt16BE(x, 18);
data.writeUInt16BE(y, 20);
this.#registerCallback(data, callback);
this.#sendData(data);
return this.writableNeedDrain;
}
moveCursor(dx, dy, callback) {
logger('moveCursor');
let data = Buffer.alloc(21);
data[0] = 0x13;
data.writeUInt16BE(dx, 17);
data.writeUInt16BE(dy, 19);
this.#registerCallback(data, callback);
this.#sendData(data);
return this.writableNeedDrain;
}
async #sendData(data) {
if (this[kSource].output.writableNeedDrain) {
await waitEvent(this[kSource].output, 'darin');
}
const writePromise = promisify((data, callback) =>
this[kSource].output.write(data, 'utf-8', callback));
await writePromise(data);
}
_construct(callback) {
callback();
}
_write(chunk, encoding, callback) {
if (!(chunk instanceof Buffer)) {
chunk = Buffer.from(chunk, encoding);
}
chunk = Buffer.concat([Buffer.from([0x02]), chunk]);
this.#sendData(chunk).then(() => callback(null)).catch(err => callback(err))
}
_destroy(err, callback) {
this[kSource].input.destory(err);
this[kSource].output.destory(err);
callback();
}
_read(_size) {
this[kSource].input.resume();
}
setRawMode() {
return this;
}
get isRaw() {
return true;
}
get isTTY() {
return true;
}
ttyReady() {
return this.#tty_ready;
}
};