mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 04:18:58 +00:00
feat(spawn): finalize PTY support with docs and tests
- Add comprehensive PTY documentation with examples: - Basic usage - Multiple PTY streams (stdin/stdout/stderr) - Custom terminal dimensions (width/height) - Colored output from git, grep, etc. - Platform support table - Limitations section - Make PTY fall back to pipe on Windows (instead of error) - Add spawnSync error for PTY (not supported) - Add tests for spawnSync PTY error 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -117,7 +117,13 @@ Configure the output stream by passing one of the following values to `stdout/st
|
||||
|
||||
## Pseudo-terminal (PTY)
|
||||
|
||||
Use `"pty"` to run a process in a pseudo-terminal, making it think it's running in an interactive terminal. This is useful for programs that change their behavior based on whether they're running in a TTY (e.g., colored output, interactive prompts).
|
||||
Use `"pty"` to spawn a process with a pseudo-terminal, making the child process believe it's running in an interactive terminal. This causes `process.stdout.isTTY` to be `true` in the child, which is useful for:
|
||||
|
||||
- Getting colored output from CLI tools that detect TTY
|
||||
- Running interactive programs that require a terminal
|
||||
- Testing terminal-dependent behavior
|
||||
|
||||
### Basic usage
|
||||
|
||||
```ts
|
||||
const proc = Bun.spawn(["ls", "--color=auto"], {
|
||||
@@ -126,24 +132,94 @@ const proc = Bun.spawn(["ls", "--color=auto"], {
|
||||
|
||||
// The child process sees process.stdout.isTTY === true
|
||||
const output = await proc.stdout.text();
|
||||
console.log(output); // colored output
|
||||
console.log(output); // includes ANSI color codes
|
||||
```
|
||||
|
||||
You can use PTY for stdin, stdout, and stderr. When multiple streams use PTY, they share the same underlying terminal:
|
||||
### Checking isTTY in child process
|
||||
|
||||
```ts
|
||||
const proc = Bun.spawn(["node", "-e", "console.log(process.stdout.isTTY, process.stdin.isTTY)"], {
|
||||
stdin: "pty",
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["bun", "-e", "console.log('isTTY:', process.stdout.isTTY)"],
|
||||
stdout: "pty",
|
||||
});
|
||||
|
||||
const output = await proc.stdout.text();
|
||||
console.log(output); // "true true"
|
||||
console.log(output); // "isTTY: true"
|
||||
```
|
||||
|
||||
<Note>
|
||||
PTY is only supported on macOS and Linux. On Windows, passing `"pty"` will throw an error.
|
||||
</Note>
|
||||
### Multiple PTY streams
|
||||
|
||||
You can use PTY for stdin, stdout, and/or stderr. When multiple streams use PTY, they share the same underlying pseudo-terminal:
|
||||
|
||||
```ts
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["bun", "-e", `
|
||||
console.log("stdout.isTTY:", process.stdout.isTTY);
|
||||
console.log("stdin.isTTY:", process.stdin.isTTY);
|
||||
console.log("stderr.isTTY:", process.stderr.isTTY);
|
||||
`],
|
||||
stdin: "pty",
|
||||
stdout: "pty",
|
||||
stderr: "pty",
|
||||
});
|
||||
|
||||
const output = await proc.stdout.text();
|
||||
// stdout.isTTY: true
|
||||
// stdin.isTTY: true
|
||||
// stderr.isTTY: true
|
||||
```
|
||||
|
||||
### Custom terminal dimensions
|
||||
|
||||
Specify terminal width and height using the object syntax:
|
||||
|
||||
```ts
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["bun", "-e", `
|
||||
console.log("columns:", process.stdout.columns);
|
||||
console.log("rows:", process.stdout.rows);
|
||||
`],
|
||||
stdout: {
|
||||
type: "pty",
|
||||
width: 120, // columns
|
||||
height: 40, // rows
|
||||
},
|
||||
});
|
||||
|
||||
const output = await proc.stdout.text();
|
||||
// columns: 120
|
||||
// rows: 40
|
||||
```
|
||||
|
||||
### Getting colored output from git, grep, etc.
|
||||
|
||||
Many CLI tools detect whether they're running in a TTY and only emit colors when they are:
|
||||
|
||||
```ts
|
||||
// Without PTY - no colors
|
||||
const noColor = Bun.spawn(["git", "status"], { stdout: "pipe" });
|
||||
console.log(await noColor.stdout.text()); // plain text
|
||||
|
||||
// With PTY - colors enabled
|
||||
const withColor = Bun.spawn(["git", "status"], { stdout: "pty" });
|
||||
console.log(await withColor.stdout.text()); // includes ANSI color codes
|
||||
```
|
||||
|
||||
### Platform support
|
||||
|
||||
| Platform | PTY Support |
|
||||
|----------|-------------|
|
||||
| macOS | ✅ Full support |
|
||||
| Linux | ✅ Full support |
|
||||
| Windows | ⚠️ Falls back to `"pipe"` (no TTY semantics) |
|
||||
|
||||
On Windows, `"pty"` silently falls back to `"pipe"` behavior. The child process will see `process.stdout.isTTY` as `undefined`. This allows cross-platform code to work without errors, though TTY-dependent features won't work on Windows.
|
||||
|
||||
### Limitations
|
||||
|
||||
- **Not supported with `spawnSync`**: PTY requires asynchronous I/O. Using `"pty"` with `Bun.spawnSync()` will throw an error.
|
||||
- **Line endings**: PTY converts `\n` to `\r\n` on output (standard terminal behavior).
|
||||
- **No dynamic resize**: Terminal dimensions are set at spawn time and cannot be changed after.
|
||||
|
||||
## Exit handling
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ pub const Stdio = union(enum) {
|
||||
stdin_used_as_out,
|
||||
out_used_as_stdin,
|
||||
blob_used_as_out,
|
||||
pty_not_supported,
|
||||
uv_pipe: bun.sys.E,
|
||||
|
||||
pub fn toStr(this: *const @This()) []const u8 {
|
||||
@@ -51,7 +50,6 @@ pub const Stdio = union(enum) {
|
||||
.stdin_used_as_out => "Stdin cannot be used for stdout or stderr",
|
||||
.out_used_as_stdin => "Stdout and stderr cannot be used for stdin",
|
||||
.blob_used_as_out => "Blobs are immutable, and cannot be used for stdout/stderr",
|
||||
.pty_not_supported => "PTY is not supported on Windows",
|
||||
.uv_pipe => @panic("TODO"),
|
||||
};
|
||||
}
|
||||
@@ -257,7 +255,7 @@ pub const Stdio = union(enum) {
|
||||
.path => |pathlike| .{ .path = pathlike.slice() },
|
||||
.inherit => .{ .inherit = {} },
|
||||
.ignore => .{ .ignore = {} },
|
||||
.pty => return .{ .err = .pty_not_supported },
|
||||
.pty => .{ .buffer = bun.handleOom(bun.default_allocator.create(uv.Pipe)) }, // PTY falls back to pipe on Windows
|
||||
|
||||
.memfd => @panic("This should never happen"),
|
||||
},
|
||||
@@ -361,10 +359,15 @@ pub const Stdio = union(enum) {
|
||||
} else if (str.eqlComptime("ipc")) {
|
||||
out_stdio.* = Stdio{ .ipc = {} };
|
||||
} else if (str.eqlComptime("pty")) {
|
||||
if (comptime Environment.isWindows) {
|
||||
return globalThis.throwInvalidArguments("PTY is not supported on Windows", .{});
|
||||
if (is_sync) {
|
||||
return globalThis.throwInvalidArguments("PTY is not supported with spawnSync", .{});
|
||||
}
|
||||
// On Windows, PTY falls back to pipe (no real PTY support)
|
||||
if (comptime Environment.isWindows) {
|
||||
out_stdio.* = Stdio{ .pipe = {} };
|
||||
} else {
|
||||
out_stdio.* = Stdio{ .pty = .{} };
|
||||
}
|
||||
out_stdio.* = Stdio{ .pty = .{} };
|
||||
} else {
|
||||
return globalThis.throwInvalidArguments("stdio must be an array of 'inherit', 'pipe', 'ignore', 'pty', Bun.file(pathOrFd), number, or null", .{});
|
||||
}
|
||||
@@ -461,8 +464,13 @@ pub const Stdio = union(enum) {
|
||||
if (type_val.isString()) {
|
||||
const type_str = try type_val.getZigString(globalThis);
|
||||
if (type_str.eqlComptime("pty")) {
|
||||
if (is_sync) {
|
||||
return globalThis.throwInvalidArguments("PTY is not supported with spawnSync", .{});
|
||||
}
|
||||
// On Windows, PTY falls back to pipe (no real PTY support)
|
||||
if (comptime Environment.isWindows) {
|
||||
return globalThis.throwInvalidArguments("PTY is not supported on Windows", .{});
|
||||
out_stdio.* = Stdio{ .pipe = {} };
|
||||
return;
|
||||
}
|
||||
var pty_opts: PtyOptions = .{};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows } from "harness";
|
||||
import { bunEnv, bunExe } from "harness";
|
||||
|
||||
describe("Bun.spawn with PTY", () => {
|
||||
test("stdout: 'pty' makes process.stdout.isTTY true", async () => {
|
||||
@@ -146,13 +146,22 @@ describe("Bun.spawn with PTY", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.if(isWindows)("Bun.spawn PTY on Windows", () => {
|
||||
test("throws error when PTY is used on Windows", () => {
|
||||
describe("Bun.spawnSync with PTY", () => {
|
||||
test("throws error when PTY is used with spawnSync", () => {
|
||||
expect(() => {
|
||||
Bun.spawn({
|
||||
Bun.spawnSync({
|
||||
cmd: ["echo", "test"],
|
||||
stdout: "pty",
|
||||
});
|
||||
}).toThrow("PTY is not supported on Windows");
|
||||
}).toThrow("PTY is not supported with spawnSync");
|
||||
});
|
||||
|
||||
test("throws error when PTY object syntax is used with spawnSync", () => {
|
||||
expect(() => {
|
||||
Bun.spawnSync({
|
||||
cmd: ["echo", "test"],
|
||||
stdout: { type: "pty", width: 80, height: 24 },
|
||||
});
|
||||
}).toThrow("PTY is not supported with spawnSync");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user