summaryrefslogtreecommitdiff
path: root/gui/internal/model/cond.go
blob: 63c0c25579b78ced28b6e322707e831f0b7f9ccd (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
// SPDX-License-Identifier: GPL-3.0-or-later

package model

import (
	"fmt"
	"strings"

	"git.labunix.xyz/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)
	}
}