▍ humdrum codex / glint v1.0.2
license AGPL-3.0

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
+}