From 2a29d9c2c16ebd939d6729fe755747799472dd6c Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 9 Oct 2011 18:19:31 +0100 Subject: [PATCH 01/16] Make conn_screen available outside of main() Will be used in other parts of the code for startup notification --- include/i3.h | 1 + src/main.c | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/include/i3.h b/include/i3.h index 22dcd476..f2b97824 100644 --- a/include/i3.h +++ b/include/i3.h @@ -20,6 +20,7 @@ #define _I3_H extern xcb_connection_t *conn; +extern int conn_screen; extern xcb_key_symbols_t *keysyms; extern char **start_argv; extern Display *xlibdpy, *xkbdpy; diff --git a/src/main.c b/src/main.c index 3ebfb5f4..6020f109 100644 --- a/src/main.c +++ b/src/main.c @@ -19,6 +19,8 @@ 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; xcb_screen_t *root_screen; xcb_window_t root; @@ -175,7 +177,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,7 +358,7 @@ 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"); @@ -368,7 +369,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); From b9db72dc8af70767a05fea73fb8ebacf0c5db6b8 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 9 Oct 2011 18:21:59 +0100 Subject: [PATCH 02/16] Implement support for startup notifications This only sets up startup notifications for the 'exec' commands and directives. Monitoring startups follows later. --- common.mk | 2 ++ include/i3.h | 8 ++++++++ src/main.c | 10 ++++++++++ src/util.c | 23 +++++++++++++++++++++++ 4 files changed, 43 insertions(+) diff --git a/common.mk b/common.mk index bc831615..9bf427e0 100644 --- 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) diff --git a/include/i3.h b/include/i3.h index f2b97824..089dfcba 100644 --- a/include/i3.h +++ b/include/i3.h @@ -12,6 +12,9 @@ #include +#define SN_API_NOT_YET_FROZEN 1 +#include + #include "queue.h" #include "data.h" #include "xcb.h" @@ -21,6 +24,11 @@ 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/src/main.c b/src/main.c index 6020f109..f985810e 100644 --- a/src/main.c +++ b/src/main.c @@ -22,6 +22,14 @@ 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; uint8_t root_depth; @@ -362,6 +370,8 @@ int main(int argc, char *argv[]) { 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. */ diff --git a/src/util.c b/src/util.c index 30371bcd..d0e8e947 100644 --- a/src/util.c +++ b/src/util.c @@ -21,6 +21,9 @@ #include #include +#define SN_API_NOT_YET_FROZEN 1 +#include + #include "all.h" static iconv_t conversion_descriptor = 0; @@ -69,11 +72,31 @@ bool update_if_necessary(uint32_t *destination, const uint32_t new_value) { * */ 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); + + LOG("startup id = %s\n", sn_launcher_context_get_startup_id(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; From d1d4f39f9eae26da05da8135e2319088f89bca67 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 9 Oct 2011 20:18:38 +0100 Subject: [PATCH 03/16] save the last timestamp received by X11 in last_timestamp We need it for startup notifications (to generate a unique id) Conflicts: include/i3.h src/main.c --- src/click.c | 2 ++ src/handlers.c | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/src/click.c b/src/click.c index c270bdec..8ea182eb 100644 --- a/src/click.c +++ b/src/click.c @@ -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); diff --git a/src/handlers.c b/src/handlers.c index b3cb1df7..995622b9 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -80,6 +80,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 +159,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 +232,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) @@ -1084,6 +1092,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; From c812cdcf9a05e6799d52d6cdfffa428e3ab216c6 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 9 Oct 2011 22:15:21 +0100 Subject: [PATCH 04/16] make handle_client_message not return anything The function returned an int for historical reasons. --- src/handlers.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/handlers.c b/src/handlers.c index 995622b9..d81d7ecc 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -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 @@ -627,20 +627,20 @@ 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) { 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 */ @@ -677,10 +677,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 From 198f16ece93e029600cf3e592a7769345e82fb0c Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Oct 2011 00:24:29 +0100 Subject: [PATCH 05/16] add testcase for the startup notification protocol --- testcases/Makefile.PL | 1 + testcases/t/175-startup-notification.t | 117 +++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 testcases/t/175-startup-notification.t diff --git a/testcases/Makefile.PL b/testcases/Makefile.PL index 4b3f1ade..da0c1570 100755 --- a/testcases/Makefile.PL +++ b/testcases/Makefile.PL @@ -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 index 00000000..2257b3e4 --- /dev/null +++ b/testcases/t/175-startup-notification.t @@ -0,0 +1,117 @@ +#!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 + +#define SN_API_NOT_YET_FROZEN 1 +#include +#include + +static SnDisplay *sndisplay; +static SnLauncheeContext *ctx; + +// TODO: this should use $x +void init_ctx() { + int screen; + xcb_connection_t *conn; + 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); +} + +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); +mark_window($win->id); + +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'); + +# TODO: the same thing, but in a CLIENT_LEADER situation + +###################################################################### +# 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; From f4f4d782bb590e7e03302586fa3028fe785ba95d Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Oct 2011 11:36:21 +0100 Subject: [PATCH 06/16] implement a startup monitor, move code to src/startup.c --- include/all.h | 1 + include/startup.h | 35 ++++++++++++++++++ include/util.h | 12 ------- src/handlers.c | 11 ++++++ src/startup.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++ src/util.c | 52 --------------------------- 6 files changed, 138 insertions(+), 64 deletions(-) create mode 100644 include/startup.h create mode 100644 src/startup.c diff --git a/include/all.h b/include/all.h index 38cda89c..fd25629e 100644 --- a/include/all.h +++ b/include/all.h @@ -66,5 +66,6 @@ #include "assignments.h" #include "regex.h" #include "libi3.h" +#include "startup.h" #endif diff --git a/include/startup.h b/include/startup.h new file mode 100644 index 00000000..9e131019 --- /dev/null +++ b/include/startup.h @@ -0,0 +1,35 @@ +/* + * 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 + +/** + * 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); + +#endif diff --git a/include/util.h b/include/util.h index 7c7b819a..efef0bd0 100644 --- a/include/util.h +++ b/include/util.h @@ -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, diff --git a/src/handlers.c b/src/handlers.c index d81d7ecc..340e24aa 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -11,6 +11,9 @@ #include +#define SN_API_NOT_YET_FROZEN 1 +#include + #include "all.h" int randr_base = -1; @@ -628,6 +631,11 @@ static int handle_expose_event(xcb_expose_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) { @@ -984,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; diff --git a/src/startup.c b/src/startup.c new file mode 100644 index 00000000..dd327355 --- /dev/null +++ b/src/startup.c @@ -0,0 +1,91 @@ +/* + * 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 + * + */ +#include +#include + +#define SN_API_NOT_YET_FROZEN 1 +#include + +#include "all.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) { + /* 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); + + LOG("startup id = %s\n", sn_launcher_context_get_startup_id(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); +} + +/* + * Called by libstartup-notification when something happens + * + */ +void startup_monitor_event(SnMonitorEvent *event, void *userdata) { + SnStartupSequence *sequence; + + DLOG("something happened\n"); + sequence = sn_monitor_event_get_startup_sequence(event); + + switch (sn_monitor_event_get_type(event)) { + case SN_MONITOR_EVENT_COMPLETED: + DLOG("startup sequence %s completed\n", sn_startup_sequence_get_id(sequence)); + break; + default: + /* ignore */ + break; + } +} diff --git a/src/util.c b/src/util.c index d0e8e947..036dce9a 100644 --- a/src/util.c +++ b/src/util.c @@ -61,58 +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) { - /* 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); - - LOG("startup id = %s\n", sn_launcher_context_get_startup_id(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); -} - /* * 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, From 499d89bdb061f2f2f22316124f835fd79055a864 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Oct 2011 12:47:56 +0100 Subject: [PATCH 07/16] Keep track of startup notifications in a TAILQ, save workspace --- include/data.h | 14 ++++++++++++++ src/startup.c | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/include/data.h b/include/data.h index f6052b9f..7a280b2e 100644 --- a/include/data.h +++ b/include/data.h @@ -138,6 +138,20 @@ 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; + + 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 diff --git a/src/startup.c b/src/startup.c index dd327355..5fc875a3 100644 --- a/src/startup.c +++ b/src/startup.c @@ -18,6 +18,9 @@ #include "all.h" +static TAILQ_HEAD(startup_sequence_head, Startup_Sequence) startup_sequences = + TAILQ_HEAD_INITIALIZER(startup_sequences); + /* * 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), @@ -46,6 +49,14 @@ void start_application(const char *command) { 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); + TAILQ_INSERT_TAIL(&startup_sequences, sequence, sequences); + LOG("executing: %s\n", command); if (fork() == 0) { /* Child process */ @@ -75,14 +86,29 @@ void start_application(const char *command) { * */ void startup_monitor_event(SnMonitorEvent *event, void *userdata) { - SnStartupSequence *sequence; + SnStartupSequence *snsequence; - DLOG("something happened\n"); - sequence = sn_monitor_event_get_startup_sequence(event); + 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(sequence)); + DLOG("startup sequence %s completed\n", sn_startup_sequence_get_id(snsequence)); break; default: /* ignore */ From 7750382b895bf0a2c661a7ced3c2c9aca9f0df37 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Oct 2011 12:48:19 +0100 Subject: [PATCH 08/16] test: set the _NET_STARTUP_ID before mapping the window --- testcases/t/175-startup-notification.t | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/testcases/t/175-startup-notification.t b/testcases/t/175-startup-notification.t index 2257b3e4..710b3af7 100644 --- a/testcases/t/175-startup-notification.t +++ b/testcases/t/175-startup-notification.t @@ -28,11 +28,11 @@ use Inline C => <<'END_OF_C_CODE'; static SnDisplay *sndisplay; static SnLauncheeContext *ctx; +static xcb_connection_t *conn; // TODO: this should use $x void init_ctx() { int screen; - xcb_connection_t *conn; if ((conn = xcb_connect(NULL, &screen)) == NULL || xcb_connection_has_error(conn)) errx(1, "x11 conn failed"); @@ -48,6 +48,7 @@ const char *get_startup_id() { void mark_window(int window) { sn_launchee_context_setup_window(ctx, (Window)window); + xcb_flush(conn); } void complete_startup() { @@ -95,8 +96,12 @@ my $second_ws = fresh_workspace; is(@{get_ws_content($second_ws)}, 0, 'no containers on the second workspace yet'); -my $win = open_window($x); +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'); From 4204b8e2b00b86916aca4718753d5f684eddfafa Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Oct 2011 12:48:43 +0100 Subject: [PATCH 09/16] Get the _NET_STARTUP_ID in manage_window, get the corresponding workspace --- include/atoms.xmacro | 1 + include/startup.h | 11 ++++++++++ src/manage.c | 17 +++++++++++++++- src/startup.c | 48 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/include/atoms.xmacro b/include/atoms.xmacro index 309a3325..f08a90d5 100644 --- a/include/atoms.xmacro +++ b/include/atoms.xmacro @@ -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) diff --git a/include/startup.h b/include/startup.h index 9e131019..555a1191 100644 --- a/include/startup.h +++ b/include/startup.h @@ -32,4 +32,15 @@ void start_application(const char *command); */ 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 diff --git a/src/manage.c b/src/manage.c index 35055d17..22c2814f 100644 --- a/src/manage.c +++ b/src/manage.c @@ -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 index 5fc875a3..30932e33 100644 --- a/src/startup.c +++ b/src/startup.c @@ -115,3 +115,51 @@ void startup_monitor_event(SnMonitorEvent *event, void *userdata) { 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) { + DLOG("No _NET_STARTUP_ID set on this window\n"); + /* TODO: check the leader, if any */ + 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; +} From 6ac098a45e823aaea84db9298d42e07d5d64efe8 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Oct 2011 13:30:52 +0100 Subject: [PATCH 10/16] support _NET_STARTUP_ID on the client leader window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (necessary for GIMP, geeqie, …) --- src/startup.c | 17 ++++++++++++++--- testcases/t/175-startup-notification.t | 12 +++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/startup.c b/src/startup.c index 30932e33..dca7b999 100644 --- a/src/startup.c +++ b/src/startup.c @@ -129,10 +129,21 @@ char *startup_workspace_for_window(i3Window *cwindow, xcb_get_property_reply_t * /* 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) { - DLOG("No _NET_STARTUP_ID set on this window\n"); - /* TODO: check the leader, if any */ FREE(startup_id_reply); - return NULL; + 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; diff --git a/testcases/t/175-startup-notification.t b/testcases/t/175-startup-notification.t index 710b3af7..56088719 100644 --- a/testcases/t/175-startup-notification.t +++ b/testcases/t/175-startup-notification.t @@ -106,7 +106,17 @@ 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'); -# TODO: the same thing, but in a CLIENT_LEADER situation +###################################################################### +# 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 From 997a539a8a09158dd4a9c87738cd9937a471042d Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Oct 2011 13:50:03 +0100 Subject: [PATCH 11/16] Implement timeouts for startup notifications --- src/startup.c | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/startup.c b/src/startup.c index dca7b999..1a584cd4 100644 --- a/src/startup.c +++ b/src/startup.c @@ -21,6 +21,40 @@ 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 internal startup sequence will be deleted, the libstartup-notification + * context will be completed and unref'd (therefore free'd aswell). + * + */ +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; + } + + if (!sequence) { + DLOG("Sequence already deleted, nevermind.\n"); + return; + } + + /* Delete our internal sequence */ + TAILQ_REMOVE(&startup_sequences, sequence, sequences); + + /* Complete and unref the context */ + sn_launcher_context_complete(w->data); + sn_launcher_context_unref(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), @@ -47,6 +81,12 @@ void start_application(const char *command) { 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 From ae7dec2774bab6bf1bfdac40bc01396003a0b6cf Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Oct 2011 15:21:08 +0100 Subject: [PATCH 12/16] Move the includes after the include guard, no need to include these files more than once --- include/data.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/include/data.h b/include/data.h index 7a280b2e..ba836d55 100644 --- a/include/data.h +++ b/include/data.h @@ -7,13 +7,15 @@ * include/data.h: This file defines all data structures used by i3 * */ + +#ifndef _DATA_H +#define _DATA_H + #include #include #include #include -#ifndef _DATA_H -#define _DATA_H #include "queue.h" /* From 2ad4fbb34ae9110477be484f24c162bc5687253b Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Oct 2011 15:30:07 +0100 Subject: [PATCH 13/16] startup: delete the startup sequence upon completion, make the timeout complete it --- include/data.h | 5 +++++ src/startup.c | 25 ++++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/include/data.h b/include/data.h index ba836d55..60e1ef26 100644 --- a/include/data.h +++ b/include/data.h @@ -11,6 +11,9 @@ #ifndef _DATA_H #define _DATA_H +#define SN_API_NOT_YET_FROZEN 1 +#include + #include #include #include @@ -150,6 +153,8 @@ struct Startup_Sequence { char *id; /** workspace on which this startup was initiated */ char *workspace; + /** libstartup-notification context for this launch */ + SnLauncherContext *context; TAILQ_ENTRY(Startup_Sequence) sequences; }; diff --git a/src/startup.c b/src/startup.c index 1a584cd4..4b6c937d 100644 --- a/src/startup.c +++ b/src/startup.c @@ -24,8 +24,8 @@ static TAILQ_HEAD(startup_sequence_head, Startup_Sequence) startup_sequences = /* * After 60 seconds, a timeout will be triggered for each startup sequence. * - * The internal startup sequence will be deleted, the libstartup-notification - * context will be completed and unref'd (therefore free'd aswell). + * 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) { @@ -41,17 +41,16 @@ static void startup_timeout(EV_P_ ev_timer *w, int revents) { 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; } - /* Delete our internal sequence */ - TAILQ_REMOVE(&startup_sequences, sequence, sequences); - - /* Complete and unref the context */ + /* Complete the startup sequence, will trigger its deletion. */ sn_launcher_context_complete(w->data); - sn_launcher_context_unref(w->data); free(w); } @@ -95,8 +94,14 @@ void start_application(const char *command) { 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 */ @@ -149,6 +154,12 @@ void startup_monitor_event(SnMonitorEvent *event, void *userdata) { 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); break; default: /* ignore */ From a09d2eee749e8053f38ce3c8005fc3459798021a Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Oct 2011 15:34:47 +0100 Subject: [PATCH 14/16] add libstartup-notification to DEPENDS --- DEPENDS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DEPENDS b/DEPENDS index 710637e9..77034d77 100644 --- 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. From ad9ffcc91703d73cb714f6209f385c6032a2c398 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Oct 2011 15:35:38 +0100 Subject: [PATCH 15/16] debian: update debian/control with new build-dep libstartup-notification0-dev --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index da13231f..d78e81d5 100644 --- a/debian/control +++ b/debian/control @@ -3,7 +3,7 @@ Section: utils Priority: extra Maintainer: Michael Stapelberg DM-Upload-Allowed: yes -Build-Depends: debhelper (>= 6), libx11-dev, libxcb-util0-dev (>= 0.3.8), libxcb-keysyms1-dev, libxcb-xinerama0-dev (>= 1.1), libxcb-randr0-dev, libxcb-icccm4-dev, libxcursor-dev, asciidoc (>= 8.4.4), xmlto, docbook-xml, pkg-config, libev-dev, flex, bison, libyajl-dev, perl, texlive-latex-base, texlive-latex-recommended, texlive-latex-extra, 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/ From 5f52c78aa04326c25c1f178c3612c4b5613b5f85 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Oct 2011 15:53:57 +0100 Subject: [PATCH 16/16] Change the root window cursor to 'watch' during startups --- include/xcb.h | 9 +++++++++ include/xcursor.h | 1 + src/main.c | 14 +++----------- src/startup.c | 17 ++++++++++++++++- src/xcb.c | 17 +++++++++++++++++ src/xcursor.c | 8 +++++--- 6 files changed, 51 insertions(+), 15 deletions(-) diff --git a/include/xcb.h b/include/xcb.h index 5bc40d2a..65e4e6c3 100644 --- a/include/xcb.h +++ b/include/xcb.h @@ -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 diff --git a/include/xcursor.h b/include/xcursor.h index e129a36f..f3ff4f25 100644 --- a/include/xcursor.h +++ b/include/xcursor.h @@ -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 }; diff --git a/src/main.c b/src/main.c index f985810e..610a2c19 100644 --- a/src/main.c +++ b/src/main.c @@ -442,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, diff --git a/src/startup.c b/src/startup.c index 4b6c937d..66cb5285 100644 --- a/src/startup.c +++ b/src/startup.c @@ -7,7 +7,9 @@ * * See file LICENSE for license information. * - * startup.c: Startup notification code + * 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 @@ -124,6 +126,11 @@ void start_application(const char *command) { 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); } /* @@ -160,6 +167,14 @@ void startup_monitor_event(SnMonitorEvent *event, void *userdata) { /* 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 */ diff --git a/src/xcb.c b/src/xcb.c index 31d78703..32537388 100644 --- 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); +} diff --git a/src/xcursor.c b/src/xcursor.c index 69518c30..5d209b56 100644 --- a/src/xcursor.c +++ b/src/xcursor.c @@ -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); }