mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
feat: add Bun.Terminal API for pseudo-terminal (PTY) support (#25415)
## Summary
This PR adds a new `Bun.Terminal` API for creating and managing
pseudo-terminals (PTYs), enabling interactive terminal applications in
Bun.
### Features
- **Standalone Terminal**: Create PTYs directly with `new
Bun.Terminal(options)`
- **Spawn Integration**: Spawn processes with PTY attached via
`Bun.spawn({ terminal: options })`
- **Full PTY Control**: Write data, resize, set raw mode, and handle
callbacks
## Examples
### Basic Terminal with Spawn (Recommended)
```typescript
const proc = Bun.spawn(["bash"], {
terminal: {
cols: 80,
rows: 24,
data(terminal, data) {
// Handle output from the terminal
process.stdout.write(data);
},
exit(terminal, code, signal) {
console.log(`Process exited with code ${code}`);
},
},
});
// Write commands to the terminal
proc.terminal.write("echo Hello from PTY!\n");
proc.terminal.write("exit\n");
await proc.exited;
proc.terminal.close();
```
### Interactive Shell
```typescript
// Create an interactive shell that mirrors to stdout
const proc = Bun.spawn(["bash", "-i"], {
terminal: {
cols: process.stdout.columns || 80,
rows: process.stdout.rows || 24,
data(term, data) {
process.stdout.write(data);
},
},
});
// Forward stdin to the terminal
process.stdin.setRawMode(true);
for await (const chunk of process.stdin) {
proc.terminal.write(chunk);
}
```
### Running Interactive Programs (vim, htop, etc.)
```typescript
const proc = Bun.spawn(["vim", "file.txt"], {
terminal: {
cols: process.stdout.columns,
rows: process.stdout.rows,
data(term, data) {
process.stdout.write(data);
},
},
});
// Handle terminal resize
process.stdout.on("resize", () => {
proc.terminal.resize(process.stdout.columns, process.stdout.rows);
});
// Forward input
process.stdin.setRawMode(true);
for await (const chunk of process.stdin) {
proc.terminal.write(chunk);
}
```
### Capturing Colored Output
```typescript
const chunks: Uint8Array[] = [];
const proc = Bun.spawn(["ls", "--color=always"], {
terminal: {
data(term, data) {
chunks.push(data);
},
},
});
await proc.exited;
proc.terminal.close();
// Output includes ANSI color codes
const output = Buffer.concat(chunks).toString();
console.log(output);
```
### Standalone Terminal (Advanced)
```typescript
const terminal = new Bun.Terminal({
cols: 80,
rows: 24,
data(term, data) {
console.log("Received:", data.toString());
},
});
// Use terminal.stdin as the fd for child process stdio
const proc = Bun.spawn(["bash"], {
stdin: terminal.stdin,
stdout: terminal.stdin,
stderr: terminal.stdin,
});
terminal.write("echo hello\n");
// Clean up
terminal.close();
```
### Testing TTY Detection
```typescript
const proc = Bun.spawn([
"bun", "-e",
"console.log('isTTY:', process.stdout.isTTY)"
], {
terminal: {},
});
// Output: isTTY: true
```
## API
### `Bun.spawn()` with `terminal` option
```typescript
const proc = Bun.spawn(cmd, {
terminal: {
cols?: number, // Default: 80
rows?: number, // Default: 24
name?: string, // Default: "xterm-256color"
data?: (terminal: Terminal, data: Uint8Array) => void,
exit?: (terminal: Terminal, code: number, signal: string | null) => void,
drain?: (terminal: Terminal) => void,
}
});
// Access the terminal
proc.terminal.write(data);
proc.terminal.resize(cols, rows);
proc.terminal.setRawMode(enabled);
proc.terminal.close();
// Note: proc.stdin, proc.stdout, proc.stderr return null when terminal is used
```
### `new Bun.Terminal(options)`
```typescript
const terminal = new Bun.Terminal({
cols?: number,
rows?: number,
name?: string,
data?: (terminal, data) => void,
exit?: (terminal, code, signal) => void,
drain?: (terminal) => void,
});
terminal.stdin; // Slave fd (for child process)
terminal.stdout; // Master fd (for reading)
terminal.closed; // boolean
terminal.write(data);
terminal.resize(cols, rows);
terminal.setRawMode(enabled);
terminal.ref();
terminal.unref();
terminal.close();
await terminal[Symbol.asyncDispose]();
```
## Implementation Details
- Uses `openpty()` to create pseudo-terminal pairs
- Properly manages file descriptor lifecycle with reference counting
- Integrates with Bun's event loop via `BufferedReader` and
`StreamingWriter`
- Supports `await using` syntax for automatic cleanup
- POSIX only (Linux, macOS) - not available on Windows
## Test Results
- 80 tests passing
- Covers: construction, writing, reading, resize, raw mode, callbacks,
spawn integration, error handling, GC safety
## Changes
- `src/bun.js/api/bun/Terminal.zig` - Terminal implementation
- `src/bun.js/api/bun/Terminal.classes.ts` - Class definition for
codegen
- `src/bun.js/api/bun/subprocess.zig` - Added terminal field and getter
- `src/bun.js/api/bun/js_bun_spawn_bindings.zig` - Terminal option
parsing
- `src/bun.js/api/BunObject.classes.ts` - Terminal getter on Subprocess
- `packages/bun-types/bun.d.ts` - TypeScript types
- `docs/runtime/child-process.mdx` - Documentation
- `test/js/bun/terminal/terminal.test.ts` - Comprehensive tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
204
packages/bun-types/bun.d.ts
vendored
204
packages/bun-types/bun.d.ts
vendored
@@ -5697,6 +5697,44 @@ declare module "bun" {
|
||||
* ```
|
||||
*/
|
||||
lazy?: boolean;
|
||||
|
||||
/**
|
||||
* Spawn the subprocess with a pseudo-terminal (PTY) attached.
|
||||
*
|
||||
* When this option is provided:
|
||||
* - `stdin`, `stdout`, and `stderr` are all connected to the terminal
|
||||
* - The subprocess sees itself running in a real terminal (`isTTY = true`)
|
||||
* - Access the terminal via `subprocess.terminal`
|
||||
* - `subprocess.stdin`, `subprocess.stdout`, `subprocess.stderr` return `null`
|
||||
*
|
||||
* Only available on POSIX systems (Linux, macOS).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const proc = Bun.spawn(["bash"], {
|
||||
* terminal: {
|
||||
* cols: 80,
|
||||
* rows: 24,
|
||||
* data: (term, data) => console.log(data.toString()),
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* proc.terminal.write("echo hello\n");
|
||||
* await proc.exited;
|
||||
* proc.terminal.close();
|
||||
* ```
|
||||
*
|
||||
* You can also pass an existing `Terminal` object for reuse across multiple spawns:
|
||||
* ```ts
|
||||
* const terminal = new Bun.Terminal({ ... });
|
||||
* const proc1 = Bun.spawn(["echo", "first"], { terminal });
|
||||
* await proc1.exited;
|
||||
* const proc2 = Bun.spawn(["echo", "second"], { terminal });
|
||||
* await proc2.exited;
|
||||
* terminal.close();
|
||||
* ```
|
||||
*/
|
||||
terminal?: TerminalOptions | Terminal;
|
||||
}
|
||||
|
||||
type ReadableToIO<X extends Readable> = X extends "pipe" | undefined
|
||||
@@ -5811,6 +5849,24 @@ declare module "bun" {
|
||||
readonly stdout: SpawnOptions.ReadableToIO<Out>;
|
||||
readonly stderr: SpawnOptions.ReadableToIO<Err>;
|
||||
|
||||
/**
|
||||
* The terminal attached to this subprocess, if spawned with the `terminal` option.
|
||||
* Returns `undefined` if no terminal was attached.
|
||||
*
|
||||
* When a terminal is attached, `stdin`, `stdout`, and `stderr` return `null`.
|
||||
* Use `terminal.write()` and the `data` callback instead.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const proc = Bun.spawn(["bash"], {
|
||||
* terminal: { data: (term, data) => console.log(data.toString()) },
|
||||
* });
|
||||
*
|
||||
* proc.terminal?.write("echo hello\n");
|
||||
* ```
|
||||
*/
|
||||
readonly terminal: Terminal | undefined;
|
||||
|
||||
/**
|
||||
* Access extra file descriptors passed to the `stdio` option in the options object.
|
||||
*/
|
||||
@@ -6102,6 +6158,154 @@ declare module "bun" {
|
||||
"ignore" | "inherit" | null | undefined
|
||||
>;
|
||||
|
||||
/**
|
||||
* Options for creating a pseudo-terminal (PTY).
|
||||
*/
|
||||
interface TerminalOptions {
|
||||
/**
|
||||
* Number of columns for the terminal.
|
||||
* @default 80
|
||||
*/
|
||||
cols?: number;
|
||||
/**
|
||||
* Number of rows for the terminal.
|
||||
* @default 24
|
||||
*/
|
||||
rows?: number;
|
||||
/**
|
||||
* Terminal name (e.g., "xterm-256color").
|
||||
* @default "xterm-256color"
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Callback invoked when data is received from the terminal.
|
||||
* @param terminal The terminal instance
|
||||
* @param data The data received as a Uint8Array
|
||||
*/
|
||||
data?: (terminal: Terminal, data: Uint8Array<ArrayBuffer>) => void;
|
||||
/**
|
||||
* Callback invoked when the PTY stream closes (EOF or read error).
|
||||
* Note: exitCode is a PTY lifecycle status (0=clean EOF, 1=error), NOT the subprocess exit code.
|
||||
* Use Subprocess.exited or onExit callback for actual process exit information.
|
||||
* @param terminal The terminal instance
|
||||
* @param exitCode PTY lifecycle status (0 for EOF, 1 for error)
|
||||
* @param signal Reserved for future signal reporting, currently null
|
||||
*/
|
||||
exit?: (terminal: Terminal, exitCode: number, signal: string | null) => void;
|
||||
/**
|
||||
* Callback invoked when the terminal is ready to receive more data.
|
||||
* @param terminal The terminal instance
|
||||
*/
|
||||
drain?: (terminal: Terminal) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A pseudo-terminal (PTY) that can be used to spawn interactive terminal programs.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* await using terminal = new Bun.Terminal({
|
||||
* cols: 80,
|
||||
* rows: 24,
|
||||
* data(term, data) {
|
||||
* console.log("Received:", new TextDecoder().decode(data));
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* // Spawn a shell connected to the PTY
|
||||
* const proc = Bun.spawn(["bash"], { terminal });
|
||||
*
|
||||
* // Write to the terminal
|
||||
* terminal.write("echo hello\n");
|
||||
*
|
||||
* // Wait for process to exit
|
||||
* await proc.exited;
|
||||
*
|
||||
* // Terminal is closed automatically by `await using`
|
||||
* ```
|
||||
*/
|
||||
class Terminal implements AsyncDisposable {
|
||||
constructor(options: TerminalOptions);
|
||||
|
||||
/**
|
||||
* Whether the terminal is closed.
|
||||
*/
|
||||
readonly closed: boolean;
|
||||
|
||||
/**
|
||||
* Write data to the terminal.
|
||||
* @param data The data to write (string or BufferSource)
|
||||
* @returns The number of bytes written
|
||||
*/
|
||||
write(data: string | BufferSource): number;
|
||||
|
||||
/**
|
||||
* Resize the terminal.
|
||||
* @param cols New number of columns
|
||||
* @param rows New number of rows
|
||||
*/
|
||||
resize(cols: number, rows: number): void;
|
||||
|
||||
/**
|
||||
* Set raw mode on the terminal.
|
||||
* In raw mode, input is passed directly without processing.
|
||||
* @param enabled Whether to enable raw mode
|
||||
*/
|
||||
setRawMode(enabled: boolean): void;
|
||||
|
||||
/**
|
||||
* Reference the terminal to keep the event loop alive.
|
||||
*/
|
||||
ref(): void;
|
||||
|
||||
/**
|
||||
* Unreference the terminal to allow the event loop to exit.
|
||||
*/
|
||||
unref(): void;
|
||||
|
||||
/**
|
||||
* Close the terminal.
|
||||
*/
|
||||
close(): void;
|
||||
|
||||
/**
|
||||
* Async dispose for use with `await using`.
|
||||
*/
|
||||
[Symbol.asyncDispose](): Promise<void>;
|
||||
|
||||
/**
|
||||
* Terminal input flags (c_iflag from termios).
|
||||
* Controls input processing behavior like ICRNL, IXON, etc.
|
||||
* Returns 0 if terminal is closed.
|
||||
* Setting returns true on success, false on failure.
|
||||
*/
|
||||
inputFlags: number;
|
||||
|
||||
/**
|
||||
* Terminal output flags (c_oflag from termios).
|
||||
* Controls output processing behavior like OPOST, ONLCR, etc.
|
||||
* Returns 0 if terminal is closed.
|
||||
* Setting returns true on success, false on failure.
|
||||
*/
|
||||
outputFlags: number;
|
||||
|
||||
/**
|
||||
* Terminal local flags (c_lflag from termios).
|
||||
* Controls local processing like ICANON, ECHO, ISIG, etc.
|
||||
* Returns 0 if terminal is closed.
|
||||
* Setting returns true on success, false on failure.
|
||||
*/
|
||||
localFlags: number;
|
||||
|
||||
/**
|
||||
* Terminal control flags (c_cflag from termios).
|
||||
* Controls hardware characteristics like CSIZE, PARENB, etc.
|
||||
* Returns 0 if terminal is closed.
|
||||
* Setting returns true on success, false on failure.
|
||||
*/
|
||||
controlFlags: number;
|
||||
}
|
||||
|
||||
// Blocked on https://github.com/oven-sh/bun/issues/8329
|
||||
// /**
|
||||
// *
|
||||
|
||||
Reference in New Issue
Block a user