From 3b0c26d149dfe5e05df338692db8255a01f0998d Mon Sep 17 00:00:00 2001 From: Ryan Dwyer Date: Wed, 9 May 2018 14:23:20 +1000 Subject: Overhaul criteria implementation The criteria struct now uses properties for each token type rather than the list_t list of tokens. The reason for this is that different token types have different data types: pcre, string and number to name a few. This solution should be more flexible moving forward. A bonus of this is that criteria is now easier to understand when looking at the struct definition. The criteria parser has been rewritten because the previous one didn't support valueless pairs (eg. [class="foo" floating]). Criteria now has types. Types at the moment are CT_COMMAND, CT_ASSIGN_WORKSPACE and CT_ASSIGN_OUTPUT. i3 uses types as well. Previously the assign command was creating a criteria with 'move to workspace ' as its command, but this caused the window to appear briefly on the focused workspace before being moved to the assigned workspace. It now creates the view directly in the assigned workspace. Each view will only execute a given criteria once. This is achieved by storing a list of executed criteria in the view. This is the same strategy used by i3. Escaping now works properly. Previously you could do things like [class="Fire\"fox"] and the stored value would be 'Fire\"fox', but it should be (and now is) 'Fire"fox'. The public functions in criteria.c are now all prefixed with criteria_. Xwayland views now listen to the set_title, set_class and set_window_type events and criteria will be run when these happen. XDG shell has none of these events so it continues to update the title in handle_commit. Each view type's get_prop function has been split into get_string_prop and get_int_prop because some properties like the X11 window ID and window type are numeric. The following new criteria tokens are now supported: * id (X11 window ID) * instance * tiling * workspace --- sway/criteria.c | 685 ++++++++++++++++++++++++++------------------------------ 1 file changed, 316 insertions(+), 369 deletions(-) (limited to 'sway/criteria.c') diff --git a/sway/criteria.c b/sway/criteria.c index 22e9a49b..6abe24af 100644 --- a/sway/criteria.c +++ b/sway/criteria.c @@ -11,435 +11,382 @@ #include "list.h" #include "log.h" -enum criteria_type { // *must* keep in sync with criteria_strings[] - CRIT_APP_ID, - 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_APP_ID] = "app_id", - [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" -}; +bool criteria_is_empty(struct criteria *criteria) { + return !criteria->title + && !criteria->app_id + && !criteria->class + && !criteria->instance + && !criteria->con_mark + && !criteria->con_id + && !criteria->id + && !criteria->window_role + && !criteria->window_type + && !criteria->floating + && !criteria->tiling + && !criteria->urgent + && !criteria->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; -}; +void criteria_destroy(struct criteria *criteria) { + pcre_free(criteria->title); + pcre_free(criteria->app_id); + pcre_free(criteria->class); + pcre_free(criteria->instance); + pcre_free(criteria->con_mark); + pcre_free(criteria->window_role); + free(criteria->workspace); + + free(criteria->raw); + free(criteria); +} -static void free_crit_token(struct crit_token *crit) { - pcre_free(crit->regex); - free(crit->raw); - free(crit); +static int regex_cmp(const char *item, const pcre *regex) { + return pcre_exec(regex, NULL, item, strlen(item), 0, 0, NULL, 0); } -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]); +static bool criteria_matches_view(struct criteria *criteria, + struct sway_view *view) { + if (criteria->title) { + const char *title = view_get_title(view); + if (!title || regex_cmp(title, criteria->title) != 0) { + return false; + } } - 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); + if (criteria->app_id) { + const char *app_id = view_get_app_id(view); + if (!app_id || regex_cmp(app_id, criteria->app_id) != 0) { + return false; + } } - int last = strlen(criteria) - 1; - if (criteria[last] == ']') { - criteria[last] = '\0'; + if (criteria->class) { + const char *class = view_get_class(view); + if (!class || regex_cmp(class, criteria->class) != 0) { + return false; + } } - 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; + if (criteria->instance) { + const char *instance = view_get_instance(view); + if (!instance || regex_cmp(instance, criteria->instance) != 0) { + return false; } } - 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) { - wlr_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; + if (criteria->con_mark) { + // TODO + return false; + } + + if (criteria->con_id) { // Internal ID + if (!view->swayc || view->swayc->id != criteria->con_id) { + return false; } - head++; } - // catch last unquoted value if needed - if (valp && !quoted && !*head) { - argv[*argc] = valp; - *argc += 1; + if (criteria->id) { // X11 window ID + uint32_t x11_window_id = view_get_x11_window_id(view); + if (!x11_window_id || x11_window_id != criteria->id) { + return false; + } } - return NULL; + if (criteria->window_role) { + // TODO + } + + if (criteria->window_type) { + uint32_t type = view_get_window_type(view); + if (!type || type != criteria->window_type) { + return false; + } + } + + if (criteria->floating) { + // TODO + return false; + } + + if (criteria->tiling) { + // TODO + } + + if (criteria->urgent) { + // TODO + return false; + } + + if (criteria->workspace) { + if (!view->swayc) { + return false; + } + struct sway_container *ws = container_parent(view->swayc, C_WORKSPACE); + if (!ws || strcmp(ws->name, criteria->workspace) != 0) { + return false; + } + } + + return true; } -// 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; +list_t *criteria_for_view(struct sway_view *view, enum criteria_type types) { + list_t *criterias = config->criteria; + list_t *matches = create_list(); + for (int i = 0; i < criterias->length; ++i) { + struct criteria *criteria = criterias->items[i]; + if ((criteria->type & types) && criteria_matches_view(criteria, view)) { + list_add(matches, criteria); } } - 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 matches; +} + +struct match_data { + struct criteria *criteria; + list_t *matches; +}; + +static void criteria_get_views_iterator(struct sway_container *container, + void *data) { + struct match_data *match_data = data; + if (container->type == C_VIEW) { + if (criteria_matches_view(match_data->criteria, container->sway_view)) { + list_add(match_data->matches, container->sway_view); + } } - return NULL; } +list_t *criteria_get_views(struct criteria *criteria) { + list_t *matches = create_list(); + struct match_data data = { + .criteria = criteria, + .matches = matches, + }; + container_for_each_descendant_dfs(&root_container, + criteria_get_views_iterator, &data); + return matches; +} + +// The error pointer is used for parsing functions, and saves having to pass it +// as an argument in several places. +char *error = NULL; + // Returns error string on failure or NULL otherwise. -static char *generate_regex(pcre **regex, char *value) { +static bool 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"; + const char *fmt = "Regex compilation for '%s' failed: %s"; int len = strlen(fmt) + strlen(value) + strlen(reg_err) - 3; - char *error = malloc(len); + error = malloc(len); snprintf(error, len, fmt, value, reg_err); - return error; + return false; } - 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__"); + return true; } -// 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; +static bool parse_token(struct criteria *criteria, char *name, char *value) { + // Require value, unless token is floating or tiled + if (!value && (strcmp(name, "title") == 0 + || strcmp(name, "app_id") == 0 + || strcmp(name, "class") == 0 + || strcmp(name, "instance") == 0 + || strcmp(name, "con_id") == 0 + || strcmp(name, "con_mark") == 0 + || strcmp(name, "window_role") == 0 + || strcmp(name, "window_type") == 0 + || strcmp(name, "id") == 0 + || strcmp(name, "urgent") == 0 + || strcmp(name, "workspace") == 0)) { + const char *fmt = "Token '%s' requires a value"; + int len = strlen(fmt) + strlen(name) - 1; + error = malloc(len); + snprintf(error, len, fmt, name); + return false; } - 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)) { - wlr_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; + + if (strcmp(name, "title") == 0) { + generate_regex(&criteria->title, value); + } else if (strcmp(name, "app_id") == 0) { + generate_regex(&criteria->app_id, value); + } else if (strcmp(name, "class") == 0) { + generate_regex(&criteria->class, value); + } else if (strcmp(name, "instance") == 0) { + generate_regex(&criteria->instance, value); + } else if (strcmp(name, "con_id") == 0) { + char *endptr; + criteria->con_id = strtoul(value, &endptr, 10); + if (*endptr != 0) { + error = strdup("The value for 'con_id' should be numeric"); + } + } else if (strcmp(name, "con_mark") == 0) { + generate_regex(&criteria->con_mark, value); + } else if (strcmp(name, "window_role") == 0) { + generate_regex(&criteria->window_role, value); + } else if (strcmp(name, "window_type") == 0) { + // TODO: This is a string but will be stored as an enum or integer + } else if (strcmp(name, "id") == 0) { + char *endptr; + criteria->id = strtoul(value, &endptr, 10); + if (*endptr != 0) { + error = strdup("The value for 'id' should be numeric"); + } + } else if (strcmp(name, "floating") == 0) { + criteria->floating = true; + } else if (strcmp(name, "tiling") == 0) { + criteria->tiling = true; + } else if (strcmp(name, "urgent") == 0) { + if (strcmp(value, "latest") == 0) { + criteria->urgent = 'l'; + } else if (strcmp(value, "oldest") == 0) { + criteria->urgent = 'o'; } else { - wlr_log(L_DEBUG, "%s -> /%s/", name, value); - list_add(tokens, token); + error = + strdup("The value for 'urgent' must be 'latest' or 'oldest'"); } + } else if (strcmp(name, "workspace") == 0) { + criteria->workspace = strdup(value); + } else { + const char *fmt = "Token '%s' is not recognized"; + int len = strlen(fmt) + strlen(name) - 1; + error = malloc(len); + snprintf(error, len, fmt, name); } -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(struct sway_container *cont, list_t *tokens) { - if (cont->type != C_CONTAINER && cont->type != C_VIEW) { + if (error) { 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: - { - const char *class = view_get_class(cont->sway_view); - if (!class) { - break; - } - if (crit->regex && regex_cmp(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: - // TODO - break; - case CRIT_FLOATING: - // TODO - break; - case CRIT_ID: - // TODO - break; - case CRIT_APP_ID: - { - const char *app_id = view_get_app_id(cont->sway_view); - if (!app_id) { - break; - } - - if (crit->regex && regex_cmp(app_id, crit->regex) == 0) { - matches++; - } - break; - } - case CRIT_INSTANCE: - { - const char *instance = view_get_instance(cont->sway_view); - if (!instance) { - break; - } - - if (crit->regex && regex_cmp(instance, crit->regex) == 0) { - matches++; - } - break; - } - case CRIT_TILING: - // TODO - break; - case CRIT_TITLE: - { - const char *title = view_get_title(cont->sway_view); - if (!title) { - break; - } - if (crit->regex && regex_cmp(title, crit->regex) == 0) { - matches++; - } - break; - } - case CRIT_URGENT: - // TODO "latest" or "oldest" - break; - case CRIT_WINDOW_ROLE: - // TODO - break; - case CRIT_WINDOW_TYPE: - // TODO - break; - case CRIT_WORKSPACE: - // TODO - break; - default: - sway_abort("Invalid criteria type (%i)", crit->type); - break; - } - } - return matches == tokens->length; + return true; } -int criteria_cmp(const void *a, const void *b) { - if (a == b) { - return 0; - } else if (!a) { - return -1; - } else if (!b) { - return 1; +static void skip_spaces(char **head) { + while (**head == ' ') { + ++*head; } - 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); +// Remove escaping slashes from value +static void unescape(char *value) { + if (!strchr(value, '\\')) { + return; } - if (crit->crit_raw) { - free(crit->crit_raw); - } - free(crit); -} - -bool criteria_any(struct sway_container *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; + char *copy = calloc(strlen(value) + 1, 1); + char *readhead = value; + char *writehead = copy; + while (*readhead) { + if (*readhead == '\\' && + (*(readhead + 1) == '"' || *(readhead + 1) == '\\')) { + // skip the slash + ++readhead; } + *writehead = *readhead; + ++writehead; + ++readhead; } - return false; + strcpy(value, copy); + free(copy); } -list_t *criteria_for(struct sway_container *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); +/** + * Parse a raw criteria string such as [class="foo" instance="bar"] into a + * criteria struct. + * + * If errors are found, NULL will be returned and the error argument will be + * populated with an error string. + */ +struct criteria *criteria_parse(char *raw, char **error_arg) { + free(error); + error = NULL; + + char *head = raw; + skip_spaces(&head); + if (*head != '[') { + *error_arg = strdup("No criteria"); + return NULL; + } + ++head; + + struct criteria *criteria = calloc(sizeof(struct criteria), 1); + char *name = NULL, *value = NULL; + bool in_quotes = false; + + while (*head && *head != ']') { + skip_spaces(&head); + // Parse token name + char *namestart = head; + while ((*head >= 'a' && *head <= 'z') || *head == '_') { + ++head; } + name = calloc(head - namestart + 1, 1); + strncpy(name, namestart, head - namestart); + // Parse token value + skip_spaces(&head); + value = NULL; + if (*head == '=') { + ++head; + skip_spaces(&head); + if (*head == '"') { + in_quotes = true; + ++head; + } + char *valuestart = head; + if (in_quotes) { + while (*head && (*head != '"' || *(head - 1) == '\\')) { + ++head; + } + if (!*head) { + *error_arg = strdup("Quote mismatch in criteria"); + goto cleanup; + } + } else { + while (*head && *head != ' ' && *head != ']') { + ++head; + } + } + value = calloc(head - valuestart + 1, 1); + strncpy(value, valuestart, head - valuestart); + if (in_quotes) { + ++head; + in_quotes = false; + } + unescape(value); + } + wlr_log(L_DEBUG, "Found pair: %s=%s", name, value); + if (!parse_token(criteria, name, value)) { + *error_arg = error; + goto cleanup; + } + skip_spaces(&head); + free(name); + free(value); + name = NULL; + value = NULL; } - return matches; -} - -struct list_tokens { - list_t *list; - list_t *tokens; -}; - -static void container_match_add(struct sway_container *container, - struct list_tokens *list_tokens) { - if (criteria_test(container, list_tokens->tokens)) { - list_add(list_tokens->list, container); + if (*head != ']') { + *error_arg = strdup("No closing brace found in criteria"); + goto cleanup; } -} -list_t *container_for_crit_tokens(list_t *tokens) { - struct list_tokens list_tokens = - (struct list_tokens){create_list(), tokens}; + if (criteria_is_empty(criteria)) { + *error_arg = strdup("Criteria is empty"); + goto cleanup; + } - container_for_each_descendant_dfs(&root_container, - (void (*)(struct sway_container *, void *))container_match_add, - &list_tokens); + ++head; + int len = head - raw; + criteria->raw = calloc(len + 1, 1); + strncpy(criteria->raw, raw, len); + return criteria; - // TODO look in the scratchpad - - return list_tokens.list; +cleanup: + free(name); + free(value); + criteria_destroy(criteria); + return NULL; } -- cgit v1.2.3-54-g00ecf From 0bf0a4fa4049cbabeb797536e549640ec5235454 Mon Sep 17 00:00:00 2001 From: Ryan Dwyer Date: Fri, 11 May 2018 10:42:24 +1000 Subject: Don't unescape \\ in criteria --- sway/criteria.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'sway/criteria.c') diff --git a/sway/criteria.c b/sway/criteria.c index 6abe24af..7da790e6 100644 --- a/sway/criteria.c +++ b/sway/criteria.c @@ -280,8 +280,7 @@ static void unescape(char *value) { char *readhead = value; char *writehead = copy; while (*readhead) { - if (*readhead == '\\' && - (*(readhead + 1) == '"' || *(readhead + 1) == '\\')) { + if (*readhead == '\\' && *(readhead + 1) == '"') { // skip the slash ++readhead; } -- cgit v1.2.3-54-g00ecf