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

fix: preview table backgrounds + H1 highlight match theme paper (TASK-026)

fc8eb5a689b9fdc91496e869ba79abcdcaa05913
Kevin Kortum <kevinkortum@me.com> · 2026-06-29 15:55

parent efdd6b34

fix: preview table backgrounds + H1 highlight match theme paper (TASK-026)

applyTheme clears all backgrounds and sets only Document/Code/CodeBlock/Table to
paper, leaving Text bg unset so H1 heading text inherits the heading-bar bg
instead of paper. fillBackground re-asserts paper after every ANSI reset and at
line starts so glamour's unstyled table borders/padding render on paper rather
than the terminal default (the mismatch-between-themes bug).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KjNrdAWUdkaxFyGdrPHaBj

3 files changed

- → Preview-table-backgrounds-H1-highlight-dont-match-theme-paper.md +31 −0
@@ -0,0 +1,31 @@
+---
+id: TASK-026
+title: 'Preview: table backgrounds + H1 highlight don''t match theme paper'
+status: "\U0001F3C1 Done"
+assignee: []
+created_date: '2026-06-29 22:55'
+updated_date: '2026-06-29 22:55'
+labels:
+  - bug
+dependencies: []
+priority: medium
+ordinal: 26000
+---
+
+## Description
+
+<!-- SECTION:DESCRIPTION:BEGIN -->
+Two glamour preview theming bugs:
+1) Table cell padding and border glyphs (│ ┼ ─) rendered with no background → fell back to the terminal-default bg, mismatching the theme paper (worse across themes). Inline-code cells also showed dark blocks.
+2) H1 heading background bar only painted the prefix/suffix margins, not behind the heading text — the word sat on paper bg because applyTheme forced the Text background to paper, overriding the H1 block bg for the heading's child text.
+
+Fix (internal/preview/preview.go): applyTheme now clears all backgrounds and sets only Document/Code/CodeBlock/Table = paper, leaving Text bg unset so heading text inherits the H1 heading bg. Added fillBackground() to re-assert paper bg after every ANSI reset and at line starts, so unstyled gaps (borders, padding) render on paper. Applied to the theme-driven styles only.
+<!-- SECTION:DESCRIPTION:END -->
+
+## Acceptance Criteria
+<!-- AC:BEGIN -->
+- [x] #1 H1 heading background fills behind the heading text, not just the margins
+- [x] #2 Table cell padding and borders render on the theme paper across all themes
+- [x] #3 Inline code in cells uses paper bg (no dark blocks)
+- [x] #4 Regression tests cover H1 bg and post-reset paper re-assert
+<!-- AC:END -->
internal/preview/preview.go +63 −14
@@ -16,10 +16,14 @@ 	"github.com/charmbracelet/glamour/ansi"
 	"github.com/charmbracelet/glamour/styles"
 )
 
-// applyTheme rewrites a glamour style so it matches the glint theme: every
-// element background becomes the theme paper (no glamour panels behind code or
-// tables), prose/headings/code/links use the theme colors, and H1 pops as a
-// filled bar whose text is chosen to stay legible on the heading color.
+// applyTheme rewrites a glamour style so it matches the glint theme: prose,
+// headings, code and links use the theme colors on the theme paper. Every
+// element background is first cleared so the only explicit backgrounds are the
+// document paper, code (also paper — no glamour panels), and the H1 heading bar.
+// Crucially the Text background is left unset: that lets heading text inherit the
+// H1 block's heading background (so the bar fills behind the words, not just the
+// margins). Regions glamour leaves unstyled (table borders, cell padding) are
+// repainted with paper by fillBackground at render time.
 func applyTheme(cfg *ansi.StyleConfig, c Colors) {
 	var zero uint
 	cfg.Document.Margin = &zero
@@ -28,10 +32,16 @@ 		return
 	}
 	bg, text, heading, code, link := c.Background, c.Text, c.Heading, c.Code, c.Link
 
-	// Disable chroma so code renders plainly, then force every background in the
-	// whole config (document, code, tables, cells, …) to the theme paper.
+	// Disable chroma so code renders plainly, then clear every background so we
+	// can set only the few we want explicitly.
 	cfg.CodeBlock.Chroma = nil
-	setAllBackgrounds(reflect.ValueOf(cfg).Elem(), &bg)
+	clearAllBackgrounds(reflect.ValueOf(cfg).Elem())
+
+	// Backgrounds: paper for the document and code; everything else inherits.
+	cfg.Document.BackgroundColor = &bg
+	cfg.Code.BackgroundColor = &bg
+	cfg.CodeBlock.BackgroundColor = &bg
+	cfg.Table.BackgroundColor = &bg
 
 	// Colors.
 	cfg.Document.Color = &text
@@ -44,7 +54,7 @@ 	}
 	cfg.Link.Color = &link
 	cfg.LinkText.Color = &link
 
-	// H1: filled bar in the heading color, with legible text and bold.
+	// H1: filled bar in the heading color (text inherits this bg), bold.
 	yes := true
 	ht := legibleText(heading)
 	cfg.H1.Color = &ht
