aboutsummaryrefslogtreecommitdiff
path: root/gui/internal/model/settings.go
blob: 7c319973ec426275e544c219bfcae16ed974e4eb (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
129
130
131
132
133
134
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])
}