Gim/internal/theme/loader.go
Hayden Hargreaves 273be90d42 feat: HUGE refactor of colorschemes, untested.
Now we can load them in via JSON files at launch time. They are embded
in the final exe though...
2026-04-08 11:59:49 -07:00

155 lines
3.7 KiB
Go

package theme
import (
"embed"
"encoding/json"
"fmt"
"io/fs"
"path/filepath"
"sort"
"strings"
"github.com/charmbracelet/lipgloss"
)
//go:embed themes/*.json
var embeddedThemes embed.FS
// LoadEmbeddedThemesJSON reads all embedded theme JSON files and unmarshals
// them into ThemeJSON objects keyed by theme name.
func LoadEmbeddedThemesJSON() (map[string]ThemeJSON, error) {
paths, err := fs.Glob(embeddedThemes, "themes/*.json")
if err != nil {
return nil, err
}
sort.Strings(paths)
out := make(map[string]ThemeJSON, len(paths))
for _, path := range paths {
b, readErr := embeddedThemes.ReadFile(path)
if readErr != nil {
return nil, fmt.Errorf("read embedded theme %q: %w", path, readErr)
}
var th ThemeJSON
if unmarshalErr := json.Unmarshal(b, &th); unmarshalErr != nil {
return nil, fmt.Errorf("decode embedded theme %q: %w", path, unmarshalErr)
}
if strings.TrimSpace(th.Name) == "" {
th.Name = strings.TrimSuffix(filepath.Base(path), ".json")
}
out[th.Name] = th
}
return out, nil
}
func MapEmbededThemeToEditorTheme(em map[string]ThemeJSON) map[string]EditorTheme {
out := make(map[string]EditorTheme, len(em))
for name, in := range em {
line := styleFromJSON(in.Line)
lineBg := colorString(in.Line.BG)
syntaxExact := make(map[string]lipgloss.Style, len(in.Syntax.Exact))
for capture, col := range in.Syntax.Exact {
c := normalizeCaptureKey(capture)
if c == "" {
continue
}
syntaxExact[c] = syntaxColorStyle(col, lineBg)
}
syntaxGroup := make(map[string]lipgloss.Style, len(in.Syntax.Group))
for group, col := range in.Syntax.Group {
g := normalizeCaptureKey(group)
if g == "" {
continue
}
syntaxGroup[g] = syntaxColorStyle(col, lineBg)
}
key := strings.TrimSpace(name)
if key == "" {
key = strings.TrimSpace(in.Name)
}
if key == "" {
continue
}
out[key] = EditorTheme{
Cursors: CursorTheme{
Normal: styleFromJSON(in.Cursors.Normal),
Insert: styleFromJSON(in.Cursors.Insert),
Command: styleFromJSON(in.Cursors.Command),
Replace: styleFromJSON(in.Cursors.Replace),
},
Gutter: GutterTheme{
Default: styleFromJSON(in.Gutter.Default),
CurrentLine: styleFromJSON(in.Gutter.CurrentLine),
},
VisualHightlight: styleFromJSON(in.VisualHighlight),
StatusBar: StatusBarTheme{
Default: styleFromJSON(in.StatusBar.Default),
},
CommandLine: CommandLineTheme{
Error: styleFromJSON(in.CommandLine.Error),
OutputBorder: styleFromJSON(in.CommandLine.OutputBorder),
ContinueMessage: styleFromJSON(in.CommandLine.ContinueMessage),
},
Line: line,
Background: styleFromJSON(in.Background),
Syntax: SyntaxTheme{
Exact: syntaxExact,
Group: syntaxGroup,
},
}
}
return out
}
// MapEmbeddedThemeToEditorTheme is a correctly spelled alias for
// MapEmbededThemeToEditorTheme.
func MapEmbeddedThemeToEditorTheme(em map[string]ThemeJSON) map[string]EditorTheme {
return MapEmbededThemeToEditorTheme(em)
}
func styleFromJSON(in ColorStyleJSON) lipgloss.Style {
out := lipgloss.NewStyle()
if fg := colorString(in.FG); fg != "" {
out = out.Foreground(lipgloss.Color(fg))
}
if bg := colorString(in.BG); bg != "" {
out = out.Background(lipgloss.Color(bg))
}
return out
}
func colorString(c string) string {
return strings.TrimSpace(c)
}
func normalizeCaptureKey(k string) string {
k = strings.TrimSpace(strings.ToLower(k))
k = strings.TrimPrefix(k, "@")
return k
}
func syntaxColorStyle(fg, bg string) lipgloss.Style {
out := lipgloss.NewStyle()
if f := colorString(fg); f != "" {
out = out.Foreground(lipgloss.Color(f))
}
if b := colorString(bg); b != "" {
out = out.Background(lipgloss.Color(b))
}
return out
}