▍ humdrum codex / glint v1.0.2

refactor: editor consumes theme.Theme; default flexoki-dark; SetTheme

b379b91cc55e7d92b28919e49e8d5f0ebd415614
humdrum <me@humdrum.me> · 2026-06-28 08:27

parent b1ef9b01

4 files changed

internal/editor/editor.go +7 −2
@@ -3,6 +3,8 @@
 import (
 	"strings"
 
+	"glint/internal/theme"
+
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
 )
@@ -21,18 +23,21 @@ 	Scroll int
 	Dirty  bool
 	Width  int
 	Height int // visible text rows
-	theme  Theme
+	theme  theme.Theme
 }
 
 // New returns an empty editor with one blank line and the default theme.
 func New() *Editor {
 	return &Editor{
 		Lines:  []string{""},
-		theme:  DefaultDarkTheme(),
+		theme:  theme.FlexokiDark(),
 		Width:  80,
 		Height: 24,
 	}
 }
+
+// SetTheme swaps the active theme; the next View re-scans with the new colors.
+func (e *Editor) SetTheme(t theme.Theme) { e.theme = t }
 
 // SetContent replaces the buffer, resetting cursor, scroll, and dirty state.
 func (e *Editor) SetContent(b []byte) {
internal/editor/scanner.go +6 −4
@@ -4,6 +4,8 @@ import (
 	"regexp"
 	"strings"
 
+	"glint/internal/theme"
+
 	"github.com/charmbracelet/lipgloss"
 )
 
@@ -22,7 +24,7 @@
 // ScanLines styles every line, threading block state across lines. The returned
 // slice is parallel to the input: out[i] are the spans for lines[i], whose
 // concatenated text equals lines[i] exactly.
-func ScanLines(lines []string, th Theme) [][]Span {
+func ScanLines(lines []string, th theme.Theme) [][]Span {
 	st := &blockState{}
 	out := make([][]Span, len(lines))
 	for i, ln := range lines {
@@ -31,7 +33,7 @@ 	}
 	return out
 }
 
-func scanLine(row int, line string, st *blockState, th Theme) []Span {
+func scanLine(row int, line string, st *blockState, th theme.Theme) []Span {
 	code := lipgloss.NewStyle().Foreground(th.Code)
 	muted := lipgloss.NewStyle().Foreground(th.Blockquote)
 	trimmed := strings.TrimSpace(line)
@@ -92,7 +94,7 @@ }
 
 // scanInline emits styled spans for inline constructs, keeping all markup
 // characters visible. Plain text gets the theme's Text foreground.
-func scanInline(text string, th Theme) []Span {
+func scanInline(text string, th theme.Theme) []Span {
 	plain := lipgloss.NewStyle().Foreground(th.Text)
 	r := []rune(text)
 	var spans []Span
@@ -118,7 +120,7 @@ }
 
 // matchToken tries to match an inline construct starting at r[i]. It returns the
 // token (including its markup), the style, the rune length, and whether matched.
-func matchToken(r []rune, i int, th Theme) (string, lipgloss.Style, int, bool) {
+func matchToken(r []rune, i int, th theme.Theme) (string, lipgloss.Style, int, bool) {
 	// Inline code: `...`
 	if r[i] == '`' {
 		if j := indexRune(r, '`', i+1); j > i {
internal/editor/scanner_test.go +9 −20
@@ -3,6 +3,8 @@
 import (
 	"testing"
 
+	"glint/internal/theme"
+
 	"github.com/charmbracelet/lipgloss"
 )
 
@@ -14,19 +16,6 @@ 	for _, sp := range spans {
 		s += sp.Text
 	}
 	return s
-}
-
-func TestDefaultThemeAllColorsSet(t *testing.T) {
-	th := DefaultDarkTheme()
-	colors := []lipgloss.Color{
-		th.Text, th.Heading, th.Code, th.Link,
-		th.Wikilink, th.ListMarker, th.Blockquote, th.Accent,
-	}
-	for i, c := range colors {
-		if c == "" {
-			t.Errorf("theme color %d is empty", i)
-		}
-	}
 }
 
 func TestRenderSpansPreservesText(t *testing.T) {
@@ -42,7 +31,7 @@ 	}
 }
 
 func TestScanPlainTextGetsExplicitForeground(t *testing.T) {
-	th := DefaultDarkTheme()
+	th := theme.FlexokiDark()
 	out := ScanLines([]string{"hello world"}, th)
 	if len(out) != 1 || len(out[0]) == 0 {
 		t.Fatalf("expected spans for one line, got %v", out)
@@ -56,7 +45,7 @@ 	}
 }
 
 func TestScanPreservesRawTextAcrossConstructs(t *testing.T) {
-	th := DefaultDarkTheme()
+	th := theme.FlexokiDark()
 	lines := []string{
 		"# Heading",
 		"plain **bold** and *italic* and `code`",
@@ -76,7 +65,7 @@ 	}
 }
 
 func TestScanFencedCodeSuppressesInline(t *testing.T) {
-	th := DefaultDarkTheme()
+	th := theme.FlexokiDark()
 	lines := []string{"```", "**x**", "```"}
 	out := ScanLines(lines, th)
 	// The fence body line should be a single code-colored span, not split
@@ -90,7 +79,7 @@ 	}
 }
 
 func TestScanLeadingFrontmatter(t *testing.T) {
-	th := DefaultDarkTheme()
+	th := theme.FlexokiDark()
 	lines := []string{"---", "title: x", "---", "body"}
 	out := ScanLines(lines, th)
 	if out[1][0].Style.GetForeground() != th.Blockquote {
@@ -103,14 +92,14 @@ 	}
 }
 
 func TestScanEmptyLineYieldsNoSpans(t *testing.T) {
-	out := ScanLines([]string{""}, DefaultDarkTheme())
+	out := ScanLines([]string{""}, theme.FlexokiDark())
 	if len(out[0]) != 0 {
 		t.Errorf("empty line should yield no spans, got %d", len(out[0]))
 	}
 }
 
 func TestScanHeadingKeepsHashes(t *testing.T) {
-	th := DefaultDarkTheme()
+	th := theme.FlexokiDark()
 	out := ScanLines([]string{"### Title"}, th)
 	if spanText(out[0]) != "### Title" {
 		t.Errorf("heading hashes dropped: %q", spanText(out[0]))
@@ -121,7 +110,7 @@ 	}
 }
 
 func TestScanListMarkerThenInline(t *testing.T) {
-	th := DefaultDarkTheme()
+	th := theme.FlexokiDark()
 	out := ScanLines([]string{"- **x**"}, th)
 	if out[0][0].Style.GetForeground() != th.ListMarker {
 		t.Errorf("first span should be the list marker, got fg %v", out[0][0].Style.GetForeground())
internal/editor/theme.go +0 −30
@@ -1,30 +0,0 @@
-package editor
-
-import "github.com/charmbracelet/lipgloss"
-
-// Theme holds the explicit foreground colors used by the styling scanner.
-// Every span gets one of these — none ever defaults to the terminal foreground.
-type Theme struct {
-	Text       lipgloss.Color
-	Heading    lipgloss.Color
-	Code       lipgloss.Color
-	Link       lipgloss.Color
-	Wikilink   lipgloss.Color
-	ListMarker lipgloss.Color
-	Blockquote lipgloss.Color
-	Accent     lipgloss.Color
-}
-
-// DefaultDarkTheme is a Tokyo-Night-ish palette that reads on dark terminals.
-func DefaultDarkTheme() Theme {
-	return Theme{
-		Text:       lipgloss.Color("#c0caf5"),
-		Heading:    lipgloss.Color("#7aa2f7"),
-		Code:       lipgloss.Color("#9ece6a"),
-		Link:       lipgloss.Color("#7dcfff"),
-		Wikilink:   lipgloss.Color("#bb9af7"),
-		ListMarker: lipgloss.Color("#bb9af7"),
-		Blockquote: lipgloss.Color("#565f89"),
-		Accent:     lipgloss.Color("#e0af68"),
-	}
-}