charmbracelet / charmbracelet/bubbletea

tea.Println content bypasses color-profile downsampling — NO_COLOR and low-color terminals get full-color scrollback

Open Beginner friendly
#1,709 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
44.9k
Forks
1.3k
PR merge metrics
No merged PRs in 30d

Description

## Describe the bug

The cell renderer downsamples `View` content to the detected color profile, but content delivered through `tea.Println` / `Program.Println` reaches the terminal verbatim. Under `NO_COLOR=1` (or a 16-color/ascii profile) the live view obeys while every line committed to scrollback keeps its original colors.

## Setup

- bubbletea `v2.0.7` (charm.land/bubbletea/v2), Go 1.26.4, darwin/arm64
- Reproduces on any terminal; captures below are from a PTY via `script`

## To reproduce

```go
package main

import (
"fmt"
"os"
"time"

tea "charm.land/bubbletea/v2"
)

const red = "\x1b[31mred\x1b[m"

type model struct{}

type doneMsg struct{}

func (m model) Init() tea.Cmd {
return tea.Batch(
tea.Println("println: "+red),
tea.Tick(300*time.Millisecond, func(time.Time) tea.Msg { return doneMsg{} }),
)
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if _, ok := msg.(doneMsg); ok {
return m, tea.Quit
}
return m, nil
}

func (m model) View() tea.View { return tea.NewView("view: " + red) }

func main() {
if _, err := tea.NewProgram(model{}, tea.WithInput(nil)).Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
```

```sh
go build -o repro .
script -q out.txt sh -c 'stty rows 24 cols 80; NO_COLOR=1 ./repro'
```

## Observed (`NO_COLOR=1`, escapes made visible)

```
println: [31mred[m <- Println path: color survives
view: red <- View path: correctly downsampled
```

Control run without `NO_COLOR` renders both red, as expected.

## Expected

Both paths obey the same profile: `colorprofile.Detect(p.output, p.environ)` already runs at startup (`tea.go`) and `cursedRenderer` holds the result in `s.profile`.

## Where it happens

`cursedRenderer.insertAbove` (`cursed_renderer.go:707`, v2.0.7) splits the queued string into lines and writes them straight to `s.w` — no `ConvertStyle`/profile pass — while regular frames are converted per cell at render time. `ultraviolet.TerminalScreen.InsertAbove` (`terminal_screen.go:733`) has the same shape: it takes content verbatim even though the screen carries a `profile` field and a `SetColorProfile` method.

## Suggested fix

In `insertAbove`, pass each line through the already-available profile before writing, e.g.

```go
var conv strings.Builder
_, _ = (&colorprofile.Writer{Forward: &conv, Profile: s.profile}).WriteString(line)
sb.WriteString(conv.String())
```

(or convert once when the string is enqueued in `printLines`). The same applies to ultraviolet's `TerminalScreen.InsertAbove` — happy to split this into a second issue there if you prefer to track it per-repo.

## Workaround

Callers can downsample before calling `Println`:

```go
profile := colorprofile.Detect(out, os.Environ()) // same detection the program runs
var sb strings.Builder
_, _ = (&colorprofile.Writer{Forward: &sb, Profile: profile}).WriteString(block)
p.Println(sb.String())
```

## Context

Found while building a `docker buildx`-style progress renderer for mage builds, which commits finished subtrees to scrollback via `Program.Println` — under `NO_COLOR=1` the live tree went monochrome while every committed block stayed in full color.

Contributor guide

Open the contributing guide

Research direction

Start in cursed_renderer.go at cursedRenderer.insertAbove and trace how tea.Println content is written to s.w; compare that path with ultraviolet/terminal_screen.go's TerminalScreen.InsertAbove and the existing color-profile handling. Done means committed Println lines are downsampled to the detected profile just like live View content, including under NO_COLOR and low-color terminals.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
cli
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.