package config import ( "fmt" "log" "os" "path/filepath" "github.com/muyue/muyue/internal/secret" "github.com/muyue/muyue/internal/version" "gopkg.in/yaml.v3" ) type Profile struct { Name string `yaml:"name" json:"name"` Pseudo string `yaml:"pseudo" json:"pseudo"` Email string `yaml:"email" json:"email"` Languages []string `yaml:"languages" json:"languages"` Preferences struct { Editor string `yaml:"editor" json:"editor"` Shell string `yaml:"shell" json:"shell"` Theme string `yaml:"theme" json:"theme"` DefaultAI string `yaml:"default_ai" json:"default_ai"` AutoUpdate bool `yaml:"auto_update" json:"auto_update"` CheckOnStart bool `yaml:"check_on_start" json:"check_on_start"` Language string `yaml:"language" json:"language"` KeyboardLayout string `yaml:"keyboard_layout" json:"keyboard_layout"` } `yaml:"preferences" json:"preferences"` } type AIProvider struct { Name string `yaml:"name" json:"name"` APIKey string `yaml:"api_key,omitempty" json:"api_key,omitempty"` BaseURL string `yaml:"base_url,omitempty" json:"base_url,omitempty"` Model string `yaml:"model" json:"model"` Active bool `yaml:"active" json:"active"` } type ToolConfig struct { Name string `yaml:"name" json:"name"` Installed bool `yaml:"installed" json:"installed"` Version string `yaml:"version" json:"version"` AutoUpdate bool `yaml:"auto_update" json:"auto_update"` } type SSHConnection struct { Name string `yaml:"name" json:"name"` Host string `yaml:"host" json:"host"` Port int `yaml:"port" json:"port"` User string `yaml:"user" json:"user"` Password string `yaml:"password,omitempty" json:"password,omitempty"` KeyPath string `yaml:"key_path,omitempty" json:"key_path,omitempty"` } type MuyueConfig struct { Version string `yaml:"version" json:"version"` Profile Profile `yaml:"profile" json:"profile"` AI struct { Providers []AIProvider `yaml:"providers" json:"providers"` } `yaml:"ai" json:"ai"` Tools []ToolConfig `yaml:"tools" json:"tools"` BMAD struct { Installed bool `yaml:"installed" json:"installed"` Version string `yaml:"version" json:"version"` Global bool `yaml:"global" json:"global"` } `yaml:"bmad" json:"bmad"` Terminal struct { CustomPrompt bool `yaml:"custom_prompt" json:"custom_prompt"` PromptTheme string `yaml:"prompt_theme" json:"prompt_theme"` SSH []SSHConnection `yaml:"ssh" json:"ssh"` FontSize int `yaml:"font_size" json:"font_size"` FontFamily string `yaml:"font_family" json:"font_family"` Theme string `yaml:"theme" json:"theme"` } `yaml:"terminal" json:"terminal"` } type TerminalTheme struct { Name string `yaml:"name"` Background string `yaml:"background"` Foreground string `yaml:"foreground"` Cursor string `yaml:"cursor"` Black string `yaml:"black"` Red string `yaml:"red"` Green string `yaml:"green"` Yellow string `yaml:"yellow"` Blue string `yaml:"blue"` Magenta string `yaml:"magenta"` Cyan string `yaml:"cyan"` White string `yaml:"white"` } var DEFAULT_TERMINAL_THEMES = map[string]TerminalTheme{ "default": { Name: "Default", Background: "#0A0A0C", Foreground: "#EAE0E2", Cursor: "#FF0033", Black: "#0A0A0C", Red: "#FF0033", Green: "#00E676", Yellow: "#FFD740", Blue: "#448AFF", Magenta: "#FF1A5E", Cyan: "#00BCD4", White: "#EAE0E2", }, "monokai": { Name: "Monokai", Background: "#272822", Foreground: "#F8F8F2", Cursor: "#F8F8F0", Black: "#272822", Red: "#F92672", Green: "#A6E22E", Yellow: "#E6DB74", Blue: "#66D9EF", Magenta: "#AE81FF", Cyan: "#A1EFE4", White: "#F8F8F2", }, "gruvbox": { Name: "Gruvbox", Background: "#282828", Foreground: "#EBDBB2", Cursor: "#FB4934", Black: "#282828", Red: "#CC241D", Green: "#98971A", Yellow: "#D79921", Blue: "#458588", Magenta: "#B16286", Cyan: "#689D6A", White: "#EBDBB2", }, "nord": { Name: "Nord", Background: "#2E3440", Foreground: "#D8DEE9", Cursor: "#D8DEE9", Black: "#2E3440", Red: "#BF616A", Green: "#A3BE8C", Yellow: "#EBCB8B", Blue: "#81A1C1", Magenta: "#B48EAD", Cyan: "#88C0D0", White: "#D8DEE9", }, "solarized-dark": { Name: "Solarized Dark", Background: "#002B36", Foreground: "#839496", Cursor: "#D33682", Black: "#002B36", Red: "#DC322F", Green: "#859900", Yellow: "#B58900", Blue: "#268BD2", Magenta: "#D33682", Cyan: "#2AA198", White: "#FDF6E3", }, "dracula": { Name: "Dracula", Background: "#282A36", Foreground: "#F8F8F2", Cursor: "#F8F8F2", Black: "#282A36", Red: "#FF5555", Green: "#50FA7B", Yellow: "#F1FA8C", Blue: "#BD93F9", Magenta: "#FF79C6", Cyan: "#8BE9FD", White: "#F8F8F2", }, } func GetTerminalTheme(name string) TerminalTheme { if theme, ok := DEFAULT_TERMINAL_THEMES[name]; ok { return theme } return DEFAULT_TERMINAL_THEMES["default"] } func ConfigDir() (string, error) { configDir, err := os.UserConfigDir() if err != nil { return "", err } dir := filepath.Join(configDir, "muyue") legacyDir := filepath.Join(homeDir(), ".muyue") if _, err := os.Stat(legacyDir); err == nil { if _, err := os.Stat(dir); err != nil { if err := os.Rename(legacyDir, dir); err != nil { log.Printf("config migration: rename %s to %s: %v", legacyDir, dir, err) } } } return dir, nil } func homeDir() string { home, err := os.UserHomeDir() if err != nil { return "/" } return home } func ConfigPath() (string, error) { dir, err := ConfigDir() if err != nil { return "", err } return filepath.Join(dir, "config.yaml"), nil } func Exists() bool { path, err := ConfigPath() if err != nil { return false } _, err = os.Stat(path) return err == nil } func Load() (*MuyueConfig, error) { path, err := ConfigPath() if err != nil { return nil, err } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading config: %w", err) } var cfg MuyueConfig if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parsing config: %w", err) } // Decrypt API keys for i := range cfg.AI.Providers { if cfg.AI.Providers[i].APIKey != "" { decrypted, err := secret.Decrypt(cfg.AI.Providers[i].APIKey) if err != nil { decrypted = cfg.AI.Providers[i].APIKey } cfg.AI.Providers[i].APIKey = decrypted } } return &cfg, nil } func Save(cfg *MuyueConfig) error { dir, err := ConfigDir() if err != nil { return err } if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("creating config dir: %w", err) } // Encrypt API keys before saving saveCfg := *cfg saveCfg.AI.Providers = make([]AIProvider, len(cfg.AI.Providers)) for i, p := range cfg.AI.Providers { saveCfg.AI.Providers[i] = p if p.APIKey != "" && !secret.IsEncrypted(p.APIKey) { enc, err := secret.Encrypt(p.APIKey) if err == nil { saveCfg.AI.Providers[i].APIKey = enc } } } path := filepath.Join(dir, "config.yaml") data, err := yaml.Marshal(&saveCfg) if err != nil { return fmt.Errorf("marshaling config: %w", err) } if err := os.WriteFile(path, data, 0600); err != nil { return fmt.Errorf("writing config: %w", err) } return nil } func Default() *MuyueConfig { cfg := &MuyueConfig{ Version: version.Version, Profile: Profile{ Name: "", Pseudo: "muyue", Languages: []string{}, }, } cfg.Profile.Preferences.DefaultAI = "minimax" cfg.Profile.Preferences.AutoUpdate = true cfg.Profile.Preferences.CheckOnStart = true cfg.Profile.Preferences.Theme = "charm" cfg.Profile.Preferences.Language = "fr" cfg.Profile.Preferences.KeyboardLayout = "azerty" cfg.AI.Providers = []AIProvider{ { Name: "minimax", Model: "MiniMax-M2.7", BaseURL: "https://api.minimax.io/v1", Active: true, }, { Name: "zai", Model: "glm", Active: false, }, { Name: "anthropic", Model: "claude-sonnet-4-20250514", Active: false, }, { Name: "openai", Model: "gpt-4o", BaseURL: "https://api.openai.com/v1", Active: false, }, { Name: "ollama", Model: "llama3", BaseURL: "http://localhost:11434/api", Active: false, }, } cfg.BMAD.Global = true cfg.Terminal.CustomPrompt = true cfg.Terminal.PromptTheme = "zerotwo" return cfg }