Bubble Tea's input reader races the tmux attach reader for stdin

Author: emersonthisCreated Aug 28, 2026Updated Aug 28, 2026

Summary

While a session is attached, two goroutines read os.Stdin concurrently. The impact is small and bounded, but it silently swallows input and it makes the terminal handoff on attach non-deterministic.

The two readers

Bubble Tea's reader. app.Run (app/app.go) creates the program and never releases the terminal for the lifetime of the app. Bubble Tea starts its own reader goroutine ([email protected]/tty.go:91, go p.readLoop()), which sits in input.Read(buf[:256]) (key.go:565) on os.Stdin.

The tmux attach reader. TmuxSession.Attach (session/tmux/tmux.go) spawns a goroutine that also reads os.Stdin and forwards to the session PTY.

They overlap because the attach callback runs synchronously on the event-loop goroutine and blocks until detach: handleKeyPressshowHelpScreen(...) with an onDismiss closure (app/app.go), invoked directly at app/help.go:159-161 (or via ui/overlay/textOverlay.go:30-38 on first attach), which then blocks on <-ch. So Update never returns while a session is attached.

Why the impact is bounded

p.msgs is unbuffered (tea.go:236) and Update is its only receiver (tea.go:508). So the sequence is:

  1. Bubble Tea's reader wins one race, reads a chunk of stdin, parses it, and blocks forever on msgs <- msg (key.go:595-603).
  2. From that point it is parked and never reads stdin again until detach.

Net effect: roughly one stdin chunk swallowed per attach — an occasional lost first keystroke right after attaching. Not catastrophic, but it is also part of why the // Nuke the first bytes of stdin 50 ms heuristic in Attach exists: some of the query replies it discards are answers to queries the outer app issued, which a clean handoff would drain deterministically instead of by timer.

Suggested fix

tea.Exec ([email protected]/exec.go:22-26, 58-65) is Bubble Tea's sanctioned mechanism — it releases the terminal, runs the child, and restores, all inside the event loop.

I'd avoid calling ReleaseTerminal/RestoreTerminal directly, for what it's worth:

  • ReleaseTerminal takes stdin out of raw mode (tea.go:779tty.go:41-60), and nothing else in cs sets raw mode, so the manual PTY relay would break.
  • It exits the alt screen, so every attach and detach would flicker.
  • app.Run discards the *tea.Program, so it isn't reachable from the model without plumbing.
  • Calling it from inside Update risks waitForReadLoop (tty.go:109-117) stalling 500 ms and then RestoreTerminal starting a second readLoop while the first is still parked on the send — trading one racing reader for two.

Happy to attempt a patch if you have a preference on the approach. Filing this separately from the Ctrl+Q detach fix, which stands on its own.