// SPDX-License-Identifier: GPL-3.0-or-later package model import ( "fmt" "strings" "krino/internal/sexp" ) // CondOps are the forms that hold other conditions. var CondOps = map[string]bool{"and": true, "or": true, "not": true} // CondTests are the tests a condition can make, in the order a picker // offers them. var CondTests = []string{"type", "name", "path", "content", "size", "age", "duplicate", "matched"} // Cond is one condition in the tree the forms editor shows: either a test, // with its arguments as the file writes them, or one of and / or / not // holding the conditions under it (GUI design ยง5.1). type Cond struct { Kind string Args string Children []*Cond } // ParseCond reads a parsed form into the tree. Anything that is not a list // - which the config parser refuses anyway - comes back as a test with the // text as its arguments, so nothing is ever silently dropped. func ParseCond(n *sexp.Node) *Cond { if n == nil { return &Cond{Kind: "type"} } if n.Kind != sexp.List || len(n.Children) == 0 { return &Cond{Kind: n.String()} } head := n.Head() if CondOps[head] { c := &Cond{Kind: head} for _, child := range n.Args() { c.Children = append(c.Children, ParseCond(child)) } return c } var args []string for _, a := range n.Args() { args = append(args, a.String()) } return &Cond{Kind: head, Args: strings.Join(args, " ")} } // Text writes the condition back as krino reads it. func (c *Cond) Text() string { if c == nil { return "" } if CondOps[c.Kind] { parts := make([]string, 0, len(c.Children)) for _, child := range c.Children { if t := child.Text(); t != "" { parts = append(parts, t) } } if len(parts) == 0 { return "" } return "(" + c.Kind + " " + strings.Join(parts, " ") + ")" } args := strings.TrimSpace(c.Args) if args == "" { return "(" + c.Kind + ")" } return "(" + c.Kind + " " + args + ")" } // Parse checks that a condition is one krino can read, and says where it is // wrong if it is not. func (c *Cond) Parse() error { text := c.Text() if text == "" { return fmt.Errorf("%s has nothing under it", c.Kind) } nodes, err := sexp.Parse("form", []byte(text)) if err != nil { return err } if len(nodes) != 1 { return fmt.Errorf("%s is not one condition", text) } return nil } // Add puts a new condition under an operator; a test gets none. func (c *Cond) Add(kind string) *Cond { if !CondOps[c.Kind] { return nil } child := &Cond{Kind: kind} c.Children = append(c.Children, child) return child } // Remove takes one condition out of the tree, wherever it is, and reports // whether it found it. The root itself is never removed by this. func (c *Cond) Remove(target *Cond) bool { for i, child := range c.Children { if child == target { c.Children = append(c.Children[:i], c.Children[i+1:]...) return true } if child.Remove(target) { return true } } return false } // Walk calls f for every condition in the tree, depth first, with how deep // it sits and the operator holding it (nil for the roots). func (c *Cond) Walk(depth int, parent *Cond, f func(node, parent *Cond, depth int)) { f(c, parent, depth) for _, child := range c.Children { child.Walk(depth+1, c, f) } }