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
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)
}
}
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: <ESC>[31mred<ESC>[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.
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:
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.
Describe the bug
The cell renderer downsamples
Viewcontent to the detected color profile, but content delivered throughtea.Println/Program.Printlnreaches the terminal verbatim. UnderNO_COLOR=1(or a 16-color/ascii profile) the live view obeys while every line committed to scrollback keeps its original colors.Setup
v2.0.7(charm.land/bubbletea/v2), Go 1.26.4, darwin/arm64scriptTo reproduce
Observed (
NO_COLOR=1, escapes made visible)Control run without
NO_COLORrenders both red, as expected.Expected
Both paths obey the same profile:
colorprofile.Detect(p.output, p.environ)already runs at startup (tea.go) andcursedRendererholds the result ins.profile.Where it happens
cursedRenderer.insertAbove(cursed_renderer.go:707, v2.0.7) splits the queued string into lines and writes them straight tos.w— noConvertStyle/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 aprofilefield and aSetColorProfilemethod.Suggested fix
In
insertAbove, pass each line through the already-available profile before writing, e.g.(or convert once when the string is enqueued in
printLines). The same applies to ultraviolet'sTerminalScreen.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:Context
Found while building a
docker buildx-style progress renderer for mage builds, which commits finished subtrees to scrollback viaProgram.Println— underNO_COLOR=1the live tree went monochrome while every committed block stayed in full color.