fix(tty): restore cursor visibility on process exit

When stdout is a TTY, write the cursor-show escape sequence (\x1b[?25h)
in bun_restore_stdio() before restoring termios settings.

Many CLI applications (like Ink) hide the cursor during operation and
rely on cleanup handlers to restore it. On macOS, if the process exits
before the cursor-show sequence is flushed, the cursor remains invisible
in the terminal. This fix ensures the cursor is always visible after Bun
exits, regardless of how the process terminates.

Fixes #26642

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-31 22:17:05 +00:00
parent a14a89ca95
commit 7b848af6ef
2 changed files with 57 additions and 0 deletions

View File

@@ -394,6 +394,22 @@ extern "C" void bun_restore_stdio()
#if !OS(WINDOWS)
// Restore cursor visibility on TTY before exiting.
// Many CLI applications (like Ink) hide the cursor during operation and rely on
// cleanup handlers to restore it. If the process exits before the cursor-show
// escape sequence is flushed, the cursor remains invisible in the terminal.
// Writing the cursor-show sequence here ensures the cursor is always visible
// after Bun exits, regardless of how the process terminates.
// See: https://github.com/oven-sh/bun/issues/26642
if (bun_stdio_tty[STDOUT_FILENO]) {
// Show cursor: CSI ? 25 h
const char show_cursor[] = "\x1b[?25h";
ssize_t ret;
do {
ret = write(STDOUT_FILENO, show_cursor, sizeof(show_cursor) - 1);
} while (ret == -1 && errno == EINTR);
}
// restore stdio
for (int32_t fd = 0; fd < 3; fd++) {
if (!bun_stdio_tty[fd])

View File

@@ -0,0 +1,41 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, isMacOS, isWindows } from "harness";
// Test that cursor visibility is restored on process exit when stdout is a TTY.
// This is needed because CLI applications like Ink hide the cursor during operation
// and rely on cleanup handlers to restore it. If the process exits before the
// cursor-show escape sequence is flushed, the cursor remains invisible.
// See: https://github.com/oven-sh/bun/issues/26642
// Skip on Windows and non-macOS - the script command behavior varies and CI
// environments often don't provide proper PTY support. The actual fix is
// most critical for macOS terminals where users reported the issue.
test.skipIf(isWindows || !isMacOS)("cursor visibility is restored on exit when stdout is TTY", async () => {
// Check if script command is available (needed to create a PTY)
const hasScript = Bun.which("script");
if (!hasScript) {
console.log("Skipping test: requires 'script' command for PTY simulation");
return;
}
// Script that just exits immediately - cursor restore should happen automatically
const testScript = `process.exit(0);`;
// Use script command to provide a PTY environment (macOS syntax)
const scriptCmd = ["script", "-q", "/dev/null", bunExe(), "-e", testScript];
await using proc = Bun.spawn({
cmd: scriptCmd,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The cursor-show escape sequence is \x1b[?25h
// It should be present in stdout when running in a TTY
const cursorShow = "\x1b[?25h";
expect(stdout).toContain(cursorShow);
expect(exitCode).toBe(0);
});