aboutsummaryrefslogtreecommitdiff
path: root/gui/internal/model/mainconf.go
diff options
context:
space:
mode:
Diffstat (limited to 'gui/internal/model/mainconf.go')
-rw-r--r--gui/internal/model/mainconf.go184
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
+}