]> git.sur5r.net Git - i3/i3/commitdiff
Merge branch 'startup-notification' into next
authorMichael Stapelberg <michael@stapelberg.de>
Mon, 10 Oct 2011 14:54:23 +0000 (15:54 +0100)
committerMichael Stapelberg <michael@stapelberg.de>
Mon, 10 Oct 2011 14:54:23 +0000 (15:54 +0100)
21 files changed:
DEPENDS
common.mk
debian/control
include/all.h
include/atoms.xmacro
include/data.h
include/i3.h
include/startup.h [new file with mode: 0644]
include/util.h
include/xcb.h
include/xcursor.h
src/click.c
src/handlers.c
src/main.c
src/manage.c
src/startup.c [new file with mode: 0644]
src/util.c
src/xcb.c
src/xcursor.c
testcases/Makefile.PL
testcases/t/175-startup-notification.t [new file with mode: 0644]

diff --git a/DEPENDS b/DEPENDS
index 710637e99582227130bd3480e34e9b11f02cf28b..77034d771c6b10d0a5fa9f758639847d06e5869d 100644 (file)
--- a/DEPENDS
+++ b/DEPENDS
@@ -21,7 +21,9 @@
 │ 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/                   │
+│ libsn¹      │ 0.12   │ 0.12   │ http://freedesktop.org/wiki/Software/startup-notification
 └─────────────┴────────┴────────┴────────────────────────────────────────┘
+ ¹ libsn = libstartup-notification
 
  i3-msg, i3-input, i3-nagbar and i3-config-wizard do not introduce any new
  dependencies.
index bc83161550209a8cd0674073a44e15744ca73820..9bf427e0e63f3854709149244794be1bd6ae5521 100644 (file)
--- a/common.mk
+++ b/common.mk
@@ -57,6 +57,7 @@ CFLAGS += $(call cflags_for_lib, x11)
 CFLAGS += $(call cflags_for_lib, yajl)
 CFLAGS += $(call cflags_for_lib, libev)
 CFLAGS += $(call cflags_for_lib, libpcre)
+CFLAGS += $(call cflags_for_lib, libstartup-notification-1.0)
 CPPFLAGS += -DI3_VERSION=\"${GIT_VERSION}\"
 CPPFLAGS += -DSYSCONFDIR=\"${SYSCONFDIR}\"
 CPPFLAGS += -DTERM_EMU=\"$(TERM_EMU)\"
@@ -84,6 +85,7 @@ 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)
+LIBS += $(call ldflags_for_lib, libstartup-notification-1.0,startup-notification-1)
 
 # 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)
index da13231f385564bdc4921157ddcd17ca59eed68a..d78e81d5df910640d5b33d767232e05819eb0fe7 100644 (file)
@@ -3,7 +3,7 @@ Section: utils
 Priority: extra
 Maintainer: Michael Stapelberg <michael@stapelberg.de>
 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, libpcre3-dev
+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, libstartup-notification0-dev (>= 0.12-1)
 Standards-Version: 3.9.2
 Homepage: http://i3wm.org/
 
index 38cda89c53824285c521329893e9312998917755..fd25629ecaf0bdd09e232313d6a2a2f52b1152db 100644 (file)
@@ -66,5 +66,6 @@
 #include "assignments.h"
 #include "regex.h"
 #include "libi3.h"
+#include "startup.h"
 
 #endif
index 309a3325f8bd3dcc972f0ab21770a32871e7380e..f08a90d5585abbbf60292b4dcea27d99c1362ba5 100644 (file)
@@ -15,6 +15,7 @@ xmacro(_NET_CLIENT_LIST_STACKING)
 xmacro(_NET_CURRENT_DESKTOP)
 xmacro(_NET_ACTIVE_WINDOW)
 xmacro(_NET_WORKAREA)
+xmacro(_NET_STARTUP_ID)
 xmacro(WM_PROTOCOLS)
 xmacro(WM_DELETE_WINDOW)
 xmacro(UTF8_STRING)
index f6052b9f81154ed98aece23cd086e1792dfb33ba..60e1ef26087aaabd91368b1045ef7dd4fee0dbc4 100644 (file)
@@ -7,13 +7,18 @@
  * include/data.h: This file defines all data structures used by i3
  *
  */
+
+#ifndef _DATA_H
+#define _DATA_H
+
+#define SN_API_NOT_YET_FROZEN 1
+#include <libsn/sn-launcher.h>
+
 #include <xcb/randr.h>
 #include <xcb/xcb_atom.h>
 #include <stdbool.h>
 #include <pcre.h>
 
