// theme.go — Zed theme JSON in, CSS custom properties out. Per-agent colors // come from the theme's player (collaborator) palette. package main import ( "encoding/json" "fmt" "os" "path/filepath" ) type SyntaxStyle struct { Color string `json:"color"` FontStyle *string `json:"font_style"` FontWeight *float64 `json:"font_weight"` } type Theme struct { Name, Appearance string Bg, Fg, Panel, Border, LineNo, Muted, Accent string Note, Hover string Syntax map[string]SyntaxStyle Players []string } // defaultTheme finds a zed theme file containing a theme of the wanted // appearance; "" means fall back to the embedded One family. func defaultTheme(appearance string) string { home, _ := os.UserHomeDir() m, _ := filepath.Glob(filepath.Join(home, ".config", "zed", "themes", "*.json")) for _, path := range m { b, err := os.ReadFile(path) if err != nil { continue } var family struct { Themes []struct { Appearance string `json:"appearance"` } `json:"themes"` } if json.Unmarshal(b, &family) != nil { continue } for _, t := range family.Themes { if t.Appearance == appearance { return path } } } return "" } func loadTheme(path, name, appearance string) *Theme { var b []byte var err error if path == "" { b, err = tfs.ReadFile("assets/one.json") } else { b, err = os.ReadFile(path) } if err != nil { fatal("%v", err) } var family struct { Name string `json:"name"` Themes []struct { Name string `json:"name"` Appearance string `json:"appearance"` Style map[string]json.RawMessage `json:"style"` } `json:"themes"` } if err := json.Unmarshal(b, &family); err != nil || len(family.Themes) == 0 { fatal("bad zed theme %s: %v", path, err) } th := family.Themes[0] if name != "" { for _, t := range family.Themes { if t.Name == name { th = t } } } else { for _, t := range family.Themes { if t.Appearance == appearance { th = t break } } } get := func(keys ...string) string { for _, k := range keys { var s string if raw, ok := th.Style[k]; ok && json.Unmarshal(raw, &s) == nil && s != "" { return s } } return "" } t := &Theme{Name: th.Name, Appearance: th.Appearance, Bg: get("editor.background", "background"), Fg: get("editor.foreground", "text"), Panel: get("panel.background", "surface.background", "background"), Border: get("border", "border.variant"), LineNo: get("editor.line_number"), Muted: get("text.muted", "text.disabled"), Accent: get("text.accent", "info"), Note: get("warning", "conflict", "modified"), Hover: get("element.hover", "ghost_element.hover"), } json.Unmarshal(th.Style["syntax"], &t.Syntax) var players []struct { Cursor string `json:"cursor"` } json.Unmarshal(th.Style["players"], &players) // agent colors: drop player colors that vanish against fg/bg or collide // with the note color, then order greedily for mutual distance so the // first few agents contrast most var usable []string for _, p := range players { c := hex6(p.Cursor) if c != "" && colorDist(c, hex6(t.Fg)) > 60 && colorDist(c, hex6(t.Bg)) > 60 && colorDist(c, hex6(t.Note)) > 60 { usable = append(usable, c) } } for len(usable) > 0 { best, bestScore := 0, -1 for i, c := range usable { score := 1 << 24 // first pick: keep theme order for _, p := range t.Players { if d := colorDist(c, p); d < score { score = d } } if score > bestScore { best, bestScore = i, score } } t.Players = append(t.Players, usable[best]) usable = append(usable[:best], usable[best+1:]...) } return t } func colorDist(a, b string) int { var ar, ag, ab, br, bg, bb int if len(a) != 7 || len(b) != 7 { return 1 << 24 } fmt.Sscanf(a[1:], "%02x%02x%02x", &ar, &ag, &ab) fmt.Sscanf(b[1:], "%02x%02x%02x", &br, &bg, &bb) d := func(x, y int) int { return (x - y) * (x - y) } return int(float64(d(ar, br)+d(ag, bg)+d(ab, bb)) / 3) } var fallbackPalette = []string{"#7aa2f7", "#e0af68", "#9ece6a", "#f7768e", "#bb9af7", "#2ac3de"} func (t *Theme) Player(i int) string { if i < len(t.Players) { return t.Players[i] } return fallbackPalette[i%len(fallbackPalette)] } // hex6 strips an alpha channel from #rrggbbaa. func hex6(c string) string { if len(c) == 9 && c[0] == '#' { return c[:7] } return c } func mixColors(cols []string) string { if len(cols) == 1 { return cols[0] } var r, g, b int n := 0 for _, c := range cols { c = hex6(c) if len(c) != 7 { continue } var cr, cg, cb int fmt.Sscanf(c[1:], "%02x%02x%02x", &cr, &cg, &cb) r += cr g += cg b += cb n++ } if n == 0 { return "#888888" } return fmt.Sprintf("#%02x%02x%02x", r/n, g/n, b/n) }