aboutsummaryrefslogtreecommitdiff
path: root/sway/criteria.c
blob: 706da04f94c9dfb7e0326c4afd671c4c02e8d89e (plain)
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
#define _XOPEN_SOURCE 500
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <pcre.h>
#include "sway/criteria.h"
#include "sway/container.h"
#include "sway/config.h"
#include "stringop.h"
#include "list.h"
#include "log.h"

enum criteria_type { // *must* keep in sync with criteria_strings[]
	CRIT_CLASS,
	CRIT_CON_MARK,
	CRIT_ID,
	CRIT_INSTANCE,
	CRIT_TITLE,
	CRIT_URGENT,
	CRIT_WINDOW_ROLE,
	CRIT_WINDOW_TYPE,
	CRIT_WORKSPACE,
	CRIT_LAST
};

// this *must* match the ordering in criteria_type enum
static const char * const criteria_strings[] = {
	"class",
	"con_mark",
	"id",
	"instance",
	"title",
	"urgent", // either "latest" or "oldest" ...
	"window_role",
	"window_type",
	"workspace"
};

/**
 * A single criteria token (ie. value/regex pair),
 * e.g. 'class="some class regex"'.
 */
struct crit_token {
	enum criteria_type type;
	pcre *regex;
	char *raw;
};

static void free_crit_token(struct crit_token *crit) {
	pcre_free(crit->regex);
	free(crit->raw);
	free(crit);
}

static void free_crit_tokens(list_t *crit_tokens) {
	for (int i = 0; i < crit_tokens->length; i++) {
		free_crit_token(crit_tokens->items[i]);
	}
	list_free(crit_tokens);
}

// Extracts criteria string from its brackets. Returns new (duplicate)
// substring.
static char *criteria_from(const char *arg) {
	char *criteria = NULL;
	if (*arg == '[') {
		criteria = strdup(arg + 1);
	} else {
		criteria = strdup(arg);
	}

	int last = strlen(criteria) - 1;
	if (criteria[last] == ']') {
		criteria[last] = '\0';
	}
	return criteria;
}

// Return instances of c found in str.
static int countchr(char *str, char c) {
	int found = 0;
	for (int i = 0; str[i]; i++) {
		if (str[i] == c) {
			++found;
		}
	}
	return found;
}

// criteria_str is e.g. '[class="some class regex" instance="instance name"]'.
//
// Will create array of pointers in buf, where first is duplicate of given
// string (must be freed) and the rest are pointers to names and values in the
// base string (every other, naturally). argc will be populated with the length
// of buf.
//
// Returns error string or NULL if successful.
static char *crit_tokens(int *argc, char ***buf, const char * const criteria_str) {
	sway_log(L_DEBUG, "Parsing criteria: '%s'", criteria_str);
	char *base = criteria_from(criteria_str);
	char *head = base;
	char *namep = head; // start of criteria name
	char *valp = NULL; // start of value

	// We're going to place EOS markers where we need to and fill up an array
	// of pointers to the start of each token (either name or value).
	int pairs = countchr(base, '=');
	int max_tokens = pairs * 2 + 1; // this gives us at least enough slots

	char **argv = *buf = calloc(max_tokens, sizeof(char*));
	argv[0] = base; // this needs to be freed by caller

	*argc = 1; // uneven = name, even = value
	while (*head && *argc < max_tokens) {
		if (namep != head && *(head - 1) == '\\') {
			// escaped character: don't try to parse this
		} else if (*head == '=' && namep != head) {
			if (*argc % 2 != 1) {
				// we're not expecting a name
				return strdup("Unable to parse criteria: "
					"Found out of place equal sign");
			} else {
				// name ends here
				char *end = head; // don't want to rewind the head
				while (*(end - 1) == ' ') {
					--end;
				}
				*end = '\0';
				if (*(namep) == ' ') {
					namep = strrchr(namep, ' ') + 1;
				}
				argv[(*argc)++] = namep;
			}
		} else if (*head == '"') {
			if (*argc % 2 != 0) {
				// we're not expecting a value
				return strdup("Unable to parse criteria: "
					"Found quoted value where it was not expected");
			} else if (!valp) { // value starts here
				valp = head + 1;
			} else {
				// value ends here
				argv[(*argc)++] = valp;
				*head = '\0';
				valp = NULL;
				namep = head + 1;
			}
		} else if (*argc % 2 == 0 && !valp && *head != ' ') {
			// We're expecting a quoted value, haven't found one yet, and this
			// is not an empty space.
			return strdup("Unable to parse criteria: "
				"Names must be unquoted, values must be quoted");
		}
		head++;
	}
	return NULL;
}