-#ifndef _DATA_H
-#define _DATA_H
 #include "queue.h"
 
 /*
@@ -138,6 +143,22 @@ struct Ignore_Event {
     SLIST_ENTRY(Ignore_Event) ignore_events;
 };
 
+/**
+ * Stores internal information about a startup sequence, like the workspace it
+ * was initiated on.
+ *
+ */
+struct Startup_Sequence {
+    /** startup ID for this sequence, generated by libstartup-notification */
+    char *id;
+    /** workspace on which this startup was initiated */
+    char *workspace;
+    /** libstartup-notification context for this launch */
+    SnLauncherContext *context;
+
+    TAILQ_ENTRY(Startup_Sequence) sequences;
+};
+
 /**
  * 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
index 22dcd476b9605349f2ef53fbd02039a3659498b1..089dfcbafcc82bea81e4cd69abf9bd4c42d9bf78 100644 (file)
@@ -12,6 +12,9 @@
 
 #include <X11/XKBlib.h>
 
+#define SN_API_NOT_YET_FROZEN 1
+#include <libsn/sn-launcher.h>
+
 #include "queue.h"
 #include "data.h"
 #include "xcb.h"
 #define _I3_H
 
 extern xcb_connection_t *conn;
+extern int conn_screen;
+/** The last timestamp we got from X11 (timestamps are included in some events
+ * and are used for some things, like determining a unique ID in startup
+ * notification). */
+extern xcb_timestamp_t last_timestamp;
+extern SnDisplay *sndisplay;
 extern xcb_key_symbols_t *keysyms;
 extern char **start_argv;
 extern Display *xlibdpy, *xkbdpy;
diff --git a/include/startup.h b/include/startup.h
new file mode 100644 (file)
index 0000000..555a119
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ *
+ * © 2009-2011 Michael Stapelberg and contributors
+ *
+ * See file LICENSE for license information.
+ *
+ */
+#ifndef _STARTUP_H
+#define _STARTUP_H
+
+#define SN_API_NOT_YET_FROZEN 1
+#include <libsn/sn-monitor.h>
+
+/**
+ * Starts the given application by passing it through a shell. We use double
+ * fork to avoid zombie processes. As the started application’s parent exits
+ * (immediately), the application is reparented to init (process-id 1), which
+ * correctly handles childs, so we don’t have to do it :-).
+ *
+ * The shell is determined by looking for the SHELL environment variable. If
+ * it does not exist, /bin/sh is used.
+ *
+ */
+void start_application(const char *command);
+
+/**
+ * Called by libstartup-notification when something happens
+ *
+ */
+void startup_monitor_event(SnMonitorEvent *event, void *userdata);
+
+/**
+ * Checks if the given window belongs to a startup notification by checking if
+ * the _NET_STARTUP_ID property is set on the window (or on its leader, if it’s
+ * unset).
+ *
+ * If so, returns the workspace on which the startup was initiated.
+ * Returns NULL otherwise.
+ *
+ */
+char *startup_workspace_for_window(i3Window *cwindow, xcb_get_property_reply_t *startup_id_reply);
+
+#endif
index 7c7b819a97269c6e8fcd5c0080839d156c3aae96..efef0bd089a6fa803a817afe815f3bfc2bdd992a 100644 (file)
@@ -66,18 +66,6 @@ Rect rect_add(Rect a, Rect b);
  */
 bool update_if_necessary(uint32_t *destination, const uint32_t new_value);
 
-/**
- * Starts the given application by passing it through a shell. We use double
- * fork to avoid zombie processes. As the started application’s parent exits
- * (immediately), the application is reparented to init (process-id 1), which
- * correctly handles childs, so we don’t have to do it :-).
- *
- * The shell is determined by looking for the SHELL environment variable. If
- * it does not exist, /bin/sh is used.
- *
- */
-void start_application(const char *command);
-
 /**
  * exec()s an i3 utility, for example the config file migration script or
  * i3-nagbar. This function first searches $PATH for the given utility named,
index 5bc40d2a4aea81d70bbc4d968931461a1e6e79bd..65e4e6c389bd60f4c846abf98913f639872d7b2d 100644 (file)
@@ -23,6 +23,7 @@
 #define XCB_CURSOR_LEFT_PTR     68
 #define XCB_CURSOR_SB_H_DOUBLE_ARROW 108
 #define XCB_CURSOR_SB_V_DOUBLE_ARROW 116
+#define XCB_CURSOR_WATCH 150
 
 /* from X11/keysymdef.h */
 #define XCB_NUM_LOCK                    0xff7f
@@ -150,4 +151,12 @@ bool xcb_reply_contains_atom(xcb_get_property_reply_t *prop, xcb_atom_t atom);
  */
 void xcb_warp_pointer_rect(xcb_connection_t *conn, Rect *rect);
 
+/**
+ * Set the cursor of the root window to the given cursor id.
+ * This function should only be used if xcursor_supported == false.
+ * Otherwise, use xcursor_set_root_cursor().
+ *
+ */
+void xcb_set_root_cursor(int cursor);
+
 #endif
index e129a36fe11a39db36f3378e7b102647d1f2f149..f3ff4f25bbc22a1f8b3f098436b6d9201b85d181 100644 (file)
@@ -10,6 +10,7 @@ enum xcursor_cursor_t {
     XCURSOR_CURSOR_POINTER = 0,
     XCURSOR_CURSOR_RESIZE_HORIZONTAL,
     XCURSOR_CURSOR_RESIZE_VERTICAL,
+    XCURSOR_CURSOR_WATCH,
     XCURSOR_CURSOR_MAX
 };
 
