From e79cca8f725284dfb2d15fdb7a57419029eb023e Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sat, 16 May 2009 17:32:36 +0200 Subject: [PATCH] Implement putting clients onto specific workspaces ("assign" in the configfile) This closes ticket #39 --- docs/Makefile | 4 +- docs/userguide | 59 ++++++++++++++++++++ include/client.h | 47 ++++++++++++++++ include/data.h | 12 +++- include/i3.h | 1 + include/util.h | 21 ++----- src/client.c | 133 ++++++++++++++++++++++++++++++++++++++++++++ src/commands.c | 82 ++++++--------------------- src/config.c | 40 ++++++++++++- src/handlers.c | 3 +- src/mainx.c | 142 ++++++++++++++++++++++++++++++++++------------- src/util.c | 112 ++++++++++++++----------------------- 12 files changed, 461 insertions(+), 195 deletions(-) create mode 100644 docs/userguide create mode 100644 include/client.h create mode 100644 src/client.c diff --git a/docs/Makefile b/docs/Makefile index e69aefc5..123f839f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,5 +1,5 @@ -all: hacking-howto.html debugging.html +all: hacking-howto.html debugging.html userguide.html hacking-howto.html: hacking-howto asciidoc -a toc -n $< @@ -7,6 +7,8 @@ hacking-howto.html: hacking-howto debugging.html: debugging asciidoc -n $< +userguide.html: userguide + asciidoc -a toc -n $< clean: rm -f */*.{aux,log,toc,bm,pdf,dvi} diff --git a/docs/userguide b/docs/userguide new file mode 100644 index 00000000..36c2af4b --- /dev/null +++ b/docs/userguide @@ -0,0 +1,59 @@ +i3 User’s Guide +=============== +Michael Stapelberg +May 2009 + +This document contains all information you need to configuring and using the i3 window +manager. If it does not, please contact me on IRC, Jabber or E-Mail and I’ll help you out. + +== Configuring i3 + +TODO: document the other options, implement variables before + +terminal:: + Specifies the terminal emulator program you prefer. It will be started by default when + you press Mod1+Enter, but you can overwrite this. Refer to it as +$terminal+ to keep things + modular. +font:: + Specifies the default font you want i3 to use. Use an X core font descriptor here, like + +-misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1+. You can use +xfontsel(1)+ + to pick one. + +=== Keyboard bindings + +TODO + +*Syntax*: +-------------------------------- +bind [Modifiers+]keycode command +-------------------------------- + +*Examples*: +-------------------------------- +# Fullscreen +bind Mod1+41 f + +# Restart +bind Mod1+Shift+27 restart +-------------------------------- + +=== Automatically putting clients on specific workspaces + +It is recommended that you match on window classes whereever possible because some applications +first create their window and then care about setting the correct title. Firefox with Vimperator +comes to mind, as the window starts up being named Firefox and only when Vimperator is loaded, +the title changes. As i3 will get the title as soon as the application maps the window (mapping +means actually displaying it on the screen), you’d need to have to match on Firefox in this case. + +*Syntax*: +---------------------------------------------------- +assign ["]window class[/window title]["] [→] workspace +---------------------------------------------------- + +*Examples*: +---------------------- +assign urxvt 2 +assign urxvt → 2 +assign "urxvt" → 2 +assign "urxvt/VIM" → 3 +---------------------- diff --git a/include/client.h b/include/client.h new file mode 100644 index 00000000..a88f8d0b --- /dev/null +++ b/include/client.h @@ -0,0 +1,47 @@ +/* + * vim:ts=8:expandtab + * + * i3 - an improved dynamic tiling window manager + * + * (c) 2009 Michael Stapelberg and contributors + * + * See file LICENSE for license information. + * + */ +#include + +#include "data.h" + +#ifndef _CLIENT_H +#define _CLIENT_H + +/** + * Removes the given client from the container, either because it will be inserted into another + * one or because it was unmapped + * + */ +void client_remove_from_container(xcb_connection_t *conn, Client *client, Container *container); + +/** + * Warps the pointer into the given client (in the middle of it, to be specific), therefore + * selecting it + * + */ +void client_warp_pointer_into(xcb_connection_t *conn, Client *client); + +/** + * Kills the given window using WM_DELETE_WINDOW or xcb_kill_window + * + */ +void client_kill(xcb_connection_t *conn, Client *window); + +/** + * Checks if the given window class and title match the given client + * Window title is passed as "normal" string and as UCS-2 converted string for + * matching _NET_WM_NAME capable clients as well as those using legacy hints. + * + */ +bool client_matches_class_name(Client *client, char *to_class, char *to_title, + char *to_title_ucs, int to_title_ucs_len); + +#endif diff --git a/include/data.h b/include/data.h index af6b3ef5..8cb60b8f 100644 --- a/include/data.h +++ b/include/data.h @@ -48,7 +48,6 @@ typedef struct Font i3Font; typedef struct Container Container; typedef struct Client Client; typedef struct Binding Binding; -typedef struct Autostart Autostart; typedef struct Workspace Workspace; typedef struct Rect Rect; typedef struct Screen i3Screen; @@ -208,6 +207,17 @@ struct Autostart { TAILQ_ENTRY(Autostart) autostarts; }; +/* + * Holds an assignment for a given window class/title to a specific workspace + * (see src/config.c) + * + */ +struct Assignment { + char *windowclass_title; + int workspace; + TAILQ_ENTRY(Assignment) assignments; +}; + /* * Data structure for cached font information: * - font id in X11 (load it once) diff --git a/include/i3.h b/include/i3.h index 1c9e1b35..06f423a0 100644 --- a/include/i3.h +++ b/include/i3.h @@ -26,6 +26,7 @@ extern char **start_argv; extern Display *xkbdpy; extern TAILQ_HEAD(bindings_head, Binding) bindings; extern TAILQ_HEAD(autostarts_head, Autostart) autostarts; +extern TAILQ_HEAD(assignments_head, Assignment) assignments; extern SLIST_HEAD(stack_wins_head, Stack_Window) stack_wins; extern xcb_event_handlers_t evenths; extern int num_screens; diff --git a/include/util.h b/include/util.h index 889dcf12..709e48a4 100644 --- a/include/util.h +++ b/include/util.h @@ -123,13 +123,6 @@ void check_error(xcb_connection_t *conn, xcb_void_cookie_t cookie, char *err_mes */ char *convert_utf8_to_ucs2(char *input, int *real_strlen); -/** - * Removes the given client from the container, either because it will be inserted into another - * one or because it was unmapped - * - */ -void remove_client_from_container(xcb_connection_t *conn, Client *client, Container *container); - /** * Returns the client which comes next in focus stack (= was selected before) for * the given container, optionally excluding the given client. @@ -169,13 +162,6 @@ void leave_stack_mode(xcb_connection_t *conn, Container *container); */ void switch_layout_mode(xcb_connection_t *conn, Container *container, int mode); -/** - * Warps the pointer into the given client (in the middle of it, to be specific), therefore - * selecting it - * - */ -void warp_pointer_into(xcb_connection_t *conn, Client *client); - /** * Toggles fullscreen mode for the given client. It updates the data structures and * reconfigures (= resizes/moves) the client and its frame to the full size of the @@ -185,9 +171,12 @@ void warp_pointer_into(xcb_connection_t *conn, Client *client); void toggle_fullscreen(xcb_connection_t *conn, Client *client); /** - * Kills the given window using WM_DELETE_WINDOW or xcb_kill_window + * Gets the first matching client for the given window class/window title. + * If the paramater specific is set to a specific client, only this one + * will be checked. * */ -void kill_window(xcb_connection_t *conn, Client *window); +Client *get_matching_client(xcb_connection_t *conn, const char *window_classtitle, + Client *specific); #endif diff --git a/src/client.c b/src/client.c new file mode 100644 index 00000000..f5409d09 --- /dev/null +++ b/src/client.c @@ -0,0 +1,133 @@ +/* + * vim:ts=8:expandtab + * + * i3 - an improved dynamic tiling window manager + * + * © 2009 Michael Stapelberg and contributors + * + * See file LICENSE for license information. + * + * client.c: holds all client-specific functions + * + */ +#include +#include + +#include +#include + +#include "data.h" +#include "i3.h" +#include "xcb.h" +#include "util.h" +#include "queue.h" + +/* + * Removes the given client from the container, either because it will be inserted into another + * one or because it was unmapped + * + */ +void client_remove_from_container(xcb_connection_t *conn, Client *client, Container *container) { + CIRCLEQ_REMOVE(&(container->clients), client, clients); + + SLIST_REMOVE(&(container->workspace->focus_stack), client, Client, focus_clients); + + /* If the container will be empty now and is in stacking mode, we need to + unmap the stack_win */ + if (CIRCLEQ_EMPTY(&(container->clients)) && container->mode == MODE_STACK) { + struct Stack_Window *stack_win = &(container->stack_win); + stack_win->rect.height = 0; + xcb_unmap_window(conn, stack_win->window); + } +} + +/* + * Warps the pointer into the given client (in the middle of it, to be specific), therefore + * selecting it + * + */ +void client_warp_pointer_into(xcb_connection_t *conn, Client *client) { + int mid_x = client->rect.width / 2, + mid_y = client->rect.height / 2; + xcb_warp_pointer(conn, XCB_NONE, client->child, 0, 0, 0, 0, mid_x, mid_y); +} + +/* + * Returns true if the client supports the given protocol atom (like WM_DELETE_WINDOW) + * + */ +static bool client_supports_protocol(xcb_connection_t *conn, Client *client, xcb_atom_t atom) { + xcb_get_property_cookie_t cookie; + xcb_get_wm_protocols_reply_t protocols; + bool result = false; + + cookie = xcb_get_wm_protocols_unchecked(conn, client->child, atoms[WM_PROTOCOLS]); + if (xcb_get_wm_protocols_reply(conn, cookie, &protocols, NULL) != 1) + return false; + + /* Check if the client’s protocols have the requested atom set */ + for (uint32_t i = 0; i < protocols.atoms_len; i++) + if (protocols.atoms[i] == atom) + result = true; + + xcb_get_wm_protocols_reply_wipe(&protocols); + + return result; +} + +/* + * Kills the given window using WM_DELETE_WINDOW or xcb_kill_window + * + */ +void client_kill(xcb_connection_t *conn, Client *window) { + /* If the client does not support WM_DELETE_WINDOW, we kill it the hard way */ + if (!client_supports_protocol(conn, window, atoms[WM_DELETE_WINDOW])) { + LOG("Killing window the hard way\n"); + xcb_kill_client(conn, window->child); + return; + } + + xcb_client_message_event_t ev; + + memset(&ev, 0, sizeof(xcb_client_message_event_t)); + + ev.response_type = XCB_CLIENT_MESSAGE; + ev.window = window->child; + ev.type = atoms[WM_PROTOCOLS]; + ev.format = 32; + ev.data.data32[0] = atoms[WM_DELETE_WINDOW]; + ev.data.data32[1] = XCB_CURRENT_TIME; + + LOG("Sending WM_DELETE to the client\n"); + xcb_send_event(conn, false, window->child, XCB_EVENT_MASK_NO_EVENT, (char*)&ev); + xcb_flush(conn); +} + +/* + * Checks if the given window class and title match the given client + * Window title is passed as "normal" string and as UCS-2 converted string for + * matching _NET_WM_NAME capable clients as well as those using legacy hints. + * + */ +bool client_matches_class_name(Client *client, char *to_class, char *to_title, + char *to_title_ucs, int to_title_ucs_len) { + /* Check if the given class is part of the window class */ + if (strcasestr(client->window_class, to_class) == NULL) + return false; + + /* If no title was given, we’re done */ + if (to_title == NULL) + return true; + + if (client->name_len > -1) { + /* UCS-2 converted window titles */ + if (memmem(client->name, (client->name_len * 2), to_title_ucs, (to_title_ucs_len * 2)) == NULL) + return false; + } else { + /* Legacy hints */ + if (strcasestr(client->name, to_title) == NULL) + return false; + } + + return true; +} diff --git a/src/commands.c b/src/commands.c index 553d347d..b84cb858 100644 --- a/src/commands.c +++ b/src/commands.c @@ -23,6 +23,7 @@ #include "layout.h" #include "i3.h" #include "xinerama.h" +#include "client.h" bool focus_window_in_container(xcb_connection_t *conn, Container *container, direction_t direction) { /* If this container is empty, we’re done */ @@ -240,7 +241,7 @@ static void move_current_window(xcb_connection_t *conn, direction_t direction) { } /* Remove it from the old container and put it into the new one */ - remove_client_from_container(conn, current_client, container); + client_remove_from_container(conn, current_client, container); if (new->currently_focused != NULL) CIRCLEQ_INSERT_AFTER(&(new->clients), new->currently_focused, current_client, clients); @@ -458,7 +459,7 @@ static void move_current_window_to_workspace(xcb_connection_t *conn, int workspa assert(to_container != NULL); - remove_client_from_container(conn, current_client, container); + client_remove_from_container(conn, current_client, container); if (container->workspace->fullscreen_client == current_client) container->workspace->fullscreen_client = NULL; @@ -538,7 +539,7 @@ void show_workspace(xcb_connection_t *conn, int workspace) { if (CUR_CELL->currently_focused != NULL) { set_focus(conn, CUR_CELL->currently_focused, true); if (need_warp) { - warp_pointer_into(conn, CUR_CELL->currently_focused); + client_warp_pointer_into(conn, CUR_CELL->currently_focused); xcb_flush(conn); } } @@ -574,7 +575,7 @@ void show_workspace(xcb_connection_t *conn, int workspace) { if (CUR_CELL->currently_focused != NULL) { set_focus(conn, CUR_CELL->currently_focused, true); if (need_warp) { - warp_pointer_into(conn, CUR_CELL->currently_focused); + client_warp_pointer_into(conn, CUR_CELL->currently_focused); xcb_flush(conn); } } else xcb_set_input_focus(conn, XCB_INPUT_FOCUS_POINTER_ROOT, root, XCB_CURRENT_TIME); @@ -582,35 +583,6 @@ void show_workspace(xcb_connection_t *conn, int workspace) { render_layout(conn); } -/* - * Checks if the given window class and title match the given client - * Window title is passed as "normal" string and as UCS-2 converted string for - * matching _NET_WM_NAME capable clients as well as those using legacy hints. - * - */ -static bool client_matches_class_name(Client *client, char *to_class, char *to_title, - char *to_title_ucs, int to_title_ucs_len) { - /* Check if the given class is part of the window class */ - if (strcasestr(client->window_class, to_class) == NULL) - return false; - - /* If no title was given, we’re done */ - if (to_title == NULL) - return true; - - if (client->name_len > -1) { - /* UCS-2 converted window titles */ - if (memmem(client->name, (client->name_len * 2), to_title_ucs, (to_title_ucs_len * 2)) == NULL) - return false; - } else { - /* Legacy hints */ - if (strcasestr(client->name, to_title) == NULL) - return false; - } - - return true; -} - /* * Jumps to the given window class / title. * Title is matched using strstr, that is, matches if it appears anywhere @@ -619,44 +591,22 @@ static bool client_matches_class_name(Client *client, char *to_class, char *to_t * */ static void jump_to_window(xcb_connection_t *conn, const char *arguments) { - char *to_class, *to_title, *to_title_ucs = NULL; - int to_title_ucs_len; + char *classtitle; + Client *client; /* The first character is a quote, this was checked before */ - to_class = sstrdup(arguments+1); + classtitle = sstrdup(arguments+1); /* The last character is a quote, we just set it to NULL */ - to_class[strlen(to_class)-1] = '\0'; - - /* If a title was specified, split both strings at the slash */ - if ((to_title = strstr(to_class, "/")) != NULL) { - *(to_title++) = '\0'; - /* Convert to UCS-2 */ - to_title_ucs = convert_utf8_to_ucs2(to_title, &to_title_ucs_len); - } - - LOG("Should jump to class \"%s\" / title \"%s\"\n", to_class, to_title); - for (int workspace = 0; workspace < 10; workspace++) { - if (workspaces[workspace].screen == NULL) - continue; - - FOR_TABLE(&(workspaces[workspace])) { - Container *con = workspaces[workspace].table[cols][rows]; - Client *client; + classtitle[strlen(classtitle)-1] = '\0'; - CIRCLEQ_FOREACH(client, &(con->clients), clients) { - LOG("Checking client with class=%s, name=%s\n", client->window_class, client->name); - if (!client_matches_class_name(client, to_class, to_title, to_title_ucs, to_title_ucs_len)) - continue; - - set_focus(conn, client, true); - goto done; - } - } + if ((client = get_matching_client(conn, classtitle, NULL)) == NULL) { + free(classtitle); + LOG("No matching client found.\n"); + return; } -done: - free(to_class); - FREE(to_title_ucs); + free(classtitle); + set_focus(conn, client, true); } /* @@ -763,7 +713,7 @@ void parse_command(xcb_connection_t *conn, const char *command) { } LOG("Killing current window\n"); - kill_window(conn, CUR_CELL->currently_focused); + client_kill(conn, CUR_CELL->currently_focused); return; } diff --git a/src/config.c b/src/config.c index ca84282a..562b2e92 100644 --- a/src/config.c +++ b/src/config.c @@ -87,7 +87,7 @@ void load_configuration(const char *override_configpath) { /* exec-lines (autostart) */ if (strcasecmp(key, "exec") == 0) { - Autostart *new = smalloc(sizeof(Autostart)); + struct Autostart *new = smalloc(sizeof(struct Autostart)); new->command = sstrdup(value); TAILQ_INSERT_TAIL(&autostarts, new, autostarts); continue; @@ -133,6 +133,44 @@ void load_configuration(const char *override_configpath) { continue; } + /* assign window class[/window title] → workspace */ + if (strcasecmp(key, "assign") == 0) { + LOG("assign: \"%s\"\n", value); + char *class_title = sstrdup(value); + char *target; + + /* If the window class/title is quoted we skip quotes */ + if (class_title[0] == '"') { + class_title++; + char *end = strchr(class_title, '"'); + if (end == NULL) + die("Malformatted assignment, couldn't find finishing quote\n"); + *end = '\0'; + } else { + /* If it is not quoted, we terminate it at the first space */ + char *end = strchr(class_title, ' '); + if (end == NULL) + die("Malformed assignment, couldn't find terminating space\n"); + *end = '\0'; + } + + /* The target is the last argument separated by a space */ + if ((target = strrchr(value, ' ')) == NULL) + die("Malformed assignment, couldn't find target\n"); + target++; + + if (atoi(target) < 1 || atoi(target) > 10) + die("Malformed assignment, invalid workspace number\n"); + + LOG("assignment parsed: \"%s\" to \"%s\"\n", class_title, target); + + struct Assignment *new = smalloc(sizeof(struct Assignment)); + new->windowclass_title = class_title; + new->workspace = atoi(target); + TAILQ_INSERT_TAIL(&assignments, new, assignments); + continue; + } + fprintf(stderr, "Unknown configfile option: %s\n", key); exit(1); } diff --git a/src/handlers.c b/src/handlers.c index 7652f07b..c7c11246 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -32,6 +32,7 @@ #include "config.h" #include "queue.h" #include "resize.h" +#include "client.h" /* After mapping/unmapping windows, a notify event is generated. However, we don’t want it, since it’d trigger an infinite loop of switching between the different windows when @@ -501,7 +502,7 @@ int handle_unmap_notify_event(void *data, xcb_connection_t *conn, xcb_unmap_noti con->workspace->fullscreen_client = NULL; /* Remove the client from the list of clients */ - remove_client_from_container(conn, client, con); + client_remove_from_container(conn, client, con); /* Set focus to the last focused client in this container */ con->currently_focused = get_last_focused_client(conn, con, NULL); diff --git a/src/mainx.c b/src/mainx.c index da79ee3a..dbce26a1 100644 --- a/src/mainx.c +++ b/src/mainx.c @@ -55,6 +55,9 @@ struct bindings_head bindings = TAILQ_HEAD_INITIALIZER(bindings); /* The list of exec-lines */ struct autostarts_head autostarts = TAILQ_HEAD_INITIALIZER(autostarts); +/* The list of assignments */ +struct assignments_head assignments = TAILQ_HEAD_INITIALIZER(assignments); + /* This is a list of Stack_Windows, global, for easier/faster access on expose events */ struct stack_wins_head stack_wins = SLIST_HEAD_INITIALIZER(stack_wins); @@ -102,17 +105,21 @@ void manage_window(xcb_property_handlers_t *prophs, xcb_connection_t *conn, xcb_ if (!attr) { wa.tag = TAG_COOKIE; wa.u.cookie = xcb_get_window_attributes(conn, window); - attr = xcb_get_window_attributes_reply(conn, wa.u.cookie, 0); - } - geom = xcb_get_geometry_reply(conn, geomc, 0); - if (attr && geom) { - reparent_window(conn, window, attr->visual, geom->root, geom->depth, - geom->x, geom->y, geom->width, geom->height); - xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, WM_CLASS); - xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, WM_NAME); - xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, WM_NORMAL_HINTS); - xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, atoms[_NET_WM_NAME]); + if ((attr = xcb_get_window_attributes_reply(conn, wa.u.cookie, 0)) == NULL) + return; } + if ((geom = xcb_get_geometry_reply(conn, geomc, 0)) == NULL) + goto out; + + /* Reparent the window and add it to our list of managed windows */ + reparent_window(conn, window, attr->visual, geom->root, geom->depth, + geom->x, geom->y, geom->width, geom->height); + + /* Generate callback events for every property we watch */ + xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, WM_CLASS); + xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, WM_NAME); + xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, WM_NORMAL_HINTS); + xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, atoms[_NET_WM_NAME]); free(geom); out: @@ -131,7 +138,8 @@ void reparent_window(xcb_connection_t *conn, xcb_window_t child, xcb_visualid_t visual, xcb_window_t root, uint8_t depth, int16_t x, int16_t y, uint16_t width, uint16_t height) { - xcb_get_property_cookie_t wm_type_cookie, strut_cookie, state_cookie; + xcb_get_property_cookie_t wm_type_cookie, strut_cookie, state_cookie, + utf8_title_cookie, title_cookie, class_cookie; uint32_t mask = 0; uint32_t values[3]; @@ -147,6 +155,9 @@ void reparent_window(xcb_connection_t *conn, xcb_window_t child, wm_type_cookie = xcb_get_any_property_unchecked(conn, false, child, atoms[_NET_WM_WINDOW_TYPE], UINT32_MAX); strut_cookie = xcb_get_any_property_unchecked(conn, false, child, atoms[_NET_WM_STRUT_PARTIAL], UINT32_MAX); state_cookie = xcb_get_any_property_unchecked(conn, false, child, atoms[_NET_WM_STATE], UINT32_MAX); + utf8_title_cookie = xcb_get_any_property_unchecked(conn, false, child, atoms[_NET_WM_NAME], 128); + title_cookie = xcb_get_any_property_unchecked(conn, false, child, WM_NAME, 128); + class_cookie = xcb_get_any_property_unchecked(conn, false, child, WM_CLASS, 128); Client *new = table_get(&by_child, child); @@ -191,6 +202,8 @@ void reparent_window(xcb_connection_t *conn, xcb_window_t child, /* Yo dawg, I heard you like windows, so I create a window around your window… */ new->frame = create_window(conn, framerect, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_CURSOR_LEFT_PTR, mask, values); + /* Set WM_STATE_NORMAL because GTK applications don’t want to drag & drop if we don’t. + * Also, xprop(1) needs that to work. */ long data[] = { XCB_WM_STATE_NORMAL, XCB_NONE }; xcb_change_property(conn, XCB_PROP_MODE_REPLACE, new->child, atoms[WM_STATE], atoms[WM_STATE], 32, 2, data); @@ -227,15 +240,16 @@ void reparent_window(xcb_connection_t *conn, xcb_window_t child, xcb_atom_t *atom; xcb_get_property_reply_t *preply = xcb_get_property_reply(conn, wm_type_cookie, NULL); if (preply != NULL && preply->value_len > 0 && (atom = xcb_get_property_value(preply))) { - for (int i = 0; i < xcb_get_property_value_length(preply); i++) - if (atom[i] == atoms[_NET_WM_WINDOW_TYPE_DOCK]) { - LOG("Window is a dock.\n"); - new->dock = true; - new->titlebar_position = TITLEBAR_OFF; - new->force_reconfigure = true; - new->container = NULL; - SLIST_INSERT_HEAD(&(c_ws->screen->dock_clients), new, dock_clients); - } + for (int i = 0; i < xcb_get_property_value_length(preply); i++) { + if (atom[i] != atoms[_NET_WM_WINDOW_TYPE_DOCK]) + continue; + LOG("Window is a dock.\n"); + new->dock = true; + new->titlebar_position = TITLEBAR_OFF; + new->force_reconfigure = true; + new->container = NULL; + SLIST_INSERT_HEAD(&(c_ws->screen->dock_clients), new, dock_clients); + } } if (new->dock) { @@ -257,18 +271,65 @@ void reparent_window(xcb_connection_t *conn, xcb_window_t child, LOG("The client didn't specify space to reserve at the screen edge, using its height (%d)\n", height); new->desired_height = height; } + } else { + /* If it’s not a dock, we can check on which workspace we should put it. */ + + /* Firstly, we need to get the window’s class / title. We asked for the properties at the + * top of this function, get them now and pass them to our callback function for window class / title + * changes. It is important that the client was already inserted into the by_child table, + * because the callbacks won’t work otherwise. */ + preply = xcb_get_property_reply(conn, utf8_title_cookie, NULL); + handle_windowname_change(NULL, conn, 0, new->child, atoms[_NET_WM_NAME], preply); + + preply = xcb_get_property_reply(conn, title_cookie, NULL); + handle_windowname_change_legacy(NULL, conn, 0, new->child, WM_NAME, preply); + + preply = xcb_get_property_reply(conn, class_cookie, NULL); + handle_windowclass_change(NULL, conn, 0, new->child, WM_CLASS, preply); + + LOG("DEBUG: should have all infos now\n"); + struct Assignment *assign; + TAILQ_FOREACH(assign, &assignments, assignments) { + if (get_matching_client(conn, assign->windowclass_title, new) == NULL) + continue; + + LOG("Assignment \"%s\" matches, so putting it on workspace %d\n", + assign->windowclass_title, assign->workspace); + + if (c_ws->screen->current_workspace == (assign->workspace-1)) { + LOG("We are already there, no need to do anything\n"); + break; + } + + LOG("Changin container/workspace and unmapping the client\n"); + Workspace *t_ws = &(workspaces[assign->workspace-1]); + if (t_ws->screen == NULL) { + LOG("initializing new workspace, setting num to %d\n", assign->workspace); + t_ws->screen = c_ws->screen; + /* Copy the dimensions from the virtual screen */ + memcpy(&(t_ws->rect), &(t_ws->screen->rect), sizeof(Rect)); + } + + new->container = t_ws->table[t_ws->current_col][t_ws->current_row]; + new->workspace = t_ws; + xcb_unmap_window(conn, new->frame); + break; + } } - /* Focus the new window if we’re not in fullscreen mode and if it is not a dock window */ - if (CUR_CELL->workspace->fullscreen_client == NULL) { - if (!new->dock) { - CUR_CELL->currently_focused = new; - xcb_set_input_focus(conn, XCB_INPUT_FOCUS_POINTER_ROOT, new->child, XCB_CURRENT_TIME); + if (CUR_CELL->workspace->fullscreen_client != NULL) { + if (new->container == CUR_CELL) { + /* If we are in fullscreen, we should lower the window to not be annoying */ + uint32_t values[] = { XCB_STACK_MODE_BELOW }; + xcb_configure_window(conn, new->frame, XCB_CONFIG_WINDOW_STACK_MODE, values); + } + } else if (!new->dock) { + /* Focus the new window if we’re not in fullscreen mode and if it is not a dock window */ + if (new->container->workspace->fullscreen_client == NULL) { + new->container->currently_focused = new; + if (new->container == CUR_CELL) + xcb_set_input_focus(conn, XCB_INPUT_FOCUS_POINTER_ROOT, new->child, XCB_CURRENT_TIME); } - } else { - /* If we are in fullscreen, we should lower the window to not be annoying */ - uint32_t values[] = { XCB_STACK_MODE_BELOW }; - xcb_configure_window(conn, new->frame, XCB_CONFIG_WINDOW_STACK_MODE, values); } /* Insert into the currently active container, if it’s not a dock window */ @@ -276,8 +337,8 @@ void reparent_window(xcb_connection_t *conn, xcb_window_t child, /* Insert after the old active client, if existing. If it does not exist, the container is empty and it does not matter, where we insert it */ if (old_focused != NULL && !old_focused->dock) - CIRCLEQ_INSERT_AFTER(&(CUR_CELL->clients), old_focused, new, clients); - else CIRCLEQ_INSERT_TAIL(&(CUR_CELL->clients), new, clients); + CIRCLEQ_INSERT_AFTER(&(new->container->clients), old_focused, new, clients); + else CIRCLEQ_INSERT_TAIL(&(new->container->clients), new, clients); SLIST_INSERT_HEAD(&(new->container->workspace->focus_stack), new, focus_clients); } @@ -287,14 +348,15 @@ void reparent_window(xcb_connection_t *conn, xcb_window_t child, if ((preply = xcb_get_property_reply(conn, state_cookie, NULL)) != NULL && (state = xcb_get_property_value(preply)) != NULL) /* Check all set _NET_WM_STATEs */ - for (int i = 0; i < xcb_get_property_value_length(preply); i++) - if (state[i] == atoms[_NET_WM_STATE_FULLSCREEN]) { - /* If the window got the fullscreen state, we just toggle fullscreen - and don’t event bother to redraw the layout – that would not change - anything anyways */ - toggle_fullscreen(conn, new); - return; - } + for (int i = 0; i < xcb_get_property_value_length(preply); i++) { + if (state[i] != atoms[_NET_WM_STATE_FULLSCREEN]) + continue; + /* If the window got the fullscreen state, we just toggle fullscreen + and don’t event bother to redraw the layout – that would not change + anything anyways */ + toggle_fullscreen(conn, new); + return; + } render_layout(conn); } @@ -531,7 +593,7 @@ int main(int argc, char *argv[], char *env[]) { } /* Autostarting exec-lines */ - Autostart *exec; + struct Autostart *exec; TAILQ_FOREACH(exec, &autostarts, autostarts) { LOG("auto-starting %s\n", exec->command); start_application(exec->command); diff --git a/src/util.c b/src/util.c index c0e74c75..5b51bd5b 100644 --- a/src/util.c +++ b/src/util.c @@ -27,6 +27,7 @@ #include "layout.h" #include "util.h" #include "xcb.h" +#include "client.h" static iconv_t conversion_descriptor = 0; struct keyvalue_table_head by_parent = TAILQ_HEAD_INITIALIZER(by_parent); @@ -224,25 +225,6 @@ char *convert_utf8_to_ucs2(char *input, int *real_strlen) { return buffer; } -/* - * Removes the given client from the container, either because it will be inserted into another - * one or because it was unmapped - * - */ -void remove_client_from_container(xcb_connection_t *conn, Client *client, Container *container) { - CIRCLEQ_REMOVE(&(container->clients), client, clients); - - SLIST_REMOVE(&(container->workspace->focus_stack), client, Client, focus_clients); - - /* If the container will be empty now and is in stacking mode, we need to - unmap the stack_win */ - if (CIRCLEQ_EMPTY(&(container->clients)) && container->mode == MODE_STACK) { - struct Stack_Window *stack_win = &(container->stack_win); - stack_win->rect.height = 0; - xcb_unmap_window(conn, stack_win->window); - } -} - /* * Returns the client which comes next in focus stack (= was selected before) for * the given container, optionally excluding the given client. @@ -435,17 +417,6 @@ void switch_layout_mode(xcb_connection_t *conn, Container *container, int mode) set_focus(conn, container->currently_focused, true); } -/* - * Warps the pointer into the given client (in the middle of it, to be specific), therefore - * selecting it - * - */ -void warp_pointer_into(xcb_connection_t *conn, Client *client) { - int mid_x = client->rect.width / 2, - mid_y = client->rect.height / 2; - xcb_warp_pointer(conn, XCB_NONE, client->child, 0, 0, 0, 0, mid_x, mid_y); -} - /* * Toggles fullscreen mode for the given client. It updates the data structures and * reconfigures (= resizes/moves) the client and its frame to the full size of the @@ -508,52 +479,55 @@ void toggle_fullscreen(xcb_connection_t *conn, Client *client) { } /* - * Returns true if the client supports the given protocol atom (like WM_DELETE_WINDOW) + * Gets the first matching client for the given window class/window title. + * If the paramater specific is set to a specific client, only this one + * will be checked. * */ -static bool client_supports_protocol(xcb_connection_t *conn, Client *client, xcb_atom_t atom) { - xcb_get_property_cookie_t cookie; - xcb_get_wm_protocols_reply_t protocols; - bool result = false; +Client *get_matching_client(xcb_connection_t *conn, const char *window_classtitle, + Client *specific) { + char *to_class, *to_title, *to_title_ucs = NULL; + int to_title_ucs_len; + Client *matching = NULL; + + to_class = sstrdup(window_classtitle); + + /* If a title was specified, split both strings at the slash */ + if ((to_title = strstr(to_class, "/")) != NULL) { + *(to_title++) = '\0'; + /* Convert to UCS-2 */ + to_title_ucs = convert_utf8_to_ucs2(to_title, &to_title_ucs_len); + } - cookie = xcb_get_wm_protocols_unchecked(conn, client->child, atoms[WM_PROTOCOLS]); - if (xcb_get_wm_protocols_reply(conn, cookie, &protocols, NULL) != 1) - return false; + /* If we were given a specific client we only check if that one matches */ + if (specific != NULL) { + if (client_matches_class_name(specific, to_class, to_title, to_title_ucs, to_title_ucs_len)) + matching = specific; + goto done; + } - /* Check if the client’s protocols have the requested atom set */ - for (uint32_t i = 0; i < protocols.atoms_len; i++) - if (protocols.atoms[i] == atom) - result = true; + LOG("Getting clients for class \"%s\" / title \"%s\"\n", to_class, to_title); + for (int workspace = 0; workspace < 10; workspace++) { + if (workspaces[workspace].screen == NULL) + continue; - xcb_get_wm_protocols_reply_wipe(&protocols); + FOR_TABLE(&(workspaces[workspace])) { + Container *con = workspaces[workspace].table[cols][rows]; + Client *client; - return result; -} + CIRCLEQ_FOREACH(client, &(con->clients), clients) { + LOG("Checking client with class=%s, name=%s\n", client->window_class, client->name); + if (!client_matches_class_name(client, to_class, to_title, to_title_ucs, to_title_ucs_len)) + continue; -/* - * Kills the given window using WM_DELETE_WINDOW or xcb_kill_window - * - */ -void kill_window(xcb_connection_t *conn, Client *window) { - /* If the client does not support WM_DELETE_WINDOW, we kill it the hard way */ - if (!client_supports_protocol(conn, window, atoms[WM_DELETE_WINDOW])) { - LOG("Killing window the hard way\n"); - xcb_kill_client(conn, window->child); - return; + matching = client; + goto done; + } + } } - xcb_client_message_event_t ev; - - memset(&ev, 0, sizeof(xcb_client_message_event_t)); - - ev.response_type = XCB_CLIENT_MESSAGE; - ev.window = window->child; - ev.type = atoms[WM_PROTOCOLS]; - ev.format = 32; - ev.data.data32[0] = atoms[WM_DELETE_WINDOW]; - ev.data.data32[1] = XCB_CURRENT_TIME; - - LOG("Sending WM_DELETE to the client\n"); - xcb_send_event(conn, false, window->child, XCB_EVENT_MASK_NO_EVENT, (char*)&ev); - xcb_flush(conn); +done: + free(to_class); + FREE(to_title_ucs); + return matching; } -- 2.39.5