Skip to content

[v2] Multi-screen Println removes an already-rendered inline View #1822

Description

@rsbin1178

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

  1. 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 .
  2. Use a 40×10 terminal, or a terminal substantially shorter than the 80 printed rows.

  3. 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.

  4. Press p once.

  5. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions