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.)
This commit is contained in:
S. Christoffer Eliesen 2015-11-17 19:27:01 +01:00
parent 402c9f4bf0
commit a06cb7cd01
7 changed files with 478 additions and 0 deletions

View file

@ -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;

36
include/criteria.h Normal file
View file

@ -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 <criteria> <cmd list>`.
*/
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

View file

@ -68,6 +68,10 @@ Commands
**fullscreen**::
Toggles fullscreen status for the focused view.
**for_window** <criteria> <command>::
Whenever a window that matches _criteria_ appears, run list of commands. See
**Criteria** section below.
**gaps** edge_gaps <on|off|toggle>::
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** <name> output <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.

View file

@ -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 },

View file

@ -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();

349
sway/criteria.c Normal file
View file

@ -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;
}

View file

@ -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;
}