Files
magpie/internal/settings/settings.go
T

103 lines
2.9 KiB
Go

// Package settings keeps the few preferences the desktop app has: which
// palette to paint with and which language to speak. Everything else magpie
// knows is derived from the agents' own files.
//
// The file is ~/.config/magpie/settings.json; a missing file means "follow
// the system" for both.
package settings
import (
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"slices"
"strings"
)
// Settings is what the user chose. "" and "system" both mean "follow the OS".
type Settings struct {
Theme string `json:"theme,omitempty"` // system | light | dark
Lang string `json:"lang,omitempty"` // system | en | zh
Tray string `json:"tray,omitempty"` // what clicking the tray icon opens: panel | window
// Proxy for magpie's own requests to vendors: "" follows the
// environment and then the system, "direct" uses none, anything else
// is the proxy (http://, https:// or socks5://; host:port means http).
Proxy string `json:"proxy,omitempty"`
}
// Themes and Langs are the accepted values, in the order the UI offers them.
var (
Themes = []string{"system", "light", "dark"}
Langs = []string{"system", "en", "zh"}
Trays = []string{"panel", "window"}
)
// Path is the settings file.
func Path() string {
if x := os.Getenv("XDG_CONFIG_HOME"); x != "" {
return filepath.Join(x, "magpie", "settings.json")
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "magpie", "settings.json")
}
// Dir is the folder every magpie file lives in.
func Dir() string { return filepath.Dir(Path()) }
// Load reads the settings; anything missing or unreadable is the default.
func Load() Settings {
var s Settings
if b, err := os.ReadFile(Path()); err == nil {
_ = json.Unmarshal(b, &s)
}
return s.normal()
}
// Save validates and writes the settings.
func Save(s Settings) error {
s = s.normal()
if !slices.Contains(Themes, s.Theme) {
return fmt.Errorf("theme must be one of %v, not %q", Themes, s.Theme)
}
if !slices.Contains(Langs, s.Lang) {
return fmt.Errorf("language must be one of %v, not %q", Langs, s.Lang)
}
if !slices.Contains(Trays, s.Tray) {
return fmt.Errorf("tray must be one of %v, not %q", Trays, s.Tray)
}
s.Proxy = strings.TrimSpace(s.Proxy)
if s.Proxy != "" && s.Proxy != "direct" {
raw := s.Proxy
if !strings.Contains(raw, "://") {
raw = "http://" + raw
}
u, err := url.Parse(raw)
if err != nil || u.Host == "" || !slices.Contains([]string{"http", "https", "socks5", "socks5h"}, u.Scheme) {
return fmt.Errorf("proxy must look like http://127.0.0.1:7890 or socks5://127.0.0.1:1080, not %q", s.Proxy)
}
}
if err := os.MkdirAll(Dir(), 0o755); err != nil {
return err
}
b, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}
return os.WriteFile(Path(), append(b, '\n'), 0o644)
}
func (s Settings) normal() Settings {
if s.Theme == "" {
s.Theme = "system"
}
if s.Lang == "" {
s.Lang = "system"
}
if s.Tray == "" {
s.Tray = "panel"
}
return s
}