From a06cb7cd01acfbb5e31dd1aacbbde7887a0509b9 Mon Sep 17 00:00:00 2001 From: "S. Christoffer Eliesen" Date: Tue, 17 Nov 2015 19:27:01 +0100 Subject: [PATCH] criteria: Add. Learn for_window command. A criteria is a string in the form of `[class="regex.*" title="str"]`. It is stored in a struct with a list of *tokens* which is a attribute/value pair (stored as a `crit_token` struct). Most tokens will also have a precompiled regex stored that will be used during criteria matching. for_window command: When a new view is created its metadata is tested against all stored criteria, and if a match is found the associated command list is executed. Unfortunately some metadata is not available in sway at the moment (specifically `instance`, `window_role` and `urgent`). Any criteria string that tries to match an unsupported attribute will fail. (Note that while the criteria code can be used to parse any criteria string it is currently only used by the `for_window` command.) --- include/config.h | 1 + include/criteria.h | 36 +++++ sway.5.txt | 35 +++++ sway/commands.c | 34 +++++ sway/config.c | 7 + sway/criteria.c | 349 +++++++++++++++++++++++++++++++++++++++++++++ sway/handlers.c | 16 +++ 7 files changed, 478 insertions(+) create mode 100644 include/criteria.h create mode 100644 sway/criteria.c diff --git a/include/config.h b/include/config.h index 3bdbdb7b..6b48063a 100644 --- a/include/config.h +++ b/include/config.h @@ -66,6 +66,7 @@ struct sway_config { list_t *cmd_queue; list_t *workspace_outputs; list_t *output_configs; + list_t *criteria; struct sway_mode *current_mode; uint32_t floating_mod; enum swayc_layouts default_orientation; diff --git a/include/criteria.h b/include/criteria.h new file mode 100644 index 00000000..5c71d172 --- /dev/null +++ b/include/criteria.h @@ -0,0 +1,36 @@ +#ifndef _SWAY_CRITERIA_H +#define _SWAY_CRITERIA_H + +#include "container.h" +#include "list.h" + +/** + * Maps criteria (as a list of criteria tokens) to a command list. + * + * A list of tokens together represent a single criteria string (e.g. + * '[class="abc" title="xyz"]' becomes two criteria tokens). + * + * for_window: Views matching all criteria will have the bound command list + * executed on them. + * + * Set via `for_window `. + */ +struct criteria { + list_t *tokens; // struct crit_token, contains compiled regex. + char *crit_raw; // entire criteria string (for logging) + + char *cmdlist; +}; + +int criteria_cmp(const void *item, const void *data); +void free_criteria(struct criteria *crit); + +// Pouplate list with crit_tokens extracted from criteria string, returns error +// string or NULL if successful. +char *extract_crit_tokens(list_t *tokens, const char *criteria); + +// Returns list of criteria that match given container. These criteria have +// been set with `for_window` commands and have an associated cmdlist. +list_t *criteria_for(swayc_t *cont); + +#endif diff --git a/sway.5.txt b/sway.5.txt index 5e9dbb1e..700de8b2 100644 --- a/sway.5.txt +++ b/sway.5.txt @@ -68,6 +68,10 @@ Commands **fullscreen**:: Toggles fullscreen status for the focused view. +**for_window** :: + Whenever a window that matches _criteria_ appears, run list of commands. See + **Criteria** section below. + **gaps** edge_gaps :: Whether or not to add gaps between views and workspace edges if amount of inner gap is not zero. When _no_, no gap is added where the view is aligned to @@ -175,3 +179,34 @@ Commands **workspace** output :: Specifies that the workspace named _name_ should appear on the specified _output_. + +Criteria +-------- + +A criteria is a string in the form of e.g.: + + [class="[Rr]egex.*" title="some title"] + +The string contains one or more (space separated) attribute/value pairs and they +are used by some commands filter which views to execute actions on. All attributes +must match for the criteria string to match. + +Currently supported attributes: + +**class**:: + Compare value against the window class. Can be a regular expression. If value + is _focused_ then the window class must be the same as that of the currently + focused window. + +**id**:: + Compare value against the app id. Can be a regular expression. + +**title**:: + Compare against the window title. Can be a regular expression. If value is + _focused_ then the window title must be the same as that of the currently + focused window. + +**workspace**:: + Compare against the workspace name for this view. Can be a regular expression. + If value is _focused_ then all the views on the currently focused workspace + matches. diff --git a/sway/commands.c b/sway/commands.c index 42105d5f..6a4af43c 100644 --- a/sway/commands.c +++ b/sway/commands.c @@ -23,6 +23,7 @@ #include "sway.h" #include "resize.h" #include "input_state.h" +#include "criteria.h" typedef struct cmd_results *sway_cmd(int argc, char **argv); @@ -41,6 +42,7 @@ static sway_cmd cmd_floating; static sway_cmd cmd_floating_mod; static sway_cmd cmd_focus; static sway_cmd cmd_focus_follows_mouse; +static sway_cmd cmd_for_window; static sway_cmd cmd_fullscreen; static sway_cmd cmd_gaps; static sway_cmd cmd_kill; @@ -1241,6 +1243,37 @@ static struct cmd_results *cmd_log_colors(int argc, char **argv) { return cmd_results_new(CMD_SUCCESS, NULL, NULL); } +static struct cmd_results *cmd_for_window(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "for_window", EXPECTED_AT_LEAST, 2))) { + return error; + } + // add command to a criteria/command pair that is run against views when they appear. + char *criteria = argv[0], *cmdlist = join_args(argv + 1, argc - 1); + + struct criteria *crit = malloc(sizeof(struct criteria)); + crit->crit_raw = strdup(criteria); + crit->cmdlist = cmdlist; + crit->tokens = create_list(); + char *err_str = extract_crit_tokens(crit->tokens, crit->crit_raw); + + if (err_str) { + error = cmd_results_new(CMD_INVALID, "for_window", err_str); + free(err_str); + free_criteria(crit); + } else if (crit->tokens->length == 0) { + error = cmd_results_new(CMD_INVALID, "for_window", "Found no name/value pairs in criteria"); + free_criteria(crit); + } else if (list_seq_find(config->criteria, criteria_cmp, crit) != -1) { + sway_log(L_DEBUG, "for_window: Duplicate, skipping."); + free_criteria(crit); + } else { + sway_log(L_DEBUG, "for_window: '%s' -> '%s' added", crit->crit_raw, crit->cmdlist); + list_add(config->criteria, crit); + } + return error ? error : cmd_results_new(CMD_SUCCESS, NULL, NULL); +} + static struct cmd_results *cmd_fullscreen(int argc, char **argv) { struct cmd_results *error = NULL; if (config->reading) return cmd_results_new(CMD_FAILURE, "fullscreen", "Can't be used in config file."); @@ -1353,6 +1386,7 @@ static struct cmd_handler handlers[] = { { "floating_modifier", cmd_floating_mod }, { "focus", cmd_focus }, { "focus_follows_mouse", cmd_focus_follows_mouse }, + { "for_window", cmd_for_window }, { "fullscreen", cmd_fullscreen }, { "gaps", cmd_gaps }, { "kill", cmd_kill }, diff --git a/sway/config.c b/sway/config.c index 13865058..fc21b2a9 100644 --- a/sway/config.c +++ b/sway/config.c @@ -10,6 +10,7 @@ #include "config.h" #include "layout.h" #include "input_state.h" +#include "criteria.h" struct sway_config *config = NULL; @@ -66,6 +67,11 @@ static void free_config(struct sway_config *config) { } list_free(config->workspace_outputs); + for (i = 0; i < config->criteria->length; ++i) { + free_criteria(config->criteria->items[i]); + } + list_free(config->criteria); + for (i = 0; i < config->output_configs->length; ++i) { free_output_config(config->output_configs->items[i]); } @@ -82,6 +88,7 @@ static void config_defaults(struct sway_config *config) { config->symbols = create_list(); config->modes = create_list(); config->workspace_outputs = create_list(); + config->criteria = create_list(); config->output_configs = create_list(); config->cmd_queue = create_list(); diff --git a/sway/criteria.c b/sway/criteria.c new file mode 100644 index 00000000..51779590 --- /dev/null +++ b/sway/criteria.c @@ -0,0 +1,349 @@ +#include +#include +#include +#include +#include "criteria.h" +#include "stringop.h" +#include "list.h" +#include "log.h" +#include "container.h" +#include "config.h" + +enum criteria_type { // *must* keep in sync with criteria_strings[] + CRIT_CLASS, + 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", + "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; + regex_t *regex; + char *raw; +}; + +static void free_crit_token(struct crit_token *crit) { + if (crit->regex) { + regfree(crit->regex); + free(crit->regex); + } + if (crit->raw) { + 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_INSTANCE || *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(regex_t **regex, char *value) { + *regex = calloc(1, sizeof(regex_t)); + int err = regcomp(*regex, value, REG_NOSUB); + if (err != 0) { + char *reg_err = malloc(64); + regerror(err, *regex, reg_err, 64); + + 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); + free(reg_err); + return error; + } + return NULL; +} + +// Pouplate 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 || strcmp(value, "focused") == 0) { + 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; +} + +// 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 (strcmp(crit->raw, "focused") == 0) { + swayc_t *focused = get_focused_view(&root_container); + if (focused->class && strcmp(cont->class, focused->class) == 0) { + matches++; + } + } else if (crit->regex && regexec(crit->regex, cont->class, 0, NULL, 0) == 0) { + matches++; + } + break; + case CRIT_ID: + if (!cont->app_id) { + // ignore + } else if (crit->regex && regexec(crit->regex, cont->app_id, 0, NULL, 0) == 0) { + matches++; + } + break; + case CRIT_INSTANCE: + break; + case CRIT_TITLE: + if (!cont->name) { + // ignore + } else if (strcmp(crit->raw, "focused") == 0) { + swayc_t *focused = get_focused_view(&root_container); + if (focused->name && strcmp(cont->name, focused->name) == 0) { + matches++; + } + } else if (crit->regex && regexec(crit->regex, cont->name, 0, NULL, 0) == 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 (strcmp(crit->raw, "focused") == 0) { + swayc_t *focused_ws = swayc_active_workspace(); + if (focused_ws->name && strcmp(cont_ws->name, focused_ws->name) == 0) { + matches++; + } + } else if (crit->regex && regexec(crit->regex, cont_ws->name, 0, NULL, 0) == 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; +} diff --git a/sway/handlers.c b/sway/handlers.c index 28fa9564..267a8f3a 100644 --- a/sway/handlers.c +++ b/sway/handlers.c @@ -19,6 +19,7 @@ #include "input_state.h" #include "resize.h" #include "extensions.h" +#include "criteria.h" // Event should be sent to client #define EVENT_PASSTHROUGH false @@ -172,6 +173,21 @@ static bool handle_view_created(wlc_handle handle) { set_focused_container(newview); swayc_t *output = swayc_parent_by_type(newview, C_OUTPUT); arrange_windows(output, -1, -1); + // check if it matches for_window in config and execute if so + list_t *criteria = criteria_for(newview); + for (int i = 0; i < criteria->length; i++) { + struct criteria *crit = criteria->items[i]; + sway_log(L_DEBUG, "for_window '%s' matches new view %p, cmd: '%s'", + crit->crit_raw, newview, crit->cmdlist); + struct cmd_results *res = handle_command(crit->cmdlist); + if (res->status != CMD_SUCCESS) { + sway_log(L_ERROR, "Command '%s' failed: %s", res->input, res->error); + } + free_cmd_results(res); + // view must be focused for commands to affect it, so always + // refocus in-between command lists + set_focused_container(newview); + } } return true; }