use constant TYPE_GET_VERSION => 7;
use constant TYPE_GET_BINDING_MODES => 8;
use constant TYPE_GET_CONFIG => 9;
+use constant TYPE_SEND_TICK => 10;
our %EXPORT_TAGS = ( 'all' => [
qw(i3 TYPE_RUN_COMMAND TYPE_COMMAND TYPE_GET_WORKSPACES TYPE_SUBSCRIBE TYPE_GET_OUTPUTS
TYPE_GET_TREE TYPE_GET_MARKS TYPE_GET_BAR_CONFIG TYPE_GET_VERSION
- TYPE_GET_BINDING_MODES TYPE_GET_CONFIG)
+ TYPE_GET_BINDING_MODES TYPE_GET_CONFIG TYPE_SEND_TICK)
] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } );
barconfig_update => ($event_mask | 4),
binding => ($event_mask | 5),
shutdown => ($event_mask | 6),
+ tick => ($event_mask | 7),
_error => 0xFFFFFFFF,
);
$self->message(TYPE_GET_CONFIG);
}
+=head2 send_tick
+
+Sends a tick event. Requires i3 >= 4.15
+
+=cut
+sub send_tick {
+ my ($self, $payload) = @_;
+
+ $self->_ensure_connection;
+
+ $self->message(TYPE_SEND_TICK, $payload);
+}
=head2 command($content)
$na =~ s/~/\\textasciitilde{}/g;
my $type = 'leaf';
if (!defined($n->{window})) {
- $type = $n->{orientation} . '-split';
+ $type = $n->{layout};
}
my $name = qq|``$na'' ($type)|;
use AnyEvent;
use AnyEvent::I3;
use v5.10;
+use utf8;
my %layouts = (
'4' => 'tabbed',
you found the section which clearly highlights the problem, additional
information might be necessary to completely diagnose the problem.
-When debugging with us in IRC, be prepared to use a so called nopaste service
+When debugging with us in IRC, be prepared to use a so-called nopaste service
such as https://pastebin.com because pasting large amounts of text in IRC
sometimes leads to incomplete lines (servers have line length limitations) or
flood kicks.
| 7 | +GET_VERSION+ | <<_version_reply,VERSION>> | Gets the i3 version.
| 8 | +GET_BINDING_MODES+ | <<_binding_modes_reply,BINDING_MODES>> | Gets the names of all currently configured binding modes.
| 9 | +GET_CONFIG+ | <<_config_reply,CONFIG>> | Returns the last loaded i3 config.
+| 10 | +SEND_TICK+ | <<_tick_reply,TICK>> | Sends a tick event with the specified payload.
|======================================================
So, a typical message could look like this:
Reply to the GET_BINDING_MODES message.
GET_CONFIG (9)::
Reply to the GET_CONFIG message.
+TICK (10)::
+ Reply to the SEND_TICK message.
[[_command_reply]]
=== COMMAND reply
{ "config": "font pango:monospace 8\nbindsym Mod4+q exit\n" }
-------------------
+[[_tick_reply]]
+=== TICK reply
+
+The reply is a map containing the "success" member. After the reply was
+received, the tick event has been written to all IPC connections which subscribe
+to tick events. UNIX sockets are usually buffered, but you can be certain that
+once you receive the tick event you just triggered, you must have received all
+events generated prior to the +SEND_TICK+ message (happened-before relation).
+
+*Example:*
+-------------------
+{ "success": true }
+-------------------
== Events
mouse
shutdown (6)::
Sent when the ipc shuts down because of a restart or exit by user command
+tick (7)::
+ Sent when the ipc client subscribes to the tick event (with +"first":
+ true+) or when any ipc client sends a SEND_TICK message (with +"first":
+ false+).
*Example:*
--------------------------------------------------------------------
}
---------------------------
+=== tick event
+
+This event is triggered by a subscription to tick events or by a +SEND_TICK+
+message.
+
+*Example (upon subscription):*
+--------------------------------------------------------------------------------
+{
+ "first": true,
+ "payload": ""
+}
+--------------------------------------------------------------------------------
+
+*Example (upon +SEND_TICK+ with a payload of +arbitrary string+):*
+--------------------------------------------------------------------------------
+{
+ "first": false,
+ "payload": "arbitrary string"
+}
+--------------------------------------------------------------------------------
+
== See also (existing libraries)
[[libraries]]
* https://github.com/drmgc/i3ipcpp
Go::
* https://github.com/mdirkse/i3ipc-go
+ * https://github.com/i3/go-i3
JavaScript::
* https://github.com/acrisci/i3ipc-gjs
Lua::
payload. Then, receive the pending +COMMAND+ message reply in big endian.
5. From here on out, send/receive all messages using the detected byte order.
+
+Find an example implementation of this technique in
+https://github.com/i3/go-i3/blob/master/byteorder.go
always be found under the symlink +latest/+. Unless told differently, it will
run the tests on a separate X server instance (using Xephyr).
-Xephyr will open a window where you can inspect the running test. You can run
-the tests without an X session with Xvfb, such as with +xvfb-run
-./complete-run+. This will also speed up the tests significantly especially on
-machines without a powerful video card.
+Xephyr will open a window where you can inspect the running test. By default,
+tests are run under Xvfb.
.Example invocation of +complete-run.pl+
---------------------------------------
== Default keybindings
For the "too long; didn’t read" people, here is an overview of the default
-keybindings (click to see the full size image):
+keybindings (click to see the full-size image):
*Keys to use with $mod (Alt):*
Throughout this guide, the keyword +$mod+ will be used to refer to the
configured modifier. This is the Alt key (+Mod1+) by default, with the Windows
-key (+Mod4+) being a popular alternative.
+key (+Mod4+) being a popular alternative that largely prevents conflicts with
+application-defined shortcuts.
=== Opening terminals and moving around
=== The tree consists of Containers
-The building blocks of our tree are so called +Containers+. A +Container+ can
+The building blocks of our tree are so-called +Containers+. A +Container+ can
host a window (meaning an X11 window, one that you can actually see and use,
like a browser). Alternatively, it could contain one or more +Containers+. A
simple example is the workspace: When you start i3 with a single monitor, a
=== The floating modifier
To move floating windows with your mouse, you can either grab their titlebar
-or configure the so called floating modifier which you can then press and
+or configure the so-called floating modifier which you can then press and
click anywhere in the window itself to move it. The most common setup is to
use the same key you use for managing windows (Mod1 for example). Then
you can press Mod1, click into a window using your left mouse button, and drag
*Syntax*:
-------------------------------------------------------
resize grow|shrink <direction> [<px> px [or <ppt> ppt]]
-resize set <width> [px] <height> [px]
+resize set <width> [px | ppt] <height> [px | ppt]
-------------------------------------------------------
Direction can either be one of +up+, +down+, +left+ or +right+. Or you can be
# The font above is very space-efficient, that is, it looks good, sharp and
# clear in small sizes. However, its unicode glyph coverage is limited, the old
# X core fonts rendering does not support right-to-left and this being a bitmap
-# font, it doesn’t scale on retina/hidpi displays.
+# font, it doesn't scale on retina/hidpi displays.
# use these keys for focus, movement, and resize directions when reaching for
# the arrows is not convenient
message_type = I3_IPC_MESSAGE_TYPE_GET_VERSION;
} else if (strcasecmp(optarg, "get_config") == 0) {
message_type = I3_IPC_MESSAGE_TYPE_GET_CONFIG;
+ } else if (strcasecmp(optarg, "send_tick") == 0) {
+ message_type = I3_IPC_MESSAGE_TYPE_SEND_TICK;
} else {
printf("Unknown message type\n");
- printf("Known types: run_command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_binding_modes, get_version, get_config\n");
+ printf("Known types: run_command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_binding_modes, get_version, get_config, send_tick\n");
exit(EXIT_FAILURE);
}
} else if (o == 'q') {
ATOM_DO(_NET_SYSTEM_TRAY_COLORS)
ATOM_DO(_XEMBED_INFO)
ATOM_DO(_XEMBED)
+ATOM_DO(I3_SYNC)
#undef ATOM_DO
/* Event watchers, to interact with the user */
ev_prepare *xcb_prep;
-ev_check *xcb_chk;
ev_io *xcb_io;
ev_io *xkb_io;
*
*/
static void handle_client_message(xcb_client_message_event_t *event) {
- if (event->type == atoms[_NET_SYSTEM_TRAY_OPCODE] &&
- event->format == 32) {
+ if (event->type == atoms[I3_SYNC]) {
+ xcb_window_t window = event->data.data32[0];
+ uint32_t rnd = event->data.data32[1];
+ DLOG("[i3 sync protocol] Forwarding random value %d, X11 window 0x%08x to i3\n", rnd, window);
+
+ void *reply = scalloc(32, 1);
+ xcb_client_message_event_t *ev = reply;
+
+ ev->response_type = XCB_CLIENT_MESSAGE;
+ ev->window = window;
+ ev->type = atoms[I3_SYNC];
+ ev->format = 32;
+ ev->data.data32[0] = window;
+ ev->data.data32[1] = rnd;
+
+ xcb_send_event(conn, false, xcb_root, XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, (char *)ev);
+ xcb_flush(conn);
+ free(reply);
+ } else 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];
}
/*
- * This function is called immediately before the main loop locks. We flush xcb
- * then (and only then)
+ * This function is called immediately before the main loop locks. We check for
+ * events from X11, handle them, then flush our outgoing queue.
*
*/
void xcb_prep_cb(struct ev_loop *loop, ev_prepare *watcher, int revents) {
- xcb_flush(xcb_connection);
-}
-
-/*
- * This function is called immediately after the main loop locks, so when one
- * of the watchers registered an event.
- * We check whether an X-Event arrived and handle it.
- *
- */
-void xcb_chk_cb(struct ev_loop *loop, ev_check *watcher, int revents) {
xcb_generic_event_t *event;
if (xcb_connection_has_error(xcb_connection)) {
}
free(event);
}
+
+ xcb_flush(xcb_connection);
}
/*
/* The various watchers to communicate with xcb */
xcb_io = smalloc(sizeof(ev_io));
xcb_prep = smalloc(sizeof(ev_prepare));
- xcb_chk = smalloc(sizeof(ev_check));
ev_io_init(xcb_io, &xcb_io_cb, xcb_get_file_descriptor(xcb_connection), EV_READ);
ev_prepare_init(xcb_prep, &xcb_prep_cb);
- ev_check_init(xcb_chk, &xcb_chk_cb);
-
- /* Within an event loop iteration, run the xcb_chk watcher last: other
- * watchers might call xcb_flush(), which, unexpectedly, can also read
- * events into the queue (see _xcb_conn_wait). Hence, we need to drain xcb’s
- * queue last, otherwise we risk dead-locking. */
- ev_set_priority(xcb_chk, EV_MINPRI);
ev_io_start(main_loop, xcb_io);
ev_prepare_start(main_loop, xcb_prep);
- ev_check_start(main_loop, xcb_chk);
/* Now we get the atoms and save them in a nice data structure */
get_atoms();
xcb_aux_sync(xcb_connection);
xcb_disconnect(xcb_connection);
- ev_check_stop(main_loop, xcb_chk);
ev_prepare_stop(main_loop, xcb_prep);
ev_io_stop(main_loop, xcb_io);
- FREE(xcb_chk);
FREE(xcb_prep);
FREE(xcb_io);
}
#include <errno.h>
#include <err.h>
#include <stdint.h>
+#include <inttypes.h>
#include <math.h>
#include <limits.h>
* Implementation of 'resize set <px> [px] <px> [px]'.
*
*/
-void cmd_resize_set(I3_CMD, long cwidth, long cheight);
+void cmd_resize_set(I3_CMD, long cwidth, const char *mode_width, long cheight, const char *mode_height);
/**
* Implementation of 'resize grow|shrink <direction> [<px> px] [or <ppt> ppt]'.
extern xcb_window_t root;
extern struct ev_loop *main_loop;
extern bool only_check_config;
+extern bool force_xinerama;
/** Request the raw last loaded i3 config. */
#define I3_IPC_MESSAGE_TYPE_GET_CONFIG 9
+/** Send a tick event to all subscribers. */
+#define I3_IPC_MESSAGE_TYPE_SEND_TICK 10
+
/*
* Messages from i3 to clients
*
#define I3_IPC_REPLY_TYPE_VERSION 7
#define I3_IPC_REPLY_TYPE_BINDING_MODES 8
#define I3_IPC_REPLY_TYPE_CONFIG 9
+#define I3_IPC_REPLY_TYPE_TICK 10
/*
* Events from i3 to clients. Events have the first bit set high.
/** The shutdown event will be triggered when the ipc shuts down */
#define I3_IPC_EVENT_SHUTDOWN (I3_IPC_EVENT_MASK | 6)
+
+/** The tick event will be sent upon a tick IPC message */
+#define I3_IPC_EVENT_TICK (I3_IPC_EVENT_MASK | 7)
int num_events;
char **events;
+ /* For clients which subscribe to the tick event: whether the first tick
+ * event has been sent by i3. */
+ bool first_tick_sent;
+
TAILQ_ENTRY(ipc_client)
clients;
} ipc_client;
-> RESIZE_WIDTH
state RESIZE_WIDTH:
- 'px'
+ mode_width = 'px', 'ppt'
->
height = number
-> RESIZE_HEIGHT
state RESIZE_HEIGHT:
- 'px', end
- -> call cmd_resize_set(&width, &height)
+ mode_height = 'px', 'ppt'
+ ->
+ end
+ -> call cmd_resize_set(&width, $mode_width, &height, $mode_height)
# rename workspace <name> to <name>
# rename workspace to <name>
}
/*
- * Implementation of 'resize set <px> [px] <px> [px]'.
+ * Implementation of 'resize set <width> [px | ppt] <height> [px | ppt]'.
*
*/
-void cmd_resize_set(I3_CMD, long cwidth, long cheight) {
- DLOG("resizing to %ldx%ld px\n", cwidth, cheight);
+void cmd_resize_set(I3_CMD, long cwidth, const char *mode_width, long cheight, const char *mode_height) {
+ DLOG("resizing to %ld %s x %ld %s\n", cwidth, mode_width, cheight, mode_height);
if (cwidth <= 0 || cheight <= 0) {
- ELOG("Resize failed: dimensions cannot be negative (was %ldx%ld)\n", cwidth, cheight);
+ ELOG("Resize failed: dimensions cannot be negative (was %ld %s x %ld %s)\n", cwidth, mode_width, cheight, mode_height);
return;
}
TAILQ_FOREACH(current, &owindows, owindows) {
Con *floating_con;
if ((floating_con = con_inside_floating(current->con))) {
+ Con *output = con_get_output(floating_con);
+ if (mode_width && strcmp(mode_width, "ppt") == 0) {
+ cwidth = output->rect.width * ((double)cwidth / 100.0);
+ }
+ if (mode_height && strcmp(mode_height, "ppt") == 0) {
+ cheight = output->rect.height * ((double)cheight / 100.0);
+ }
floating_resize(floating_con, cwidth, cheight);
} else {
ELOG("Resize failed: %p not a floating container\n", current->con);
con->workspace_layout = ws_layout;
DLOG("Setting layout to %d\n", layout);
con->layout = layout;
- } else if (layout == L_STACKED || layout == L_TABBED) {
+ } else if (layout == L_STACKED || layout == L_TABBED || layout == L_SPLITV || layout == L_SPLITH) {
DLOG("Creating new split container\n");
/* 1: create a new split container */
Con *new = con_new(NULL, NULL);
* change to the opposite split layout. */
if (parent->layout != L_SPLITH && parent->layout != L_SPLITV) {
layout = parent->last_split_layout;
+ /* In case last_split_layout was not initialized… */
+ if (layout == L_DEFAULT) {
+ layout = L_SPLITH;
+ }
} else {
layout = (parent->layout == L_SPLITH) ? L_SPLITV : L_SPLITH;
}
wait(&status);
if (!WIFEXITED(status)) {
fprintf(stderr, "Child did not terminate normally, using old config file (will lead to broken behaviour)\n");
+ FREE(converted);
return NULL;
}
fprintf(stderr, "# i3 config file (v4)\n");
/* TODO: nag the user with a message to include a hint for i3 in their config file */
}
+ FREE(converted);
return NULL;
}
FREE(current_config);
current_config = scalloc(stbuf.st_size + 1, 1);
- fread(current_config, 1, stbuf.st_size, fstr);
+ if ((ssize_t)fread(current_config, 1, stbuf.st_size, fstr) != stbuf.st_size) {
+ die("Could not fread: %s\n", strerror(errno));
+ }
rewind(fstr);
bool invalid_sets = false;
/* Custom data structure used to track dragging-related events. */
struct drag_x11_cb {
- ev_check check;
+ ev_prepare prepare;
/* Whether this modal event loop should be exited and with which result. */
drag_result_t result;
const void *extra;
};
-static void xcb_drag_check_cb(EV_P_ ev_check *w, int revents) {
+static void xcb_drag_prepare_cb(EV_P_ ev_prepare *w, int revents) {
struct drag_x11_cb *dragloop = (struct drag_x11_cb *)w->data;
xcb_motion_notify_event_t *last_motion_notify = NULL;
xcb_generic_event_t *event;
dragloop->extra);
}
free(last_motion_notify);
+
+ xcb_flush(conn);
}
/*
.callback = callback,
.extra = extra,
};
- ev_check *check = &loop.check;
+ ev_prepare *prepare = &loop.prepare;
if (con)
loop.old_rect = con->rect;
- ev_check_init(check, xcb_drag_check_cb);
- check->data = &loop;
+ ev_prepare_init(prepare, xcb_drag_prepare_cb);
+ prepare->data = &loop;
main_set_x11_cb(false);
- ev_check_start(main_loop, check);
+ ev_prepare_start(main_loop, prepare);
while (loop.result == DRAGGING)
ev_run(main_loop, EVRUN_ONCE);
- ev_check_stop(main_loop, check);
+ ev_prepare_stop(main_loop, prepare);
main_set_x11_cb(true);
xcb_ungrab_keyboard(conn, XCB_CURRENT_TIME);
scratchpad_show(con);
} else {
workspace_show(ws);
+ /* Re-set focus, even if unchanged from i3’s perspective. */
+ focused_id = XCB_NONE;
con_focus(con);
}
} else {
}
DLOG("ConfigureNotify for root window 0x%08x\n", event->event);
+ if (force_xinerama) {
+ return;
+ }
randr_query_outputs();
}
memcpy(client->events[event], s, len);
DLOG("client is now subscribed to:\n");
- for (int i = 0; i < client->num_events; i++)
+ for (int i = 0; i < client->num_events; i++) {
DLOG("event %s\n", client->events[i]);
+ }
DLOG("(done)\n");
return 1;
yajl_free(p);
const char *reply = "{\"success\":true}";
ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_SUBSCRIBE, (const uint8_t *)reply);
+
+ if (client->first_tick_sent) {
+ return;
+ }
+
+ bool is_tick = false;
+ for (int i = 0; i < client->num_events; i++) {
+ if (strcmp(client->events[i], "tick") == 0) {
+ is_tick = true;
+ break;
+ }
+ }
+ if (!is_tick) {
+ return;
+ }
+
+ client->first_tick_sent = true;
+ const char *payload = "{\"first\":true,\"payload\":\"\"}";
+ ipc_send_message(client->fd, strlen(payload), I3_IPC_EVENT_TICK, (const uint8_t *)payload);
}
/*
y(free);
}
+/*
+ * Sends the tick event from the message payload to subscribers. Establishes a
+ * synchronization point in event-related tests.
+ */
+IPC_HANDLER(send_tick) {
+ yajl_gen gen = ygenalloc();
+
+ y(map_open);
+
+ ystr("payload");
+ yajl_gen_string(gen, (unsigned char *)message, message_size);
+
+ y(map_close);
+
+ const unsigned char *payload;
+ ylength length;
+ y(get_buf, &payload, &length);
+
+ ipc_send_event("tick", I3_IPC_EVENT_TICK, (const char *)payload);
+ y(free);
+
+ const char *reply = "{\"success\":true}";
+ ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_TICK, (const uint8_t *)reply);
+ DLOG("Sent tick event\n");
+}
+
/* The index of each callback function corresponds to the numeric
* value of the message type (see include/i3/ipc.h) */
-handler_t handlers[10] = {
+handler_t handlers[11] = {
handle_run_command,
handle_get_workspaces,
handle_subscribe,
handle_get_version,
handle_get_binding_modes,
handle_get_config,
+ handle_send_tick,
};
/*
/** The number of file descriptors passed via socket activation. */
int listen_fds;
-/* We keep the xcb_check watcher around to be able to enable and disable it
+/* We keep the xcb_prepare watcher around to be able to enable and disable it
* temporarily for drag_pointer(). */
-static struct ev_check *xcb_check;
+static struct ev_prepare *xcb_prepare;
extern Con *focused;
bool xcursor_supported = true;
bool xkb_supported = true;
+bool force_xinerama = false;
+
/*
- * This callback is only a dummy, see xcb_prepare_cb and xcb_check_cb.
+ * This callback is only a dummy, see xcb_prepare_cb.
* See also man libev(3): "ev_prepare" and "ev_check" - customise your event loop
*
*/
static void xcb_got_event(EV_P_ struct ev_io *w, int revents) {
- /* empty, because xcb_prepare_cb and xcb_check_cb are used */
+ /* empty, because xcb_prepare_cb are used */
}
/*
- * Flush before blocking (and waiting for new events)
+ * Called just before the event loop sleeps. Ensures xcb’s incoming and outgoing
+ * queues are empty so that any activity will trigger another event loop
+ * iteration, and hence another xcb_prepare_cb invocation.
*
*/
static void xcb_prepare_cb(EV_P_ ev_prepare *w, int revents) {
- xcb_flush(conn);
-}
-
-/*
- * Instead of polling the X connection socket we leave this to
- * xcb_poll_for_event() which knows better than we can ever know.
- *
- */
-static void xcb_check_cb(EV_P_ ev_check *w, int revents) {
+ /* Process all queued (and possibly new) events before the event loop
+ sleeps. */
xcb_generic_event_t *event;
while ((event = xcb_poll_for_event(conn)) != NULL) {
free(event);
}
+
+ /* Flush all queued events to X11. */
+ xcb_flush(conn);
}
/*
void main_set_x11_cb(bool enable) {
DLOG("Setting main X11 callback to enabled=%d\n", enable);
if (enable) {
- ev_check_start(main_loop, xcb_check);
+ ev_prepare_start(main_loop, xcb_prepare);
/* Trigger the watcher explicitly to handle all remaining X11 events.
* drag_pointer()’s event handler exits in the middle of the loop. */
- ev_feed_event(main_loop, xcb_check, 0);
+ ev_feed_event(main_loop, xcb_prepare, 0);
} else {
- ev_check_stop(main_loop, xcb_check);
+ ev_prepare_stop(main_loop, xcb_prepare);
}
}
bool autostart = true;
char *layout_path = NULL;
bool delete_layout_path = false;
- bool force_xinerama = false;
bool disable_randr15 = false;
char *fake_outputs = NULL;
bool disable_signalhandler = false;
config.ipc_socket_path = sstrdup(config.ipc_socket_path);
}
+ if (config.force_xinerama) {
+ force_xinerama = true;
+ }
+
xcb_void_cookie_t cookie;
cookie = xcb_change_window_attributes_checked(conn, root, XCB_CW_EVENT_MASK, (uint32_t[]){ROOT_EVENT_MASK});
xcb_generic_error_t *error = xcb_request_check(conn, cookie);
fake_outputs_init(fake_outputs);
FREE(fake_outputs);
config.fake_outputs = NULL;
- } else if (force_xinerama || config.force_xinerama) {
+ } else if (force_xinerama) {
/* Force Xinerama (for drivers which don't support RandR yet, esp. the
* nVidia binary graphics driver), when specified either in the config
* file or on command-line */
ewmh_update_desktop_viewport();
struct ev_io *xcb_watcher = scalloc(1, sizeof(struct ev_io));
- xcb_check = scalloc(1, sizeof(struct ev_check));
- struct ev_prepare *xcb_prepare = scalloc(1, sizeof(struct ev_prepare));
+ xcb_prepare = scalloc(1, sizeof(struct ev_prepare));
ev_io_init(xcb_watcher, xcb_got_event, xcb_get_file_descriptor(conn), EV_READ);
ev_io_start(main_loop, xcb_watcher);
- ev_check_init(xcb_check, xcb_check_cb);
- ev_check_start(main_loop, xcb_check);
-
ev_prepare_init(xcb_prepare, xcb_prepare_cb);
ev_prepare_start(main_loop, xcb_prepare);
static xcb_connection_t *restore_conn;
static struct ev_io *xcb_watcher;
-static struct ev_check *xcb_check;
static struct ev_prepare *xcb_prepare;
static void restore_handle_event(int type, xcb_generic_event_t *event);
}
static void restore_xcb_prepare_cb(EV_P_ ev_prepare *w, int revents) {
- xcb_flush(restore_conn);
-}
-
-static void restore_xcb_check_cb(EV_P_ ev_check *w, int revents) {
xcb_generic_event_t *event;
if (xcb_connection_has_error(restore_conn)) {
free(event);
}
+
+ xcb_flush(restore_conn);
}
/*
/* This is not the initial connect, but a reconnect, most likely
* because our X11 connection was killed (e.g. by a user with xkill. */
ev_io_stop(main_loop, xcb_watcher);
- ev_check_stop(main_loop, xcb_check);
ev_prepare_stop(main_loop, xcb_prepare);
placeholder_state *state;
*/
xcb_disconnect(restore_conn);
free(xcb_watcher);
- free(xcb_check);
free(xcb_prepare);
}
}
xcb_watcher = scalloc(1, sizeof(struct ev_io));
- xcb_check = scalloc(1, sizeof(struct ev_check));
xcb_prepare = scalloc(1, sizeof(struct ev_prepare));
ev_io_init(xcb_watcher, restore_xcb_got_event, xcb_get_file_descriptor(restore_conn), EV_READ);
ev_io_start(main_loop, xcb_watcher);
- ev_check_init(xcb_check, restore_xcb_check_cb);
- ev_check_start(main_loop, xcb_check);
-
ev_prepare_init(xcb_prepare, restore_xcb_prepare_cb);
ev_prepare_start(main_loop, xcb_prepare);
}
size_t n = fread(*buf, 1, stbuf.st_size, f);
fclose(f);
if ((ssize_t)n != stbuf.st_size) {
- ELOG("File \"%s\" could not be read entirely: got %zd, want %zd\n", path, n, stbuf.st_size);
+ ELOG("File \"%s\" could not be read entirely: got %zd, want %" PRIi64 "\n", path, n, (int64_t)stbuf.st_size);
free(*buf);
*buf = NULL;
return -1;
# subshell or situations like that.
AnyEvent::Util::close_all_fds_except(0, 1, 2);
+our @CLEANUP;
+
# convenience wrapper to write to the log file
my $log;
sub Log { say $log "@_" }
xtrace => 0,
coverage => 0,
restart => 0,
+ xvfb => 1,
);
my $keep_xserver_output = 0;
"valgrind" => \$options{valgrind},
"strace" => \$options{strace},
"xtrace" => \$options{xtrace},
+ "xvfb" => \$options{xvfb},
"display=s" => \@displays,
"parallel=i" => \$parallel,
"help|?" => \$help,
qx(Xephyr -help 2>&1);
die "Xephyr was not found in your path. Please install Xephyr (xserver-xephyr on Debian)." if $?;
+qx(xvfb-run --help 2>&1);
+if ($? && $options{xvfb}) {
+ say "xvfb-run not found, not running tests under xvfb. Install the xvfb package to speed up tests";
+ $options{xvfb} = 0;
+}
+
+if ($options{xvfb}) {
+ for (my $n = 99; $n < 120; $n++) {
+ my $path = File::Temp::tmpnam($ENV{TMPDIR} // "/tmp", "i3-testsXXXXXX");
+ if (!defined(POSIX::mkfifo($path, 0600))) {
+ die "mkfifo: $!";
+ }
+ my $pid = fork // die "fork: $!";
+ if ($pid == 0) {
+ # Child
+
+ # Xvfb checks whether the parent ignores USR1 and sends USR1 to the
+ # parent when ready, so that the wait call will be interrupted. We
+ # can’t implement this in Perl, as Perl’s waitpid transparently
+ # handles -EINTR.
+ exec('/bin/sh', '-c', qq|trap "exit" INT; trap : USR1; (trap '' USR1; exec Xvfb :$n -screen 0 640x480x8 -nolisten tcp) & PID=\$!; wait; if ! kill -0 \$PID 2>/dev/null; then echo 1:\$PID > $path; else echo 0:\$PID > $path; wait \$PID; fi|);
+ die "exec: $!";
+ }
+ chomp(my $kill = slurp($path));
+ unlink($path);
+ my ($code, $xvfbpid) = ($kill =~ m,^([0-1]):(.*)$,);
+ next unless $code eq '0';
+
+ $ENV{DISPLAY} = ":$n";
+ say "Running tests under Xvfb display $ENV{DISPLAY}";
+
+ push(@CLEANUP, sub {
+ kill(15, $xvfbpid);
+ });
+ last;
+ }
+}
+
@displays = split(/,/, join(',', @displays));
@displays = map { s/ //g; $_ } @displays;
sub cleanup {
my $exitcode = $?;
- $_->() for our @CLEANUP;
+ $_->() for @CLEANUP;
exit $exitcode;
}
wait_for_unmap
$x
kill_all_windows
+ events_for
+ listen_for_binding
);
=head1 NAME
my ($class, %args) = @_;
my $pkg = caller;
+ $x ||= i3test::X11->new;
+ # set the pointer to a predictable position in case a previous test has
+ # disturbed it
+ $x->warp_pointer(
+ 0, # src_window (None)
+ $x->get_root_window(), # dst_window (None)
+ 0, # src_x
+ 0, # src_y
+ 0, # src_width
+ 0, # src_height
+ 0, # dst_x
+ 0); # dst_y
+ # Synchronize with X11 to ensure the pointer has been warped before i3
+ # starts up.
+ $x->get_input_focus_reply($x->get_input_focus()->{sequence});
+
$i3_autostart = delete($args{i3_autostart}) // 1;
my $i3_config = delete($args{i3_config}) // '-default';
strict->import;
warnings->import;
- $x ||= i3test::X11->new;
- # set the pointer to a predictable position in case a previous test has
- # disturbed it
- $x->root->warp_pointer(0, 0);
$cv->recv if $i3_autostart;
@_ = ($class);
sub wait_for_event {
my ($timeout, $cb) = @_;
- my $cv = AE::cv;
-
$x->flush;
- # unfortunately, there is no constant for this
- my $ae_read = 0;
-
- my $guard = AE::io $x->get_file_descriptor, $ae_read, sub {
- while (defined(my $event = $x->poll_for_event)) {
- if ($cb->($event)) {
- $cv->send(1);
- last;
- }
- }
- };
-
- # Trigger timeout after $timeout seconds (can be fractional)
- my $t = AE::timer $timeout, 0, sub { warn "timeout ($timeout secs)"; $cv->send(0) };
-
- my $result = $cv->recv;
- undef $t;
- undef $guard;
- return $result;
+ while (defined(my $event = $x->wait_for_event)) {
+ return 1 if $cb->($event);
+ }
}
=head2 wait_for_map($window)
$window->map;
wait_for_map($window);
+
+ # MapWindow is sent before i3 even starts rendering: the window is placed at
+ # temporary off-screen coordinates first, and x_push_changes() sends further
+ # X11 requests to set focus etc. Hence, we sync with i3 before continuing.
+ sync_with_i3();
+
return $window;
}
$_sync_window = open_window(
rect => [ -15, -15, 10, 10 ],
override_redirect => 1,
+ dont_map => 1,
);
}
cmd '[title=".*"] kill';
}
+=head2 events_for($subscribecb, [ $rettype ], [ $eventcbs ])
+
+Helper function which returns an array containing all events of type $rettype
+which were generated by i3 while $subscribecb was running.
+
+Set $eventcbs to subscribe to multiple event types and/or perform your own event
+aggregation.
+
+=cut
+sub events_for {
+ my ($subscribecb, $rettype, $eventcbs) = @_;
+
+ my @events;
+ $eventcbs //= {};
+ if (defined($rettype)) {
+ $eventcbs->{$rettype} = sub { push @events, shift };
+ }
+ my $subscribed = AnyEvent->condvar;
+ my $flushed = AnyEvent->condvar;
+ $eventcbs->{tick} = sub {
+ my ($event) = @_;
+ if ($event->{first}) {
+ $subscribed->send($event);
+ } else {
+ $flushed->send($event);
+ }
+ };
+ my $i3 = i3(get_socket_path(0));
+ $i3->connect->recv;
+ $i3->subscribe($eventcbs)->recv;
+ $subscribed->recv;
+ # Subscription established, run the callback.
+ $subscribecb->();
+ # Now generate a tick event, which we know we’ll receive (and at which point
+ # all other events have been received).
+ my $nonce = int(rand(255)) + 1;
+ $i3->send_tick($nonce);
+
+ my $tick = $flushed->recv;
+ $tester->is_eq($tick->{payload}, $nonce, 'tick nonce received');
+ return @events;
+}
+
+=head2 listen_for_binding($cb)
+
+Helper function to evaluate whether sending KeyPress/KeyRelease events via XTEST
+triggers an i3 key binding or not. Expects key bindings to be configured in the
+form “bindsym <binding> nop <binding>”, e.g. “bindsym Mod4+Return nop
+Mod4+Return”.
+
+ is(listen_for_binding(
+ sub {
+ xtest_key_press(133); # Super_L
+ xtest_key_press(36); # Return
+ xtest_key_release(36); # Return
+ xtest_key_release(133); # Super_L
+ xtest_sync_with_i3;
+ },
+ ),
+ 'Mod4+Return',
+ 'triggered the "Mod4+Return" keybinding');
+
+=cut
+
+sub listen_for_binding {
+ my ($cb) = @_;
+ my $triggered = AnyEvent->condvar;
+ my @events = events_for(
+ $cb,
+ 'binding');
+
+ $tester->is_eq(scalar @events, 1, 'Received precisely one event');
+ $tester->is_eq($events[0]->{change}, 'run', 'change is "run"');
+ # We look at the command (which is “nop <binding>”) because that is easier
+ # than re-assembling the string representation of $event->{binding}.
+ my $command = $events[0]->{binding}->{command};
+ $command =~ s/^nop //g;
+ return $command;
+}
+
=head1 AUTHOR
Michael Stapelberg <michael@i3wm.org>
use Exporter ();
our @EXPORT = qw(
inlinec_connect
+ xtest_sync_with
+ xtest_sync_with_i3
set_xkb_group
xtest_key_press
xtest_key_release
xtest_button_press
xtest_button_release
- listen_for_binding
- start_binding_capture
binding_events
);
# ineffective.
my %sn_config;
BEGIN {
- %sn_config = ExtUtils::PkgConfig->find('xcb-xkb xcb-xtest');
+ %sn_config = ExtUtils::PkgConfig->find('xcb-xkb xcb-xtest xcb-util');
}
use Inline C => Config => LIBS => $sn_config{libs}, CCFLAGS => $sn_config{cflags};
#include <xcb/xcb.h>
#include <xcb/xkb.h>
#include <xcb/xtest.h>
+#include <xcb/xcb_aux.h>
static xcb_connection_t *conn = NULL;
+static xcb_window_t sync_window;
+static xcb_window_t root_window;
+static xcb_atom_t i3_sync_atom;
bool inlinec_connect() {
int screen;
}
free(usereply);
+ xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(conn, xcb_intern_atom(conn, 0, strlen("I3_SYNC"), "I3_SYNC"), NULL);
+ i3_sync_atom = reply->atom;
+ free(reply);
+
+ xcb_screen_t *root_screen = xcb_aux_get_screen(conn, screen);
+ root_window = root_screen->root;
+ sync_window = xcb_generate_id(conn);
+ xcb_create_window(conn,
+ XCB_COPY_FROM_PARENT, // depth
+ sync_window, // window
+ root_window, // parent
+ -15, // x
+ -15, // y
+ 1, // width
+ 1, // height
+ 0, // border_width
+ XCB_WINDOW_CLASS_INPUT_OUTPUT, // class
+ XCB_COPY_FROM_PARENT, // visual
+ XCB_CW_OVERRIDE_REDIRECT, // value_mask
+ (uint32_t[]){
+ 1, // override_redirect
+ }); // value_list
+
return true;
}
+void xtest_sync_with(int window) {
+ xcb_client_message_event_t ev;
+ memset(&ev, '\0', sizeof(xcb_client_message_event_t));
+
+ const int nonce = rand() % 255;
+
+ ev.response_type = XCB_CLIENT_MESSAGE;
+ ev.window = sync_window;
+ ev.type = i3_sync_atom;
+ ev.format = 32;
+ ev.data.data32[0] = sync_window;
+ ev.data.data32[1] = nonce;
+
+ xcb_send_event(conn, false, (xcb_window_t)window, XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, (char *)&ev);
+ xcb_flush(conn);
+
+ xcb_generic_event_t *event = NULL;
+ while (1) {
+ free(event);
+ if ((event = xcb_wait_for_event(conn)) == NULL) {
+ break;
+ }
+ if (event->response_type == 0) {
+ fprintf(stderr, "X11 Error received! sequence %x\n", event->sequence);
+ continue;
+ }
+
+ /* Strip off the highest bit (set if the event is generated) */
+ const int type = (event->response_type & 0x7F);
+ switch (type) {
+ case XCB_CLIENT_MESSAGE: {
+ xcb_client_message_event_t *ev = (xcb_client_message_event_t *)event;
+ {
+ const uint32_t got = ev->data.data32[0];
+ const uint32_t want = sync_window;
+ if (got != want) {
+ fprintf(stderr, "Ignoring ClientMessage: unknown window: got %d, want %d\n", got, want);
+ continue;
+ }
+ }
+ {
+ const uint32_t got = ev->data.data32[1];
+ const uint32_t want = nonce;
+ if (got != want) {
+ fprintf(stderr, "Ignoring ClientMessage: unknown nonce: got %d, want %d\n", got, want);
+ continue;
+ }
+ }
+ return;
+ }
+ default:
+ fprintf(stderr, "Unexpected X11 event of type %d received (XCB_CLIENT_MESSAGE = %d)\n", type, XCB_CLIENT_MESSAGE);
+ break;
+ }
+ }
+ free(event);
+}
+
+void xtest_sync_with_i3() {
+ xtest_sync_with((int)root_window);
+}
+
// NOTE: while |group| should be a uint8_t, Inline::C will not define the
// function unless we use an int.
bool set_xkb_group(int group) {
=cut
-my $i3;
-our @binding_events;
-
-=head2 start_binding_capture()
-
-Captures all binding events sent by i3 in the C<@binding_events> symbol, so
-that you can verify the correct number of binding events was generated.
-
- my $pid = launch_with_config($config);
- start_binding_capture;
- # …
- sync_with_i3;
- is(scalar @i3test::XTEST::binding_events, 2, 'Received exactly 2 binding events');
-
-=cut
-
-sub start_binding_capture {
- # Store a copy of each binding event so that we can count the expected
- # events in test cases.
- $i3 = i3(get_socket_path());
- $i3->connect()->recv;
- $i3->subscribe({
- binding => sub {
- my ($event) = @_;
- @binding_events = (@binding_events, $event);
- },
- })->recv;
-}
-
-=head2 listen_for_binding($cb)
-
-Helper function to evaluate whether sending KeyPress/KeyRelease events via
-XTEST triggers an i3 key binding or not (with a timeout of 0.5s). Expects key
-bindings to be configured in the form “bindsym <binding> nop <binding>”, e.g.
-“bindsym Mod4+Return nop Mod4+Return”.
-
- is(listen_for_binding(
- sub {
- xtest_key_press(133); # Super_L
- xtest_key_press(36); # Return
- xtest_key_release(36); # Return
- xtest_key_release(133); # Super_L
- },
- ),
- 'Mod4+Return',
- 'triggered the "Mod4+Return" keybinding');
-
-=cut
-
-sub listen_for_binding {
- my ($cb) = @_;
- my $triggered = AnyEvent->condvar;
- my $i3 = i3(get_socket_path());
- $i3->connect()->recv;
- $i3->subscribe({
- binding => sub {
- my ($event) = @_;
- return unless $event->{change} eq 'run';
- # We look at the command (which is “nop <binding>”) because that is
- # easier than re-assembling the string representation of
- # $event->{binding}.
- $triggered->send($event->{binding}->{command});
- },
- })->recv;
-
- my $t;
- $t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $triggered->send('timeout');
- }
- );
-
- $cb->();
-
- my $recv = $triggered->recv;
- $recv =~ s/^nop //g;
- return $recv;
-}
-
=head2 set_xkb_group($group)
Changes the current XKB group from the default of 1 to C<$group>, which must be
Returns false when there was an X11 error, true otherwise.
+=head2 xtest_sync_with($window)
+
+Ensures the specified window has processed all X11 events which were triggered
+by this module, provided the window response to the i3 sync protocol.
+
+=head2 xtest_sync_with_i3()
+
+Ensures i3 has processed all X11 events which were triggered by this module.
+
=head1 AUTHOR
Michael Stapelberg <michael@i3wm.org>
use i3test;
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-################################
-# Workspaces requests and events
-################################
-
my $old_ws = get_ws(focused_ws());
-# Events
-
# We are switching to an empty workpspace from an empty workspace, so we expect
# to receive "init", "focus", and "empty".
-my $init = AnyEvent->condvar;
-my $focus = AnyEvent->condvar;
-my $empty = AnyEvent->condvar;
-$i3->subscribe({
- workspace => sub {
- my ($event) = @_;
- if ($event->{change} eq 'init') {
- $init->send($event);
- } elsif ($event->{change} eq 'focus') {
- $focus->send($event);
- } elsif ($event->{change} eq 'empty') {
- $empty->send($event);
- }
- }
-})->recv;
-
-cmd 'workspace 2';
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $init->send(0);
- $focus->send(0);
- $empty->send(0);
- }
-);
-
-my $init_event = $init->recv;
-my $focus_event = $focus->recv;
-my $empty_event = $empty->recv;
+my @events = events_for(
+ sub { cmd 'workspace 2' },
+ 'workspace');
my $current_ws = get_ws(focused_ws());
-ok($init_event, 'workspace "init" event received');
-is($init_event->{current}->{id}, $current_ws->{id}, 'the "current" property should contain the initted workspace con');
+is(scalar @events, 3, 'Received 3 events');
+is($events[0]->{change}, 'init', 'First event has change = init');
+is($events[0]->{current}->{id}, $current_ws->{id}, 'the "current" property contains the initted workspace con');
-ok($focus_event, 'workspace "focus" event received');
-is($focus_event->{current}->{id}, $current_ws->{id}, 'the "current" property should contain the focused workspace con');
-is($focus_event->{old}->{id}, $old_ws->{id}, 'the "old" property should contain the workspace con that was focused last');
+is($events[1]->{change}, 'focus', 'Second event has change = focus');
+is($events[1]->{current}->{id}, $current_ws->{id}, 'the "current" property should contain the focused workspace con');
+is($events[1]->{old}->{id}, $old_ws->{id}, 'the "old" property should contain the workspace con that was focused last');
-ok($empty_event, 'workspace "empty" event received');
-is($empty_event->{current}->{id}, $old_ws->{id}, 'the "current" property should contain the emptied workspace con');
+is($events[2]->{change}, 'empty', 'Third event has change = empty');
+is($events[2]->{current}->{id}, $old_ws->{id}, 'the "current" property should contain the emptied workspace con');
done_testing;
isnt($content[0]->{layout}, 'tabbed', 'layout not tabbed');
isnt($content[1]->{layout}, 'tabbed', 'layout not tabbed');
+exit_gracefully($pid);
+
+#####################################################################
+# 16: Check that the command 'layout toggle split' works regardless
+# of what layout we're using.
+#####################################################################
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+workspace_layout default
+EOT
+
+$pid = launch_with_config($config);
+
+$tmp = fresh_workspace;
+
+my @layouts = ('splith', 'splitv', 'tabbed', 'stacked');
+my $first_layout;
+
+foreach $first_layout (@layouts) {
+ cmd 'layout ' . $first_layout;
+ $first = open_window;
+ $second = open_window;
+ cmd 'layout toggle split';
+ @content = @{get_ws_content($tmp)};
+ if ($first_layout eq 'splith') {
+ is($content[0]->{layout}, 'splitv', 'layout toggles to splitv');
+ } else {
+ is($content[0]->{layout}, 'splith', 'layout toggles to splith');
+ }
+
+ cmd '[id="' . $first->id . '"] kill';
+ cmd '[id="' . $second->id . '"] kill';
+ sync_with_i3;
+}
exit_gracefully($pid);
+#####################################################################
+# 17: Check about setting a new layout.
+#####################################################################
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+workspace_layout default
+EOT
+
+$pid = launch_with_config($config);
+
+$tmp = fresh_workspace;
+
+my $second_layout;
+
+foreach $first_layout (@layouts) {
+ foreach $second_layout (@layouts) {
+ cmd 'layout ' . $first_layout;
+ $first = open_window;
+ $second = open_window;
+ cmd 'layout ' . $second_layout;
+ @content = @{get_ws_content($tmp)};
+ is($content[0]->{layout}, $second_layout, 'layout changes to ' . $second_layout);
+
+ cmd '[id="' . $first->id . '"] kill';
+ cmd '[id="' . $second->id . '"] kill';
+ sync_with_i3;
+ }
+}
+
done_testing;
}
EOT
-my $i3 = i3(get_socket_path(0));
-$i3->connect->recv;
+my @events = events_for(
+ sub { cmd 'mode "m1"' },
+ 'mode');
-my $cv = AnyEvent->condvar;
-
-$i3->subscribe({
- mode => sub {
- my ($event) = @_;
- $cv->send($event->{change} eq 'm1');
- }
-})->recv;
-
-cmd 'mode "m1"';
-
-# Timeout after 0.5s
-my $t;
-$t = AnyEvent->timer(after => 0.5, cb => sub { $cv->send(0); });
-
-ok($cv->recv, 'Mode event received');
+my @changes = map { $_->{change} } @events;
+is_deeply(\@changes, [ 'm1' ], 'Mode event received');
done_testing;
use i3test;
-SKIP: {
-
- skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
-
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-################################
-# Window event
-################################
-
-# Events
-
my $new = AnyEvent->condvar;
my $focus = AnyEvent->condvar;
-$i3->subscribe({
- window => sub {
- my ($event) = @_;
- if ($event->{change} eq 'new') {
- $new->send($event);
- } elsif ($event->{change} eq 'focus') {
- $focus->send($event);
- }
- }
-})->recv;
-
-open_window;
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $new->send(0);
- $focus->send(0);
- }
-);
-is($new->recv->{container}->{focused}, 0, 'Window "new" event received');
-is($focus->recv->{container}->{focused}, 1, 'Window "focus" event received');
+my @events = events_for(
+ sub { open_window },
+ 'window');
-}
+is(scalar @events, 2, 'Received 2 events');
+is($events[0]->{container}->{focused}, 0, 'Window "new" event received');
+is($events[1]->{container}->{focused}, 1, 'Window "focus" event received');
done_testing;
use i3test;
-SKIP: {
-
- skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
-
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
################################
# Window focus event
################################
my $win1 = open_window;
my $win2 = open_window;
-my $focus = AnyEvent->condvar;
-
-$i3->subscribe({
- window => sub {
- my ($event) = @_;
- $focus->send($event);
- }
-})->recv;
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $focus->send(0);
- }
-);
-
# ensure the rightmost window contains input focus
-$i3->command('[id="' . $win2->id . '"] focus')->recv;
+cmd '[id="' . $win2->id . '"] focus';
is($x->input_focus, $win2->id, "Window 2 focused");
-cmd 'focus left';
-my $event = $focus->recv;
-is($event->{change}, 'focus', 'Focus event received');
-is($focus->recv->{container}->{name}, 'Window 1', 'Window 1 focused');
-
-$focus = AnyEvent->condvar;
-cmd 'focus left';
-$event = $focus->recv;
-is($event->{change}, 'focus', 'Focus event received');
-is($event->{container}->{name}, 'Window 0', 'Window 0 focused');
-
-$focus = AnyEvent->condvar;
-cmd 'focus right';
-$event = $focus->recv;
-is($event->{change}, 'focus', 'Focus event received');
-is($event->{container}->{name}, 'Window 1', 'Window 1 focused');
+sub focus_subtest {
+ my ($cmd, $name) = @_;
-$focus = AnyEvent->condvar;
-cmd 'focus right';
-$event = $focus->recv;
-is($event->{change}, 'focus', 'Focus event received');
-is($event->{container}->{name}, 'Window 2', 'Window 2 focused');
+ my $focus = AnyEvent->condvar;
-$focus = AnyEvent->condvar;
-cmd 'focus right';
-$event = $focus->recv;
-is($event->{change}, 'focus', 'Focus event received');
-is($event->{container}->{name}, 'Window 0', 'Window 0 focused');
-
-$focus = AnyEvent->condvar;
-cmd 'focus left';
-$event = $focus->recv;
-is($event->{change}, 'focus', 'Focus event received');
-is($event->{container}->{name}, 'Window 2', 'Window 2 focused');
+ my @events = events_for(
+ sub { cmd $cmd },
+ 'window');
+ is(scalar @events, 1, 'Received 1 event');
+ is($events[0]->{change}, 'focus', 'Focus event received');
+ is($events[0]->{container}->{name}, $name, "$name focused");
}
+subtest 'focus left (1)', \&focus_subtest, 'focus left', $win1->name;
+subtest 'focus left (2)', \&focus_subtest, 'focus left', $win0->name;
+subtest 'focus right (1)', \&focus_subtest, 'focus right', $win1->name;
+subtest 'focus right (2)', \&focus_subtest, 'focus right', $win2->name;
+subtest 'focus right (3)', \&focus_subtest, 'focus right', $win0->name;
+subtest 'focus left', \&focus_subtest, 'focus left', $win2->name;
+
done_testing;
use i3test;
-SKIP: {
-
- skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
-
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-################################
-# Window title event
-################################
-
my $window = open_window(name => 'Window 0');
-my $title = AnyEvent->condvar;
-
-$i3->subscribe({
- window => sub {
- my ($event) = @_;
- $title->send($event);
- }
-})->recv;
-
-$window->name('New Window Title');
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $title->send(0);
- }
-);
-
-my $event = $title->recv;
-is($event->{change}, 'title', 'Window title change event received');
-is($event->{container}->{name}, 'New Window Title', 'Window title changed');
+my @events = events_for(
+ sub {
+ $window->name('New Window Title');
+ sync_with_i3;
+ },
+ 'window');
-}
+is(scalar @events, 1, 'Received 1 event');
+is($events[0]->{change}, 'title', 'Window title change event received');
+is($events[0]->{container}->{name}, 'New Window Title', 'Window title changed');
done_testing;
# Bug still in: 4.7.2-135-g7deb23c
use i3test;
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
+open_window;
-my $cv;
-my $t;
+sub fullscreen_subtest {
+ my ($want) = @_;
+ my @events = events_for(
+ sub { cmd 'fullscreen' },
+ 'window');
-sub reset_test {
- $cv = AE::cv;
- $t = AE::timer(0.5, 0, sub { $cv->send(0); });
+ is(scalar @events, 1, 'Received 1 event');
+ is($events[0]->{container}->{fullscreen_mode}, $want, "fullscreen_mode now $want");
}
-reset_test;
-
-$i3->subscribe({
- window => sub {
- my ($e) = @_;
- if ($e->{change} eq 'fullscreen_mode') {
- $cv->send($e->{container});
- }
- },
- })->recv;
-
-my $window = open_window;
-
-cmd 'fullscreen';
-my $con = $cv->recv;
-
-ok($con, 'got fullscreen window event (on)');
-is($con->{fullscreen_mode}, 1, 'window is fullscreen');
-
-reset_test;
-cmd 'fullscreen';
-$con = $cv->recv;
-
-ok($con, 'got fullscreen window event (off)');
-is($con->{fullscreen_mode}, 0, 'window is not fullscreen');
+subtest 'fullscreen on', \&fullscreen_subtest, 1;
+subtest 'fullscreen off', \&fullscreen_subtest, 0;
done_testing;
#
use i3test;
-SKIP: {
-
- skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
-
################################################################################
-# check that the workspace empty event is send upon workspace switch when the
+# check that the workspace empty event is sent upon workspace switch when the
# old workspace is empty
################################################################################
subtest 'Workspace empty event upon switch', sub {
cmd '[id="' . $w1->id . '"] kill';
my $cond = AnyEvent->condvar;
- my $client = i3(get_socket_path(0));
- $client->connect()->recv;
- $client->subscribe({
- workspace => sub {
- my ($event) = @_;
- $cond->send($event);
- }
- })->recv;
-
- cmd "workspace $ws2";
-
- sync_with_i3;
+ my @events = events_for(
+ sub { cmd "workspace $ws2" },
+ 'workspace');
- my $event = $cond->recv;
- is($event->{change}, 'empty', '"Empty" event received upon workspace switch');
- is($event->{current}->{name}, $ws1, '"current" property should be set to the workspace con');
+ is(scalar @events, 2, 'Received 2 event');
+ is($events[1]->{change}, 'empty', '"Empty" event received upon workspace switch');
+ is($events[1]->{current}->{name}, $ws1, '"current" property should be set to the workspace con');
};
################################################################################
-# check that no workspace empty event is send upon workspace switch if the
+# check that no workspace empty event is sent upon workspace switch if the
# workspace is not empty
################################################################################
subtest 'No workspace empty event', sub {
my $ws1 = fresh_workspace;
my $w1 = open_window();
- my @events;
- my $cond = AnyEvent->condvar;
- my $client = i3(get_socket_path(0));
- $client->connect()->recv;
- $client->subscribe({
- workspace => sub {
- my ($event) = @_;
- push @events, $event;
- }
- })->recv;
-
- # Wait for the workspace event on a new connection. Events will be delivered
- # to older connections earlier, so by the time it arrives here, it should be
- # in @events already.
- my $ws_event_block_conn = i3(get_socket_path(0));
- $ws_event_block_conn->connect()->recv;
- $ws_event_block_conn->subscribe({ workspace => sub { $cond->send(1) }});
-
- cmd "workspace $ws2";
+ my @events = events_for(
+ sub { cmd "workspace $ws2" },
+ 'workspace');
- sync_with_i3;
-
- my @expected_events = grep { $_->{change} eq 'focus' } @events;
- my @empty_events = grep { $_->{change} eq 'empty' } @events;
- is(@expected_events, 1, '"Focus" event received');
- is(@empty_events, 0, 'No "empty" events received');
+ is(scalar @events, 1, 'Received 1 event');
+ is($events[0]->{change}, 'focus', 'Event change is "focus"');
};
################################################################################
-# check that workspace empty event is send when the last window has been closed
+# check that workspace empty event is sent when the last window has been closed
# on invisible workspace
################################################################################
subtest 'Workspace empty event upon window close', sub {
my $ws2 = fresh_workspace;
my $w2 = open_window();
- my $cond = AnyEvent->condvar;
- my $client = i3(get_socket_path(0));
- $client->connect()->recv;
- $client->subscribe({
- workspace => sub {
- my ($event) = @_;
- $cond->send($event);
- }
- })->recv;
-
- cmd '[id="' . $w1->id . '"] kill';
-
- sync_with_i3;
+ my @events = events_for(
+ sub {
+ $w1->unmap;
+ sync_with_i3;
+ },
+ 'workspace');
- my $event = $cond->recv;
- is($event->{change}, 'empty', '"Empty" event received upon window close');
- is($event->{current}->{name}, $ws1, '"current" property should be set to the workspace con');
+ is(scalar @events, 1, 'Received 1 event');
+ is($events[0]->{change}, 'empty', '"Empty" event received upon window close');
+ is($events[0]->{current}->{name}, $ws1, '"current" property should be set to the workspace con');
};
-}
-
done_testing;
# Bug still in: 4.8-7-gf4a8253
use i3test;
-my $i3 = i3(get_socket_path());
-$i3->connect->recv;
+sub floating_subtest {
+ my ($win, $cmd, $want) = @_;
-my $cv = AnyEvent->condvar;
+ my @events = events_for(
+ sub { cmd $cmd },
+ 'window');
-$i3->subscribe({
- window => sub {
- my ($event) = @_;
- $cv->send($event) if $event->{change} eq 'floating';
- }
- })->recv;
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $cv->send(0);
- }
-);
+ my @floating = grep { $_->{change} eq 'floating' } @events;
+ is(scalar @floating, 1, 'Received 1 floating event');
+ is($floating[0]->{container}->{window}, $win->{id}, "window id matches");
+ is($floating[0]->{container}->{floating}, $want, "floating is $want");
+}
my $win = open_window();
-cmd '[id="' . $win->{id} . '"] floating enable';
-my $e = $cv->recv;
-
-isnt($e, 0, 'floating a container should send an ipc window event');
-is($e->{container}->{window}, $win->{id}, 'the event should contain information about the window');
-is($e->{container}->{floating}, 'user_on', 'the container should be floating');
-
-$cv = AnyEvent->condvar;
-cmd '[id="' . $win->{id} . '"] floating disable';
-$e = $cv->recv;
-
-isnt($e, 0, 'disabling floating on a container should send an ipc window event');
-is($e->{container}->{window}, $win->{id}, 'the event should contain information about the window');
-is($e->{container}->{floating}, 'user_off', 'the container should not be floating');
+subtest 'floating enable', \&floating_subtest, $win, '[id="' . $win->{id} . '"] floating enable', 'user_on';
+subtest 'floating disable', \&floating_subtest, $win, '[id="' . $win->{id} . '"] floating disable', 'user_off';
done_testing;
skip 'xdotool is required to test the binding event. `[apt-get install|pacman -S] xdotool`', 1 if $?;
- skip "AnyEvent::I3 too old (need >= 0.16)", 1 if $AnyEvent::I3::VERSION < 0.16;
-
my $pid = launch_with_config($config);
- my $i3 = i3(get_socket_path());
- $i3->connect->recv;
-
- my $cv = AE::cv;
- my $timer = AE::timer 0.5, 0, sub { $cv->send(0); };
-
- $i3->subscribe({
- binding => sub {
- $cv->send(shift);
- }
- })->recv;
-
- qx(xdotool key $binding_symbol);
-
- my $e = $cv->recv;
-
- does_i3_live;
+ my $cv = AnyEvent->condvar;
- diag "Event:\n", Dumper($e);
+ my @events = events_for(
+ sub {
+ # TODO: this is still flaky: we need to synchronize every X11
+ # connection with i3. Move to XTEST and synchronize that connection.
+ qx(xdotool key $binding_symbol);
+ },
+ 'binding');
- ok($e,
- 'the binding event should emit when user input triggers an i3 binding event');
+ is(scalar @events, 1, 'Received 1 event');
- is($e->{change}, 'run',
+ is($events[0]->{change}, 'run',
'the `change` field should indicate this binding has run');
- ok($e->{binding},
+ ok($events[0]->{binding},
'the `binding` field should be a hash that contains information about the binding');
- is($e->{binding}->{input_type}, 'keyboard',
+ is($events[0]->{binding}->{input_type}, 'keyboard',
'the input_type field should be the input type of the binding (keyboard or mouse)');
note 'the `mods` field should contain the symbols for the modifiers of the binding';
foreach (@mods) {
- ok(grep(/$_/i, @{$e->{binding}->{mods}}), "`mods` contains the modifier $_");
+ ok(grep(/$_/i, @{$events[0]->{binding}->{mods}}), "`mods` contains the modifier $_");
}
- is($e->{binding}->{command}, $command,
+ is($events[0]->{binding}->{command}, $command,
'the `command` field should contain the command the binding ran');
- is($e->{binding}->{input_code}, 0,
+ is($events[0]->{binding}->{input_code}, 0,
'the input_code should be the specified code if the key was bound with bindcode, and otherwise zero');
exit_gracefully($pid);
# Test behavior of "resize <width> <height>" command.
# Ticket: #1727
# Bug still in: 4.10.2-1-gc0dbc5d
-use i3test;
+use i3test i3_config => <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+fake-outputs 1333x999+0+0
+workspace ws output fake-0
+EOT
################################################################################
# Check that setting floating windows size works
cmp_ok($content[0]->{rect}->{width}, '==', 100, 'width changed to 100 px');
cmp_ok($content[0]->{rect}->{height}, '==', 250, 'height changed to 250 px');
+################################################################################
+# Same but with ppt instead of px
+################################################################################
+
+kill_all_windows;
+$tmp = 'ws';
+cmd "workspace $tmp";
+open_floating_window;
+
+@content = @{get_ws($tmp)->{floating_nodes}};
+is(@content, 1, 'one floating node on this ws');
+
+$oldrect = $content[0]->{rect};
+
+cmd 'resize set 33 ppt 20 ppt';
+my $expected_width = int(0.33 * 1333);
+my $expected_height = int(0.2 * 999);
+
+@content = @{get_ws($tmp)->{floating_nodes}};
+cmp_ok($content[0]->{rect}->{x}, '==', $oldrect->{x}, 'x untouched');
+cmp_ok($content[0]->{rect}->{y}, '==', $oldrect->{y}, 'y untouched');
+cmp_ok($content[0]->{rect}->{width}, '!=', $oldrect->{width}, 'width changed');
+cmp_ok($content[0]->{rect}->{height}, '!=', $oldrect->{width}, 'height changed');
+cmp_ok($content[0]->{rect}->{width}, '==', $expected_width, "width changed to $expected_width px");
+cmp_ok($content[0]->{rect}->{height}, '==', $expected_height, "height changed to $expected_height px");
+
+################################################################################
+# Mix ppt and px in a single resize set command
+################################################################################
+
+cmd 'resize set 44 ppt 111 px';
+my $expected_width = int(0.44 * 1333);
+my $expected_height = 111;
+
+@content = @{get_ws($tmp)->{floating_nodes}};
+cmp_ok($content[0]->{rect}->{x}, '==', $oldrect->{x}, 'x untouched');
+cmp_ok($content[0]->{rect}->{y}, '==', $oldrect->{y}, 'y untouched');
+cmp_ok($content[0]->{rect}->{width}, '==', $expected_width, "width changed to $expected_width px");
+cmp_ok($content[0]->{rect}->{height}, '==', $expected_height, "height changed to $expected_height px");
+
+cmd 'resize set 222 px 100 ppt';
+my $expected_width = 222;
+my $expected_height = 999;
+
+@content = @{get_ws($tmp)->{floating_nodes}};
+cmp_ok($content[0]->{rect}->{x}, '==', $oldrect->{x}, 'x untouched');
+cmp_ok($content[0]->{rect}->{y}, '==', $oldrect->{y}, 'y untouched');
+cmp_ok($content[0]->{rect}->{width}, '==', $expected_width, "width changed to $expected_width px");
+cmp_ok($content[0]->{rect}->{height}, '==', $expected_height, "height changed to $expected_height px");
+
done_testing;
skip "setxkbmap not found", 1 if
system(q|setxkbmap -print >/dev/null|) != 0;
-start_binding_capture;
-
system(q|setxkbmap us,ru -option grp:alt_shift_toggle|);
is(listen_for_binding(
sub {
xtest_key_press(107);
xtest_key_release(107);
+ xtest_sync_with_i3;
},
),
'Print',
xtest_key_press(36); # Return
xtest_key_release(36); # Return
xtest_key_release(133); # Super_L
+ xtest_sync_with_i3;
},
),
'Mod4+Return',
sub {
xtest_key_press(107);
xtest_key_release(107);
+ xtest_sync_with_i3;
},
),
'Print',
xtest_key_press(36); # Return
xtest_key_release(36); # Return
xtest_key_release(133); # Super_L
+ xtest_sync_with_i3;
},
),
'Mod4+Return',
'triggered the "Mod4+Return" keybinding');
-sync_with_i3;
-is(scalar @i3test::XTEST::binding_events, 4, 'Received exactly 4 binding events');
-
# Disable the grp:alt_shift_toggle option, as we use Alt+Shift in other testcases.
system(q|setxkbmap us -option|);
skip "libxcb-xkb too old (need >= 1.11)", 1 unless
ExtUtils::PkgConfig->atleast_version('xcb-xkb', '1.11');
-start_binding_capture;
-
is(listen_for_binding(
sub {
xtest_key_press(107); # Print
xtest_key_release(107); # Print
+ xtest_sync_with_i3;
},
),
'Print',
xtest_key_press(107); # Print
xtest_key_release(107); # Print
xtest_key_release(37); # Control_L
+ xtest_sync_with_i3;
},
),
'Control+Print',
xtest_key_press(56); # b
xtest_key_release(56); # b
xtest_key_release(64); # Alt_L
+ xtest_sync_with_i3;
},
),
'Mod1+b',
xtest_key_release(56); # b
xtest_key_release(50); # Shift_L
xtest_key_release(64); # Alt_L
+ xtest_sync_with_i3;
},
),
'Mod1+Shift+b release',
'triggered the "Mod1+Shift+b" release keybinding');
-sync_with_i3;
-is(scalar @i3test::XTEST::binding_events, 4, 'Received exactly 4 binding events');
-
}
done_testing;
cmd 'mode othermode';
-my $i3 = i3(get_socket_path(0));
-$i3->connect->recv;
+my @events = events_for(
+ sub { cmd 'reload' },
+ 'mode');
-my $cv = AnyEvent->condvar;
-$i3->subscribe({
- mode => sub {
- my ($event) = @_;
- $cv->send($event->{change} eq 'default');
- }
-})->recv;
-
-cmd 'reload';
-
-# Timeout after 0.5s
-my $t;
-$t = AnyEvent->timer(after => 0.5, cb => sub { $cv->send(0); });
-
-ok($cv->recv, 'Mode event received');
+is(scalar @events, 1, 'Received 1 event');
+is($events[0]->{change}, 'default', 'change is "default"');
done_testing;
# Ticket: #2501
use i3test;
-my ($i3, $timer, $event, $mark);
+sub mark_subtest {
+ my ($cmd) = @_;
-$i3 = i3(get_socket_path());
-$i3->connect()->recv;
+ my @events = events_for(
+ sub { cmd $cmd },
+ 'window');
-$i3->subscribe({
- window => sub {
- my ($event) = @_;
- return unless defined $mark;
- return unless $event->{change} eq 'mark';
-
- $mark->send($event);
- }
-})->recv;
-
-$timer = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $mark->send(0);
- }
-);
+ my @mark = grep { $_->{change} eq 'mark' } @events;
+ is(scalar @mark, 1, 'Received 1 window::mark event');
+}
###############################################################################
# Marking a container triggers a 'mark' event.
fresh_workspace;
open_window;
-$mark = AnyEvent->condvar;
-cmd 'mark x';
-
-$event = $mark->recv;
-ok($event, 'window::mark event has been received');
+subtest 'mark', \&mark_subtest, 'mark x';
###############################################################################
# Unmarking a container triggers a 'mark' event.
open_window;
cmd 'mark x';
-$mark = AnyEvent->condvar;
-cmd 'unmark x';
-
-$event = $mark->recv;
-ok($event, 'window::mark event has been received');
+subtest 'unmark', \&mark_subtest, 'unmark x';
###############################################################################
my $i3 = i3(get_socket_path());
$i3->connect->recv;
-my $cv = AE::cv;
-my $timer = AE::timer 0.5, 0, sub { $cv->send(0); };
+my $cv = AnyEvent->condvar;
+my $timer = AnyEvent->timer(after => 0.5, interval => 0, cb => sub { $cv->send(0); });
my $last_config = $i3->get_config()->recv;
chomp($last_config->{config});
# Bug still in: 4.8-7-gf4a8253
use i3test;
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-my $cv;
-my $t;
-
-sub reset_test {
- $cv = AE::cv;
- $t = AE::timer(0.5, 0, sub { $cv->send(0); });
-}
-
-reset_test;
-
-$i3->subscribe({
- window => sub {
- my ($e) = @_;
- if ($e->{change} eq 'close') {
- $cv->send($e->{container});
- }
- },
- })->recv;
-
my $window = open_window;
-cmd 'kill';
-my $con = $cv->recv;
+my @events = events_for(
+ sub {
+ $window->unmap;
+ sync_with_i3;
+ },
+ 'window');
-ok($con, 'closing a window should send the window::close event');
-is($con->{window}, $window->{id}, 'the event should contain information about the window');
+my @close = grep { $_->{change} eq 'close' } @events;
+is(scalar @close, 1, 'Received 1 window::close event');
+is($close[0]->{container}->{window}, $window->{id}, 'the event should contain information about the window');
done_testing;
# Bug still in: 4.8-7-gf4a8253
use i3test;
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-my $cv;
-my $t;
-
-sub reset_test {
- $cv = AE::cv;
- $t = AE::timer(0.5, 0, sub { $cv->send(0); });
-}
-
-reset_test;
-
-$i3->subscribe({
- window => sub {
- my ($e) = @_;
- if ($e->{change} eq 'move') {
- $cv->send($e->{container});
- }
- },
- })->recv;
-
my $dummy_window = open_window;
my $window = open_window;
-cmd 'move right';
-my $con = $cv->recv;
-
-ok($con, 'moving a window should emit the window::move event');
-is($con->{window}, $window->{id}, 'the event should contain info about the window');
+sub move_subtest {
+ my ($cmd) = @_;
+ my $cv = AnyEvent->condvar;
+ my @events = events_for(
+ sub { cmd $cmd },
+ 'window');
-reset_test;
-
-cmd 'move to workspace ws_new';
-$con = $cv->recv;
+ my @move = grep { $_->{change} eq 'move' } @events;
+ is(scalar @move, 1, 'Received 1 window::move event');
+ is($move[0]->{container}->{window}, $window->{id}, 'window id matches');
+}
-ok($con, 'moving a window to a different workspace should emit the window::move event');
-is($con->{window}, $window->{id}, 'the event should contain info about the window');
+subtest 'move right', \&move_subtest, 'move right';
+subtest 'move to workspace', \&move_subtest, 'move to workspace ws_new';
done_testing;
#
use i3test;
-my $config = <<EOT;
-# i3 config file (v4)
-font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
-
-force_display_urgency_hint 0ms
-EOT
-
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-my $cv;
-$i3->subscribe({
- window => sub {
- my ($event) = @_;
- $cv->send($event) if $event->{change} eq 'urgent';
- }
-})->recv;
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $cv->send(0);
- }
-);
-
-$cv = AnyEvent->condvar;
fresh_workspace;
my $win = open_window;
my $dummy_win = open_window;
-$win->add_hint('urgency');
-my $event = $cv->recv;
-
-isnt($event, 0, 'an urgent con should emit the window::urgent event');
-is($event->{container}->{window}, $win->{id}, 'the event should contain information about the window');
-is($event->{container}->{urgent}, 1, 'the container should be urgent');
-
-$cv = AnyEvent->condvar;
-$win->delete_hint('urgency');
-$event = $cv->recv;
-
-isnt($event, 0, 'an urgent con should emit the window::urgent event');
-is($event->{container}->{window}, $win->{id}, 'the event should contain information about the window');
-is($event->{container}->{urgent}, 0, 'the container should not be urgent');
+sub urgency_subtest {
+ my ($subscribecb, $win, $want) = @_;
+
+ my @events = events_for(
+ $subscribecb,
+ 'window');
+
+ my @urgent = grep { $_->{change} eq 'urgent' } @events;
+ is(scalar @urgent, 1, 'Received 1 window::urgent event');
+ is($urgent[0]->{container}->{window}, $win->{id}, "window id matches");
+ is($urgent[0]->{container}->{urgent}, $want, "urgent is $want");
+}
+
+subtest "urgency set", \&urgency_subtest,
+ sub {
+ $win->add_hint('urgency');
+ sync_with_i3;
+ },
+ $win,
+ 1;
+
+subtest "urgency unset", \&urgency_subtest,
+ sub {
+ $win->delete_hint('urgency');
+ sync_with_i3;
+ },
+ $win,
+ 0;
done_testing;
xtest_button_press(4, 50, 50);
xtest_button_release(4, 50, 50);
-sync_with_i3;
+xtest_sync_with_i3;
is(focused_ws(), 'special', 'the binding was triggered');
# Bug still in: 4.12-46-g2123888
use i3test;
-SKIP: {
- skip "AnyEvent::I3 too old (need >= 0.17)", 1 if $AnyEvent::I3::VERSION < 0.17;
+# We cannot use events_for in this test as we cannot send events after
+# issuing the restart/shutdown command.
my $i3 = i3(get_socket_path());
$i3->connect->recv;
-my $cv = AE::cv;
-my $timer = AE::timer 0.5, 0, sub { $cv->send(0); };
+my $cv = AnyEvent->condvar;
+my $timer = AnyEvent->timer(after => 0.5, interval => 0, cb => sub { $cv->send(0); });
$i3->subscribe({
shutdown => sub {
$i3 = i3(get_socket_path());
$i3->connect->recv;
-$cv = AE::cv;
-$timer = AE::timer 0.5, 0, sub { $cv->send(0); };
+$cv = AnyEvent->condvar;
+$timer = AnyEvent->timer(after => 0.5, interval => 0, cb => sub { $cv->send(0); });
$i3->subscribe({
shutdown => sub {
diag "Event:\n", Dumper($e);
ok($e, 'the shutdown event should emit when the ipc is exited by command');
is($e->{change}, 'exit', 'the `change` field should tell the reason for the shutdown');
-}
done_testing;
my $pid = launch_with_config($config);
-start_binding_capture;
-
is(listen_for_binding(
sub {
xtest_key_press(87); # KP_End
xtest_key_release(87); # KP_End
+ xtest_sync_with_i3;
},
),
'KP_End',
xtest_key_release(87); # KP_1
xtest_key_press(77); # disable Num_Lock
xtest_key_release(77); # disable Num_Lock
+ xtest_sync_with_i3;
},
),
'KP_1',
xtest_key_press(38); # a
xtest_key_release(38); # a
xtest_key_release(133); # Super_L
+ xtest_sync_with_i3;
},
),
'a',
xtest_key_release(133); # Super_L
xtest_key_press(77); # disable Num_Lock
xtest_key_release(77); # disable Num_Lock
+ xtest_sync_with_i3;
},
),
'a',
sub {
xtest_key_press(9); # Escape
xtest_key_release(9); # Escape
+ xtest_sync_with_i3;
},
),
'Escape',
xtest_key_release(9); # Escape
xtest_key_press(77); # disable Num_Lock
xtest_key_release(77); # disable Num_Lock
+ xtest_sync_with_i3;
},
),
'Escape',
xtest_key_press(9); # Escape
xtest_key_release(9); # Escape
xtest_key_release(50); # Shift_L
+ xtest_sync_with_i3;
},
),
'Shift+Escape',
xtest_key_release(50); # Shift_L
xtest_key_press(77); # disable Num_Lock
xtest_key_release(77); # disable Num_Lock
+ xtest_sync_with_i3;
},
),
'Shift+Escape',
xtest_key_release(24); # q
xtest_key_release(64); # Alt_L
xtest_key_release(50); # Shift_L
+ xtest_sync_with_i3;
},
),
'Mod1+Shift+q',
xtest_key_release(50); # Shift_L
xtest_key_press(77); # disable Num_Lock
xtest_key_release(77); # disable Num_Lock
+ xtest_sync_with_i3;
},
),
'Mod1+Shift+q',
sub {
xtest_key_press(39); # s
xtest_key_release(39); # s
+ xtest_sync_with_i3;
},
),
's',
xtest_key_release(39); # s
xtest_key_press(77); # disable Num_Lock
xtest_key_release(77); # disable Num_Lock
+ xtest_sync_with_i3;
},
),
's',
'triggered the "s" keybinding with Num_Lock');
-sync_with_i3;
-is(scalar @i3test::XTEST::binding_events, 12, 'Received exactly 12 binding events');
-
exit_gracefully($pid);
################################################################################
$pid = launch_with_config($config);
-start_binding_capture;
-
is(listen_for_binding(
sub {
xtest_key_press(133); # Super_L
xtest_key_release(133); # Super_L
+ xtest_sync_with_i3;
},
),
'Super_L',
xtest_key_release(133); # Super_L
xtest_key_press(77); # disable Num_Lock
xtest_key_release(77); # disable Num_Lock
+ xtest_sync_with_i3;
},
),
'Super_L',
xtest_key_press(36); # Return
xtest_key_release(36); # Return
xtest_key_release(133); # Super_L
+ xtest_sync_with_i3;
},
),
'Return',
xtest_key_release(133); # Super_L
xtest_key_press(77); # disable Num_Lock
xtest_key_release(77); # disable Num_Lock
+ xtest_sync_with_i3;
},
),
'Return',
'triggered the "Return" keybinding with Num_Lock');
-sync_with_i3;
-is(scalar @i3test::XTEST::binding_events, 16, 'Received exactly 16 binding events');
-
exit_gracefully($pid);
################################################################################
$pid = launch_with_config($config);
-start_binding_capture;
-
is(listen_for_binding(
sub {
xtest_key_press(87); # KP_End
xtest_key_release(87); # KP_End
+ xtest_sync_with_i3;
},
),
'KP_End',
sub {
xtest_key_press(88); # KP_Down
xtest_key_release(88); # KP_Down
+ xtest_sync_with_i3;
},
),
'KP_Down',
'triggered the "KP_Down" keybinding');
-is(listen_for_binding(
+my @unexpected = events_for(
sub {
xtest_key_press(77); # enable Num_Lock
xtest_key_release(77); # enable Num_Lock
xtest_key_release(87); # KP_1
xtest_key_press(77); # disable Num_Lock
xtest_key_release(77); # disable Num_Lock
+ xtest_sync_with_i3;
},
- ),
- 'timeout',
- 'Did not trigger the KP_End keybinding with KP_1');
+ 'binding');
+is(scalar @unexpected, 0, 'Did not trigger the KP_End keybinding with KP_1');
-is(listen_for_binding(
+my @unexpected2 = events_for(
sub {
xtest_key_press(77); # enable Num_Lock
xtest_key_release(77); # enable Num_Lock
xtest_key_release(88); # KP_2
xtest_key_press(77); # disable Num_Lock
xtest_key_release(77); # disable Num_Lock
+ xtest_sync_with_i3;
},
- ),
- 'timeout',
- 'Did not trigger the KP_Down keybinding with KP_2');
+ 'binding');
-# TODO: This test does not verify that i3 does _NOT_ grab keycode 87 with Mod2.
+is(scalar @unexpected2, 0, 'Did not trigger the KP_Down keybinding with KP_2');
-sync_with_i3;
-is(scalar @i3test::XTEST::binding_events, 18, 'Received exactly 18 binding events');
+# TODO: This test does not verify that i3 does _NOT_ grab keycode 87 with Mod2.
exit_gracefully($pid);
my $win = open_window;
-start_binding_capture;
-
is(listen_for_binding(
sub {
xtest_key_press(77); # enable Num_Lock
xtest_button_release(4, 50, 50);
xtest_key_press(77); # disable Num_Lock
xtest_key_release(77); # disable Num_Lock
+ xtest_sync_with_i3;
},
),
'button4',
is(listen_for_binding(
sub {
- xtest_button_press(4, 50, 50);
- xtest_button_release(4, 50, 50);
+ xtest_button_press(4, 50, 50);
+ xtest_button_release(4, 50, 50);
+ xtest_sync_with_i3;
},
),
'button4',
#
# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
# (unless you are already familiar with Perl)
-#
+#
# Ticket: #990
# Bug still in: 4.5.1-23-g82b5978
fake-outputs 1024x768+0+0,1024x768+1024+0
EOT
-my $i3 = i3(get_socket_path());
-
-$i3->connect()->recv;
-
-################################
-# Workspaces requests and events
-################################
-
my $old_ws = get_ws(focused_ws);
-# Events
-
-# We are switching to an empty workpspace on the output to the right from an empty workspace on the output on the left, so we expect
-# to receive "init", "focus", and "empty".
my $focus = AnyEvent->condvar;
-$i3->subscribe({
- workspace => sub {
- my ($event) = @_;
- if ($event->{change} eq 'focus') {
- $focus->send($event);
- }
- }
-})->recv;
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $focus->send(0);
- }
-);
-
-cmd 'focus output right';
-
-my $event = $focus->recv;
+my @events = events_for(
+ sub { cmd 'focus output right' },
+ 'workspace');
my $current_ws = get_ws(focused_ws);
-ok($event, 'Workspace "focus" event received');
-is($event->{current}->{id}, $current_ws->{id}, 'Event gave correct current workspace');
-is($event->{old}->{id}, $old_ws->{id}, 'Event gave correct old workspace');
+is(scalar @events, 1, 'Received 1 event');
+is($events[0]->{current}->{id}, $current_ws->{id}, 'Event gave correct current workspace');
+is($events[0]->{old}->{id}, $old_ws->{id}, 'Event gave correct old workspace');
done_testing;
workspace ws-right output fake-1
EOT
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-# subscribe to the 'focus' ipc event
-my $focus = AnyEvent->condvar;
-$i3->subscribe({
- workspace => sub {
- my ($event) = @_;
- if ($event->{change} eq 'focus') {
- $focus->send($event);
- }
- }
-})->recv;
-
-# give up after 0.5 seconds
-my $timer = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $focus->send(0);
- }
-);
-
# open two windows on the left output
cmd 'workspace ws-left';
open_window;
open_window;
-# move a window over to the right output
-cmd 'move right';
-my $event = $focus->recv;
+sub focus_subtest {
+ my ($cmd, $want) = @_;
-ok($event, 'moving from workspace with two windows triggered focus ipc event');
-is($event->{current}->{name}, 'ws-right', 'focus event gave the right workspace');
-is(@{$event->{current}->{nodes}}, 1, 'focus event gave the right number of windows on the workspace');
+ my @events = events_for(
+ sub { cmd $cmd },
+ 'workspace');
-# reset and try again
-$focus = AnyEvent->condvar;
-cmd 'workspace ws-left';
-$focus->recv;
+ my @focus = grep { $_->{change} eq 'focus' } @events;
+ is(scalar @focus, 1, 'Received 1 workspace::focus event');
+ is($focus[0]->{current}->{name}, 'ws-right', 'focus event gave the right workspace');
+ is(@{$focus[0]->{current}->{nodes}}, $want, 'focus event gave the right number of windows on the workspace');
+}
+
+# move a window over to the right output
+subtest 'move right (1)', \&focus_subtest, 'move right', 1;
-$focus = AnyEvent->condvar;
-cmd 'move right';
-$event = $focus->recv;
-ok($event, 'moving from workspace with one window triggered focus ipc event');
-is($event->{current}->{name}, 'ws-right', 'focus event gave the right workspace');
-is(@{$event->{current}->{nodes}}, 2, 'focus event gave the right number of windows on the workspace');
+# move another window
+cmd 'workspace ws-left';
+subtest 'move right (2)', \&focus_subtest, 'move right', 2;
done_testing;
EOT
use i3test::XTEST;
-my ($cv, $timer);
-sub reset_test {
- $cv = AE::cv;
- $timer = AE::timer(1, 0, sub { $cv->send(0); });
-}
-
my $i3 = i3(get_socket_path());
$i3->connect()->recv;
my $ws = fresh_workspace;
-reset_test;
+my $cv = AnyEvent->condvar;
+my $timer = AnyEvent->timer(after => 1, interval => 0, cb => sub { $cv->send(0) });
$i3->subscribe({
window => sub {
my ($event) = @_;
},
})->recv;
-my $con;
-
sub i3bar_present {
my ($nodes) = @_;
for my $node (@{$nodes}) {
my $props = $node->{window_properties};
if (defined($props) && $props->{class} eq 'i3bar') {
- return 1;
+ return $node->{window};
}
}
return i3bar_present(\@children);
}
-if (i3bar_present($i3->get_tree->recv->{nodes})) {
+my $i3bar_window = i3bar_present($i3->get_tree->recv->{nodes});
+if ($i3bar_window) {
ok(1, 'i3bar present');
} else {
- $con = $cv->recv;
+ my $con = $cv->recv;
ok($con, 'i3bar appeared');
+ $i3bar_window = $con->{window};
}
+diag('i3bar window = ' . $i3bar_window);
+
my $left = open_window;
my $right = open_window;
sync_with_i3;
-$con = $cv->recv;
-is($con->{window}, $right->{id}, 'focus is initially on the right container');
-reset_test;
-xtest_button_press(1, 3, 3);
-xtest_button_release(1, 3, 3);
-sync_with_i3;
-$con = $cv->recv;
-is($con->{window}, $left->{id}, 'button 1 moves focus left');
-reset_test;
-
-xtest_button_press(2, 3, 3);
-xtest_button_release(2, 3, 3);
-sync_with_i3;
-$con = $cv->recv;
-is($con->{window}, $right->{id}, 'button 2 moves focus right');
-reset_test;
-
-xtest_button_press(3, 3, 3);
-xtest_button_release(3, 3, 3);
-sync_with_i3;
-$con = $cv->recv;
-is($con->{window}, $left->{id}, 'button 3 moves focus left');
-reset_test;
-
-xtest_button_press(4, 3, 3);
-xtest_button_release(4, 3, 3);
-sync_with_i3;
-$con = $cv->recv;
-is($con->{window}, $right->{id}, 'button 4 moves focus right');
-reset_test;
+sub focus_subtest {
+ my ($subscribecb, $want, $msg) = @_;
+ my @events = events_for(
+ $subscribecb,
+ 'window');
+ my @focus = map { $_->{container}->{window} } grep { $_->{change} eq 'focus' } @events;
+ is_deeply(\@focus, $want, $msg);
+}
-xtest_button_press(5, 3, 3);
-xtest_button_release(5, 3, 3);
-sync_with_i3;
-$con = $cv->recv;
-is($con->{window}, $left->{id}, 'button 5 moves focus left');
-reset_test;
+subtest 'button 1 moves focus left', \&focus_subtest,
+ sub {
+ xtest_button_press(1, 3, 3);
+ xtest_button_release(1, 3, 3);
+ xtest_sync_with($i3bar_window);
+ },
+ [ $left->{id} ],
+ 'button 1 moves focus left';
+
+subtest 'button 2 moves focus right', \&focus_subtest,
+ sub {
+ xtest_button_press(2, 3, 3);
+ xtest_button_release(2, 3, 3);
+ xtest_sync_with($i3bar_window);
+ },
+ [ $right->{id} ],
+ 'button 2 moves focus right';
+
+subtest 'button 3 moves focus left', \&focus_subtest,
+ sub {
+ xtest_button_press(3, 3, 3);
+ xtest_button_release(3, 3, 3);
+ xtest_sync_with($i3bar_window);
+ },
+ [ $left->{id} ],
+ 'button 3 moves focus left';
+
+subtest 'button 4 moves focus right', \&focus_subtest,
+ sub {
+ xtest_button_press(4, 3, 3);
+ xtest_button_release(4, 3, 3);
+ xtest_sync_with($i3bar_window);
+ },
+ [ $right->{id} ],
+ 'button 4 moves focus right';
+
+subtest 'button 5 moves focus left', \&focus_subtest,
+ sub {
+ xtest_button_press(5, 3, 3);
+ xtest_button_release(5, 3, 3);
+ xtest_sync_with($i3bar_window);
+ },
+ [ $left->{id} ],
+ 'button 5 moves focus left';
done_testing;
# Try running the tests in parallel so that the common case (tests pass) is
# quick, but fall back to running them in sequence to make debugging easier.
-if ! xvfb-run make check
+if ! make check
then
- xvfb-run ./testcases/complete-run.pl --parallel=1 || (cat latest/complete-run.log; false)
+ ./testcases/complete-run.pl --parallel=1 || (cat latest/complete-run.log; false)
fi