UNPKG

@skitee3000/bun-pty

Version:

Cross-platform pseudoterminal (PTY) implementation for Bun with native performance

177 lines (171 loc) 4.87 kB
// @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 };