mirror of
https://github.com/gwenhael-le-moine/sway-patched-tray-menu.git
synced 2024-12-28 22:23:42 +01:00
514eed7e4b
Sometimes it doesn't really make sense to quote them (numeric values for example) In that case, the value is parsed until the next space or the end of the whole criteria expression
443 lines
12 KiB
C
443 lines
12 KiB
C
#define _XOPEN_SOURCE 700
|
|
#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_ID,
|
|
CRIT_CON_MARK,
|
|
CRIT_FLOATING,
|
|
CRIT_ID,
|
|
CRIT_INSTANCE,
|
|
CRIT_TILING,
|
|
CRIT_TITLE,
|
|
CRIT_URGENT,
|
|
CRIT_WINDOW_ROLE,
|
|
CRIT_WINDOW_TYPE,
|
|
CRIT_WORKSPACE,
|
|
CRIT_LAST
|
|
};
|
|
|
|
static const char * const criteria_strings[CRIT_LAST] = {
|
|
[CRIT_CLASS] = "class",
|
|
[CRIT_CON_ID] = "con_id",
|
|
[CRIT_CON_MARK] = "con_mark",
|
|
[CRIT_FLOATING] = "floating",
|
|
[CRIT_ID] = "id",
|
|
[CRIT_INSTANCE] = "instance",
|
|
[CRIT_TILING] = "tiling",
|
|
[CRIT_TITLE] = "title",
|
|
[CRIT_URGENT] = "urgent", // either "latest" or "oldest" ...
|
|
[CRIT_WINDOW_ROLE] = "window_role",
|
|
[CRIT_WINDOW_TYPE] = "window_type",
|
|
[CRIT_WORKSPACE] = "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
|
|
bool quoted = true;
|
|
|
|
*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;
|
|
*argc += 1;
|
|
}
|
|
} 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;
|
|
quoted = true;
|
|
} else {
|
|
// value ends here
|
|
argv[*argc] = valp;
|
|
*argc += 1;
|
|
*head = '\0';
|
|
valp = NULL;
|
|
namep = head + 1;
|
|
}
|
|
} else if (*argc % 2 == 0 && *head != ' ') {
|
|
// parse unquoted values
|
|
if (!valp) {
|
|
quoted = false;
|
|
valp = head; // value starts here
|
|
}
|
|
} else if (valp && !quoted && *head == ' ') {
|
|
// value ends here
|
|
argv[*argc] = valp;
|
|
*argc += 1;
|
|
*head = '\0';
|
|
valp = NULL;
|
|
namep = head + 1;
|
|
}
|
|
head++;
|
|
}
|
|
|
|
// catch last unquoted value if needed
|
|
if (valp && !quoted && !*head) {
|
|
argv[*argc] = valp;
|
|
*argc += 1;
|
|
}
|
|
|
|
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, ®_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_ID: {
|
|
char *endptr;
|
|
size_t crit_id = strtoul(crit->raw, &endptr, 10);
|
|
|
|
if (*endptr == 0 && cont->id == crit_id) {
|
|
++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_FLOATING:
|
|
if (cont->is_floating) {
|
|
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_TILING:
|
|
if (!cont->is_floating) {
|
|
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);
|
|
}
|
|
|
|
bool criteria_any(swayc_t *cont, list_t *criteria) {
|
|
for (int i = 0; i < criteria->length; i++) {
|
|
struct criteria *bc = criteria->items[i];
|
|
if (criteria_test(cont, bc->tokens)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|