| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- // Package config manages application configuration from various sources.
- package config
- import (
- "encoding/json"
- "fmt"
- "log/slog"
- "os"
- "os/user"
- "path/filepath"
- "strings"
- "github.com/spf13/viper"
- )
- // Data defines storage configuration.
- type Data struct {
- Directory string `json:"directory,omitempty"`
- }
- // TUIConfig defines the configuration for the Terminal User Interface.
- type TUIConfig struct {
- Theme string `json:"theme,omitempty"`
- CustomTheme map[string]any `json:"customTheme,omitempty"`
- }
- // ShellConfig defines the configuration for the shell used by the bash tool.
- type ShellConfig struct {
- Path string `json:"path,omitempty"`
- Args []string `json:"args,omitempty"`
- }
- // Config is the main configuration structure for the application.
- type Config struct {
- Data Data `json:"data"`
- WorkingDir string `json:"wd,omitempty"`
- Debug bool `json:"debug,omitempty"`
- DebugLSP bool `json:"debugLSP,omitempty"`
- ContextPaths []string `json:"contextPaths,omitempty"`
- TUI TUIConfig `json:"tui"`
- Shell ShellConfig `json:"shell,omitempty"`
- }
- // Application constants
- const (
- defaultDataDirectory = ".opencode"
- defaultLogLevel = "info"
- appName = "opencode"
- MaxTokensFallbackDefault = 4096
- )
- var defaultContextPaths = []string{
- ".github/copilot-instructions.md",
- ".cursorrules",
- ".cursor/rules/",
- "CLAUDE.md",
- "CLAUDE.local.md",
- "CONTEXT.md",
- "CONTEXT.local.md",
- "opencode.md",
- "opencode.local.md",
- "OpenCode.md",
- "OpenCode.local.md",
- "OPENCODE.md",
- "OPENCODE.local.md",
- }
- // Global configuration instance
- var cfg *Config
- // Load initializes the configuration from environment variables and config files.
- // If debug is true, debug mode is enabled and log level is set to debug.
- // It returns an error if configuration loading fails.
- func Load(workingDir string, debug bool) (*Config, error) {
- if cfg != nil {
- return cfg, nil
- }
- cfg = &Config{
- WorkingDir: workingDir,
- }
- configureViper()
- setDefaults(debug)
- // Read global config
- if err := readConfig(viper.ReadInConfig()); err != nil {
- return cfg, err
- }
- // Load and merge local config
- mergeLocalConfig(workingDir)
- // Apply configuration to the struct
- if err := viper.Unmarshal(cfg); err != nil {
- return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
- }
- defaultLevel := slog.LevelInfo
- if cfg.Debug {
- defaultLevel = slog.LevelDebug
- }
- slog.SetLogLoggerLevel(defaultLevel)
- // Validate configuration
- if err := Validate(); err != nil {
- return cfg, fmt.Errorf("config validation failed: %w", err)
- }
- return cfg, nil
- }
- // configureViper sets up viper's configuration paths and environment variables.
- func configureViper() {
- viper.SetConfigName(fmt.Sprintf(".%s", appName))
- viper.SetConfigType("json")
- viper.AddConfigPath("$HOME")
- viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
- viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
- viper.SetEnvPrefix(strings.ToUpper(appName))
- viper.AutomaticEnv()
- }
- // setDefaults configures default values for configuration options.
- func setDefaults(debug bool) {
- viper.SetDefault("data.directory", defaultDataDirectory)
- viper.SetDefault("contextPaths", defaultContextPaths)
- viper.SetDefault("tui.theme", "opencode")
- if debug {
- viper.SetDefault("debug", true)
- viper.Set("log.level", "debug")
- } else {
- viper.SetDefault("debug", false)
- viper.SetDefault("log.level", defaultLogLevel)
- }
- }
- // readConfig handles the result of reading a configuration file.
- func readConfig(err error) error {
- if err == nil {
- return nil
- }
- // It's okay if the config file doesn't exist
- if _, ok := err.(viper.ConfigFileNotFoundError); ok {
- return nil
- }
- return fmt.Errorf("failed to read config: %w", err)
- }
- // mergeLocalConfig loads and merges configuration from the local directory.
- func mergeLocalConfig(workingDir string) {
- local := viper.New()
- local.SetConfigName(fmt.Sprintf(".%s", appName))
- local.SetConfigType("json")
- local.AddConfigPath(workingDir)
- // Merge local config if it exists
- if err := local.ReadInConfig(); err == nil {
- viper.MergeConfigMap(local.AllSettings())
- }
- }
- // Validate checks if the configuration is valid and applies defaults where needed.
- func Validate() error {
- if cfg == nil {
- return fmt.Errorf("config not loaded")
- }
- return nil
- }
- // Get returns the current configuration.
- // It's safe to call this function multiple times.
- func Get() *Config {
- return cfg
- }
- // WorkingDirectory returns the current working directory from the configuration.
- func WorkingDirectory() string {
- if cfg == nil {
- panic("config not loaded")
- }
- return cfg.WorkingDir
- }
- // GetHostname returns the system hostname or "User" if it can't be determined
- func GetHostname() (string, error) {
- hostname, err := os.Hostname()
- if err != nil {
- return "User", err
- }
- return hostname, nil
- }
- // GetUsername returns the current user's username
- func GetUsername() (string, error) {
- currentUser, err := user.Current()
- if err != nil {
- return "User", err
- }
- return currentUser.Username, nil
- }
- func updateCfgFile(updateCfg func(config *Config)) error {
- if cfg == nil {
- return fmt.Errorf("config not loaded")
- }
- // 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))
- slog.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 userCfg *Config
- if err := json.Unmarshal(configData, &userCfg); err != nil {
- return fmt.Errorf("failed to parse config file: %w", err)
- }
- updateCfg(userCfg)
- // Write the updated config back to file
- updatedData, err := json.MarshalIndent(userCfg, "", " ")
- 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
- }
- // 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
- // Update the file config
- return updateCfgFile(func(config *Config) {
- config.TUI.Theme = themeName
- })
- }
|