Feature #16: Advanced window matching

- Add advanced window matching system, capable of matching against
  arbitrary window properties as well as a series of internal
  properties, with 4 additional operators (>, <, >=, <=) useful for
  integer targets, and support of logical operators. The old matching
  system is removed, but compatibility with the format is retained.

- As the new matching system is pretty complicated, and I have no past
  experience in writing a parser, it's pretty possible that bugs are
  present. It also has inferior performance, but I hope it doesn't
  matter on modern CPUs.

- It's possible to disable matching system at compile time with NO_C2=1
  now.

- Add ps->o.config_file to track which config file we have actually
  read. Queryable via D-Bus.

- Parse -d in first pass in get_cfg() as c2 needs to query X to get
  atoms during condition parsing.

- Fix a bug in wid_get_prop_adv() that 0 == rformat is not handled
  correctly.

- Fix incompatibility with FreeBSD sed in dbus-examples/cdbus-driver.sh
  .

- Add recipe to generate .clang_complete in Makefile, used by Vim
  clang_complete plugin.

- Add DEBUG_C2 for debugging condition string parsing. DEBUG_WINMATCH is
  still used for match debugging.

- Rename win_on_wdata_change() to win_on_factor_change().

- Extra malloc() failure checks. Add const to matching cache members in
  session_t. Code clean-up. Documentation update.
This commit is contained in:
Richard Grenville
2013-01-28 21:39:38 +08:00
parent 00424b1082
commit 6d36ef2d0f
11 changed files with 1839 additions and 392 deletions

View File