// Returns error string on failure or NULL otherwise.
static char *parse_criteria_name(enum criteria_type *type, char *name) {
	*type = CRIT_LAST;
	for (int i = 0; i < CRIT_LAST; i++) {
		if (strcmp(criteria_strings[i], name) == 0) {
			*type = (enum criteria_type) i;
			break;
		}
	}
	if (*type == CRIT_LAST) {
		const char *fmt = "Criteria type '%s' is invalid or unsupported.";
		int len = strlen(name) + strlen(fmt) - 1;
		char *error = malloc(len);
		snprintf(error, len, fmt, name);
		return error;
	} else if (*type == CRIT_URGENT || *type == CRIT_WINDOW_ROLE ||
			*type == CRIT_WINDOW_TYPE) {
		// (we're just being helpful here)
		const char *fmt = "\"%s\" criteria currently unsupported, "
			"no window will match this";
		int len = strlen(fmt) + strlen(name) - 1;
		char *error = malloc(len);
		snprintf(error, len, fmt, name);
		return error;
	}
	return NULL;
}

// Returns error string on failure or NULL otherwise.
static char *generate_regex(pcre **regex, char *value) {
	const char *reg_err;
	int offset;

	*regex = pcre_compile(value, PCRE_UTF8 | PCRE_UCP, &reg_err, &offset, NULL);

	if (!*regex) {
		const char *fmt = "Regex compilation (for '%s') failed: %s";
		int len = strlen(fmt) + strlen(value) + strlen(reg_err) - 3;
		char *error = malloc(len);
		snprintf(error, len, fmt, value, reg_err);
		return error;
	}
	return NULL;
}

// Test whether the criterion corresponds to the currently focused window
static bool crit_is_focused(const char *value) {
	return !strcmp(value, "focused") || !strcmp(value, "__focused__");
}

// Populate list with crit_tokens extracted from criteria string, returns error
// string or NULL if successful.
char *extract_crit_tokens(list_t *tokens, const char * const criteria) {
	int argc;
	char **argv = NULL, *error = NULL;
	if ((error = crit_tokens(&argc, &argv, criteria))) {
		goto ect_cleanup;
	}
	for (int i = 1; i + 1 < argc; i += 2) {
		char* name = argv[i], *value = argv[i + 1];
		struct crit_token *token = calloc(1, sizeof(struct crit_token));
		token->raw = strdup(value);

		if ((error = parse_criteria_name(&token->type, name))) {
			free_crit_token(token);
			goto ect_cleanup;
		} else if (token->type == CRIT_URGENT || crit_is_focused(value)) {
			sway_log(L_DEBUG, "%s -> \"%s\"", name, value);
			list_add(tokens, token);
		} else if((error = generate_regex(&token->regex, value))) {
			free_crit_token(token);
			goto ect_cleanup;
		} else {
			sway_log(L_DEBUG, "%s -> /%s/", name, value);
			list_add(tokens, token);
		}
	}
ect_cleanup:
	free(argv[0]); // base string
	free(argv);
	return error;
}

static int regex_cmp(const char *item, const pcre *regex) {
    return pcre_exec(regex, NULL, item, strlen(item), 0, 0, NULL, 0);
}

