Files
iknowyou/back/config/config.go
2026-04-06 15:12:34 +02:00

145 lines
3.8 KiB
Go

package config
import (
"fmt"
"log"
"os"
"gopkg.in/yaml.v3"
)
type Config struct {
Tools map[string]yaml.Node `yaml:"tools" json:"tools"`
Profiles map[string]Profile `yaml:"profiles" json:"profiles"`
}
type Profile struct {
Notes string `yaml:"notes,omitempty" json:"notes,omitempty"`
Tools map[string]yaml.Node `yaml:"tools" json:"tools"`
Enabled []string `yaml:"enabled" json:"enabled"`
Disabled []string `yaml:"disabled" json:"disabled"`
}
func (c *Config) DecodeEffective(toolName, profileName string, dst any) error {
if node, ok := c.Tools[toolName]; ok {
if err := node.Decode(dst); err != nil {
return fmt.Errorf("config: decoding global config for tool %q: %w", toolName, err)
}
}
if profileName != "" {
// Builtin profiles have their overrides defined in Go, not in YAML.
if _, isBuiltin := BuiltinProfiles[profileName]; isBuiltin {
return ApplyBuiltinToolOverride(profileName, toolName, dst)
}
p, ok := c.Profiles[profileName]
if !ok {
return fmt.Errorf("config: unknown profile %q", profileName)
}
if node, ok := p.Tools[toolName]; ok {
if err := node.Decode(dst); err != nil {
return fmt.Errorf("config: decoding profile %q override for tool %q: %w", profileName, toolName, err)
}
}
}
return nil
}
func (c *Config) ActiveTools(profileName string, allToolNames []string) ([]string, error) {
if profileName == "" {
return allToolNames, nil
}
if builtin, ok := BuiltinProfiles[profileName]; ok {
return ActiveToolsForProfile(builtin.Profile, allToolNames), nil
}
p, ok := c.Profiles[profileName]
if !ok {
return nil, fmt.Errorf("config: unknown profile %q", profileName)
}
return ActiveToolsForProfile(p, allToolNames), nil
}
// IsReadonly reports whether the config file at path cannot be written to.
// Returns false if the file does not exist (it can still be created).
func IsReadonly(path string) bool {
f, err := os.OpenFile(path, os.O_WRONLY, 0)
if err != nil {
return os.IsPermission(err)
}
f.Close()
return false
}
func Load(path string) (*Config, error) {
f, err := os.Open(path)
if os.IsNotExist(err) {
log.Printf("config: %q not found, starting with empty config", path)
return Default(), nil
}
if err != nil {
return nil, fmt.Errorf("config: open %q: %w", path, err)
}
defer func() { _ = f.Close() }()
var cfg Config
dec := yaml.NewDecoder(f)
dec.KnownFields(true)
if err := dec.Decode(&cfg); err != nil {
return nil, fmt.Errorf("config: decode: %w", err)
}
return &cfg, nil
}
func Default() *Config {
return &Config{
Tools: make(map[string]yaml.Node),
Profiles: make(map[string]Profile),
}
}
func Save(path string, cfg *Config) error {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("config: create %q: %w", path, err)
}
defer func() { _ = f.Close() }()
enc := yaml.NewEncoder(f)
enc.SetIndent(2)
if err := enc.Encode(cfg); err != nil {
return fmt.Errorf("config: encode: %w", err)
}
return nil
}
// MergeNodePatch merges patch key-values into an existing yaml.Node (mapping).
// If existing is a zero value, it starts from an empty mapping.
func MergeNodePatch(existing yaml.Node, patch map[string]any) (yaml.Node, error) {
var m map[string]any
if existing.Kind != 0 {
if err := existing.Decode(&m); err != nil {
return yaml.Node{}, fmt.Errorf("config: decode node: %w", err)
}
}
if m == nil {
m = make(map[string]any)
}
for k, v := range patch {
m[k] = v
}
b, err := yaml.Marshal(m)
if err != nil {
return yaml.Node{}, fmt.Errorf("config: marshal: %w", err)
}
var doc yaml.Node
if err := yaml.Unmarshal(b, &doc); err != nil {
return yaml.Node{}, fmt.Errorf("config: unmarshal: %w", err)
}
if doc.Kind == yaml.DocumentNode && len(doc.Content) == 1 {
return *doc.Content[0], nil
}
return doc, nil
}