summaryrefslogtreecommitdiff
path: root/gui/internal/model/settings.go
blob: 6fa71569b43a45344f1141579df2f534b62240f3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// SPDX-License-Identifier: GPL-3.0-or-later

package model

import (
	"fmt"
	"strings"

	"git.labunix.xyz/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 == "":
		r.Text = clearForm(r.Text, node.Pos.Offset, node.End.Offset)
	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
}

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