// test a single view if it matches list of criteria tokens (all of them).
static bool criteria_test(swayc_t *cont, list_t *tokens) {
	if (cont->type != C_VIEW) {
		return false;
	}
	int matches = 0;
	for (int i = 0; i < tokens->length; i++) {
		struct crit_token *crit = tokens->items[i];
		switch (crit->type) {
		case CRIT_CLASS:
			if (!cont->class) {
				// ignore
			} else if (crit_is_focused(crit->raw)) {
				swayc_t *focused = get_focused_view(&root_container);
				if (focused->class && strcmp(cont->class, focused->class) == 0) {
					matches++;
				}
			} else if (crit->regex && regex_cmp(cont->class, crit->regex) == 0) {
				matches++;
			}
			break;
		case CRIT_CON_MARK:
			if (crit->regex && cont->marks && (list_seq_find(cont->marks, (int (*)(const void *, const void *))regex_cmp, crit->regex) != -1)) {
				// Make sure it isn't matching the NUL string
				if ((strcmp(crit->raw, "") == 0) == (list_seq_find(cont->marks, (int (*)(const void *, const void *))strcmp, "") != -1)) {
					++matches;
				}
			}
			break;
		case CRIT_ID:
			if (!cont->app_id) {
				// ignore
			} else if (crit->regex && regex_cmp(cont->app_id, crit->regex) == 0) {
				matches++;
			}
			break;
		case CRIT_INSTANCE:
			if (!cont->instance) {
				// ignore
			} else if (crit_is_focused(crit->raw)) {
				swayc_t *focused = get_focused_view(&root_container);
				if (focused->instance && strcmp(cont->instance, focused->instance) == 0) {
					matches++;
				}
			} else if (crit->regex && regex_cmp(cont->instance, crit->regex) == 0) {
				matches++;
			}
			break;
		case CRIT_TITLE:
			if (!cont->name) {
				// ignore
			} else if (crit_is_focused(crit->raw)) {
				swayc_t *focused = get_focused_view(&root_container);
				if (focused->name && strcmp(cont->name, focused->name) == 0) {
					matches++;
				}
			} else if (crit->regex && regex_cmp(cont->name, crit->regex) == 0) {
				matches++;
			}
			break;
		case CRIT_URGENT: // "latest" or "oldest"
			break;
		case CRIT_WINDOW_ROLE:
			break;
		case CRIT_WINDOW_TYPE:
			// TODO wlc indeed exposes this information
			break;
		case CRIT_WORKSPACE: ;
			swayc_t *cont_ws = swayc_parent_by_type(cont, C_WORKSPACE);
			if (!cont_ws || !cont_ws->name) {
				// ignore
			} else if (crit_is_focused(crit->raw)) {
				swayc_t *focused_ws = swayc_active_workspace();
				if (focused_ws->name && strcmp(cont_ws->name, focused_ws->name) == 0) {
					matches++;
				}
			} else if (crit->regex && regex_cmp(cont_ws->name, crit->regex) == 0) {
				matches++;
			}
			break;
		default:
			sway_abort("Invalid criteria type (%i)", crit->type);
			break;
		}
	}
	return matches == tokens->length;
}

int criteria_cmp(const void *a, const void *b) {
	if (a == b) {
		return 0;
	} else if (!a) {
		return -1;
	} else if (!b) {
		return 1;
	}
	const struct criteria *crit_a = a, *crit_b = b;
	int cmp = lenient_strcmp(crit_a->cmdlist, crit_b->cmdlist);
	if (cmp != 0) {
		return cmp;
	}
	return lenient_strcmp(crit_a->crit_raw, crit_b->crit_raw);
}

void free_criteria(struct criteria *crit) {
	if (crit->tokens) {
		free_crit_tokens(crit->tokens);
	}
	if (crit->cmdlist) {
		free(crit->cmdlist);
	}
	if (crit->crit_raw) {
		free(crit->crit_raw);
	}
	free(crit);
}

list_t *criteria_for(swayc_t *cont) {
	list_t *criteria = config->criteria, *matches = create_list();
	for (int i = 0; i < criteria->length; i++) {
		struct criteria *bc = criteria->items[i];
		if (criteria_test(cont, bc->tokens)) {
			list_add(matches, bc);
		}
	}
	return matches;
}

struct list_tokens {
	list_t *list;
	list_t *tokens;
};

static void container_match_add(swayc_t *container, struct list_tokens *list_tokens) {
	if (criteria_test(container, list_tokens->tokens)) {
		list_add(list_tokens->list, container);
	}
}
list_t *container_for(list_t *tokens) {
	struct list_tokens list_tokens = (struct list_tokens){create_list(), tokens};

	container_map(&root_container, (void (*)(swayc_t *, void *))container_match_add, &list_tokens);

	return list_tokens.list;
}