-i3-wm (4.2.1-0) unstable; urgency=low
+i3-wm (4.3.1-0) unstable; urgency=low
* NOT YET RELEASED
- -- Michael Stapelberg <michael@stapelberg.de> Fri, 27 Jan 2012 19:34:11 +0000
+ -- Michael Stapelberg <michael@stapelberg.de> Wed, 19 Sep 2012 18:13:15 +0200
+
+i3-wm (4.3-1) experimental; urgency=low
+
+ * New upstream release
+
+ -- Michael Stapelberg <stapelberg@debian.org> Wed, 19 Sep 2012 18:13:40 +0200
i3-wm (4.2-1) unstable; urgency=low
+Description: list x-terminal-emulator as one of i3-sensible-terminal’s choices
+Author: Michael Stapelberg <stapelberg@debian.org>
+Origin: vendor
+Forwarded: not-needed
+Last-Update: 2011-12-28
+
+---
+
Index: i3-4.1.1/man/i3-sensible-terminal.man
===================================================================
--- i3-4.1.1.orig/man/i3-sensible-terminal.man 2011-12-28 23:56:55.487581000 +0100
-Index: i3-4.1.1/i3-sensible-terminal
+Description: i3-sensible-terminal: try x-terminal-emulator first
+Author: Michael Stapelberg <stapelberg@debian.org>
+Origin: vendor
+Forwarded: not-needed
+Last-Update: 2012-09-19
+
+---
+
+Index: i3-4.3/i3-sensible-terminal
===================================================================
---- i3-4.1.1.orig/i3-sensible-terminal 2011-12-28 23:51:52.455610236 +0100
-+++ i3-4.1.1/i3-sensible-terminal 2011-12-28 23:52:00.826775027 +0100
+--- i3-4.3.orig/i3-sensible-terminal 2012-09-19 18:08:09.000000000 +0200
++++ i3-4.3/i3-sensible-terminal 2012-09-19 18:32:06.393883488 +0200
@@ -4,11 +4,7 @@
#
# This script tries to exec a terminal emulator by trying some known terminal
-# Distributions/packagers should enhance this script with a
-# distribution-specific mechanism to find the preferred terminal emulator. On
-# Debian, there is the x-terminal-emulator symlink for example.
--for terminal in $TERMINAL urxvt rxvt terminator Eterm aterm xterm gnome-terminal roxterm; do
-+for terminal in $TERMINAL x-terminal-emulator urxvt rxvt terminator Eterm aterm xterm gnome-terminal roxterm; do
+-for terminal in $TERMINAL urxvt rxvt terminator Eterm aterm xterm gnome-terminal roxterm xfce4-terminal; do
++for terminal in $TERMINAL x-terminal-emulator urxvt rxvt terminator Eterm aterm xterm gnome-terminal roxterm xfce4-terminal; do
if which $terminal > /dev/null 2>&1; then
exec $terminal "$@"
fi
$(MAKE) -C docs
override_dh_installchangelogs:
- dh_installchangelogs RELEASE-NOTES-4.2
+ dh_installchangelogs RELEASE-NOTES-4.3
override_dh_install:
$(MAKE) DESTDIR=$(CURDIR)/debian/i3-wm/ install
to provide the correct version. The header block is terminated by a newline and
consists of a single JSON hash:
-*Example*:
+*Minimal example*:
----------------
{ "version": 1 }
----------------
+*All features example*:
+----------------
+{ "version": 1, "stop_signal": 10, "cont_signal": 12 }
+----------------
+
(Note that before i3 v4.3 the precise format had to be +{"version":1}+,
byte-for-byte.)
+status_command+ in the bar configuration at
http://code.stapelberg.de/git/i3/tree/contrib/trivial-bar-script.sh?h=next
+=== Header in detail
+
+version::
+ The version number (as an integer) of the i3bar protocol you will use.
+stop_signal::
+ Specify to i3bar the signal (as an integer) to send to stop your
+ processing.
+ The default value (if none is specified) is SIGSTOP.
+cont_signal::
+ Specify to i3bar the signal (as an integer)to send to continue your
+ processing.
+ The default value (if none is specified) is SIGCONT.
+
=== Blocks in detail
full_text::
border (string)::
Can be either "normal", "none" or "1pixel", dependending on the
container’s border style.
+current_border_width (integer)::
+ Number of pixels of the border width.
layout (string)::
Can be either "splith", "splitv", "stacked", "tabbed", "dockarea" or
"output".
output (1)::
Sent when RandR issues a change notification (of either screens,
outputs, CRTCs or output properties).
+mode (2)::
+ Sent whenever i3 changes its binding mode.
*Example:*
--------------------------------------------------------------------
{ "change": "unspecified" }
---------------------------
+=== mode event
+
+This event consists of a single serialized map containing a property
++change (string)+ which holds the name of current mode in use. The name
+is the same as specified in config when creating a mode. The default
+mode is simply named default.
+
+*Example:*
+---------------------------
+{ "change": "default" }
+---------------------------
+
== See also
For some languages, libraries are available (so you don’t have to implement
*Syntax*:
---------------------------------------------
-new_window <normal|1pixel|none>
+new_window <normal|1pixel|none|pixel>
---------------------------------------------
-
*Example*:
---------------------
new_window 1pixel
---------------------
+The "normal" and "pixel" border styles support an optional border width in
+pixels:
+
+*Example*:
+---------------------
+# The same as new_window none
+new_window pixel 0
+
+# A 3 px border
+new_window pixel 3
+---------------------
+
+
=== Hiding vertical borders
You can hide vertical borders adjacent to the screen edges using
workspace_auto_back_and_forth yes
---------------------------------
+=== Delaying urgency hint reset on workspace change
+
+If an application on another workspace sets an urgency hint, switching to this
+workspace may lead to immediate focus of the application, which also means the
+window decoration color would be immediately resetted to +client.focused+. This
+may make it unnecessarily hard to tell which window originally raised the
+event.
+
+In order to prevent this, you can tell i3 to delay resetting the urgency state
+by a certain time using the +force_display_urgency_hint+ directive. Setting the
+value to 0 disables this feature.
+
+The default is 500ms.
+
+*Syntax*:
+---------------------------------------
+force_display_urgency_hint <timeout> ms
+---------------------------------------
+
+*Example*:
+---------------------------------
+force_display_urgency_hint 500 ms
+---------------------------------
+
== Configuring i3bar
The bar at the bottom of your monitor is drawn by a separate process called
[[back_and_forth]]
To switch back to the previously focused workspace, use +workspace
-back_and_forth+.
+back_and_forth+; likewise, you can move containers to the previously focused
+workspace using +move container to workspace back_and_forth+.
*Syntax*:
-----------------------------------
workspace <next|prev|next_on_output|prev_on_output>
workspace back_and_forth
workspace <name>
-workspace number <number>
+workspace number <name>
move [window|container] [to] workspace <name>
-move [window|container] [to] workspace number <number>
+move [window|container] [to] workspace number <name>
move [window|container] [to] workspace <prev|next|current>
-----------------------------------
# switch between the current and the previously focused one
bindsym mod+b workspace back_and_forth
+bindsym mod+Shift+b move container to workspace back_and_forth
# move the whole workspace to the next output
bindsym mod+x move workspace to output right
will order them numerically. Also, you will be able to use +workspace number 1+
to switch to the workspace which begins with number 1, regardless of which name
it has. This is useful in case you are changing the workspace’s name
-dynamically.
+dynamically. To combine both commands you can use +workspace number 1: mail+ to
+specify a default name if there's currently no workspace starting with a "1".
==== Renaming workspaces
Instead, when using +scratchpad show+, the window will be shown again, as a
floating window, centered on your current workspace (using +scratchpad show+ on
a visible scratchpad window will make it hidden again, so you can have a
-keybinding to toggle).
+keybinding to toggle). Note that this is just a normal floating window, so if
+you want to "remove it from scratchpad", you can simple make it tiling again
+(+floating toggle+).
As the name indicates, this is useful for having a window with your favorite
editor always at hand. However, you can also use this for other permanently
3. If you have many workspaces on many monitors, it might get hard to keep
track of which window you put where. Thus, you can use vim-like marks to
quickly switch between windows. See <<vim_like_marks>>.
+4. For information on how to move existing workspaces between monitors,
+ see <<_moving_containers_workspaces_to_randr_outputs>>.
== i3 and the rest of your software world
-#ifndef _XCB_H
-#define _XCB_H
+#ifndef I3_XCB_H
+#define I3_XCB_H
/* from X11/keysymdef.h */
#define XCB_NUM_LOCK 0xff7f
-#ifndef _I3_INPUT
-#define _I3_INPUT
+#ifndef I3_INPUT
+#define I3_INPUT
#include <err.h>
-#ifndef _I3_NAGBAR
-#define _I3_NAGBAR
+#ifndef I3_NAGBAR
+#define I3_NAGBAR
#include <err.h>
#ifndef CHILD_H_
#define CHILD_H_
+#include <stdbool.h>
+
#define STDIN_CHUNK_SIZE 1024
+typedef struct {
+ pid_t pid;
+
+ /**
+ * The version number is an uint32_t to avoid machines with different sizes of
+ * 'int' to allow different values here. It’s highly unlikely we ever exceed
+ * even an int8_t, but still…
+ */
+ uint32_t version;
+
+ bool stopped;
+ /**
+ * The signal requested by the client to inform it of the hidden state of i3bar
+ */
+ int stop_signal;
+ /**
+ * The signal requested by the client to inform it of theun hidden state of i3bar
+ */
+ int cont_signal;
+} i3bar_child;
+
/*
* Start a child-process with the specified command and reroute stdin.
* We actually start a $SHELL to execute the command so we don't have to care
char *color;
+ bool urgent;
+
/* The amount of pixels necessary to render this block. This variable is
* only temporarily used in refresh_statusline(). */
uint32_t width;
#include "xcb.h"
#include "config.h"
#include "libi3.h"
-#include "determine_json_version.h"
+#include "parse_json_header.h"
#endif
+++ /dev/null
-/*
- * vim:ts=4:sw=4:expandtab
- *
- * i3bar - an xcb-based status- and ws-bar for i3
- * © 2010-2012 Axel Wagner and contributors (see also: LICENSE)
- *
- * determine_json_version.c: Determines the JSON protocol version based on the
- * first line of input from a child program.
- *
- */
-#ifndef DETERMINE_JSON_VERSION_H_
-#define DETERMINE_JSON_VERSION_H_
-
-#include <stdint.h>
-
-/*
- * Determines the JSON i3bar protocol version from the given buffer. In case
- * the buffer does not contain valid JSON, or no version field is found, this
- * function returns -1. The amount of bytes consumed by parsing the header is
- * returned in *consumed (if non-NULL).
- *
- * The return type is an int32_t to avoid machines with different sizes of
- * 'int' to allow different values here. It’s highly unlikely we ever exceed
- * even an int8_t, but still…
- *
- */
-int32_t determine_json_version(const unsigned char *buffer, int length, unsigned int *consumed);
-
-#endif
--- /dev/null
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3bar - an xcb-based status- and ws-bar for i3
+ * © 2010-2012 Axel Wagner and contributors (see also: LICENSE)
+ *
+ * parse_json_header.c: Parse the JSON protocol header to determine
+ * protocol version and features.
+ *
+ */
+#ifndef PARSE_JSON_HEADER_H_
+#define PARSE_JSON_HEADER_H_
+
+#include <stdint.h>
+
+/**
+ * Parse the JSON protocol header to determine protocol version and features.
+ * In case the buffer does not contain a valid header (invalid JSON, or no
+ * version field found), the 'correct' field of the returned header is set to
+ * false. The amount of bytes consumed by parsing the header is returned in
+ * *consumed (if non-NULL).
+ *
+ */
+void parse_json_header(i3bar_child *child, const unsigned char *buffer, int length, unsigned int *consumed);
+
+#endif
#undef MIN
#define MIN(x,y) ((x) < (y) ? (x) : (y))
+#define STARTS_WITH(string, len, needle) ((len >= strlen(needle)) && strncasecmp(string, needle, strlen(needle)) == 0)
+
/* Securely free p */
#define FREE(p) do { \
if (p != NULL) { \
* Render the bars, with buttons and statusline
*
*/
-void draw_bars(void);
+void draw_bars(bool force_unhide);
/*
* Redraw the bars, i.e. simply copy the buffer to the barwindow
#include "common.h"
/* Global variables for child_*() */
-pid_t child_pid;
+i3bar_child child = { 0 };
/* stdin- and sigchild-watchers */
ev_io *stdin_io;
ev_child *child_sig;
/* JSON parser for stdin */
-bool first_line = true;
-bool plaintext = false;
yajl_callbacks callbacks;
yajl_handle parser;
typedef struct parser_ctx {
+ /* True if one of the parsed blocks was urgent */
+ bool has_urgent;
+
/* A copy of the last JSON map key. */
char *last_map_key;
ev_child_stop(main_loop, child_sig);
FREE(child_sig);
}
+
+ memset(&child, 0, sizeof(i3bar_child));
}
/*
return 1;
}
+static int stdin_boolean(void *context, int val) {
+ parser_ctx *ctx = context;
+ if (strcasecmp(ctx->last_map_key, "urgent") == 0) {
+ ctx->block.urgent = val;
+ }
+ return 1;
+}
+
#if YAJL_MAJOR >= 2
static int stdin_string(void *context, const unsigned char *val, size_t len) {
#else
* i3bar doesn’t crash and the user gets an annoying message. */
if (!new_block->full_text)
new_block->full_text = i3string_from_utf8("SPEC VIOLATION (null)");
+ if (new_block->urgent)
+ ctx->has_urgent = true;
TAILQ_INSERT_TAIL(&statusline_head, new_block, blocks);
return 1;
}
}
/*
- * Callbalk for stdin. We read a line from stdin and store the result
- * in statusline
+ * Helper function to read stdin
*
*/
-void stdin_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) {
+static unsigned char *get_buffer(ev_io *watcher, int *ret_buffer_len) {
int fd = watcher->fd;
int n = 0;
int rec = 0;
/* end of file, kill the watcher */
ELOG("stdin: received EOF\n");
cleanup();
- draw_bars();
- return;
+ draw_bars(false);
+ *ret_buffer_len = -1;
+ return NULL;
}
rec += n;
}
if (*buffer == '\0') {
FREE(buffer);
- return;
+ rec = -1;
}
+ *ret_buffer_len = rec;
+ return buffer;
+}
- unsigned char *json_input = buffer;
- if (first_line) {
- DLOG("Detecting input type based on buffer *%.*s*\n", rec, buffer);
- /* Detect whether this is JSON or plain text. */
- unsigned int consumed = 0;
- /* At the moment, we don’t care for the version. This might change
- * in the future, but for now, we just discard it. */
- plaintext = (determine_json_version(buffer, buffer_len, &consumed) == -1);
- if (plaintext) {
- /* In case of plaintext, we just add a single block and change its
- * full_text pointer later. */
- struct status_block *new_block = scalloc(sizeof(struct status_block));
- TAILQ_INSERT_TAIL(&statusline_head, new_block, blocks);
- } else {
- json_input += consumed;
- rec -= consumed;
- }
- first_line = false;
- }
- if (!plaintext) {
- yajl_status status = yajl_parse(parser, json_input, rec);
+static void read_flat_input(char *buffer, int length) {
+ struct status_block *first = TAILQ_FIRST(&statusline_head);
+ /* Clear the old buffer if any. */
+ I3STRING_FREE(first->full_text);
+ /* Remove the trailing newline and terminate the string at the same
+ * time. */
+ if (buffer[length-1] == '\n' || buffer[length-1] == '\r')
+ buffer[length-1] = '\0';
+ else buffer[length] = '\0';
+ first->full_text = i3string_from_utf8(buffer);
+}
+
+static bool read_json_input(unsigned char *input, int length) {
+ yajl_status status = yajl_parse(parser, input, length);
+ bool has_urgent = false;
#if YAJL_MAJOR >= 2
- if (status != yajl_status_ok) {
+ if (status != yajl_status_ok) {
#else
- if (status != yajl_status_ok && status != yajl_status_insufficient_data) {
+ if (status != yajl_status_ok && status != yajl_status_insufficient_data) {
#endif
- fprintf(stderr, "[i3bar] Could not parse JSON input (code %d): %.*s\n",
- status, rec, json_input);
+ fprintf(stderr, "[i3bar] Could not parse JSON input (code %d): %.*s\n",
+ status, length, input);
+ } else if (parser_context.has_urgent) {
+ has_urgent = true;
+ }
+ return has_urgent;
+}
+
+/*
+ * Callbalk for stdin. We read a line from stdin and store the result
+ * in statusline
+ *
+ */
+void stdin_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) {
+ int rec;
+ unsigned char *buffer = get_buffer(watcher, &rec);
+ if (buffer == NULL)
+ return;
+ bool has_urgent = false;
+ if (child.version > 0) {
+ has_urgent = read_json_input(buffer, rec);
+ } else {
+ read_flat_input((char*)buffer, rec);
+ }
+ free(buffer);
+ draw_bars(has_urgent);
+}
+
+/*
+ * Callbalk for stdin first line. We read the first line to detect
+ * whether this is JSON or plain text
+ *
+ */
+void stdin_io_first_line_cb(struct ev_loop *loop, ev_io *watcher, int revents) {
+ int rec;
+ unsigned char *buffer = get_buffer(watcher, &rec);
+ if (buffer == NULL)
+ return;
+ DLOG("Detecting input type based on buffer *%.*s*\n", rec, buffer);
+ /* Detect whether this is JSON or plain text. */
+ unsigned int consumed = 0;
+ /* At the moment, we don’t care for the version. This might change
+ * in the future, but for now, we just discard it. */
+ parse_json_header(&child, buffer, rec, &consumed);
+ if (child.version > 0) {
+ /* If hide-on-modifier is set, we start of by sending the
+ * child a SIGSTOP, because the bars aren't mapped at start */
+ if (config.hide_on_modifier) {
+ stop_child();
}
+ read_json_input(buffer + consumed, rec - consumed);
} else {
- struct status_block *first = TAILQ_FIRST(&statusline_head);
- /* Clear the old buffer if any. */
- I3STRING_FREE(first->full_text);
- /* Remove the trailing newline and terminate the string at the same
- * time. */
- if (buffer[rec-1] == '\n' || buffer[rec-1] == '\r')
- buffer[rec-1] = '\0';
- else buffer[rec] = '\0';
- first->full_text = i3string_from_utf8((const char *)buffer);
+ /* In case of plaintext, we just add a single block and change its
+ * full_text pointer later. */
+ struct status_block *new_block = scalloc(sizeof(struct status_block));
+ TAILQ_INSERT_TAIL(&statusline_head, new_block, blocks);
+ read_flat_input((char*)buffer, rec);
}
free(buffer);
- draw_bars();
+ ev_io_stop(main_loop, stdin_io);
+ ev_io_init(stdin_io, &stdin_io_cb, STDIN_FILENO, EV_READ);
+ ev_io_start(main_loop, stdin_io);
}
/*
*/
void child_sig_cb(struct ev_loop *loop, ev_child *watcher, int revents) {
ELOG("Child (pid: %d) unexpectedly exited with status %d\n",
- child_pid,
+ child.pid,
watcher->rstatus);
cleanup();
}
/* Allocate a yajl parser which will be used to parse stdin. */
memset(&callbacks, '\0', sizeof(yajl_callbacks));
callbacks.yajl_map_key = stdin_map_key;
+ callbacks.yajl_boolean = stdin_boolean;
callbacks.yajl_string = stdin_string;
callbacks.yajl_start_array = stdin_start_array;
callbacks.yajl_end_array = stdin_end_array;
parser = yajl_alloc(&callbacks, NULL, &parser_context);
#endif
- child_pid = 0;
if (command != NULL) {
int fd[2];
if (pipe(fd) == -1)
err(EXIT_FAILURE, "pipe(fd)");
- child_pid = fork();
- switch (child_pid) {
+ child.pid = fork();
+ switch (child.pid) {
case -1:
ELOG("Couldn't fork(): %s\n", strerror(errno));
exit(EXIT_FAILURE);
dup2(fd[0], STDIN_FILENO);
- /* If hide-on-modifier is set, we start of by sending the
- * child a SIGSTOP, because the bars aren't mapped at start */
- if (config.hide_on_modifier) {
- stop_child();
- }
-
break;
}
}
fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK);
stdin_io = smalloc(sizeof(ev_io));
- ev_io_init(stdin_io, &stdin_io_cb, STDIN_FILENO, EV_READ);
+ ev_io_init(stdin_io, &stdin_io_first_line_cb, STDIN_FILENO, EV_READ);
ev_io_start(main_loop, stdin_io);
/* We must cleanup, if the child unexpectedly terminates */
child_sig = smalloc(sizeof(ev_child));
- ev_child_init(child_sig, &child_sig_cb, child_pid, 0);
+ ev_child_init(child_sig, &child_sig_cb, child.pid, 0);
ev_child_start(main_loop, child_sig);
atexit(kill_child_at_exit);
*
*/
void kill_child_at_exit(void) {
- if (child_pid != 0) {
- kill(child_pid, SIGCONT);
- kill(child_pid, SIGTERM);
+ if (child.pid > 0) {
+ if (child.cont_signal > 0 && child.stopped)
+ kill(child.pid, child.cont_signal);
+ kill(child.pid, SIGTERM);
}
}
*
*/
void kill_child(void) {
- if (child_pid != 0) {
- kill(child_pid, SIGCONT);
- kill(child_pid, SIGTERM);
+ if (child.pid > 0) {
+ if (child.cont_signal > 0 && child.stopped)
+ kill(child.pid, child.cont_signal);
+ kill(child.pid, SIGTERM);
int status;
- waitpid(child_pid, &status, 0);
- child_pid = 0;
+ waitpid(child.pid, &status, 0);
cleanup();
}
}
*
*/
void stop_child(void) {
- if (child_pid != 0) {
- kill(child_pid, SIGSTOP);
+ if (child.stop_signal > 0 && !child.stopped) {
+ child.stopped = true;
+ kill(child.pid, child.stop_signal);
}
}
*
*/
void cont_child(void) {
- if (child_pid != 0) {
- kill(child_pid, SIGCONT);
+ if (child.cont_signal > 0 && child.stopped) {
+ child.stopped = false;
+ kill(child.pid, child.cont_signal);
}
}
+++ /dev/null
-/*
- * vim:ts=4:sw=4:expandtab
- *
- * i3bar - an xcb-based status- and ws-bar for i3
- * © 2010-2012 Axel Wagner and contributors (see also: LICENSE)
- *
- * determine_json_version.c: Determines the JSON protocol version based on the
- * first line of input from a child program.
- *
- */
-#include <stdlib.h>
-#include <unistd.h>
-#include <sys/types.h>
-#include <sys/wait.h>
-#include <signal.h>
-#include <stdio.h>
-#include <fcntl.h>
-#include <string.h>
-#include <errno.h>
-#include <err.h>
-#include <ev.h>
-#include <stdbool.h>
-#include <stdint.h>
-#include <yajl/yajl_common.h>
-#include <yajl/yajl_parse.h>
-#include <yajl/yajl_version.h>
-
-static bool version_key;
-static int32_t version_number;
-
-#if YAJL_MAJOR >= 2
-static int version_integer(void *ctx, long long val) {
-#else
-static int version_integer(void *ctx, long val) {
-#endif
- if (version_key)
- version_number = (uint32_t)val;
- return 1;
-}
-
-#if YAJL_MAJOR >= 2
-static int version_map_key(void *ctx, const unsigned char *stringval, size_t stringlen) {
-#else
-static int version_map_key(void *ctx, const unsigned char *stringval, unsigned int stringlen) {
-#endif
- version_key = (stringlen == strlen("version") &&
- strncmp((const char*)stringval, "version", strlen("version")) == 0);
- return 1;
-}
-
-static yajl_callbacks version_callbacks = {
- NULL, /* null */
- NULL, /* boolean */
- &version_integer,
- NULL, /* double */
- NULL, /* number */
- NULL, /* string */
- NULL, /* start_map */
- &version_map_key,
- NULL, /* end_map */
- NULL, /* start_array */
- NULL /* end_array */
-};
-
-/*
- * Determines the JSON i3bar protocol version from the given buffer. In case
- * the buffer does not contain valid JSON, or no version field is found, this
- * function returns -1. The amount of bytes consumed by parsing the header is
- * returned in *consumed (if non-NULL).
- *
- * The return type is an int32_t to avoid machines with different sizes of
- * 'int' to allow different values here. It’s highly unlikely we ever exceed
- * even an int8_t, but still…
- *
- */
-int32_t determine_json_version(const unsigned char *buffer, int length, unsigned int *consumed) {
-#if YAJL_MAJOR >= 2
- yajl_handle handle = yajl_alloc(&version_callbacks, NULL, NULL);
- /* Allow trailing garbage. yajl 1 always behaves that way anyways, but for
- * yajl 2, we need to be explicit. */
- yajl_config(handle, yajl_allow_trailing_garbage, 1);
-#else
- yajl_parser_config parse_conf = { 0, 0 };
-
- yajl_handle handle = yajl_alloc(&version_callbacks, &parse_conf, NULL, NULL);
-#endif
-
- version_key = false;
- version_number = -1;
-
- yajl_status state = yajl_parse(handle, buffer, length);
- if (state != yajl_status_ok) {
- version_number = -1;
- if (consumed != NULL)
- *consumed = 0;
- } else {
- if (consumed != NULL)
- *consumed = yajl_get_bytes_consumed(handle);
- }
-
- yajl_free(handle);
-
- return version_number;
-}
void got_workspace_reply(char *reply) {
DLOG("Got Workspace-Data!\n");
parse_workspaces_json(reply);
- draw_bars();
+ draw_bars(false);
}
/*
kick_tray_clients(o_walk);
}
- draw_bars();
+ draw_bars(false);
}
/*
--- /dev/null
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3bar - an xcb-based status- and ws-bar for i3
+ * © 2010-2012 Axel Wagner and contributors (see also: LICENSE)
+ *
+ * parse_json_header.c: Parse the JSON protocol header to determine
+ * protocol version and features.
+ *
+ */
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <signal.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <string.h>
+#include <errno.h>
+#include <err.h>
+#include <ev.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <yajl/yajl_common.h>
+#include <yajl/yajl_parse.h>
+#include <yajl/yajl_version.h>
+
+#include "common.h"
+
+static enum {
+ KEY_VERSION,
+ KEY_STOP_SIGNAL,
+ KEY_CONT_SIGNAL,
+ NO_KEY
+} current_key;
+
+#if YAJL_MAJOR >= 2
+static int header_integer(void *ctx, long long val) {
+#else
+static int header_integer(void *ctx, long val) {
+#endif
+ i3bar_child *child = ctx;
+
+ switch (current_key) {
+ case KEY_VERSION:
+ child->version = val;
+ break;
+ case KEY_STOP_SIGNAL:
+ child->stop_signal = val;
+ break;
+ case KEY_CONT_SIGNAL:
+ child->cont_signal = val;
+ break;
+ default:
+ break;
+ }
+ return 1;
+}
+
+#define CHECK_KEY(name) (stringlen == strlen(name) && \
+ STARTS_WITH((const char*)stringval, stringlen, name))
+
+#if YAJL_MAJOR >= 2
+static int header_map_key(void *ctx, const unsigned char *stringval, size_t stringlen) {
+#else
+static int header_map_key(void *ctx, const unsigned char *stringval, unsigned int stringlen) {
+#endif
+ if (CHECK_KEY("version")) {
+ current_key = KEY_VERSION;
+ } else if (CHECK_KEY("stop_signal")) {
+ current_key = KEY_STOP_SIGNAL;
+ } else if (CHECK_KEY("cont_signal")) {
+ current_key = KEY_CONT_SIGNAL;
+ }
+ return 1;
+}
+
+static yajl_callbacks version_callbacks = {
+ NULL, /* null */
+ NULL, /* boolean */
+ &header_integer,
+ NULL, /* double */
+ NULL, /* number */
+ NULL, /* string */
+ NULL, /* start_map */
+ &header_map_key,
+ NULL, /* end_map */
+ NULL, /* start_array */
+ NULL /* end_array */
+};
+
+static void child_init(i3bar_child *child) {
+ child->version = 0;
+ child->stop_signal = SIGSTOP;
+ child->cont_signal = SIGCONT;
+}
+
+/*
+ * Parse the JSON protocol header to determine protocol version and features.
+ * In case the buffer does not contain a valid header (invalid JSON, or no
+ * version field found), the 'correct' field of the returned header is set to
+ * false. The amount of bytes consumed by parsing the header is returned in
+ * *consumed (if non-NULL).
+ *
+ */
+void parse_json_header(i3bar_child *child, const unsigned char *buffer, int length, unsigned int *consumed) {
+ child_init(child);
+
+ current_key = NO_KEY;
+
+#if YAJL_MAJOR >= 2
+ yajl_handle handle = yajl_alloc(&version_callbacks, NULL, child);
+ /* Allow trailing garbage. yajl 1 always behaves that way anyways, but for
+ * yajl 2, we need to be explicit. */
+ yajl_config(handle, yajl_allow_trailing_garbage, 1);
+#else
+ yajl_parser_config parse_conf = { 0, 0 };
+
+ yajl_handle handle = yajl_alloc(&version_callbacks, &parse_conf, NULL, child);
+#endif
+
+ yajl_status state = yajl_parse(handle, buffer, length);
+ if (state != yajl_status_ok) {
+ child_init(child);
+ if (consumed != NULL)
+ *consumed = 0;
+ } else {
+ if (consumed != NULL)
+ *consumed = yajl_get_bytes_consumed(handle);
+ }
+
+ yajl_free(handle);
+}
/* Trigger an update to copy the statusline text to the appropriate
* position */
configure_trayclients();
- draw_bars();
+ draw_bars(false);
}
}
}
/* Trigger an update, we now have more space for the statusline */
configure_trayclients();
- draw_bars();
+ draw_bars(false);
return;
}
}
xcb_unmap_window(xcb_connection, trayclient->win);
trayclient->mapped = map_it;
configure_trayclients();
- draw_bars();
+ draw_bars(false);
} else if (!trayclient->mapped && map_it) {
/* need to map the window */
xcb_map_window(xcb_connection, trayclient->win);
trayclient->mapped = map_it;
configure_trayclients();
- draw_bars();
+ draw_bars(false);
}
free(xembedr);
}
* Render the bars, with buttons and statusline
*
*/
-void draw_bars(void) {
+void draw_bars(bool unhide) {
DLOG("Drawing Bars...\n");
int i = 0;
refresh_statusline();
+ static char *last_urgent_ws = NULL;
+ bool walks_away = true;
+
i3_output *outputs_walk;
SLIST_FOREACH(outputs_walk, outputs, slist) {
if (!outputs_walk->active) {
}
i3_ws *ws_walk;
- static char *last_urgent_ws = NULL;
- bool has_urgent = false, walks_away = true;
TAILQ_FOREACH(ws_walk, outputs_walk->workspaces, tailq) {
DLOG("Drawing Button for WS %s at x = %d, len = %d\n", i3string_as_utf8(ws_walk->name), i, ws_walk->name_width);
fg_color = colors.urgent_ws_fg;
bg_color = colors.urgent_ws_bg;
border_color = colors.urgent_ws_border;
- has_urgent = true;
+ unhide = true;
if (!ws_walk->focused) {
FREE(last_urgent_ws);
last_urgent_ws = sstrdup(i3string_as_utf8(ws_walk->name));
}
- /* The urgent-hint should get noticed, so we unhide the bars shortly */
- unhide_bars();
}
uint32_t mask = XCB_GC_FOREGROUND | XCB_GC_BACKGROUND;
uint32_t vals_border[] = { border_color, border_color };
i += 10 + ws_walk->name_width + 1;
}
- if (!has_urgent && !mod_pressed && walks_away) {
+ i = 0;
+ }
+
+ if (!mod_pressed) {
+ if (unhide) {
+ /* The urgent-hint should get noticed, so we unhide the bars shortly */
+ unhide_bars();
+ } else if (walks_away) {
FREE(last_urgent_ws);
hide_bars();
}
-
- i = 0;
}
redraw_bars();
* compile-time.
*
*/
-#ifndef _ALL_H
-#define _ALL_H
+#ifndef I3_ALL_H
+#define I3_ALL_H
#include <assert.h>
#include <stdbool.h>
* assignments.c: Assignments for specific windows (for_window).
*
*/
-#ifndef _ASSIGNMENTS_H
-#define _ASSIGNMENTS_H
+#ifndef I3_ASSIGNMENTS_H
+#define I3_ASSIGNMENTS_H
/**
* Checks the list of assignments for the given window and runs all matching
* click.c: Button press (mouse click) events.
*
*/
-#ifndef _CLICK_H
-#define _CLICK_H
+#ifndef I3_CLICK_H
+#define I3_CLICK_H
/**
* The button press X callback. This function determines whether the floating
* cmdparse.y: the parser for commands you send to i3 (or bind on keys)
*
*/
-#ifndef _CMDPARSE_H
-#define _CMDPARSE_H
+#ifndef I3_CMDPARSE_H
+#define I3_CMDPARSE_H
char *parse_cmd(const char *new);
* commands.c: all command functions (see commands_parser.c)
*
*/
-#ifndef _COMMANDS_H
-#define _COMMANDS_H
+#ifndef I3_COMMANDS_H
+#define I3_COMMANDS_H
#include "commands_parser.h"
*/
void cmd_move_con_to_workspace(I3_CMD, char *which);
+/**
+ * Implementation of 'move [window|container] [to] workspace back_and_forth'.
+ *
+ */
+void cmd_move_con_to_workspace_back_and_forth(I3_CMD);
+
/**
* Implementation of 'move [window|container] [to] workspace <name>'.
*
* Implementation of 'border normal|none|1pixel|toggle'.
*
*/
-void cmd_border(I3_CMD, char *border_style_str);
+void cmd_border(I3_CMD, char *border_style_str, char *border_width);
/**
* Implementation of 'nop <comment>'.
* commands.c: all command functions (see commands_parser.c)
*
*/
-#ifndef _COMMANDS_PARSER_H
-#define _COMMANDS_PARSER_H
+#ifndef I3_COMMANDS_PARSER_H
+#define I3_COMMANDS_PARSER_H
#include <yajl/yajl_gen.h>
* …).
*
*/
-#ifndef _CON_H
-#define _CON_H
+#ifndef I3_CON_H
+#define I3_CON_H
/**
* Create a new container (and attach it to the given parent, if not NULL).
*/
Con *con_get_fullscreen_con(Con *con, int fullscreen_mode);
+/**
+ * Returns true if the container is internal, such as __i3_scratch
+ *
+ */
+bool con_is_internal(Con *con);
+
/**
* Returns true if the node is floating.
*
* floating window.
*
*/
-void con_set_border_style(Con *con, int border_style);
+void con_set_border_style(Con *con, int border_style, int border_width);
/**
* This function changes the layout of a given container. Use it to handle
*/
bool con_fullscreen_permits_focusing(Con *con);
+/**
+ * Checks if the given container has an urgent child.
+ *
+ */
+bool con_has_urgent_child(Con *con);
+
+/**
+ * Make all parent containers urgent if con is urgent or clear the urgent flag
+ * of all parent containers if there are no more urgent children left.
+ *
+ */
+void con_update_parents_urgency(Con *con);
+
+/**
+ * Create a string representing the subtree under con.
+ *
+ */
+char *con_get_tree_representation(Con *con);
+
#endif
* mode).
*
*/
-#ifndef _CONFIG_H
-#define _CONFIG_H
+#ifndef I3_CONFIG_H
+#define I3_CONFIG_H
#include <stdbool.h>
#include "queue.h"
int default_layout;
int container_stack_limit;
int container_stack_limit_value;
+ int default_border_width;
/** Default orientation for new containers */
int default_orientation;
* between two workspaces. */
bool workspace_auto_back_and_forth;
+ /** By default, urgency is cleared immediately when switching to another
+ * workspace leads to focusing the con with the urgency hint. When having
+ * multiple windows on that workspace, the user needs to guess which
+ * application raised the event. To prevent this, the reset of the urgency
+ * flag can be delayed using an urgency timer. */
+ float workspace_urgency_timer;
+
/** The default border style for new windows. */
border_style_t default_border;
* include/data.h: This file defines all data structures used by i3
*
*/
-#ifndef _DATA_H
-#define _DATA_H
+#ifndef I3_DATA_H
+#define I3_DATA_H
#define SN_API_NOT_YET_FROZEN 1
#include <libsn/sn-launcher.h>
*****************************************************************************/
typedef enum { D_LEFT, D_RIGHT, D_UP, D_DOWN } direction_t;
typedef enum { NO_ORIENTATION = 0, HORIZ, VERT } orientation_t;
-typedef enum { BS_NORMAL = 0, BS_NONE = 1, BS_1PIXEL = 2 } border_style_t;
+typedef enum { BS_NORMAL = 0, BS_NONE = 1, BS_PIXEL = 2 } border_style_t;
/** parameter to specify whether tree_close() and x_window_kill() should kill
* only this specific window or the whole X11 client */
/* the x11 border pixel attribute */
int border_width;
+ int current_border_width;
/* minimum increment size specified for the window (in pixels) */
int width_increment;
* inside this container (if any) sets the urgency hint, for example. */
bool urgent;
+ /* timer used for disabling urgency */
+ struct ev_timer *urgency_timer;
+
/* ids/pixmap/graphics context for the frame window */
xcb_window_t frame;
xcb_pixmap_t pixmap;
* events. This code is from xcb-util.
*
*/
-#ifndef _DEBUG_H
-#define _DEBUG_H
+#ifndef I3_DEBUG_H
+#define I3_DEBUG_H
int handle_event(void *ignored, xcb_connection_t *c, xcb_generic_event_t *e);
* display_version.c: displays the running i3 version, runs as part of
* i3 --moreversion.
*/
-#ifndef _DISPLAY_VERSION_H
-#define _DISPLAY_VERSION_H
+#ifndef I3_DISPLAY_VERSION_H
+#define I3_DISPLAY_VERSION_H
/**
* Connects to i3 to find out the currently running version. Useful since it
* ewmh.c: Get/set certain EWMH properties easily.
*
*/
-#ifndef _EWMH_C
-#define _EWMH_C
+#ifndef I3_EWMH_C
+#define I3_EWMH_C
/**
* Updates _NET_CURRENT_DESKTOP with the current desktop number.
* which don’t support multi-monitor in a useful way) and for our testsuite.
*
*/
-#ifndef _FAKE_OUTPUTS_H
-#define _FAKE_OUTPUTS_H
+#ifndef I3_FAKE_OUTPUTS_H
+#define I3_FAKE_OUTPUTS_H
/**
* Creates outputs according to the given specification.
* floating.c: Floating windows.
*
*/
-#ifndef _FLOATING_H
-#define _FLOATING_H
+#ifndef I3_FLOATING_H
+#define I3_FLOATING_H
#include "tree.h"
*
*/
void drag_pointer(Con *con, const xcb_button_press_event_t *event,
- xcb_window_t confine_to, border_t border, callback_t callback,
- const void *extra);
+ xcb_window_t confine_to, border_t border, int cursor,
+ callback_t callback, const void *extra);
/**
* Repositions the CT_FLOATING_CON to have the coordinates specified by
* …).
*
*/
-#ifndef _HANDLERS_H
-#define _HANDLERS_H
+#ifndef I3_HANDLERS_H
+#define I3_HANDLERS_H
#include <xcb/randr.h>
* i3.h: global variables that are used all over i3.
*
*/
-#ifndef _I3_H
-#define _I3_H
+#ifndef I3_I3_H
+#define I3_I3_H
#include <sys/time.h>
#include <sys/resource.h>
* for the IPC interface to i3 (see docs/ipc for more information).
*
*/
-#ifndef _I3_IPC_H
-#define _I3_IPC_H
+#ifndef I3_I3_IPC_H
+#define I3_I3_IPC_H
/*
* Messages from clients to i3
/* The output event will be triggered upon changes in the output list */
#define I3_IPC_EVENT_OUTPUT (I3_IPC_EVENT_MASK | 1)
+/* The output event will be triggered upon mode changes */
+#define I3_IPC_EVENT_MODE (I3_IPC_EVENT_MASK | 2)
+
#endif
* ipc.c: UNIX domain socket IPC (initialization, client handling, protocol).
*
*/
-#ifndef _IPC_H
-#define _IPC_H
+#ifndef I3_IPC_H
+#define I3_IPC_H
#include <ev.h>
#include <stdbool.h>
* key_press.c: key press handler
*
*/
-#ifndef _KEY_PRESS_H
-#define _KEY_PRESS_H
+#ifndef I3_KEY_PRESS_H
+#define I3_KEY_PRESS_H
/**
* There was a key press. We compare this key code with our bindings table and pass
* as i3-msg, i3-config-wizard, …
*
*/
-#ifndef _LIBI3_H
-#define _LIBI3_H
+#ifndef I3_LIBI3_H
+#define I3_LIBI3_H
#include <stdbool.h>
#include <stdarg.h>
* restart.
*
*/
-#ifndef _LOAD_LAYOUT_H
-#define _LOAD_LAYOUT_H
+#ifndef I3_LOAD_LAYOUT_H
+#define I3_LOAD_LAYOUT_H
void tree_append_json(const char *filename);
* log.c: Logging functions.
*
*/
-#ifndef _LOG_H
-#define _LOG_H
+#ifndef I3_LOG_H
+#define I3_LOG_H
#include <stdarg.h>
#include <stdbool.h>
* manage.c: Initially managing new windows (or existing ones on restart).
*
*/
-#ifndef _MANAGE_H
-#define _MANAGE_H
+#ifndef I3_MANAGE_H
+#define I3_MANAGE_H
#include "data.h"
* match_matches_window() to find the windows affected by this command.
*
*/
-#ifndef _MATCH_H
-#define _MATCH_H
+#ifndef I3_MATCH_H
+#define I3_MATCH_H
/*
* Initializes the Match data structure. This function is necessary because the
* move.c: Moving containers into some direction.
*
*/
-#ifndef _MOVE_H
-#define _MOVE_H
+#ifndef I3_MOVE_H
+#define I3_MOVE_H
/**
* Moves the current container in the given direction (TOK_LEFT, TOK_RIGHT,
* output.c: Output (monitor) related functions.
*
*/
-#ifndef _OUTPUT_H
-#define _OUTPUT_H
+#ifndef I3_OUTPUT_H
+#define I3_OUTPUT_H
/**
* Returns the output container below the given output container.
* (take your time to read it completely, it answers all questions).
*
*/
-#ifndef _RANDR_H
-#define _RANDR_H
+#ifndef I3_RANDR_H
+#define I3_RANDR_H
#include "data.h"
#include <xcb/randr.h>
TAILQ_HEAD(outputs_head, xoutput);
extern struct outputs_head outputs;
+typedef enum {
+ CLOSEST_OUTPUT = 0,
+ FARTHEST_OUTPUT = 1
+} output_close_far_t;
+
/**
* We have just established a connection to the X server and need the initial
* XRandR information to setup workspaces for each screen.
* Gets the output which is the next one in the given direction.
*
*/
-Output *get_output_next(direction_t direction, Output *current);
+Output *get_output_next(direction_t direction, Output *current, output_close_far_t close_far);
#endif
* regex.c: Interface to libPCRE (perl compatible regular expressions).
*
*/
-#ifndef _REGEX_H
-#define _REGEX_H
+#ifndef I3_REGEX_H
+#define I3_REGEX_H
/**
* Creates a new 'regex' struct containing the given pattern and a PCRE
* various rects. Needs to be pushed to X11 (see x.c) to be visible.
*
*/
-#ifndef _RENDER_H
-#define _RENDER_H
+#ifndef I3_RENDER_H
+#define I3_RENDER_H
/**
* "Renders" the given container (and its children), meaning that all rects are
* resize.c: Interactive resizing.
*
*/
-#ifndef _RESIZE_H
-#define _RESIZE_H
+#ifndef I3_RESIZE_H
+#define I3_RESIZE_H
int resize_graphical_handler(Con *first, Con *second, orientation_t orientation, const xcb_button_press_event_t *event);
* scratchpad.c: Scratchpad functions (TODO: more description)
*
*/
-#ifndef _SCRATCHPAD_H
-#define _SCRATCHPAD_H
+#ifndef I3_SCRATCHPAD_H
+#define I3_SCRATCHPAD_H
/**
* Moves the specified window to the __i3_scratch workspace, making it floating
* default (ringbuffer for storing the debug log).
*
*/
-#ifndef _I3_SHMLOG_H
-#define _I3_SHMLOG_H
+#ifndef I3_I3_SHMLOG_H
+#define I3_I3_SHMLOG_H
#include <stdint.h>
#include <pthread.h>
* to restart inplace).
*
*/
-#ifndef _SIGHANDLER_H
-#define _SIGHANDLER_H
+#ifndef I3_SIGHANDLER_H
+#define I3_SIGHANDLER_H
/**
* Setup signal handlers to safely handle SIGSEGV and SIGFPE
* the appropriate workspace.
*
*/
-#ifndef _STARTUP_H
-#define _STARTUP_H
+#ifndef I3_STARTUP_H
+#define I3_STARTUP_H
#define SN_API_NOT_YET_FROZEN 1
#include <libsn/sn-monitor.h>
* tree.c: Everything that primarily modifies the layout tree data structure.
*
*/
-#ifndef _TREE_H
-#define _TREE_H
+#ifndef I3_TREE_H
+#define I3_TREE_H
extern Con *croot;
/* TODO: i am not sure yet how much access to the focused container should
* also libi3).
*
*/
-#ifndef _UTIL_H
-#define _UTIL_H
+#ifndef I3_UTIL_H
+#define I3_UTIL_H
#include <err.h>
* window.c: Updates window attributes (X11 hints/properties).
*
*/
-#ifndef _WINDOW_H
-#define _WINDOW_H
+#ifndef I3_WINDOW_H
+#define I3_WINDOW_H
/**
* Updates the WM_CLASS (consisting of the class and instance) for the
* workspaces.
*
*/
-#ifndef _WORKSPACE_H
-#define _WORKSPACE_H
+#ifndef I3_WORKSPACE_H
+#define I3_WORKSPACE_H
#include "data.h"
#include "tree.h"
*/
void workspace_back_and_forth(void);
+/**
+ * Returns the previously focused workspace con, or NULL if unavailable.
+ *
+ */
+Con *workspace_back_and_forth_get(void);
+
#if 0
/**
* render.c). Basically a big state machine.
*
*/
-#ifndef _X_H
-#define _X_H
+#ifndef I3_X_H
+#define I3_X_H
/** Stores the X11 window ID of the currently focused window */
extern xcb_window_t focused_id;
* xcb.c: Helper functions for easier usage of XCB
*
*/
-#ifndef _XCB_H
-#define _XCB_H
+#ifndef I3_XCB_H
+#define I3_XCB_H
#include "data.h"
#include "xcursor.h"
* older versions.
*
*/
-#ifndef _XCB_COMPAT_H
-#define _XCB_COMPAT_H
+#ifndef I3_XCB_COMPAT_H
+#define I3_XCB_COMPAT_H
#define xcb_icccm_get_wm_protocols_reply_t xcb_get_wm_protocols_reply_t
#define xcb_icccm_get_wm_protocols xcb_get_wm_protocols
* xcursor.c: libXcursor support for themed cursors.
*
*/
-#ifndef _XCURSOR_CURSOR_H
-#define _XCURSOR_CURSOR_H
+#ifndef I3_XCURSOR_CURSOR_H
+#define I3_XCURSOR_CURSOR_H
#include <X11/Xlib.h>
XCURSOR_CURSOR_POINTER = 0,
XCURSOR_CURSOR_RESIZE_HORIZONTAL,
XCURSOR_CURSOR_RESIZE_VERTICAL,
+ XCURSOR_CURSOR_TOP_LEFT_CORNER,
+ XCURSOR_CURSOR_TOP_RIGHT_CORNER,
+ XCURSOR_CURSOR_BOTTOM_LEFT_CORNER,
+ XCURSOR_CURSOR_BOTTOM_RIGHT_CORNER,
XCURSOR_CURSOR_WATCH,
+ XCURSOR_CURSOR_MOVE,
XCURSOR_CURSOR_MAX
};
* driver which does not support RandR in 2011 *sigh*.
*
*/
-#ifndef _XINERAMA_H
-#define _XINERAMA_H
+#ifndef I3_XINERAMA_H
+#define I3_XINERAMA_H
#include "data.h"
command = string
-> call cmd_exec($nosn, $command)
-# border normal|none|1pixel|toggle
+# border normal|none|1pixel|toggle|1pixel
state BORDER:
- border_style = 'normal', 'none', '1pixel', 'toggle'
- -> call cmd_border($border_style)
+ border_style = 'normal', 'pixel'
+ -> BORDER_WIDTH
+ border_style = 'none', 'toggle'
+ -> call cmd_border($border_style, "0")
+ border_style = '1pixel'
+ -> call cmd_border($border_style, "1")
+
+state BORDER_WIDTH:
+ end
+ -> call cmd_border($border_style, "2")
+ border_width = word
+ -> call cmd_border($border_style, $border_width)
# layout default|stacked|stacking|tabbed|splitv|splith
# layout toggle [split|all]
-> MOVE_WORKSPACE_TO_OUTPUT
workspace = 'next', 'prev', 'next_on_output', 'prev_on_output', 'current'
-> call cmd_move_con_to_workspace($workspace)
+ 'back_and_forth'
+ -> call cmd_move_con_to_workspace_back_and_forth()
'number'
-> MOVE_WORKSPACE_NUMBER
workspace = string
%s OUTPUT_COND
%s FOR_WINDOW_COND
%s EAT_WHITESPACE
+%s BORDER_WIDTH
%x BUFFER_LINE
%x BAR
}
<ASSIGN_TARGET_COND>[ \t]*→[ \t]* { BEGIN(WANT_STRING); }
<ASSIGN_TARGET_COND>[ \t]+ { BEGIN(WANT_STRING); }
+<BORDER_WIDTH>[^\n][0-9]+ { printf("Border width set to: %s\n", yytext); yylval.number = atoi(yytext); return NUMBER;}
<EXEC>--no-startup-id { printf("no startup id\n"); yy_pop_state(); return TOK_NO_STARTUP_ID; }
<EXEC>. { printf("anything else: *%s*\n", yytext); yyless(0); yy_pop_state(); yy_pop_state(); }
<OPTRELEASE>--release { printf("--release\n"); yy_pop_state(); return TOK_RELEASE; }
workspace_layout { return TOK_WORKSPACE_LAYOUT; }
new_window { return TOKNEWWINDOW; }
new_float { return TOKNEWFLOAT; }
-normal { return TOK_NORMAL; }
+normal { yy_push_state(BORDER_WIDTH); return TOK_NORMAL; }
none { return TOK_NONE; }
1pixel { return TOK_1PIXEL; }
+pixel { yy_push_state(BORDER_WIDTH); return TOK_PIXEL; }
hide_edge_borders { return TOK_HIDE_EDGE_BORDERS; }
both { return TOK_BOTH; }
focus_follows_mouse { return TOKFOCUSFOLLOWSMOUSE; }
fake_outputs { WS_STRING; return TOK_FAKE_OUTPUTS; }
fake-outputs { WS_STRING; return TOK_FAKE_OUTPUTS; }
workspace_auto_back_and_forth { return TOK_WORKSPACE_AUTO_BAF; }
+force_display_urgency_hint { return TOK_WORKSPACE_URGENCY_TIMER; }
+ms { return TOK_TIME_MS; }
workspace_bar { return TOKWORKSPACEBAR; }
popup_during_fullscreen { return TOK_POPUP_DURING_FULLSCREEN; }
ignore { return TOK_IGNORE; }
strncasecmp(bind, "focus down", strlen("focus down")) == 0 ||
strncasecmp(bind, "border normal", strlen("border normal")) == 0 ||
strncasecmp(bind, "border 1pixel", strlen("border 1pixel")) == 0 ||
+ strncasecmp(bind, "border pixel", strlen("border pixel")) == 0 ||
strncasecmp(bind, "border borderless", strlen("border borderless")) == 0 ||
strncasecmp(bind, "--no-startup-id", strlen("--no-startup-id")) == 0 ||
strncasecmp(bind, "bar", strlen("bar")) == 0) {
%token <color> TOKCOLOR
%token TOKARROW "→"
%token TOKMODE "mode"
+%token TOK_TIME_MS "ms"
%token TOK_BAR "bar"
%token TOK_ORIENTATION "default_orientation"
%token TOK_HORIZ "horizontal"
%token TOKNEWFLOAT "new_float"
%token TOK_NORMAL "normal"
%token TOK_NONE "none"
+%token TOK_PIXEL "pixel"
%token TOK_1PIXEL "1pixel"
%token TOK_HIDE_EDGE_BORDERS "hide_edge_borders"
%token TOK_BOTH "both"
%token TOK_FORCE_XINERAMA "force_xinerama"
%token TOK_FAKE_OUTPUTS "fake_outputs"
%token TOK_WORKSPACE_AUTO_BAF "workspace_auto_back_and_forth"
+%token TOK_WORKSPACE_URGENCY_TIMER "force_display_urgency_hint"
%token TOKWORKSPACEBAR "workspace_bar"
%token TOK_DEFAULT "default"
%token TOK_STACKING "stacking"
%type <number> bar_mode_mode
%type <number> bar_modifier_modifier
%type <number> optional_no_startup_id
+%type <number> optional_border_width
%type <number> optional_release
%type <string> command
%type <string> word_or_number
+%type <string> duration
%type <string> qstring_or_number
%type <string> optional_workspace_name
%type <string> workspace_name
| force_focus_wrapping
| force_xinerama
| fake_outputs
+ | force_display_urgency_hint
| workspace_back_and_forth
| workspace_bar
| workspace
}
;
+duration:
+ NUMBER { sasprintf(&$$, "%d", $1); }
+ | NUMBER TOK_TIME_MS { sasprintf(&$$, "%d", $1); }
+ ;
+
mode:
TOKMODE QUOTEDSTRING '{' modelines '}'
{
;
border_style:
- TOK_NORMAL { $$ = BS_NORMAL; }
- | TOK_NONE { $$ = BS_NONE; }
- | TOK_1PIXEL { $$ = BS_1PIXEL; }
+ TOK_NORMAL optional_border_width
+ {
+ config.default_border_width = $2;
+ $$ = BS_NORMAL;
+ }
+ | TOK_1PIXEL
+ {
+ config.default_border_width = 1;
+ $$ = BS_PIXEL;
+ }
+ | TOK_NONE
+ {
+ config.default_border_width = 0;
+ $$ = BS_NONE;
+ }
+ | TOK_PIXEL optional_border_width
+ {
+ config.default_border_width = $2;
+ $$ = BS_PIXEL;
+ }
;
bool:
}
;
+force_display_urgency_hint:
+ TOK_WORKSPACE_URGENCY_TIMER duration
+ {
+ DLOG("workspace urgency_timer = %f\n", atoi($2) / 1000.0);
+ config.workspace_urgency_timer = atoi($2) / 1000.0;
+ }
+ ;
+
workspace_bar:
TOKWORKSPACEBAR bool
{
}
;
+optional_border_width:
+ /* empty */ { $$ = 2; } // 2 pixels is the default value for any type of border
+ | NUMBER { $$ = $1; }
+ ;
+
optional_no_startup_id:
/* empty */ { $$ = false; }
| TOK_NO_STARTUP_ID { $$ = true; }
return route_click(con, event, mod_pressed, CLICK_INSIDE);
if (!(con = con_by_frame_id(event->event))) {
+ /* If the root window is clicked, find the relevant output from the
+ * click coordinates and focus the output's active workspace. */
+ if (event->event == root) {
+ Con *output, *ws;
+ TAILQ_FOREACH(output, &(croot->nodes_head), nodes) {
+ if (con_is_internal(output) ||
+ !rect_contains(output->rect, event->event_x, event->event_y))
+ continue;
+
+ ws = TAILQ_FIRST(&(output_get_content(output)->focus_head));
+ if (ws != con_get_workspace(focused)) {
+ workspace_show(ws);
+ tree_render();
+ }
+ return 1;
+ }
+ return 0;
+ }
+
ELOG("Clicked into unknown window?!\n");
xcb_allow_events(conn, XCB_ALLOW_REPLAY_POINTER, event->time);
xcb_flush(conn);
Output *output;
if (strcasecmp(output_str, "left") == 0) {
- output = get_output_next(D_LEFT, current_output);
+ output = get_output_next(D_LEFT, current_output, CLOSEST_OUTPUT);
if (!output)
output = get_output_most(D_RIGHT, current_output);
} else if (strcasecmp(output_str, "right") == 0) {
- output = get_output_next(D_RIGHT, current_output);
+ output = get_output_next(D_RIGHT, current_output, CLOSEST_OUTPUT);
if (!output)
output = get_output_most(D_LEFT, current_output);
} else if (strcasecmp(output_str, "up") == 0) {
- output = get_output_next(D_UP, current_output);
+ output = get_output_next(D_UP, current_output, CLOSEST_OUTPUT);
if (!output)
output = get_output_most(D_DOWN, current_output);
} else if (strcasecmp(output_str, "down") == 0) {
- output = get_output_next(D_DOWN, current_output);
+ output = get_output_next(D_DOWN, current_output, CLOSEST_OUTPUT);
if (!output)
output = get_output_most(D_UP, current_output);
} else output = get_output_by_name(output_str);
return true;
}
+/*
+ * Return the passed workspace unless it is the current one and auto back and
+ * forth is enabled, in which case the back_and_forth workspace is returned.
+ */
+static Con *maybe_auto_back_and_forth_workspace(Con *workspace) {
+ Con *current, *baf;
+
+ if (!config.workspace_auto_back_and_forth)
+ return workspace;
+
+ current = con_get_workspace(focused);
+
+ if (current == workspace) {
+ baf = workspace_back_and_forth_get();
+ if (baf != NULL) {
+ DLOG("Substituting workspace with back_and_forth, as it is focused.\n");
+ return baf;
+ }
+ }
+
+ return workspace;
+}
+
// This code is commented out because we might recycle it for popping up error
// messages on parser errors.
#if 0
ysuccess(true);
}
+/**
+ * Implementation of 'move [window|container] [to] workspace back_and_forth'.
+ *
+ */
+void cmd_move_con_to_workspace_back_and_forth(I3_CMD) {
+ owindow *current;
+ Con *ws;
+
+ ws = workspace_back_and_forth_get();
+
+ if (ws == NULL) {
+ y(map_open);
+ ystr("success");
+ y(bool, false);
+ ystr("error");
+ ystr("No workspace was previously active.");
+ y(map_close);
+ return;
+ }
+
+ HANDLE_EMPTY_MATCH;
+
+ TAILQ_FOREACH(current, &owindows, owindows) {
+ DLOG("matching: %p / %s\n", current->con, current->con->name);
+ con_move_to_workspace(current->con, ws, true, false);
+ }
+
+ cmd_output->needs_tree_render = true;
+ // XXX: default reply for now, make this a better reply
+ ysuccess(true);
+}
+
/*
* Implementation of 'move [window|container] [to] workspace <name>'.
*
/* get the workspace */
Con *ws = workspace_get(name, NULL);
+ ws = maybe_auto_back_and_forth_workspace(ws);
+
HANDLE_EMPTY_MATCH;
TAILQ_FOREACH(current, &owindows, owindows) {
}
/*
- * Implementation of 'move [window|container] [to] workspace number <number>'.
+ * Implementation of 'move [window|container] [to] workspace number <name>'.
*
*/
void cmd_move_con_to_workspace_number(I3_CMD, char *which) {
if (parsed_num == LONG_MIN ||
parsed_num == LONG_MAX ||
parsed_num < 0 ||
- *endptr != '\0') {
- LOG("Could not parse \"%s\" as a number.\n", which);
+ endptr == which) {
+ LOG("Could not parse initial part of \"%s\" as a number.\n", which);
y(map_open);
ystr("success");
y(bool, false);
workspace = workspace_get(which, NULL);
}
+ workspace = maybe_auto_back_and_forth_workspace(workspace);
+
HANDLE_EMPTY_MATCH;
TAILQ_FOREACH(current, &owindows, owindows) {
}
/*
- * Implementation of 'border normal|none|1pixel|toggle'.
+ * Implementation of 'border normal|none|1pixel|toggle|pixel'.
*
*/
-void cmd_border(I3_CMD, char *border_style_str) {
- DLOG("border style should be changed to %s\n", border_style_str);
+void cmd_border(I3_CMD, char *border_style_str, char *border_width ) {
+ DLOG("border style should be changed to %s with border width %s\n", border_style_str, border_width);
owindow *current;
HANDLE_EMPTY_MATCH;
TAILQ_FOREACH(current, &owindows, owindows) {
DLOG("matching: %p / %s\n", current->con, current->con->name);
int border_style = current->con->border_style;
+ char *end;
+ int tmp_border_width = -1;
+ tmp_border_width = strtol(border_width, &end, 10);
+ if (end == border_width) {
+ /* no valid digits found */
+ tmp_border_width = -1;
+ }
if (strcmp(border_style_str, "toggle") == 0) {
border_style++;
border_style %= 3;
+ if (border_style == BS_NORMAL)
+ tmp_border_width = 2;
+ else if (border_style == BS_NONE)
+ tmp_border_width = 0;
+ else if (border_style == BS_PIXEL)
+ tmp_border_width = 1;
} else {
if (strcmp(border_style_str, "normal") == 0)
border_style = BS_NORMAL;
- else if (strcmp(border_style_str, "none") == 0)
+ else if (strcmp(border_style_str, "pixel") == 0)
+ border_style = BS_PIXEL;
+ else if (strcmp(border_style_str, "1pixel") == 0){
+ border_style = BS_PIXEL;
+ tmp_border_width = 1;
+ } else if (strcmp(border_style_str, "none") == 0)
border_style = BS_NONE;
- else if (strcmp(border_style_str, "1pixel") == 0)
- border_style = BS_1PIXEL;
else {
ELOG("BUG: called with border_style=%s\n", border_style_str);
ysuccess(false);
return;
}
}
- con_set_border_style(current->con, border_style);
+ con_set_border_style(current->con, border_style, tmp_border_width);
}
cmd_output->needs_tree_render = true;
}
/*
- * Implementation of 'workspace number <number>'
+ * Implementation of 'workspace number <name>'
*
*/
void cmd_workspace_number(I3_CMD, char *which) {
if (parsed_num == LONG_MIN ||
parsed_num == LONG_MAX ||
parsed_num < 0 ||
- *endptr != '\0') {
- LOG("Could not parse \"%s\" as a number.\n", which);
+ endptr == which) {
+ LOG("Could not parse initial part of \"%s\" as a number.\n", which);
y(map_open);
ystr("success");
y(bool, false);
if (!workspace) {
LOG("There is no workspace with number %ld, creating a new one.\n", parsed_num);
ysuccess(true);
- /* terminate the which string after the endposition of the number */
- *endptr = '\0';
workspace_show_by_name(which);
cmd_output->needs_tree_render = true;
return;
// TODO: clean this up with commands.spec as soon as we switched away from the lex/yacc command parser
if (strcasecmp(name, "up") == 0)
- output = get_output_next(D_UP, current_output);
+ output = get_output_next(D_UP, current_output, CLOSEST_OUTPUT);
else if (strcasecmp(name, "down") == 0)
- output = get_output_next(D_DOWN, current_output);
+ output = get_output_next(D_DOWN, current_output, CLOSEST_OUTPUT);
else if (strcasecmp(name, "left") == 0)
- output = get_output_next(D_LEFT, current_output);
+ output = get_output_next(D_LEFT, current_output, CLOSEST_OUTPUT);
else if (strcasecmp(name, "right") == 0)
- output = get_output_next(D_RIGHT, current_output);
+ output = get_output_next(D_RIGHT, current_output, CLOSEST_OUTPUT);
else
output = get_output_by_name(name);
Con *content = output_get_content(output->con);
LOG("got output %p with content %p\n", output, content);
+ Con *previously_visible_ws = TAILQ_FIRST(&(content->nodes_head));
+ LOG("Previously visible workspace = %p / %s\n", previously_visible_ws, previously_visible_ws->name);
+
Con *ws = con_get_workspace(current->con);
LOG("should move workspace %p / %s\n", ws, ws->name);
+ bool workspace_was_visible = workspace_is_visible(ws);
if (con_num_children(ws->parent) == 1) {
LOG("Creating a new workspace to replace \"%s\" (last on its output).\n", ws->name);
}
/* detach from the old output and attach to the new output */
- bool workspace_was_visible = workspace_is_visible(ws);
Con *old_content = ws->parent;
con_detach(ws);
if (workspace_was_visible) {
/* Focus the moved workspace on the destination output. */
workspace_show(ws);
}
+
+ /* Call the on_remove_child callback of the workspace which previously
+ * was visible on the destination output. Since it is no longer
+ * visible, it might need to get cleaned up. */
+ CALL(previously_visible_ws, on_remove_child);
}
cmd_output->needs_tree_render = true;
static void con_on_remove_child(Con *con);
+/*
+ * force parent split containers to be redrawn
+ *
+ */
+static void con_force_split_parents_redraw(Con *con) {
+ Con *parent = con;
+
+ while (parent && parent->type != CT_WORKSPACE && parent->type != CT_DOCKAREA) {
+ if (parent->split)
+ FREE(parent->deco_render_params);
+ parent = parent->parent;
+ }
+}
+
/*
* Create a new container (and attach it to the given parent, if not NULL).
* This function initializes the data structures and creates the appropriate
new->type = CT_CON;
new->window = window;
new->border_style = config.default_border;
+ new->current_border_width = -1;
static int cnt = 0;
DLOG("opening window %d\n", cnt);
* This way, we have the option to insert Cons without having
* to focus them. */
TAILQ_INSERT_TAIL(focus_head, con, focused);
+ con_force_split_parents_redraw(con);
}
/*
*
*/
void con_detach(Con *con) {
+ con_force_split_parents_redraw(con);
if (con->type == CT_FLOATING_CON) {
TAILQ_REMOVE(&(con->parent->floating_head), con, floating_windows);
TAILQ_REMOVE(&(con->parent->focus_head), con, focused);
con_focus(con->parent);
focused = con;
- if (con->urgent) {
+ /* We can't blindly reset non-leaf containers since they might have
+ * other urgent children. Therefore we only reset leafs and propagate
+ * the changes upwards via con_update_parents_urgency() which does proper
+ * checks before resetting the urgency.
+ */
+ if (con->urgent && con_is_leaf(con)) {
con->urgent = false;
+ con_update_parents_urgency(con);
workspace_update_urgent_flag(con_get_workspace(con));
}
}
return NULL;
}
+/**
+ * Returns true if the container is internal, such as __i3_scratch
+ *
+ */
+bool con_is_internal(Con *con) {
+ return (con->name[0] == '_' && con->name[1] == '_');
+}
+
/*
* Returns true if the node is floating.
*
* calling tree_render(), so for the "real" focus this is a no-op).
* We don’t focus the con for i3 pseudo workspaces like __i3_scratch and
* we don’t focus when there is a fullscreen con on that workspace. */
- if ((workspace->name[0] != '_' || workspace->name[1] != '_') &&
+ if (!con_is_internal(workspace) &&
con_get_fullscreen_con(workspace, CF_OUTPUT) == NULL)
con_focus(con_descend_focused(con));
* don’t want to focus invisible workspaces */
if (source_output != dest_output &&
workspace_is_visible(workspace) &&
- workspace->name[0] != '_' &&
- workspace->name[1] != '_') {
+ !con_is_internal(workspace)) {
DLOG("Moved to a different output, focusing target\n");
} else {
/* Descend focus stack in case focus_next is a workspace which can
*/
Rect con_border_style_rect(Con *con) {
adjacent_t borders_to_hide = ADJ_NONE;
+ int border_width = con->current_border_width;
+ DLOG("The border width for con is set to: %d\n", con->current_border_width);
Rect result;
+ if (con->current_border_width < 0)
+ border_width = config.default_border_width;
+ DLOG("Effective border width is set to: %d\n", border_width);
/* Shortcut to avoid calling con_adjacent_borders() on dock containers. */
int border_style = con_border_style(con);
if (border_style == BS_NONE)
return (Rect){ 0, 0, 0, 0 };
borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders;
- switch (border_style) {
- case BS_NORMAL:
- result = (Rect){2, 0, -(2 * 2), -2};
- if (borders_to_hide & ADJ_LEFT_SCREEN_EDGE) {
- result.x -= 2;
- result.width += 2;
- }
- if (borders_to_hide & ADJ_RIGHT_SCREEN_EDGE) {
- result.width += 2;
- }
- /* With normal borders we never hide the upper border */
- if (borders_to_hide & ADJ_LOWER_SCREEN_EDGE) {
- result.height += 2;
- }
- return result;
-
- case BS_1PIXEL:
- result = (Rect){1, 1, -2, -2};
- if (borders_to_hide & ADJ_LEFT_SCREEN_EDGE) {
- result.x -= 1;
- result.width += 1;
- }
- if (borders_to_hide & ADJ_RIGHT_SCREEN_EDGE) {
- result.width += 1;
- }
- if (borders_to_hide & ADJ_UPPER_SCREEN_EDGE) {
- result.y -= 1;
- result.height += 1;
- }
- if (borders_to_hide & ADJ_LOWER_SCREEN_EDGE) {
- result.height += 1;
- }
- return result;
-
- case BS_NONE:
- return (Rect){0, 0, 0, 0};
-
- default:
- assert(false);
+ if (border_style == BS_NORMAL) {
+ result = (Rect){border_width, 0 , -(2 * border_width), -(border_width)};
+ } else {
+ result = (Rect){border_width, border_width, -(2 * border_width), -(2 * border_width)};
+ }
+ if (borders_to_hide & ADJ_LEFT_SCREEN_EDGE) {
+ result.x -= border_width;
+ result.width += border_width;
+ }
+ if (borders_to_hide & ADJ_RIGHT_SCREEN_EDGE) {
+ result.width += border_width;
+ }
+ if (borders_to_hide & ADJ_UPPER_SCREEN_EDGE && (border_style != BS_NORMAL)) {
+ result.y -= border_width;
+ result.height += border_width;
}
+ if (borders_to_hide & ADJ_LOWER_SCREEN_EDGE) {
+ result.height += border_width;
+ }
+ return result;
+
}
/*
* floating window.
*
*/
-void con_set_border_style(Con *con, int border_style) {
+void con_set_border_style(Con *con, int border_style, int border_width) {
/* Handle the simple case: non-floating containerns */
if (!con_is_floating(con)) {
con->border_style = border_style;
+ con->current_border_width = border_width;
return;
}
/* Change the border style, get new border/decoration values. */
con->border_style = border_style;
+ con->current_border_width = border_width;
bsr = con_border_style_rect(con);
int deco_height =
(con->border_style == BS_NORMAL ? config.font.height + 5 : 0);
tree_flatten(croot);
}
+ con_force_split_parents_redraw(con);
return;
}
} else {
con->layout = layout;
}
+ con_force_split_parents_redraw(con);
}
/*
return;
}
+ con_force_split_parents_redraw(con);
+
/* TODO: check if this container would swallow any other client and
* don’t close it automatically. */
int children = con_num_children(con);
/* Focusing con would hide it behind a fullscreen window, disallow it. */
return false;
}
+
+/*
+ *
+ * Checks if the given container has an urgent child.
+ *
+ */
+bool con_has_urgent_child(Con *con) {
+ Con *child;
+
+ if (con_is_leaf(con))
+ return con->urgent;
+
+ /* We are not interested in floating windows since they can only be
+ * attached to a workspace → nodes_head instead of focus_head */
+ TAILQ_FOREACH(child, &(con->nodes_head), nodes) {
+ if (con_has_urgent_child(child))
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * Make all parent containers urgent if con is urgent or clear the urgent flag
+ * of all parent containers if there are no more urgent children left.
+ *
+ */
+void con_update_parents_urgency(Con *con) {
+ Con *parent = con->parent;
+
+ bool new_urgency_value = con->urgent;
+ while (parent && parent->type != CT_WORKSPACE && parent->type != CT_DOCKAREA) {
+ if (new_urgency_value) {
+ parent->urgent = true;
+ } else {
+ /* We can only reset the urgency when the parent
+ * has no other urgent children */
+ if (!con_has_urgent_child(parent))
+ parent->urgent = false;
+ }
+ parent = parent->parent;
+ }
+}
+
+/*
+ * Create a string representing the subtree under con.
+ *
+ */
+char *con_get_tree_representation(Con *con) {
+ /* this code works as follows:
+ * 1) create a string with the layout type (D/V/H/T/S) and an opening bracket
+ * 2) append the tree representation of the children to the string
+ * 3) add closing bracket
+ *
+ * The recursion ends when we hit a leaf, in which case we return the
+ * class_instance of the contained window.
+ */
+
+ /* end of recursion */
+ if (con_is_leaf(con)) {
+ if (!con->window)
+ return sstrdup("nowin");
+
+ if (!con->window->class_instance)
+ return sstrdup("noinstance");
+
+ return sstrdup(con->window->class_instance);
+ }
+
+ char *buf;
+ /* 1) add the Layout type to buf */
+ if (con->layout == L_DEFAULT)
+ buf = sstrdup("D[");
+ else if (con->layout == L_SPLITV)
+ buf = sstrdup("V[");
+ else if (con->layout == L_SPLITH)
+ buf = sstrdup("H[");
+ else if (con->layout == L_TABBED)
+ buf = sstrdup("T[");
+ else if (con->layout == L_STACKED)
+ buf = sstrdup("S[");
+
+ /* 2) append representation of children */
+ Con *child;
+ TAILQ_FOREACH(child, &(con->nodes_head), nodes) {
+ char *child_txt = con_get_tree_representation(child);
+
+ char *tmp_buf;
+ sasprintf(&tmp_buf, "%s%s%s", buf,
+ (TAILQ_FIRST(&(con->nodes_head)) == child ? "" : " "), child_txt);
+ free(buf);
+ buf = tmp_buf;
+ }
+
+ /* 3) close the brackets */
+ char *complete_buf;
+ sasprintf(&complete_buf, "%s]", buf);
+ free(buf);
+
+ return complete_buf;
+}
bindings = mode->bindings;
translate_keysyms();
grab_all_keys(conn, false);
+
+ char *event_msg;
+ sasprintf(&event_msg, "{\"change\":\"%s\"}", mode->name);
+
+ ipc_send_event("mode", I3_IPC_EVENT_MODE, event_msg);
+ FREE(event_msg);
+
return;
}
config.default_border = BS_NORMAL;
config.default_floating_border = BS_NORMAL;
+ config.default_border_width = 2;
/* Set default_orientation to NO_ORIENTATION for auto orientation. */
config.default_orientation = NO_ORIENTATION;
+ /* Set default urgency reset delay to 500ms */
+ if (config.workspace_urgency_timer == 0)
+ config.workspace_urgency_timer = 0.5;
+
parse_configuration(override_configpath);
if (reload) {
tree_render();
/* Drag the window */
- drag_pointer(con, event, XCB_NONE, BORDER_TOP /* irrelevant */, drag_window_callback, event);
+ drag_pointer(con, event, XCB_NONE, BORDER_TOP /* irrelevant */, XCURSOR_CURSOR_MOVE, drag_window_callback, event);
tree_render();
}
corner |= BORDER_LEFT;
else corner |= BORDER_RIGHT;
- if (event->event_y <= (con->rect.height / 2))
+ int cursor = 0;
+ if (event->event_y <= (con->rect.height / 2)) {
corner |= BORDER_TOP;
- else corner |= BORDER_BOTTOM;
+ cursor = (corner & BORDER_LEFT) ?
+ XCURSOR_CURSOR_TOP_LEFT_CORNER : XCURSOR_CURSOR_TOP_RIGHT_CORNER;
+ }
+ else {
+ corner |= BORDER_BOTTOM;
+ cursor = (corner & BORDER_LEFT) ?
+ XCURSOR_CURSOR_BOTTOM_LEFT_CORNER : XCURSOR_CURSOR_BOTTOM_RIGHT_CORNER;
+ }
struct resize_window_callback_params params = { corner, proportional, event };
- drag_pointer(con, event, XCB_NONE, BORDER_TOP /* irrelevant */, resize_window_callback, ¶ms);
+ drag_pointer(con, event, XCB_NONE, BORDER_TOP /* irrelevant */, cursor, resize_window_callback, ¶ms);
}
/*
*
*/
void drag_pointer(Con *con, const xcb_button_press_event_t *event, xcb_window_t
- confine_to, border_t border, callback_t callback, const void *extra)
+ confine_to, border_t border, int cursor, callback_t callback, const void *extra)
{
uint32_t new_x, new_y;
Rect old_rect = { 0, 0, 0, 0 };
if (con != NULL)
memcpy(&old_rect, &(con->rect), sizeof(Rect));
+ Cursor xcursor = (cursor && xcursor_supported) ?
+ xcursor_get_cursor(cursor) : XCB_NONE;
+
/* Grab the pointer */
xcb_grab_pointer_cookie_t cookie;
xcb_grab_pointer_reply_t *reply;
XCB_GRAB_MODE_ASYNC, /* pointer events should continue as normal */
XCB_GRAB_MODE_ASYNC, /* keyboard mode */
confine_to, /* confine_to = in which window should the cursor stay */
- XCB_NONE, /* don’t display a special cursor */
+ xcursor, /* possibly display a special cursor */
XCB_CURRENT_TIME);
if ((reply = xcb_grab_pointer_reply(conn, cookie, NULL)) == NULL) {
}
/* Update the flag on the client directly */
- con->urgent = (xcb_icccm_wm_hints_get_urgency(&hints) != 0);
+ bool hint_urgent = (xcb_icccm_wm_hints_get_urgency(&hints) != 0);
+
+ if (con->urgency_timer == NULL) {
+ con->urgent = hint_urgent;
+ } else
+ DLOG("Discarding urgency WM_HINT because timer is running\n");
+
//CLIENT_LOG(con);
if (con->window) {
if (con->urgent) {
con->window->urgent.tv_usec = 0;
}
}
+
+ con_update_parents_urgency(con);
+
LOG("Urgency flag changed to %d\n", con->urgent);
Con *ws;
ELOG("mkdir(%s) failed: %s\n", path, strerror(errno));
return false;
}
- char *copy = strdup(path);
+ char *copy = sstrdup(path);
/* strip trailing slashes, if any */
while (copy[strlen(copy)-1] == '/')
copy[strlen(copy)-1] = '\0';
case BS_NONE:
ystr("none");
break;
- case BS_1PIXEL:
- ystr("1pixel");
+ case BS_PIXEL:
+ ystr("pixel");
break;
}
+ ystr("current_border_width");
+ y(integer, con->current_border_width);
+
dump_rect(gen, "rect", con->rect);
dump_rect(gen, "window_rect", con->window_rect);
dump_rect(gen, "geometry", con->geometry);
Con *output;
TAILQ_FOREACH(output, &(croot->nodes_head), nodes) {
- if (output->name[0] == '_' && output->name[1] == '_')
+ if (con_is_internal(output))
continue;
Con *ws;
TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) {
sasprintf(&buf, "%.*s", (int)len, val);
if (strcasecmp(buf, "none") == 0)
json_node->border_style = BS_NONE;
- else if (strcasecmp(buf, "1pixel") == 0)
- json_node->border_style = BS_1PIXEL;
+ else if (strcasecmp(buf, "1pixel") == 0) {
+ json_node->border_style = BS_PIXEL;
+ json_node->current_border_width = 1;
+ } else if (strcasecmp(buf, "pixel") == 0)
+ json_node->border_style = BS_PIXEL;
else if (strcasecmp(buf, "normal") == 0)
json_node->border_style = BS_NORMAL;
else LOG("Unhandled \"border\": %s\n", buf);
if (strcasecmp(last_key, "num") == 0)
json_node->num = val;
+ if (strcasecmp(last_key, "current_border_width") == 0)
+ json_node->current_border_width = val;
+
if (!parsing_swallows && strcasecmp(last_key, "id") == 0)
json_node->old_id = val;
/* TODO: percent of other windows are not correctly fixed at the moment */
FILE *f;
if ((f = fopen(filename, "r")) == NULL) {
- LOG("Cannot open file\n");
+ LOG("Cannot open file \"%s\"\n", filename);
+ return;
+ }
+ struct stat stbuf;
+ if (fstat(fileno(f), &stbuf) != 0) {
+ LOG("Cannot fstat() the file\n");
+ fclose(f);
+ return;
+ }
+ char *buf = smalloc(stbuf.st_size);
+ int n = fread(buf, 1, stbuf.st_size, f);
+ if (n != stbuf.st_size) {
+ LOG("File \"%s\" could not be read entirely, not loading.\n", filename);
+ fclose(f);
return;
}
- char *buf = malloc(65535); /* TODO */
- int n = fread(buf, 1, 65535, f);
LOG("read %d bytes\n", n);
yajl_gen g;
yajl_handle hand;
*
*/
static void handle_signal(int sig, siginfo_t *info, void *data) {
- fprintf(stderr, "Received signal %d, terminating\n", sig);
if (*shmlogname != '\0') {
- fprintf(stderr, "Closing SHM log \"%s\"\n", shmlogname);
shm_unlink(shmlogname);
}
- fflush(stderr);
raise(sig);
}
uint32_t mask = XCB_CW_EVENT_MASK;
uint32_t values[] = { XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT |
+ XCB_EVENT_MASK_BUTTON_PRESS |
XCB_EVENT_MASK_STRUCTURE_NOTIFY | /* when the user adds a screen (e.g. video
projector), the root window gets a
ConfigureNotify */
*
*/
Output *get_output_most(direction_t direction, Output *current) {
- Output *output, *candidate = NULL;
- int position = 0;
- TAILQ_FOREACH(output, &outputs, outputs) {
- if (!output->active)
- continue;
-
- /* Repeated calls of WIN determine the winner of the comparison */
- #define WIN(variable, condition) \
- if (variable condition) { \
- candidate = output; \
- position = variable; \
- } \
- break;
-
- if (((direction == D_UP) || (direction == D_DOWN)) &&
- (current->rect.x != output->rect.x))
- continue;
-
- if (((direction == D_LEFT) || (direction == D_RIGHT)) &&
- (current->rect.y != output->rect.y))
- continue;
-
- switch (direction) {
- case D_UP:
- WIN(output->rect.y, <= position);
- case D_DOWN:
- WIN(output->rect.y, >= position);
- case D_LEFT:
- WIN(output->rect.x, <= position);
- case D_RIGHT:
- WIN(output->rect.x, >= position);
- }
- }
-
- assert(candidate != NULL);
-
- return candidate;
+ Output *best = get_output_next(direction, current, FARTHEST_OUTPUT);
+ if (!best)
+ best = current;
+ DLOG("current = %s, best = %s\n", current->name, best->name);
+ return best;
}
/*
* Gets the output which is the next one in the given direction.
*
*/
-Output *get_output_next(direction_t direction, Output *current) {
- Output *output, *candidate = NULL;
-
+Output *get_output_next(direction_t direction, Output *current, output_close_far_t close_far) {
+ Rect *cur = &(current->rect),
+ *other;
+ Output *output,
+ *best = NULL;
TAILQ_FOREACH(output, &outputs, outputs) {
if (!output->active)
continue;
- if (((direction == D_UP) || (direction == D_DOWN)) &&
- (current->rect.x != output->rect.x))
+ other = &(output->rect);
+
+ if ((direction == D_RIGHT && other->x > cur->x) ||
+ (direction == D_LEFT && other->x < cur->x)) {
+ /* Skip the output when it doesn’t overlap the other one’s y
+ * coordinate at all. */
+ if ((other->y + other->height) <= cur->y ||
+ (cur->y + cur->height) <= other->y)
+ continue;
+ } else if ((direction == D_DOWN && other->y > cur->y) ||
+ (direction == D_UP && other->y < cur->y)) {
+ /* Skip the output when it doesn’t overlap the other one’s x
+ * coordinate at all. */
+ if ((other->x + other->width) <= cur->x ||
+ (cur->x + cur->width) <= other->x)
+ continue;
+ } else
continue;
- if (((direction == D_LEFT) || (direction == D_RIGHT)) &&
- (current->rect.y != output->rect.y))
+ /* No candidate yet? Start with this one. */
+ if (!best) {
+ best = output;
continue;
+ }
- switch (direction) {
- case D_UP:
- if (output->rect.y < current->rect.y &&
- (!candidate || output->rect.y < candidate->rect.y))
- candidate = output;
- break;
- case D_DOWN:
- if (output->rect.y > current->rect.y &&
- (!candidate || output->rect.y > candidate->rect.y))
- candidate = output;
- break;
- case D_LEFT:
- if (output->rect.x < current->rect.x &&
- (!candidate || output->rect.x > candidate->rect.x))
- candidate = output;
- break;
- case D_RIGHT:
- if (output->rect.x > current->rect.x &&
- (!candidate || output->rect.x < candidate->rect.x))
- candidate = output;
- break;
+ if (close_far == CLOSEST_OUTPUT) {
+ /* Is this output better (closer to the current output) than our
+ * current best bet? */
+ if ((direction == D_RIGHT && other->x < best->rect.x) ||
+ (direction == D_LEFT && other->x > best->rect.x) ||
+ (direction == D_DOWN && other->y < best->rect.y) ||
+ (direction == D_UP && other->y > best->rect.y)) {
+ best = output;
+ continue;
+ }
+ } else {
+ /* Is this output better (farther to the current output) than our
+ * current best bet? */
+ if ((direction == D_RIGHT && other->x > best->rect.x) ||
+ (direction == D_LEFT && other->x < best->rect.x) ||
+ (direction == D_DOWN && other->y > best->rect.y) ||
+ (direction == D_UP && other->y < best->rect.y)) {
+ best = output;
+ continue;
+ }
}
}
- return candidate;
+ DLOG("current = %s, best = %s\n", current->name, (best ? best->name : "NULL"));
+ return best;
}
/*
if (con->layout == L_OUTPUT) {
/* Skip i3-internal outputs */
- if (con->name[0] == '_' && con->name[1] == '_')
+ if (con_is_internal(con))
return;
render_l_output(con);
} else if (con->type == CT_ROOT) {
* windows/containers so that they overlap on another output. */
DLOG("Rendering floating windows:\n");
TAILQ_FOREACH(output, &(con->nodes_head), nodes) {
- if (output->name[0] == '_' && output->name[1] == '_')
+ if (con_is_internal(output))
continue;
/* Get the active workspace of that output */
Con *content = output_get_content(output);
Con *workspace = TAILQ_FIRST(&(content->focus_head));
- /* Check for (floating!) fullscreen nodes */
+ /* Check for fullscreen nodes */
/* XXX: This code duplication is unfortunate. Keep in mind to fix
* this when we clean up the whole render.c */
Con *fullscreen = NULL;
fullscreen = con_get_fullscreen_con(workspace, CF_OUTPUT);
- if (fullscreen) {
- /* Either the fullscreen window is inside the floating
- * container, then we need to render and raise it now… */
- if (con_inside_floating(fullscreen)) {
- fullscreen->rect = output->rect;
- x_raise_con(fullscreen);
- render_con(fullscreen, true);
+ if (fullscreen)
continue;
- } else {
- /* …or it’s a tiling window, in which case the floating
- * windows should not overlap it, so we skip rendering this
- * output. */
- continue;
- }
- }
Con *child;
TAILQ_FOREACH(child, &(workspace->floating_head), floating_windows) {
child->deco_rect.width = child->rect.width;
child->deco_rect.height = deco_height;
- if (children > 1 || (child->border_style != BS_1PIXEL && child->border_style != BS_NONE)) {
+ if (children > 1 || (child->border_style != BS_PIXEL && child->border_style != BS_NONE)) {
child->rect.y += (deco_height * children);
child->rect.height -= (deco_height * children);
}
child->deco_rect.x = x - con->rect.x + i * child->deco_rect.width;
child->deco_rect.y = y - con->rect.y;
- if (children > 1 || (child->border_style != BS_1PIXEL && child->border_style != BS_NONE)) {
+ if (children > 1 || (child->border_style != BS_PIXEL && child->border_style != BS_NONE)) {
child->rect.y += deco_height;
child->rect.height -= deco_height;
child->deco_rect.height = deco_height;
} else {
- child->deco_rect.height = (child->border_style == BS_1PIXEL ? 1 : 0);
+ child->deco_rect.height = (child->border_style == BS_PIXEL ? 1 : 0);
}
}
const struct callback_params params = { orientation, output, helpwin, &new_position };
- drag_pointer(NULL, event, grabwin, BORDER_TOP, resize_callback, ¶ms);
+ drag_pointer(NULL, event, grabwin, BORDER_TOP, 0, resize_callback, ¶ms);
xcb_destroy_window(conn, helpwin);
xcb_destroy_window(conn, grabwin);
return;
}
- /* 1: Ensure the window is floating. From now on, we deal with the
- * CT_FLOATING_CON. We use automatic == false because the user made the
- * choice that this window should be a scratchpad (and floating). */
- floating_enable(con, false);
- con = con->parent;
+ /* 1: Ensure the window or any parent is floating. From now on, we deal
+ * with the CT_FLOATING_CON. We use automatic == false because the user
+ * made the choice that this window should be a scratchpad (and floating).
+ */
+ Con *maybe_floating_con = con_inside_floating(con);
+ if (maybe_floating_con == NULL) {
+ floating_enable(con, false);
+ con = con->parent;
+ } else {
+ con = maybe_floating_con;
+ }
/* 2: Send the window to the __i3_scratch workspace, mainting its
* coordinates and not warping the pointer. */
x_con_kill(con);
con_detach(con);
+
+ /* disable urgency timer, if needed */
+ if (con->urgency_timer != NULL) {
+ DLOG("Removing urgency timer of con %p\n", con);
+ workspace_update_urgent_flag(con_get_workspace(con));
+ ev_timer_stop(main_loop, con->urgency_timer);
+ FREE(con->urgency_timer);
+ }
+
if (con->type != CT_FLOATING_CON) {
/* If the container is *not* floating, we might need to re-distribute
* percentage values for the resized containers. */
else
return false;
- next_output = get_output_next(direction, current_output);
+ next_output = get_output_next(direction, current_output, CLOSEST_OUTPUT);
if (!next_output)
return false;
DLOG("Next output is %s\n", next_output->name);
workspace_reassign_sticky(current);
}
+/*
+ * Callback to reset the urgent flag of the given con to false. May be started by
+ * _workspace_show to avoid urgency hints being lost by switching to a workspace
+ * focusing the con.
+ *
+ */
+static void workspace_defer_update_urgent_hint_cb(EV_P_ ev_timer *w, int revents) {
+ Con *con = w->data;
+
+ DLOG("Resetting urgency flag of con %p by timer\n", con);
+ con->urgent = false;
+ con_update_parents_urgency(con);
+ workspace_update_urgent_flag(con_get_workspace(con));
+ tree_render();
+
+ ev_timer_stop(main_loop, con->urgency_timer);
+ FREE(con->urgency_timer);
+}
static void _workspace_show(Con *workspace) {
Con *current, *old = NULL;
/* safe-guard against showing i3-internal workspaces like __i3_scratch */
- if (workspace->name[0] == '_' && workspace->name[1] == '_')
+ if (con_is_internal(workspace))
return;
/* disable fullscreen for the other workspaces and get the workspace we are
LOG("switching to %p\n", workspace);
Con *next = con_descend_focused(workspace);
+ /* Memorize current output */
+ Con *old_output = con_get_output(focused);
+
+ /* Display urgency hint for a while if the newly visible workspace would
+ * focus and thereby immediately destroy it */
+ if (next->urgent && (int)(config.workspace_urgency_timer * 1000) > 0) {
+ /* focus for now… */
+ con_focus(next);
+
+ /* … but immediately reset urgency flags; they will be set to false by
+ * the timer callback in case the container is focused at the time of
+ * its expiration */
+ focused->urgent = true;
+ workspace->urgent = true;
+
+ if (focused->urgency_timer == NULL) {
+ DLOG("Deferring reset of urgency flag of con %p on newly shown workspace %p\n",
+ focused, workspace);
+ focused->urgency_timer = scalloc(sizeof(struct ev_timer));
+ /* use a repeating timer to allow for easy resets */
+ ev_timer_init(focused->urgency_timer, workspace_defer_update_urgent_hint_cb,
+ config.workspace_urgency_timer, config.workspace_urgency_timer);
+ focused->urgency_timer->data = focused;
+ ev_timer_start(main_loop, focused->urgency_timer);
+ } else {
+ DLOG("Resetting urgency timer of con %p on workspace %p\n",
+ focused, workspace);
+ ev_timer_again(main_loop, focused->urgency_timer);
+ }
+ } else
+ con_focus(next);
+
+ /* Close old workspace if necessary. This must be done *after* doing
+ * urgency handling, because tree_close() will do a con_focus() on the next
+ * client, which will clear the urgency flag too early. Also, there is no
+ * way for con_focus() to know about when to clear urgency immediately and
+ * when to defer it. */
if (old && TAILQ_EMPTY(&(old->nodes_head)) && TAILQ_EMPTY(&(old->floating_head))) {
/* check if this workspace is currently visible */
if (!workspace_is_visible(old)) {
}
}
- /* Memorize current output */
- Con *old_output = con_get_output(focused);
-
- con_focus(next);
workspace->fullscreen_mode = CF_OUTPUT;
LOG("focused now = %p / %s\n", focused, focused->name);
*/
void workspace_show_by_name(const char *num) {
Con *workspace;
- bool changed_num_workspaces;
- workspace = workspace_get(num, &changed_num_workspaces);
+ workspace = workspace_get(num, NULL);
_workspace_show(workspace);
}
/* If currently a numbered workspace, find next numbered workspace. */
TAILQ_FOREACH(output, &(croot->nodes_head), nodes) {
/* Skip outputs starting with __, they are internal. */
- if (output->name[0] == '_' && output->name[1] == '_')
+ if (con_is_internal(output))
continue;
NODES_FOREACH(output_get_content(output)) {
if (child->type != CT_WORKSPACE)
bool found_current = false;
TAILQ_FOREACH(output, &(croot->nodes_head), nodes) {
/* Skip outputs starting with __, they are internal. */
- if (output->name[0] == '_' && output->name[1] == '_')
+ if (con_is_internal(output))
continue;
NODES_FOREACH(output_get_content(output)) {
if (child->type != CT_WORKSPACE)
if (!next) {
TAILQ_FOREACH(output, &(croot->nodes_head), nodes) {
/* Skip outputs starting with __, they are internal. */
- if (output->name[0] == '_' && output->name[1] == '_')
+ if (con_is_internal(output))
continue;
NODES_FOREACH(output_get_content(output)) {
if (child->type != CT_WORKSPACE)
/* If numbered workspace, find previous numbered workspace. */
TAILQ_FOREACH_REVERSE(output, &(croot->nodes_head), nodes_head, nodes) {
/* Skip outputs starting with __, they are internal. */
- if (output->name[0] == '_' && output->name[1] == '_')
+ if (con_is_internal(output))
continue;
NODES_FOREACH_REVERSE(output_get_content(output)) {
if (child->type != CT_WORKSPACE || child->num == -1)
bool found_current = false;
TAILQ_FOREACH_REVERSE(output, &(croot->nodes_head), nodes_head, nodes) {
/* Skip outputs starting with __, they are internal. */
- if (output->name[0] == '_' && output->name[1] == '_')
+ if (con_is_internal(output))
continue;
NODES_FOREACH_REVERSE(output_get_content(output)) {
if (child->type != CT_WORKSPACE)
if (!prev) {
TAILQ_FOREACH_REVERSE(output, &(croot->nodes_head), nodes_head, nodes) {
/* Skip outputs starting with __, they are internal. */
- if (output->name[0] == '_' && output->name[1] == '_')
+ if (con_is_internal(output))
continue;
NODES_FOREACH_REVERSE(output_get_content(output)) {
if (child->type != CT_WORKSPACE)
workspace_show_by_name(previous_workspace_name);
}
+/*
+ * Returns the previously focused workspace con, or NULL if unavailable.
+ *
+ */
+Con *workspace_back_and_forth_get(void) {
+ if (!previous_workspace_name) {
+ DLOG("no previous workspace name set.");
+ return NULL;
+ }
+
+ Con *workspace;
+ workspace = workspace_get(previous_workspace_name, NULL);
+
+ return workspace;
+}
+
static bool get_urgency_flag(Con *con) {
Con *child;
TAILQ_FOREACH(child, &(con->nodes_head), nodes)
xcb_poly_fill_rectangle(conn, con->pixmap, con->pm_gc, 1, &bottomline);
}
/* 1pixel border needs an additional line at the top */
- if (p->border_style == BS_1PIXEL && !(borders_to_hide & ADJ_UPPER_SCREEN_EDGE)) {
+ if (p->border_style == BS_PIXEL && !(borders_to_hide & ADJ_UPPER_SCREEN_EDGE)) {
xcb_rectangle_t topline = { br.x, 0, con->rect.width + br.width + br.x, br.y };
xcb_poly_fill_rectangle(conn, con->pixmap, con->pm_gc, 1, &topline);
}
/* 5: draw two unconnected lines in border color */
xcb_change_gc(conn, parent->pm_gc, XCB_GC_FOREGROUND, (uint32_t[]){ p->color->border });
Rect *dr = &(con->deco_rect);
+ int deco_diff_l = 2;
+ int deco_diff_r = 2;
+ if (parent->layout == L_TABBED) {
+ if (TAILQ_PREV(con, nodes_head, nodes) != NULL)
+ deco_diff_l = 0;
+ if (TAILQ_NEXT(con, nodes) != NULL)
+ deco_diff_r = 0;
+ }
xcb_segment_t segments[] = {
{ dr->x, dr->y,
dr->x + dr->width - 1, dr->y },
- { dr->x + 2, dr->y + dr->height - 1,
- dr->x + dr->width - 3, dr->y + dr->height - 1 }
+ { dr->x + deco_diff_l, dr->y + dr->height - 1,
+ dr->x - deco_diff_r + dr->width - 1, dr->y + dr->height - 1 }
};
xcb_poly_segment(conn, parent->pixmap, parent->pm_gc, 2, segments);
int text_offset_y = (con->deco_rect.height - config.font.height) / 2;
struct Window *win = con->window;
- if (win == NULL || win->name == NULL) {
- /* this is a non-leaf container, we need to make up a good description */
- // TODO: use a good description instead of just "another container"
- draw_text_ascii("another container",
+ if (win == NULL) {
+ /* we have a split container which gets a representation
+ * of its children as title
+ */
+ char *title;
+ char *tree = con_get_tree_representation(con);
+ sasprintf(&title, "i3: %s", tree);
+ free(tree);
+
+ draw_text_ascii(title,
parent->pixmap, parent->pm_gc,
con->deco_rect.x + 2, con->deco_rect.y + text_offset_y,
con->deco_rect.width - 2);
+ free(title);
+
goto copy_pixmaps;
}
+ if (win->name == NULL)
+ goto copy_pixmaps;
+
int indent_level = 0,
indent_mult = 0;
Con *il_parent = parent;
}
void xcursor_load_cursors(void) {
- 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");
+ 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");
+ cursors[XCURSOR_CURSOR_MOVE] = load_cursor("fleur");
+ cursors[XCURSOR_CURSOR_TOP_LEFT_CORNER] = load_cursor("top_left_corner");
+ cursors[XCURSOR_CURSOR_TOP_RIGHT_CORNER] = load_cursor("top_right_corner");
+ cursors[XCURSOR_CURSOR_BOTTOM_LEFT_CORNER] = load_cursor("bottom_left_corner");
+ cursors[XCURSOR_CURSOR_BOTTOM_RIGHT_CORNER] = load_cursor("bottom_right_corner");
}
/*
MIN_PERL_VERSION => '5.010000', # 5.10.0
PREREQ_PM => {
'AnyEvent' => 0,
- 'AnyEvent::I3' => '0.09',
+ 'AnyEvent::I3' => '0.14',
'X11::XCB' => '0.03',
'Inline' => 0,
'ExtUtils::PkgConfig' => 0,
pod2usage(-verbose => 2, -exitcode => 0) if $help;
+# Check for missing executables
+my @binaries = qw(
+ ../i3
+ ../i3bar/i3bar
+ ../i3-config-wizard/i3-config-wizard
+ ../i3-dump-log/i3-dump-log
+ ../i3-input/i3-input
+ ../i3-msg/i3-msg
+ ../i3-nagbar/i3-nagbar
+ );
+
+foreach my $binary (@binaries) {
+ die "$binary executable not found" unless -e $binary;
+ die "$binary is not an executable" unless -x $binary;
+}
+
@displays = split(/,/, join(',', @displays));
@displays = map { s/ //g; $_ } @displays;
# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
# (unless you are already familiar with Perl)
-use i3test;
+use i3test i3_autostart => 0;
use List::Util qw(first);
+my $config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+force_display_urgency_hint 0ms
+EOT
+my $pid = launch_with_config($config);
+
my $tmp = fresh_workspace;
#####################################################################
$bottom->delete_hint('urgency');
sync_with_i3;
+################################################################################
+# Check if urgent flag gets propagated to parent containers
+################################################################################
+
+cmd 'split v';
+
+
+
+sub count_urgent {
+ my ($con) = @_;
+
+ my @children = (@{$con->{nodes}}, @{$con->{floating_nodes}});
+ my $urgent = grep { $_->{urgent} } @children;
+ $urgent += count_urgent($_) for @children;
+ return $urgent;
+}
+
+$tmp = fresh_workspace;
+
+my $win1 = open_window;
+my $win2 = open_window;
+cmd 'layout stacked';
+cmd 'split vertical';
+my $win3 = open_window;
+my $win4 = open_window;
+cmd 'split horizontal' ;
+my $win5 = open_window;
+my $win6 = open_window;
+
+sync_with_i3;
+
+
+my $urgent = count_urgent(get_ws($tmp));
+is($urgent, 0, 'no window got the urgent flag');
+
+cmd '[id="' . $win2->id . '"] focus';
+sync_with_i3;
+$win5->add_hint('urgency');
+$win6->add_hint('urgency');
+sync_with_i3;
+
+# we should have 5 urgent cons. win5, win6 and their 3 split parents.
+
+$urgent = count_urgent(get_ws($tmp));
+is($urgent, 5, '2 windows and 3 split containers got the urgent flag');
+
+cmd '[id="' . $win5->id . '"] focus';
+sync_with_i3;
+
+# now win5 and still the split parents should be urgent.
+$urgent = count_urgent(get_ws($tmp));
+is($urgent, 4, '1 window and 3 split containers got the urgent flag');
+
+cmd '[id="' . $win6->id . '"] focus';
+sync_with_i3;
+
+# now now window should be urgent.
+$urgent = count_urgent(get_ws($tmp));
+is($urgent, 0, 'All urgent flags got cleared');
+
+################################################################################
+# Regression test: Check that urgent floating containers work properly (ticket
+# #821)
+################################################################################
+
+$tmp = fresh_workspace;
+my $floating_win = open_floating_window;
+
+# switch away
+fresh_workspace;
+
+$floating_win->add_hint('urgency');
+sync_with_i3;
+
+cmd "workspace $tmp";
+
+does_i3_live;
+
+exit_gracefully($pid);
+
done_testing;
border => 'normal',
'floating_nodes' => $ignore,
workspace_layout => 'default',
+ current_border_width => -1,
};
# a shallow copy is sufficient, since we only ignore values at the root
cmd 'workspace number 5';
ok(workspace_exists('5'), 'workspace 5 was created');
+################################################################################
+# Check that we can go to workspace "7: foo" with the command
+# "workspace number 7: bar", i.e. the additional workspace name is ignored.
+################################################################################
+
+ok(!workspace_exists('7'), 'workspace 7 does not exist');
+ok(!workspace_exists('7: bar'), 'workspace 7: bar does not exist');
+ok(!workspace_exists('7: foo'), 'workspace 7: foo does not exist yet');
+cmd 'workspace 7: foo';
+ok(workspace_exists('7: foo'), 'workspace 7: foo was created');
+cmd 'open';
+
+cmd 'workspace 6';
+ok(workspace_exists('7: foo'), 'workspace 7: foo still open');
+cmd 'workspace number 7: bar';
+is(focused_ws(), '7: foo', 'now on workspace 7: foo');
+ok(!workspace_exists('7'), 'workspace 7 still does not exist');
+ok(!workspace_exists('7: bar'), 'workspace 7: bar still does not exist');
+
+################################################################################
+# Check that "workspace number 8: foo" will create workspace "8: foo" if it
+# does not yet exist (just like "workspace 8: foo" would).
+################################################################################
+
+ok(!workspace_exists('8: foo'), 'workspace 8: foo does not exist');
+cmd 'workspace number 8: foo';
+ok(workspace_exists('8: foo'), 'workspace 8: foo was created');
+
################################################################################
# Verify that renaming workspaces works.
################################################################################
ok(!workspace_exists('13'), 'workspace 13 does still not exist');
+################################################################################
+# Check that 'move to workspace number <number><name>' works to move a window to
+# named workspaces which start with <number>.
+################################################################################
+
+cmd 'workspace 15: meh';
+cmd 'open';
+is_num_children('15: meh', 1, 'one container on 15: meh');
+
+ok(!workspace_exists('15'), 'workspace 15 does not exist yet');
+ok(!workspace_exists('15: duh'), 'workspace 15: duh does not exist yet');
+
+cmd 'workspace 14';
+cmd 'open';
+
+cmd 'move to workspace number 15: duh';
+is_num_children('15: meh', 2, 'two containers on 15: meh');
+is_num_children('14', 0, 'no container on 14 anymore');
+
+ok(!workspace_exists('15'), 'workspace 15 does still not exist');
+ok(!workspace_exists('15: duh'), 'workspace 15 does still not exist');
+
###################################################################
# check if 'move workspace next' and 'move workspace prev' work
###################################################################
cmd 'border 1pixel';
-is(get_border_style(), '1pixel', 'border style 1pixel after changing');
+is(get_border_style(), 'pixel', 'border style 1pixel after changing');
# perform an inplace-restart
cmd 'restart';
does_i3_live;
-is(get_border_style(), '1pixel', 'border style still 1pixel after restart');
+is(get_border_style(), 'pixel', 'border style still 1pixel after restart');
done_testing;
cmd 'border 1pixel';
@nodes = @{get_ws_content($tmp)};
-is($nodes[0]->{border}, '1pixel', 'border style 1pixel');
+is($nodes[0]->{border}, 'pixel', 'border style 1pixel');
+is($nodes[0]->{current_border_width}, 1, 'border width = 1px');
cmd 'border none';
@nodes = @{get_ws_content($tmp)};
is($nodes[0]->{border}, 'none', 'border style none');
+is($nodes[0]->{current_border_width}, 0, 'border width = 0px');
cmd 'border normal';
@nodes = @{get_ws_content($tmp)};
is($nodes[0]->{border}, 'normal', 'border style back to normal');
+is($nodes[0]->{current_border_width}, 2, 'border width = 2px');
cmd 'border toggle';
@nodes = @{get_ws_content($tmp)};
is($nodes[0]->{border}, 'none', 'border style none');
+is($nodes[0]->{current_border_width}, 0, 'border width = 0px');
cmd 'border toggle';
@nodes = @{get_ws_content($tmp)};
-is($nodes[0]->{border}, '1pixel', 'border style 1pixel');
+is($nodes[0]->{border}, 'pixel', 'border style 1pixel');
+is($nodes[0]->{current_border_width}, 1, 'border width = 1px');
cmd 'border toggle';
@nodes = @{get_ws_content($tmp)};
is($nodes[0]->{border}, 'normal', 'border style back to normal');
+is($nodes[0]->{current_border_width}, 2, 'border width = 2px');
done_testing;
@content = @{get_ws_content($tmp)};
ok(@content == 1, 'one container opened');
-is($content[0]->{border}, '1pixel', 'border normal by default');
+is($content[0]->{border}, 'pixel', 'border pixel by default');
+is($content[0]->{current_border_width}, -1, 'border width pixels -1 (default)');
exit_gracefully($pid);
@floating = @{$wscontent->{floating_nodes}};
ok(@floating == 1, 'one floating container opened');
$floatingcon = $floating[0];
-is($floatingcon->{nodes}->[0]->{border}, '1pixel', 'border normal by default');
+is($floatingcon->{nodes}->[0]->{border}, 'pixel', 'border pixel by default');
exit_gracefully($pid);
cmd qq|workspace "$second_ws"|;
ok(get_ws($second_ws)->{focused}, 'second workspace still focused');
+################################################################################
+# verify that 'move workspace back_and_forth' works as expected
+################################################################################
+
+cmd qq|workspace "$first_ws"|;
+my $first_win = open_window;
+
+cmd qq|workspace "$second_ws"|;
+my $second_win = open_window;
+
+is(@{get_ws_content($first_ws)}, 1, 'one container on ws 1 before moving');
+cmd 'move workspace back_and_forth';
+is(@{get_ws_content($first_ws)}, 2, 'two containers on ws 1 before moving');
+
+my $third_win = open_window;
+
+################################################################################
+# verify that moving to the current ws is a no-op without
+# workspace_auto_back_and_forth.
+################################################################################
+
+cmd qq|workspace "$first_ws"|;
+
+is(@{get_ws_content($second_ws)}, 1, 'one container on ws 2 before moving');
+cmd qq|move workspace "$first_ws"|;
+is(@{get_ws_content($second_ws)}, 1, 'still one container');
+
exit_gracefully($pid);
#####################################################################
cmd qq|workspace "$third_ws"|;
ok(get_ws($second_ws)->{focused}, 'second workspace focused');
+$first_win = open_window;
+
+################################################################################
+# verify that moving to the current ws moves to the previous one with
+# workspace_auto_back_and_forth.
+################################################################################
+
+cmd qq|workspace "$first_ws"|;
+$second_win = open_window;
+
+is(@{get_ws_content($second_ws)}, 1, 'one container on ws 2 before moving');
+cmd qq|move workspace "$first_ws"|;
+is(@{get_ws_content($second_ws)}, 2, 'two containers on ws 2');
################################################################################
# Now see if "workspace number <number>" also works as expected with
# 11: focus a workspace and move all of its children to the scratchpad area
################################################################################
-$tmp = fresh_workspace;
+sub verify_scratchpad_move_multiple_win {
+ my $floating = shift;
-my $first = open_window;
-my $second = open_window;
+ my $first = open_window;
+ my $second = open_window;
-cmd 'focus parent';
-cmd 'move scratchpad';
+ if ($floating) {
+ cmd 'floating toggle';
+ cmd 'focus tiling';
+ }
-does_i3_live;
+ cmd 'focus parent';
+ cmd 'move scratchpad';
-$ws = get_ws($tmp);
-is(scalar @{$ws->{nodes}}, 0, 'no windows on ws');
-is(scalar @{$ws->{floating_nodes}}, 0, 'no floating windows on ws');
+ does_i3_live;
-# show the first window.
-cmd 'scratchpad show';
+ $ws = get_ws($tmp);
+ is(scalar @{$ws->{nodes}}, 0, 'no windows on ws');
+ is(scalar @{$ws->{floating_nodes}}, 0, 'no floating windows on ws');
-$ws = get_ws($tmp);
-is(scalar @{$ws->{nodes}}, 0, 'no windows on ws');
-is(scalar @{$ws->{floating_nodes}}, 1, 'one floating windows on ws');
+ # show the first window.
+ cmd 'scratchpad show';
-$old_focus = get_focused($tmp);
+ $ws = get_ws($tmp);
+ is(scalar @{$ws->{nodes}}, 0, 'no windows on ws');
+ is(scalar @{$ws->{floating_nodes}}, 1, 'one floating windows on ws');
-cmd 'scratchpad show';
+ $old_focus = get_focused($tmp);
-# show the second window.
-cmd 'scratchpad show';
+ cmd 'scratchpad show';
-$ws = get_ws($tmp);
-is(scalar @{$ws->{nodes}}, 0, 'no windows on ws');
-is(scalar @{$ws->{floating_nodes}}, 1, 'one floating windows on ws');
+ # show the second window.
+ cmd 'scratchpad show';
-isnt(get_focused($tmp), $old_focus, 'focus changed');
+ $ws = get_ws($tmp);
+ is(scalar @{$ws->{nodes}}, 0, 'no windows on ws');
+ is(scalar @{$ws->{floating_nodes}}, 1, 'one floating windows on ws');
+
+ isnt(get_focused($tmp), $old_focus, 'focus changed');
+}
+
+$tmp = fresh_workspace;
+verify_scratchpad_move_multiple_win(0);
+$tmp = fresh_workspace;
+verify_scratchpad_move_multiple_win(1);
# TODO: make i3bar display *something* when a window on the scratchpad has the urgency hint
#!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)
+#
# When using a command which moves a window to scratchpad from an invisible
# (e.g. unfocused) workspace and immediately shows that window again, i3
# crashed.
--- /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)
+#
+# Verifies that the IPC 'mode' event is sent when modes are changed.
+use i3test i3_autostart => 0;
+
+my $config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+mode "m1" {
+ bindsym Mod1+x nop foo
+}
+
+mode "with spaces" {
+ bindsym Mod1+y nop bar
+}
+EOT
+
+my $pid = launch_with_config($config);
+
+my $i3 = i3(get_socket_path(0));
+$i3->connect->recv;
+
+my $cv = AnyEvent->condvar;
+
+$i3->subscribe({
+ mode => sub {
+ my ($event) = @_;
+ $cv->send($event->{change} eq 'm1');
+ }
+})->recv;
+
+cmd 'mode "m1"';
+
+# Timeout after 0.5s
+my $t;
+$t = AnyEvent->timer(after => 0.5, cb => sub { $cv->send(0); });
+
+ok($cv->recv, 'Mode event received');
+
+exit_gracefully($pid);
+
+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)
+#
+#
+# Tests whether the urgency timer works as expected and does not break
+# urgency handling.
+#
+
+use List::Util qw(first);
+use i3test i3_autostart => 0;
+use Time::HiRes qw(sleep);
+
+# Ensure the pointer is at (0, 0) so that we really start on the first
+# (the left) workspace.
+$x->root->warp_pointer(0, 0);
+
+my $config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+force_display_urgency_hint 150ms
+EOT
+my $pid = launch_with_config($config);
+
+#####################################################################
+# Initial setup: one window on ws1, empty ws2
+#####################################################################
+
+my $tmp1 = fresh_workspace;
+my $w = open_window;
+
+my $tmp2 = fresh_workspace;
+cmd "workspace $tmp2";
+
+$w->add_hint('urgency');
+sync_with_i3;
+
+#######################################################################
+# Create a window on ws1, then switch to ws2, set urgency, switch back
+#######################################################################
+
+isnt($x->input_focus, $w->id, 'window not focused');
+
+my @content = @{get_ws_content($tmp1)};
+my @urgent = grep { $_->{urgent} } @content;
+is(@urgent, 1, "window marked as urgent");
+
+# switch to ws1
+cmd "workspace $tmp1";
+
+# this will start the timer
+sleep(0.1);
+@content = @{get_ws_content($tmp1)};
+@urgent = grep { $_->{urgent} } @content;
+is(@urgent, 1, 'window still marked as urgent');
+
+# now check if the timer was triggered
+cmd "workspace $tmp2";
+
+sleep(0.1);
+@content = @{get_ws_content($tmp1)};
+@urgent = grep { $_->{urgent} } @content;
+is(@urgent, 0, 'window not marked as urgent anymore');
+
+#######################################################################
+# Create another window on ws1, focus it, switch to ws2, make the other
+# window urgent, and switch back. This should not trigger the timer.
+#######################################################################
+
+cmd "workspace $tmp1";
+my $w2 = open_window;
+is($x->input_focus, $w2->id, 'window 2 focused');
+
+cmd "workspace $tmp2";
+$w->add_hint('urgency');
+sync_with_i3;
+
+@content = @{get_ws_content($tmp1)};
+@urgent = grep { $_->{urgent} } @content;
+is(@urgent, 1, 'window 1 marked as urgent');
+
+# Switch back to ws1. This should focus w2.
+cmd "workspace $tmp1";
+is($x->input_focus, $w2->id, 'window 2 focused');
+
+@content = @{get_ws_content($tmp1)};
+@urgent = grep { $_->{urgent} } @content;
+is(@urgent, 1, 'window 1 still marked as urgent');
+
+# explicitly focusing the window should result in immediate urgency reset
+cmd '[id="' . $w->id . '"] focus';
+@content = @{get_ws_content($tmp1)};
+@urgent = grep { $_->{urgent} } @content;
+is(@urgent, 0, 'window 1 not marked as urgent anymore');
+
+################################################################################
+# open a stack, mark one window as urgent, switch to that workspace and verify
+# it’s cleared correctly.
+################################################################################
+
+sub count_total_urgent {
+ my ($con) = @_;
+
+ my $urgent = ($con->{urgent} ? 1 : 0);
+ $urgent += count_total_urgent($_) for (@{$con->{nodes}}, @{$con->{floating_nodes}});
+ return $urgent;
+}
+
+my $tmp3 = fresh_workspace;
+open_window;
+open_window;
+cmd 'split v';
+my $split_left = open_window;
+cmd 'layout stacked';
+
+cmd "workspace $tmp2";
+
+is(count_total_urgent(get_ws($tmp3)), 0, "no urgent windows on workspace $tmp3");
+
+$split_left->add_hint('urgency');
+sync_with_i3;
+
+cmp_ok(count_total_urgent(get_ws($tmp3)), '>=', 0, "more than one urgent window on workspace $tmp3");
+
+cmd "workspace $tmp3";
+
+# Remove the urgency hint.
+$split_left->delete_hint('urgency');
+sync_with_i3;
+
+sleep(0.2);
+is(count_total_urgent(get_ws($tmp3)), 0, "no more urgent windows on workspace $tmp3");
+
+exit_gracefully($pid);
+
+done_testing;
is($old_rect->{width}, $new_rect->{width}, 'width unchanged');
is($old_rect->{height}, $new_rect->{height}, 'height unchanged');
+################################################################################
+# Verify that empty workspaces get cleaned up when moving a different workspace
+# to that output.
+################################################################################
+
+my $empty_ws = fresh_workspace(output => 0);
+my $other_output_ws = fresh_workspace(output => 1);
+cmd 'open';
+
+($x0, $x1) = workspaces_per_screen();
+ok($empty_ws ~~ @$x0, 'empty_ws on fake-0');
+ok($other_output_ws ~~ @$x1, 'other_output_ws on fake-1');
+
+cmd 'move workspace to output left';
+
+($x0, $x1) = workspaces_per_screen();
+ok(!($empty_ws ~~ @$x0), 'empty_ws not on fake-0');
+ok(!($empty_ws ~~ @$x1), 'empty_ws not on fake-1');
+ok($other_output_ws ~~ @$x0, 'other_output_ws on fake-0');
+
exit_gracefully($pid);
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)
+#
+# Verifies that focus output right works with monitor setups that don’t line up
+# on their x/y coordinates.
+#
+# ticket #771, bug still present in commit dd743f3b55b2f86d9f1f69ef7952ae8ece4de504
+#
+use i3test i3_autostart => 0;
+
+sub test_focus_left_right {
+ my ($config) = @_;
+
+ my $pid = launch_with_config($config);
+
+ my $i3 = i3(get_socket_path(0));
+
+ $x->root->warp_pointer(0, 0);
+ sync_with_i3;
+
+ ############################################################################
+ # Ensure that moving left and right works.
+ ############################################################################
+
+ # First ensure both workspaces have something to focus
+ cmd "workspace 1";
+ my $left_win = open_window;
+
+ cmd "workspace 2";
+ my $right_win = open_window;
+
+ is($x->input_focus, $right_win->id, 'right window focused');
+
+ cmd "focus output left";
+ is($x->input_focus, $left_win->id, 'left window focused');
+
+ cmd "focus output right";
+ is($x->input_focus, $right_win->id, 'right window focused');
+
+ cmd "focus output right";
+ is($x->input_focus, $left_win->id, 'left window focused (wrapping)');
+
+ cmd "focus output left";
+ is($x->input_focus, $right_win->id, 'right window focused (wrapping)');
+
+ ############################################################################
+ # Ensure that moving down/up from S0 doesn’t crash i3 and is a no-op.
+ ############################################################################
+
+ my $second = fresh_workspace(output => 1);
+ my $third_win = open_window;
+
+ cmd "focus output down";
+ is($x->input_focus, $third_win->id, 'right window still focused');
+
+ cmd "focus output up";
+ is($x->input_focus, $third_win->id, 'right window still focused');
+
+ does_i3_live;
+
+ exit_gracefully($pid);
+}
+
+# Screen setup looks like this:
+# +----+
+# | |--------+
+# | S1 | S2 |
+# | |--------+
+# +----+
+#
+my $config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+fake-outputs 1080x1920+0+0,1920x1080+1080+500
+EOT
+
+test_focus_left_right($config);
+
+# Screen setup looks like this:
+# +----+--------+
+# | | S2 |
+# | S1 |--------+
+# | |
+# +----+
+#
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+fake-outputs 1080x1920+0+0,1920x200+1080+0
+EOT
+
+test_focus_left_right($config);
+
+# Screen setup looks like this:
+# +----+
+# | |
+# | S1 |--------+
+# | | S2 |
+# +----+--------+
+#
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+fake-outputs 1080x1920+0+0,1920x200+1080+1720
+EOT
+
+test_focus_left_right($config);
+
+# Screen setup looks like this:
+# +----+ +----+
+# | | | |
+# | S1 |--------+ S3 |
+# | | S2 | |
+# +----+--------+----+
+#
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+fake-outputs 1080x1920+0+0,1920x200+1080+1720,1080x1920+1280+0
+EOT
+
+my $pid = launch_with_config($config);
+
+my $i3 = i3(get_socket_path(0));
+
+$x->root->warp_pointer(0, 0);
+sync_with_i3;
+
+############################################################################
+# Ensure that focusing right/left works in the expected order.
+############################################################################
+
+sub get_focused_output {
+ my $tree = i3(get_socket_path())->get_tree->recv;
+ my ($focused_id) = @{$tree->{focus}};
+ my ($output) = grep { $_->{id} == $focused_id } @{$tree->{nodes}};
+ return $output->{name};
+}
+
+is(get_focused_output(), 'fake-0', 'focus on fake-0');
+
+cmd 'focus output right';
+is(get_focused_output(), 'fake-1', 'focus on fake-1');
+
+cmd 'focus output right';
+is(get_focused_output(), 'fake-2', 'focus on fake-2');
+
+cmd 'focus output left';
+is(get_focused_output(), 'fake-1', 'focus on fake-1');
+
+cmd 'focus output left';
+is(get_focused_output(), 'fake-0', 'focus on fake-0');
+
+cmd 'focus output left';
+is(get_focused_output(), 'fake-2', 'focus on fake-2 (wrapping)');
+
+cmd 'focus output right';
+is(get_focused_output(), 'fake-0', 'focus on fake-0 (wrapping)');
+
+exit_gracefully($pid);
+
+done_testing;