package config import "os" import "log" import "sync" import "slices" import "path/filepath" import "github.com/fsnotify/fsnotify" import "git.tebibyte.media/tomo/tomo/event" type config struct { open bool watcher *fsnotify.Watcher paths struct { user string system []string watching map[string] struct { } } data struct { user *File system []map[string] Value } on struct { lock sync.RWMutex change event.Broadcaster[func (string)] } } // NewConfig creates a new Config using paths to the user-level config file, and // a set of system config files. These files need not exist: the user-level file // will be created when Set is called for the first time if it does not exist // already, and nonexistent system files are simply ignored (unless the Config // finds them at any point to have been spontaneously created). // // The user file is written to when Set is called, and the system files are only // read from. Values in the user file override those in the system files, and // system files specified nearer to the start of the vararg list will override // those farther down. func NewConfig (user string, system ...string) (ConfigCloser, error) { conf := new(config) conf.paths.user = user conf.paths.system = system err := conf.init() if err != nil { return nil, err } go conf.run() return conf, nil } func (this *config) init () error { watcher, err := fsnotify.NewWatcher() if err != nil { return err } this.watcher = watcher this.paths.watching = make(map[string] struct { }) this.watcher.Add(filepath.Dir(this.paths.user)) this.paths.watching[this.paths.user] = struct { } { } this.reloadUser() for index, path := range this.paths.system { this.watcher.Add(filepath.Dir(path)) this.paths.watching[path] = struct { } { } this.reloadSystem(index) } return nil } func (this *config) run () { for event := range this.watcher.Events { if !(event.Has(fsnotify.Write)) { continue } if _, ok := this.paths.watching[event.Name]; !ok { continue } if event.Name == this.paths.user { this.reloadUser() } else { index := slices.Index(this.paths.system, event.Name) if index > 0 { this.reloadSystem(index) } } // TODO diff and call event handler if changed } } func (this *config) Close () error { this.open = false return this.watcher.Close() } func (this *config) errIfClosed () error { if this.open { return nil } else { return ErrClosed } } func (this *config) reloadUser () { file, err := os.Open(this.paths.user) if err != nil { return } defer file.Close() userFile, err := Parse(file) if err != nil { log.Printf("nasin: problems loading user config file %s: %v", this.paths.user, err) return } this.data.user = userFile } func (this *config) reloadSystem (index int) { path := this.paths.system[index] file, err := os.Open(path) if err != nil { return } defer file.Close() systemFile, err := Parse(file) if err != nil { log.Printf("nasin: problems loading system config file %s: %v", path, err) return } this.data.system[index] = systemFile.Map() } func (this *config) saveUser () error { // TODO set some sort of flag to ignore the next inotify event for the // user file so we dont reload it immediately after. also need to // broadacast Changed event. enclosingDir := filepath.Dir(this.paths.user) err := os.MkdirAll(enclosingDir, 755) if err != nil { return err } file, err := os.Create(this.paths.user) if err != nil { return err } defer file.Close() _, err = this.data.user.WriteTo(file) if err != nil { return err } return nil } func (this *config) Get (key string, fallback Value) (Value, error) { // try user config if !KeyValid(key) { return nil, ErrMalformedKey } if this.data.user != nil { value, err := this.data.user.Get(key) if err == nil && err != ErrNonexistentEntry { return value, nil } } // try system configs for _, config := range this.data.system { if value, ok := config[key]; ok { return value, nil } } // use fallback return fallback, nil } func (this *config) GetString (key string, fallback string) (string, error) { value, err := this.Get(key, ValueString(fallback)) if err != nil { return "", err } if value, ok := value.(ValueString); ok { return string(value), nil } return fallback, nil } func (this *config) GetNumber (key string, fallback float64) (float64, error) { value, err := this.Get(key, ValueNumber(fallback)) if err != nil { return 0, err } if value, ok := value.(ValueNumber); ok { return float64(value), nil } return fallback, nil } func (this *config) GetBool (key string, fallback bool) (bool, error) { value, err := this.Get(key, ValueBool(fallback)) if err != nil { return false, err } if value, ok := value.(ValueBool); ok { return bool(value), nil } return fallback, nil } func (this *config) Set (key string, value Value) error { if this.data.user == nil { this.data.user = NewFile() } err := this.data.user.Set(key, value) if err != nil { return err } return this.saveUser() } func (this *config) Reset (key string) error { if this.data.user == nil { this.data.user = NewFile() } err := this.data.user.Reset(key) if err != nil { return err } return this.saveUser() } func (this *config) OnChange (callback func (string)) event.Cookie { this.on.lock.Lock() defer this.on.lock.Unlock() return this.on.change.Connect(callback) }