aboutsummaryrefslogtreecommitdiff
path: root/gui/internal/model/settings.go
diff options
context:
space:
mode:
Diffstat (limited to 'gui/internal/model/settings.go')
-rw-r--r--gui/internal/model/settings.go135
1 files changed, 135 insertions, 0 deletions
diff --git a/gui/internal/model/settings.go b/gui/internal/model/settings.go
new file mode 100644
index 0000000..7c31997
--- /dev/null
+++ b/gui/internal/model/settings.go
@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package model
+
+import (
+ "fmt"
+ "strings"
+
+ "krino/internal/sexp"
+)
+
+// DirSettings are the forms a directory file may carry above its rules, in
+// the order the form shows them. They are the settings krino.conf(5)
+// documents for a directory; anything else in the file is a rule, an
+// exclude, or not krino's (GUI design ยง5.1).
+var DirSettings = []string{
+ "path", "recursive", "max-depth", "min-age", "max-read", "max-size",
+ "busy", "case", "fold", "on-conflict", "ignore",
+}
+
+// Setting reads one directory setting as it is written - the form's
+// arguments, not their meaning - and whether the file sets it at all.
+func (r *Rules) Setting(head string) (args string, set bool, err error) {
+ if !isDirSetting(head) {
+ return "", false, fmt.Errorf("model: %s is not a directory setting", head)
+ }
+ node, err := r.settingNode(head)
+ if err != nil || node == nil {
+ return "", false, err
+ }
+ return argsOf(node, r.Text), true, nil
+}
+
+// SetSetting writes one directory setting: args as they are to be written,
+// or "" to take the setting out of the file. A setting already in the file
+// is rewritten where it stands; a new one goes at the end of the header,
+// above the first exclude or rule, so a directory file keeps its shape.
+// The path is the one setting that cannot be cleared - without it the file
+// does not load.
+func (r *Rules) SetSetting(head, args string) error {
+ if !isDirSetting(head) {
+ return fmt.Errorf("model: %s is not a directory setting", head)
+ }
+ args = strings.TrimSpace(args)
+ if args == "" && head == "path" {
+ return fmt.Errorf("model: a directory needs its (path ...)")
+ }
+ node, err := r.settingNode(head)
+ if err != nil {
+ return err
+ }
+ switch {
+ case node != nil && args == "":
+ start, end := r.lineSpan(node)
+ r.Text = join(r.Text[:start], r.Text[end:])
+ case node != nil:
+ r.Text = r.Text[:node.Pos.Offset] + "(" + head + " " + args + ")" + r.Text[node.End.Offset:]
+ default:
+ at, err := r.headerEnd()
+ if err != nil {
+ return err
+ }
+ r.Text = r.Text[:at] + "(" + head + " " + args + ")\n" + r.Text[at:]
+ }
+ return nil
+}
+
+// isDirSetting reports whether head is one of the directory settings.
+func isDirSetting(head string) bool {
+ for _, s := range DirSettings {
+ if s == head {
+ return true
+ }
+ }
+ return false
+}
+
+// settingNode is the form for head in the current text, or nil. A file that
+// does not parse has no settings to read.
+func (r *Rules) settingNode(head string) (*sexp.Node, error) {
+ nodes, err := sexp.Parse(r.File, []byte(r.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
+}
+
+// headerEnd is where a new setting goes: after the last setting already
+// written, and in any case above the first exclude or rule.
+func (r *Rules) headerEnd() (int, error) {
+ nodes, err := sexp.Parse(r.File, []byte(r.Text))
+ if err != nil {
+ return 0, err
+ }
+ at := 0
+ for _, n := range nodes {
+ if n.Kind != sexp.List {
+ continue
+ }
+ switch {
+ case isDirSetting(n.Head()):
+ at = lineEnd(r.Text, n.End.Offset)
+ case n.Head() == "exclude" || n.Head() == "rule":
+ if at == 0 {
+ at = lineStart(r.Text, n.Pos.Offset)
+ }
+ return at, nil
+ }
+ }
+ if at == 0 {
+ at = len(r.Text)
+ }
+ return at, nil
+}
+
+// lineSpan is the whole line a setting is written on, so clearing it takes
+// the comment that trails it rather than leaving it to dangle.
+func (r *Rules) lineSpan(n *sexp.Node) (start, end int) {
+ return lineStart(r.Text, n.Pos.Offset), lineEnd(r.Text, n.End.Offset)
+}
+
+// argsOf is a form's arguments exactly as the file writes them.
+func argsOf(n *sexp.Node, text string) string {
+ if len(n.Children) < 2 {
+ return ""
+ }
+ from := n.Children[1].Pos.Offset
+ to := n.Children[len(n.Children)-1].End.Offset
+ return strings.TrimSpace(text[from:to])
+}