// Package theme is glint's single source of color truth. Every styled span in // every theme gets an explicit foreground — no terminal-default fallbacks — so // the editor reads cleanly on both light and dark terminals. package theme import ( "math" "strconv" "strings" "github.com/charmbracelet/lipgloss" ) // Theme holds every color glint paints, plus its name and the glamour style the // read-preview should use to stay visually in sync. type Theme struct { Name string GlamourStyle string // Markdown element colors. Text lipgloss.Color // base prose Emphasis lipgloss.Color // bold/italic — higher contrast than Text Heading lipgloss.Color // heading text (bold) Code lipgloss.Color // inline + fenced code Link lipgloss.Color // links, URLs, and wikilink targets Wikilink lipgloss.Color // retained == Link (kept for the all-colors check) ListMarker lipgloss.Color // list bullets / numbers Blockquote lipgloss.Color // blockquote marker + border (muted tone) Comment lipgloss.Color // HTML / %% comments — visible, not dimmed Accent lipgloss.Color // frontmatter keys, selection Highlight lipgloss.Color // ==highlight== background tint Spell lipgloss.Color // misspelled-word undercurl (red) Grammar lipgloss.Color // grammar-issue undercurl (green; Harper, TASK-043) TK lipgloss.Color // 'TK'/'tk' placeholder badge background (fill-in marker, TASK-045) // Merge-conflict highlighting (git markers <<<<<<< ||||||| ======= >>>>>>>). ConflictMarker lipgloss.Color // bold marker lines ConflictOurs lipgloss.Color // background tint for the "ours"/base block ConflictTheirs lipgloss.Color // background tint for the "theirs" block // UI colors. Background lipgloss.Color Muted lipgloss.Color // markup punctuation, dimmed StatusFg lipgloss.Color StatusBg lipgloss.Color SelFg lipgloss.Color SelBg lipgloss.Color Pointer lipgloss.Color } // CycleOrder is the order Ctrl+T steps through themes. var CycleOrder = []string{"flexoki-light", "flexoki-dark", "charm"} func registry() map[string]Theme { return map[string]Theme{ "flexoki-light": FlexokiLight(), "flexoki-dark": FlexokiDark(), "charm": Charm(), } } // ByName looks up a registered theme. func ByName(name string) (Theme, bool) { t, ok := registry()[name] return t, ok } // Next returns the theme after name in CycleOrder, wrapping around. An unknown // name yields the first theme in the cycle. func Next(name string) Theme { idx := 0 for i, n := range CycleOrder { if n == name { idx = (i + 1) % len(CycleOrder) break } } t, _ := ByName(CycleOrder[idx]) return t } // Resolve picks a theme from a config value: "auto"/"" → OS detection; a known // name → that theme; an unknown name → OS detection. It never errors and never // returns an empty theme. func Resolve(configValue string) Theme { if configValue == "" || configValue == "auto" { t, _ := ByName(Detect()) return t } if t, ok := ByName(configValue); ok { return t } t, _ := ByName(Detect()) return t } // HexToRGB converts "#RRGGBB" to the "R;G;B" decimal form used in SGR codes. // A malformed color yields "" so callers can fall back to unstyled output. 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 heading text stays readable on // any heading color. func LegibleText(bgHex string) string { if RelLuminance(bgHex) > 0.5 { return "#100F0F" } return "#FFFCF0" } // RelLuminance is the WCAG relative luminance of an "#RRGGBB" color, 0 for a // malformed one. func RelLuminance(hex string) float64 { h := strings.TrimPrefix(hex, "#") if len(h) != 6 { return 0 } chan8 := func(s string) float64 { n, _ := strconv.ParseInt(s, 16, 0) c := float64(n) / 255 if c <= 0.03928 { return c / 12.92 } return math.Pow((c+0.055)/1.055, 2.4) } return 0.2126*chan8(h[0:2]) + 0.7152*chan8(h[2:4]) + 0.0722*chan8(h[4:6]) }