From b1a21760cbc11d9ae28cd1426a72fd18a3a62a77 Mon Sep 17 00:00:00 2001 From: ilanis Date: Tue, 23 Jun 2026 12:03:45 +0300 Subject: [PATCH] fix: drain OSC response when cursor position reply arrives first In some terminals (iTerm2, Terminal.app on macOS) the cursor position response to CSI 6n arrives before the OSC 11 background-color response. termStatusReport detected a non-OSC reply and returned early, but re-enabled TTY echo (via the deferred raw-mode restore) before the OSC response had arrived. The OSC response then arrived with echo ON and was: 1. echoed directly to the terminal display as garbled output, and 2. left in the shell's input buffer, appearing as a phantom shell command after the process exited. Fix: when the first response is not an OSC reply, poll for up to 100 ms for a pending OSC response and drain it (including the trailing backslash of the ESC-backslash string terminator) before returning ErrStatusReport. The 100 ms window is generous enough for the response to arrive over a local Unix PTY without adding noticeable latency on terminals that truly don't support OSC 11. Reproducer: any program that calls HasDarkBackground() during package init (e.g. charmbracelet/bubbletea v1 does this in tea_init.go) while running in iTerm2 or Terminal.app. --- termenv_unix.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/termenv_unix.go b/termenv_unix.go index bef49ca..9a0875f 100644 --- a/termenv_unix.go +++ b/termenv_unix.go @@ -277,8 +277,23 @@ func (o Output) termStatusReport(sequence int) (string, error) { return "", fmt.Errorf("%s: %s", ErrStatusReport, err) } - // if this is not OSC response, then the terminal does not support it + // if this is not OSC response, then the cursor position response arrived first. + // The terminal may still send the OSC response; drain it so it doesn't leak + // into the TTY buffer and appear as garbage output or a phantom shell command. if !isOSC { + if tty := o.TTY(); tty != nil { + fd := int(tty.Fd()) + tv := unix.NsecToTimeval(int64(100 * time.Millisecond)) + var rfds unix.FdSet + rfds.Set(fd) + if n, _ := unix.Select(fd+1, &rfds, nil, nil, &tv); n > 0 { + drained, _, _ := o.readNextResponse() + // readNextResponse stops at ESC (first byte of ST = ESC+\); consume the trailing \. + if strings.HasSuffix(drained, string(ESC)) { + _, _ = o.readNextByte() + } + } + } return "", ErrStatusReport }