nasin/config/impl.go

211 lines
5.3 KiB
Go
Raw Normal View History

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)
}