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"),
- }
-}