index c270bdecdef3affe71ea3eb28f211ff8333f6549..8ea182eb99647218f368ec8d1e34ca6f6b93bb1b 100644 (file)
@@ -259,6 +259,8 @@ int handle_button_press(xcb_button_press_event_t *event) {
     Con *con;
     DLOG("Button %d pressed on window 0x%08x\n", event->state, event->event);
 
+    last_timestamp = event->time;
+
     const uint32_t mod = config.floating_modifier;
     bool mod_pressed = (mod != 0 && (event->state & mod) == mod);
     DLOG("floating_mod = %d, detail = %d\n", mod_pressed, event->detail);
index b3cb1df7b703c28b26b48b46956ec04be96250d2..340e24aa6c25f9b66ba0d21ed25469146a33ab65 100644 (file)
@@ -2,7 +2,7 @@
  * vim:ts=4:sw=4:expandtab
  *
  * i3 - an improved dynamic tiling window manager
- * © 2009-2010 Michael Stapelberg and contributors (see also: LICENSE)
+ * © 2009-2011 Michael Stapelberg and contributors (see also: LICENSE)
  *
  */
 #include <time.h>
@@ -11,6 +11,9 @@
 
 #include <X11/XKBlib.h>
 
+#define SN_API_NOT_YET_FROZEN 1
+#include <libsn/sn-monitor.h>
+
 #include "all.h"
 
 int randr_base = -1;
@@ -80,6 +83,9 @@ bool event_is_ignored(const int sequence, const int response_type) {
  *
  */
 static int handle_key_press(xcb_key_press_event_t *event) {
+
+    last_timestamp = event->time;
+
     DLOG("Keypress %d, state raw = %d\n", event->detail, event->state);
 
     /* Remove the numlock bit, all other bits are modifiers we can bind to */
@@ -156,6 +162,8 @@ static void check_crossing_screen_boundary(uint32_t x, uint32_t y) {
 static int handle_enter_notify(xcb_enter_notify_event_t *event) {
     Con *con;
 
+    last_timestamp = event->time;
+
     DLOG("enter_notify for %08x, mode = %d, detail %d, serial %d\n",
          event->event, event->mode, event->detail, event->sequence);
     DLOG("coordinates %d, %d\n", event->event_x, event->event_y);
@@ -227,6 +235,9 @@ static int handle_enter_notify(xcb_enter_notify_event_t *event) {
  *
  */
 static int handle_motion_notify(xcb_motion_notify_event_t *event) {
+
+    last_timestamp = event->time;
+
     /* Skip events where the pointer was over a child window, we are only
      * interested in events on the root window. */
     if (event->child != 0)
@@ -619,20 +630,25 @@ static int handle_expose_event(xcb_expose_event_t *event) {
  * Handle client messages (EWMH)
  *
  */
-static int handle_client_message(xcb_client_message_event_t *event) {
+static void handle_client_message(xcb_client_message_event_t *event) {
+    /* If this is a startup notification ClientMessage, the library will handle
+     * it and call our monitor_event() callback. */
+    if (sn_xcb_display_process_event(sndisplay, (xcb_generic_event_t*)event))
+        return;
+
     LOG("ClientMessage for window 0x%08x\n", event->window);
     if (event->type == A__NET_WM_STATE) {
         if (event->format != 32 || event->data.data32[1] != A__NET_WM_STATE_FULLSCREEN) {
             DLOG("atom in clientmessage is %d, fullscreen is %d\n",
                     event->data.data32[1], A__NET_WM_STATE_FULLSCREEN);
             DLOG("not about fullscreen atom\n");
-            return 0;
+            return;
         }
 
         Con *con = con_by_window_id(event->window);
         if (con == NULL) {
             DLOG("Could not get window for client message\n");
-            return 0;
+            return;
         }
 
         /* Check if the fullscreen state should be toggled */
@@ -669,10 +685,8 @@ static int handle_client_message(xcb_client_message_event_t *event) {
         free(reply);
     } else {
         ELOG("unhandled clientmessage\n");
-        return 0;
+        return;
     }
-
-    return 1;
 }
 
 #if 0
@@ -978,6 +992,9 @@ static struct property_handler_t property_handlers[] = {
  *
  */
 void property_handlers_init() {
+
+    sn_monitor_context_new(sndisplay, conn_screen, startup_monitor_event, NULL, NULL);
+
     property_handlers[0].atom = A__NET_WM_NAME;
     property_handlers[1].atom = XCB_ATOM_WM_HINTS;
     property_handlers[2].atom = XCB_ATOM_WM_NAME;
@@ -1084,6 +1101,7 @@ void handle_event(int type, xcb_generic_event_t *event) {
         case XCB_PROPERTY_NOTIFY:
             DLOG("Property notify\n");
             xcb_property_notify_event_t *e = (xcb_property_notify_event_t*)event;
+            last_timestamp = e->time;
             property_notify(e->state, e->window, e->atom);
             break;
 
index 3ebfb5f404569889f4c34aabe98d3a21f37726e4..610a2c19648009b0cc02701372238134c74d6c42 100644 (file)
@@ -19,6 +19,16 @@ extern Con *focused;
 char **start_argv;
 
 xcb_connection_t *conn;
+/* The screen (0 when you are using DISPLAY=:0) of the connection 'conn' */
+int conn_screen;
+
+/* Display handle for libstartup-notification */
+SnDisplay *sndisplay;
+
+/* The last timestamp we got from X11 (timestamps are included in some events
+ * and are used for some things, like determining a unique ID in startup
+ * notification). */
+xcb_timestamp_t last_timestamp = XCB_CURRENT_TIME;
 
 xcb_screen_t *root_screen;
 xcb_window_t root;
@@ -175,7 +185,6 @@ static void i3_exit() {
 }
 
 int main(int argc, char *argv[]) {
-    int screens;
     char *override_configpath = NULL;
     bool autostart = true;
     char *layout_path = NULL;
@@ -357,10 +366,12 @@ int main(int argc, char *argv[]) {
 
     LOG("i3 (tree) version " I3_VERSION " starting\n");
 
-    conn = xcb_connect(NULL, &screens);
+    conn = xcb_connect(NULL, &conn_screen);
     if (xcb_connection_has_error(conn))
         errx(EXIT_FAILURE, "Cannot open display\n");
 
+    sndisplay = sn_xcb_display_new(conn, NULL, NULL);
+
     /* Initialize the libev event loop. This needs to be done before loading
      * the config file because the parser will install an ev_child watcher
      * for the nagbar when config errors are found. */
@@ -368,7 +379,7 @@ int main(int argc, char *argv[]) {
     if (main_loop == NULL)
             die("Could not initialize libev. Bad LIBEV_FLAGS?\n");
 
-    root_screen = xcb_aux_get_screen(conn, screens);
+    root_screen = xcb_aux_get_screen(conn, conn_screen);
     root = root_screen->root;
     root_depth = root_screen->root_depth;
     xcb_get_geometry_cookie_t gcookie = xcb_get_geometry(conn, root);
@@ -431,17 +442,9 @@ int main(int argc, char *argv[]) {
 
     /* Set a cursor for the root window (otherwise the root window will show no
        cursor until the first client is launched). */
-    if (xcursor_supported) {
-        xcursor_set_root_cursor();
-    } else {
-        xcb_cursor_t cursor_id = xcb_generate_id(conn);
-        i3Font cursor_font = load_font("cursor", false);
-        int xcb_cursor = xcursor_get_xcb_cursor(XCURSOR_CURSOR_POINTER);
-        xcb_create_glyph_cursor(conn, cursor_id, cursor_font.id, cursor_font.id,
-                xcb_cursor, xcb_cursor + 1, 0, 0, 0, 65535, 65535, 65535);
-        xcb_change_window_attributes(conn, root, XCB_CW_CURSOR, &cursor_id);
-        xcb_free_cursor(conn, cursor_id);
-    }
+    if (xcursor_supported)
+        xcursor_set_root_cursor(XCURSOR_CURSOR_POINTER);
+    else xcb_set_root_cursor(XCURSOR_CURSOR_POINTER);
 
     if (xkb_supported) {
         int errBase,
index 35055d17ea04e7787efe40dde42e6cb6fdf3e751..22c2814fa4fcdcfb10f697e388b82a9640988d4b 100644 (file)
@@ -84,7 +84,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
     xcb_get_property_cookie_t wm_type_cookie, strut_cookie, state_cookie,
                               utf8_title_cookie, title_cookie,
                               class_cookie, leader_cookie, transient_cookie,
-                              role_cookie;
+                              role_cookie, startup_id_cookie;
 
 
     geomc = xcb_get_geometry(conn, d);
@@ -147,6 +147,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
     title_cookie = GET_PROPERTY(XCB_ATOM_WM_NAME, 128);
     class_cookie = GET_PROPERTY(XCB_ATOM_WM_CLASS, 128);
     role_cookie = GET_PROPERTY(A_WM_WINDOW_ROLE, 128);
+    startup_id_cookie = GET_PROPERTY(A__NET_STARTUP_ID, 512);
     /* TODO: also get wm_normal_hints here. implement after we got rid of xcb-event */
 
     DLOG("reparenting!\n");
@@ -175,6 +176,11 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
     window_update_strut_partial(cwindow, xcb_get_property_reply(conn, strut_cookie, NULL));
     window_update_role(cwindow, xcb_get_property_reply(conn, role_cookie, NULL), true);
 
+    xcb_get_property_reply_t *startup_id_reply;
+    startup_id_reply = xcb_get_property_reply(conn, startup_id_cookie, NULL);
+    char *startup_ws = startup_workspace_for_window(cwindow, startup_id_reply);
+    DLOG("startup workspace = %s\n", startup_ws);
+
     /* check if the window needs WM_TAKE_FOCUS */
     cwindow->needs_take_focus = window_supports_protocol(cwindow->id, A_WM_TAKE_FOCUS);
 
@@ -233,6 +239,15 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
                 else nc = tree_open_con(nc->parent, cwindow);
             }
         /* TODO: handle assignments with type == A_TO_OUTPUT */
+        } else if (startup_ws) {
+            /* If it’s not assigned, but was started on a specific workspace,
+             * we want to open it there */
+            DLOG("Using workspace on which this application was started (%s)\n", startup_ws);
+            nc = con_descend_tiling_focused(workspace_get(startup_ws, NULL));
+            DLOG("focused on ws %s: %p / %s\n", startup_ws, nc, nc->name);
+            if (nc->type == CT_WORKSPACE)
+                nc = tree_open_con(nc, cwindow);
+            else nc = tree_open_con(nc->parent, cwindow);
         } else {
             /* If not, insert it at the currently focused position */
             if (focused->type == CT_CON && con_accepts_window(focused)) {
diff --git a/src/startup.c b/src/startup.c
new file mode 100644 (file)
index 0000000..66cb528
--- /dev/null
@@ -0,0 +1,242 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ *
+ * © 2009-2011 Michael Stapelberg and contributors
+ *
+ * See file LICENSE for license information.
+ *
+ * startup.c: Startup notification code. Ensures a startup notification context
+ * is setup when launching applications. We store the current workspace to open
+ * windows in that startup notification context on the appropriate workspace.
+ *
+ */
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#define SN_API_NOT_YET_FROZEN 1
+#include <libsn/sn-launcher.h>
+
+#include "all.h"
+
+static TAILQ_HEAD(startup_sequence_head, Startup_Sequence) startup_sequences =
+    TAILQ_HEAD_INITIALIZER(startup_sequences);
+
+/*
+ * After 60 seconds, a timeout will be triggered for each startup sequence.
+ *
+ * The timeout will just trigger completion of the sequence, so the normal
+ * completion process takes place (startup_monitor_event will free it).
+ *
+ */
+static void startup_timeout(EV_P_ ev_timer *w, int revents) {
+    const char *id = sn_launcher_context_get_startup_id(w->data);
+    DLOG("Timeout for startup sequence %s\n", id);
+
+    struct Startup_Sequence *current, *sequence = NULL;
+    TAILQ_FOREACH(current, &startup_sequences, sequences) {
+        if (strcmp(current->id, id) != 0)
+            continue;
+
+        sequence = current;
+        break;
+    }
+
+    /* Unref the context (for the timeout itself, see start_application) */
+    sn_launcher_context_unref(w->data);
+
+    if (!sequence) {
+        DLOG("Sequence already deleted, nevermind.\n");
+        return;
+    }
+
+    /* Complete the startup sequence, will trigger its deletion. */
+    sn_launcher_context_complete(w->data);
+    free(w);
+}
+
+/*
+ * Starts the given application by passing it through a shell. We use double fork
+ * to avoid zombie processes. As the started application’s parent exits (immediately),
+ * the application is reparented to init (process-id 1), which correctly handles
+ * childs, so we don’t have to do it :-).
+ *
+ * The shell is determined by looking for the SHELL environment variable. If it
+ * does not exist, /bin/sh is used.
+ *
+ */
+void start_application(const char *command) {
+    /* Create a startup notification context to monitor the progress of this
+     * startup. */
+    SnLauncherContext *context;
+    context = sn_launcher_context_new(sndisplay, conn_screen);
+    sn_launcher_context_set_name(context, "i3");
+    sn_launcher_context_set_description(context, "exec command in i3");
+    /* Chop off everything starting from the first space (if there are any
+     * spaces in the command), since we don’t want the parameters. */
+    char *first_word = sstrdup(command);
+    char *space = strchr(first_word, ' ');
+    if (space)
+        *space = '\0';
+    sn_launcher_context_initiate(context, "i3", first_word, last_timestamp);
+    free(first_word);
+
+    /* Trigger a timeout after 60 seconds */
+    struct ev_timer *timeout = scalloc(sizeof(struct ev_timer));
+    ev_timer_init(timeout, startup_timeout, 60.0, 0.);
+    timeout->data = context;
+    ev_timer_start(main_loop, timeout);
+
+    LOG("startup id = %s\n", sn_launcher_context_get_startup_id(context));
+
+    /* Save the ID and current workspace in our internal list of startup
+     * sequences */
+    Con *ws = con_get_workspace(focused);
+    struct Startup_Sequence *sequence = scalloc(sizeof(struct Startup_Sequence));
+    sequence->id = sstrdup(sn_launcher_context_get_startup_id(context));
+    sequence->workspace = sstrdup(ws->name);
+    sequence->context = context;
+    TAILQ_INSERT_TAIL(&startup_sequences, sequence, sequences);
+
+    /* Increase the refcount once (it starts with 1, so it will be 2 now) for
+     * the timeout. Even if the sequence gets completed, the timeout still
+     * needs the context (but will unref it then) */
+    sn_launcher_context_ref(context);
+
+    LOG("executing: %s\n", command);
+    if (fork() == 0) {
+        /* Child process */
+        setsid();
+        if (fork() == 0) {
+            /* Setup the environment variable(s) */
+            sn_launcher_context_setup_child_process(context);
+
+            /* Stores the path of the shell */
+            static const char *shell = NULL;
+
+            if (shell == NULL)
+                if ((shell = getenv("SHELL")) == NULL)
+                    shell = "/bin/sh";
+
+            /* This is the child */
+            execl(shell, shell, "-c", command, (void*)NULL);
+            /* not reached */
+        }
+        exit(0);
+    }
+    wait(0);
+
+    /* Change the pointer of the root window to indicate progress */
+    if (xcursor_supported)
+        xcursor_set_root_cursor(XCURSOR_CURSOR_WATCH);
+    else xcb_set_root_cursor(XCURSOR_CURSOR_WATCH);
+}
+
+/*
+ * Called by libstartup-notification when something happens
+ *
+ */
+void startup_monitor_event(SnMonitorEvent *event, void *userdata) {
+    SnStartupSequence *snsequence;
+
+    snsequence = sn_monitor_event_get_startup_sequence(event);
+
+    /* Get the corresponding internal startup sequence */
+    const char *id = sn_startup_sequence_get_id(snsequence);
+    struct Startup_Sequence *current, *sequence = NULL;
+    TAILQ_FOREACH(current, &startup_sequences, sequences) {
+        if (strcmp(current->id, id) != 0)
+            continue;
+
+        sequence = current;
+        break;
+    }
+
+    if (!sequence) {
+        DLOG("Got event for startup sequence that we did not initiate (ID = %s). Ignoring.\n", id);
+        return;
+    }
+
+    switch (sn_monitor_event_get_type(event)) {
+        case SN_MONITOR_EVENT_COMPLETED:
+            DLOG("startup sequence %s completed\n", sn_startup_sequence_get_id(snsequence));
+
+            /* Unref the context, will be free()d */
+            sn_launcher_context_unref(sequence->context);
+
+            /* Delete our internal sequence */
+            TAILQ_REMOVE(&startup_sequences, sequence, sequences);
+
+            if (TAILQ_EMPTY(&startup_sequences)) {
+                DLOG("No more startup sequences running, changing root window cursor to default pointer.\n");
+                /* Change the pointer of the root window to indicate progress */
+                if (xcursor_supported)
+                    xcursor_set_root_cursor(XCURSOR_CURSOR_POINTER);
+                else xcb_set_root_cursor(XCURSOR_CURSOR_POINTER);
+            }
+            break;
+        default:
+            /* ignore */
+            break;
+    }
+}
+
+/*
+ * Checks if the given window belongs to a startup notification by checking if
+ * the _NET_STARTUP_ID property is set on the window (or on its leader, if it’s
+ * unset).
+ *
+ * If so, returns the workspace on which the startup was initiated.
+ * Returns NULL otherwise.
+ *
+ */
+char *startup_workspace_for_window(i3Window *cwindow, xcb_get_property_reply_t *startup_id_reply) {
+    /* The _NET_STARTUP_ID is only needed during this function, so we get it
+     * here and don’t save it in the 'cwindow'. */
+    if (startup_id_reply == NULL || xcb_get_property_value_length(startup_id_reply) == 0) {
+        FREE(startup_id_reply);
+        DLOG("No _NET_STARTUP_ID set on this window\n");
+        if (cwindow->leader == XCB_NONE)
+            return NULL;
+
+        xcb_get_property_cookie_t cookie;
+        cookie = xcb_get_property(conn, false, cwindow->leader, A__NET_STARTUP_ID, XCB_GET_PROPERTY_TYPE_ANY, 0, 512);
+        DLOG("Checking leader window 0x%08x\n", cwindow->leader);
+        startup_id_reply = xcb_get_property_reply(conn, cookie, NULL);
+
+        if (startup_id_reply == NULL || xcb_get_property_value_length(startup_id_reply) == 0) {
+            DLOG("No _NET_STARTUP_ID set on the leader either\n");
+            FREE(startup_id_reply);
+            return NULL;
+        }
+    }
+
+    char *startup_id;
+    if (asprintf(&startup_id, "%.*s", xcb_get_property_value_length(startup_id_reply),
+                 (char*)xcb_get_property_value(startup_id_reply)) == -1) {
+        perror("asprintf()");
+        DLOG("Could not get _NET_STARTUP_ID\n");
+        free(startup_id_reply);
+        return NULL;
+    }
+
+    struct Startup_Sequence *current, *sequence = NULL;
+    TAILQ_FOREACH(current, &startup_sequences, sequences) {
+        if (strcmp(current->id, startup_id) != 0)
+            continue;
+
+        sequence = current;
+        break;
+    }
+
+    free(startup_id);
+    free(startup_id_reply);
+
+    if (!sequence) {
+        DLOG("WARNING: This sequence (ID %s) was not found\n", startup_id);
+        return NULL;
+    }
+
+    return sequence->workspace;
+}
index 30371bcdbf86493c7c4056e1f59ebd8b6828baa0..036dce9ab616c09bb425e4700a0bc04f6bdeeca7 100644 (file)
@@ -21,6 +21,9 @@
 #include <yajl/yajl_version.h>
 #include <libgen.h>
 
+#define SN_API_NOT_YET_FROZEN 1
+#include <libsn/sn-launcher.h>
+
 #include "all.h"
 
 static iconv_t conversion_descriptor = 0;
@@ -58,38 +61,6 @@ bool update_if_necessary(uint32_t *destination, const uint32_t new_value) {
     return ((*destination = new_value) != old_value);
 }
 
-/*
- * Starts the given application by passing it through a shell. We use double fork
- * to avoid zombie processes. As the started application’s parent exits (immediately),
- * the application is reparented to init (process-id 1), which correctly handles
- * childs, so we don’t have to do it :-).
- *
- * The shell is determined by looking for the SHELL environment variable. If it
- * does not exist, /bin/sh is used.
- *
- */
-void start_application(const char *command) {
-    LOG("executing: %s\n", command);
-    if (fork() == 0) {
-        /* Child process */
-        setsid();
-        if (fork() == 0) {
-            /* Stores the path of the shell */
-            static const char *shell = NULL;
-
-            if (shell == NULL)
-                if ((shell = getenv("SHELL")) == NULL)
-                    shell = "/bin/sh";
-
-            /* This is the child */
-            execl(shell, shell, "-c", command, (void*)NULL);
-            /* not reached */
-        }
-        exit(0);
-    }
-    wait(0);
-}
-
 /*
  * exec()s an i3 utility, for example the config file migration script or
  * i3-nagbar. This function first searches $PATH for the given utility named,
index 31d7870387462dfbea4919563fcbbd9c2fa69a75..32537388bd3e8f93fe8e92e152b004815a363518 100644 (file)
--- a/src/xcb.c
+++ b/src/xcb.c
@@ -341,3 +341,20 @@ void xcb_warp_pointer_rect(xcb_connection_t *conn, Rect *rect) {
     LOG("warp pointer to: %d %d\n", mid_x, mid_y);
     xcb_warp_pointer(conn, XCB_NONE, root, 0, 0, 0, 0, mid_x, mid_y);
 }
+
+/*
+ * Set the cursor of the root window to the given cursor id.
+ * This function should only be used if xcursor_supported == false.
+ * Otherwise, use xcursor_set_root_cursor().
+ *
+ */
+void xcb_set_root_cursor(int cursor) {
+    xcb_cursor_t cursor_id = xcb_generate_id(conn);
+    i3Font cursor_font = load_font("cursor", false);
+    int xcb_cursor = xcursor_get_xcb_cursor(cursor);
+    xcb_create_glyph_cursor(conn, cursor_id, cursor_font.id, cursor_font.id,
+            xcb_cursor, xcb_cursor + 1, 0, 0, 0, 65535, 65535, 65535);
+    xcb_change_window_attributes(conn, root, XCB_CW_CURSOR, &cursor_id);
+    xcb_free_cursor(conn, cursor_id);
+    xcb_flush(conn);
+}
index 69518c3073621451bb65cb8ba0bef9f0c69620be..5d209b56baf465057f65dd1ef6f8702352463867 100644 (file)
@@ -14,7 +14,8 @@ static Cursor cursors[XCURSOR_CURSOR_MAX];
 static const int xcb_cursors[XCURSOR_CURSOR_MAX] = {
     XCB_CURSOR_LEFT_PTR,
     XCB_CURSOR_SB_H_DOUBLE_ARROW,
-    XCB_CURSOR_SB_V_DOUBLE_ARROW
+    XCB_CURSOR_SB_V_DOUBLE_ARROW,
+    XCB_CURSOR_WATCH
 };
 
 static Cursor load_cursor(const char *name) {
@@ -28,6 +29,7 @@ void xcursor_load_cursors() {
     cursors[XCURSOR_CURSOR_POINTER] = load_cursor("left_ptr");
     cursors[XCURSOR_CURSOR_RESIZE_HORIZONTAL] = load_cursor("sb_h_double_arrow");
     cursors[XCURSOR_CURSOR_RESIZE_VERTICAL] = load_cursor("sb_v_double_arrow");
+    cursors[XCURSOR_CURSOR_WATCH] = load_cursor("watch");
 }
 
 /*
@@ -41,9 +43,9 @@ void xcursor_load_cursors() {
  * races might occur (even though we flush the Xlib connection).
  *
  */
-void xcursor_set_root_cursor() {
+void xcursor_set_root_cursor(int cursor_id) {
     XSetWindowAttributes attributes;
-    attributes.cursor = xcursor_get_cursor(XCURSOR_CURSOR_POINTER);
+    attributes.cursor = xcursor_get_cursor(cursor_id);
     XChangeWindowAttributes(xlibdpy, DefaultRootWindow(xlibdpy), CWCursor, &attributes);
     XFlush(xlibdpy);
 }
index 4b3f1ade4fc43db84c8ea7c61ef598b61d850625..da0c15707ce44b43e8cbdaa19273b2dae95dd87a 100755 (executable)
@@ -12,6 +12,7 @@ WriteMakefile(
         'Test::Most'   => 0,
         'Test::Deep'   => 0,
        'EV'           => 0,
+       'Inline'       => 0,
     },
     # don't install any files from this directory
     PM => {},
diff --git a/testcases/t/175-startup-notification.t b/testcases/t/175-startup-notification.t
new file mode 100644 (file)
index 0000000..5608871
--- /dev/null
@@ -0,0 +1,132 @@
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Test for the startup notification protocol.
+#
+
+use i3test;
+use POSIX qw(mkfifo);
+use File::Temp qw(:POSIX);
+
+my $x = X11::XCB::Connection->new;
+use ExtUtils::PkgConfig;
+
+# setup dependency on libstartup-notification using pkg-config
+my %sn_config;
+BEGIN {
+    %sn_config = ExtUtils::PkgConfig->find('libstartup-notification-1.0');
+}
+
+use Inline C => Config => LIBS => $sn_config{libs}, CCFLAGS => $sn_config{cflags};
+use Inline C => <<'END_OF_C_CODE';
+
+#include <xcb/xcb.h>
+
+#define SN_API_NOT_YET_FROZEN 1
+#include <libsn/sn-common.h>
+#include <libsn/sn-launchee.h>
+
+static SnDisplay *sndisplay;
+static SnLauncheeContext *ctx;
+static xcb_connection_t *conn;
+
+// TODO: this should use $x
+void init_ctx() {
+    int screen;
+    if ((conn = xcb_connect(NULL, &screen)) == NULL ||
+        xcb_connection_has_error(conn))
+        errx(1, "x11 conn failed");
+
+    printf("screen = %d\n", screen);
+    sndisplay = sn_xcb_display_new(conn, NULL, NULL);
+    ctx = sn_launchee_context_new_from_environment(sndisplay, screen);
+}
+
+const char *get_startup_id() {
+    return sn_launchee_context_get_startup_id(ctx);
+}
+
+void mark_window(int window) {
+    sn_launchee_context_setup_window(ctx, (Window)window);
+    xcb_flush(conn);
+}
+
+void complete_startup() {
+    /* mark the startup process complete */
+    sn_launchee_context_complete(ctx);
+}
+END_OF_C_CODE
+
+my $first_ws = fresh_workspace;
+
+is(@{get_ws_content($first_ws)}, 0, 'no containers on this workspace yet');
+
+######################################################################
+# 1) initiate startup, switch workspace, create window
+# (should be placed on the original workspace)
+######################################################################
+
+# Start a new process via i3 (to initialize a new startup notification
+# context), then steal its DESKTOP_STARTUP_ID variable. We handle the startup
+# notification in the testcase from there on.
+#
+# This works by setting up a FIFO in which the process (started by i3) will
+# echo its $DESKTOP_STARTUP_ID. We (blockingly) read the variable into
+# $startup_id in the testcase.
+my $tmp = tmpnam();
+mkfifo($tmp, 0600) or die "Could not create FIFO in $tmp";
+
+cmd qq|exec echo \$DESKTOP_STARTUP_ID >$tmp|;
+
+open(my $fh, '<', $tmp);
+chomp(my $startup_id = <$fh>);
+close($fh);
+
+unlink($tmp);
+
+$ENV{DESKTOP_STARTUP_ID} = $startup_id;
+
+# Create a new libstartup-notification launchee context
+init_ctx();
+
+# Make sure the context was set up successfully
+is(get_startup_id(), $startup_id, 'libstartup-notification returns the same id');
+
+my $second_ws = fresh_workspace;
+
+is(@{get_ws_content($second_ws)}, 0, 'no containers on the second workspace yet');
+
+my $win = open_window($x, { dont_map => 1 });
+mark_window($win->id);
+$win->map;
+wait_for_map($x);
+# We sync with i3 here to make sure $x->input_focus is updated.
+sync_with_i3($x);
+
+is(@{get_ws_content($second_ws)}, 0, 'still no containers on the second workspace');
+is(@{get_ws_content($first_ws)}, 1, 'one container on the first workspace');
+
+######################################################################
+# same thing, but with _NET_STARTUP_ID set on the leader
+######################################################################
+
+my $leader = open_window($x, { dont_map => 1 });
+mark_window($leader->id);
+
+$win = open_window($x, { client_leader => $leader });
+
+is(@{get_ws_content($second_ws)}, 0, 'still no containers on the second workspace');
+is(@{get_ws_content($first_ws)}, 2, 'two containers on the first workspace');
+
+######################################################################
+# 2) open another window after the startup process is completed
+# (should be placed on the current workspace)
+######################################################################
+
+complete_startup();
+sync_with_i3($x);
+
+my $otherwin = open_window($x);
+is(@{get_ws_content($second_ws)}, 1, 'one container on the second workspace');
+
+done_testing;