aboutsummaryrefslogtreecommitdiff
path: root/internal/config/settings.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-11 14:47:10 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-11 15:01:57 +0200
commit42b02c47be9b285099203e44a2570636d4ca6f03 (patch)
tree82bcb9e19bd886f36e1ca7b2d94a204c988f1fd1 /internal/config/settings.go
downloadkrino-42b02c47be9b285099203e44a2570636d4ca6f03.tar.gz
krino-42b02c47be9b285099203e44a2570636d4ca6f03.zip
krino: foundation — sexp reader, config language, init/new/check
Diffstat (limited to 'internal/config/settings.go')
-rw-r--r--internal/config/settings.go204
1 files changed, 204 insertions, 0 deletions
diff --git a/internal/config/settings.go b/internal/config/settings.go
new file mode 100644
index 0000000..97355a5
--- /dev/null
+++ b/internal/config/settings.go
@@ -0,0 +1,204 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package config
+
+import (
+ "slices"
+ "time"
+
+ "krino/internal/sexp"
+)
+
+// CaseMode says whether name, path and content matching ignores case.
+type CaseMode int
+
+const (
+ CaseIgnore CaseMode = iota
+ CaseStrict
+)
+
+// Conflict is what to do when an action's target already exists.
+type Conflict int
+
+const (
+ ConflictSuffix Conflict = iota
+ ConflictSkip
+ ConflictOverwrite
+)
+
+// Settings holds what one level of config sets; nil means not set there.
+type Settings struct {
+ Case *CaseMode
+ Fold *bool
+ Recursive *bool
+ MaxDepth *int
+ MinAge *time.Duration
+ MaxRead *int64
+ Busy *[]string
+ OnConflict *Conflict
+}
+
+// Resolved is a complete set of settings.
+type Resolved struct {
+ Case CaseMode
+ Fold bool
+ Recursive bool
+ MaxDepth int // 0 means unlimited
+ MinAge time.Duration
+ MaxRead int64
+ Busy []string
+ OnConflict Conflict
+}
+
+// Builtin is what applies when nothing is set.
+func Builtin() Resolved {
+ return Resolved{
+ Case: CaseIgnore,
+ Fold: true,
+ MinAge: 2 * time.Minute,
+ MaxRead: 50 << 20,
+ Busy: []string{".part", ".aria2", ".crdownload"},
+ OnConflict: ConflictSuffix,
+ }
+}
+
+// Over returns base with every setting that s sets replaced.
+func (s Settings) Over(base Resolved) Resolved {
+ r := base
+ if s.Case != nil {
+ r.Case = *s.Case
+ }
+ if s.Fold != nil {
+ r.Fold = *s.Fold
+ }
+ if s.Recursive != nil {
+ r.Recursive = *s.Recursive
+ }
+ if s.MaxDepth != nil {
+ r.MaxDepth = *s.MaxDepth
+ }
+ if s.MinAge != nil {
+ r.MinAge = *s.MinAge
+ }
+ if s.MaxRead != nil {
+ r.MaxRead = *s.MaxRead
+ }
+ if s.Busy != nil {
+ r.Busy = *s.Busy
+ }
+ if s.OnConflict != nil {
+ r.OnConflict = *s.OnConflict
+ }
+ return r
+}
+
+var settingNames = []string{"case", "fold", "recursive", "max-depth", "min-age", "max-read", "busy", "on-conflict"}
+
+// ruleSettings are the settings a rule may override.
+var ruleSettings = map[string]bool{"case": true, "fold": true, "on-conflict": true}
+
+func isSetting(name string) bool { return slices.Contains(settingNames, name) }
+
+var settingHint = map[string]string{
+ "case": "(case ignore) or (case strict)",
+ "fold": "(fold yes) or (fold no)",
+ "recursive": "(recursive yes) or (recursive no)",
+ "max-depth": "a number, like (max-depth 3)",
+ "min-age": "a duration, like (min-age 2m)",
+ "max-read": "a size, like (max-read 50M)",
+ "on-conflict": "(on-conflict suffix), skip or overwrite",
+}
+
+// parse reads the setting form n into s, reporting problems to d. seen
+// catches a setting given twice at the same level.
+func (s *Settings) parse(n *sexp.Node, d *diags, seen map[string]*sexp.Node) {
+ name := n.Head()
+ if first, ok := seen[name]; ok {
+ d.at(n, "%s set twice (first at line %d)", name, first.Pos.Line)
+ return
+ }
+ seen[name] = n
+ args := n.Args()
+ if name == "busy" {
+ list := []string{}
+ for _, a := range args {
+ if a.Kind != sexp.String {
+ d.at(a, `busy takes strings, like ".part"; got %s`, a)
+ continue
+ }
+ list = append(list, a.Text)
+ }
+ s.Busy = &list
+ return
+ }
+ if len(args) == 1 && args[0].Kind == sexp.String {
+ d.at(args[0], "%s values are bare words: write (%s %s)", name, name, args[0].Text)
+ return
+ }
+ if len(args) != 1 || args[0].Kind != sexp.Symbol {
+ d.at(n, "%s takes one value: %s", name, settingHint[name])
+ return
+ }
+ v, at := args[0].Text, args[0]
+ switch name {
+ case "case":
+ switch v {
+ case "ignore":
+ c := CaseIgnore
+ s.Case = &c
+ case "strict":
+ c := CaseStrict
+ s.Case = &c
+ default:
+ d.at(at, "case is ignore or strict, not %s", v)
+ }
+ case "fold", "recursive":
+ b, ok := yesNo(v)
+ if !ok {
+ d.at(at, "%s is yes or no, not %s", name, v)
+ } else if name == "fold" {
+ s.Fold = &b
+ } else {
+ s.Recursive = &b
+ }
+ case "max-depth":
+ depth, err := parseCount(v)
+ if err != nil || depth < 1 || depth > 1<<20 {
+ d.at(at, "max-depth is a whole number from 1, not %s", v)
+ return
+ }
+ i := int(depth)
+ s.MaxDepth = &i
+ case "min-age":
+ dur, err := ParseDuration(v)
+ if err != nil {
+ d.at(at, "min-age: %v", err)
+ return
+ }
+ s.MinAge = &dur
+ case "max-read":
+ size, err := ParseSize(v)
+ if err != nil {
+ d.at(at, "max-read: %v", err)
+ return
+ }
+ s.MaxRead = &size
+ case "on-conflict":
+ c, ok := map[string]Conflict{"suffix": ConflictSuffix, "skip": ConflictSkip, "overwrite": ConflictOverwrite}[v]
+ if !ok {
+ d.at(at, "on-conflict is suffix, skip or overwrite, not %s", v)
+ return
+ }
+ s.OnConflict = &c
+ }
+}
+
+func yesNo(v string) (bool, bool) {
+ switch v {
+ case "yes":
+ return true, true
+ case "no":
+ return false, true
+ }
+ return false, false
+}