-
Notifications
You must be signed in to change notification settings - Fork 1
/
hook.go
275 lines (245 loc) · 8.09 KB
/
hook.go
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
// Copyright (c) 2018, Mark "Happy-Ferret" Bauermeister
//
// This software may be modified and distributed under the terms
// of the BSD license. See the LICENSE file for details.
package hook
import (
"fmt"
"strings"
"github.com/BurntSushi/gribble"
"github.com/Anima-OS/Wonderland/logger"
"github.com/Anima-OS/Wonderland/wini"
)
// All available hook groups. Each hook group corresponds to some action in
// Wingo. When that action happens, every hook in the corresponding group
// is fired.
const (
Startup Type = "startup"
Restarted Type = "restart"
Managed Type = "managed"
Focused Type = "focused"
Unfocused Type = "unfocused"
)
var (
// A global corresponding to the Gribble execution environment.
gribbleEnv *gribble.Environment
// A map from group constants to group values.
groups = map[Type]group{
Startup: make(group, 0),
Restarted: make(group, 0),
Managed: make(group, 0),
Focused: make(group, 0),
Unfocused: make(group, 0),
}
)
type Type string
type group []hook
type hook struct {
// From the config file. Nice for error messages.
name string
// List of match conditions expressed as Gribble commands.
satisfies []string
// When true, each condition in 'satisfies' will be combined as a set
// of conjunctions.
// When false, each condition in 'satisfies' will be combined as a set
// of disjunctions.
conjunction bool
// List of consequences expressed as Gribble commands. All consequences
// are fired if '(and satisfies[0] satisfies[1] ... satisfies[n-1])' is
// satisfied.
consequences []string
}
// Initializes the hooks package with a Gribble execution environment and
// a file path to a wini formatted hooks configuration file. If the
// initialization fails, only a warning is logged since hooks are not
// essential for Wingo to run.
func Initialize(env *gribble.Environment, fpath string) {
gribbleEnv = env
cdata, err := wini.Parse(fpath)
if err != nil {
logger.Warning.Printf("Could not parse '%s': %s", fpath, err)
return
}
for _, hookName := range cdata.Sections() {
if err := readSection(cdata, hookName); err != nil {
logger.Warning.Printf("Could not load hook '%s': %s", hookName, err)
}
}
}
// Fire will attempt to run every hook in the group specified, while replacing
// special strings in every command executed with those provided by args.
//
// Running a hook is a two step process. First, the match conditions are
// executed. If all match conditions return true, we proceed to execute all
// of the consequences. If any of the match conditions are false, we stop
// and condinue on to the next hook.
//
// Note that Fire immediately return a channel, as it executes in its own
// goroutine. The channel can be used to block until Fire has finished.
func Fire(hk Type, args Args) {
go func() {
if _, ok := groups[hk]; !ok {
logger.Warning.Printf("Unknown hook group '%s'.", hk)
return
}
for _, hook := range groups[hk] {
// Run all of the match conditions. Depending upon the value
// of hk.conjunction, we treat the conditions as either a set
// of conjunctions or a set of disjunctions.
andMatched := true
orMatched := false
for _, condCmd := range hook.satisfies {
val, err := gribbleEnv.Run(args.apply(condCmd))
if err != nil {
logger.Warning.Printf("When executing the 'match' "+
"conditions for your '%s' hook in the '%s' group, "+
"the command '%s' returned an error: %s",
hook.name, hk, condCmd, err)
andMatched = false
orMatched = false
break
}
if gribbleBool(val) {
logger.Lots.Printf("Condition '%s' matched "+
"for the hook '%s' in the '%s' group.",
condCmd, hook.name, hk)
orMatched = true
if !hook.conjunction {
break
}
} else {
logger.Lots.Printf("Condition '%s' failed to match "+
"for the hook '%s' in the '%s' group.",
condCmd, hook.name, hk)
andMatched = false
if hook.conjunction {
break
}
}
}
if hook.conjunction && !andMatched {
continue
}
if !hook.conjunction && !orMatched {
continue
}
logger.Lots.Printf("The hook '%s' in the '%s' group has matched!",
hook.name, hk)
// We have a match! Let's proceed to the consequences...
for _, consequentCmd := range hook.consequences {
_, err := gribbleEnv.Run(args.apply(consequentCmd))
if err != nil {
logger.Warning.Printf("When executing the consequences "+
"for your '%s' hook in the '%s' group, the command "+
"'%s' returned an error: %s",
hook.name, hk, consequentCmd, err)
// consequent commands are independent, so we march on.
}
}
}
}()
}
// gribbleBool translates a value returned by a Gribble command to a boolean
// value. A command returns true if and only if it returns the integer 1.
// Any other value results in false.
func gribbleBool(val gribble.Any) bool {
if v, ok := val.(int); ok && v == 1 {
return true
}
return false
}
// readSection loads a particular section from the configuration file into
// the hook groups. One section may result in the same hook being added to
// multiple groups.
func readSection(cdata *wini.Data, section string) error {
// First lets roll up the match conditions.
match := cdata.GetKey(section, "match")
if match == nil {
return fmt.Errorf("Could not find 'match' in the '%s' hook.", section)
}
satisfies := make([]string, len(match.Strings()))
copy(satisfies, match.Strings())
// Check each satisfies command to make sure it's a valid Gribble command.
if cmd, err := checkCommands(satisfies); err != nil {
return fmt.Errorf("The match command '%s' in the '%s' hook could "+
"not be parsed: %s", cmd, section, err)
}
// Now try to find whether it's a conjunction or not.
conjunction := true
conjunctionKey := cdata.GetKey(section, "conjunction")
if conjunctionKey != nil {
if vals, err := conjunctionKey.Bools(); err != nil {
logger.Warning.Println(err)
} else {
conjunction = vals[0]
}
}
// Now traverse all of the keys in the section. We'll skip "match" since
// we've already grabbed the data. Any other key should correspond to
// a hook group name.
addedOne := false
for _, key := range cdata.Keys(section) {
groupName := Type(key.Name())
if groupName == "match" || groupName == "conjunction" {
continue
}
if _, ok := groups[groupName]; !ok {
return fmt.Errorf("Unrecognized hook group '%s' in the '%s' hook.",
groupName, section)
}
consequences := make([]string, len(key.Strings()))
copy(consequences, key.Strings())
// Check each consequent command to make sure it's valid.
if cmd, err := checkCommands(consequences); err != nil {
return fmt.Errorf("The '%s' command '%s' in the '%s' hook could "+
"not be parsed: %s", groupName, cmd, section, err)
}
hook := hook{
name: section,
satisfies: satisfies,
conjunction: conjunction,
consequences: consequences,
}
groups[groupName] = append(groups[groupName], hook)
addedOne = true
}
if !addedOne {
return fmt.Errorf(
"No hook groups were detected in the '%s' hook.", section)
}
return nil
}
// checkCommands runs through a list of strings and tries to parse each as
// a Gribble command in 'gribbleEnv'. If an error occurs in any of them,
// the errant command and the error are returned.
func checkCommands(cmds []string) (string, error) {
for _, cmd := range cmds {
if err := gribbleEnv.Check(cmd); err != nil {
return cmd, err
}
}
return "", nil
}
// Args represents a value that specifies what special strings get replaced
// with in user defined hooks. So for instance, the "focused" hook is
// specifically defined on a particular client, so its hook is fired like so:
//
// args := Args{
// Client: "identifier of window being focused",
// }
// hook.Fire(hook.Focused, args)
type Args struct {
Client string
}
// apply takes a command string and replaces special strings with values in
// Args that have non-zero length.
func (args Args) apply(cmd string) string {
replace := make([]string, 0)
if len(args.Client) > 0 {
replace = append(replace, []string{"\":client:\"", args.Client}...)
}
if len(replace) == 0 {
return cmd
}
return strings.NewReplacer(replace...).Replace(cmd)
}