diff --git a/docs/userguide b/docs/userguide index 184848aa..49a967c2 100644 --- a/docs/userguide +++ b/docs/userguide @@ -754,6 +754,8 @@ class:: Compares the window class (the second part of WM_CLASS) instance:: Compares the window instance (the first part of WM_CLASS) +window_role:: + Compares the window role (WM_WINDOW_ROLE). id:: Compares the X11 window ID, which you can get via +xwininfo+ for example. title:: @@ -764,9 +766,9 @@ con_id:: Compares the i3-internal container ID, which you can get via the IPC interface. Handy for scripting. -The criteria +class+, +instance+, +title+ and +mark+ are actually regular -expressions (PCRE). See +pcresyntax(3)+ or +perldoc perlre+ for information on -how to use them. +The criteria +class+, +instance+, +role+, +title+ and +mark+ are actually +regular expressions (PCRE). See +pcresyntax(3)+ or +perldoc perlre+ for +information on how to use them. === Splitting containers diff --git a/include/atoms.xmacro b/include/atoms.xmacro index 300a8aba..7f83a7ee 100644 --- a/include/atoms.xmacro +++ b/include/atoms.xmacro @@ -21,5 +21,6 @@ xmacro(UTF8_STRING) xmacro(WM_STATE) xmacro(WM_CLIENT_LEADER) xmacro(WM_TAKE_FOCUS) +xmacro(WM_WINDOW_ROLE) xmacro(I3_SOCKET_PATH) xmacro(I3_CONFIG_PATH) diff --git a/include/data.h b/include/data.h index 1db7c442..f6052b9f 100644 --- a/include/data.h +++ b/include/data.h @@ -264,6 +264,11 @@ struct Window { * application supports _NET_WM_NAME, in COMPOUND_TEXT otherwise). */ char *name_x; + /** The WM_WINDOW_ROLE of this window (for example, the pidgin buddy window + * sets "buddy list"). Useful to match specific windows in assignments or + * for_window. */ + char *role; + /** Flag to force re-rendering the decoration upon changes */ bool name_x_changed; @@ -298,6 +303,7 @@ struct Match { struct regex *class; struct regex *instance; struct regex *mark; + struct regex *role; enum { M_DONTCHECK = -1, M_NODOCK = 0, diff --git a/include/window.h b/include/window.h index fe282aa0..cb9fbbc5 100644 --- a/include/window.h +++ b/include/window.h @@ -42,4 +42,10 @@ void window_update_transient_for(i3Window *win, xcb_get_property_reply_t *prop); */ void window_update_strut_partial(i3Window *win, xcb_get_property_reply_t *prop); +/** + * Updates the WM_WINDOW_ROLE + * + */ +void window_update_role(i3Window *win, xcb_get_property_reply_t *prop, bool before_mgmt); + #endif diff --git a/src/cfgparse.l b/src/cfgparse.l index e5dd29c0..021e3516 100644 --- a/src/cfgparse.l +++ b/src/cfgparse.l @@ -170,6 +170,7 @@ shift { return TOKSHIFT; } class { yy_push_state(WANT_QSTRING); return TOK_CLASS; } instance { yy_push_state(WANT_QSTRING); return TOK_INSTANCE; } +window_role { yy_push_state(WANT_QSTRING); return TOK_WINDOW_ROLE; } 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; } diff --git a/src/cfgparse.y b/src/cfgparse.y index 57f943c8..016953fd 100644 --- a/src/cfgparse.y +++ b/src/cfgparse.y @@ -632,6 +632,7 @@ void parse_file(const char *f) { %token TOK_MARK "mark" %token TOK_CLASS "class" %token TOK_INSTANCE "instance" +%token TOK_WINDOW_ROLE "window_role" %token TOK_ID "id" %token TOK_CON_ID "con_id" %token TOK_TITLE "title" @@ -792,6 +793,12 @@ criterion: current_match.instance = regex_new($3); free($3); } + | TOK_WINDOW_ROLE '=' STR + { + printf("criteria: window_role = %s\n", $3); + current_match.role = regex_new($3); + free($3); + } | TOK_CON_ID '=' STR { printf("criteria: id = %s\n", $3); diff --git a/src/cmdparse.l b/src/cmdparse.l index c7c64e35..968b7e52 100644 --- a/src/cmdparse.l +++ b/src/cmdparse.l @@ -155,6 +155,7 @@ no { return TOK_DISABLE; } class { BEGIN(WANT_QSTRING); return TOK_CLASS; } instance { BEGIN(WANT_QSTRING); return TOK_INSTANCE; } +window_role { BEGIN(WANT_QSTRING); return TOK_WINDOW_ROLE; } id { BEGIN(WANT_QSTRING); return TOK_ID; } con_id { BEGIN(WANT_QSTRING); return TOK_CON_ID; } con_mark { BEGIN(WANT_QSTRING); return TOK_MARK; } diff --git a/src/cmdparse.y b/src/cmdparse.y index 650a2eb8..0a99c224 100644 --- a/src/cmdparse.y +++ b/src/cmdparse.y @@ -177,6 +177,7 @@ bool definitelyGreaterThan(float a, float b, float epsilon) { %token TOK_CLASS "class" %token TOK_INSTANCE "instance" +%token TOK_WINDOW_ROLE "window_role" %token TOK_ID "id" %token TOK_CON_ID "con_id" %token TOK_TITLE "title" @@ -308,6 +309,12 @@ criterion: current_match.instance = regex_new($3); free($3); } + | TOK_WINDOW_ROLE '=' STR + { + printf("criteria: window_role = %s\n", $3); + current_match.role = regex_new($3); + free($3); + } | TOK_CON_ID '=' STR { printf("criteria: id = %s\n", $3); diff --git a/src/handlers.c b/src/handlers.c index b3467990..36cf039f 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -557,6 +557,21 @@ static bool handle_windowname_change_legacy(void *data, xcb_connection_t *conn, return true; } +/* + * Called when a window changes its WM_WINDOW_ROLE. + * + */ +static bool handle_windowrole_change(void *data, xcb_connection_t *conn, uint8_t state, + xcb_window_t window, xcb_atom_t atom, xcb_get_property_reply_t *prop) { + Con *con; + if ((con = con_by_window_id(window)) == NULL || con->window == NULL) + return false; + + window_update_role(con->window, prop, false); + + return true; +} + #if 0 /* * Updates the client’s WM_CLASS property @@ -933,7 +948,8 @@ static struct property_handler_t property_handlers[] = { { 0, 128, handle_windowname_change_legacy }, { 0, UINT_MAX, handle_normal_hints }, { 0, UINT_MAX, handle_clientleader_change }, - { 0, UINT_MAX, handle_transient_for } + { 0, UINT_MAX, handle_transient_for }, + { 0, 128, handle_windowrole_change } }; #define NUM_HANDLERS (sizeof(property_handlers) / sizeof(struct property_handler_t)) @@ -949,6 +965,7 @@ void property_handlers_init() { property_handlers[3].atom = XCB_ATOM_WM_NORMAL_HINTS; property_handlers[4].atom = A_WM_CLIENT_LEADER; property_handlers[5].atom = XCB_ATOM_WM_TRANSIENT_FOR; + property_handlers[6].atom = A_WM_WINDOW_ROLE; } static void property_notify(uint8_t state, xcb_window_t window, xcb_atom_t atom) { diff --git a/src/manage.c b/src/manage.c index 9ee3dd72..35055d17 100644 --- a/src/manage.c +++ b/src/manage.c @@ -83,7 +83,8 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki xcb_get_property_cookie_t wm_type_cookie, strut_cookie, state_cookie, utf8_title_cookie, title_cookie, - class_cookie, leader_cookie, transient_cookie; + class_cookie, leader_cookie, transient_cookie, + role_cookie; geomc = xcb_get_geometry(conn, d); @@ -145,6 +146,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki transient_cookie = GET_PROPERTY(XCB_ATOM_WM_TRANSIENT_FOR, UINT32_MAX); title_cookie = GET_PROPERTY(XCB_ATOM_WM_NAME, 128); class_cookie = GET_PROPERTY(XCB_ATOM_WM_CLASS, 128); + role_cookie = GET_PROPERTY(A_WM_WINDOW_ROLE, 128); /* TODO: also get wm_normal_hints here. implement after we got rid of xcb-event */ DLOG("reparenting!\n"); @@ -171,6 +173,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki 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)); + window_update_role(cwindow, xcb_get_property_reply(conn, role_cookie, NULL), true); /* check if the window needs WM_TAKE_FOCUS */ cwindow->needs_take_focus = window_supports_protocol(cwindow->id, A_WM_TAKE_FOCUS); diff --git a/src/match.c b/src/match.c index 3514acee..745989ba 100644 --- a/src/match.c +++ b/src/match.c @@ -39,6 +39,7 @@ bool match_is_empty(Match *match) { match->application == NULL && match->class == NULL && match->instance == NULL && + match->role == NULL && match->id == XCB_NONE && match->con_id == NULL && match->dock == -1 && @@ -65,6 +66,7 @@ void match_copy(Match *dest, Match *src) { DUPLICATE_REGEX(application); DUPLICATE_REGEX(class); DUPLICATE_REGEX(instance); + DUPLICATE_REGEX(role); } /* @@ -113,6 +115,16 @@ bool match_matches_window(Match *match, i3Window *window) { } } + if (match->role != NULL) { + if (window->role != NULL && + regex_matches(match->role, window->role)) { + LOG("window_role matches (%s)\n", window->role); + } else { + LOG("window_role does not match\n"); + return false; + } + } + if (match->dock != -1) { LOG("match->dock = %d, window->dock = %d\n", match->dock, window->dock); if ((window->dock == W_DOCK_TOP && match->dock == M_DOCK_TOP) || @@ -148,6 +160,7 @@ void match_free(Match *match) { regex_free(match->class); regex_free(match->instance); regex_free(match->mark); + regex_free(match->role); /* Second step: free the regex helper struct itself */ FREE(match->title); @@ -155,4 +168,5 @@ void match_free(Match *match) { FREE(match->class); FREE(match->instance); FREE(match->mark); + FREE(match->role); } diff --git a/src/window.c b/src/window.c index 3dd66456..4b8b6614 100644 --- a/src/window.c +++ b/src/window.c @@ -215,3 +215,36 @@ void window_update_strut_partial(i3Window *win, xcb_get_property_reply_t *prop) free(prop); } + +/* + * Updates the WM_WINDOW_ROLE + * + */ +void window_update_role(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"); + FREE(prop); + return; + } + + char *new_role; + if (asprintf(&new_role, "%.*s", xcb_get_property_value_length(prop), + (char*)xcb_get_property_value(prop)) == -1) { + perror("asprintf()"); + DLOG("Could not get WM_WINDOW_ROLE\n"); + free(prop); + return; + } + FREE(win->role); + win->role = new_role; + LOG("WM_WINDOW_ROLE changed to \"%s\"\n", win->role); + + if (before_mgmt) { + free(prop); + return; + } + + run_assignments(win); + + free(prop); +} diff --git a/testcases/complete-run.pl b/testcases/complete-run.pl index 6f00877c..8f740d8c 100755 --- a/testcases/complete-run.pl +++ b/testcases/complete-run.pl @@ -242,7 +242,7 @@ sub take_job { my $output; my $parser = TAP::Parser->new({ - exec => [ 'sh', '-c', "DISPLAY=$display /usr/bin/perl -It/lib $test" ], + exec => [ 'sh', '-c', qq|DISPLAY=$display LOGPATH="$logpath" /usr/bin/perl -It/lib $test| ], spool => IO::Scalar->new(\$output), merge => 1, }); diff --git a/testcases/t/65-for_window.t b/testcases/t/65-for_window.t index fb4c2812..1746d117 100644 --- a/testcases/t/65-for_window.t +++ b/testcases/t/65-for_window.t @@ -349,5 +349,108 @@ is($content[0]->{border}, 'normal', 'normal border'); exit_gracefully($process->pid); +############################################################## +# 8: check that the role criterion works properly +############################################################## + +# this configuration is broken because "asdf" is not a valid integer +# the for_window should therefore recognize this error and don’t add the +# assignment +$config = <root->create_child( + class => WINDOW_CLASS_INPUT_OUTPUT, + rect => [ 0, 0, 30, 30 ], + background_color => '#00ff00', +); + +$window->_create; + +my $atomname = $x->atom(name => 'WM_WINDOW_ROLE'); +my $atomtype = $x->atom(name => 'STRING'); +$x->change_property( + PROP_MODE_REPLACE, + $window->id, + $atomname->id, + $atomtype->id, + 8, + length("i3test") + 1, + "i3test\x00" +); + +$window->name('usethis'); +$window->map; +sleep 0.25; + +@content = @{get_ws_content($tmp)}; +cmp_ok(@content, '==', 1, 'one node on this workspace now'); +is($content[0]->{border}, 'none', 'no border (window_role)'); + +exit_gracefully($process->pid); + +############################################################## +# 9: another test for the window_role, but this time it changes +# *after* the window has been mapped +############################################################## + +# this configuration is broken because "asdf" is not a valid integer +# the for_window should therefore recognize this error and don’t add the +# assignment +$config = <root->create_child( + class => WINDOW_CLASS_INPUT_OUTPUT, + rect => [ 0, 0, 30, 30 ], + background_color => '#00ff00', +); + +$window->_create; + +$window->name('usethis'); +$window->map; +sleep 0.25; + +@content = @{get_ws_content($tmp)}; +cmp_ok(@content, '==', 1, 'one node on this workspace now'); +is($content[0]->{border}, 'normal', 'normal border (window_role 2)'); + +$atomname = $x->atom(name => 'WM_WINDOW_ROLE'); +$atomtype = $x->atom(name => 'STRING'); +$x->change_property( + PROP_MODE_REPLACE, + $window->id, + $atomname->id, + $atomtype->id, + 8, + length("i3test") + 1, + "i3test\x00" +); + +$x->flush; + +sleep 0.25; + +@content = @{get_ws_content($tmp)}; +cmp_ok(@content, '==', 1, 'one node on this workspace now'); +is($content[0]->{border}, 'none', 'no border (window_role 2)'); + +exit_gracefully($process->pid); + done_testing; diff --git a/testcases/t/lib/i3test.pm b/testcases/t/lib/i3test.pm index 749b89b7..3d2d85af 100644 --- a/testcases/t/lib/i3test.pm +++ b/testcases/t/lib/i3test.pm @@ -259,7 +259,10 @@ sub launch_with_config { say $fh "ipc-socket $tmp_socket_path"; close($fh); - my $i3cmd = "exec " . abs_path("../i3") . " -V -d all --disable-signalhandler -c $tmpfile >/dev/null 2>/dev/null"; + # Use $ENV{LOGPATH}, gets set in complete-run.pl. We append instead of + # overwrite because there might be multiple instances of i3 running during + # one test case. + my $i3cmd = "exec " . abs_path("../i3") . " -V -d all --disable-signalhandler -c $tmpfile >>$ENV{LOGPATH} 2>&1"; my $process = Proc::Background->new($i3cmd); sleep 1;