fix: preview eliminates terminal-bg bleed; legible H1 text (TASK-026 follow-up)
4254a57cdbfcd27b290a54bb68fd750d09e85000
Kevin Kortum <kevinkortum@me.com> · 2026-06-29 16:11
parent fc8eb5a6
fix: preview eliminates terminal-bg bleed; legible H1 text (TASK-026 follow-up) fillBackground re-asserts paper after the bare \x1b[m reset too (not just \x1b[0m) and runs at View() time so the viewport's own padding/resets are covered — unstyled regions no longer fall back to the terminal background (cream-over-dark / dark-over-light bleed). Text.Color is left unset so H1 heading text inherits the H1 block's legible color instead of the prose color. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KjNrdAWUdkaxFyGdrPHaBj
2 files changed
internal/preview/preview.go +20 −9
@@ -43,9 +43,11 @@ cfg.Code.BackgroundColor = &bg
cfg.CodeBlock.BackgroundColor = &bg
cfg.Table.BackgroundColor = &bg
- // Colors.
+ // Colors. Text.Color is left unset so prose inherits Document.Color while
+ // heading text inherits the H1 block's legible color (set below) — if Text
+ // carried its own color it would override the heading text, leaving it
+ // illegible on the heading bar.
cfg.Document.Color = &text
- cfg.Text.Color = &text
cfg.Code.Color = &code
cfg.CodeBlock.Color = &code
for _, h := range []*ansi.StyleBlock{&cfg.Heading, &cfg.H2, &cfg.H3, &cfg.H4, &cfg.H5, &cfg.H6} {
@@ -96,7 +98,12 @@ if rgb == "" {
return s
}
paper := "\x1b[48;2;" + rgb + "m"
+ // Re-assert paper after every reset. Glamour/termenv emit both "\x1b[0m" and
+ // the bare "\x1b[m"; regions after an unhandled reset fall back to the
+ // terminal's own background (cream bleed on a dark theme over a light
+ // terminal, and the reverse) — so both forms must be covered.
s = strings.ReplaceAll(s, "\x1b[0m", "\x1b[0m"+paper)
+ s = strings.ReplaceAll(s, "\x1b[m", "\x1b[m"+paper)
lines := strings.Split(s, "\n")
for i, ln := range lines {
lines[i] = paper + ln
@@ -203,11 +210,6 @@ out, err := r.Render(markdown)
if err != nil {
return err
}
- // For the theme-driven styles, repaint unstyled gaps with the theme paper so
- // table borders/padding don't fall back to the terminal background.
- if m.colors.Background != "" && (m.style == "" || m.style == "light" || m.style == "dark") {
- out = fillBackground(out, m.colors.Background)
- }
m.vp.SetContent(out)
m.vp.GotoTop()
return nil
@@ -273,8 +275,17 @@ m.vp, cmd = m.vp.Update(msg)
return cmd
}
-// View renders the viewport.
-func (m *Model) View() string { return m.vp.View() }
+// View renders the viewport. For the theme-driven styles it repaints every
+// unstyled gap (table borders/padding, viewport fill) with the theme paper so
+// nothing falls back to the terminal's own background. Applied here (not in
+// Render) so the viewport's own padding/resets are covered too.
+func (m *Model) View() string {
+ out := m.vp.View()
+ if m.colors.Background != "" && (m.style == "" || m.style == "light" || m.style == "dark") {
+ out = fillBackground(out, m.colors.Background)
+ }
+ return out
+}
// Style returns the current glamour style (used in tests).
func (m *Model) Style() string { return m.style }
internal/preview/theme_test.go +38 −0
@@ -47,3 +47,41 @@ if resets != reasserted {
t.Fatalf("bare resets remain: %d resets, %d re-asserted paper bg", resets, reasserted)
}
}
+
+// The bare "\x1b[m" reset form must also be followed by paper, else those
+// regions fall back to the terminal background (cream-over-dark / dark-over-light
+// bleed reported on real terminals).
+func TestBareResetAlsoReassertsPaper(t *testing.T) {
+ out := renderPreview(t, "# glint\n\n| Key | Action |\n| --- | --- |\n| `Tab` | indent |\n")
+ paper := "\x1b[48;2;16;15;15m"
+ // Walk every ESC[m occurrence; each must be immediately followed by paper.
+ for i := 0; ; {
+ j := strings.Index(out[i:], "\x1b[m")
+ if j < 0 {
+ break
+ }
+ at := i + j
+ if !strings.HasPrefix(out[at+len("\x1b[m"):], paper) {
+ t.Fatalf("bare reset at %d not followed by paper bg: %q", at, out[at:min(at+24, len(out))])
+ }
+ i = at + len("\x1b[m")
+ }
+}
+
+// H1 heading text must use the legible color for the heading background, not the
+// prose text color (which can be illegible on the heading bar in light themes).
+func TestH1TextUsesLegibleColor(t *testing.T) {
+ out := renderPreview(t, "# glint\n")
+ want := legibleText(testColors.Heading) // e.g. #FFFCF0 -> 255;252;240
+ rgb := hexToRGB(want)
+ if !strings.Contains(out, "38;2;"+rgb+";48;2;209;77;65;1mglint") {
+ t.Fatalf("H1 text not using legible color %s (%s):\n%q", want, rgb, out)
+ }
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}