package main import ( "fmt" "strconv" "strings" "github.com/alecthomas/chroma/v2/styles" "github.com/charmbracelet/lipgloss" ) type themePalette struct { Mode string Title, Dim, Text string ActiveForeground string ActiveBackground string Success, Warning, Error string EditorForeground string EditorBackground string PaneInactive, PaneActive string Quote string SelectionBackground string SuggestionRemoveBackground string SuggestionAddBackground string ChangedRemoveBackground string ChangedAddBackground string AuthorPalette []string SyntaxTheme string NoColor bool HighContrast bool } var ( currentThemeName = "dark" themeIsLight bool editorMarkdownTheme themePalette ) func applyTheme(name string, custom ...CustomThemeConfig) error { var configured CustomThemeConfig if len(custom) > 0 { configured = custom[0] } palette, err := resolveThemePalette(name, configured) if err != nil { return err } colorEnabled = !palette.NoColor themeIsLight = palette.Mode == "light" editorMarkdownTheme = palette if palette.NoColor { titleStyle, dimStyle = lipgloss.NewStyle(), lipgloss.NewStyle() activeStyle = lipgloss.NewStyle().Reverse(true) okStyle, warnStyle, badStyle = lipgloss.NewStyle(), lipgloss.NewStyle(), lipgloss.NewStyle() editorLineStyle = lipgloss.NewStyle().Reverse(true) paneInactiveColor, paneActiveColor = "", "" authorPalette = []lipgloss.Color{""} quoteRailStyle = lipgloss.NewStyle() selectedLineBackground, suggestionRemoveBackground, suggestionAddBackground = "", "", "" changedRemoveBackground, changedAddBackground = "", "" codeHighlightTheme, markdownStyleName = "github", "notty" } else { titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(palette.Title)) dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Dim)) activeStyle = lipgloss.NewStyle().Bold(true). Foreground(lipgloss.Color(palette.ActiveForeground)). Background(lipgloss.Color(palette.ActiveBackground)) okStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Success)) warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Warning)) badStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Error)) editorLineStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(palette.EditorForeground)). Background(lipgloss.Color(palette.EditorBackground)) paneInactiveColor = lipgloss.Color(palette.PaneInactive) paneActiveColor = lipgloss.Color(palette.PaneActive) quoteRailStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Quote)) authorPalette = make([]lipgloss.Color, len(palette.AuthorPalette)) for index, color := range palette.AuthorPalette { authorPalette[index] = lipgloss.Color(color) } selectedLineBackground = backgroundSequence(palette.SelectionBackground) suggestionRemoveBackground = backgroundSequence(palette.SuggestionRemoveBackground) suggestionAddBackground = backgroundSequence(palette.SuggestionAddBackground) changedRemoveBackground = backgroundSequence(palette.ChangedRemoveBackground) changedAddBackground = backgroundSequence(palette.ChangedAddBackground) codeHighlightTheme = palette.SyntaxTheme markdownStyleName = "dark" if themeIsLight { markdownStyleName = "light" } if palette.HighContrast { titleStyle = titleStyle.Underline(true) activeStyle = lipgloss.NewStyle().Bold(true).Reverse(true) okStyle, warnStyle, badStyle = okStyle.Bold(true), warnStyle.Bold(true), badStyle.Bold(true) editorLineStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(palette.EditorForeground)).Reverse(true) quoteRailStyle = quoteRailStyle.Bold(true) selectedLineBackground = "\x1b[7m" } } currentThemeName = name commentMarkdownRenderers.Clear() commentMarkdownLines.clear() renderedSuggestions.clear() highlightedDiffs.clear() return nil } func resolveThemePalette(name string, custom CustomThemeConfig) (themePalette, error) { if name == "custom" { base := custom.Base if base == "" { base = "dark" } if base == "custom" { return themePalette{}, fmt.Errorf("custom_theme.base cannot be custom") } palette, err := resolveThemePalette(base, CustomThemeConfig{}) if err != nil { return themePalette{}, fmt.Errorf("custom_theme.base: %w", err) } applyCustomTheme(&palette, custom) if err := validateThemePalette(palette); err != nil { return themePalette{}, fmt.Errorf("custom_theme: %w", err) } return palette, nil } palette, ok := builtinThemePalettes()[name] if !ok { return themePalette{}, fmt.Errorf( "unknown theme %q; use dark, light, catppuccin, catppuccin-latte, "+ "gruvbox, gruvbox-light, one-dark-pro, github, github-light, "+ "high-contrast, no-color, or custom", name, ) } return palette, nil } func applyCustomTheme(palette *themePalette, custom CustomThemeConfig) { set := func(target *string, value string) { if value != "" { *target = value } } set(&palette.Mode, custom.Mode) set(&palette.Title, custom.Title) set(&palette.Dim, custom.Dim) set(&palette.Text, custom.Text) set(&palette.ActiveForeground, custom.ActiveForeground) set(&palette.ActiveBackground, custom.ActiveBackground) set(&palette.Success, custom.Success) set(&palette.Warning, custom.Warning) set(&palette.Error, custom.Error) set(&palette.EditorForeground, custom.EditorForeground) set(&palette.EditorBackground, custom.EditorBackground) set(&palette.PaneInactive, custom.PaneInactive) set(&palette.PaneActive, custom.PaneActive) set(&palette.Quote, custom.Quote) set(&palette.SelectionBackground, custom.SelectionBackground) set(&palette.SuggestionRemoveBackground, custom.SuggestionRemoveBackground) set(&palette.SuggestionAddBackground, custom.SuggestionAddBackground) set(&palette.ChangedRemoveBackground, custom.ChangedRemoveBackground) set(&palette.ChangedAddBackground, custom.ChangedAddBackground) set(&palette.SyntaxTheme, custom.SyntaxTheme) if len(custom.AuthorPalette) > 0 { palette.AuthorPalette = append([]string(nil), custom.AuthorPalette...) } palette.NoColor, palette.HighContrast = false, false } func validateThemePalette(palette themePalette) error { if palette.Mode != "dark" && palette.Mode != "light" { return fmt.Errorf("mode must be dark or light") } colors := map[string]string{ "title": palette.Title, "dim": palette.Dim, "text": palette.Text, "active_foreground": palette.ActiveForeground, "active_background": palette.ActiveBackground, "success": palette.Success, "warning": palette.Warning, "error": palette.Error, "editor_foreground": palette.EditorForeground, "editor_background": palette.EditorBackground, "pane_inactive": palette.PaneInactive, "pane_active": palette.PaneActive, "quote": palette.Quote, "selection_background": palette.SelectionBackground, "suggestion_remove_background": palette.SuggestionRemoveBackground, "suggestion_add_background": palette.SuggestionAddBackground, "changed_remove_background": palette.ChangedRemoveBackground, "changed_add_background": palette.ChangedAddBackground, } for name, value := range colors { if !validHexColor(value) { return fmt.Errorf("%s must be a #RRGGBB color", name) } } if len(palette.AuthorPalette) == 0 { return fmt.Errorf("author_palette must contain at least one color") } for _, color := range palette.AuthorPalette { if !validHexColor(color) { return fmt.Errorf("author_palette contains invalid color %q", color) } } if _, ok := styles.Registry[palette.SyntaxTheme]; !ok { return fmt.Errorf("unknown Chroma syntax_theme %q", palette.SyntaxTheme) } return nil } func validHexColor(value string) bool { if len(value) != 7 || value[0] != '#' { return false } _, err := strconv.ParseUint(value[1:], 16, 24) return err == nil } func backgroundSequence(color string) string { value, err := strconv.ParseUint(strings.TrimPrefix(color, "#"), 16, 24) if err != nil { return "" } return fmt.Sprintf( "\x1b[48;2;%d;%d;%dm", (value>>16)&0xff, (value>>8)&0xff, value&0xff, ) } func foregroundSequence(color string) string { value, err := strconv.ParseUint(strings.TrimPrefix(color, "#"), 16, 24) if err != nil { return "" } return fmt.Sprintf( "\x1b[38;2;%d;%d;%dm", (value>>16)&0xff, (value>>8)&0xff, value&0xff, ) } func darkenColor(color lipgloss.Color) lipgloss.Color { value, err := strconv.ParseUint(strings.TrimPrefix(string(color), "#"), 16, 24) if err != nil { return color } const numerator, denominator = uint64(3), uint64(4) red := ((value >> 16) & 0xff) * numerator / denominator green := ((value >> 8) & 0xff) * numerator / denominator blue := (value & 0xff) * numerator / denominator return lipgloss.Color(fmt.Sprintf("#%02X%02X%02X", red, green, blue)) } func builtinThemePalettes() map[string]themePalette { dark := palette( "dark", "#F0B72F", "#777777", "#D7DAE8", "#FFFFFF", "#3B4261", "#67C587", "#E5C07B", "#E06C75", "#D7DAE8", "#2C3045", "#50566F", "#F0B72F", "#777777", "#18344F", "#370000", "#003700", "#370000", "#003700", "onedark", "#61AFEF", "#C678DD", "#56B6C2", "#E5C07B", "#E06C75", "#98C379", "#D19A66", "#7FC8FF", ) light := palette( "light", "#9A6700", "#656D76", "#24292F", "#FFFFFF", "#0969DA", "#1A7F37", "#9A6700", "#CF222E", "#24292F", "#DDE8FF", "#8C959F", "#0969DA", "#656D76", "#ADD6FF", "#FFD7D5", "#CCFFD8", "#FFAAAA", "#AAE6AA", "github", "#0550AE", "#8250DF", "#0A706F", "#9A6700", "#CF222E", "#116329", "#953800", "#0969DA", ) catMocha := palette( "dark", "#CBA6F7", "#6C7086", "#CDD6F4", "#1E1E2E", "#CBA6F7", "#A6E3A1", "#F9E2AF", "#F38BA8", "#CDD6F4", "#313244", "#45475A", "#CBA6F7", "#6C7086", "#313244", "#452B36", "#23402E", "#512B3A", "#254936", "catppuccin-mocha", "#89B4FA", "#CBA6F7", "#94E2D5", "#F9E2AF", "#F38BA8", "#A6E3A1", "#FAB387", ) catLatte := palette( "light", "#8839EF", "#8C8FA1", "#4C4F69", "#EFF1F5", "#8839EF", "#40A02B", "#DF8E1D", "#D20F39", "#4C4F69", "#DCE0E8", "#9CA0B0", "#8839EF", "#8C8FA1", "#CCD0DA", "#F2CDCD", "#C9E7CA", "#EFB8C0", "#B8DDB5", "catppuccin-latte", "#1E66F5", "#8839EF", "#179299", "#DF8E1D", "#D20F39", "#40A02B", "#FE640B", ) gruvbox := palette( "dark", "#FABD2F", "#928374", "#EBDBB2", "#282828", "#458588", "#B8BB26", "#FABD2F", "#FB4934", "#EBDBB2", "#3C3836", "#665C54", "#D3869B", "#928374", "#3C3836", "#4C2828", "#324028", "#5A2929", "#354929", "gruvbox", "#83A598", "#D3869B", "#8EC07C", "#FABD2F", "#FB4934", "#B8BB26", "#FE8019", ) gruvboxLight := palette( "light", "#D79921", "#928374", "#3C3836", "#FBF1C7", "#458588", "#98971A", "#D79921", "#CC241D", "#3C3836", "#EBDBB2", "#A89984", "#B16286", "#928374", "#D5C4A1", "#F2C8C5", "#D8E0B0", "#E9B9B3", "#C9D59A", "gruvbox-light", "#458588", "#B16286", "#689D6A", "#D79921", "#CC241D", "#98971A", "#D65D0E", ) oneDark := palette( "dark", "#E5C07B", "#5C6370", "#ABB2BF", "#FFFFFF", "#3E4451", "#98C379", "#E5C07B", "#E06C75", "#ABB2BF", "#2C313C", "#4B5263", "#61AFEF", "#5C6370", "#2C313C", "#4B2B31", "#2D4032", "#562D35", "#314A35", "onedark", "#61AFEF", "#C678DD", "#56B6C2", "#E5C07B", "#E06C75", "#98C379", "#D19A66", ) githubDark := palette( "dark", "#D29922", "#8B949E", "#E6EDF3", "#FFFFFF", "#1F6FEB", "#3FB950", "#D29922", "#F85149", "#E6EDF3", "#161B22", "#30363D", "#58A6FF", "#8B949E", "#1F2937", "#4A2028", "#183D2A", "#5A222C", "#1C4A30", "github-dark", "#58A6FF", "#BC8CFF", "#39C5CF", "#D29922", "#F85149", "#3FB950", "#DB6D28", ) highContrast := palette( "dark", "#FFFF00", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#000000", "#00FF00", "#FFFF00", "#FF5555", "#FFFFFF", "#000000", "#FFFFFF", "#FFFF00", "#FFFFFF", "#000080", "#5F0000", "#005F00", "#5F0000", "#005F00", "github-dark", "#00FFFF", "#FF55FF", "#FFFF00", "#00FF00", "#FFFFFF", ) highContrast.HighContrast = true noColor := dark noColor.NoColor = true return map[string]themePalette{ "dark": dark, "light": light, "catppuccin": catMocha, "catppuccin-mocha": catMocha, "catppuccin-latte": catLatte, "gruvbox": gruvbox, "gruvbox-dark": gruvbox, "gruvbox-light": gruvboxLight, "one-dark-pro": oneDark, "onedark": oneDark, "github": githubDark, "github-dark": githubDark, "github-light": light, "high-contrast": highContrast, "no-color": noColor, } } func palette( mode, title, dim, text, activeFG, activeBG, success, warning, failure, editorFG, editorBG, paneInactive, paneActive, quote, selection, suggestionRemove, suggestionAdd, changedRemove, changedAdd, syntax string, authors ...string, ) themePalette { return themePalette{ Mode: mode, Title: title, Dim: dim, Text: text, ActiveForeground: activeFG, ActiveBackground: activeBG, Success: success, Warning: warning, Error: failure, EditorForeground: editorFG, EditorBackground: editorBG, PaneInactive: paneInactive, PaneActive: paneActive, Quote: quote, SelectionBackground: selection, SuggestionRemoveBackground: suggestionRemove, SuggestionAddBackground: suggestionAdd, ChangedRemoveBackground: changedRemove, ChangedAddBackground: changedAdd, AuthorPalette: append([]string(nil), authors...), SyntaxTheme: syntax, } }