@skitee3000/bun-pty
Version:
Cross-platform pseudoterminal (PTY) implementation for Bun with native performance
177 lines (171 loc) • 4.87 kB
JavaScript
// @bun
// src/terminal.ts
import { dlopen, FFIType, ptr } from "bun:ffi";
import { Buffer } from "buffer";
// src/interfaces.ts
class EventEmitter {
listeners = [];
event = (listener) => {
this.listeners.push(listener);
return {
dispose: () => {
const i = this.listeners.indexOf(listener);
if (i !== -1) {
this.listeners.splice(i, 1);
}
}
};
};
fire(data) {
for (const listener of this.listeners) {
listener(data);
}
}
}
// src/terminal.ts
import { join } from "path";
import { existsSync } from "fs";
var DEFAULT_COLS = 80;
var DEFAULT_ROWS = 24;
var DEFAULT_FILE = "sh";
var DEFAULT_NAME = "xterm";
function resolveLibPath() {
const env = process.env.BUN_PTY_LIB;
if (env && existsSync(env))
return env;
const platform = process.platform;
const arch = process.arch;
const filename = platform === "darwin" ? arch === "arm64" ? "librust_pty_arm64.dylib" : "librust_pty.dylib" : platform === "win32" ? "rust_pty.dll" : arch === "arm64" ? "librust_pty_arm64.so" : "librust_pty.so";
const base = Bun.fileURLToPath(import.meta.url);
const here = base.replace(/[\/\\]dist[\/\\].*$/, "");
const fallbackPaths = [
join(here, "rust-pty", "target", "release", filename),
join(here, "..", "@skitee3000/bun-pty", "rust-pty", "target", "release", filename),
join(process.cwd(), "node_modules", "@skitee3000/bun-pty", "rust-pty", "target", "release", filename)
];
for (const path of fallbackPaths) {
if (existsSync(path))
return path;
}
throw new Error(`librust_pty shared library not found.
Checked:
- BUN_PTY_LIB=${env ?? "<unset>"}
- ${fallbackPaths.join(`
- `)}
Set BUN_PTY_LIB or ensure one of these paths contains the file.`);
}
var libPath = resolveLibPath();
var lib;
try {
lib = dlopen(libPath, {
bun_pty_spawn: {
args: [FFIType.cstring, FFIType.cstring, FFIType.i32, FFIType.i32],
returns: FFIType.i32
},
bun_pty_write: {
args: [FFIType.i32, FFIType.pointer, FFIType.i32],
returns: FFIType.i32
},
bun_pty_read: {
args: [FFIType.i32, FFIType.pointer, FFIType.i32],
returns: FFIType.i32
},
bun_pty_resize: {
args: [FFIType.i32, FFIType.i32, FFIType.i32],
returns: FFIType.i32
},
bun_pty_kill: { args: [FFIType.i32], returns: FFIType.i32 },
bun_pty_get_pid: { args: [FFIType.i32], returns: FFIType.i32 },
bun_pty_close: { args: [FFIType.i32], returns: FFIType.void }
});
} catch (error) {
console.error("Failed to load lib", error);
}
class Terminal {
handle = -1;
_pid = -1;
_cols = DEFAULT_COLS;
_rows = DEFAULT_ROWS;
_name = DEFAULT_NAME;
_readLoop = false;
_closing = false;
_onData = new EventEmitter;
_onExit = new EventEmitter;
constructor(file = DEFAULT_FILE, args = [], opts = { name: DEFAULT_NAME }) {
this._cols = opts.cols ?? DEFAULT_COLS;
this._rows = opts.rows ?? DEFAULT_ROWS;
const cwd = opts.cwd ?? process.cwd();
const cmdline = [file, ...args].join(" ");
this.handle = lib.symbols.bun_pty_spawn(Buffer.from(`${cmdline}\x00`, "utf8"), Buffer.from(`${cwd}\x00`, "utf8"), this._cols, this._rows);
if (this.handle < 0)
throw new Error("PTY spawn failed");
this._pid = lib.symbols.bun_pty_get_pid(this.handle);
this._startReadLoop();
}
get pid() {
return this._pid;
}
get cols() {
return this._cols;
}
get rows() {
return this._rows;
}
get process() {
return "shell";
}
get onData() {
return this._onData.event;
}
get onExit() {
return this._onExit.event;
}
write(data) {
if (this._closing)
return;
const buf = Buffer.from(data, "utf8");
lib.symbols.bun_pty_write(this.handle, ptr(buf), buf.length);
}
resize(cols, rows) {
if (this._closing)
return;
this._cols = cols;
this._rows = rows;
lib.symbols.bun_pty_resize(this.handle, cols, rows);
}
kill(signal = "SIGTERM") {
if (this._closing)
return;
this._closing = true;
lib.symbols.bun_pty_kill(this.handle);
lib.symbols.bun_pty_close(this.handle);
this._onExit.fire({ exitCode: 0, signal });
}
async _startReadLoop() {
if (this._readLoop)
return;
this._readLoop = true;
const buf = Buffer.allocUnsafe(4096);
while (this._readLoop && !this._closing) {
const n = lib.symbols.bun_pty_read(this.handle, ptr(buf), buf.length);
if (n > 0) {
this._onData.fire(buf.subarray(0, n).toString("utf8"));
} else if (n === -2) {
this._onExit.fire({ exitCode: 0 });
break;
} else if (n < 0) {
break;
} else {
await new Promise((r) => setTimeout(r, 8));
}
}
}
}
// src/index.ts
function spawn(file, args, options) {
return new Terminal(file, args, options);
}
export {
spawn,
Terminal
};