IPC interface (interprocess communication)
==========================================
Michael Stapelberg <michael@i3wm.org>
-October 2012
+February 2014
This document describes how to interface with i3 from a separate process. This
is useful for example to remote-control i3 (to write test cases for example) or
Sent whenever i3 changes its binding mode.
window (3)::
Sent when a client's window is successfully reparented (that is when i3
- has finished fitting it into a container).
+ has finished fitting it into a container), when a window received input
+ focus or when a window title has been updated.
barconfig_update (4)::
Sent when the hidden_state or mode field in the barconfig of any bar
instance was updated.
=== window event
This event consists of a single serialized map containing a property
-+change (string)+ which currently can indicate only that a new window
-has been successfully reparented (the value will be "new").
++change (string)+ which indicates the type of the change ("focus", "new",
+"title").
Additionally a +container (object)+ field will be present, which consists
-of the window's parent container. Be aware that the container will hold
-the initial name of the newly reparented window (e.g. if you run urxvt
-with a shell that changes the title, you will still at this point get the
-window title as "urxvt").
+of the window's parent container. Be aware that for the "new" event, the
+container will hold the initial name of the newly reparented window (e.g.
+if you run urxvt with a shell that changes the title, you will still at
+this point get the window title as "urxvt").
*Example:*
---------------------------
* respectively.
*/
void ipc_send_workspace_focus_event(Con *current, Con *old);
+
+/**
+ * For the window events we send, along the usual "change" field,
+ * also the window container, in "container".
+ */
+void ipc_send_window_event(const char *property, Con *con);
handle_unmap_notify_event(&unmap);
}
+static bool window_name_changed(i3Window *window, char *old_name) {
+ if ((old_name == NULL) && (window->name == NULL))
+ return false;
+
+ /* Either the old or the new one is NULL, but not both. */
+ if ((old_name == NULL) ^ (window->name == NULL))
+ return true;
+
+ return (strcmp(old_name, i3string_as_utf8(window->name)) != 0);
+}
+
/*
* Called when a window changes its title
*
if ((con = con_by_window_id(window)) == NULL || con->window == NULL)
return false;
+ char *old_name = (con->window->name != NULL ? sstrdup(i3string_as_utf8(con->window->name)) : NULL);
+
window_update_name(con->window, prop, false);
x_push_changes(croot);
+ if (window_name_changed(con->window, old_name))
+ ipc_send_window_event("title", con);
+
+ FREE(old_name);
+
return true;
}
if ((con = con_by_window_id(window)) == NULL || con->window == NULL)
return false;
+ char *old_name = (con->window->name != NULL ? sstrdup(i3string_as_utf8(con->window->name)) : NULL);
+
window_update_name_legacy(con->window, prop, false);
x_push_changes(croot);
+ if (window_name_changed(con->window, old_name))
+ ipc_send_window_event("title", con);
+
+ FREE(old_name);
+
return true;
}
y(free);
setlocale(LC_NUMERIC, "");
}
+
+/**
+ * For the window events we send, along the usual "change" field,
+ * also the window container, in "container".
+ */
+void ipc_send_window_event(const char *property, Con *con) {
+ DLOG("Issue IPC window %s event for X11 window 0x%08x\n", property, con->window->id);
+
+ setlocale(LC_NUMERIC, "C");
+ yajl_gen gen = ygenalloc();
+
+ y(map_open);
+
+ ystr("change");
+ ystr(property);
+
+ ystr("container");
+ dump_node(gen, con, false);
+
+ y(map_close);
+
+ const unsigned char *payload;
+ ylength length;
+ y(get_buf, &payload, &length);
+
+ ipc_send_event("window", I3_IPC_EVENT_WINDOW, (const char *)payload);
+ y(free);
+ setlocale(LC_NUMERIC, "");
+}
xcb_aux_sync(conn);
}
-/*
- * The following function sends a new window event, which consists
- * of fields "change" and "container", the latter containing a dump
- * of the window's container.
- *
- */
-static void ipc_send_window_new_event(Con *con) {
- setlocale(LC_NUMERIC, "C");
- yajl_gen gen = ygenalloc();
-
- y(map_open);
-
- ystr("change");
- ystr("new");
-
- ystr("container");
- dump_node(gen, con, false);
-
- y(map_close);
-
- const unsigned char *payload;
- ylength length;
- y(get_buf, &payload, &length);
-
- ipc_send_event("window", I3_IPC_EVENT_WINDOW, (const char *)payload);
- y(free);
- setlocale(LC_NUMERIC, "");
-}
-
/*
* Do some sanity checks and then reparent the window.
*
FREE(state_reply);
+ bool set_focus = false;
+
if (fs == NULL) {
DLOG("Not in fullscreen mode, focusing\n");
if (!cwindow->dock) {
if (workspace_is_visible(ws) && current_output == target_output) {
if (!match || !match->restart_mode) {
- con_focus(nc);
+ set_focus = true;
} else DLOG("not focusing, matched with restart_mode == true\n");
} else DLOG("workspace not visible, not focusing\n");
} else DLOG("dock, not focusing\n");
transient_win->transient_for != XCB_NONE) {
if (transient_win->transient_for == fs->window->id) {
LOG("This floating window belongs to the fullscreen window (popup_during_fullscreen == smart)\n");
- con_focus(nc);
+ set_focus = true;
break;
}
Con *next_transient = con_by_window_id(transient_win->transient_for);
tree_render();
/* Send an event about window creation */
- ipc_send_window_new_event(nc);
+ ipc_send_window_event("new", nc);
+
+ /* Defer setting focus after the 'new' event has been sent to ensure the
+ * proper window event sequence. */
+ if (set_focus) {
+ con_focus(nc);
+ tree_render();
+ }
/* Windows might get managed with the urgency hint already set (Pidgin is
* known to do that), so check for that and handle the hint accordingly.
/* Stores the X11 window ID of the currently focused window */
xcb_window_t focused_id = XCB_NONE;
+/* Because 'focused_id' might be reset to force input focus (after click to
+ * raise), we separately keep track of the X11 window ID to be able to always
+ * tell whether the focused window actually changed. */
+static xcb_window_t last_focused = XCB_NONE;
+
/* The bottom-to-top window stack of all windows which are managed by i3.
* Used for x_get_window_stack(). */
static xcb_window_t *btt_stack;
free(state);
/* Invalidate focused_id to correctly focus new windows with the same ID */
- focused_id = XCB_NONE;
+ focused_id = last_focused = XCB_NONE;
}
/*
x_push_node_unmaps(current);
}
+/*
+ * Returns true if the given container is currently attached to its parent.
+ *
+ * TODO: Remove once #1185 has been fixed
+ */
+static bool is_con_attached(Con *con) {
+ if (con->parent == NULL)
+ return false;
+
+ Con *current;
+ TAILQ_FOREACH(current, &(con->parent->nodes_head), nodes) {
+ if (current == con)
+ return true;
+ }
+
+ return false;
+}
+
/*
* Pushes all changes (state of each node, see x_push_node() and the window
* stack) to X11.
send_take_focus(to_focus);
set_focus = !focused->window->doesnt_accept_focus;
DLOG("set_focus = %d\n", set_focus);
+
+ if (!set_focus && to_focus != last_focused && is_con_attached(focused))
+ ipc_send_window_event("focus", focused);
}
if (set_focus) {
}
ewmh_update_active_window(to_focus);
+
+ if (to_focus != XCB_NONE && to_focus != last_focused && focused->window != NULL && is_con_attached(focused))
+ ipc_send_window_event("focus", focused);
}
- focused_id = to_focus;
+ focused_id = last_focused = to_focus;
}
}
# Events
my $new = AnyEvent->condvar;
+my $focus = AnyEvent->condvar;
$i3->subscribe({
window => sub {
my ($event) = @_;
- $new->send($event->{change} eq 'new');
+ 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); });
+$t = AnyEvent->timer(
+ after => 0.5,
+ cb => sub {
+ $new->send(0);
+ $focus->send(0);
+ }
+);
-ok($new->recv, 'Window "new" event received');
+is($new->recv->{container}->{focused}, 0, 'Window "new" event received');
+is($focus->recv->{container}->{focused}, 1, 'Window "focus" event received');
}
--- /dev/null
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Please read the following documents before working on tests:
+# • http://build.i3wm.org/docs/testsuite.html
+# (or docs/testsuite)
+#
+# • http://build.i3wm.org/docs/lib-i3test.html
+# (alternatively: perldoc ./testcases/lib/i3test.pm)
+#
+# • http://build.i3wm.org/docs/ipc.html
+# (or docs/ipc)
+#
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
+# (unless you are already familiar with Perl)
+
+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
+################################
+
+cmd 'split h';
+
+my $win0 = open_window;
+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;
+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');
+
+$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');
+
+$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');
+
+}
+
+done_testing;
--- /dev/null
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Please read the following documents before working on tests:
+# • http://build.i3wm.org/docs/testsuite.html
+# (or docs/testsuite)
+#
+# • http://build.i3wm.org/docs/lib-i3test.html
+# (alternatively: perldoc ./testcases/lib/i3test.pm)
+#
+# • http://build.i3wm.org/docs/ipc.html
+# (or docs/ipc)
+#
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
+# (unless you are already familiar with Perl)
+
+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');
+
+}
+
+done_testing;