@@ -52,13 +62,13 @@ 	cfg.H1.BackgroundColor = &heading
 	cfg.H1.Bold = &yes
 }
 
-// setAllBackgrounds sets every *string field named "BackgroundColor" in the
-// (recursively walked) value to bg.
-func setAllBackgrounds(v reflect.Value, bg *string) {
+// clearAllBackgrounds nils every *string field named "BackgroundColor" in the
+// (recursively walked) value.
+func clearAllBackgrounds(v reflect.Value) {
 	switch v.Kind() {
 	case reflect.Pointer:
 		if !v.IsNil() {
-			setAllBackgrounds(v.Elem(), bg)
+			clearAllBackgrounds(v.Elem())
 		}
 	case reflect.Struct:
 		t := v.Type()
@@ -66,15 +76,49 @@ 		for i := 0; i < v.NumField(); i++ {
 			f := v.Field(i)
 			if t.Field(i).Name == "BackgroundColor" && f.Type() == reflect.TypeOf((*string)(nil)) {
 				if f.CanSet() {
-					f.Set(reflect.ValueOf(bg))
+					f.Set(reflect.Zero(f.Type()))
 				}
 				continue
 			}
-			setAllBackgrounds(f, bg)
+			clearAllBackgrounds(f)
 		}
 	}
 }
 
+// fillBackground re-asserts the paper background after every ANSI reset and at
+// the start of every line, so glyphs glamour emits without a background (table
+// borders, cell padding, list bullets) render on the theme paper instead of the
+// terminal default. Spans that set their own background still override it
+// immediately, so colored text is unaffected.
+func fillBackground(s, bgHex string) string {
+	rgb := hexToRGB(bgHex)
+	if rgb == "" {
+		return s
+	}
+	paper := "\x1b[48;2;" + rgb + "m"
+	s = strings.ReplaceAll(s, "\x1b[0m", "\x1b[0m"+paper)
+	lines := strings.Split(s, "\n")
+	for i, ln := range lines {
+		lines[i] = paper + ln
+	}
+	return strings.Join(lines, "\n")
+}
+
+// hexToRGB converts "#RRGGBB" to the "R;G;B" decimal form used in SGR codes.
+func hexToRGB(hex string) string {
+	h := strings.TrimPrefix(hex, "#")
+	if len(h) != 6 {
+		return ""
+	}
+	r, err1 := strconv.ParseInt(h[0:2], 16, 0)
+	g, err2 := strconv.ParseInt(h[2:4], 16, 0)
+	b, err3 := strconv.ParseInt(h[4:6], 16, 0)
+	if err1 != nil || err2 != nil || err3 != nil {
+		return ""
+	}
+	return strconv.FormatInt(r, 10) + ";" + strconv.FormatInt(g, 10) + ";" + strconv.FormatInt(b, 10)
+}
+
 // legibleText returns a near-black or near-paper text color, whichever contrasts
 // better with the background hex (so H1 text stays readable on any heading color).
 func legibleText(bgHex string) string {
@@ -158,6 +202,11 @@ 	}
 	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()
internal/preview/theme_test.go +49 −0
@@ -0,0 +1,49 @@
+package preview
+
+import (
+	"strings"
+	"testing"
+)
+
+var testColors = Colors{
+	Background: "#100F0F", Text: "#CECDC3", Heading: "#D14D41", Code: "#879A39", Link: "#4385BE",
+}
+
+func renderPreview(t *testing.T, md string) string {
+	t.Helper()
+	m := New("dark")
+	m.SetColors(testColors)
+	m.SetSize(40, 20)
+	if err := m.Render(md); err != nil {
+		t.Fatal(err)
+	}
+	return m.View()
+}
+
+// H1 text must sit on the heading background, not the paper background.
+func TestH1BackgroundBehindText(t *testing.T) {
+	out := renderPreview(t, "# glint\n")
+	if !strings.Contains(out, "48;2;209;77;65") {
+		t.Fatalf("H1 missing heading background (209;77;65):\n%q", out)
+	}
+	// the word "glint" itself must carry the heading bg, not paper (16;15;15)
+	if !strings.Contains(out, "48;2;209;77;65;1mglint") {
+		t.Fatalf("H1 text not painted on heading background:\n%q", out)
+	}
+}
+
+// Every ANSI reset must be followed by a re-assert of the paper background so
+// unstyled regions (table borders, cell padding) never fall back to the
+// terminal default — the table-background-between-themes bug.
+func TestPaperBackgroundReassertedAfterResets(t *testing.T) {
+	out := renderPreview(t, "| Key | Action |\n| --- | --- |\n| `Tab` | indent |\n")
+	paper := "\x1b[48;2;16;15;15m"
+	resets := strings.Count(out, "\x1b[0m")
+	reasserted := strings.Count(out, "\x1b[0m"+paper)
+	if resets == 0 {
+		t.Fatal("expected some resets in table output")
+	}
+	if resets != reasserted {
+		t.Fatalf("bare resets remain: %d resets, %d re-asserted paper bg", resets, reasserted)
+	}
+}