aboutsummaryrefslogtreecommitdiffstats
path: root/sway/criteria.c
diff options
context:
space:
mode:
Diffstat (limited to 'sway/criteria.c')
-rw-r--r--sway/criteria.c445
1 files changed, 445 insertions, 0 deletions
diff --git a/sway/criteria.c b/sway/criteria.c
new file mode 100644
index 00000000..2eee331c
--- /dev/null
+++ b/sway/criteria.c
@@ -0,0 +1,445 @@
1#define _XOPEN_SOURCE 700
2#include <stdlib.h>
3#include <stdio.h>
4#include <stdbool.h>
5#include <pcre.h>
6#include "sway/criteria.h"
7#include "sway/container.h"
8#include "sway/config.h"
9#include "sway/view.h"
10#include "stringop.h"
11#include "list.h"
12#include "log.h"
13
14enum criteria_type { // *must* keep in sync with criteria_strings[]
15 CRIT_APP_ID,
16 CRIT_CLASS,
17 CRIT_CON_ID,
18 CRIT_CON_MARK,
19 CRIT_FLOATING,
20 CRIT_ID,
21 CRIT_INSTANCE,
22 CRIT_TILING,
23 CRIT_TITLE,
24 CRIT_URGENT,
25 CRIT_WINDOW_ROLE,
26 CRIT_WINDOW_TYPE,
27 CRIT_WORKSPACE,
28 CRIT_LAST
29};
30
31static const char * const criteria_strings[CRIT_LAST] = {
32 [CRIT_APP_ID] = "app_id",
33 [CRIT_CLASS] = "class",
34 [CRIT_CON_ID] = "con_id",
35 [CRIT_CON_MARK] = "con_mark",
36 [CRIT_FLOATING] = "floating",
37 [CRIT_ID] = "id",
38 [CRIT_INSTANCE] = "instance",
39 [CRIT_TILING] = "tiling",
40 [CRIT_TITLE] = "title",
41 [CRIT_URGENT] = "urgent", // either "latest" or "oldest" ...
42 [CRIT_WINDOW_ROLE] = "window_role",
43 [CRIT_WINDOW_TYPE] = "window_type",
44 [CRIT_WORKSPACE] = "workspace"
45};
46
47/**
48 * A single criteria token (ie. value/regex pair),
49 * e.g. 'class="some class regex"'.
50 */
51struct crit_token {
52 enum criteria_type type;
53 pcre *regex;
54 char *raw;
55};
56
57static void free_crit_token(struct crit_token *crit) {
58 pcre_free(crit->regex);
59 free(crit->raw);
60 free(crit);
61}
62
63static void free_crit_tokens(list_t *crit_tokens) {
64 for (int i = 0; i < crit_tokens->length; i++) {
65 free_crit_token(crit_tokens->items[i]);
66 }
67 list_free(crit_tokens);
68}
69
70// Extracts criteria string from its brackets. Returns new (duplicate)
71// substring.
72static char *criteria_from(const char *arg) {
73 char *criteria = NULL;
74 if (*arg == '[') {
75 criteria = strdup(arg + 1);
76 } else {
77 criteria = strdup(arg);
78 }
79
80 int last = strlen(criteria) - 1;
81 if (criteria[last] == ']') {
82 criteria[last] = '\0';
83 }
84 return criteria;
85}
86
87// Return instances of c found in str.
88static int countchr(char *str, char c) {
89 int found = 0;
90 for (int i = 0; str[i]; i++) {
91 if (str[i] == c) {
92 ++found;
93 }
94 }
95 return found;
96}
97
98// criteria_str is e.g. '[class="some class regex" instance="instance name"]'.
99//
100// Will create array of pointers in buf, where first is duplicate of given
101// string (must be freed) and the rest are pointers to names and values in the
102// base string (every other, naturally). argc will be populated with the length
103// of buf.
104//
105// Returns error string or NULL if successful.
106static char *crit_tokens(int *argc, char ***buf,
107 const char * const criteria_str) {
108 wlr_log(L_DEBUG, "Parsing criteria: '%s'", criteria_str);
109 char *base = criteria_from(criteria_str);
110 char *head = base;
111 char *namep = head; // start of criteria name
112 char *valp = NULL; // start of value
113
114 // We're going to place EOS markers where we need to and fill up an array
115 // of pointers to the start of each token (either name or value).
116 int pairs = countchr(base, '=');
117 int max_tokens = pairs * 2 + 1; // this gives us at least enough slots
118
119 char **argv = *buf = calloc(max_tokens, sizeof(char*));
120 argv[0] = base; // this needs to be freed by caller
121 bool quoted = true;
122
123 *argc = 1; // uneven = name, even = value
124 while (*head && *argc < max_tokens) {
125 if (namep != head && *(head - 1) == '\\') {
126 // escaped character: don't try to parse this
127 } else if (*head == '=' && namep != head) {
128 if (*argc % 2 != 1) {
129 // we're not expecting a name
130 return strdup("Unable to parse criteria: "
131 "Found out of place equal sign");
132 } else {
133 // name ends here
134 char *end = head; // don't want to rewind the head
135 while (*(end - 1) == ' ') {
136 --end;
137 }
138 *end = '\0';
139 if (*(namep) == ' ') {
140 namep = strrchr(namep, ' ') + 1;
141 }
142 argv[*argc] = namep;
143 *argc += 1;
144 }
145 } else if (*head == '"') {
146 if (*argc % 2 != 0) {
147 // we're not expecting a value
148 return strdup("Unable to parse criteria: "
149 "Found quoted value where it was not expected");
150 } else if (!valp) { // value starts here
151 valp = head + 1;
152 quoted = true;
153 } else {
154 // value ends here
155 argv[*argc] = valp;
156 *argc += 1;
157 *head = '\0';
158 valp = NULL;
159 namep = head + 1;
160 }
161 } else if (*argc % 2 == 0 && *head != ' ') {
162 // parse unquoted values
163 if (!valp) {
164 quoted = false;
165 valp = head; // value starts here
166 }
167 } else if (valp && !quoted && *head == ' ') {
168 // value ends here
169 argv[*argc] = valp;
170 *argc += 1;
171 *head = '\0';
172 valp = NULL;
173 namep = head + 1;
174 }
175 head++;
176 }
177
178 // catch last unquoted value if needed
179 if (valp && !quoted && !*head) {
180 argv[*argc] = valp;
181 *argc += 1;
182 }
183
184 return NULL;
185}
186
187// Returns error string on failure or NULL otherwise.
188static char *parse_criteria_name(enum criteria_type *type, char *name) {
189 *type = CRIT_LAST;
190 for (int i = 0; i < CRIT_LAST; i++) {
191 if (strcmp(criteria_strings[i], name) == 0) {
192 *type = (enum criteria_type) i;
193 break;
194 }
195 }
196 if (*type == CRIT_LAST) {
197 const char *fmt = "Criteria type '%s' is invalid or unsupported.";
198 int len = strlen(name) + strlen(fmt) - 1;
199 char *error = malloc(len);
200 snprintf(error, len, fmt, name);
201 return error;
202 } else if (*type == CRIT_URGENT || *type == CRIT_WINDOW_ROLE ||
203 *type == CRIT_WINDOW_TYPE) {
204 // (we're just being helpful here)
205 const char *fmt = "\"%s\" criteria currently unsupported, "
206 "no window will match this";
207 int len = strlen(fmt) + strlen(name) - 1;
208 char *error = malloc(len);
209 snprintf(error, len, fmt, name);
210 return error;
211 }
212 return NULL;
213}
214
215// Returns error string on failure or NULL otherwise.
216static char *generate_regex(pcre **regex, char *value) {
217 const char *reg_err;
218 int offset;
219
220 *regex = pcre_compile(value, PCRE_UTF8 | PCRE_UCP, &reg_err, &offset, NULL);
221
222 if (!*regex) {
223 const char *fmt = "Regex compilation (for '%s') failed: %s";
224 int len = strlen(fmt) + strlen(value) + strlen(reg_err) - 3;
225 char *error = malloc(len);
226 snprintf(error, len, fmt, value, reg_err);
227 return error;
228 }
229 return NULL;
230}
231
232// Test whether the criterion corresponds to the currently focused window
233static bool crit_is_focused(const char *value) {
234 return !strcmp(value, "focused") || !strcmp(value, "__focused__");
235}
236
237// Populate list with crit_tokens extracted from criteria string, returns error
238// string or NULL if successful.
239char *extract_crit_tokens(list_t *tokens, const char * const criteria) {
240 int argc;
241 char **argv = NULL, *error = NULL;
242 if ((error = crit_tokens(&argc, &argv, criteria))) {
243 goto ect_cleanup;
244 }
245 for (int i = 1; i + 1 < argc; i += 2) {
246 char* name = argv[i], *value = argv[i + 1];
247 struct crit_token *token = calloc(1, sizeof(struct crit_token));
248 token->raw = strdup(value);
249
250 if ((error = parse_criteria_name(&token->type, name))) {
251 free_crit_token(token);
252 goto ect_cleanup;
253 } else if (token->type == CRIT_URGENT || crit_is_focused(value)) {
254 wlr_log(L_DEBUG, "%s -> \"%s\"", name, value);
255 list_add(tokens, token);
256 } else if((error = generate_regex(&token->regex, value))) {
257 free_crit_token(token);
258 goto ect_cleanup;
259 } else {
260 wlr_log(L_DEBUG, "%s -> /%s/", name, value);
261 list_add(tokens, token);
262 }
263 }
264ect_cleanup:
265 free(argv[0]); // base string
266 free(argv);
267 return error;
268}
269
270static int regex_cmp(const char *item, const pcre *regex) {
271 return pcre_exec(regex, NULL, item, strlen(item), 0, 0, NULL, 0);
272}
273
274// test a single view if it matches list of criteria tokens (all of them).
275static bool criteria_test(swayc_t *cont, list_t *tokens) {
276 if (cont->type != C_VIEW) {
277 return false;
278 }
279 int matches = 0;
280 for (int i = 0; i < tokens->length; i++) {
281 struct crit_token *crit = tokens->items[i];
282 switch (crit->type) {
283 case CRIT_CLASS:
284 {
285 const char *class = view_get_class(cont->sway_view);
286 if (!class) {
287 break;
288 }
289 if (crit->regex && regex_cmp(class, crit->regex) == 0) {
290 matches++;
291 }
292 break;
293 }
294 case CRIT_CON_ID:
295 {
296 char *endptr;
297 size_t crit_id = strtoul(crit->raw, &endptr, 10);
298
299 if (*endptr == 0 && cont->id == crit_id) {
300 ++matches;
301 }
302 break;
303 }
304 case CRIT_CON_MARK:
305 // TODO
306 break;
307 case CRIT_FLOATING:
308 // TODO
309 break;
310 case CRIT_ID:
311 // TODO
312 break;
313 case CRIT_APP_ID:
314 {
315 const char *app_id = view_get_app_id(cont->sway_view);
316 if (!app_id) {
317 break;
318 }
319
320 if (crit->regex && regex_cmp(app_id, crit->regex) == 0) {
321 matches++;
322 }
323 break;
324 }
325 case CRIT_INSTANCE:
326 {
327 const char *instance = view_get_instance(cont->sway_view);
328 if (!instance) {
329 break;
330 }
331
332 if (crit->regex && regex_cmp(instance, crit->regex) == 0) {
333 matches++;
334 }
335 break;
336 }
337 case CRIT_TILING:
338 // TODO
339 break;
340 case CRIT_TITLE:
341 {
342 const char *title = view_get_title(cont->sway_view);
343 if (!title) {
344 break;
345 }
346
347 if (crit->regex && regex_cmp(title, crit->regex) == 0) {
348 matches++;
349 }
350 break;
351 }
352 case CRIT_URGENT:
353 // TODO "latest" or "oldest"
354 break;
355 case CRIT_WINDOW_ROLE:
356 // TODO
357 break;
358 case CRIT_WINDOW_TYPE:
359 // TODO
360 break;
361 case CRIT_WORKSPACE:
362 // TODO
363 break;
364 default:
365 sway_abort("Invalid criteria type (%i)", crit->type);
366 break;
367 }
368 }
369 return matches == tokens->length;
370}
371
372int criteria_cmp(const void *a, const void *b) {
373 if (a == b) {
374 return 0;
375 } else if (!a) {
376 return -1;
377 } else if (!b) {
378 return 1;
379 }
380 const struct criteria *crit_a = a, *crit_b = b;
381 int cmp = lenient_strcmp(crit_a->cmdlist, crit_b->cmdlist);
382 if (cmp != 0) {
383 return cmp;
384 }
385 return lenient_strcmp(crit_a->crit_raw, crit_b->crit_raw);
386}
387
388void free_criteria(struct criteria *crit) {
389 if (crit->tokens) {
390 free_crit_tokens(crit->tokens);
391 }
392 if (crit->cmdlist) {
393 free(crit->cmdlist);
394 }
395 if (crit->crit_raw) {
396 free(crit->crit_raw);
397 }
398 free(crit);
399}
400
401bool criteria_any(swayc_t *cont, list_t *criteria) {
402 for (int i = 0; i < criteria->length; i++) {
403 struct criteria *bc = criteria->items[i];
404 if (criteria_test(cont, bc->tokens)) {
405 return true;
406 }
407 }
408 return false;
409}
410
411list_t *criteria_for(swayc_t *cont) {
412 list_t *criteria = config->criteria, *matches = create_list();
413 for (int i = 0; i < criteria->length; i++) {
414 struct criteria *bc = criteria->items[i];
415 if (criteria_test(cont, bc->tokens)) {
416 list_add(matches, bc);
417 }
418 }
419 return matches;
420}
421
422struct list_tokens {
423 list_t *list;
424 list_t *tokens;
425};
426
427static void container_match_add(swayc_t *container,
428 struct list_tokens *list_tokens) {
429 if (criteria_test(container, list_tokens->tokens)) {
430 list_add(list_tokens->list, container);
431 }
432}
433
434list_t *container_for_crit_tokens(list_t *tokens) {
435 struct list_tokens list_tokens =
436 (struct list_tokens){create_list(), tokens};
437
438 container_map(&root_container,
439 (void (*)(swayc_t *, void *))container_match_add,
440 &list_tokens);
441
442 // TODO look in the scratchpad
443
444 return list_tokens.list;
445}