Add the Config view: edit and save the settings, applied without a restart
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+101
-1
@@ -103,14 +103,54 @@ func Load(path string) (*Config, []string, error) {
|
||||
// Parse decodes config JSON, applies environment overrides and defaults, and
|
||||
// validates the result. Unknown fields are rejected to catch typos.
|
||||
func Parse(data []byte) (*Config, error) {
|
||||
raw, err := parseRaw(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return raw.Effective(os.Getenv)
|
||||
}
|
||||
|
||||
// parseRaw decodes config JSON as it stands in the file: no environment
|
||||
// overrides, no defaults, no validation.
|
||||
func parseRaw(data []byte) (*Config, error) {
|
||||
var cfg Config
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("parsing config: %w", err)
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
cfg.applyEnv(os.Getenv)
|
||||
// LoadFile reads the config at path exactly as written: no environment
|
||||
// overrides, no defaults, no validation. It is what an editor of the file must
|
||||
// start from, so values that only came from the environment are not written
|
||||
// back to disk.
|
||||
func LoadFile(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading config: %w", err)
|
||||
}
|
||||
cfg, err := parseRaw(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Effective returns the configuration the program runs with: a copy with the
|
||||
// environment overrides and defaults applied, validated.
|
||||
func (c *Config) Effective(env func(string) string) (*Config, error) {
|
||||
cfg := *c
|
||||
if c.UI.Covers != nil {
|
||||
v := *c.UI.Covers
|
||||
cfg.UI.Covers = &v
|
||||
}
|
||||
if c.UI.NotifyDownloads != nil {
|
||||
v := *c.UI.NotifyDownloads
|
||||
cfg.UI.NotifyDownloads = &v
|
||||
}
|
||||
cfg.applyEnv(env)
|
||||
cfg.applyDefaults()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
@@ -118,6 +158,66 @@ func Parse(data []byte) (*Config, error) {
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// envVars maps the settings that can be overridden by the environment to the
|
||||
// variable that does it.
|
||||
var envVars = map[string]string{
|
||||
"radarr.url": "ARR_TUI_RADARR_URL",
|
||||
"radarr.apikey": "ARR_TUI_RADARR_APIKEY",
|
||||
"sonarr.url": "ARR_TUI_SONARR_URL",
|
||||
"sonarr.apikey": "ARR_TUI_SONARR_APIKEY",
|
||||
"sabnzbd.url": "ARR_TUI_SABNZBD_URL",
|
||||
"sabnzbd.apikey": "ARR_TUI_SABNZBD_APIKEY",
|
||||
}
|
||||
|
||||
// Overrides returns, for every setting that the environment currently
|
||||
// overrides, the name of the variable. Keys look like "radarr.url".
|
||||
func Overrides(env func(string) string) map[string]string {
|
||||
out := map[string]string{}
|
||||
for key, name := range envVars {
|
||||
if env(name) != "" {
|
||||
out[key] = name
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Save writes the config to path as indented JSON. The file is replaced
|
||||
// atomically (a temporary file in the same directory, then a rename) and keeps
|
||||
// the permissions of the existing file, or is private (0600) when new: it
|
||||
// holds API keys.
|
||||
func Save(path string, cfg *Config) error {
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
|
||||
mode := os.FileMode(0o600)
|
||||
if info, err := os.Stat(path); err == nil {
|
||||
mode = info.Mode().Perm()
|
||||
}
|
||||
tmp, err := os.CreateTemp(filepath.Dir(path), ".config-*.tmp")
|
||||
if err != nil {
|
||||
return fmt.Errorf("saving config: %w", err)
|
||||
}
|
||||
defer os.Remove(tmp.Name()) // no-op after a successful rename
|
||||
if err := tmp.Chmod(mode); err != nil {
|
||||
tmp.Close()
|
||||
return fmt.Errorf("saving config: %w", err)
|
||||
}
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
tmp.Close()
|
||||
return fmt.Errorf("saving config: %w", err)
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return fmt.Errorf("saving config: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmp.Name(), path); err != nil {
|
||||
return fmt.Errorf("saving config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) applyEnv(get func(string) string) {
|
||||
override := func(dst *string, key string) {
|
||||
if v := get(key); v != "" {
|
||||
|
||||
Reference in New Issue
Block a user