Describe the bug
In inline mode, one tea.Println containing more rows than the terminal can accommodate above the managed View removes the already-rendered View. The View stays byte-for-byte identical throughout the reproduction.
This occurs after the initial frame is visibly rendered, with no resize, model switch, streaming, raw terminal output, or clear-screen operation. All printed lines are short ASCII strings, so this is not a wrapping or wide-grapheme case.
I reproduced it with the latest release, charm.land/bubbletea/v2 v2.0.10, and with the renderer/Program source from current main (ff51ba4c51f85875761b15a64f9ab9aa0eaa9fa1). The renderer file on that commit is identical to v2.0.10; the additional main-branch change concerns shutdown signal handling.
Setup
- OS: macOS 26.5.1, arm64
- Go: go1.26.6 darwin/arm64
- Reproduction environment: a local controlling PTY at 40 columns × 10 rows, whose output is replayed in pyte 0.8.2 for screen/history assertions
- Environment:
TERM=xterm-256color, TERM_PROGRAM=Apple_Terminal, NO_COLOR=1. The TERM_PROGRAM value is an explicit harness setting, not a claim that the test ran in the Apple Terminal GUI.
- Shell: the harness launches the compiled binary directly; no shell is involved
- SSH / terminal multiplexer: none
- Also observed with renderer-level screen assertions using
github.com/charmbracelet/x/vt
To Reproduce
-
Put the source below in main.go in a new directory and run:
go mod init bubbletea-inline-repro
go get charm.land/bubbletea/v2@v2.0.10
go run .
-
Use a 40×10 terminal, or a terminal substantially shorter than the 80 printed rows.
-
Wait until COMPOSER, STATUS, and the key hint are visible. This matters: the example does not print from Init, and the first View has already been painted.
-
Press p once.
-
Observe the screen before pressing q, since shutdown may repaint the View.
Source Code
package main
import (
"fmt"
"os"
"strings"
tea "charm.land/bubbletea/v2"
)
type model struct{}
func (model) Init() tea.Cmd { return nil }
func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
if key, ok := message.(tea.KeyPressMsg); ok {
switch key.String() {
case "p":
lines := make([]string, 80)
for i := range lines {
lines[i] = fmt.Sprintf("ROW-%03d", i)
}
return m, tea.Println(strings.Join(lines, "\n"))
case "q", "ctrl+c":
return m, tea.Quit
}
}
return m, nil
}
func (model) View() tea.View {
view := tea.NewView("COMPOSER\nSTATUS\np: print history | q: quit")
view.Cursor = tea.NewCursor(0, 0)
return view
}
func main() {
if _, err := tea.NewProgram(model{}).Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Expected behavior
ROW-000 through ROW-079 enter native history exactly once and in order.
- The three-line managed View remains visible beneath that output.
- The visible cursor is restored to the View's requested position.
- Neither another keypress nor a changed View/resize/Quit should be required to repair the screen.
Actual screen
Captured after p, before any further input or Quit (trailing spaces omitted):
ROW-071
ROW-072
ROW-073
ROW-074
ROW-075
ROW-076
ROW-077
ROW-078
ROW-079
COMPOSER, STATUS, and the key hint are absent. The terminal cursor is at (0,9) instead of on the managed View. In this single-print minimal example, all 80 numbered rows are still present and ordered in screen + history; the demonstrated failure is the lost managed View/cursor, not lost message data.
| Source under test |
Initial View observed before p |
All 80 rows ordered and unique |
Managed controls visible after print |
| Official v2.0.10 |
yes |
yes |
no |
Current main renderer/Program source (ff51ba4c51f85) |
yes |
yes |
no |
| Local renderer fix |
yes |
yes |
yes |
Additional context / likely cause
In insertAbove, the payload row count is reserved below the managed frame using newlines, followed by CursorUp(offset + h - 1) and InsertLine(offset). With an 80-row payload on a 10-row terminal containing a 3-row View, the reservation exceeds the 7 available rows and displaces the managed content. The final internal cursor reset does not repaint it; the equal-View fast path can then skip the necessary restoration.
I searched open and closed issues and PRs. These are related, but their reproducers concern different conditions:
I did not find a report covering this already-flushed, fixed-geometry, oversized-insertion case. If you prefer tracking it under an existing issue, I am happy to consolidate the reproduction there.
We have a local fix that keeps insertion inside the renderer, uses execution-time geometry, avoids oversized blank-row reservations, and restores the managed frame/cursor before returning. We would be happy to submit a focused PR with automated regression tests, including the no-free-rows/full-height case. We can coordinate with #1741 rather than duplicating its pending-frame-flush work. No new public printing API is required by the local approach.
Describe the bug
In inline mode, one
tea.Printlncontaining more rows than the terminal can accommodate above the managed View removes the already-rendered View. The View stays byte-for-byte identical throughout the reproduction.This occurs after the initial frame is visibly rendered, with no resize, model switch, streaming, raw terminal output, or clear-screen operation. All printed lines are short ASCII strings, so this is not a wrapping or wide-grapheme case.
I reproduced it with the latest release,
charm.land/bubbletea/v2 v2.0.10, and with the renderer/Program source from currentmain(ff51ba4c51f85875761b15a64f9ab9aa0eaa9fa1). The renderer file on that commit is identical to v2.0.10; the additional main-branch change concerns shutdown signal handling.Setup
TERM=xterm-256color,TERM_PROGRAM=Apple_Terminal,NO_COLOR=1. TheTERM_PROGRAMvalue is an explicit harness setting, not a claim that the test ran in the Apple Terminal GUI.github.com/charmbracelet/x/vtTo Reproduce
Put the source below in
main.goin a new directory and run:go mod init bubbletea-inline-repro go get charm.land/bubbletea/v2@v2.0.10 go run .Use a 40×10 terminal, or a terminal substantially shorter than the 80 printed rows.
Wait until
COMPOSER,STATUS, and the key hint are visible. This matters: the example does not print fromInit, and the first View has already been painted.Press
ponce.Observe the screen before pressing
q, since shutdown may repaint the View.Source Code
Expected behavior
ROW-000throughROW-079enter native history exactly once and in order.Actual screen
Captured after
p, before any further input or Quit (trailing spaces omitted):COMPOSER,STATUS, and the key hint are absent. The terminal cursor is at(0,9)instead of on the managed View. In this single-print minimal example, all 80 numbered rows are still present and ordered in screen + history; the demonstrated failure is the lost managed View/cursor, not lost message data.pff51ba4c51f85)Additional context / likely cause
In
insertAbove, the payload row count is reserved below the managed frame using newlines, followed byCursorUp(offset + h - 1)andInsertLine(offset). With an 80-row payload on a 10-row terminal containing a 3-row View, the reservation exceeds the 7 available rows and displaces the managed content. The final internal cursor reset does not repaint it; the equal-View fast path can then skip the necessary restoration.I searched open and closed issues and PRs. These are related, but their reproducers concern different conditions:
pis sent only after the complete first frame is visible.tea.Println) #959: historical width truncation; the lines here are only 7 columns wide.I did not find a report covering this already-flushed, fixed-geometry, oversized-insertion case. If you prefer tracking it under an existing issue, I am happy to consolidate the reproduction there.
We have a local fix that keeps insertion inside the renderer, uses execution-time geometry, avoids oversized blank-row reservations, and restores the managed frame/cursor before returning. We would be happy to submit a focused PR with automated regression tests, including the no-free-rows/full-height case. We can coordinate with #1741 rather than duplicating its pending-frame-flush work. No new public printing API is required by the local approach.