package api import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" "github.com/muyue/muyue/internal/config" ) func (s *Server) handleUpdatePreferences(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { writeError(w, "PUT only", http.StatusMethodNotAllowed) return } if s.config == nil { writeError(w, "no config", http.StatusNotFound) return } var body struct { Language string `json:"language"` KeyboardLayout string `json:"keyboard_layout"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } if body.Language != "" { s.config.Profile.Preferences.Language = body.Language } if body.KeyboardLayout != "" { s.config.Profile.Preferences.KeyboardLayout = body.KeyboardLayout } if err := config.Save(s.config); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "ok"}) } func (s *Server) handleSaveProfile(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { writeError(w, "PUT only", http.StatusMethodNotAllowed) return } if s.config == nil { writeError(w, "no config", http.StatusNotFound) return } var body struct { Name string `json:"name"` Pseudo string `json:"pseudo"` Email string `json:"email"` Editor string `json:"editor"` Shell string `json:"shell"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } if body.Name != "" { s.config.Profile.Name = body.Name } if body.Pseudo != "" { s.config.Profile.Pseudo = body.Pseudo } if body.Email != "" { s.config.Profile.Email = body.Email } if body.Editor != "" { s.config.Profile.Preferences.Editor = body.Editor } if body.Shell != "" { s.config.Profile.Preferences.Shell = body.Shell } if err := config.Save(s.config); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "ok"}) } func (s *Server) handleSaveProvider(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { writeError(w, "PUT only", http.StatusMethodNotAllowed) return } if s.config == nil { writeError(w, "no config", http.StatusNotFound) return } var body struct { Name string `json:"name"` APIKey string `json:"api_key"` Model string `json:"model"` BaseURL string `json:"base_url"` Active *bool `json:"active"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } if body.Name == "" { writeError(w, "name required", http.StatusBadRequest) return } found := false for i := range s.config.AI.Providers { if s.config.AI.Providers[i].Name == body.Name { if body.APIKey != "" { s.config.AI.Providers[i].APIKey = body.APIKey } if body.Model != "" { s.config.AI.Providers[i].Model = body.Model } if body.BaseURL != "" { s.config.AI.Providers[i].BaseURL = body.BaseURL } if body.Active != nil { if *body.Active { for j := range s.config.AI.Providers { s.config.AI.Providers[j].Active = false } } s.config.AI.Providers[i].Active = *body.Active } found = true break } } if !found { writeError(w, "provider not found", http.StatusNotFound) return } if err := config.Save(s.config); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "ok"}) } func (s *Server) handleValidateProvider(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } var body struct { Name string `json:"name"` APIKey string `json:"api_key"` Model string `json:"model"` BaseURL string `json:"base_url"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } if body.APIKey == "" { writeError(w, "api_key required", http.StatusBadRequest) return } baseURL := body.BaseURL if baseURL == "" { for _, p := range s.config.AI.Providers { if p.Name == body.Name { baseURL = p.BaseURL break } } } if baseURL == "" { switch body.Name { case "minimax": baseURL = "https://api.minimax.io/v1" case "openai": baseURL = "https://api.openai.com/v1" case "anthropic": baseURL = "https://api.anthropic.com/v1" default: baseURL = "https://api.minimax.io/v1" } } model := body.Model if model == "" { for _, p := range s.config.AI.Providers { if p.Name == body.Name { model = p.Model break } } } if model == "" { model = "MiniMax-M2.7" } reqBody, _ := json.Marshal(map[string]interface{}{ "model": model, "messages": []map[string]string{{"role": "user", "content": "Hi"}}, "max_tokens": 5, "stream": false, }) url := strings.TrimRight(baseURL, "/") + "/chat/completions" req, err := http.NewRequest("POST", url, bytes.NewReader(reqBody)) if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+body.APIKey) client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(req) if err != nil { writeError(w, "connection failed: "+err.Error(), http.StatusBadGateway) return } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { writeError(w, "invalid_api_key", http.StatusUnauthorized) return } if resp.StatusCode != http.StatusOK { writeError(w, "api_error: "+string(respBody), http.StatusBadGateway) return } writeJSON(w, map[string]interface{}{"status": "valid"}) } func (s *Server) handleSaveTerminalSettings(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { writeError(w, "PUT only", http.StatusMethodNotAllowed) return } if s.config == nil { writeError(w, "no config", http.StatusNotFound) return } var body struct { FontSize int `json:"font_size"` FontFamily string `json:"font_family"` Theme string `json:"theme"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } if body.FontSize > 0 { s.config.Terminal.FontSize = body.FontSize } if body.FontFamily != "" { s.config.Terminal.FontFamily = body.FontFamily } if body.Theme != "" { s.config.Terminal.Theme = body.Theme } if err := config.Save(s.config); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]interface{}{ "status": "ok", "theme": config.GetTerminalTheme(s.config.Terminal.Theme), }) } func (s *Server) handleGetTerminalThemes(w http.ResponseWriter, r *http.Request) { themes := make([]map[string]string, 0, len(config.DEFAULT_TERMINAL_THEMES)) for id, theme := range config.DEFAULT_TERMINAL_THEMES { themes = append(themes, map[string]string{ "id": id, "name": theme.Name, }) } writeJSON(w, map[string]interface{}{"themes": themes}) } func (s *Server) handleResetConfig(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } dir, err := config.ConfigDir() if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } path := filepath.Join(dir, "config.yaml") if err := os.Remove(path); err != nil && !os.IsNotExist(err) { writeError(w, err.Error(), http.StatusInternalServerError) return } s.config = config.Default() if err := config.Save(s.config); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "ok"}) } func (s *Server) handleApplyStarshipTheme(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } var body struct { Theme string `json:"theme"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } if body.Theme == "" { body.Theme = s.config.Terminal.PromptTheme } cfgDir, err := config.ConfigDir() if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } starshipDir := filepath.Join(cfgDir, "starship") if err := os.MkdirAll(starshipDir, 0755); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } themeFile := filepath.Join(starshipDir, "starship.toml") themeContent := getStarshipThemeConfig(body.Theme) if err := os.WriteFile(themeFile, []byte(themeContent), 0644); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } home, _ := os.UserHomeDir() shellRCs := []string{ filepath.Join(home, ".bashrc"), filepath.Join(home, ".zshrc"), } for _, rc := range shellRCs { if _, err := os.Stat(rc); err != nil { continue } content, _ := os.ReadFile(rc) if strings.Contains(string(content), "STARSHIP_CONFIG") { continue } exportLine := fmt.Sprintf("\n# Muyue Starship config\nexport STARSHIP_CONFIG=%s\n", themeFile) f, err := os.OpenFile(rc, os.O_APPEND|os.O_WRONLY, 0644) if err != nil { continue } f.WriteString(exportLine) f.Close() } s.config.Terminal.PromptTheme = body.Theme config.Save(s.config) writeJSON(w, map[string]interface{}{"status": "ok", "config": themeFile}) } func getStarshipThemeConfig(theme string) string { switch theme { case "charm": return `[format] before_format = "$" format = """ $username$directory$git_branch$git_status$cmd_duration$line_break$character""" [character] success_symbol = "[➜](bold #00E676)" error_symbol = "[✗](bold #FF0033)" [directory] truncation_length = 3 truncation_symbol = "…/" style = "bold #00BCD4" [username] show_on_left = false style_user = "bold #FF0033" style_root = "bold #FF0033" [git_branch] symbol = " " format = "on [$symbol$branch]($style)" style = "bold #FFD740" [git_status] format = "[$all_status$ahead_behind]($style) " style = "bold #FF1A5E" conflicted = "!" untracked = "?" modified = "~" staged = "[+]" renamed = "»" deleted = "-" [cmd_duration] min_time = 500 format = "took [$duration]($style)" style = "bold #75715E" ` case "zerotwo": return `[format] before_format = "$" format = """ $username$directory$git_branch$git_status$cmd_duration$line_break$character""" [character] success_symbol = "[❯](bold #3B82F6)" error_symbol = "[❯](bold #EF4444)" [directory] truncation_length = 3 truncation_symbol = "…/" style = "bold #8B5CF6" [username] show_on_left = false style_user = "bold #EC4899" style_root = "bold #EF4444" [git_branch] symbol = " " format = "on [$symbol$branch]($style)" style = "bold #F472B6" [git_status] format = "[$all_status$ahead_behind]($style) " style = "bold #EF4444" conflicted = "!" untracked = "?" modified = "~" staged = "[+]" renamed = "»" deleted = "-" [cmd_duration] min_time = 500 format = "took [$duration]($style)" style = "bold #6B7280" ` default: return `[format] before_format = "$" format = """ $username$directory$git_branch$git_status$line_break$character""" [character] success_symbol = "[❯](bold green)" error_symbol = "[❯](bold red)" [directory] truncation_length = 3 truncation_symbol = "…/" style = "bold cyan" [username] show_on_left = false style_user = "bold red" style_root = "bold red" [git_branch] symbol = " " format = "on [$symbol$branch]($style)" style = "bold yellow" [cmd_duration] min_time = 500 format = "took [$duration]($style)" style = "bold bright-black" ` } }