198 lines
4.2 KiB
JavaScript
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;
|
|
}
|
|
};
|