diff options
Diffstat (limited to 'gui/internal/model/mainconf.go')
| -rw-r--r-- | gui/internal/model/mainconf.go | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/gui/internal/model/mainconf.go b/gui/internal/model/mainconf.go new file mode 100644 index 0000000..327451e --- /dev/null +++ b/gui/internal/model/mainconf.go @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package model + +import ( + "crypto/sha256" + "fmt" + "os" + "strings" + + "krino/internal/config" + "krino/internal/engine" + "krino/internal/sexp" +) + +// MainSettings are the settings krino.conf carries for every directory, +// inside its (defaults ...) form, in the order the window shows them. +var MainSettings = []string{ + "case", "fold", "recursive", "min-age", "max-read", "max-size", + "busy", "on-conflict", +} + +// MainConf is krino.conf open for editing. Only its (defaults ...) block +// and the log path are edited here - the includes and the excludes are the +// file's own business, and the Text tab is where those are written (GUI +// design §5). +type MainConf struct { + File string + Text string + + e *engine.Engine + saved string + stamp [32]byte +} + +// OpenMain reads krino.conf. +func OpenMain(e *engine.Engine) (*MainConf, error) { + text, err := os.ReadFile(e.MainFile) + if err != nil { + return nil, err + } + return &MainConf{File: e.MainFile, Text: string(text), e: e, + saved: string(text), stamp: sha256.Sum256(text)}, nil +} + +// Modified reports whether the text differs from what is on disk. +func (m *MainConf) Modified() bool { return m.Text != m.saved } + +// Check loads the whole configuration with this text in place of the file +// and reports what is wrong with it. +func (m *MainConf) Check() []*config.Diag { + over := map[string][]byte{m.File: []byte(m.Text)} + _, diags := engine.LoadWith(m.File, over) + return diags +} + +// Setting reads one default as it is written: "log" from the top level, +// everything else from inside (defaults ...). +func (m *MainConf) Setting(head string) (args string, set bool, err error) { + if head != "log" && !isMainSetting(head) { + return "", false, fmt.Errorf("model: %s is not a setting of krino.conf", head) + } + node, err := m.node(head) + if err != nil || node == nil { + return "", false, err + } + return argsOf(node, m.Text), true, nil +} + +// SetSetting writes one default, or takes it out when args is empty. A +// setting that is not in the file yet is written into (defaults ...), which +// is created if the file has none. +func (m *MainConf) SetSetting(head, args string) error { + if head != "log" && !isMainSetting(head) { + return fmt.Errorf("model: %s is not a setting of krino.conf", head) + } + args = strings.TrimSpace(args) + node, err := m.node(head) + if err != nil { + return err + } + switch { + case node != nil && args == "": + start, end := lineStart(m.Text, node.Pos.Offset), lineEnd(m.Text, node.End.Offset) + m.Text = join(m.Text[:start], m.Text[end:]) + case node != nil: + m.Text = m.Text[:node.Pos.Offset] + "(" + head + " " + args + ")" + m.Text[node.End.Offset:] + case head == "log": + m.Text = join(m.Text, "\n(log "+args+")\n") + default: + if err := m.setInDefaults(head, args); err != nil { + return err + } + } + return nil +} + +// setInDefaults writes a setting inside (defaults ...), adding the form +// when the file has none. +func (m *MainConf) setInDefaults(head, args string) error { + defaults, err := m.topLevel("defaults") + if err != nil { + return err + } + if defaults == nil { + m.Text = join(m.Text, "\n(defaults\n ("+head+" "+args+"))\n") + return nil + } + // Just inside the closing paren, on a line of its own. + at := defaults.End.Offset - 1 + m.Text = m.Text[:at] + "\n (" + head + " " + args + ")" + m.Text[at:] + return nil +} + +// node is the form for a setting: (log ...) at the top level, the rest +// inside (defaults ...). +func (m *MainConf) node(head string) (*sexp.Node, error) { + if head == "log" { + return m.topLevel("log") + } + defaults, err := m.topLevel("defaults") + if err != nil || defaults == nil { + return nil, err + } + for _, n := range defaults.Args() { + if n.Kind == sexp.List && n.Head() == head { + return n, nil + } + } + return nil, nil +} + +// topLevel is the top-level form with that head, or nil. +func (m *MainConf) topLevel(head string) (*sexp.Node, error) { + nodes, err := sexp.Parse(m.File, []byte(m.Text)) + if err != nil { + return nil, err + } + for _, n := range nodes { + if n.Kind == sexp.List && n.Head() == head { + return n, nil + } + } + return nil, nil +} + +// Save writes krino.conf, refusing a file that will not load and one that +// changed on disk since it was opened - the rules the directory files are +// saved by (GUI design §5.3). The previous text is kept as krino.conf.bak. +func (m *MainConf) Save() error { + if diags := m.Check(); len(diags) > 0 { + return fmt.Errorf("%s", diags[0]) + } + on, err := os.ReadFile(m.File) + if err != nil { + return err + } + if sha256.Sum256(on) != m.stamp { + return ErrChangedOnDisk + } + mode := os.FileMode(0o600) + if fi, err := os.Stat(m.File); err == nil { + mode = fi.Mode().Perm() + } + if err := os.WriteFile(m.File+".bak", on, mode); err != nil { + return err + } + text := []byte(m.Text) + if err := config.Replace(m.File, text); err != nil { + return err + } + m.saved, m.stamp = m.Text, sha256.Sum256(text) + return nil +} + +// isMainSetting reports whether head is one of the defaults. +func isMainSetting(head string) bool { + for _, s := range MainSettings { + if s == head { + return true + } + } + return false +} |