@@ -567,7 +567,7 @@ wid_get_prop_adv(const session_t *ps, Window w, Atom atom, long offset,
if (Success == XGetWindowProperty(ps->dpy, w, atom, offset, length,
False, rtype, &type, &format, &nitems, &after, &data)
&& nitems && (AnyPropertyType == type || type == rtype)
&& (!format || format == rformat)
&& (!rformat || format == rformat)
&& (8 == format || 16 == format || 32 == format)) {
return (winprop_t) {
.data.p8 = data,
@@ -629,244 +629,20 @@ win_rounded_corners(session_t *ps, win *w) {
XFree(rects);
}
/**
* Match a window against a single window condition.
*
* @return true if matched, false otherwise.
*/
static bool
win_match_once(win *w, const wincond_t *cond) {
const char *target;
bool matched = false;
#ifdef DEBUG_WINMATCH
printf("win_match_once(%#010lx \"%s\"): cond = %p", w->id, w->name,
cond);
#endif
if (InputOnly == w->a.class) {
#ifdef DEBUG_WINMATCH
printf(": InputOnly\n");
#endif
return false;
}
// Determine the target
target = NULL;
switch (cond->target) {
case CONDTGT_NAME:
target = w->name;
break;
case CONDTGT_CLASSI:
target = w->class_instance;
break;
case CONDTGT_CLASSG:
target = w->class_general;
break;
case CONDTGT_ROLE:
target = w->role;
break;
}
if (!target) {
#ifdef DEBUG_WINMATCH
printf(": Target not found\n");
#endif
return false;
}
// Determine pattern type and match
switch (cond->type) {
case CONDTP_EXACT:
if (cond->flags & CONDF_IGNORECASE)
matched = !strcasecmp(target, cond->pattern);
else
matched = !strcmp(target, cond->pattern);
break;
case CONDTP_ANYWHERE:
if (cond->flags & CONDF_IGNORECASE)
matched = strcasestr(target, cond->pattern);
else
matched = strstr(target, cond->pattern);
break;
case CONDTP_FROMSTART:
if (cond->flags & CONDF_IGNORECASE)
matched = !strncasecmp(target, cond->pattern,
strlen(cond->pattern));
else
matched = !strncmp(target, cond->pattern,
strlen(cond->pattern));
break;
case CONDTP_WILDCARD:
{
int flags = 0;
if (cond->flags & CONDF_IGNORECASE)
flags = FNM_CASEFOLD;
matched = !fnmatch(cond->pattern, target, flags);
}
break;
case CONDTP_REGEX_PCRE:
#ifdef CONFIG_REGEX_PCRE
matched = (pcre_exec(cond->regex_pcre, cond->regex_pcre_extra,
target, strlen(target), 0, 0, NULL, 0) >= 0);
#endif
break;
}
#ifdef DEBUG_WINMATCH
printf(", matched = %d\n", matched);
#endif
return matched;
}
/**
* Match a window against a condition linked list.
*
* @param cache a place to cache the last matched condition
* @return true if matched, false otherwise.
*/
static bool
win_match(win *w, wincond_t *condlst, wincond_t **cache) {
// Check if the cached entry matches firstly
if (cache && *cache && win_match_once(w, *cache))
return true;
// Then go through the whole linked list
for (; condlst; condlst = condlst->next) {
if (win_match_once(w, condlst)) {
if (cache)
*cache = condlst;
return true;
}
}
return false;
}
/**
* Add a pattern to a condition linked list.
*/
static bool
condlst_add(wincond_t **pcondlst, const char *pattern) {
condlst_add(session_t *ps, c2_lptr_t **pcondlst, const char *pattern) {
if (!pattern)
return false;
unsigned plen = strlen(pattern);
wincond_t *cond;
const char *pos;
if (plen < 4 || ':' != pattern[1] || !strchr(pattern + 2, ':')) {
printf("Pattern \"%s\": Format invalid.\n", pattern);
return false;
}
// Allocate memory for the new condition
cond = malloc(sizeof(wincond_t));
// Determine the pattern target
switch (pattern[0]) {
case 'n':
cond->target = CONDTGT_NAME;
break;
case 'i':
cond->target = CONDTGT_CLASSI;
break;
case 'g':
cond->target = CONDTGT_CLASSG;
break;
case 'r':
cond->target = CONDTGT_ROLE;
break;
default:
printf("Pattern \"%s\": Target \"%c\" invalid.\n",
pattern, pattern[0]);
free(cond);
return false;
}
// Determine the pattern type
switch (pattern[2]) {
case 'e':
cond->type = CONDTP_EXACT;
break;
case 'a':
cond->type = CONDTP_ANYWHERE;
break;
case 's':
cond->type = CONDTP_FROMSTART;
break;
case 'w':
cond->type = CONDTP_WILDCARD;
break;
#ifdef CONFIG_REGEX_PCRE
case 'p':
cond->type = CONDTP_REGEX_PCRE;
break;
#ifdef CONFIG_C2
if (!c2_parse(ps, pcondlst, pattern))
exit(1);
#else
printf_errfq(1, "(): Condition support not compiled in.");
#endif
default:
printf("Pattern \"%s\": Type \"%c\" invalid.\n",
pattern, pattern[2]);
free(cond);
return false;
}
// Determine the pattern flags
pos = &pattern[3];
cond->flags = 0;
while (':' != *pos) {
switch (*pos) {
case 'i':
cond->flags |= CONDF_IGNORECASE;
break;
default:
printf("Pattern \"%s\": Flag \"%c\" invalid.\n",
pattern, *pos);
break;
}
++pos;
}
// Copy the pattern
++pos;
cond->pattern = NULL;
#ifdef CONFIG_REGEX_PCRE
cond->regex_pcre = NULL;
cond->regex_pcre_extra = NULL;
#endif
if (CONDTP_REGEX_PCRE == cond->type) {
#ifdef CONFIG_REGEX_PCRE
const char *error = NULL;
int erroffset = 0;
int options = 0;
if (cond->flags & CONDF_IGNORECASE)
options |= PCRE_CASELESS;
cond->regex_pcre = pcre_compile(pos, options, &error, &erroffset,
NULL);
if (!cond->regex_pcre) {
printf("Pattern \"%s\": PCRE regular expression parsing failed on "
"offset %d: %s\n", pattern, erroffset, error);
free(cond);
return false;
}
#ifdef CONFIG_REGEX_PCRE_JIT
cond->regex_pcre_extra = pcre_study(cond->regex_pcre, PCRE_STUDY_JIT_COMPILE, &error);
if (!cond->regex_pcre_extra) {
printf("Pattern \"%s\": PCRE regular expression study failed: %s",
pattern, error);
}
#endif
#endif
}
else {
cond->pattern = mstrcpy(pos);
}
// Insert it into the linked list
cond->next = *pcondlst;
*pcondlst = cond;
return true;
}
@@ -2313,7 +2089,7 @@ win_determine_shadow(session_t *ps, win *w) {
w->shadow = (UNSET == w->shadow_force ?
(ps->o.wintype_shadow[w->window_type]
&& !win_match(w, ps->o.shadow_blacklist, &w->cache_sblst)
&& !win_match(ps, w, ps->o.shadow_blacklist, &w->cache_sblst)
&& !(ps->o.shadow_ignore_shaped && w->bounding_shaped
&& !w->rounded_corners)
&& !(ps->o.respect_prop_shadow && 0 == w->prop_shadow))
@@ -2347,7 +2123,7 @@ win_determine_invert_color(session_t *ps, win *w) {
if (UNSET != w->invert_color_force)
w->invert_color = w->invert_color_force;
else
w->invert_color = win_match(w, ps->o.invert_color_list, &w->cache_ivclst);
w->invert_color = win_match(ps, w, ps->o.invert_color_list, &w->cache_ivclst);
if (w->invert_color != invert_color_old)
add_damage_win(ps, w);
@@ -2361,13 +2137,15 @@ win_on_wtype_change(session_t *ps, win *w) {
win_determine_shadow(ps, w);
win_determine_fade(ps, w);
win_update_focused(ps, w);
if (ps->o.invert_color_list)
win_determine_invert_color(ps, w);
}
/**
* Function to be called on window data changes.
*/
static void
win_on_wdata_change(session_t *ps, win *w) {
win_on_factor_change(session_t *ps, win *w) {
if (ps->o.shadow_blacklist)
win_determine_shadow(ps, w);
if (ps->o.fade_blacklist)
@@ -2481,9 +2259,11 @@ win_mark_client(session_t *ps, win *w, Window client) {
win_get_name(ps, w);
win_get_class(ps, w);
win_get_role(ps, w);
win_on_wdata_change(ps, w);
}
// Update everything related to conditions
win_on_factor_change(ps, w);
// Update window focus state
win_update_focused(ps, w);
}
@@ -3048,7 +2828,7 @@ win_update_focused(session_t *ps, win *w) {
|| (ps->o.mark_wmwin_focused && w->wmwin)
|| (ps->o.mark_ovredir_focused
&& w->id == w->client_win && !w->wmwin)
|| win_match(w, ps->o.focus_blacklist, &w->cache_fcblst))
|| win_match(ps, w, ps->o.focus_blacklist, &w->cache_fcblst))
w->focused = true;
// If window grouping detection is enabled, mark the window active if
@@ -3102,6 +2882,9 @@ win_set_focused(session_t *ps, win *w, bool focused) {
else {
win_update_focused(ps, w);
}
// Update everything related to conditions
win_on_factor_change(ps, w);
}
}
/**
@@ -3153,6 +2936,9 @@ win_set_leader(session_t *ps, win *w, Window nleader) {
else {
win_update_focused(ps, w);
}
// Update everything related to conditions
win_on_factor_change(ps, w);
}
}
@@ -3746,7 +3532,7 @@ ev_property_notify(session_t *ps, XPropertyEvent *ev) {
&& (ps->atom_name == ev->atom || ps->atom_name_ewmh == ev->atom)) {
win *w = find_toplevel(ps, ev->window);
if (w && 1 == win_get_name(ps, w)) {
win_on_wdata_change(ps, w);
win_on_factor_change(ps, w);
}
}
@@ -3755,7 +3541,7 @@ ev_property_notify(session_t *ps, XPropertyEvent *ev) {
win *w = find_toplevel(ps, ev->window);
if (w) {
win_get_class(ps, w);
win_on_wdata_change(ps, w);
win_on_factor_change(ps, w);
}
}
@@ -3763,7 +3549,7 @@ ev_property_notify(session_t *ps, XPropertyEvent *ev) {
if (ps->o.track_wdata && ps->atom_role == ev->atom) {
win *w = find_toplevel(ps, ev->window);
if (w && 1 == win_get_role(ps, w)) {
win_on_wdata_change(ps, w);
win_on_factor_change(ps, w);
}
}
@@ -3790,7 +3576,7 @@ ev_property_notify(session_t *ps, XPropertyEvent *ev) {
if (!w)
w = find_toplevel(ps, ev->window);
if (w)
win_on_wdata_change(ps, w);
win_on_factor_change(ps, w);
break;
}
}
@@ -4095,24 +3881,7 @@ usage(void) {
" inverted color. Resource-hogging, and is not well tested.\n"
"--dbus\n"
" Enable remote control via D-Bus. See the D-BUS API section in the\n"
" man page for more details.\n"
"\n"
"Format of a condition:\n"
"\n"
" condition = <target>:<type>[<flags>]:<pattern>\n"
"\n"
" <target> is one of \"n\" (window name), \"i\" (window class\n"
" instance), \"g\" (window general class), and \"r\"\n"
" (window role).\n"
"\n"
" <type> is one of \"e\" (exact match), \"a\" (match anywhere),\n"
" \"s\" (match from start), \"w\" (wildcard), and \"p\" (PCRE\n"
" regular expressions, if compiled with the support).\n"
"\n"
" <flags> could be a series of flags. Currently the only defined\n"
" flag is \"i\" (ignore case).\n"
"\n"
" <pattern> is the actual pattern string.\n";
" man page for more details.\n";
fputs(usage_text , stderr);
exit(1);
@@ -4270,8 +4039,6 @@ open_config_file(char *cpath, char **ppath) {
f = fopen(path, "r");
if (f && ppath)
*ppath = path;
else
free(path);
return f;
}
@@ -4356,7 +4123,7 @@ parse_vsync(session_t *ps, const char *optarg) {
* Parse a condition list in configuration file.
*/
static void
parse_cfg_condlst(const config_t *pcfg, wincond_t **pcondlst,
parse_cfg_condlst(session_t *ps, const config_t *pcfg, c2_lptr_t **pcondlst,
const char *name) {
config_setting_t *setting = config_lookup(pcfg, name);
if (setting) {
@@ -4364,12 +4131,12 @@ parse_cfg_condlst(const config_t *pcfg, wincond_t **pcondlst,
if (config_setting_is_array(setting)) {
int i = config_setting_length(setting);
while (i--) {
condlst_add(pcondlst, config_setting_get_string_elem(setting, i));
condlst_add(ps, pcondlst, config_setting_get_string_elem(setting, i));
}
}
// Treat it as a single pattern if it's a string
else if (CONFIG_TYPE_STRING == config_setting_type(setting)) {
condlst_add(pcondlst, config_setting_get_string(setting));
condlst_add(ps, pcondlst, config_setting_get_string(setting));
}
}
}
@@ -4378,7 +4145,7 @@ parse_cfg_condlst(const config_t *pcfg, wincond_t **pcondlst,
* Parse a configuration file from default location.
*/
static void
parse_config(session_t *ps, char *cpath, struct options_tmp *pcfgtmp) {
parse_config(session_t *ps, struct options_tmp *pcfgtmp) {
char *path = NULL;
FILE *f;
config_t cfg;
@@ -4386,10 +4153,14 @@ parse_config(session_t *ps, char *cpath, struct options_tmp *pcfgtmp) {
double dval = 0.0;
const char *sval = NULL;
f = open_config_file(cpath, &path);
f = open_config_file(ps->o.config_file, &path);
if (!f) {
if (cpath)
printf_errfq(1, "(): Failed to read the specified configuration file.");
if (ps->o.config_file) {
printf_errfq(1, "(): Failed to read configuration file \"%s\".",
ps->o.config_file);
free(ps->o.config_file);
ps->o.config_file = NULL;
}
return;
}
@@ -4417,7 +4188,10 @@ parse_config(session_t *ps, char *cpath, struct options_tmp *pcfgtmp) {
}
config_set_auto_convert(&cfg, 1);
free(path);
if (path != ps->o.config_file) {
free(ps->o.config_file);
ps->o.config_file = path;
}
// Get options from the configuration file. We don't do range checking
// right now. It will be done later
@@ -4512,11 +4286,11 @@ parse_config(session_t *ps, char *cpath, struct options_tmp *pcfgtmp) {
lcfg_lookup_bool(&cfg, "detect-client-leader",
&ps->o.detect_client_leader);
// --shadow-exclude
parse_cfg_condlst(&cfg, &ps->o.shadow_blacklist, "shadow-exclude");
parse_cfg_condlst(ps, &cfg, &ps->o.shadow_blacklist, "shadow-exclude");
// --focus-exclude
parse_cfg_condlst(&cfg, &ps->o.focus_blacklist, "focus-exclude");
parse_cfg_condlst(ps, &cfg, &ps->o.focus_blacklist, "focus-exclude");
// --invert-color-include
parse_cfg_condlst(&cfg, &ps->o.invert_color_list, "invert-color-include");
parse_cfg_condlst(ps, &cfg, &ps->o.invert_color_list, "invert-color-include");
// --blur-background
lcfg_lookup_bool(&cfg, "blur-background", &ps->o.blur_background);
// --blur-background-frame
@@ -4554,7 +4328,7 @@ parse_config(session_t *ps, char *cpath, struct options_tmp *pcfgtmp) {
* Process arguments and configuration files.
*/
static void
get_cfg(session_t *ps, int argc, char *const *argv) {
get_cfg(session_t *ps, int argc, char *const *argv, bool first_pass) {
const static char *shortopts = "D:I:O:d:r:o:m:l:t:i:e:hscnfFCaSzGb";
const static struct option longopts[] = {
{ "help", no_argument, NULL, 'h' },
@@ -4595,14 +4369,33 @@ get_cfg(session_t *ps, int argc, char *const *argv) {
{ NULL, 0, NULL, 0 },
};
int o = 0, longopt_idx = -1, i = 0;
if (first_pass) {
// Pre-parse the commandline arguments to check for --config and invalid
// switches
// Must reset optind to 0 here in case we reread the commandline
// arguments
optind = 1;
while (-1 !=
(o = getopt_long(argc, argv, shortopts, longopts, &longopt_idx))) {
if (256 == o)
ps->o.config_file = mstrcpy(optarg);
else if ('d' == o)
ps->o.display = mstrcpy(optarg);
else if ('?' == o || ':' == o)
usage();
}
return;
}
struct options_tmp cfgtmp = {
.no_dock_shadow = false,
.no_dnd_shadow = false,
.menu_opacity = 1.0,
};
bool shadow_enable = false, fading_enable = false;
int o, longopt_idx, i;
char *config_file = NULL;
char *lc_numeric_old = mstrcpy(setlocale(LC_NUMERIC, NULL));
for (i = 0; i < NUM_WINTYPES; ++i) {
@@ -4611,21 +4404,8 @@ get_cfg(session_t *ps, int argc, char *const *argv) {
ps->o.wintype_opacity[i] = 1.0;
}
// Pre-parse the commandline arguments to check for --config and invalid
// switches
// Must reset optind to 0 here in case we reread the commandline
// arguments
optind = 1;
while (-1 !=
(o = getopt_long(argc, argv, shortopts, longopts, &longopt_idx))) {
if (256 == o)
config_file = mstrcpy(optarg);
else if ('?' == o || ':' == o)
usage();
}
#ifdef CONFIG_LIBCONFIG
parse_config(ps, config_file, &cfgtmp);
parse_config(ps, &cfgtmp);
#endif
// Parse commandline arguments. Range checking will be done later.
@@ -4643,7 +4423,6 @@ get_cfg(session_t *ps, int argc, char *const *argv) {
usage();
break;
case 'd':
ps->o.display = mstrcpy(optarg);
break;
case 'D':
ps->o.fade_delta = atoi(optarg);
@@ -4732,7 +4511,7 @@ get_cfg(session_t *ps, int argc, char *const *argv) {
break;
case 263:
// --shadow-exclude
condlst_add(&ps->o.shadow_blacklist, optarg);
condlst_add(ps, &ps->o.shadow_blacklist, optarg);
break;
case 264:
// --mark-ovredir-focused
@@ -4796,7 +4575,7 @@ get_cfg(session_t *ps, int argc, char *const *argv) {
break;
case 279:
// --focus-exclude
condlst_add(&ps->o.focus_blacklist, optarg);
condlst_add(ps, &ps->o.focus_blacklist, optarg);
break;
case 280:
// --inactive-dim-fixed
@@ -4832,7 +4611,7 @@ get_cfg(session_t *ps, int argc, char *const *argv) {
break;
case 288:
// --invert-color-include
condlst_add(&ps->o.invert_color_list, optarg);
condlst_add(ps, &ps->o.invert_color_list, optarg);
break;
default:
usage();
@@ -4884,11 +4663,6 @@ get_cfg(session_t *ps, int argc, char *const *argv) {
ps->o.track_focus = true;
}
// Determine whether we need to track window name and class
if (ps->o.shadow_blacklist || ps->o.fade_blacklist
|| ps->o.focus_blacklist || ps->o.invert_color_list)
ps->o.track_wdata = true;
// Determine whether we track window grouping
if (ps->o.detect_transient || ps->o.detect_client_leader) {
ps->o.track_leader = true;
@@ -5702,7 +5476,8 @@ session_init(session_t *ps_old, int argc, char **argv) {
ps->o.wintype_focus[WINTYPE_NORMAL] = false;
ps->o.wintype_focus[WINTYPE_UTILITY] = false;
get_cfg(ps, argc, argv);
// First pass
get_cfg(ps, argc, argv, true);
// Inherit old Display if possible, primarily for resource leak checking
if (ps_old && ps_old->dpy)
@@ -5712,11 +5487,13 @@ session_init(session_t *ps_old, int argc, char **argv) {
if (!ps->dpy) {
ps->dpy = XOpenDisplay(ps->o.display);
if (!ps->dpy) {
fprintf(stderr, "Can't open display\n");
exit(1);
printf_errfq(1, "(): Can't open display.");
}
}
// Second pass
get_cfg(ps, argc, argv, false);
XSetErrorHandler(error);
if (ps->o.synchronize) {
XSynchronize(ps->dpy, 1);
@@ -5973,11 +5750,24 @@ session_destroy(session_t *ps) {
ps->alpha_picts = NULL;
}
#ifdef CONFIG_C2
// Free blacklists
free_wincondlst(&ps->o.shadow_blacklist);
free_wincondlst(&ps->o.fade_blacklist);
free_wincondlst(&ps->o.focus_blacklist);
free_wincondlst(&ps->o.invert_color_list);
#endif
// Free tracked atom list
{
latom_t *next = NULL;
for (latom_t *this = ps->track_atom_lst; this; this = next) {
next = this->next;
free(this);
}
ps->track_atom_lst = NULL;
}
// Free ignore linked list
{
@@ -6025,6 +5815,7 @@ session_destroy(session_t *ps) {
free(ps->gaussian_map);
free(ps->o.display);
free(ps->o.logpath);
free(ps->o.config_file);
free(ps->pfds_read);
free(ps->pfds_write);
free(ps->pfds_except);
@@ -6164,11 +5955,6 @@ main(int argc, char **argv) {
printf_errf("Failed to create new session.");
return 1;
}
#ifdef DEBUG_C2
// c2_parse(ps_g, NULL, "name ~= \"master\"");
// c2_parse(ps_g, NULL, "n:e:Notification");
c2_parse(ps_g, NULL, "(WM_NAME:16s = 'Notification' || class_g = 'fox') && class_g = 'fox'");
#endif
session_run(ps_g);
ps_old = ps_g;
session_destroy(ps_g);