diff options
| author | adamdottv <[email protected]> | 2025-04-30 11:05:59 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-04-30 11:05:59 -0500 |
| commit | 91ae9b33d37df7a53bda958d787268ef0f917ffd (patch) | |
| tree | 27d1ceb87f30c666e20ebf6c7baeb8e1bd34693a /internal | |
| parent | a42175c067dd6b3e594d1e8de4f39a441bd9603b (diff) | |
| download | opencode-91ae9b33d37df7a53bda958d787268ef0f917ffd.tar.gz opencode-91ae9b33d37df7a53bda958d787268ef0f917ffd.zip | |
feat: custom themes
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/config/config.go | 9 | ||||
| -rw-r--r-- | internal/tui/styles/icons.go | 1 | ||||
| -rw-r--r-- | internal/tui/theme/manager.go | 101 | ||||
| -rw-r--r-- | internal/tui/theme/theme.go | 51 |
4 files changed, 155 insertions, 7 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index 8d6ad39d8..88f3a1838 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -68,7 +68,8 @@ type LSPConfig struct { // TUIConfig defines the configuration for the Terminal User Interface. type TUIConfig struct { - Theme string `json:"theme,omitempty"` + Theme string `json:"theme,omitempty"` + CustomTheme map[string]any `json:"customTheme,omitempty"` } // Config is the main configuration structure for the application. @@ -747,16 +748,16 @@ func UpdateTheme(themeName string) error { } // Parse the JSON - var configMap map[string]interface{} + var configMap map[string]any if err := json.Unmarshal(configData, &configMap); err != nil { return fmt.Errorf("failed to parse config file: %w", err) } // Update just the theme value - tuiConfig, ok := configMap["tui"].(map[string]interface{}) + tuiConfig, ok := configMap["tui"].(map[string]any) if !ok { // TUI config doesn't exist yet, create it - configMap["tui"] = map[string]interface{}{"theme": themeName} + configMap["tui"] = map[string]any{"theme": themeName} } else { // Update existing TUI config tuiConfig["theme"] = themeName diff --git a/internal/tui/styles/icons.go b/internal/tui/styles/icons.go index 314e8d85b..985465cc8 100644 --- a/internal/tui/styles/icons.go +++ b/internal/tui/styles/icons.go @@ -11,4 +11,3 @@ const ( SpinnerIcon string = "..." LoadingIcon string = "⟳" ) - diff --git a/internal/tui/theme/manager.go b/internal/tui/theme/manager.go index a81ba45c1..7bb887ffa 100644 --- a/internal/tui/theme/manager.go +++ b/internal/tui/theme/manager.go @@ -25,6 +25,9 @@ var globalManager = &Manager{ currentName: "", } +// Default theme instance for custom theme defaulting +var defaultThemeColors = NewOpenCodeTheme() + // RegisterTheme adds a new theme to the registry. // If this is the first theme registered, it becomes the default. func RegisterTheme(name string, theme Theme) { @@ -46,7 +49,22 @@ func SetTheme(name string) error { defer globalManager.mu.Unlock() delete(styles.Registry, "charm") - if _, exists := globalManager.themes[name]; !exists { + + // Handle custom theme + if name == "custom" { + cfg := config.Get() + if cfg == nil || cfg.TUI.CustomTheme == nil || len(cfg.TUI.CustomTheme) == 0 { + return fmt.Errorf("custom theme selected but no custom theme colors defined in config") + } + + customTheme, err := LoadCustomTheme(cfg.TUI.CustomTheme) + if err != nil { + return fmt.Errorf("failed to load custom theme: %w", err) + } + + // Register the custom theme + globalManager.themes["custom"] = customTheme + } else if _, exists := globalManager.themes[name]; !exists { return fmt.Errorf("theme '%s' not found", name) } @@ -111,6 +129,87 @@ func GetTheme(name string) Theme { return globalManager.themes[name] } +// LoadCustomTheme creates a new theme instance based on the custom theme colors +// defined in the configuration. It uses the default OpenCode theme as a base +// and overrides colors that are specified in the customTheme map. +func LoadCustomTheme(customTheme map[string]any) (Theme, error) { + // Create a new theme based on the default OpenCode theme + theme := NewOpenCodeTheme() + + // Process each color in the custom theme map + for key, value := range customTheme { + adaptiveColor, err := ParseAdaptiveColor(value) + if err != nil { + logging.Warn("Invalid color definition in custom theme", "key", key, "error", err) + continue // Skip this color but continue processing others + } + + // Set the color in the theme based on the key + switch strings.ToLower(key) { + case "primary": + theme.PrimaryColor = adaptiveColor + case "secondary": + theme.SecondaryColor = adaptiveColor + case "accent": + theme.AccentColor = adaptiveColor + case "error": + theme.ErrorColor = adaptiveColor + case "warning": + theme.WarningColor = adaptiveColor + case "success": + theme.SuccessColor = adaptiveColor + case "info": + theme.InfoColor = adaptiveColor + case "text": + theme.TextColor = adaptiveColor + case "textmuted": + theme.TextMutedColor = adaptiveColor + case "textemphasized": + theme.TextEmphasizedColor = adaptiveColor + case "background": + theme.BackgroundColor = adaptiveColor + case "backgroundsecondary": + theme.BackgroundSecondaryColor = adaptiveColor + case "backgrounddarker": + theme.BackgroundDarkerColor = adaptiveColor + case "bordernormal": + theme.BorderNormalColor = adaptiveColor + case "borderfocused": + theme.BorderFocusedColor = adaptiveColor + case "borderdim": + theme.BorderDimColor = adaptiveColor + case "diffadded": + theme.DiffAddedColor = adaptiveColor + case "diffremoved": + theme.DiffRemovedColor = adaptiveColor + case "diffcontext": + theme.DiffContextColor = adaptiveColor + case "diffhunkheader": + theme.DiffHunkHeaderColor = adaptiveColor + case "diffhighlightadded": + theme.DiffHighlightAddedColor = adaptiveColor + case "diffhighlightremoved": + theme.DiffHighlightRemovedColor = adaptiveColor + case "diffaddedbg": + theme.DiffAddedBgColor = adaptiveColor + case "diffremovedbg": + theme.DiffRemovedBgColor = adaptiveColor + case "diffcontextbg": + theme.DiffContextBgColor = adaptiveColor + case "difflinenumber": + theme.DiffLineNumberColor = adaptiveColor + case "diffaddedlinenumberbg": + theme.DiffAddedLineNumberBgColor = adaptiveColor + case "diffremovedlinenumberbg": + theme.DiffRemovedLineNumberBgColor = adaptiveColor + default: + logging.Warn("Unknown color key in custom theme", "key", key) + } + } + + return theme, nil +} + // updateConfigTheme updates the theme setting in the configuration file func updateConfigTheme(themeName string) error { // Use the config package to update the theme diff --git a/internal/tui/theme/theme.go b/internal/tui/theme/theme.go index 4ee14a07f..d0c64eb43 100644 --- a/internal/tui/theme/theme.go +++ b/internal/tui/theme/theme.go @@ -1,6 +1,9 @@ package theme import ( + "fmt" + "regexp" + "github.com/charmbracelet/lipgloss" ) @@ -205,4 +208,50 @@ func (t *BaseTheme) SyntaxString() lipgloss.AdaptiveColor { return t.SyntaxStrin func (t *BaseTheme) SyntaxNumber() lipgloss.AdaptiveColor { return t.SyntaxNumberColor } func (t *BaseTheme) SyntaxType() lipgloss.AdaptiveColor { return t.SyntaxTypeColor } func (t *BaseTheme) SyntaxOperator() lipgloss.AdaptiveColor { return t.SyntaxOperatorColor } -func (t *BaseTheme) SyntaxPunctuation() lipgloss.AdaptiveColor { return t.SyntaxPunctuationColor }
\ No newline at end of file +func (t *BaseTheme) SyntaxPunctuation() lipgloss.AdaptiveColor { return t.SyntaxPunctuationColor } + +// ParseAdaptiveColor parses a color value from the config file into a lipgloss.AdaptiveColor. +// It accepts either a string (hex color) or a map with "dark" and "light" keys. +func ParseAdaptiveColor(value any) (lipgloss.AdaptiveColor, error) { + // Regular expression to validate hex color format + hexColorRegex := regexp.MustCompile(`^#[0-9a-fA-F]{6}$`) + + // Case 1: String value (same color for both dark and light modes) + if hexColor, ok := value.(string); ok { + if !hexColorRegex.MatchString(hexColor) { + return lipgloss.AdaptiveColor{}, fmt.Errorf("invalid hex color format: %s", hexColor) + } + return lipgloss.AdaptiveColor{ + Dark: hexColor, + Light: hexColor, + }, nil + } + + // Case 2: Map with dark and light keys + if colorMap, ok := value.(map[string]any); ok { + darkVal, darkOk := colorMap["dark"] + lightVal, lightOk := colorMap["light"] + + if !darkOk || !lightOk { + return lipgloss.AdaptiveColor{}, fmt.Errorf("color map must contain both 'dark' and 'light' keys") + } + + darkHex, darkIsString := darkVal.(string) + lightHex, lightIsString := lightVal.(string) + + if !darkIsString || !lightIsString { + return lipgloss.AdaptiveColor{}, fmt.Errorf("color values must be strings") + } + + if !hexColorRegex.MatchString(darkHex) || !hexColorRegex.MatchString(lightHex) { + return lipgloss.AdaptiveColor{}, fmt.Errorf("invalid hex color format") + } + + return lipgloss.AdaptiveColor{ + Dark: darkHex, + Light: lightHex, + }, nil + } + + return lipgloss.AdaptiveColor{}, fmt.Errorf("color must be either a hex string or an object with dark/light keys") +}
\ No newline at end of file |
