// SPDX-License-Identifier: GPL-3.0-or-later package config import ( "errors" "fmt" "io/fs" "os" "path/filepath" "krino/internal/sexp" "krino/internal/xdg" ) // Config is the whole configuration: the main file and the directories it // includes. type Config struct { Main *Main Dirs []*Dir } // readSource is the text of file: the caller's override when it has one, // else the file's own bytes. func readSource(overrides map[string][]byte, file string) ([]byte, error) { if src, ok := overrides[file]; ok { return src, nil } return os.ReadFile(file) } // DefaultFile is krino.conf in the XDG config directory. func DefaultFile() string { return filepath.Join(xdg.ConfigHome(), "krino", "krino.conf") } // DirFile is where directory name's config lives, beside the main file. func DirFile(mainFile, name string) string { return filepath.Join(filepath.Dir(mainFile), "dirs", name+".conf") } // Load reads the main file and the files of the directories it includes. // With names, only those directories are read, and each must be included. // The Config is nil only when the main file itself cannot be read. func Load(mainFile string, names ...string) (*Config, []*Diag) { return LoadWith(mainFile, nil, names...) } // LoadWith is Load with some files' text supplied by the caller: overrides // maps a file path - mainFile, or DirFile(mainFile, name) - to the text to // read instead of that file's own, so an editor can have unsaved text // checked exactly as a run would read it (GUI design §1.3). A nil map is // Load. func LoadWith(mainFile string, overrides map[string][]byte, names ...string) (*Config, []*Diag) { src, err := readSource(overrides, mainFile) if errors.Is(err, fs.ErrNotExist) { return nil, []*Diag{{File: mainFile, Msg: "not found; create it with: krino init"}} } if err != nil { return nil, []*Diag{{File: mainFile, Msg: err.Error()}} } m, errs := ParseMain(mainFile, src) cfg := &Config{Main: m} if _, perr := sexp.Parse(mainFile, src); perr != nil { // krino.conf itself is unreadable: report just that, and load none // of the directories, so a syntax error never also produces // "NAME is not in include" for names the caller asked for. return cfg, errs } want := m.Include if len(names) > 0 { want = nil for _, n := range names { if _, ok := m.IncludePos[n]; !ok { errs = append(errs, &Diag{File: mainFile, Msg: fmt.Sprintf("%q is not in include", n)}) continue } want = append(want, n) } } for _, name := range want { file := DirFile(mainFile, name) src, err := readSource(overrides, file) if err != nil { msg := err.Error() if errors.Is(err, fs.ErrNotExist) { msg = fmt.Sprintf("included %q, but %s does not exist; create it with: krino new %s PATH", name, file, name) } errs = append(errs, &Diag{File: mainFile, Pos: m.IncludePos[name], Msg: msg}) continue } dir, derrs := ParseDir(name, file, src) errs = append(errs, derrs...) cfg.Dirs = append(cfg.Dirs, dir) } return cfg, errs } // Resolved is the settings that apply in dir: built-in, then the main // file's defaults, then the directory's own. func (c *Config) Resolved(dir *Dir) Resolved { return dir.Settings.Over(c.Main.Defaults.Over(Builtin())) } // LogFile is the main file's (log ...), or the default under XDG_STATE_HOME. func (c *Config) LogFile() string { if c.Main.Log != "" { return c.Main.Log } return filepath.Join(xdg.StateHome(), "krino", "krino.log") } // LockFile is where a directory's lock lives while a run is active: // $XDG_STATE_HOME/krino/.lock, beside the log (spec §3). func (c *Config) LockFile(name string) string { return filepath.Join(xdg.StateHome(), "krino", name+".lock") }