diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-11 14:47:10 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-11 15:01:57 +0200 |
| commit | 42b02c47be9b285099203e44a2570636d4ca6f03 (patch) | |
| tree | 82bcb9e19bd886f36e1ca7b2d94a204c988f1fd1 /internal/config/settings.go | |
| download | krino-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.go | 204 |
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 +} |
