Styled content spanning multiple lines bleeds into borders and padding
When content passed to Style.Render contains an SGR span that crosses the content's own newlines, the escape state leaks into the border and padding of the rendered rows.
Reproduction (lipgloss v2.0.6, Go 1.24, Linux — terminal-agnostic, visible in the raw bytes):
style := lipgloss.NewStyle().
Border(lipgloss.NormalBorder()).
Padding(0, 2)
fmt.Println(style.Render("\x1b[31mred one\nred two\x1b[0m plain"))Raw output:
"┌─────────────────┐\n│ \x1b[31mred one │\n│ red two\x1b[0m plain │\n└─────────────────┘"The \x1b[31m is never closed before the row ends, so on a real terminal the first row's padding and right border render red, as does the second row's left border and padding — the red stays active until the user's own \x1b[0m in row two. A well-formed span (properly opened and reset) produces the same effect whenever it crosses a line break, since SGR state survives newlines but each row is assembled with border glyphs around it.
Expected: border and padding render in the border style regardless of styling inside the content — e.g. by closing open SGR state at each row end and re-opening it on the next row, so every rendered line is self-contained.
Found this while hardening another library built on charmbracelet/x/ansi, where the same class of bug came up; happy to share notes on the per-row isolation approach we ended up with (decoder-based, using ansi.DecodeSequenceWc) if that's useful.
Source: charmbracelet/lipgloss