From: Michael Stapelberg Date: Wed, 14 Sep 2011 22:00:02 +0000 (+0100) Subject: Merge branch 'master' into next X-Git-Tag: 4.1~160 X-Git-Url: https://git.sur5r.net/?a=commitdiff_plain;h=ad568aa8c1d909e5e40cbc9e5a99900f8a1e0f2b;hp=1e18952d092620e84130712cc0620b518865d08e;p=i3%2Fi3 Merge branch 'master' into next --- diff --git a/DEPENDS b/DEPENDS index ea7133a5..a6c0986d 100644 --- a/DEPENDS +++ b/DEPENDS @@ -20,6 +20,7 @@ │ docbook-xml │ 4.5 │ 4.5 │ http://www.methods.co.nz/asciidoc/ │ │ libxcursor │ 1.1.11 │ 1.1.11 │ http://ftp.x.org/pub/current/src/lib/ │ │ Xlib │ 1.3.3 │ 1.4.3 │ http://ftp.x.org/pub/current/src/lib/ │ +│ PCRE │ 8.12 │ 8.12 │ http://www.pcre.org/ │ └─────────────┴────────┴────────┴────────────────────────────────────────┘ i3-msg, i3-input, i3-nagbar and i3-config-wizard do not introduce any new diff --git a/common.mk b/common.mk index ce41f287..62eb9958 100644 --- a/common.mk +++ b/common.mk @@ -49,6 +49,7 @@ CFLAGS += $(call cflags_for_lib, xcursor) CFLAGS += $(call cflags_for_lib, x11) CFLAGS += $(call cflags_for_lib, yajl) CFLAGS += $(call cflags_for_lib, libev) +CFLAGS += $(call cflags_for_lib, libpcre) CPPFLAGS += -DI3_VERSION=\"${GIT_VERSION}\" CPPFLAGS += -DSYSCONFDIR=\"${SYSCONFDIR}\" CPPFLAGS += -DTERM_EMU=\"$(TERM_EMU)\" @@ -70,6 +71,7 @@ LIBS += $(call ldflags_for_lib, xcursor, Xcursor) LIBS += $(call ldflags_for_lib, x11, X11) LIBS += $(call ldflags_for_lib, yajl, yajl) LIBS += $(call ldflags_for_lib, libev, ev) +LIBS += $(call ldflags_for_lib, libpcre, pcre) # Please test if -Wl,--as-needed works on your platform and send me a patch. # it is known not to work on Darwin (Mac OS X) diff --git a/debian/changelog b/debian/changelog index 990badb2..e670cd8f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,18 @@ -i3-wm (4.0.3-0) unstable; urgency=low +i3-wm (4.1-0) unstable; urgency=low * NOT YET RELEASED! + * Implement system tray support in i3bar (for NetworkManager, Skype, …) + * Implement support for PCRE regular expressions in criteria + * Implement a new assign syntax which uses criteria + * Sort named workspaces whose name starts with a number accordingly + * Warn on duplicate bindings for the same key + * Restrict 'resize' command to left/right for horizontal containers, up/down + for vertical containers + * Implement the GET_MARKS IPC request to get all marks + * Implement the new_float config option (border style for floating windows) + * Implement passing IPC sockets to i3 (systemd-style socket activation) + * Implement the 'move output' command to move containers to a specific output + * Bugfix: Preserve marks when restarting -- Michael Stapelberg Sun, 28 Aug 2011 20:17:31 +0200 diff --git a/debian/control b/debian/control index b546e650..da13231f 100644 --- a/debian/control +++ b/debian/control @@ -3,7 +3,7 @@ Section: utils Priority: extra Maintainer: Michael Stapelberg DM-Upload-Allowed: yes -Build-Depends: debhelper (>= 6), libx11-dev, libxcb-util0-dev (>= 0.3.8), libxcb-keysyms1-dev, libxcb-xinerama0-dev (>= 1.1), libxcb-randr0-dev, libxcb-icccm4-dev, libxcursor-dev, asciidoc (>= 8.4.4), xmlto, docbook-xml, pkg-config, libev-dev, flex, bison, libyajl-dev, perl, texlive-latex-base, texlive-latex-recommended, texlive-latex-extra +Build-Depends: debhelper (>= 6), libx11-dev, libxcb-util0-dev (>= 0.3.8), libxcb-keysyms1-dev, libxcb-xinerama0-dev (>= 1.1), libxcb-randr0-dev, libxcb-icccm4-dev, libxcursor-dev, asciidoc (>= 8.4.4), xmlto, docbook-xml, pkg-config, libev-dev, flex, bison, libyajl-dev, perl, texlive-latex-base, texlive-latex-recommended, texlive-latex-extra, libpcre3-dev Standards-Version: 3.9.2 Homepage: http://i3wm.org/ diff --git a/docs/ipc b/docs/ipc index 7e713260..4093ffce 100644 --- a/docs/ipc +++ b/docs/ipc @@ -59,6 +59,10 @@ GET_TREE (4):: Gets the layout tree. i3 uses a tree as data structure which includes every container. The reply will be the JSON-encoded tree (see the reply section). +GET_MARKS (5):: + Gets a list of marks (identifiers for containers to easily jump to them + later). The reply will be a JSON-encoded list of window marks (see + reply section). So, a typical message could look like this: -------------------------------------------------- @@ -110,6 +114,8 @@ GET_OUTPUTS (3):: Reply to the GET_OUTPUTS message. GET_TREE (4):: Reply to the GET_TREE message. +GET_MARKS (5):: + Reply to the GET_MARKS message. === COMMAND reply @@ -416,6 +422,16 @@ JSON dump: } ] } + + +=== GET_MARKS reply + +The reply consists of a single array of strings for each container that has a +mark. The order of that array is undefined. If more than one container has the +same mark, it will be represented multiple times in the reply (the array +contents are not unique). + +If no window has a mark the response will be the empty array []. ------------------------ diff --git a/docs/userguide b/docs/userguide index 27598bc1..184848aa 100644 --- a/docs/userguide +++ b/docs/userguide @@ -431,7 +431,7 @@ change their border style, for example. *Syntax*: ----------------------------- -for_window [criteria] command +for_window command ----------------------------- *Examples*: @@ -478,37 +478,59 @@ configuration file and run it before starting i3 (for example in your [[assign_workspace]] -Specific windows can be matched by window class and/or window title. It is -recommended that you match on window classes instead of window titles whenever -possible because some applications first create their window, and then worry -about setting the correct title. Firefox with Vimperator comes to mind. The -window starts up being named Firefox, and only when Vimperator is loaded does -the title change. As i3 will get the title as soon as the application maps the +To automatically make a specific window show up on a specific workspace, you +can use an *assignment*. You can match windows by using any criteria, +see <>. It is recommended that you match on window classes +(and instances, when appropriate) instead of window titles whenever possible +because some applications first create their window, and then worry about +setting the correct title. Firefox with Vimperator comes to mind. The window +starts up being named Firefox, and only when Vimperator is loaded does the +title change. 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. -You can prefix or suffix workspaces with a `~` to specify that matching clients -should be put into floating mode. If you specify only a `~`, the client will -not be put onto any workspace, but will be set floating on the current one. - *Syntax*: ------------------------------------------------------------ -assign ["]window class[/window title]["] [→] [workspace] +assign [→] workspace ------------------------------------------------------------ *Examples*: ---------------------- -assign urxvt 2 -assign urxvt → 2 -assign urxvt → work -assign "urxvt" → 2 -assign "urxvt/VIM" → 3 -assign "gecko" → 4 +# Assign URxvt terminals to workspace 2 +assign [class="URxvt"] 2 + +# Same thing, but more precise (exact match instead of substring) +assign [class="^URxvt$"] 2 + +# Same thing, but with a beautiful arrow :) +assign [class="^URxvt$"] → 2 + +# Assignment to a named workspace +assign [class="^URxvt$"] → work + +# Start urxvt -name irssi +assign [class="^URxvt$" instance="^irssi$"] → 3 ---------------------- Note that the arrow is not required, it just looks good :-). If you decide to use it, it has to be a UTF-8 encoded arrow, not `->` or something like that. +To get the class and instance, you can use +xprop+. After clicking on the +window, you will see the following output: + +*xwininfo*: +----------------------------------- +WM_CLASS(STRING) = "irssi", "URxvt" +----------------------------------- + +The first part of the WM_CLASS is the instance ("irssi" in this example), the +second part is the class ("URxvt" in this example). + +Should you have any problems with assignments, make sure to check the i3 +logfile first (see http://i3wm.org/docs/debugging.html). It includes more +details about the matching process and the window’s actual class, instance and +title when starting up. + === Automatically starting applications on i3 startup By using the +exec+ keyword outside a keybinding, you can configure @@ -721,6 +743,9 @@ which have the class Firefox, use: *Example*: ------------------------------------ bindsym mod+x [class="Firefox"] kill + +# same thing, but case-insensitive +bindsym mod+x [class="(?i)firefox"] kill ------------------------------------ The criteria which are currently implemented are: @@ -739,8 +764,9 @@ con_id:: Compares the i3-internal container ID, which you can get via the IPC interface. Handy for scripting. -Note that currently all criteria are compared case-insensitive and do not -support regular expressions. This is planned to change in the future. +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. === Splitting containers @@ -838,6 +864,11 @@ You can also switch to the next and previous workspace with the commands workspace 1, 3, 4 and 9 and you want to cycle through them with a single key combination. +To move a container to another xrandr output such as +LVDS1+ or +VGA1+, you can +use the +move output+ command followed by the name of the target output. You +may also use +left+, +right+, +up+, +down+ instead of the xrandr output name to +move to the the next output in the specified direction. + *Examples*: ------------------------- bindsym mod+1 workspace 1 diff --git a/i3-msg/main.c b/i3-msg/main.c index 630a345d..2d7cef0e 100644 --- a/i3-msg/main.c +++ b/i3-msg/main.c @@ -180,9 +180,11 @@ int main(int argc, char *argv[]) { message_type = I3_IPC_MESSAGE_TYPE_GET_OUTPUTS; else if (strcasecmp(optarg, "get_tree") == 0) message_type = I3_IPC_MESSAGE_TYPE_GET_TREE; + else if (strcasecmp(optarg, "get_marks") == 0) + message_type = I3_IPC_MESSAGE_TYPE_GET_MARKS; else { printf("Unknown message type\n"); - printf("Known types: command, get_workspaces, get_outputs, get_tree\n"); + printf("Known types: command, get_workspaces, get_outputs, get_tree, get_marks\n"); exit(EXIT_FAILURE); } } else if (o == 'q') { @@ -243,7 +245,7 @@ int main(int argc, char *argv[]) { uint32_t reply_length; uint8_t *reply; ipc_recv_message(sockfd, message_type, &reply_length, &reply); - printf("%.*s", reply_length, reply); + printf("%.*s\n", reply_length, reply); free(reply); close(sockfd); diff --git a/i3bar/include/common.h b/i3bar/include/common.h index 22e3ca43..74bd2152 100644 --- a/i3bar/include/common.h +++ b/i3bar/include/common.h @@ -9,18 +9,19 @@ #ifndef COMMON_H_ #define COMMON_H_ +#include + typedef struct rect_t rect; -typedef int bool; struct ev_loop* main_loop; char *statusline; char *statusline_buffer; struct rect_t { - int x; - int y; - int w; - int h; + int x; + int y; + int w; + int h; }; #include "queue.h" @@ -29,6 +30,7 @@ struct rect_t { #include "outputs.h" #include "util.h" #include "workspaces.h" +#include "trayclients.h" #include "xcb.h" #include "ucs2_to_utf8.h" #include "config.h" diff --git a/i3bar/include/outputs.h b/i3bar/include/outputs.h index f74048da..c6402a5b 100644 --- a/i3bar/include/outputs.h +++ b/i3bar/include/outputs.h @@ -22,33 +22,34 @@ struct outputs_head *outputs; * Start parsing the received json-string * */ -void parse_outputs_json(char* json); +void parse_outputs_json(char* json); /* * Initiate the output-list * */ -void init_outputs(); +void init_outputs(); /* * Returns the output with the given name * */ -i3_output* get_output_by_name(char* name); +i3_output* get_output_by_name(char* name); struct i3_output { - char* name; /* Name of the output */ - bool active; /* If the output is active */ - int ws; /* The number of the currently visible ws */ - rect rect; /* The rect (relative to the root-win) */ + char* name; /* Name of the output */ + bool active; /* If the output is active */ + int ws; /* The number of the currently visible ws */ + rect rect; /* The rect (relative to the root-win) */ - xcb_window_t bar; /* The id of the bar of the output */ - xcb_pixmap_t buffer; /* An extra pixmap for double-buffering */ - xcb_gcontext_t bargc; /* The graphical context of the bar */ + xcb_window_t bar; /* The id of the bar of the output */ + xcb_pixmap_t buffer; /* An extra pixmap for double-buffering */ + xcb_gcontext_t bargc; /* The graphical context of the bar */ - struct ws_head *workspaces; /* The workspaces on this output */ + struct ws_head *workspaces; /* The workspaces on this output */ + struct tc_head *trayclients; /* The tray clients on this output */ - SLIST_ENTRY(i3_output) slist; /* Pointer for the SLIST-Macro */ + SLIST_ENTRY(i3_output) slist; /* Pointer for the SLIST-Macro */ }; #endif diff --git a/i3bar/include/trayclients.h b/i3bar/include/trayclients.h new file mode 100644 index 00000000..1113daeb --- /dev/null +++ b/i3bar/include/trayclients.h @@ -0,0 +1,26 @@ +/* + * i3bar - an xcb-based status- and ws-bar for i3 + * + * © 2010-2011 Axel Wagner and contributors + * + * See file LICNSE for license information + * + */ +#ifndef TRAYCLIENT_H_ +#define TRAYCLIENT_H_ + +#include "common.h" + +typedef struct trayclient trayclient; + +TAILQ_HEAD(tc_head, trayclient); + +struct trayclient { + xcb_window_t win; /* The window ID of the tray client */ + bool mapped; /* Whether this window is mapped */ + int xe_version; /* The XEMBED version supported by the client */ + + TAILQ_ENTRY(trayclient) tailq; /* Pointer for the TAILQ-Macro */ +}; + +#endif diff --git a/i3bar/include/xcb.h b/i3bar/include/xcb.h index 531fdfe9..c1b7cc14 100644 --- a/i3bar/include/xcb.h +++ b/i3bar/include/xcb.h @@ -12,6 +12,18 @@ #include //#include "outputs.h" +#ifdef XCB_COMPAT +#define XCB_ATOM_CARDINAL CARDINAL +#endif + +#define _NET_SYSTEM_TRAY_ORIENTATION_HORZ 0 +#define _NET_SYSTEM_TRAY_ORIENTATION_VERT 1 +#define SYSTEM_TRAY_REQUEST_DOCK 0 +#define SYSTEM_TRAY_BEGIN_MESSAGE 1 +#define SYSTEM_TRAY_CANCEL_MESSAGE 2 +#define XEMBED_MAPPED (1 << 0) +#define XEMBED_EMBEDDED_NOTIFY 0 + struct xcb_color_strings_t { char *bar_fg; char *bar_bg; diff --git a/i3bar/include/xcb_atoms.def b/i3bar/include/xcb_atoms.def index 5d168873..b75ceabd 100644 --- a/i3bar/include/xcb_atoms.def +++ b/i3bar/include/xcb_atoms.def @@ -2,4 +2,10 @@ ATOM_DO(_NET_WM_WINDOW_TYPE) ATOM_DO(_NET_WM_WINDOW_TYPE_DOCK) ATOM_DO(_NET_WM_STRUT_PARTIAL) ATOM_DO(I3_SOCKET_PATH) +ATOM_DO(MANAGER) +ATOM_DO(_NET_SYSTEM_TRAY_ORIENTATION) +ATOM_DO(_NET_SYSTEM_TRAY_VISUAL) +ATOM_DO(_NET_SYSTEM_TRAY_OPCODE) +ATOM_DO(_XEMBED_INFO) +ATOM_DO(_XEMBED) #undef ATOM_DO diff --git a/i3bar/src/child.c b/i3bar/src/child.c index faab9142..aa9d6554 100644 --- a/i3bar/src/child.c +++ b/i3bar/src/child.c @@ -90,7 +90,7 @@ void stdin_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) { if (rec == buffer_len) { buffer_len += STDIN_CHUNK_SIZE; buffer = realloc(buffer, buffer_len); - } + } } if (*buffer == '\0') { FREE(buffer); diff --git a/i3bar/src/outputs.c b/i3bar/src/outputs.c index 9daf328d..464f24a0 100644 --- a/i3bar/src/outputs.c +++ b/i3bar/src/outputs.c @@ -43,7 +43,7 @@ static int outputs_null_cb(void *params_) { * Parse a boolean value (active) * */ -static int outputs_boolean_cb(void *params_, bool val) { +static int outputs_boolean_cb(void *params_, int val) { struct outputs_json_params *params = (struct outputs_json_params*) params_; if (strcmp(params->cur_key, "active")) { @@ -161,6 +161,9 @@ static int outputs_start_map_cb(void *params_) { new_output->workspaces = malloc(sizeof(struct ws_head)); TAILQ_INIT(new_output->workspaces); + new_output->trayclients = malloc(sizeof(struct tc_head)); + TAILQ_INIT(new_output->trayclients); + params->outputs_walk = new_output; return 1; diff --git a/i3bar/src/ucs2_to_utf8.c b/i3bar/src/ucs2_to_utf8.c index 8c79c3f9..68984227 100644 --- a/i3bar/src/ucs2_to_utf8.c +++ b/i3bar/src/ucs2_to_utf8.c @@ -23,18 +23,18 @@ static iconv_t conversion_descriptor2 = 0; * */ char *convert_ucs_to_utf8(char *input) { - size_t input_size = 2; - /* UTF-8 may consume up to 4 byte */ - int buffer_size = 8; + size_t input_size = 2; + /* UTF-8 may consume up to 4 byte */ + int buffer_size = 8; - char *buffer = calloc(buffer_size, 1); + char *buffer = calloc(buffer_size, 1); if (buffer == NULL) err(EXIT_FAILURE, "malloc() failed\n"); - size_t output_size = buffer_size; - /* We need to use an additional pointer, because iconv() modifies it */ - char *output = buffer; + size_t output_size = buffer_size; + /* We need to use an additional pointer, because iconv() modifies it */ + char *output = buffer; - /* We convert the input into UCS-2 big endian */ + /* We convert the input into UCS-2 big endian */ if (conversion_descriptor == 0) { conversion_descriptor = iconv_open("UTF-8", "UCS-2BE"); if (conversion_descriptor == 0) { @@ -43,17 +43,17 @@ char *convert_ucs_to_utf8(char *input) { } } - /* Get the conversion descriptor back to original state */ - iconv(conversion_descriptor, NULL, NULL, NULL, NULL); + /* Get the conversion descriptor back to original state */ + iconv(conversion_descriptor, NULL, NULL, NULL, NULL); - /* Convert our text */ - int rc = iconv(conversion_descriptor, (void*)&input, &input_size, &output, &output_size); + /* Convert our text */ + int rc = iconv(conversion_descriptor, (void*)&input, &input_size, &output, &output_size); if (rc == (size_t)-1) { perror("Converting to UCS-2 failed"); return NULL; - } + } - return buffer; + return buffer; } /* @@ -64,18 +64,18 @@ char *convert_ucs_to_utf8(char *input) { * */ char *convert_utf8_to_ucs2(char *input, int *real_strlen) { - size_t input_size = strlen(input) + 1; - /* UCS-2 consumes exactly two bytes for each glyph */ - int buffer_size = input_size * 2; + size_t input_size = strlen(input) + 1; + /* UCS-2 consumes exactly two bytes for each glyph */ + int buffer_size = input_size * 2; - char *buffer = malloc(buffer_size); + char *buffer = malloc(buffer_size); if (buffer == NULL) err(EXIT_FAILURE, "malloc() failed\n"); - size_t output_size = buffer_size; - /* We need to use an additional pointer, because iconv() modifies it */ - char *output = buffer; + size_t output_size = buffer_size; + /* We need to use an additional pointer, because iconv() modifies it */ + char *output = buffer; - /* We convert the input into UCS-2 big endian */ + /* We convert the input into UCS-2 big endian */ if (conversion_descriptor2 == 0) { conversion_descriptor2 = iconv_open("UCS-2BE", "UTF-8"); if (conversion_descriptor2 == 0) { @@ -84,20 +84,20 @@ char *convert_utf8_to_ucs2(char *input, int *real_strlen) { } } - /* Get the conversion descriptor back to original state */ - iconv(conversion_descriptor2, NULL, NULL, NULL, NULL); + /* Get the conversion descriptor back to original state */ + iconv(conversion_descriptor2, NULL, NULL, NULL, NULL); - /* Convert our text */ - int rc = iconv(conversion_descriptor2, (void*)&input, &input_size, &output, &output_size); + /* Convert our text */ + int rc = iconv(conversion_descriptor2, (void*)&input, &input_size, &output, &output_size); if (rc == (size_t)-1) { perror("Converting to UCS-2 failed"); if (real_strlen != NULL) - *real_strlen = 0; + *real_strlen = 0; return NULL; - } + } if (real_strlen != NULL) - *real_strlen = ((buffer_size - output_size) / 2) - 1; + *real_strlen = ((buffer_size - output_size) / 2) - 1; - return buffer; + return buffer; } diff --git a/i3bar/src/workspaces.c b/i3bar/src/workspaces.c index eeb9ca34..a84e152b 100644 --- a/i3bar/src/workspaces.c +++ b/i3bar/src/workspaces.c @@ -29,7 +29,7 @@ struct workspaces_json_params { * Parse a boolean value (visible, focused, urgent) * */ -static int workspaces_boolean_cb(void *params_, bool val) { +static int workspaces_boolean_cb(void *params_, int val) { struct workspaces_json_params *params = (struct workspaces_json_params*) params_; if (!strcmp(params->cur_key, "visible")) { diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index 51a2781b..dd6e39dc 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -63,6 +63,7 @@ xcb_atom_t atoms[NUM_ATOMS]; /* Variables, that are the same for all functions at all times */ xcb_connection_t *xcb_connection; +int screen; xcb_screen_t *xcb_screen; xcb_window_t xcb_root; xcb_font_t xcb_font; @@ -389,6 +390,256 @@ void handle_button(xcb_button_press_event_t *event) { i3_send_msg(I3_IPC_MESSAGE_TYPE_COMMAND, buffer); } +/* + * Configures the x coordinate of all trayclients. To be called after adding a + * new tray client or removing an old one. + * + */ +static void configure_trayclients() { + trayclient *trayclient; + i3_output *output; + SLIST_FOREACH(output, outputs, slist) { + if (!output->active) + continue; + + int clients = 0; + TAILQ_FOREACH_REVERSE(trayclient, output->trayclients, tc_head, tailq) { + clients++; + + DLOG("Configuring tray window %08x to x=%d\n", + trayclient->win, output->rect.w - (clients * (font_height + 2))); + uint32_t x = output->rect.w - (clients * (font_height + 2)); + xcb_configure_window(xcb_connection, + trayclient->win, + XCB_CONFIG_WINDOW_X, + &x); + } + } +} + +/* + * Handles ClientMessages (messages sent from another client directly to us). + * + * At the moment, only the tray window will receive client messages. All + * supported client messages currently are _NET_SYSTEM_TRAY_OPCODE. + * + */ +static void handle_client_message(xcb_client_message_event_t* event) { + if (event->type == atoms[_NET_SYSTEM_TRAY_OPCODE] && + event->format == 32) { + DLOG("_NET_SYSTEM_TRAY_OPCODE received\n"); + /* event->data.data32[0] is the timestamp */ + uint32_t op = event->data.data32[1]; + uint32_t mask; + uint32_t values[2]; + if (op == SYSTEM_TRAY_REQUEST_DOCK) { + xcb_window_t client = event->data.data32[2]; + + /* Listen for PropertyNotify events to get the most recent value of + * the XEMBED_MAPPED atom, also listen for UnmapNotify events */ + mask = XCB_CW_EVENT_MASK; + values[0] = XCB_EVENT_MASK_PROPERTY_CHANGE | + XCB_EVENT_MASK_STRUCTURE_NOTIFY; + xcb_change_window_attributes(xcb_connection, + client, + mask, + values); + + /* Request the _XEMBED_INFO property. The XEMBED specification + * (which is referred by the tray specification) says this *has* to + * be set, but VLC does not set it… */ + bool map_it = true; + int xe_version = 1; + xcb_get_property_cookie_t xembedc; + xembedc = xcb_get_property_unchecked(xcb_connection, + 0, + client, + atoms[_XEMBED_INFO], + XCB_GET_PROPERTY_TYPE_ANY, + 0, + 2 * 32); + + xcb_get_property_reply_t *xembedr = xcb_get_property_reply(xcb_connection, + xembedc, + NULL); + if (xembedr != NULL && xembedr->length != 0) { + DLOG("xembed format = %d, len = %d\n", xembedr->format, xembedr->length); + uint32_t *xembed = xcb_get_property_value(xembedr); + DLOG("xembed version = %d\n", xembed[0]); + DLOG("xembed flags = %d\n", xembed[1]); + map_it = ((xembed[1] & XEMBED_MAPPED) == XEMBED_MAPPED); + xe_version = xembed[0]; + if (xe_version > 1) + xe_version = 1; + free(xembedr); + } else { + ELOG("Window %08x violates the XEMBED protocol, _XEMBED_INFO not set\n", client); + } + + DLOG("X window %08x requested docking\n", client); + i3_output *walk, *output = NULL; + SLIST_FOREACH(walk, outputs, slist) { + if (!walk->active) + continue; + DLOG("using output %s\n", walk->name); + output = walk; + } + if (output == NULL) { + ELOG("No output found\n"); + return; + } + xcb_reparent_window(xcb_connection, + client, + output->bar, + output->rect.w - font_height - 2, + 2); + /* We reconfigure the window to use a reasonable size. The systray + * specification explicitly says: + * Tray icons may be assigned any size by the system tray, and + * should do their best to cope with any size effectively + */ + mask = XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT; + values[0] = font_height; + values[1] = font_height; + xcb_configure_window(xcb_connection, + client, + mask, + values); + + /* send the XEMBED_EMBEDDED_NOTIFY message */ + void *event = calloc(32, 1); + xcb_client_message_event_t *ev = event; + ev->response_type = XCB_CLIENT_MESSAGE; + ev->window = client; + ev->type = atoms[_XEMBED]; + ev->format = 32; + ev->data.data32[0] = XCB_CURRENT_TIME; + ev->data.data32[1] = atoms[XEMBED_EMBEDDED_NOTIFY]; + ev->data.data32[2] = output->bar; + ev->data.data32[3] = xe_version; + xcb_send_event(xcb_connection, + 0, + client, + XCB_EVENT_MASK_NO_EVENT, + (char*)ev); + free(event); + + if (map_it) { + DLOG("Mapping dock client\n"); + xcb_map_window(xcb_connection, client); + } else { + DLOG("Not mapping dock client yet\n"); + } + trayclient *tc = malloc(sizeof(trayclient)); + tc->win = client; + tc->mapped = map_it; + tc->xe_version = xe_version; + TAILQ_INSERT_TAIL(output->trayclients, tc, tailq); + + /* Trigger an update to copy the statusline text to the appropriate + * position */ + configure_trayclients(); + draw_bars(); + } + } +} + +/* + * Handles UnmapNotify events. These events happen when a tray window unmaps + * itself. We then update our data structure + * + */ +static void handle_unmap_notify(xcb_unmap_notify_event_t* event) { + DLOG("UnmapNotify for window = %08x, event = %08x\n", event->window, event->event); + + i3_output *walk; + SLIST_FOREACH(walk, outputs, slist) { + if (!walk->active) + continue; + DLOG("checking output %s\n", walk->name); + trayclient *trayclient; + TAILQ_FOREACH(trayclient, walk->trayclients, tailq) { + if (trayclient->win != event->window) + continue; + + DLOG("Removing tray client with window ID %08x\n", event->window); + TAILQ_REMOVE(walk->trayclients, trayclient, tailq); + + /* Trigger an update, we now have more space for the statusline */ + configure_trayclients(); + draw_bars(); + return; + } + } +} + +/* + * Handle PropertyNotify messages. Currently only the _XEMBED_INFO property is + * handled, which tells us whether a dock client should be mapped or unmapped. + * + */ +static void handle_property_notify(xcb_property_notify_event_t *event) { + DLOG("PropertyNotify\n"); + if (event->atom == atoms[_XEMBED_INFO] && + event->state == XCB_PROPERTY_NEW_VALUE) { + DLOG("xembed_info updated\n"); + trayclient *trayclient = NULL, *walk; + i3_output *o_walk; + SLIST_FOREACH(o_walk, outputs, slist) { + if (!o_walk->active) + continue; + + TAILQ_FOREACH(walk, o_walk->trayclients, tailq) { + if (walk->win != event->window) + continue; + trayclient = walk; + break; + } + + if (trayclient) + break; + } + if (!trayclient) { + ELOG("PropertyNotify received for unknown window %08x\n", + event->window); + return; + } + xcb_get_property_cookie_t xembedc; + xembedc = xcb_get_property_unchecked(xcb_connection, + 0, + trayclient->win, + atoms[_XEMBED_INFO], + XCB_GET_PROPERTY_TYPE_ANY, + 0, + 2 * 32); + + xcb_get_property_reply_t *xembedr = xcb_get_property_reply(xcb_connection, + xembedc, + NULL); + if (xembedr == NULL || xembedr->length == 0) + return; + + DLOG("xembed format = %d, len = %d\n", xembedr->format, xembedr->length); + uint32_t *xembed = xcb_get_property_value(xembedr); + DLOG("xembed version = %d\n", xembed[0]); + DLOG("xembed flags = %d\n", xembed[1]); + bool map_it = ((xembed[1] & XEMBED_MAPPED) == XEMBED_MAPPED); + DLOG("map-state now %d\n", map_it); + if (trayclient->mapped && !map_it) { + /* need to unmap the window */ + xcb_unmap_window(xcb_connection, trayclient->win); + trayclient->mapped = map_it; + draw_bars(); + } else if (!trayclient->mapped && map_it) { + /* need to map the window */ + xcb_map_window(xcb_connection, trayclient->win); + trayclient->mapped = map_it; + draw_bars(); + } + free(xembedr); + } +} + /* * This function is called immediately before the main loop locks. We flush xcb * then (and only then) @@ -419,6 +670,19 @@ void xcb_chk_cb(struct ev_loop *loop, ev_check *watcher, int revents) { /* Button-press-events are mouse-buttons clicked on one of our bars */ handle_button((xcb_button_press_event_t*) event); break; + case XCB_CLIENT_MESSAGE: + /* Client messages are used for client-to-client communication, for + * example system tray widgets talk to us directly via client messages. */ + handle_client_message((xcb_client_message_event_t*) event); + break; + case XCB_UNMAP_NOTIFY: + /* UnmapNotifies are received when a tray window unmaps itself */ + handle_unmap_notify((xcb_unmap_notify_event_t*) event); + break; + case XCB_PROPERTY_NOTIFY: + /* PropertyNotify */ + handle_property_notify((xcb_property_notify_event_t*) event); + break; } FREE(event); } @@ -476,7 +740,7 @@ void xkb_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) { */ char *init_xcb(char *fontname) { /* FIXME: xcb_connect leaks Memory */ - xcb_connection = xcb_connect(NULL, NULL); + xcb_connection = xcb_connect(NULL, &screen); if (xcb_connection_has_error(xcb_connection)) { ELOG("Cannot open display\n"); exit(EXIT_FAILURE); @@ -640,6 +904,95 @@ char *init_xcb(char *fontname) { return path; } +/* + * Initializes tray support by requesting the appropriate _NET_SYSTEM_TRAY atom + * for the X11 display we are running on, then acquiring the selection for this + * atom. Afterwards, tray clients will send ClientMessages to our window. + * + */ +void init_tray() { + /* request the tray manager atom for the X11 display we are running on */ + char atomname[strlen("_NET_SYSTEM_TRAY_S") + 11]; + snprintf(atomname, strlen("_NET_SYSTEM_TRAY_S") + 11, "_NET_SYSTEM_TRAY_S%d", screen); + xcb_intern_atom_cookie_t tray_cookie; + xcb_intern_atom_reply_t *tray_reply; + tray_cookie = xcb_intern_atom(xcb_connection, 0, strlen(atomname), atomname); + + /* tray support: we need a window to own the selection */ + xcb_window_t selwin = xcb_generate_id(xcb_connection); + uint32_t selmask = XCB_CW_OVERRIDE_REDIRECT; + uint32_t selval[] = { 1 }; + xcb_create_window(xcb_connection, + xcb_screen->root_depth, + selwin, + xcb_root, + -1, -1, + 1, 1, + 1, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + xcb_screen->root_visual, + selmask, + selval); + + uint32_t orientation = _NET_SYSTEM_TRAY_ORIENTATION_HORZ; + /* set the atoms */ + xcb_change_property(xcb_connection, + XCB_PROP_MODE_REPLACE, + selwin, + atoms[_NET_SYSTEM_TRAY_ORIENTATION], + XCB_ATOM_CARDINAL, + 32, + 1, + &orientation); + + if (!(tray_reply = xcb_intern_atom_reply(xcb_connection, tray_cookie, NULL))) { + ELOG("Could not get atom %s\n", atomname); + exit(EXIT_FAILURE); + } + + xcb_set_selection_owner(xcb_connection, + selwin, + tray_reply->atom, + XCB_CURRENT_TIME); + + /* Verify that we have the selection */ + xcb_get_selection_owner_cookie_t selcookie; + xcb_get_selection_owner_reply_t *selreply; + + selcookie = xcb_get_selection_owner(xcb_connection, tray_reply->atom); + if (!(selreply = xcb_get_selection_owner_reply(xcb_connection, selcookie, NULL))) { + ELOG("Could not get selection owner for %s\n", atomname); + exit(EXIT_FAILURE); + } + + if (selreply->owner != selwin) { + ELOG("Could not set the %s selection. " \ + "Maybe another tray is already running?\n", atomname); + /* NOTE that this error is not fatal. We just can’t provide tray + * functionality */ + free(selreply); + return; + } + + /* Inform clients waiting for a new _NET_SYSTEM_TRAY that we are here */ + void *event = calloc(32, 1); + xcb_client_message_event_t *ev = event; + ev->response_type = XCB_CLIENT_MESSAGE; + ev->window = xcb_root; + ev->type = atoms[MANAGER]; + ev->format = 32; + ev->data.data32[0] = XCB_CURRENT_TIME; + ev->data.data32[1] = tray_reply->atom; + ev->data.data32[2] = selwin; + xcb_send_event(xcb_connection, + 0, + xcb_root, + XCB_EVENT_MASK_STRUCTURE_NOTIFY, + (char*)ev); + free(event); + free(tray_reply); +} + /* * Cleanup the xcb-stuff. * Called once, before the program terminates. @@ -647,15 +1000,27 @@ char *init_xcb(char *fontname) { */ void clean_xcb() { i3_output *o_walk; + trayclient *trayclient; free_workspaces(); SLIST_FOREACH(o_walk, outputs, slist) { + TAILQ_FOREACH(trayclient, o_walk->trayclients, tailq) { + /* Unmap, then reparent (to root) the tray client windows */ + xcb_unmap_window(xcb_connection, trayclient->win); + xcb_reparent_window(xcb_connection, + trayclient->win, + xcb_root, + 0, + 0); + } destroy_window(o_walk); + FREE(o_walk->trayclients); FREE(o_walk->workspaces); FREE(o_walk->name); } FREE_SLIST(outputs, i3_output); FREE(outputs); + xcb_flush(xcb_connection); xcb_disconnect(xcb_connection); ev_check_stop(main_loop, xcb_chk); @@ -765,6 +1130,9 @@ void reconfig_windows() { if (walk->bar == XCB_NONE) { DLOG("Creating Window for output %s\n", walk->name); + /* TODO: only call init_tray() if the tray is configured for this output */ + init_tray(); + walk->bar = xcb_generate_id(xcb_connection); walk->buffer = xcb_generate_id(xcb_connection); mask = XCB_CW_BACK_PIXEL | XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK; @@ -954,13 +1322,26 @@ void draw_bars() { /* Luckily we already prepared a seperate pixmap containing the rendered * statusline, we just have to copy the relevant parts to the relevant * position */ + trayclient *trayclient; + int traypx = 0; + TAILQ_FOREACH(trayclient, outputs_walk->trayclients, tailq) { + if (!trayclient->mapped) + continue; + /* We assume the tray icons are quadratic (we use the font + * *height* as *width* of the icons) because we configured them + * like this. */ + traypx += font_height; + } + /* Add 2px of padding if there are any tray icons */ + if (traypx > 0) + traypx += 2; xcb_copy_area(xcb_connection, statusline_pm, outputs_walk->buffer, outputs_walk->bargc, MAX(0, (int16_t)(statusline_width - outputs_walk->rect.w + 4)), 0, - MAX(0, (int16_t)(outputs_walk->rect.w - statusline_width - 4)), 3, - MIN(outputs_walk->rect.w - 4, statusline_width), font_height); + MAX(0, (int16_t)(outputs_walk->rect.w - statusline_width - traypx - 4)), 3, + MIN(outputs_walk->rect.w - traypx - 4, statusline_width), font_height); } if (config.disable_ws) { diff --git a/include/all.h b/include/all.h index b87be518..9c08ebef 100644 --- a/include/all.h +++ b/include/all.h @@ -64,5 +64,6 @@ #include "output.h" #include "ewmh.h" #include "assignments.h" +#include "regex.h" #endif diff --git a/include/config.h b/include/config.h index 1021a612..3234b91e 100644 --- a/include/config.h +++ b/include/config.h @@ -126,6 +126,9 @@ struct Config { /** The default border style for new windows. */ border_style_t default_border; + /** The default border style for new floating windows. */ + border_style_t default_floating_border; + /** The modifier which needs to be pressed in combination with your mouse * buttons to do things with floating windows (move, resize) */ uint32_t floating_modifier; diff --git a/include/data.h b/include/data.h index 5797b7d8..1db7c442 100644 --- a/include/data.h +++ b/include/data.h @@ -10,6 +10,7 @@ #include #include #include +#include #ifndef _DATA_H #define _DATA_H @@ -137,6 +138,21 @@ struct Ignore_Event { SLIST_ENTRY(Ignore_Event) ignore_events; }; +/** + * Regular expression wrapper. It contains the pattern itself as a string (like + * ^foo[0-9]$) as well as a pointer to the compiled PCRE expression and the + * pcre_extra data returned by pcre_study(). + * + * This makes it easier to have a useful logfile, including the matching or + * non-matching pattern. + * + */ +struct regex { + char *pattern; + pcre *regex; + pcre_extra *extra; +}; + /****************************************************************************** * Major types *****************************************************************************/ @@ -277,12 +293,11 @@ struct Window { }; struct Match { - char *title; - int title_len; - char *application; - char *class; - char *instance; - char *mark; + struct regex *title; + struct regex *application; + struct regex *class; + struct regex *instance; + struct regex *mark; enum { M_DONTCHECK = -1, M_NODOCK = 0, diff --git a/include/i3/ipc.h b/include/i3/ipc.h index e81f9a15..30b2d304 100644 --- a/include/i3/ipc.h +++ b/include/i3/ipc.h @@ -38,6 +38,8 @@ /** Requests the tree layout from i3 */ #define I3_IPC_MESSAGE_TYPE_GET_TREE 4 +/** Request the current defined marks from i3 */ +#define I3_IPC_MESSAGE_TYPE_GET_MARKS 5 /* * Messages from i3 to clients @@ -59,6 +61,8 @@ /** Tree reply type */ #define I3_IPC_REPLY_TYPE_TREE 4 +/** Marks reply type*/ +#define I3_IPC_REPLY_TYPE_MARKS 5 /* * Events from i3 to clients. Events have the first bit set high. diff --git a/include/match.h b/include/match.h index 2786c66a..6c0694ef 100644 --- a/include/match.h +++ b/include/match.h @@ -28,4 +28,10 @@ void match_copy(Match *dest, Match *src); */ bool match_matches_window(Match *match, i3Window *window); +/** + * Frees the given match. It must not be used afterwards! + * + */ +void match_free(Match *match); + #endif diff --git a/include/regex.h b/include/regex.h new file mode 100644 index 00000000..adfa6656 --- /dev/null +++ b/include/regex.h @@ -0,0 +1,34 @@ +/* + * vim:ts=4:sw=4:expandtab + * + */ +#ifndef _REGEX_H +#define _REGEX_H + +/** + * Creates a new 'regex' struct containing the given pattern and a PCRE + * compiled regular expression. Also, calls pcre_study because this regex will + * most likely be used often (like for every new window and on every relevant + * property change of existing windows). + * + * Returns NULL if the pattern could not be compiled into a regular expression + * (and ELOGs an appropriate error message). + * + */ +struct regex *regex_new(const char *pattern); + +/** + * Frees the given regular expression. It must not be used afterwards! + * + */ +void regex_free(struct regex *regex); + +/** + * Checks if the given regular expression matches the given input and returns + * true if it does. In either case, it logs the outcome using LOG(), so it will + * be visible without any debug loglevel. + * + */ +bool regex_matches(struct regex *regex, const char *input); + +#endif diff --git a/include/sd-daemon.h b/include/sd-daemon.h new file mode 100644 index 00000000..4b853a15 --- /dev/null +++ b/include/sd-daemon.h @@ -0,0 +1,265 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +#ifndef foosddaemonhfoo +#define foosddaemonhfoo + +/*** + Copyright 2010 Lennart Poettering + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +***/ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + Reference implementation of a few systemd related interfaces for + writing daemons. These interfaces are trivial to implement. To + simplify porting we provide this reference implementation. + Applications are welcome to reimplement the algorithms described + here if they do not want to include these two source files. + + The following functionality is provided: + + - Support for logging with log levels on stderr + - File descriptor passing for socket-based activation + - Daemon startup and status notification + - Detection of systemd boots + + You may compile this with -DDISABLE_SYSTEMD to disable systemd + support. This makes all those calls NOPs that are directly related to + systemd (i.e. only sd_is_xxx() will stay useful). + + Since this is drop-in code we don't want any of our symbols to be + exported in any case. Hence we declare hidden visibility for all of + them. + + You may find an up-to-date version of these source files online: + + http://cgit.freedesktop.org/systemd/plain/src/sd-daemon.h + http://cgit.freedesktop.org/systemd/plain/src/sd-daemon.c + + This should compile on non-Linux systems, too, but with the + exception of the sd_is_xxx() calls all functions will become NOPs. + + See sd-daemon(7) for more information. +*/ + +#ifndef _sd_printf_attr_ +#if __GNUC__ >= 4 +#define _sd_printf_attr_(a,b) __attribute__ ((format (printf, a, b))) +#else +#define _sd_printf_attr_(a,b) +#endif +#endif + +#ifndef _sd_hidden_ +#if (__GNUC__ >= 4) && !defined(SD_EXPORT_SYMBOLS) +#define _sd_hidden_ __attribute__ ((visibility("hidden"))) +#else +#define _sd_hidden_ +#endif +#endif + +/* + Log levels for usage on stderr: + + fprintf(stderr, SD_NOTICE "Hello World!\n"); + + This is similar to printk() usage in the kernel. +*/ +#define SD_EMERG "<0>" /* system is unusable */ +#define SD_ALERT "<1>" /* action must be taken immediately */ +#define SD_CRIT "<2>" /* critical conditions */ +#define SD_ERR "<3>" /* error conditions */ +#define SD_WARNING "<4>" /* warning conditions */ +#define SD_NOTICE "<5>" /* normal but significant condition */ +#define SD_INFO "<6>" /* informational */ +#define SD_DEBUG "<7>" /* debug-level messages */ + +/* The first passed file descriptor is fd 3 */ +#define SD_LISTEN_FDS_START 3 + +/* + Returns how many file descriptors have been passed, or a negative + errno code on failure. Optionally, removes the $LISTEN_FDS and + $LISTEN_PID file descriptors from the environment (recommended, but + problematic in threaded environments). If r is the return value of + this function you'll find the file descriptors passed as fds + SD_LISTEN_FDS_START to SD_LISTEN_FDS_START+r-1. Returns a negative + errno style error code on failure. This function call ensures that + the FD_CLOEXEC flag is set for the passed file descriptors, to make + sure they are not passed on to child processes. If FD_CLOEXEC shall + not be set, the caller needs to unset it after this call for all file + descriptors that are used. + + See sd_listen_fds(3) for more information. +*/ +int sd_listen_fds(int unset_environment) _sd_hidden_; + +/* + Helper call for identifying a passed file descriptor. Returns 1 if + the file descriptor is a FIFO in the file system stored under the + specified path, 0 otherwise. If path is NULL a path name check will + not be done and the call only verifies if the file descriptor + refers to a FIFO. Returns a negative errno style error code on + failure. + + See sd_is_fifo(3) for more information. +*/ +int sd_is_fifo(int fd, const char *path) _sd_hidden_; + +/* + Helper call for identifying a passed file descriptor. Returns 1 if + the file descriptor is a socket of the specified family (AF_INET, + ...) and type (SOCK_DGRAM, SOCK_STREAM, ...), 0 otherwise. If + family is 0 a socket family check will not be done. If type is 0 a + socket type check will not be done and the call only verifies if + the file descriptor refers to a socket. If listening is > 0 it is + verified that the socket is in listening mode. (i.e. listen() has + been called) If listening is == 0 it is verified that the socket is + not in listening mode. If listening is < 0 no listening mode check + is done. Returns a negative errno style error code on failure. + + See sd_is_socket(3) for more information. +*/ +int sd_is_socket(int fd, int family, int type, int listening) _sd_hidden_; + +/* + Helper call for identifying a passed file descriptor. Returns 1 if + the file descriptor is an Internet socket, of the specified family + (either AF_INET or AF_INET6) and the specified type (SOCK_DGRAM, + SOCK_STREAM, ...), 0 otherwise. If version is 0 a protocol version + check is not done. If type is 0 a socket type check will not be + done. If port is 0 a socket port check will not be done. The + listening flag is used the same way as in sd_is_socket(). Returns a + negative errno style error code on failure. + + See sd_is_socket_inet(3) for more information. +*/ +int sd_is_socket_inet(int fd, int family, int type, int listening, uint16_t port) _sd_hidden_; + +/* + Helper call for identifying a passed file descriptor. Returns 1 if + the file descriptor is an AF_UNIX socket of the specified type + (SOCK_DGRAM, SOCK_STREAM, ...) and path, 0 otherwise. If type is 0 + a socket type check will not be done. If path is NULL a socket path + check will not be done. For normal AF_UNIX sockets set length to + 0. For abstract namespace sockets set length to the length of the + socket name (including the initial 0 byte), and pass the full + socket path in path (including the initial 0 byte). The listening + flag is used the same way as in sd_is_socket(). Returns a negative + errno style error code on failure. + + See sd_is_socket_unix(3) for more information. +*/ +int sd_is_socket_unix(int fd, int type, int listening, const char *path, size_t length) _sd_hidden_; + +/* + Informs systemd about changed daemon state. This takes a number of + newline separated environment-style variable assignments in a + string. The following variables are known: + + READY=1 Tells systemd that daemon startup is finished (only + relevant for services of Type=notify). The passed + argument is a boolean "1" or "0". Since there is + little value in signaling non-readiness the only + value daemons should send is "READY=1". + + STATUS=... Passes a single-line status string back to systemd + that describes the daemon state. This is free-from + and can be used for various purposes: general state + feedback, fsck-like programs could pass completion + percentages and failing programs could pass a human + readable error message. Example: "STATUS=Completed + 66% of file system check..." + + ERRNO=... If a daemon fails, the errno-style error code, + formatted as string. Example: "ERRNO=2" for ENOENT. + + BUSERROR=... If a daemon fails, the D-Bus error-style error + code. Example: "BUSERROR=org.freedesktop.DBus.Error.TimedOut" + + MAINPID=... The main pid of a daemon, in case systemd did not + fork off the process itself. Example: "MAINPID=4711" + + Daemons can choose to send additional variables. However, it is + recommended to prefix variable names not listed above with X_. + + Returns a negative errno-style error code on failure. Returns > 0 + if systemd could be notified, 0 if it couldn't possibly because + systemd is not running. + + Example: When a daemon finished starting up, it could issue this + call to notify systemd about it: + + sd_notify(0, "READY=1"); + + See sd_notifyf() for more complete examples. + + See sd_notify(3) for more information. +*/ +int sd_notify(int unset_environment, const char *state) _sd_hidden_; + +/* + Similar to sd_notify() but takes a format string. + + Example 1: A daemon could send the following after initialization: + + sd_notifyf(0, "READY=1\n" + "STATUS=Processing requests...\n" + "MAINPID=%lu", + (unsigned long) getpid()); + + Example 2: A daemon could send the following shortly before + exiting, on failure: + + sd_notifyf(0, "STATUS=Failed to start up: %s\n" + "ERRNO=%i", + strerror(errno), + errno); + + See sd_notifyf(3) for more information. +*/ +int sd_notifyf(int unset_environment, const char *format, ...) _sd_printf_attr_(2,3) _sd_hidden_; + +/* + Returns > 0 if the system was booted with systemd. Returns < 0 on + error. Returns 0 if the system was not booted with systemd. Note + that all of the functions above handle non-systemd boots just + fine. You should NOT protect them with a call to this function. Also + note that this function checks whether the system, not the user + session is controlled by systemd. However the functions above work + for both user and system services. + + See sd_booted(3) for more information. +*/ +int sd_booted(void) _sd_hidden_; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/cfgparse.l b/src/cfgparse.l index 4cf1a1c3..e5dd29c0 100644 --- a/src/cfgparse.l +++ b/src/cfgparse.l @@ -75,6 +75,14 @@ EOL (\r?\n) "]" { yy_pop_state(); return ']'; } +"[" { + /* this is the case for the new assign syntax + * that uses criteria */ + yy_pop_state(); + yy_push_state(FOR_WINDOW_COND); + /* afterwards we will be in ASSIGN_TARGET_COND */ + return '['; + } [ \t]* { yy_pop_state(); } \"[^\"]+\" { yy_pop_state(); @@ -98,13 +106,6 @@ bindsym { yy_push_state(BINDSYM_COND); yy_push_state(EAT floating_modifier { BEGIN(INITIAL); return TOKFLOATING_MODIFIER; } workspace { BEGIN(INITIAL); return TOKWORKSPACE; } output { yy_push_state(OUTPUT_COND); yy_push_state(EAT_WHITESPACE); return TOKOUTPUT; } -screen { - /* for compatibility until v3.φ */ - ELOG("Assignments to screens are DEPRECATED and will not work. " \ - "Please replace them with assignments to outputs.\n"); - yy_push_state(OUTPUT_COND); yy_push_state(EAT_WHITESPACE); - return TOKOUTPUT; - } terminal { WS_STRING; return TOKTERMINAL; } font { WS_STRING; return TOKFONT; } assign { yy_push_state(ASSIGN_TARGET_COND); yy_push_state(ASSIGN_COND); return TOKASSIGN; } @@ -118,6 +119,7 @@ vertical { return TOK_VERT; } auto { return TOK_AUTO; } workspace_layout { return TOK_WORKSPACE_LAYOUT; } new_window { return TOKNEWWINDOW; } +new_float { return TOKNEWFLOAT; } normal { return TOK_NORMAL; } none { return TOK_NONE; } 1pixel { return TOK_1PIXEL; } @@ -193,7 +195,7 @@ title { yy_push_state(WANT_QSTRING); return TOK_TITLE; yylval.string = copy; return QUOTEDSTRING; } -[^ \t\"]+ { BEGIN(ASSIGN_TARGET_COND); yylval.string = sstrdup(yytext); return STR_NG; } +[^ \t\"\[]+ { BEGIN(ASSIGN_TARGET_COND); yylval.string = sstrdup(yytext); return STR_NG; } [a-zA-Z0-9_]+ { yylval.string = sstrdup(yytext); return WORD; } [a-zA-Z]+ { yylval.string = sstrdup(yytext); return WORD; } . { return (int)yytext[0]; } diff --git a/src/cfgparse.y b/src/cfgparse.y index 10ca48cc..8faeff85 100644 --- a/src/cfgparse.y +++ b/src/cfgparse.y @@ -240,6 +240,18 @@ static void nagbar_exited(EV_P_ ev_child *watcher, int revents) { configerror_pid = -1; } +/* + * Cleanup handler. Will be called when i3 exits. Kills i3-nagbar with signal + * SIGKILL (9) to make sure there are no left-over i3-nagbar processes. + * + */ +static void nagbar_cleanup(EV_P_ ev_cleanup *watcher, int revent) { + if (configerror_pid != -1) { + LOG("Sending SIGKILL (9) to i3-nagbar with PID %d\n", configerror_pid); + kill(configerror_pid, SIGKILL); + } +} + /* * Starts an i3-nagbar process which alerts the user that his configuration * file contains one or more errors. Also offers two buttons: One to launch an @@ -283,6 +295,12 @@ static void start_configerror_nagbar(const char *config_path) { ev_child *child = smalloc(sizeof(ev_child)); ev_child_init(child, &nagbar_exited, configerror_pid, 0); ev_child_start(main_loop, child); + + /* install a cleanup watcher (will be called when i3 exits and i3-nagbar is + * still running) */ + ev_cleanup *cleanup = smalloc(sizeof(ev_cleanup)); + ev_cleanup_init(cleanup, nagbar_cleanup); + ev_cleanup_start(main_loop, cleanup); } /* @@ -311,6 +329,53 @@ void kill_configerror_nagbar(bool wait_for_it) { waitpid(configerror_pid, NULL, 0); } +/* + * Checks for duplicate key bindings (the same keycode or keysym is configured + * more than once). If a duplicate binding is found, a message is printed to + * stderr and the has_errors variable is set to true, which will start + * i3-nagbar. + * + */ +static void check_for_duplicate_bindings(struct context *context) { + Binding *bind, *current; + TAILQ_FOREACH(current, bindings, bindings) { + TAILQ_FOREACH(bind, bindings, bindings) { + /* Abort when we reach the current keybinding, only check the + * bindings before */ + if (bind == current) + break; + + /* Check if one is using keysym while the other is using bindsym. + * If so, skip. */ + /* XXX: It should be checked at a later place (when translating the + * keysym to keycodes) if there are any duplicates */ + if ((bind->symbol == NULL && current->symbol != NULL) || + (bind->symbol != NULL && current->symbol == NULL)) + continue; + + /* If bind is NULL, current has to be NULL, too (see above). + * If the keycodes differ, it can't be a duplicate. */ + if (bind->symbol != NULL && + strcasecmp(bind->symbol, current->symbol) != 0) + continue; + + /* Check if the keycodes or modifiers are different. If so, they + * can't be duplicate */ + if (bind->keycode != current->keycode || + bind->mods != current->mods) + continue; + context->has_errors = true; + if (current->keycode != 0) { + ELOG("Duplicate keybinding in config file:\n modmask %d with keycode %d, command \"%s\"\n", + current->mods, current->keycode, current->command); + } else { + ELOG("Duplicate keybinding in config file:\n modmask %d with keysym %s, command \"%s\"\n", + current->mods, current->symbol, current->command); + } + } + } +} + void parse_file(const char *f) { SLIST_HEAD(variables_head, Variable) variables = SLIST_HEAD_INITIALIZER(&variables); int fd, ret, read_bytes = 0; @@ -469,6 +534,8 @@ void parse_file(const char *f) { exit(1); } + check_for_duplicate_bindings(context); + if (context->has_errors) { start_configerror_nagbar(f); } @@ -536,6 +603,7 @@ void parse_file(const char *f) { %token TOK_AUTO "auto" %token TOK_WORKSPACE_LAYOUT "workspace_layout" %token TOKNEWWINDOW "new_window" +%token TOKNEWFLOAT "new_float" %token TOK_NORMAL "normal" %token TOK_NONE "none" %token TOK_1PIXEL "1pixel" @@ -567,6 +635,7 @@ void parse_file(const char *f) { %type layout_mode %type border_style %type new_window +%type new_float %type colorpixel %type bool %type popup_setting @@ -591,6 +660,7 @@ line: | orientation | workspace_layout | new_window + | new_float | focus_follows_mouse | force_focus_wrapping | workspace_bar @@ -703,12 +773,14 @@ criterion: TOK_CLASS '=' STR { printf("criteria: class = %s\n", $3); - current_match.class = $3; + current_match.class = regex_new($3); + free($3); } | TOK_INSTANCE '=' STR { printf("criteria: instance = %s\n", $3); - current_match.instance = $3; + current_match.instance = regex_new($3); + free($3); } | TOK_CON_ID '=' STR { @@ -743,12 +815,14 @@ criterion: | TOK_MARK '=' STR { printf("criteria: mark = %s\n", $3); - current_match.mark = $3; + current_match.mark = regex_new($3); + free($3); } | TOK_TITLE '=' STR { printf("criteria: title = %s\n", $3); - current_match.title = $3; + current_match.title = regex_new($3); + free($3); } ; @@ -885,6 +959,14 @@ new_window: } ; +new_float: + TOKNEWFLOAT border_style + { + DLOG("new floating windows should start with border style %d\n", $2); + config.default_floating_border = $2; + } + ; + border_style: TOK_NORMAL { $$ = BS_NORMAL; } | TOK_NONE { $$ = BS_NONE; } @@ -998,6 +1080,13 @@ workspace_name: assign: TOKASSIGN window_class STR { + /* This is the old, deprecated form of assignments. It’s provided for + * compatibility in version (4.1, 4.2, 4.3) and will be removed + * afterwards. It triggers an i3-nagbar warning starting from 4.1. */ + ELOG("You are using the old assign syntax (without criteria). " + "Please see the User's Guide for the new syntax and fix " + "your config file.\n"); + context->has_errors = true; printf("assignment of %s to *%s*\n", $2, $3); char *workspace = $3; char *criteria = $2; @@ -1009,15 +1098,27 @@ assign: char *separator = NULL; if ((separator = strchr(criteria, '/')) != NULL) { *(separator++) = '\0'; - match->title = sstrdup(separator); + char *pattern; + if (asprintf(&pattern, "(?i)%s", separator) == -1) { + ELOG("asprintf failed\n"); + break; + } + match->title = regex_new(pattern); + free(pattern); + printf(" title = %s\n", separator); + } + if (*criteria != '\0') { + char *pattern; + if (asprintf(&pattern, "(?i)%s", criteria) == -1) { + ELOG("asprintf failed\n"); + break; + } + match->class = regex_new(pattern); + free(pattern); + printf(" class = %s\n", criteria); } - if (*criteria != '\0') - match->class = sstrdup(criteria); free(criteria); - printf(" class = %s\n", match->class); - printf(" title = %s\n", match->title); - /* Compatibility with older versions: If the assignment target starts * with ~, we create the equivalent of: * @@ -1045,6 +1146,19 @@ assign: assignment->dest.workspace = workspace; TAILQ_INSERT_TAIL(&assignments, assignment, assignments); } + | TOKASSIGN match STR + { + if (match_is_empty(¤t_match)) { + ELOG("Match is empty, ignoring this assignment\n"); + break; + } + printf("new assignment, using above criteria, to workspace %s\n", $3); + Assignment *assignment = scalloc(sizeof(Assignment)); + assignment->match = current_match; + assignment->type = A_TO_WORKSPACE; + assignment->dest.workspace = $3; + TAILQ_INSERT_TAIL(&assignments, assignment, assignments); + } ; window_class: diff --git a/src/cmdparse.l b/src/cmdparse.l index 6c756b0d..c7c64e35 100644 --- a/src/cmdparse.l +++ b/src/cmdparse.l @@ -123,6 +123,7 @@ floating { return TOK_FLOATING; } toggle { return TOK_TOGGLE; } mode_toggle { return TOK_MODE_TOGGLE; } workspace { WS_STRING; return TOK_WORKSPACE; } +output { WS_STRING; return TOK_OUTPUT; } focus { return TOK_FOCUS; } move { return TOK_MOVE; } open { return TOK_OPEN; } diff --git a/src/cmdparse.y b/src/cmdparse.y index 9ea82efd..174b5e05 100644 --- a/src/cmdparse.y +++ b/src/cmdparse.y @@ -149,6 +149,7 @@ bool definitelyGreaterThan(float a, float b, float epsilon) { %token TOK_ENABLE "enable" %token TOK_DISABLE "disable" %token TOK_WORKSPACE "workspace" +%token TOK_OUTPUT "output" %token TOK_TOGGLE "toggle" %token TOK_FOCUS "focus" %token TOK_MOVE "move" @@ -266,10 +267,9 @@ matchend: } } else if (current_match.mark != NULL && current->con->mark != NULL && - strcasecmp(current_match.mark, current->con->mark) == 0) { + regex_matches(current_match.mark, current->con->mark)) { printf("match by mark\n"); - TAILQ_INSERT_TAIL(&owindows, current, owindows); - + TAILQ_INSERT_TAIL(&owindows, current, owindows); } else { if (current->con->window == NULL) continue; @@ -299,12 +299,14 @@ criterion: TOK_CLASS '=' STR { printf("criteria: class = %s\n", $3); - current_match.class = $3; + current_match.class = regex_new($3); + free($3); } | TOK_INSTANCE '=' STR { printf("criteria: instance = %s\n", $3); - current_match.instance = $3; + current_match.instance = regex_new($3); + free($3); } | TOK_CON_ID '=' STR { @@ -339,12 +341,14 @@ criterion: | TOK_MARK '=' STR { printf("criteria: mark = %s\n", $3); - current_match.mark = $3; + current_match.mark = regex_new($3); + free($3); } | TOK_TITLE '=' STR { printf("criteria: title = %s\n", $3); - current_match.title = $3; + current_match.title = regex_new($3); + free($3); } ; @@ -704,6 +708,51 @@ move: tree_render(); } + | TOK_MOVE TOK_OUTPUT STR + { + owindow *current; + + printf("should move window to output %s", $3); + + HANDLE_EMPTY_MATCH; + + /* get the output */ + Output *current_output = NULL; + Output *output; + + TAILQ_FOREACH(current, &owindows, owindows) + current_output = get_output_containing(current->con->rect.x, current->con->rect.y); + + assert(current_output != NULL); + + if (strcasecmp($3, "up") == 0) + output = get_output_next(D_UP, current_output); + else if (strcasecmp($3, "down") == 0) + output = get_output_next(D_DOWN, current_output); + else if (strcasecmp($3, "left") == 0) + output = get_output_next(D_LEFT, current_output); + else if (strcasecmp($3, "right") == 0) + output = get_output_next(D_RIGHT, current_output); + else + output = get_output_by_name($3); + free($3); + + if (!output) + break; + + /* get visible workspace on output */ + Con *ws = NULL; + GREP_FIRST(ws, output_get_content(output->con), workspace_is_visible(child)); + if (!ws) + break; + + TAILQ_FOREACH(current, &owindows, owindows) { + printf("matching: %p / %s\n", current->con, current->con->name); + con_move_to_workspace(current->con, ws, false); + } + + tree_render(); + } ; append_layout: @@ -811,6 +860,17 @@ resize: double percentage = 1.0 / children; LOG("default percentage = %f\n", percentage); + orientation_t orientation = current->parent->orientation; + + if ((orientation == HORIZ && + (direction == TOK_UP || direction == TOK_DOWN)) || + (orientation == VERT && + (direction == TOK_LEFT || direction == TOK_RIGHT))) { + LOG("You cannot resize in that direction. Your focus is in a %s split container currently.\n", + (orientation == HORIZ ? "horizontal" : "vertical")); + break; + } + if (direction == TOK_UP || direction == TOK_LEFT) { other = TAILQ_PREV(current, nodes_head, nodes); } else { @@ -818,7 +878,7 @@ resize: } if (other == TAILQ_END(workspaces)) { LOG("No other container in this direction found, cannot resize.\n"); - return 0; + break; } LOG("other->percent = %f\n", other->percent); LOG("current->percent before = %f\n", current->percent); diff --git a/src/config.c b/src/config.c index 14fc6e02..c979d8cd 100644 --- a/src/config.c +++ b/src/config.c @@ -257,111 +257,116 @@ static void parse_configuration(const char *override_configpath) { * */ void load_configuration(xcb_connection_t *conn, const char *override_configpath, bool reload) { - if (reload) { - /* First ungrab the keys */ - ungrab_all_keys(conn); + if (reload) { + /* First ungrab the keys */ + ungrab_all_keys(conn); - struct Mode *mode; - Binding *bind; - while (!SLIST_EMPTY(&modes)) { - mode = SLIST_FIRST(&modes); - FREE(mode->name); - - /* Clear the old binding list */ - bindings = mode->bindings; - while (!TAILQ_EMPTY(bindings)) { - bind = TAILQ_FIRST(bindings); - TAILQ_REMOVE(bindings, bind, bindings); - FREE(bind->translated_to); - FREE(bind->command); - FREE(bind); - } - FREE(bindings); - SLIST_REMOVE(&modes, mode, Mode, modes); - } + struct Mode *mode; + Binding *bind; + while (!SLIST_EMPTY(&modes)) { + mode = SLIST_FIRST(&modes); + FREE(mode->name); + + /* Clear the old binding list */ + bindings = mode->bindings; + while (!TAILQ_EMPTY(bindings)) { + bind = TAILQ_FIRST(bindings); + TAILQ_REMOVE(bindings, bind, bindings); + FREE(bind->translated_to); + FREE(bind->command); + FREE(bind); + } + FREE(bindings); + SLIST_REMOVE(&modes, mode, Mode, modes); + } -#if 0 - struct Assignment *assign; - while (!TAILQ_EMPTY(&assignments)) { - assign = TAILQ_FIRST(&assignments); - FREE(assign->windowclass_title); - TAILQ_REMOVE(&assignments, assign, assignments); - FREE(assign); - } -#endif + struct Assignment *assign; + while (!TAILQ_EMPTY(&assignments)) { + assign = TAILQ_FIRST(&assignments); + if (assign->type == A_TO_WORKSPACE) + FREE(assign->dest.workspace); + else if (assign->type == A_TO_OUTPUT) + FREE(assign->dest.output); + else if (assign->type == A_COMMAND) + FREE(assign->dest.command); + match_free(&(assign->match)); + TAILQ_REMOVE(&assignments, assign, assignments); + FREE(assign); + } - /* Clear workspace names */ + /* Clear workspace names */ #if 0 - Workspace *ws; - TAILQ_FOREACH(ws, workspaces, workspaces) - workspace_set_name(ws, NULL); + Workspace *ws; + TAILQ_FOREACH(ws, workspaces, workspaces) + workspace_set_name(ws, NULL); #endif - } + } - SLIST_INIT(&modes); + SLIST_INIT(&modes); - struct Mode *default_mode = scalloc(sizeof(struct Mode)); - default_mode->name = sstrdup("default"); - default_mode->bindings = scalloc(sizeof(struct bindings_head)); - TAILQ_INIT(default_mode->bindings); - SLIST_INSERT_HEAD(&modes, default_mode, modes); + struct Mode *default_mode = scalloc(sizeof(struct Mode)); + default_mode->name = sstrdup("default"); + default_mode->bindings = scalloc(sizeof(struct bindings_head)); + TAILQ_INIT(default_mode->bindings); + SLIST_INSERT_HEAD(&modes, default_mode, modes); - bindings = default_mode->bindings; + bindings = default_mode->bindings; #define REQUIRED_OPTION(name) \ - if (config.name == NULL) \ - die("You did not specify required configuration option " #name "\n"); + if (config.name == NULL) \ + die("You did not specify required configuration option " #name "\n"); - /* Clear the old config or initialize the data structure */ - memset(&config, 0, sizeof(config)); + /* Clear the old config or initialize the data structure */ + memset(&config, 0, sizeof(config)); - /* Initialize default colors */ + /* Initialize default colors */ #define INIT_COLOR(x, cborder, cbackground, ctext) \ - do { \ - x.border = get_colorpixel(cborder); \ - x.background = get_colorpixel(cbackground); \ - x.text = get_colorpixel(ctext); \ - } while (0) - - config.client.background = get_colorpixel("#000000"); - INIT_COLOR(config.client.focused, "#4c7899", "#285577", "#ffffff"); - INIT_COLOR(config.client.focused_inactive, "#333333", "#5f676a", "#ffffff"); - INIT_COLOR(config.client.unfocused, "#333333", "#222222", "#888888"); - INIT_COLOR(config.client.urgent, "#2f343a", "#900000", "#ffffff"); - INIT_COLOR(config.bar.focused, "#4c7899", "#285577", "#ffffff"); - INIT_COLOR(config.bar.unfocused, "#333333", "#222222", "#888888"); - INIT_COLOR(config.bar.urgent, "#2f343a", "#900000", "#ffffff"); - - config.default_border = BS_NORMAL; - /* Set default_orientation to NO_ORIENTATION for auto orientation. */ - config.default_orientation = NO_ORIENTATION; - - parse_configuration(override_configpath); - - if (reload) { - translate_keysyms(); - grab_all_keys(conn, false); - } + do { \ + x.border = get_colorpixel(cborder); \ + x.background = get_colorpixel(cbackground); \ + x.text = get_colorpixel(ctext); \ + } while (0) + + config.client.background = get_colorpixel("#000000"); + INIT_COLOR(config.client.focused, "#4c7899", "#285577", "#ffffff"); + INIT_COLOR(config.client.focused_inactive, "#333333", "#5f676a", "#ffffff"); + INIT_COLOR(config.client.unfocused, "#333333", "#222222", "#888888"); + INIT_COLOR(config.client.urgent, "#2f343a", "#900000", "#ffffff"); + INIT_COLOR(config.bar.focused, "#4c7899", "#285577", "#ffffff"); + INIT_COLOR(config.bar.unfocused, "#333333", "#222222", "#888888"); + INIT_COLOR(config.bar.urgent, "#2f343a", "#900000", "#ffffff"); + + config.default_border = BS_NORMAL; + config.default_floating_border = BS_NORMAL; + /* Set default_orientation to NO_ORIENTATION for auto orientation. */ + config.default_orientation = NO_ORIENTATION; + + parse_configuration(override_configpath); + + if (reload) { + translate_keysyms(); + grab_all_keys(conn, false); + } - if (config.font.id == 0) { - ELOG("You did not specify required configuration option \"font\"\n"); - config.font = load_font("fixed", true); - } + if (config.font.id == 0) { + ELOG("You did not specify required configuration option \"font\"\n"); + config.font = load_font("fixed", true); + } #if 0 - /* Set an empty name for every workspace which got no name */ - Workspace *ws; - TAILQ_FOREACH(ws, workspaces, workspaces) { - if (ws->name != NULL) { - /* If the font was not specified when the workspace name - * was loaded, we need to predict the text width now */ - if (ws->text_width == 0) - ws->text_width = predict_text_width(global_conn, - config.font, ws->name, ws->name_len); - continue; - } - - workspace_set_name(ws, NULL); - } + /* Set an empty name for every workspace which got no name */ + Workspace *ws; + TAILQ_FOREACH(ws, workspaces, workspaces) { + if (ws->name != NULL) { + /* If the font was not specified when the workspace name + * was loaded, we need to predict the text width now */ + if (ws->text_width == 0) + ws->text_width = predict_text_width(global_conn, + config.font, ws->name, ws->name_len); + continue; + } + + workspace_set_name(ws, NULL); + } #endif } diff --git a/src/floating.c b/src/floating.c index 0266dfa2..13c84e47 100644 --- a/src/floating.c +++ b/src/floating.c @@ -168,6 +168,10 @@ void floating_enable(Con *con, bool automatic) { con->percent = 1.0; con->floating = FLOATING_USER_ON; + /* 4: set the border style as specified with new_float */ + if (automatic) + con->border_style = config.default_floating_border; + TAILQ_INSERT_TAIL(&(nc->nodes_head), con, nodes); TAILQ_INSERT_TAIL(&(nc->focus_head), con, focused); diff --git a/src/ipc.c b/src/ipc.c index 5f4d59cc..e5034939 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -205,6 +205,11 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) { ystr("urgent"); y(bool, con->urgent); + if (con->mark != NULL) { + ystr("mark"); + ystr(con->mark); + } + ystr("focused"); y(bool, (con == focused)); @@ -338,6 +343,7 @@ IPC_HANDLER(tree) { y(free); } + /* * Formats the reply message for a GET_WORKSPACES request and sends it to the * client @@ -468,6 +474,38 @@ IPC_HANDLER(get_outputs) { y(free); } +/* + * Formats the reply message for a GET_MARKS request and sends it to the + * client + * + */ +IPC_HANDLER(get_marks) { +#if YAJL_MAJOR >= 2 + yajl_gen gen = yajl_gen_alloc(NULL); +#else + yajl_gen gen = yajl_gen_alloc(NULL, NULL); +#endif + y(array_open); + + Con *con; + TAILQ_FOREACH(con, &all_cons, all_cons) + if (con->mark != NULL) + ystr(con->mark); + + y(array_close); + + const unsigned char *payload; +#if YAJL_MAJOR >= 2 + size_t length; +#else + unsigned int length; +#endif + y(get_buf, &payload, &length); + + ipc_send_message(fd, payload, I3_IPC_REPLY_TYPE_MARKS, length); + y(free); +} + /* * Callback for the YAJL parser (will be called when a string is parsed). * @@ -555,12 +593,13 @@ IPC_HANDLER(subscribe) { /* The index of each callback function corresponds to the numeric * value of the message type (see include/i3/ipc.h) */ -handler_t handlers[5] = { +handler_t handlers[6] = { handle_command, handle_get_workspaces, handle_subscribe, handle_get_outputs, - handle_tree + handle_tree, + handle_get_marks }; /* @@ -683,7 +722,7 @@ void ipc_new_client(EV_P_ struct ev_io *w, int revents) { ev_io_init(package, ipc_receive_message, client, EV_READ); ev_io_start(EV_A_ package); - DLOG("IPC: new client connected\n"); + DLOG("IPC: new client connected on fd %d\n", w->fd); ipc_client *new = scalloc(sizeof(ipc_client)); new->fd = client; diff --git a/src/load_layout.c b/src/load_layout.c index b61a0e5c..37322c4e 100644 --- a/src/load_layout.c +++ b/src/load_layout.c @@ -146,6 +146,10 @@ static int json_string(void *ctx, const unsigned char *val, unsigned int len) { json_node->layout = L_OUTPUT; else LOG("Unhandled \"layout\": %s\n", buf); free(buf); + } else if (strcasecmp(last_key, "mark") == 0) { + char *buf = NULL; + asprintf(&buf, "%.*s", (int)len, val); + json_node->mark = buf; } } return 1; diff --git a/src/main.c b/src/main.c index 77e295fc..ea02bb6e 100644 --- a/src/main.c +++ b/src/main.c @@ -5,6 +5,8 @@ #include #include "all.h" +#include "sd-daemon.h" + static int xkb_event_base; int xkb_current_group; @@ -161,6 +163,14 @@ static void xkb_got_event(EV_P_ struct ev_io *w, int revents) { DLOG("Done\n"); } +/* + * Exit handler which destroys the main_loop. Will trigger cleanup handlers. + * + */ +static void i3_exit() { + ev_loop_destroy(main_loop); +} + int main(int argc, char *argv[]) { //parse_cmd("[ foo ] attach, attach ; focus"); int screens; @@ -459,6 +469,21 @@ int main(int argc, char *argv[]) { ev_io_start(main_loop, ipc_io); } + /* Also handle the UNIX domain sockets passed via socket activation */ + int fds = sd_listen_fds(1); + if (fds < 0) + ELOG("socket activation: Error in sd_listen_fds\n"); + else if (fds == 0) + DLOG("socket activation: no sockets passed\n"); + else { + for (int fd = SD_LISTEN_FDS_START; fd < (SD_LISTEN_FDS_START + fds); fd++) { + DLOG("socket activation: also listening on fd %d\n", fd); + struct ev_io *ipc_io = scalloc(sizeof(struct ev_io)); + ev_io_init(ipc_io, ipc_new_client, fd, EV_READ); + ev_io_start(main_loop, ipc_io); + } + } + /* Set up i3 specific atoms like I3_SOCKET_PATH and I3_CONFIG_PATH */ x_set_i3_atoms(); @@ -512,5 +537,9 @@ int main(int argc, char *argv[]) { start_application(exec_always->command); } + /* Make sure to destroy the event loop to invoke the cleeanup callbacks + * when calling exit() */ + atexit(i3_exit); + ev_loop(main_loop, 0); } diff --git a/src/manage.c b/src/manage.c index 3dcdbac8..9ee3dd72 100644 --- a/src/manage.c +++ b/src/manage.c @@ -325,7 +325,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki if (want_floating) { DLOG("geometry = %d x %d\n", nc->geometry.width, nc->geometry.height); - floating_enable(nc, false); + floating_enable(nc, true); } /* to avoid getting an UnmapNotify event due to reparenting, we temporarily diff --git a/src/match.c b/src/match.c index 3a346117..3514acee 100644 --- a/src/match.c +++ b/src/match.c @@ -52,16 +52,19 @@ bool match_is_empty(Match *match) { void match_copy(Match *dest, Match *src) { memcpy(dest, src, sizeof(Match)); -#define STRDUP(field) do { \ +/* The DUPLICATE_REGEX macro creates a new regular expression from the + * ->pattern of the old one. It therefore does use a little more memory then + * with a refcounting system, but it’s easier this way. */ +#define DUPLICATE_REGEX(field) do { \ if (src->field != NULL) \ - dest->field = sstrdup(src->field); \ + dest->field = regex_new(src->field->pattern); \ } while (0) - STRDUP(title); - STRDUP(mark); - STRDUP(application); - STRDUP(class); - STRDUP(instance); + DUPLICATE_REGEX(title); + DUPLICATE_REGEX(mark); + DUPLICATE_REGEX(application); + DUPLICATE_REGEX(class); + DUPLICATE_REGEX(instance); } /* @@ -71,9 +74,9 @@ void match_copy(Match *dest, Match *src) { bool match_matches_window(Match *match, i3Window *window) { LOG("checking window %d (%s)\n", window->id, window->class_class); - /* TODO: pcre, full matching, … */ if (match->class != NULL) { - if (window->class_class != NULL && strcasecmp(match->class, window->class_class) == 0) { + if (window->class_class != NULL && + regex_matches(match->class, window->class_class)) { LOG("window class matches (%s)\n", window->class_class); } else { LOG("window class does not match\n"); @@ -82,7 +85,8 @@ bool match_matches_window(Match *match, i3Window *window) { } if (match->instance != NULL) { - if (window->class_instance != NULL && strcasecmp(match->instance, window->class_instance) == 0) { + if (window->class_instance != NULL && + regex_matches(match->instance, window->class_instance)) { LOG("window instance matches (%s)\n", window->class_instance); } else { LOG("window instance does not match\n"); @@ -99,9 +103,9 @@ bool match_matches_window(Match *match, i3Window *window) { } } - /* TODO: pcre match */ if (match->title != NULL) { - if (window->name_json != NULL && strcasecmp(match->title, window->name_json) == 0) { + if (window->name_json != NULL && + regex_matches(match->title, window->name_json)) { LOG("title matches (%s)\n", window->name_json); } else { LOG("title does not match\n"); @@ -132,3 +136,23 @@ bool match_matches_window(Match *match, i3Window *window) { return true; } + +/* + * Frees the given match. It must not be used afterwards! + * + */ +void match_free(Match *match) { + /* First step: free the regex fields / patterns */ + regex_free(match->title); + regex_free(match->application); + regex_free(match->class); + regex_free(match->instance); + regex_free(match->mark); + + /* Second step: free the regex helper struct itself */ + FREE(match->title); + FREE(match->application); + FREE(match->class); + FREE(match->instance); + FREE(match->mark); +} diff --git a/src/randr.c b/src/randr.c index 6b6cd67d..dd30925b 100644 --- a/src/randr.c +++ b/src/randr.c @@ -447,10 +447,12 @@ void init_ws_for_output(Output *output, Con *content) { if (!exists) { /* Set ->num to the number of the workspace, if the name actually * is a number or starts with a number */ - long parsed_num = strtol(ws->name, NULL, 10); + char *endptr = NULL; + long parsed_num = strtol(ws->name, &endptr, 10); if (parsed_num == LONG_MIN || parsed_num == LONG_MAX || - parsed_num <= 0) + parsed_num < 0 || + endptr == ws->name) ws->num = -1; else ws->num = parsed_num; LOG("Used number %d for workspace with name %s\n", ws->num, ws->name); diff --git a/src/regex.c b/src/regex.c new file mode 100644 index 00000000..f419e4bb --- /dev/null +++ b/src/regex.c @@ -0,0 +1,90 @@ +/* + * 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" + +/* + * Creates a new 'regex' struct containing the given pattern and a PCRE + * compiled regular expression. Also, calls pcre_study because this regex will + * most likely be used often (like for every new window and on every relevant + * property change of existing windows). + * + * Returns NULL if the pattern could not be compiled into a regular expression + * (and ELOGs an appropriate error message). + * + */ +struct regex *regex_new(const char *pattern) { + const char *error; + int errorcode, offset; + + struct regex *re = scalloc(sizeof(struct regex)); + re->pattern = sstrdup(pattern); + /* We use PCRE_UCP so that \B, \b, \D, \d, \S, \s, \W, \w and some POSIX + * character classes play nicely with Unicode */ + int options = PCRE_UCP | PCRE_UTF8; + while (!(re->regex = pcre_compile2(pattern, options, &errorcode, &error, &offset, NULL))) { + /* If the error is that PCRE was not compiled with UTF-8 support we + * disable it and try again */ + if (errorcode == 32) { + options &= ~PCRE_UTF8; + continue; + } + ELOG("PCRE regular expression compilation failed at %d: %s\n", + offset, error); + return NULL; + } + re->extra = pcre_study(re->regex, 0, &error); + /* If an error happened, we print the error message, but continue. + * Studying the regular expression leads to faster matching, but it’s not + * absolutely necessary. */ + if (error) { + ELOG("PCRE regular expression studying failed: %s\n", error); + } + return re; +} + +/* + * Frees the given regular expression. It must not be used afterwards! + * + */ +void regex_free(struct regex *regex) { + if (!regex) + return; + FREE(regex->pattern); + FREE(regex->regex); + FREE(regex->extra); +} + +/* + * Checks if the given regular expression matches the given input and returns + * true if it does. In either case, it logs the outcome using LOG(), so it will + * be visible without any debug loglevel. + * + */ +bool regex_matches(struct regex *regex, const char *input) { + int rc; + + /* We use strlen() because pcre_exec() expects the length of the input + * string in bytes */ + if ((rc = pcre_exec(regex->regex, regex->extra, input, strlen(input), 0, 0, NULL, 0)) == 0) { + LOG("Regular expression \"%s\" matches \"%s\"\n", + regex->pattern, input); + return true; + } + + if (rc == PCRE_ERROR_NOMATCH) { + LOG("Regular expression \"%s\" does not match \"%s\"\n", + regex->pattern, input); + return false; + } + + ELOG("PCRE error %d while trying to use regular expression \"%s\" on input \"%s\", see pcreapi(3)\n", + rc, regex->pattern, input); + return false; +} diff --git a/src/sd-daemon.c b/src/sd-daemon.c new file mode 100644 index 00000000..6d1eebff --- /dev/null +++ b/src/sd-daemon.c @@ -0,0 +1,436 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + Copyright 2010 Lennart Poettering + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +***/ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sd-daemon.h" + +int sd_listen_fds(int unset_environment) { + +#if defined(DISABLE_SYSTEMD) || !defined(__linux__) + return 0; +#else + int r, fd; + const char *e; + char *p = NULL; + unsigned long l; + + if (!(e = getenv("LISTEN_PID"))) { + r = 0; + goto finish; + } + + errno = 0; + l = strtoul(e, &p, 10); + + if (errno != 0) { + r = -errno; + goto finish; + } + + if (!p || *p || l <= 0) { + r = -EINVAL; + goto finish; + } + + /* Is this for us? */ + if (getpid() != (pid_t) l) { + r = 0; + goto finish; + } + + if (!(e = getenv("LISTEN_FDS"))) { + r = 0; + goto finish; + } + + errno = 0; + l = strtoul(e, &p, 10); + + if (errno != 0) { + r = -errno; + goto finish; + } + + if (!p || *p) { + r = -EINVAL; + goto finish; + } + + for (fd = SD_LISTEN_FDS_START; fd < SD_LISTEN_FDS_START + (int) l; fd ++) { + int flags; + + if ((flags = fcntl(fd, F_GETFD)) < 0) { + r = -errno; + goto finish; + } + + if (flags & FD_CLOEXEC) + continue; + + if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) { + r = -errno; + goto finish; + } + } + + r = (int) l; + +finish: + if (unset_environment) { + unsetenv("LISTEN_PID"); + unsetenv("LISTEN_FDS"); + } + + return r; +#endif +} + +int sd_is_fifo(int fd, const char *path) { + struct stat st_fd; + + if (fd < 0) + return -EINVAL; + + memset(&st_fd, 0, sizeof(st_fd)); + if (fstat(fd, &st_fd) < 0) + return -errno; + + if (!S_ISFIFO(st_fd.st_mode)) + return 0; + + if (path) { + struct stat st_path; + + memset(&st_path, 0, sizeof(st_path)); + if (stat(path, &st_path) < 0) { + + if (errno == ENOENT || errno == ENOTDIR) + return 0; + + return -errno; + } + + return + st_path.st_dev == st_fd.st_dev && + st_path.st_ino == st_fd.st_ino; + } + + return 1; +} + +static int sd_is_socket_internal(int fd, int type, int listening) { + struct stat st_fd; + + if (fd < 0 || type < 0) + return -EINVAL; + + if (fstat(fd, &st_fd) < 0) + return -errno; + + if (!S_ISSOCK(st_fd.st_mode)) + return 0; + + if (type != 0) { + int other_type = 0; + socklen_t l = sizeof(other_type); + + if (getsockopt(fd, SOL_SOCKET, SO_TYPE, &other_type, &l) < 0) + return -errno; + + if (l != sizeof(other_type)) + return -EINVAL; + + if (other_type != type) + return 0; + } + + if (listening >= 0) { + int accepting = 0; + socklen_t l = sizeof(accepting); + + if (getsockopt(fd, SOL_SOCKET, SO_ACCEPTCONN, &accepting, &l) < 0) + return -errno; + + if (l != sizeof(accepting)) + return -EINVAL; + + if (!accepting != !listening) + return 0; + } + + return 1; +} + +union sockaddr_union { + struct sockaddr sa; + struct sockaddr_in in4; + struct sockaddr_in6 in6; + struct sockaddr_un un; + struct sockaddr_storage storage; +}; + +int sd_is_socket(int fd, int family, int type, int listening) { + int r; + + if (family < 0) + return -EINVAL; + + if ((r = sd_is_socket_internal(fd, type, listening)) <= 0) + return r; + + if (family > 0) { + union sockaddr_union sockaddr; + socklen_t l; + + memset(&sockaddr, 0, sizeof(sockaddr)); + l = sizeof(sockaddr); + + if (getsockname(fd, &sockaddr.sa, &l) < 0) + return -errno; + + if (l < sizeof(sa_family_t)) + return -EINVAL; + + return sockaddr.sa.sa_family == family; + } + + return 1; +} + +int sd_is_socket_inet(int fd, int family, int type, int listening, uint16_t port) { + union sockaddr_union sockaddr; + socklen_t l; + int r; + + if (family != 0 && family != AF_INET && family != AF_INET6) + return -EINVAL; + + if ((r = sd_is_socket_internal(fd, type, listening)) <= 0) + return r; + + memset(&sockaddr, 0, sizeof(sockaddr)); + l = sizeof(sockaddr); + + if (getsockname(fd, &sockaddr.sa, &l) < 0) + return -errno; + + if (l < sizeof(sa_family_t)) + return -EINVAL; + + if (sockaddr.sa.sa_family != AF_INET && + sockaddr.sa.sa_family != AF_INET6) + return 0; + + if (family > 0) + if (sockaddr.sa.sa_family != family) + return 0; + + if (port > 0) { + if (sockaddr.sa.sa_family == AF_INET) { + if (l < sizeof(struct sockaddr_in)) + return -EINVAL; + + return htons(port) == sockaddr.in4.sin_port; + } else { + if (l < sizeof(struct sockaddr_in6)) + return -EINVAL; + + return htons(port) == sockaddr.in6.sin6_port; + } + } + + return 1; +} + +int sd_is_socket_unix(int fd, int type, int listening, const char *path, size_t length) { + union sockaddr_union sockaddr; + socklen_t l; + int r; + + if ((r = sd_is_socket_internal(fd, type, listening)) <= 0) + return r; + + memset(&sockaddr, 0, sizeof(sockaddr)); + l = sizeof(sockaddr); + + if (getsockname(fd, &sockaddr.sa, &l) < 0) + return -errno; + + if (l < sizeof(sa_family_t)) + return -EINVAL; + + if (sockaddr.sa.sa_family != AF_UNIX) + return 0; + + if (path) { + if (length <= 0) + length = strlen(path); + + if (length <= 0) + /* Unnamed socket */ + return l == offsetof(struct sockaddr_un, sun_path); + + if (path[0]) + /* Normal path socket */ + return + (l >= offsetof(struct sockaddr_un, sun_path) + length + 1) && + memcmp(path, sockaddr.un.sun_path, length+1) == 0; + else + /* Abstract namespace socket */ + return + (l == offsetof(struct sockaddr_un, sun_path) + length) && + memcmp(path, sockaddr.un.sun_path, length) == 0; + } + + return 1; +} + +int sd_notify(int unset_environment, const char *state) { +#if defined(DISABLE_SYSTEMD) || !defined(__linux__) || !defined(SOCK_CLOEXEC) + return 0; +#else + int fd = -1, r; + struct msghdr msghdr; + struct iovec iovec; + union sockaddr_union sockaddr; + const char *e; + + if (!state) { + r = -EINVAL; + goto finish; + } + + if (!(e = getenv("NOTIFY_SOCKET"))) + return 0; + + /* Must be an abstract socket, or an absolute path */ + if ((e[0] != '@' && e[0] != '/') || e[1] == 0) { + r = -EINVAL; + goto finish; + } + + if ((fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0)) < 0) { + r = -errno; + goto finish; + } + + memset(&sockaddr, 0, sizeof(sockaddr)); + sockaddr.sa.sa_family = AF_UNIX; + strncpy(sockaddr.un.sun_path, e, sizeof(sockaddr.un.sun_path)); + + if (sockaddr.un.sun_path[0] == '@') + sockaddr.un.sun_path[0] = 0; + + memset(&iovec, 0, sizeof(iovec)); + iovec.iov_base = (char*) state; + iovec.iov_len = strlen(state); + + memset(&msghdr, 0, sizeof(msghdr)); + msghdr.msg_name = &sockaddr; + msghdr.msg_namelen = offsetof(struct sockaddr_un, sun_path) + strlen(e); + + if (msghdr.msg_namelen > sizeof(struct sockaddr_un)) + msghdr.msg_namelen = sizeof(struct sockaddr_un); + + msghdr.msg_iov = &iovec; + msghdr.msg_iovlen = 1; + + if (sendmsg(fd, &msghdr, MSG_NOSIGNAL) < 0) { + r = -errno; + goto finish; + } + + r = 1; + +finish: + if (unset_environment) + unsetenv("NOTIFY_SOCKET"); + + if (fd >= 0) + close(fd); + + return r; +#endif +} + +int sd_notifyf(int unset_environment, const char *format, ...) { +#if defined(DISABLE_SYSTEMD) || !defined(__linux__) + return 0; +#else + va_list ap; + char *p = NULL; + int r; + + va_start(ap, format); + r = vasprintf(&p, format, ap); + va_end(ap); + + if (r < 0 || !p) + return -ENOMEM; + + r = sd_notify(unset_environment, p); + free(p); + + return r; +#endif +} + +int sd_booted(void) { +#if defined(DISABLE_SYSTEMD) || !defined(__linux__) + return 0; +#else + + struct stat a, b; + + /* We simply test whether the systemd cgroup hierarchy is + * mounted */ + + if (lstat("/sys/fs/cgroup", &a) < 0) + return 0; + + if (lstat("/sys/fs/cgroup/systemd", &b) < 0) + return 0; + + return a.st_dev != b.st_dev; +#endif +} diff --git a/src/workspace.c b/src/workspace.c index cf1b4070..27899a37 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -49,12 +49,12 @@ Con *workspace_get(const char *num, bool *created) { workspace->name = sstrdup(num); /* We set ->num to the number if this workspace’s name consists only of * a positive number. Otherwise it’s a named ws and num will be -1. */ - char *end; - long parsed_num = strtol(num, &end, 10); + char *endptr = NULL; + long parsed_num = strtol(num, &endptr, 10); if (parsed_num == LONG_MIN || parsed_num == LONG_MAX || parsed_num < 0 || - (end && *end != '\0')) + endptr == num) workspace->num = -1; else workspace->num = parsed_num; LOG("num = %d\n", workspace->num); diff --git a/testcases/complete-run.pl b/testcases/complete-run.pl index c05d6e5b..6f00877c 100755 --- a/testcases/complete-run.pl +++ b/testcases/complete-run.pl @@ -29,6 +29,18 @@ use Try::Tiny; use Getopt::Long; use Time::HiRes qw(sleep); use X11::XCB::Connection; +use IO::Socket::UNIX; # core +use POSIX; # core +use AnyEvent::Handle; + +# open a file so that we get file descriptor 3. we will later close it in the +# child and dup() the listening socket file descriptor to 3 to pass it to i3 +open(my $reserved, '<', '/dev/null'); +if (fileno($reserved) != 3) { + warn "Socket file descriptor is not 3."; + warn "Please don't start this script within a subshell of vim or something."; + exit 1; +} # install a dummy CHLD handler to overwrite the CHLD handler of AnyEvent / EV # XXX: we could maybe also use a different loop than the default loop in EV? @@ -72,7 +84,6 @@ for my $display (@displays) { }; } -my $i3cmd = abs_path("../i3") . " -V -d all --disable-signalhandler"; my $config = slurp('i3-test.config'); # 1: get a list of all testcases @@ -116,21 +127,90 @@ take_job($_) for @wdisplays; # sub take_job { my ($display) = @_; + + my $test = shift @testfiles; + return unless $test; + my $dont_start = (slurp($test) =~ /# !NO_I3_INSTANCE!/); + my $logpath = "$outdir/i3-log-for-" . basename($test); + my ($fh, $tmpfile) = tempfile(); say $fh $config; say $fh "ipc-socket /tmp/nested-$display"; close($fh); - my $test = shift @testfiles; - return unless $test; - my $logpath = "$outdir/i3-log-for-" . basename($test); - my $cmd = "export DISPLAY=$display; exec $i3cmd -c $tmpfile >$logpath 2>&1"; - my $dont_start = (slurp($test) =~ /# !NO_I3_INSTANCE!/); + my $activate_cv = AnyEvent->condvar; + my $start_i3 = sub { + # remove the old unix socket + unlink("/tmp/nested-$display-activation"); + + # pass all file descriptors up to three to the children. + # we need to set this flag before opening the socket. + open(my $fdtest, '<', '/dev/null'); + $^F = fileno($fdtest); + close($fdtest); + my $socket = IO::Socket::UNIX->new( + Listen => 1, + Local => "/tmp/nested-$display-activation", + ); - my $process = Proc::Background->new($cmd) unless $dont_start; - say "[$display] Running $test with logfile $logpath"; + my $pid = fork; + if (!defined($pid)) { + die "could not fork()"; + } + say "pid = $pid"; + if ($pid == 0) { + say "child!"; + $ENV{LISTEN_PID} = $$; + $ENV{LISTEN_FDS} = 1; + $ENV{DISPLAY} = $display; + $^F = 3; + + say "fileno is " . fileno($socket); + close($reserved); + POSIX::dup2(fileno($socket), 3); + + # now execute i3 + my $i3cmd = abs_path("../i3") . " -V -d all --disable-signalhandler"; + my $cmd = "exec $i3cmd -c $tmpfile >$logpath 2>&1"; + exec "/bin/sh", '-c', $cmd; + + # if we are still here, i3 could not be found or exec failed. bail out. + exit 1; + } + + my $child_watcher; + $child_watcher = AnyEvent->child(pid => $pid, cb => sub { + say "child died. pid = $pid"; + undef $child_watcher; + }); + + # close the socket, the child process should be the only one which keeps a file + # descriptor on the listening socket. + $socket->close; + + # We now connect (will succeed immediately) and send a request afterwards. + # As soon as the reply is there, i3 is considered ready. + my $cl = IO::Socket::UNIX->new(Peer => "/tmp/nested-$display-activation"); + my $hdl; + $hdl = AnyEvent::Handle->new(fh => $cl, on_error => sub { $activate_cv->send(0) }); + + # send a get_tree message without payload + $hdl->push_write('i3-ipc' . pack("LL", 0, 4)); + + # wait for the reply + $hdl->push_read(chunk => 1, => sub { + my ($h, $line) = @_; + say "read something from i3"; + $activate_cv->send(1); + undef $hdl; + }); + + return $pid; + }; + + my $pid; + $pid = $start_i3->() unless $dont_start; - sleep 0.5; my $kill_i3 = sub { # Don’t bother killing i3 when we haven’t started it return if $dont_start; @@ -148,53 +228,65 @@ sub take_job { } say "[$display] killing i3"; - kill(9, $process->pid) or die "could not kill i3"; + kill(9, $pid) or die "could not kill i3"; }; - my $output; - my $parser = TAP::Parser->new({ - exec => [ 'sh', '-c', "DISPLAY=$display /usr/bin/perl -It/lib $test" ], - spool => IO::Scalar->new(\$output), - merge => 1, - }); - - my @watchers; - my ($stdout, $stderr) = $parser->get_select_handles; - for my $handle ($parser->get_select_handles) { - my $w; - $w = AnyEvent->io( - fh => $handle, - poll => 'r', - cb => sub { - # Ignore activity on stderr (unnecessary with merge => 1, - # but let’s keep it in here if we want to use merge => 0 - # for some reason in the future). - return if defined($stderr) and $handle == $stderr; - - my $result = $parser->next; - if (defined($result)) { - # TODO: check if we should bail out - return; + # This will be called as soon as i3 is running and answered to our + # IPC request + $activate_cv->cb(sub { + say "cb"; + my ($status) = $activate_cv->recv; + say "complete-run: status = $status"; + + say "[$display] Running $test with logfile $logpath"; + + my $output; + my $parser = TAP::Parser->new({ + exec => [ 'sh', '-c', "DISPLAY=$display /usr/bin/perl -It/lib $test" ], + spool => IO::Scalar->new(\$output), + merge => 1, + }); + + my @watchers; + my ($stdout, $stderr) = $parser->get_select_handles; + for my $handle ($parser->get_select_handles) { + my $w; + $w = AnyEvent->io( + fh => $handle, + poll => 'r', + cb => sub { + # Ignore activity on stderr (unnecessary with merge => 1, + # but let’s keep it in here if we want to use merge => 0 + # for some reason in the future). + return if defined($stderr) and $handle == $stderr; + + my $result = $parser->next; + if (defined($result)) { + # TODO: check if we should bail out + return; + } + + # $result is not defined, we are done parsing + say "[$display] $test finished"; + close($parser->delete_spool); + $aggregator->add($test, $parser); + push @done, [ $test, $output ]; + + $kill_i3->(); + + undef $_ for @watchers; + if (@done == $num) { + $cv->send; + } else { + take_job($display); + } } + ); + push @watchers, $w; + } + }); - # $result is not defined, we are done parsing - say "[$display] $test finished"; - close($parser->delete_spool); - $aggregator->add($test, $parser); - push @done, [ $test, $output ]; - - $kill_i3->(); - - undef $_ for @watchers; - if (@done == $num) { - $cv->send; - } else { - take_job($display); - } - } - ); - push @watchers, $w; - } + $activate_cv->send(1) if $dont_start; } $cv->recv; diff --git a/testcases/t/17-workspace.t b/testcases/t/17-workspace.t index 19e2df34..3c3b6cc6 100644 --- a/testcases/t/17-workspace.t +++ b/testcases/t/17-workspace.t @@ -98,4 +98,23 @@ cmd 'workspace "prev"'; ok(workspace_exists('prev'), 'workspace "prev" exists'); is(focused_ws(), 'prev', 'now on workspace prev'); +##################################################################### +# check that the numbers are assigned/recognized correctly +##################################################################### + +cmd "workspace 3: $tmp"; +my $ws = get_ws("3: $tmp"); +ok(defined($ws), "workspace 3: $tmp was created"); +is($ws->{num}, 3, 'workspace number is 3'); + +cmd "workspace 0: $tmp"; +my $ws = get_ws("0: $tmp"); +ok(defined($ws), "workspace 0: $tmp was created"); +is($ws->{num}, 0, 'workspace number is 0'); + +cmd "workspace aa: $tmp"; +my $ws = get_ws("aa: $tmp"); +ok(defined($ws), "workspace aa: $tmp was created"); +is($ws->{num}, -1, 'workspace number is -1'); + done_testing; diff --git a/testcases/t/19-match.t b/testcases/t/19-match.t index 2332bc71..e4fc6ec0 100644 --- a/testcases/t/19-match.t +++ b/testcases/t/19-match.t @@ -34,9 +34,10 @@ my $win = $content->[0]; # first test that matches which should not match this window really do # not match it ###################################################################### -# TODO: use PCRE expressions # TODO: specify more match types -cmd q|[class="*"] kill|; +# we can match on any (non-empty) class here since that window does not have +# WM_CLASS set +cmd q|[class=".*"] kill|; cmd q|[con_id="99999"] kill|; $content = get_ws_content($tmp); @@ -118,4 +119,62 @@ sleep 0.25; $content = get_ws_content($tmp); is(@{$content}, 1, 'one window still there'); +###################################################################### +# check that regular expressions work +###################################################################### + +$tmp = fresh_workspace; + +$left = $x->root->create_child( + class => WINDOW_CLASS_INPUT_OUTPUT, + rect => [ 0, 0, 30, 30 ], + background_color => '#0000ff', +); + +$left->_create; +set_wm_class($left->id, 'special7', 'special7'); +$left->name('left'); +$left->map; +sleep 0.25; + +# two windows should be here +$content = get_ws_content($tmp); +ok(@{$content} == 1, 'window opened'); + +cmd '[class="^special[0-9]$"] kill'; + +sleep 0.25; + +$content = get_ws_content($tmp); +is(@{$content}, 0, 'window killed'); + +###################################################################### +# check that UTF-8 works when matching +###################################################################### + +$tmp = fresh_workspace; + +$left = $x->root->create_child( + class => WINDOW_CLASS_INPUT_OUTPUT, + rect => [ 0, 0, 30, 30 ], + background_color => '#0000ff', +); + +$left->_create; +set_wm_class($left->id, 'special7', 'special7'); +$left->name('ä 3'); +$left->map; +sleep 0.25; + +# two windows should be here +$content = get_ws_content($tmp); +ok(@{$content} == 1, 'window opened'); + +cmd '[title="^\w [3]$"] kill'; + +sleep 0.25; + +$content = get_ws_content($tmp); +is(@{$content}, 0, 'window killed'); + done_testing; diff --git a/testcases/t/66-assign.t b/testcases/t/66-assign.t index 776710e7..b8366917 100644 --- a/testcases/t/66-assign.t +++ b/testcases/t/66-assign.t @@ -182,6 +182,46 @@ exit_gracefully($process->pid); sleep 0.25; +##################################################################### +# make sure that assignments are case-insensitive in the old syntax. +##################################################################### + +$config = <root->create_child( + class => WINDOW_CLASS_INPUT_OUTPUT, + rect => [ 0, 0, 30, 30 ], + background_color => '#0000ff', +); + +$window->_create; +set_wm_class($window->id, 'SPEcial', 'SPEcial'); +$window->name('special window'); +$window->map; +sleep 0.25; + +my $content = get_ws($tmp); +ok(@{$content->{nodes}} == 0, 'no tiling cons'); +ok(@{$content->{floating_nodes}} == 1, 'one floating con'); + +$window->destroy; + +exit_gracefully($process->pid); + +sleep 0.25; + ##################################################################### # regression test: dock clients with floating assignments should not crash # (instead, nothing should happen - dock clients can’t float) @@ -200,7 +240,9 @@ $tmp = fresh_workspace; ok(@{get_ws_content($tmp)} == 0, 'no containers yet'); my @docked = get_dock_clients; -is(@docked, 0, 'no dock clients yet'); +# We expect i3-nagbar as the first dock client due to using the old assign +# syntax +is(@docked, 1, 'one dock client yet'); my $window = $x->root->create_child( class => WINDOW_CLASS_INPUT_OUTPUT, @@ -219,7 +261,7 @@ my $content = get_ws($tmp); ok(@{$content->{nodes}} == 0, 'no tiling cons'); ok(@{$content->{floating_nodes}} == 0, 'one floating con'); @docked = get_dock_clients; -is(@docked, 1, 'no dock clients yet'); +is(@docked, 2, 'two dock clients now'); $window->destroy; diff --git a/testcases/t/73-get-marks.t b/testcases/t/73-get-marks.t new file mode 100644 index 00000000..86481747 --- /dev/null +++ b/testcases/t/73-get-marks.t @@ -0,0 +1,63 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# checks if the IPC message type get_marks works correctly +# +use i3test; + +# TODO: this will be available in AnyEvent::I3 soon +sub get_marks { + my $i3 = i3(get_socket_path()); + $i3->connect->recv; + my $cv = AnyEvent->condvar; + my $msg = $i3->message(5); + my $t; + $msg->cb(sub { + my ($_cv) = @_; + $cv->send($_cv->recv); + }); + $t = AnyEvent->timer(after => 2, cb => sub { + $cv->croak('timeout while waiting for the marks'); + }); + return $cv->recv; +} + +############################################################## +# 1: check that get_marks returns no marks yet +############################################################## + +my $tmp = fresh_workspace; + +my $marks = get_marks(); +cmp_deeply($marks, [], 'no marks set so far'); + +############################################################## +# 2: check that setting a mark is reflected in the get_marks reply +############################################################## + +cmd 'open'; +cmd 'mark foo'; + +cmp_deeply(get_marks(), [ 'foo' ], 'mark foo set'); + +############################################################## +# 3: check that the mark is gone after killing the container +############################################################## + +cmd 'kill'; + +cmp_deeply(get_marks(), [ ], 'mark gone'); + +############################################################## +# 4: check that duplicate marks are included twice in the get_marks reply +############################################################## + +cmd 'open'; +cmd 'mark bar'; + +cmd 'open'; +cmd 'mark bar'; + +cmp_deeply(get_marks(), [ 'bar', 'bar' ], 'duplicate mark found twice'); + +done_testing; diff --git a/testcases/t/74-border-config.t b/testcases/t/74-border-config.t new file mode 100644 index 00000000..c8a4d73d --- /dev/null +++ b/testcases/t/74-border-config.t @@ -0,0 +1,140 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# !NO_I3_INSTANCE! will prevent complete-run.pl from starting i3 +# +# Tests the new_window and new_float config option. +# + +use i3test; +use X11::XCB qw(:all); +use X11::XCB::Connection; + +my $x = X11::XCB::Connection->new; + +##################################################################### +# 1: check that new windows start with 'normal' border unless configured +# otherwise +##################################################################### + +my $config = <{border}, 'normal', 'border normal by default'); + +exit_gracefully($process->pid); + +##################################################################### +# 2: check that new tiling windows start with '1pixel' border when +# configured +##################################################################### + +$config = <{border}, '1pixel', 'border normal by default'); + +exit_gracefully($process->pid); + +##################################################################### +# 3: check that new floating windows start with 'normal' border unless +# configured otherwise +##################################################################### + +$config = <root->create_child( + class => WINDOW_CLASS_INPUT_OUTPUT, + rect => [ 0, 0, 30, 30], + background_color => '#C0C0C0', + # replace the type with 'utility' as soon as the coercion works again in X11::XCB + window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'), +); + +$first->map; + +sleep 0.25; + +my $wscontent = get_ws($tmp); +my @floating = @{$wscontent->{floating_nodes}}; +ok(@floating == 1, 'one floating container opened'); +my $floatingcon = $floating[0]; +is($floatingcon->{nodes}->[0]->{border}, 'normal', 'border normal by default'); + +exit_gracefully($process->pid); + +##################################################################### +# 4: check that new floating windows start with '1pixel' border when +# configured +##################################################################### + +$config = <root->create_child( + class => WINDOW_CLASS_INPUT_OUTPUT, + rect => [ 0, 0, 30, 30], + background_color => '#C0C0C0', + # replace the type with 'utility' as soon as the coercion works again in X11::XCB + window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'), +); + +$first->map; + +sleep 0.25; + +$wscontent = get_ws($tmp); +@floating = @{$wscontent->{floating_nodes}}; +ok(@floating == 1, 'one floating container opened'); +$floatingcon = $floating[0]; +is($floatingcon->{nodes}->[0]->{border}, '1pixel', 'border normal by default'); + +exit_gracefully($process->pid); + +done_testing;