From 7b848af6efaa3b261e6f3004c39540b271cdfe36 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Sat, 31 Jan 2026 22:17:05 +0000 Subject: [PATCH] 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 --- src/bun.js/bindings/c-bindings.cpp | 16 +++++++++++ test/regression/issue/26642.test.ts | 41 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 test/regression/issue/26642.test.ts diff --git a/src/bun.js/bindings/c-bindings.cpp b/src/bun.js/bindings/c-bindings.cpp index 71c3e46c79..8d12608f3e 100644 --- a/src/bun.js/bindings/c-bindings.cpp +++ b/src/bun.js/bindings/c-bindings.cpp @@ -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]) diff --git a/test/regression/issue/26642.test.ts b/test/regression/issue/26642.test.ts new file mode 100644 index 0000000000..3e1f592f18 --- /dev/null +++ b/test/regression/issue/26642.test.ts @@ -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); +});