344 lines
13 KiB
Go
344 lines
13 KiB
Go
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()
|
|
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,
|
|
}
|
|
}
|