From 5ae4620a2484a390dd63642ba66fbb4ca09cbd21 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 15 May 2011 20:10:25 +0200 Subject: [PATCH] Time Lord technology: for_window config directive to run arbitrary cmds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An example to set all XTerms floating: for_window [class="XTerm"] mode floating To make all urxvts use a 1-pixel border: for_window [class="urxvt"] border 1pixel A less useful, but rather funny example: for_window [title="x200: ~/work"] mode floating The commands are not completely arbitrary. The commands above were tested, others may need some fixing. Internally, windows are compared against your criteria (class, title, …) when they are initially managed and whenever one of the relevant values change. Then, the specified command is run *once* (per window). It gets prefixed with a criteria to make it match only the specific window that triggered it. So, if you configure "mode floating", i3 runs something like '[id="8393923"] mode floating'. --- Makefile | 2 +- include/all.h | 1 + include/assignments.h | 15 +++++++ include/data.h | 35 ++++++++++++--- include/i3.h | 1 + include/window.h | 6 +-- src/assignments.c | 50 ++++++++++++++++++++++ src/cfgparse.l | 32 ++++++++++++++ src/cfgparse.y | 99 +++++++++++++++++++++++++++++++++++++++++++ src/cmdparse.y | 38 +++++++++++------ src/handlers.c | 6 +-- src/main.c | 2 + src/manage.c | 9 ++-- src/match.c | 6 +++ src/window.c | 23 ++++++++-- 15 files changed, 291 insertions(+), 34 deletions(-) create mode 100644 include/assignments.h create mode 100644 src/assignments.c diff --git a/Makefile b/Makefile index c9359142..6e032f41 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ include $(TOPDIR)/common.mk # Depend on the object files of all source-files in src/*.c and on all header files AUTOGENERATED:=src/cfgparse.tab.c src/cfgparse.yy.c src/cmdparse.tab.c src/cmdparse.yy.c -FILES:=src/ipc.c src/main.c src/log.c src/util.c src/tree.c src/xcb.c src/manage.c src/workspace.c src/x.c src/floating.c src/click.c src/config.c src/handlers.c src/randr.c src/xinerama.c src/con.c src/load_layout.c src/render.c src/window.c src/match.c src/xcursor.c src/resize.c src/sighandler.c src/move.c src/output.c src/ewmh.c +FILES:=src/ipc.c src/main.c src/log.c src/util.c src/tree.c src/xcb.c src/manage.c src/workspace.c src/x.c src/floating.c src/click.c src/config.c src/handlers.c src/randr.c src/xinerama.c src/con.c src/load_layout.c src/render.c src/window.c src/match.c src/xcursor.c src/resize.c src/sighandler.c src/move.c src/output.c src/ewmh.c src/assignments.c FILES:=$(FILES:.c=.o) HEADERS:=$(filter-out include/loglevels.h,$(wildcard include/*.h)) diff --git a/include/all.h b/include/all.h index f9f12a70..ba582a80 100644 --- a/include/all.h +++ b/include/all.h @@ -62,5 +62,6 @@ #include "move.h" #include "output.h" #include "ewmh.h" +#include "assignments.h" #endif diff --git a/include/assignments.h b/include/assignments.h new file mode 100644 index 00000000..ecd8b808 --- /dev/null +++ b/include/assignments.h @@ -0,0 +1,15 @@ +/* + * vim:ts=4:sw=4:expandtab + * + */ +#ifndef _ASSIGNMENTS_H +#define _ASSIGNMENTS_H + +/** + * Checks the list of assignments for the given window and runs all matching + * ones (unless they have already been run for this specific window). + * + */ +void run_assignments(i3Window *window); + +#endif diff --git a/include/data.h b/include/data.h index c408d3f6..832200ee 100644 --- a/include/data.h +++ b/include/data.h @@ -33,6 +33,7 @@ typedef struct Rect Rect; typedef struct xoutput Output; typedef struct Con Con; typedef struct Match Match; +typedef struct Assignment Assignment; typedef struct Window i3Window; @@ -274,11 +275,14 @@ struct Window { /** Pixels the window reserves. left/right/top/bottom */ struct reservedpx reserved; + + /** Pointers to the Assignments which were already ran for this Window + * (assignments run only once) */ + uint32_t nr_assignments; + Assignment **ran_assignments; }; struct Match { - enum { M_WINDOW, M_CON } what; - char *title; int title_len; char *application; @@ -296,10 +300,6 @@ struct Match { Con *con_id; enum { M_ANY = 0, M_TILING, M_FLOATING } floating; - enum { M_GLOBAL = 0, M_OUTPUT, M_WORKSPACE } levels; - - enum { M_USER = 0, M_RESTART } source; - char *target_ws; /* Where the window looking for a match should be inserted: @@ -317,6 +317,29 @@ struct Match { TAILQ_ENTRY(Match) assignments; }; +struct Assignment { + /** type of this assignment: + * + * A_COMMAND = run the specified command for the matching window + * A_TO_WORKSPACE = assign the matching window to the specified workspace + * A_TO_OUTPUT = assign the matching window to the specified output + * + */ + enum { A_COMMAND = 0, A_TO_WORKSPACE = 1, A_TO_OUTPUT = 2 } type; + + /** the criteria to check if a window matches */ + Match match; + + /** destination workspace/output/command, depending on the type */ + union { + char *command; + char *workspace; + char *output; + } dest; + + TAILQ_ENTRY(Assignment) real_assignments; +}; + struct Con { bool mapped; enum { diff --git a/include/i3.h b/include/i3.h index a18cd6bc..6aeea847 100644 --- a/include/i3.h +++ b/include/i3.h @@ -28,6 +28,7 @@ extern TAILQ_HEAD(bindings_head, Binding) *bindings; extern TAILQ_HEAD(autostarts_head, Autostart) autostarts; extern TAILQ_HEAD(assignments_head, Match) assignments; extern TAILQ_HEAD(ws_assignments_head, Workspace_Assignment) ws_assignments; +extern TAILQ_HEAD(real_assignments_head, Assignment) real_assignments; extern SLIST_HEAD(stack_wins_head, Stack_Window) stack_wins; extern uint8_t root_depth; extern bool xcursor_supported, xkb_supported; diff --git a/include/window.h b/include/window.h index 1c48c012..fe282aa0 100644 --- a/include/window.h +++ b/include/window.h @@ -6,14 +6,14 @@ * given window. * */ -void window_update_class(i3Window *win, xcb_get_property_reply_t *prop); +void window_update_class(i3Window *win, xcb_get_property_reply_t *prop, bool before_mgmt); /** * Updates the name by using _NET_WM_NAME (encoded in UTF-8) for the given * window. Further updates using window_update_name_legacy will be ignored. * */ -void window_update_name(i3Window *win, xcb_get_property_reply_t *prop); +void window_update_name(i3Window *win, xcb_get_property_reply_t *prop, bool before_mgmt); /** * Updates the name by using WM_NAME (encoded in COMPOUND_TEXT). We do not @@ -22,7 +22,7 @@ void window_update_name(i3Window *win, xcb_get_property_reply_t *prop); * window_update_name()). * */ -void window_update_name_legacy(i3Window *win, xcb_get_property_reply_t *prop); +void window_update_name_legacy(i3Window *win, xcb_get_property_reply_t *prop, bool before_mgmt); /** * Updates the CLIENT_LEADER (logical parent window). diff --git a/src/assignments.c b/src/assignments.c new file mode 100644 index 00000000..f41877f0 --- /dev/null +++ b/src/assignments.c @@ -0,0 +1,50 @@ +/* + * vim:ts=4:sw=4:expandtab + * + * i3 - an improved dynamic tiling window manager + * © 2009-2011 Michael Stapelberg and contributors (see also: LICENSE) + * + */ +#include "all.h" + +/* + * Checks the list of assignments for the given window and runs all matching + * ones (unless they have already been run for this specific window). + * + */ +void run_assignments(i3Window *window) { + DLOG("Checking assignments...\n"); + + /* Check if any assignments match */ + Assignment *current; + TAILQ_FOREACH(current, &real_assignments, real_assignments) { + if (!match_matches_window(&(current->match), window)) + continue; + + bool skip = false; + for (int c = 0; c < window->nr_assignments; c++) { + if (window->ran_assignments[c] != current) + continue; + + DLOG("This assignment already ran for the given window, not executing it again.\n"); + skip = true; + break; + } + + if (skip) + continue; + + DLOG("matching assignment, would do:\n"); + if (current->type == A_COMMAND) { + DLOG("execute command %s\n", current->dest.command); + char *full_command; + asprintf(&full_command, "[id=\"%d\"] %s", window->id, current->dest.command); + parse_cmd(full_command); + } + + /* Store that we ran this assignment to not execute it again */ + window->nr_assignments++; + window->ran_assignments = srealloc(window->ran_assignments, sizeof(Assignment*) * window->nr_assignments); + window->ran_assignments[window->nr_assignments-1] = current; + } +} diff --git a/src/cfgparse.l b/src/cfgparse.l index 1615288e..09cf9c44 100644 --- a/src/cfgparse.l +++ b/src/cfgparse.l @@ -43,6 +43,9 @@ EOL (\r?\n) %s COLOR_COND %s OUTPUT_COND %s OUTPUT_AWS_COND +%s WANT_QSTRING +%s FOR_WINDOW_COND +%s REQUIRE_WS %x BUFFER_LINE %% @@ -68,6 +71,16 @@ EOL (\r?\n) } +"]" { yy_pop_state(); return ']'; } +[ \t]* { yy_pop_state(); return WHITESPACE; } +\"[^\"]+\" { + yy_pop_state(); + /* strip quotes */ + char *copy = sstrdup(yytext+1); + copy[strlen(copy)-1] = '\0'; + yylval.string = copy; + return STR; + } [^\n]+ { BEGIN(INITIAL); yylval.string = strdup(yytext); return STR; } [a-zA-Z0-9_-]+ { yylval.string = strdup(yytext); return OUTPUT; } ^[ \t]*#[^\n]* { return TOKCOMMENT; } @@ -108,6 +121,18 @@ workspace_bar { return TOKWORKSPACEBAR; } popup_during_fullscreen { return TOK_POPUP_DURING_FULLSCREEN; } ignore { return TOK_IGNORE; } leave_fullscreen { return TOK_LEAVE_FULLSCREEN; } +for_window { + /* Example: for_window [class="urxvt"] border none + * + * First, we wait for the ']' that finishes a match (FOR_WINDOW_COND) + * Then, we require a whitespace (REQUIRE_WS) + * And the rest of the line is parsed as a string + */ + yy_push_state(BIND_A2WS_COND); + yy_push_state(REQUIRE_WS); + yy_push_state(FOR_WINDOW_COND); + return TOK_FOR_WINDOW; + } default { /* yylval.number = MODE_DEFAULT; */return TOK_DEFAULT; } stacking { /* yylval.number = MODE_STACK; */return TOK_STACKING; } stacked { return TOK_STACKING; } @@ -134,6 +159,13 @@ control { return TOKCONTROL; } ctrl { return TOKCONTROL; } shift { return TOKSHIFT; } → { return TOKARROW; } + +class { yy_push_state(WANT_QSTRING); return TOK_CLASS; } +id { yy_push_state(WANT_QSTRING); return TOK_ID; } +con_id { yy_push_state(WANT_QSTRING); return TOK_CON_ID; } +con_mark { yy_push_state(WANT_QSTRING); return TOK_MARK; } +title { yy_push_state(WANT_QSTRING); return TOK_TITLE; } + {EOL} { FREE(context->line_copy); context->line_number++; diff --git a/src/cfgparse.y b/src/cfgparse.y index a5e0f0ba..00190dbc 100644 --- a/src/cfgparse.y +++ b/src/cfgparse.y @@ -7,9 +7,12 @@ #include #include #include +#include #include "all.h" +static Match current_match; + typedef struct yy_buffer_state *YY_BUFFER_STATE; extern int yylex(struct context *context); extern int yyparse(void); @@ -242,6 +245,13 @@ void parse_file(const char *f) { %token TOK_POPUP_DURING_FULLSCREEN "popup_during_fullscreen" %token TOK_IGNORE "ignore" %token TOK_LEAVE_FULLSCREEN "leave_fullscreen" +%token TOK_FOR_WINDOW "for_window" + +%token TOK_MARK "mark" +%token TOK_CLASS "class" +%token TOK_ID "id" +%token TOK_CON_ID "con_id" +%token TOK_TITLE "title" %type binding %type bindcode @@ -272,6 +282,7 @@ lines: /* empty */ line: bindline + | for_window | mode | floating_modifier | orientation @@ -292,6 +303,10 @@ line: | popup_during_fullscreen ; +optwhitespace: + | WHITESPACE + ; + comment: TOKCOMMENT ; @@ -340,6 +355,90 @@ bindsym: } ; +for_window: + TOK_FOR_WINDOW WHITESPACE match WHITESPACE command + { + printf("\t should execute command %s for the criteria mentioned above\n", $5); + Assignment *assignment = scalloc(sizeof(Assignment)); + assignment->type = A_COMMAND; + assignment->match = current_match; + assignment->dest.command = $5; + TAILQ_INSERT_TAIL(&real_assignments, assignment, real_assignments); + } + ; + +match: + | matchstart optwhitespace criteria optwhitespace matchend + { + printf("match parsed\n"); + } + ; + +matchstart: + '[' + { + printf("start\n"); + match_init(¤t_match); + } + ; + +matchend: + ']' + { + printf("match specification finished\n"); + } + ; + +criteria: + TOK_CLASS '=' STR + { + printf("criteria: class = %s\n", $3); + current_match.class = $3; + } + | TOK_CON_ID '=' STR + { + printf("criteria: id = %s\n", $3); + char *end; + long parsed = strtol($3, &end, 10); + if (parsed == LONG_MIN || + parsed == LONG_MAX || + parsed < 0 || + (end && *end != '\0')) { + ELOG("Could not parse con id \"%s\"\n", $3); + } else { + current_match.con_id = (Con*)parsed; + printf("id as int = %p\n", current_match.con_id); + } + } + | TOK_ID '=' STR + { + printf("criteria: window id = %s\n", $3); + char *end; + long parsed = strtol($3, &end, 10); + if (parsed == LONG_MIN || + parsed == LONG_MAX || + parsed < 0 || + (end && *end != '\0')) { + ELOG("Could not parse window id \"%s\"\n", $3); + } else { + current_match.id = parsed; + printf("window id as int = %d\n", current_match.id); + } + } + | TOK_MARK '=' STR + { + printf("criteria: mark = %s\n", $3); + current_match.mark = $3; + } + | TOK_TITLE '=' STR + { + printf("criteria: title = %s\n", $3); + current_match.title = $3; + } + ; + + + word_or_number: WORD | NUMBER diff --git a/src/cmdparse.y b/src/cmdparse.y index eb41c820..bc4858f2 100644 --- a/src/cmdparse.y +++ b/src/cmdparse.y @@ -3,7 +3,7 @@ * vim:ts=4:sw=4:expandtab * * i3 - an improved dynamic tiling window manager - * © 2009-2010 Michael Stapelberg and contributors (see also: LICENSE) + * © 2009-2011 Michael Stapelberg and contributors (see also: LICENSE) * * cmdparse.y: the parser for commands you send to i3 (or bind on keys) * @@ -80,6 +80,7 @@ int cmdyywrap() { } char *parse_cmd(const char *new) { + LOG("COMMAND: *%s*\n", new); cmdyy_scan_string(new); match_init(¤t_match); @@ -512,15 +513,21 @@ direction: mode: TOK_MODE WHITESPACE window_mode { - if ($3 == TOK_TOGGLE) { - printf("should toggle mode\n"); - toggle_floating_mode(focused, false); - } else { - printf("should switch mode to %s\n", ($3 == TOK_FLOATING ? "floating" : "tiling")); - if ($3 == TOK_FLOATING) { - floating_enable(focused, false); + HANDLE_EMPTY_MATCH; + + owindow *current; + TAILQ_FOREACH(current, &owindows, owindows) { + printf("matching: %p / %s\n", current->con, current->con->name); + if ($3 == TOK_TOGGLE) { + printf("should toggle mode\n"); + toggle_floating_mode(current->con, false); } else { - floating_disable(focused, false); + printf("should switch mode to %s\n", ($3 == TOK_FLOATING ? "floating" : "tiling")); + if ($3 == TOK_FLOATING) { + floating_enable(current->con, false); + } else { + floating_disable(current->con, false); + } } } } @@ -608,11 +615,14 @@ layout: printf("changing layout to %d\n", $3); owindow *current; - HANDLE_EMPTY_MATCH; - - TAILQ_FOREACH(current, &owindows, owindows) { - printf("matching: %p / %s\n", current->con, current->con->name); - con_set_layout(current->con, $3); + /* check if the match is empty, not if the result is empty */ + if (match_is_empty(¤t_match)) + con_set_layout(focused->parent, $3); + else { + TAILQ_FOREACH(current, &owindows, owindows) { + printf("matching: %p / %s\n", current->con, current->con->name); + con_set_layout(current->con, $3); + } } } ; diff --git a/src/handlers.c b/src/handlers.c index f1e1c4b0..b87bb418 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -541,7 +541,7 @@ static int handle_windowname_change(void *data, xcb_connection_t *conn, uint8_t if ((con = con_by_window_id(window)) == NULL || con->window == NULL) return 1; - window_update_name(con->window, prop); + window_update_name(con->window, prop, false); x_push_changes(croot); @@ -559,7 +559,7 @@ static int handle_windowname_change_legacy(void *data, xcb_connection_t *conn, u if ((con = con_by_window_id(window)) == NULL || con->window == NULL) return 1; - window_update_name_legacy(con->window, prop); + window_update_name_legacy(con->window, prop, false); x_push_changes(croot); @@ -576,7 +576,7 @@ static int handle_windowclass_change(void *data, xcb_connection_t *conn, uint8_t if ((con = con_by_window_id(window)) == NULL || con->window == NULL) return 1; - window_update_class(con->window, prop); + window_update_class(con->window, prop, false); return 0; } diff --git a/src/main.c b/src/main.c index c079d8dc..39702223 100644 --- a/src/main.c +++ b/src/main.c @@ -37,6 +37,8 @@ struct assignments_head assignments = TAILQ_HEAD_INITIALIZER(assignments); * output) */ struct ws_assignments_head ws_assignments = TAILQ_HEAD_INITIALIZER(ws_assignments); +struct real_assignments_head real_assignments = TAILQ_HEAD_INITIALIZER(real_assignments); + /* We hope that those are supported and set them to true */ bool xcursor_supported = true; bool xkb_supported = true; diff --git a/src/manage.c b/src/manage.c index b511189c..7d240f42 100644 --- a/src/manage.c +++ b/src/manage.c @@ -158,9 +158,9 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki /* update as much information as possible so far (some replies may be NULL) */ - window_update_class(cwindow, xcb_get_property_reply(conn, class_cookie, NULL)); - window_update_name_legacy(cwindow, xcb_get_property_reply(conn, title_cookie, NULL)); - window_update_name(cwindow, xcb_get_property_reply(conn, utf8_title_cookie, NULL)); + window_update_class(cwindow, xcb_get_property_reply(conn, class_cookie, NULL), true); + window_update_name_legacy(cwindow, xcb_get_property_reply(conn, title_cookie, NULL), true); + window_update_name(cwindow, xcb_get_property_reply(conn, utf8_title_cookie, NULL), true); window_update_leader(cwindow, xcb_get_property_reply(conn, leader_cookie, NULL)); window_update_transient_for(cwindow, xcb_get_property_reply(conn, transient_cookie, NULL)); window_update_strut_partial(cwindow, xcb_get_property_reply(conn, strut_cookie, NULL)); @@ -330,6 +330,9 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki * cleanup) */ xcb_change_save_set(conn, XCB_SET_MODE_INSERT, window); + /* Check if any assignments match */ + run_assignments(cwindow); + tree_render(); free(geom); diff --git a/src/match.c b/src/match.c index 9ed4d434..fd024d59 100644 --- a/src/match.c +++ b/src/match.c @@ -66,6 +66,12 @@ bool match_matches_window(Match *match, i3Window *window) { return true; } + /* TODO: pcre match */ + if (match->title != NULL && window->name_json != NULL && strcasecmp(match->title, window->name_json) == 0) { + LOG("match made by title (%s)\n", window->name_json); + return true; + } + LOG("match->dock = %d, window->dock = %d\n", match->dock, window->dock); if (match->dock != -1 && ((window->dock == W_DOCK_TOP && match->dock == M_DOCK_TOP) || diff --git a/src/window.c b/src/window.c index cd96890c..b6aaae9e 100644 --- a/src/window.c +++ b/src/window.c @@ -2,7 +2,7 @@ * vim:ts=4:sw=4:expandtab * * i3 - an improved dynamic tiling window manager - * © 2009-2010 Michael Stapelberg and contributors (see also: LICENSE) + * © 2009-2011 Michael Stapelberg and contributors (see also: LICENSE) * */ #include "all.h" @@ -12,7 +12,7 @@ * given window. * */ -void window_update_class(i3Window *win, xcb_get_property_reply_t *prop) { +void window_update_class(i3Window *win, xcb_get_property_reply_t *prop, bool before_mgmt) { if (prop == NULL || xcb_get_property_value_length(prop) == 0) { DLOG("empty property, not updating\n"); return; @@ -32,6 +32,11 @@ void window_update_class(i3Window *win, xcb_get_property_reply_t *prop) { else win->class_class = NULL; LOG("WM_CLASS changed to %s (instance), %s (class)\n", win->class_instance, win->class_class); + + if (before_mgmt) + return; + + run_assignments(win); } /* @@ -39,7 +44,7 @@ void window_update_class(i3Window *win, xcb_get_property_reply_t *prop) { * window. Further updates using window_update_name_legacy will be ignored. * */ -void window_update_name(i3Window *win, xcb_get_property_reply_t *prop) { +void window_update_name(i3Window *win, xcb_get_property_reply_t *prop, bool before_mgmt) { if (prop == NULL || xcb_get_property_value_length(prop) == 0) { DLOG("_NET_WM_NAME not specified, not changing\n"); return; @@ -70,6 +75,11 @@ void window_update_name(i3Window *win, xcb_get_property_reply_t *prop) { LOG("_NET_WM_NAME changed to \"%s\"\n", win->name_json); win->uses_net_wm_name = true; + + if (before_mgmt) + return; + + run_assignments(win); } /* @@ -79,7 +89,7 @@ void window_update_name(i3Window *win, xcb_get_property_reply_t *prop) { * window_update_name()). * */ -void window_update_name_legacy(i3Window *win, xcb_get_property_reply_t *prop) { +void window_update_name_legacy(i3Window *win, xcb_get_property_reply_t *prop, bool before_mgmt) { if (prop == NULL || xcb_get_property_value_length(prop) == 0) { DLOG("prop == NULL\n"); return; @@ -106,6 +116,11 @@ void window_update_name_legacy(i3Window *win, xcb_get_property_reply_t *prop) { win->name_json = sstrdup(new_name); win->name_len = strlen(new_name); win->name_x_changed = true; + + if (before_mgmt) + return; + + run_assignments(win); } /*