aboutsummaryrefslogtreecommitdiff
path: root/sway
diff options
context:
space:
mode:
authorS. Christoffer Eliesen <christoffer@eliesen.no>2015-11-17 19:27:01 +0100
committerS. Christoffer Eliesen <christoffer@eliesen.no>2015-11-25 14:34:33 +0100
commita06cb7cd01acfbb5e31dd1aacbbde7887a0509b9 (patch)
tree1474dbed01d4c31318883a3a837e02ce30125bef /sway
parent402c9f4bf0fab1228b757990b292eb7b83690dd6 (diff)
downloadsway-a06cb7cd01acfbb5e31dd1aacbbde7887a0509b9.tar.xz
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.)
Diffstat (limited to 'sway')
-rw-r--r--sway/commands.c34
-rw-r--r--sway/config.c7
-rw-r--r--sway/criteria.c349
-rw-r--r--sway/handlers.c16
4 files changed, 406 insertions, 0 deletions
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 <stdlib.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <regex.h>
+#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;
}