feat: themes (#113)

* feat: themes

* feat: flexoki theme

* feat: onedark theme

* feat: monokai pro theme

* feat: opencode theme (default)

* feat: dracula theme

* feat: tokyonight theme

* feat: tron theme

* some small fixes

---------

Co-authored-by: Kujtim Hoxha <kujtimii.h@gmail.com>
This commit is contained in:
Adam 2025-05-01 05:49:26 -05:00 committed by GitHub
commit 82de14371d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 4595 additions and 1923 deletions

View file

@ -2,9 +2,11 @@
package config
import (
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/opencode-ai/opencode/internal/llm/models"
@ -65,6 +67,11 @@ type LSPConfig struct {
Options any `json:"options"`
}
// TUIConfig defines the configuration for the Terminal User Interface.
type TUIConfig struct {
Theme string `json:"theme,omitempty"`
}
// Config is the main configuration structure for the application.
type Config struct {
Data Data `json:"data"`
@ -76,6 +83,7 @@ type Config struct {
Debug bool `json:"debug,omitempty"`
DebugLSP bool `json:"debugLSP,omitempty"`
ContextPaths []string `json:"contextPaths,omitempty"`
TUI TUIConfig `json:"tui"`
}
// Application constants
@ -203,6 +211,7 @@ func configureViper() {
func setDefaults(debug bool) {
viper.SetDefault("data.directory", defaultDataDirectory)
viper.SetDefault("contextPaths", defaultContextPaths)
viper.SetDefault("tui.theme", "opencode")
if debug {
viper.SetDefault("debug", true)
@ -714,3 +723,62 @@ func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error {
return nil
}
// UpdateTheme updates the theme in the configuration and writes it to the config file.
func UpdateTheme(themeName string) error {
if cfg == nil {
return fmt.Errorf("config not loaded")
}
// Update the in-memory config
cfg.TUI.Theme = themeName
// Get the config file path
configFile := viper.ConfigFileUsed()
var configData []byte
if configFile == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
configFile = filepath.Join(homeDir, fmt.Sprintf(".%s.json", appName))
logging.Info("config file not found, creating new one", "path", configFile)
configData = []byte(`{}`)
} else {
// Read the existing config file
data, err := os.ReadFile(configFile)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
configData = data
}
// Parse the JSON
var configMap map[string]interface{}
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{})
if !ok {
// TUI config doesn't exist yet, create it
configMap["tui"] = map[string]interface{}{"theme": themeName}
} else {
// Update existing TUI config
tuiConfig["theme"] = themeName
configMap["tui"] = tuiConfig
}
// Write the updated config back to file
updatedData, err := json.MarshalIndent(configMap, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(configFile, updatedData, 0o644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}