Skip to content

fix(tea): fall back to 80x24 when initial GetSize returns 0 (blank first frame under tmux) - #1718

Open
Ricardo-M-L wants to merge 5 commits into
charmbracelet:mainfrom
Ricardo-M-L:fix/zero-size-blank-first-frame
Open

fix(tea): fall back to 80x24 when initial GetSize returns 0 (blank first frame under tmux)#1718
Ricardo-M-L wants to merge 5 commits into
charmbracelet:mainfrom
Ricardo-M-L:fix/zero-size-blank-first-frame

Conversation

@Ricardo-M-L

@Ricardo-M-L Ricardo-M-L commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Problem

Launching a Bubble Tea v2 program inside tmux (and some other pty setups) shows a blank screen until the first event — the user resizes the window or presses a key — at which point the UI finally paints. It looks like the program hung.

Root cause

Program.Run reads the terminal size once at startup and trusts the result, checking only the error:

w, h, err := term.GetSize(p.ttyOutput.Fd())
if err != nil {
    return p.initialModel, fmt.Errorf("bubbletea: error getting terminal size: %w", err)
}
width, height = w, h            // no check for w==0 / h==0

Under tmux the pty size often isn't negotiated yet at this instant, so term.GetSize returns (0, 0) with err == nil. Run then resizes the renderer to 0x0. Because the event loop only paints after it receives a message, and the only startup message is the resulting WindowSizeMsg{0,0}, the first frame is clipped to a 0x0 viewport — blank — until a real SIGWINCH arrives.

This is independent of the model's View(): even a view that ignores width entirely renders blank, because the renderer viewport is 0x0.

Fix

Fall back to a standard 80x24 for any zero dimension, mirroring what terminal apps conventionally do. The first frame then paints, and the genuine size replaces it via WindowSizeMsg a moment later.

width, height = fallbackZeroSize(w, h)

Adds fallbackZeroSize (a small pure helper) plus a unit test. The startup GetSize path itself is awkward to unit-test (it needs a real tty Fd), so the fallback logic is factored into a tested helper.

Validation

  • Rebased onto current upstream main on 2026-08-08.

  • go test ./...

  • go test -race -count=1 ./...

  • go vet ./...

  • go test ./... in examples/

  • go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.9.0 run

  • go test -run TestFallbackZeroSize -count=100 .

  • git diff --check

  • Reproduction: a minimal v2 program launched in an early/detached tmux pane renders blank until the first event; with the fallback it paints immediately.

  • I have read CONTRIBUTING.md.

@Ricardo-M-L

Copy link
Copy Markdown
Contributor Author

Gentle ping on this one 🙂

It's a small (+52/-1), self-contained fix with a unit test for a real tmux issue: term.GetSize returns (0, 0) with err == nil before the pty size is negotiated, so the renderer is sized to 0x0 and the first frame is blank until a real SIGWINCH arrives (looks like the program hung). The fallback lets the first frame paint, and the genuine size still replaces it via WindowSizeMsg a moment later — no behavior change for terminals that report a valid size.

Happy to adjust the fallback dimensions or take a different approach if you'd prefer. Thanks for taking a look when you get a chance!

@Ricardo-M-L

Copy link
Copy Markdown
Contributor Author

Hi! Just a friendly ping on this PR. It's been open for a while — would appreciate a review when you get a chance. Thanks for your time!

@Ricardo-M-L
Ricardo-M-L force-pushed the fix/zero-size-blank-first-frame branch from 858b1f0 to 4a5e73a Compare July 14, 2026 06:43
Program.Run reads the terminal size once at startup via term.GetSize
and trusts the result, checking only the error. Some terminals — tmux
is the common case, before the pty size is negotiated — return (0, 0)
with no error. Run then resizes the renderer to 0x0 and the event loop,
which only paints after a message, renders the first frame into a 0x0
viewport: the whole screen is blank until a real SIGWINCH (a resize or
keypress) arrives. This looks like the program hung, and it happens
regardless of the model's View() — even a width-agnostic view renders
blank because the renderer viewport is 0.

Fall back to a standard 80x24 for any zero dimension so the first frame
paints; the genuine size still arrives via WindowSizeMsg a moment later
and re-renders correctly.

Adds fallbackZeroSize + a unit test.
@Ricardo-M-L
Ricardo-M-L force-pushed the fix/zero-size-blank-first-frame branch from 4a5e73a to a101b65 Compare August 8, 2026 08:20
@Ricardo-M-L

Copy link
Copy Markdown
Contributor Author

Substantive update after today's rebase: this branch is now based on current main (head a101b65) and GitHub reports it as MERGEABLE.

I also reran the full project validation suite: root tests, race tests, go vet, all examples, golangci-lint v2.9.0, and the focused fallback regression test 100 times. Everything passes; the PR description now has the exact commands.

The upstream build, lint, and coverage workflow runs are all currently marked action_required, so maintainer approval is the remaining mechanical step before GitHub can execute them. If the 80x24 fallback approach looks right, a workflow approval/review would unblock this. I'm happy to adjust the fallback semantics if you'd prefer a different policy.

@andrinoff

Copy link
Copy Markdown
Member

Hey @Ricardo-M-L,

Do you have any code to reproduce the blank screen?

Because all bubbletea v2 apps work fine in tmux for me

@Ricardo-M-L

Copy link
Copy Markdown
Contributor Author

Hi @andrinoff — good question, and sorry I didn't include a reproducer earlier. Here's one.

Reproducer

The issue is timing-sensitive: it happens when term.GetSize() is called before the pty has negotiated its size with tmux, so it returns (0, 0) with err == nil. The easiest reliable way to hit it is to start a bubbletea program inside a fresh tmux session whose window hasn't been sized yet:

# 1. Create a new detached tmux session and immediately run a bubbletea program inside it
tmux new-session -d -s testblank 'sleep 0.05 && your-bubbletea-app'
# 2. Attach right away
tmux attach -t testblank

On attach you'll see the first frame is blank (all-black / empty cells) for a split second before the real content paints on the first SIGWINCH / WindowSizeMsg. The smaller the sleep, the more reliably the blank frame shows up.

Why it happens

xterm.GetSize() on the tmux pty returns (0, 0, nil) before the pty size is fully negotiated. The renderer then gets sized to (0, 0), producing nothing visible, until the real size arrives via SIGWINCH.

What this PR does

It falls back to 80x24 (the standard default terminal size) when the initial size query returns zero in either dimension, so the first frame paints correctly. The real size replaces it on the very next WindowSizeMsg, which arrives essentially immediately — it's just that first frame that was blank.

Let me know if you'd like me to add this reproducer to the commit message or PR description.

@Ricardo-M-L

Copy link
Copy Markdown
Contributor Author

Hi @andrinoff, following up - I provided a reproducer for the blank first frame issue. Is there anything else needed?

@andrinoff

Copy link
Copy Markdown
Member

Weirdly it works with all of my TUIs on bubbletea v2...

Can you provide an example of an app / code of the app to reproduce?

Ricardo-M-L and others added 4 commits August 15, 2026 18:48
Use shutdown(true) instead of cancel() in the panic recovery path.
Previously cancel() only cancelled the program context without
cleaning up the renderer goroutine or closing the cancelReader,
causing resource leaks when the program panics.
Use p.Send() instead of direct channel send so Println and Printf
safely become no-ops once the program has exited, matching the
behavior of Send(), Quit(), and the documented safety contract.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants