]> git.sur5r.net Git - i3/i3/commitdiff
Merge branch 'master' into next
authorMichael Stapelberg <michael@stapelberg.de>
Sun, 2 Oct 2011 11:57:27 +0000 (12:57 +0100)
committerMichael Stapelberg <michael@stapelberg.de>
Sun, 2 Oct 2011 11:57:27 +0000 (12:57 +0100)
108 files changed:
DEPENDS
Makefile
PACKAGE-MAINTAINER
common.mk
debian/changelog
debian/control
docs/Makefile
docs/i3-sync-working.dia [new file with mode: 0644]
docs/i3-sync-working.png [new file with mode: 0644]
docs/i3-sync.dia [new file with mode: 0644]
docs/i3-sync.png [new file with mode: 0644]
docs/ipc
docs/multi-monitor
docs/testsuite [new file with mode: 0644]
docs/userguide
i3-input/main.c
i3-msg/main.c
i3-sensible-editor [new file with mode: 0755]
i3-sensible-pager [new file with mode: 0755]
i3-sensible-terminal [new file with mode: 0755]
i3.config
i3.config.keycodes
i3bar/include/common.h
i3bar/include/outputs.h
i3bar/include/trayclients.h [new file with mode: 0644]
i3bar/include/xcb.h
i3bar/include/xcb_atoms.def
i3bar/src/child.c
i3bar/src/outputs.c
i3bar/src/ucs2_to_utf8.c
i3bar/src/workspaces.c
i3bar/src/xcb.c
include/all.h
include/atoms.xmacro
include/config.h
include/data.h
include/i3/ipc.h
include/match.h
include/regex.h [new file with mode: 0644]
include/sd-daemon.h [new file with mode: 0644]
include/window.h
man/i3-input.man
src/cfgparse.l
src/cfgparse.y
src/cmdparse.l
src/cmdparse.y
src/config.c
src/floating.c
src/handlers.c
src/ipc.c
src/load_layout.c
src/main.c
src/manage.c
src/match.c
src/randr.c
src/regex.c [new file with mode: 0644]
src/sd-daemon.c [new file with mode: 0644]
src/tree.c
src/window.c
src/workspace.c
testcases/complete-run.pl
testcases/t/02-fullscreen.t
testcases/t/04-floating.t
testcases/t/05-ipc.t
testcases/t/06-focus.t
testcases/t/08-focus-stack.t
testcases/t/09-stacking.t
testcases/t/10-dock.t
testcases/t/11-goto.t
testcases/t/12-floating-resize.t
testcases/t/13-urgent.t
testcases/t/14-client-leader.t
testcases/t/17-workspace.t
testcases/t/19-match.t
testcases/t/29-focus-after-close.t
testcases/t/33-size-hints.t
testcases/t/35-floating-focus.t
testcases/t/36-floating-ws-empty.t
testcases/t/37-floating-unmap.t
testcases/t/38-floating-attach.t
testcases/t/39-ws-numbers.t
testcases/t/40-focus-lost.t
testcases/t/41-resize.t
testcases/t/45-flattening.t
testcases/t/46-floating-reinsert.t
testcases/t/47-regress-floatingmove.t
testcases/t/48-regress-floatingmovews.t
testcases/t/50-regress-dock-restart.t
testcases/t/53-floating-originalsize.t
testcases/t/54-regress-multiple-dock.t
testcases/t/55-floating-split-size.t
testcases/t/56-fullscreen-focus.t
testcases/t/57-regress-fullscreen-level-up.t
testcases/t/58-wm_take_focus.t
testcases/t/59-socketpaths.t
testcases/t/61-regress-borders-restart.t
testcases/t/62-regress-dock-urgent.t
testcases/t/63-wm-state.t
testcases/t/64-kill-win-vs-client.t
testcases/t/65-for_window.t
testcases/t/66-assign.t
testcases/t/67-workspace_layout.t
testcases/t/68-regress-fullscreen-restart.t
testcases/t/70-force_focus_wrapping.t
testcases/t/71-config-migrate.t
testcases/t/73-get-marks.t [new file with mode: 0644]
testcases/t/74-border-config.t [new file with mode: 0644]
testcases/t/lib/i3test.pm

diff --git a/DEPENDS b/DEPENDS
index ea7133a5552f7f8ae263538be754677fc685856a..710637e99582227130bd3480e34e9b11f02cf28b 100644 (file)
--- a/DEPENDS
+++ b/DEPENDS
@@ -11,7 +11,7 @@
 │ xcb-proto   │ 1.3    │ 1.6    │ http://xcb.freedesktop.org/dist/       │
 │ libxcb      │ 1.1.93 │ 1.7    │ http://xcb.freedesktop.org/dist/       │
 │ xcb-util    │ 0.3.3  │ 0.3.8  │ http://xcb.freedesktop.org/dist/       │
-│ libev       │ 3.0    │ 4.04   │ http://libev.schmorp.de/               │
+│ libev       │ 4.0    │ 4.04   │ http://libev.schmorp.de/               │
 │ flex        │ 2.5.35 │ 2.5.35 │ http://flex.sourceforge.net/           │
 │ bison       │ 2.4.1  │ 2.4.1  │ http://www.gnu.org/software/bison/     │
 │ yajl        │ 1.0.8  │ 2.0.1  │ http://lloyd.github.com/yajl/          │
@@ -20,6 +20,7 @@
 │ docbook-xml │ 4.5    │ 4.5    │ http://www.methods.co.nz/asciidoc/     │
 │ libxcursor  │ 1.1.11 │ 1.1.11 │ http://ftp.x.org/pub/current/src/lib/  │
 │ Xlib        │ 1.3.3  │ 1.4.3  │ http://ftp.x.org/pub/current/src/lib/  │
+│ PCRE        │ 8.12   │ 8.12   │ http://www.pcre.org/                   │
 └─────────────┴────────┴────────┴────────────────────────────────────────┘
 
  i3-msg, i3-input, i3-nagbar and i3-config-wizard do not introduce any new
index 15ccf0e648f49385be5ef237cbf5cc53dc92d8d2..4289eb4df09d52b6b8ec9a8eae6696176e1cdbec 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -80,6 +80,9 @@ install: all
        $(INSTALL) -d -m 0755 $(DESTDIR)$(PREFIX)/share/xsessions
        $(INSTALL) -m 0755 i3 $(DESTDIR)$(PREFIX)/bin/
        $(INSTALL) -m 0755 i3-migrate-config-to-v4 $(DESTDIR)$(PREFIX)/bin/
+       $(INSTALL) -m 0755 i3-sensible-editor $(DESTDIR)$(PREFIX)/bin/
+       $(INSTALL) -m 0755 i3-sensible-pager $(DESTDIR)$(PREFIX)/bin/
+       $(INSTALL) -m 0755 i3-sensible-terminal $(DESTDIR)$(PREFIX)/bin/
        test -e $(DESTDIR)$(SYSCONFDIR)/i3/config || $(INSTALL) -m 0644 i3.config $(DESTDIR)$(SYSCONFDIR)/i3/config
        test -e $(DESTDIR)$(SYSCONFDIR)/i3/config.keycodes || $(INSTALL) -m 0644 i3.config.keycodes $(DESTDIR)$(SYSCONFDIR)/i3/config.keycodes
        $(INSTALL) -m 0644 i3.welcome $(DESTDIR)$(SYSCONFDIR)/i3/welcome
@@ -93,7 +96,7 @@ dist: distclean
        [ ! -d i3-${VERSION} ] || rm -rf i3-${VERSION}
        [ ! -e i3-${VERSION}.tar.bz2 ] || rm i3-${VERSION}.tar.bz2
        mkdir i3-${VERSION}
-       cp i3-migrate-config-to-v4 i3.config.keycodes DEPENDS GOALS LICENSE PACKAGE-MAINTAINER TODO RELEASE-NOTES-${VERSION} i3.config i3.desktop i3.welcome pseudo-doc.doxygen i3-wsbar Makefile i3-${VERSION}
+       cp i3-migrate-config-to-v4 i3-sensible-* i3.config.keycodes DEPENDS GOALS LICENSE PACKAGE-MAINTAINER TODO RELEASE-NOTES-${VERSION} i3.config i3.desktop i3.welcome pseudo-doc.doxygen i3-wsbar Makefile i3-${VERSION}
        cp -r src i3-msg i3-nagbar i3-config-wizard i3bar yajl-fallback include man i3-${VERSION}
        # Only copy toplevel documentation (important stuff)
        mkdir i3-${VERSION}/docs
index 3d9a8e1812f995df02520f0c5588dbc6096f9ccd..269ce0fd12992be1670ca400ce6113c53df83731 100644 (file)
@@ -10,15 +10,21 @@ packages for them.
 Please make sure the manpage for i3 will be properly created and installed
 in your package.
 
-Also please provide the path to a suitable terminal emulator which is installed
-as a dependency of your package (e.g. urxvt). On systems which have a special
-commend to launch the best available terminal emulator, please use this one
-(e.g. x-terminal-emulator on debian).
+Please check the i3-sensible-{editor,pager,terminal} scripts and modify them if
+necessary. The former two provide fallbacks in case $PAGER or $EDITOR is not
+set (which might be more common than you think, because they have to be set in
+~/.xsession, not in the shell configuration!) while the latter tries to launch
+a terminal emulator. The scripts are most prominently used in i3-nagbar, which
+alerts the user when the configuration is broken for some reason. Also,
+i3-sensible-terminal is used in the default configuration.
 
-On debian, this looks like this:
+If your distribution has a mechanism to get the preferred terminal, such as the
+x-terminal-emulator symlink in Debian, please use it in i3-sensible-terminal.
+
+On debian, compilation and installing the manpages looks like this:
 
        # Compilation
-       $(MAKE) TERM_EMU=x-terminal-emulator
+       $(MAKE)
        $(MAKE) -C man
 
        # Installation
index ce41f287394af52d3400517ab19875dcda845f7f..b8ceea3bfc65475a72f9152db1c5c09e72999576 100644 (file)
--- a/common.mk
+++ b/common.mk
@@ -12,7 +12,6 @@ ifndef SYSCONFDIR
     SYSCONFDIR=$(PREFIX)/etc
   endif
 endif
-TERM_EMU=xterm
 # The escaping is absurd, but we need to escape for shell, sed, make, define
 GIT_VERSION:="$(shell git describe --tags --always) ($(shell git log --pretty=format:%cd --date=short -n1), branch $(shell [ -f $(TOPDIR)/.git/HEAD ] && sed 's/ref: refs\/heads\/\(.*\)/\\\\\\"\1\\\\\\"/g' $(TOPDIR)/.git/HEAD || echo 'unknown'))"
 VERSION:=$(shell git describe --tags --abbrev=0)
@@ -49,10 +48,15 @@ CFLAGS += $(call cflags_for_lib, xcursor)
 CFLAGS += $(call cflags_for_lib, x11)
 CFLAGS += $(call cflags_for_lib, yajl)
 CFLAGS += $(call cflags_for_lib, libev)
+CFLAGS += $(call cflags_for_lib, libpcre)
 CPPFLAGS += -DI3_VERSION=\"${GIT_VERSION}\"
 CPPFLAGS += -DSYSCONFDIR=\"${SYSCONFDIR}\"
 CPPFLAGS += -DTERM_EMU=\"$(TERM_EMU)\"
 
+ifeq ($(shell pkg-config --atleast-version=8.10 libpcre && echo 1),1)
+CPPFLAGS += -DPCRE_HAS_UCP=1
+endif
+
 LIBS += -lm
 LIBS += $(call ldflags_for_lib, xcb-event, xcb-event)
 LIBS += $(call ldflags_for_lib, xcb-keysyms, xcb-keysyms)
@@ -70,6 +74,7 @@ LIBS += $(call ldflags_for_lib, xcursor, Xcursor)
 LIBS += $(call ldflags_for_lib, x11, X11)
 LIBS += $(call ldflags_for_lib, yajl, yajl)
 LIBS += $(call ldflags_for_lib, libev, ev)
+LIBS += $(call ldflags_for_lib, libpcre, pcre)
 
 # Please test if -Wl,--as-needed works on your platform and send me a patch.
 # it is known not to work on Darwin (Mac OS X)
index 990badb2dd93e221e0e782d2ab1a5d1bd76560b6..e670cd8fdb41f381265ebb0d6b6b135ce345894a 100644 (file)
@@ -1,6 +1,18 @@
-i3-wm (4.0.3-0) unstable; urgency=low
+i3-wm (4.1-0) unstable; urgency=low
 
   * NOT YET RELEASED!
+  * Implement system tray support in i3bar (for NetworkManager, Skype, …)
+  * Implement support for PCRE regular expressions in criteria
+  * Implement a new assign syntax which uses criteria
+  * Sort named workspaces whose name starts with a number accordingly
+  * Warn on duplicate bindings for the same key
+  * Restrict 'resize' command to left/right for horizontal containers, up/down
+    for vertical containers
+  * Implement the GET_MARKS IPC request to get all marks
+  * Implement the new_float config option (border style for floating windows)
+  * Implement passing IPC sockets to i3 (systemd-style socket activation)
+  * Implement the 'move output' command to move containers to a specific output
+  * Bugfix: Preserve marks when restarting
 
  -- Michael Stapelberg <michael@stapelberg.de>  Sun, 28 Aug 2011 20:17:31 +0200
 
index b546e650d5555687fdea71f6bcbd83f65275a8c2..da13231f385564bdc4921157ddcd17ca59eed68a 100644 (file)
@@ -3,7 +3,7 @@ Section: utils
 Priority: extra
 Maintainer: Michael Stapelberg <michael@stapelberg.de>
 DM-Upload-Allowed: yes
-Build-Depends: debhelper (>= 6), libx11-dev, libxcb-util0-dev (>= 0.3.8), libxcb-keysyms1-dev, libxcb-xinerama0-dev (>= 1.1), libxcb-randr0-dev, libxcb-icccm4-dev, libxcursor-dev, asciidoc (>= 8.4.4), xmlto, docbook-xml, pkg-config, libev-dev, flex, bison, libyajl-dev, perl, texlive-latex-base, texlive-latex-recommended, texlive-latex-extra
+Build-Depends: debhelper (>= 6), libx11-dev, libxcb-util0-dev (>= 0.3.8), libxcb-keysyms1-dev, libxcb-xinerama0-dev (>= 1.1), libxcb-randr0-dev, libxcb-icccm4-dev, libxcursor-dev, asciidoc (>= 8.4.4), xmlto, docbook-xml, pkg-config, libev-dev, flex, bison, libyajl-dev, perl, texlive-latex-base, texlive-latex-recommended, texlive-latex-extra, libpcre3-dev
 Standards-Version: 3.9.2
 Homepage: http://i3wm.org/
 
index 9d70243df13be1154cb72a2d97bb1f0cae252535..990bba877d1869d3800f2d01d72266a72369ed92 100644 (file)
@@ -1,5 +1,5 @@
 
-all: hacking-howto.html debugging.html userguide.html ipc.html multi-monitor.html wsbar.html refcard.pdf
+all: hacking-howto.html debugging.html userguide.html ipc.html multi-monitor.html wsbar.html refcard.pdf testsuite.html
 
 hacking-howto.html: hacking-howto
        asciidoc -a toc -n $<
@@ -10,6 +10,9 @@ debugging.html: debugging
 userguide.html: userguide
        asciidoc -a toc -n $<
 
+testsuite.html: testsuite
+       asciidoc -a toc -n $<
+
 ipc.html: ipc
        asciidoc -a toc -n $<
 
diff --git a/docs/i3-sync-working.dia b/docs/i3-sync-working.dia
new file mode 100644 (file)
index 0000000..9f1c3bc
Binary files /dev/null and b/docs/i3-sync-working.dia differ
diff --git a/docs/i3-sync-working.png b/docs/i3-sync-working.png
new file mode 100644 (file)
index 0000000..dce44ac
Binary files /dev/null and b/docs/i3-sync-working.png differ
diff --git a/docs/i3-sync.dia b/docs/i3-sync.dia
new file mode 100644 (file)
index 0000000..0945ae2
Binary files /dev/null and b/docs/i3-sync.dia differ
diff --git a/docs/i3-sync.png b/docs/i3-sync.png
new file mode 100644 (file)
index 0000000..b64cce2
Binary files /dev/null and b/docs/i3-sync.png differ
index 7e71326022606aa2b4d1abf93d4d77d3c448fe1a..4093ffce26e2233b7258c62bedc4e180b3c70fad 100644 (file)
--- a/docs/ipc
+++ b/docs/ipc
@@ -59,6 +59,10 @@ GET_TREE (4)::
        Gets the layout tree. i3 uses a tree as data structure which includes
        every container. The reply will be the JSON-encoded tree (see the reply
        section).
+GET_MARKS (5)::
+       Gets a list of marks (identifiers for containers to easily jump to them
+       later). The reply will be a JSON-encoded list of window marks (see
+       reply section).
 
 So, a typical message could look like this:
 --------------------------------------------------
@@ -110,6 +114,8 @@ GET_OUTPUTS (3)::
        Reply to the GET_OUTPUTS message.
 GET_TREE (4)::
        Reply to the GET_TREE message.
+GET_MARKS (5)::
+       Reply to the GET_MARKS message.
 
 === COMMAND reply
 
@@ -416,6 +422,16 @@ JSON dump:
    }
  ]
 }
+
+
+=== GET_MARKS reply
+
+The reply consists of a single array of strings for each container that has a
+mark. The order of that array is undefined. If more than one container has the
+same mark, it will be represented multiple times in the reply (the array
+contents are not unique).
+
+If no window has a mark the response will be the empty array [].
 ------------------------
 
 
index ec0256c053c70c5aeee874a710dda7622cdf1b41..a1fd6dc039273b454c2ddc9ddcd322c4726d754c 100644 (file)
@@ -1,7 +1,7 @@
 The multi-monitor situation
 ===========================
 Michael Stapelberg <michael+i3@stapelberg.de>
-March 2010
+September 2011
 
 …or: oh no, I have an nVidia graphics card!
 
@@ -16,6 +16,8 @@ i3, like so:
 exec i3 --force-xinerama -V >>~/.i3/i3log 2>&1
 ----------------------------------------------
 
+…or use +force_xinerama yes+ in your configuration file.
+
 == The explanation
 
 Starting with version 3.ε, i3 uses the RandR (Rotate and Resize) API instead
@@ -50,9 +52,13 @@ these are two screens).
 
 For this very reason, we decided to implement the following workaround: As
 long as the nVidia driver does not support RandR, an option called
-+--force-xinerama+ is available in i3. This option gets the list of screens
-*once* when starting, and never updates it. As the nVidia driver cannot do
-dynamic configuration anyways, this is not a big deal.
++--force-xinerama+ is available in i3 (alternatively, you can use the
++force_xinerama+ configuration file directive). This option gets the list of
+screens *once* when starting, and never updates it. As the nVidia driver cannot
+do dynamic configuration anyways, this is not a big deal.
+
+Also note that your output names are not descriptive (like +HDMI1+) when using
+Xinerama, instead they are counted up, starting at 0: +xinerama-0+, +xinerama-1+, …
 
 == See also
 
diff --git a/docs/testsuite b/docs/testsuite
new file mode 100644 (file)
index 0000000..b3b76c7
--- /dev/null
@@ -0,0 +1,451 @@
+i3 testsuite
+============
+Michael Stapelberg <michael+i3@stapelberg.de>
+September 2011
+
+This document explains how the i3 testsuite works, how to use it and extend it.
+It is targeted at developers who not necessarily have been doing testing before
+or have not been testing in Perl before. In general, the testsuite is not of
+interest for end users.
+
+
+== Introduction
+
+The i3 testsuite is a collection of files which contain testcases for various
+i3 features. Some of them test if a certain workflow works correctly (moving
+windows, focus behaviour, …). Others are regression tests and contain code
+which previously made i3 crash or lead to unexpected behaviour. They then check
+if i3 still runs (meaning it did not crash) and if it handled everything
+correctly.
+
+The goal of having these tests is to automatically find problems and to
+automatically get a feel for whether a change in the source code breaks any
+existing feature. After every modification of the i3 sourcecode, the developer
+should run the full testsuite. If one of the tests fails, the corresponding
+problem should be fixed (or, in some cases, the testcase has to be modified).
+For every bugreport, a testcase should be written to test the correct
+behaviour. Initially, it will fail, but after fixing the bug, it will pass.
+This ensures (or increases the chance) that bugs which have been fixed once
+will never be found again.
+
+Also, when implementing a new feature, a testcase might be a good way to be
+able to easily test if the feature is working correctly. Many developers will
+test manually if everything works. Having a testcase not only helps you with
+that, but it will also be useful for every future change.
+
+== Implementation
+
+For several reasons, the i3 testsuite has been implemented in Perl:
+
+1. Perl has a long tradition of testing. Every popular/bigger Perl module which
+   you can find on CPAN will not only come with documentation, but also with
+   tests. Therefore, the available infrastructure for tests is comprehensive.
+   See for example the excellent http://search.cpan.org/perldoc?Test::More
+   and the referenced http://search.cpan.org/perldoc?Test::Tutorial.
+
+2. Perl is widely available and has a well-working package infrastructure.
+3. The author is familiar with Perl :).
+
+Please do not start programming language flamewars at this point.
+
+=== Mechanisms
+
+==== Script: complete-run
+
+The testcases are run by a script called +complete-run.pl+. It runs all
+testcases by default, but you can be more specific and let it only run one or
+more testcases. Also, it takes care of starting up a separate instance of i3
+with an appropriate configuration file and creates a folder for each run
+containing the appropriate i3 logfile for each testcase.  The latest folder can
+always be found under the symlink +latest/+. It is recommended that you run the
+tests on one or more separate X server instances (you can only start one window
+manager per X session), for example using the provided Xdummy script.
++complete-run.pl+ takes one or more X11 display specifications and parallelizes
+the testcases appropriately:
+
+.Example invocation of complete-run.pl+
+---------------------------------------
+$ cd ~/i3/testcases
+
+# start two dummy X11 instances in the background
+$ ./Xdummy :1 &
+$ ./Xdummy :2 &
+
+$ ./complete-run.pl -d :1,:2
+# output omitted because it is very long
+All tests successful.
+Files=78, Tests=734, 27 wallclock secs ( 0.38 usr  0.48 sys + 17.65 cusr  3.21 csys = 21.72 CPU)
+Result: PASS
+
+$ ./complete-run.pl -d :1 t/04-floating.t
+[:3] i3 startup: took 0.07s, status = 1
+[:3] Running t/04-floating.t with logfile testsuite-2011-09-24-16-06-04-4.0.2-226-g1eb011a/i3-log-for-04-floating.t
+[:3] t/04-floating.t finished
+[:3] killing i3
+output for t/04-floating.t:
+ok 1 - use X11::XCB::Window;
+ok 2 - The object isa X11::XCB::Window
+ok 3 - Window is mapped
+ok 4 - i3 raised the width to 75
+ok 5 - i3 raised the height to 50
+ok 6 - i3 did not map it to (0x0)
+ok 7 - The object isa X11::XCB::Window
+ok 8 - i3 let the width at 80
+ok 9 - i3 let the height at 90
+ok 10 - i3 mapped it to x=1
+ok 11 - i3 mapped it to y=18
+ok 12 - The object isa X11::XCB::Window
+ok 13 - i3 let the width at 80
+ok 14 - i3 let the height at 90
+1..14
+
+All tests successful.
+Files=1, Tests=14,  0 wallclock secs ( 0.01 usr  0.00 sys +  0.19 cusr  0.03 csys =  0.23 CPU)
+Result: PASS
+
+$ less latest/i3-log-for-04-floating.t
+----------------------------------------
+
+==== IPC interface
+
+The testsuite makes extensive use of the IPC (Inter-Process Communication)
+interface which i3 provides. It is used for the startup process of i3, for
+terminating it cleanly and (most importantly) for modifying and getting the
+current state (layout tree).
+
+See [http://i3wm.org/docs/ipc.html] for documentation on the IPC interface.
+
+==== X11::XCB
+
+In order to open new windows, change attributes, get events, etc., the
+testsuite uses X11::XCB, a new (and quite specific to i3 at the moment) Perl
+module which uses the XCB protocol description to generate Perl bindings to
+X11. They work in a very similar way to libxcb (which i3 uses) and provide
+relatively high-level interfaces (objects such as +X11::XCB::Window+) aswell as
+access to the low-level interface, which is very useful when testing a window
+manager.
+
+=== Filesystem structure
+
+In the git root of i3, the testcases live in the folder +testcases+. This
+folder contains the +complete-run.pl+ and +Xdummy+ scripts and a base
+configuration file which will be used for the tests. The different testcases
+(their file extension is .t, not .pl) themselves can be found in the
+conventionally named subfolder +t+:
+
+.Filesystem structure
+--------------------------------------------
+├── testcases
+│   ├── complete-run.pl
+│   ├── i3-test.config
+│   ├── t
+│   │   ├── 00-load.t
+│   │   ├── 01-tile.t
+│   │   ├── 02-fullscreen.t
+│   │   ├── ...
+│   │   ├── omitted for brevity
+│   │   ├── ...
+│   │   ├── 74-regress-focus-toggle.t
+│   │   └── lib
+│   │       └── i3test.pm
+│   └── Xdummy
+--------------------------------------------
+
+== Anatomy of a testcase
+
+Learning by example is definitely a good strategy when you are wondering how to
+write a testcase. Let's take +t/11-goto.t+ as an easy example and go through it
+step by step:
+
+.t/11-goto.t: Boilerplate
+----------------------
+#!perl
+# vim:ts=4:sw=4:expandtab
+
+use i3test;
+use File::Temp;
+
+my $x = X11::XCB::Connection->new;
+-----------------------
+
+This is what we call boilerplate. It exists at the top of every test file (to
+some extent). The first line is the shebang, which specifies that this file is
+a Perl script. The second line contains VIM specific settings on how to
+edit/format this file (use spaces instead of tabs, indent using 4 spaces).
+Afterwards, the +i3test+ module is used. This module contains i3 testsuite
+specific functions which you are strongly encouraged to use. They make writing
+testcases a lot easier and will make it easier for other people to read your
+tests.
+
+The next line uses the +File::Temp+ module. This is specific to this testcase,
+because it needs to generate a temporary name during the test. Many testcases
+use only the +i3test+ module.
+
+The last line opens a connection to X11. You might or might not need this in
+your testcase, depending on whether you are going to open windows (etc.) or
+only use i3 commands.
+
+.t/11-goto.t: Setup
+----------------------
+my $tmp = fresh_workspace;
+
+cmd 'split h';
+----------------------
+
+The first line calls i3test's +fresh_workspace+ function which looks for a
+currently unused workspace, switches to it, and returns its name. The variable
++$tmp+ will end up having a value such as +"/tmp/87kBVcHbA9"+. Note that this
+is not (necessarily) a valid path, it's just a random workspace name.
+
+So, now that we are on a new workspace, we ensure that the workspace uses
+horizontal orientation by issuing the +split h+ command (see the i3 User's
+Guide for a list of commands). This is not strictly necessary, but good style.
+In general, the +cmd+ function executes the specified i3 command by using the
+IPC interface and returns once i3 acknowledged the command.
+
+.t/11-goto.t: Setup
+----------------------
+#####################################################################
+# Create two windows and make sure focus switching works
+#####################################################################
+
+my $top = open_window($x);
+my $mid = open_window($x);
+my $bottom = open_window($x);
+----------------------
+
+In every major section of a testcase, you should put a comment like the one
+above. This makes it immediately clear how the file is structured.
+
+The +open_window+ function opens a standard window, which will then be put into
+tiling mode by i3. If you want a floating window, use the
++open_floating_window+ function. These functions accept the same parameters as
++X11::XCB::Window->new+, see the i3test documentation at TODO.
+
+.t/11-goto.t: Helper function
+----------------------
+#
+# Returns the input focus after sending the given command to i3 via IPC
+# and syncing with i3
+#
+sub focus_after {
+    my $msg = shift;
+
+    cmd $msg;
+    sync_with_i3 $x;
+    return $x->input_focus;
+}
+----------------------
+
+This section defines a helper function which will be used over and over in this
+testcase. If you have code which gets executed more than once or twice
+(depending on the length of your test, use your best judgement), please put it
+in a function. Tests should be short, concise and clear.
+
+The +focus_after+ function executes a command and returns the X11 focus after
+the command was executed. The +sync_with_i3+ command makes sure that i3 could
+push its state to X11. See <<i3_sync>> to learn how this works exactly.
+
+.t/11-goto.t: Test assumptions
+----------------------
+$focus = $x->input_focus;
+is($focus, $bottom->id, "Latest window focused");
+
+$focus = focus_after('focus left');
+is($focus, $mid->id, "Middle window focused");
+----------------------
+
+Now, we run the first two real tests. They use +Test::More+'s +is+ function,
+which compares two values and prints the differences if they are not the same.
+After the arguments, we supply a short comment to indicate what we are testing
+here. This makes it vastly more easy for the developer to spot which testcase
+is the problem in case one fails.
+
+The first test checks that the most recently opened window is focused.
+Afterwards, the command +focus left+ is issued and it is verified that the
+middle window now has focus.
+
+Note that this is not a comprehensive test of the +focus+ command -- we would
+have to test wrapping, focus when using a more complex layout, focusing the
+parent/child containers, etc. But that is not the point of this testcase.
+Instead, we just want to know if +$x->input_focus+ corresponds with what we are
+expecting. If not, something is completely wrong with the test environment and
+this trivial test will fail.
+
+.t/11-goto.t: Test that the feature does not work (yet)
+----------------------
+#####################################################################
+# Now goto a mark which does not exist
+#####################################################################
+
+my $random_mark = mktemp('mark.XXXXXX');
+
+$focus = focus_after(qq|[con_mark="$random_mark"] focus|);
+is($focus, $mid->id, "focus unchanged");
+----------------------
+
+Syntax hint: The qq keyword is the interpolating quote operator. It lets you
+chose a quote character (in this case the +|+ character, a pipe). This makes
+having double quotes in our string easy.
+
+In this new major section, a random mark (mark is an identifier for a window,
+see "VIM-like marks" in the i3 User’s Guide) will be generated. Afterwards, we
+test that trying to focus that mark will not do anything. This is important: Do
+not only test that using a feature has the expected outcome, but also test that
+using it without properly initializing it does no harm. This command could for
+example have changed focus anyways (a bug) or crash i3 (obviously a bug).
+
+.t/11-goto.t: Test that the feature does work
+----------------------
+cmd "mark $random_mark";
+
+$focus = focus_after('focus left');
+is($focus, $top->id, "Top window focused");
+
+$focus = focus_after(qq|[con_mark="$random_mark"] focus|);
+is($focus, $mid->id, "goto worked");
+----------------------
+
+Remember: Focus was on the middle window (we verified that earlier in "Test
+assumptions"). We now mark the middle window with our randomly generated mark.
+Afterwards, we switch focus away from the middle window to be able to tell if
+focusing it via its mark will work. If the test works, the goto command seems
+to be working.
+
+.t/11-goto.t: Test corner case
+----------------------
+# check that we can specify multiple criteria
+
+$focus = focus_after('focus left');
+is($focus, $top->id, "Top window focused");
+
+$focus = focus_after(qq|[con_mark="$random_mark" con_mark="$random_mark"] focus|);
+is($focus, $mid->id, "goto worked");
+----------------------
+
+Now we test the same feature, but specifying the mark twice in the command.
+This should have no effect, but let’s be sure: test it and see if things go
+wrong.
+
+.t/11-goto.t: Test second code path
+----------------------
+#####################################################################
+# Check whether the focus command will switch to a different
+# workspace if necessary
+#####################################################################
+
+my $tmp2 = fresh_workspace;
+
+is(focused_ws(), $tmp2, 'tmp2 now focused');
+
+cmd qq|[con_mark="$random_mark"] focus|;
+
+is(focused_ws(), $tmp, 'tmp now focused');
+----------------------
+
+This part of the test checks that focusing windows by mark works across
+workspaces. It uses i3test's +focused_ws+ function to get the current
+workspace.
+
+.t/11-goto.t: Test second code path
+----------------------
+done_testing;
+----------------------
+
+The end of every testcase has to contain the +done_testing+ line. This tells
++complete-run.pl+ that the test was finished successfully. If it does not
+occur, the test might have crashed during execution -- some of the reasons why
+that could happen are bugs in the used modules, bugs in the testcase itself or
+an i3 crash resulting in the testcase being unable to communicate with i3 via
+IPC anymore.
+
+[[i3_sync]]
+== Appendix A: The i3 sync protocol
+
+Consider the following situation: You open two windows in your testcase, then
+you use +focus left+ and want to verify that the X11 focus has been updated
+properly. Sounds simple, right? Let’s assume you use this straight-forward
+implementation:
+
+.Racey focus testcase
+-----------
+my $left = open_window($x);
+my $right = open_window($x);
+cmd 'focus left';
+is($x->input_focus, $left->id, 'left window focused');
+----------
+
+However, the test fails. Sometimes. Apparantly, there is a race condition in
+your test. If you think about it, this is because you are using two different
+pieces of software: You tell i3 to update focus, i3 confirms that, and then you
+ask X11 to give you the current focus. There is a certain time i3 needs to
+update the X11 state. If the testcase gets CPU time before X11 processed i3's
+requests, the test will fail.
+
+image::i3-sync.png["Diagram of the race condition", title="Diagram of the race condition"]
+
+One way to "solve" this would be to add +sleep 0.5;+ after the +cmd+ call.
+After 0.5 seconds it should be safe to assume that focus has been updated,
+right?
+
+In practice, this usually works. However, it has several problems:
+
+1. This is obviously not a clean solution, but a workaround. Ugly.
+2. On very slow machines, this might not work. Unlikely, but in different
+   situations (a delay to wait for i3 to startup) the necessary time is much
+   harder to guess, even for fast machines.
+3. This *wastes a lot of time*. Usually, your computer is much faster than 0.5s
+   to update the status. However, sometimes, it might take 0.4s, so we can’t
+   make it +sleep 0.1+.
+
+To illustrate how grave the problem with wasting time actually is: Before
+removing all sleeps from the testsuite, a typical run using 4 separate X
+servers took around 50 seconds on my machine. After removing all the sleeps,
+we achieved times of about 25 seconds. This is very significant and influences
+the way you think about tests -- the faster they are, the more likely you are
+to check whether everything still works quite often (which you should).
+
+What I am trying to say is: Delays adds up quickly and make the test suite
+less robust.
+
+The real solution for this problem is a mechanism which I call "the i3 sync
+protocol". The idea is to send a request (which does not modify state) via X11
+to i3 which will then be answered. Due to the request's position in the event
+queue (*after* all previous events), you can be sure that by the time you
+receive the reply, all other events have been dealt with by i3 (and, more
+importantly, X11).
+
+image::i3-sync-working.png["Diagram of the i3 sync solution", title="Diagram of the i3 sync solution"]
+
+=== Implementation details
+
+The client which wants to sync with i3 initiates the protocol by sending a
+ClientMessage to the X11 root window:
+
+.Send ClientMessage
+-------------------
+# Generate a ClientMessage, see xcb_client_message_t
+my $msg = pack "CCSLLLLLLL",
+    CLIENT_MESSAGE, # response_type
+    32,     # format
+    0,      # sequence
+    $root,  # destination window
+    $x->atom(name => 'I3_SYNC')->id,
+
+    $_sync_window->id,    # data[0]: our own window id
+    $myrnd, # data[1]: a random value to identify the request
+    0,
+    0,
+    0;
+
+# Send it to the root window -- since i3 uses the SubstructureRedirect
+# event mask, it will get the ClientMessage.
+$x->send_event(0, $root, EVENT_MASK_SUBSTRUCTURE_REDIRECT, $msg);
+-------------------
+
+i3 will then reply with the same ClientMessage, sent to the window specified in
++data[0]+. In the reply, +data[0]+ and +data[1]+ are exactly the same as in the
+request. You should use a random value in +data[1]+ and check that you received
+the same one when getting the reply.
+
+== Appendix B: Socket activation
index a8317d0421f8e24ee1224b2706e8e586570ba565..8b3a685963aa362feacc1dd09e0f74a5c892406a 100644 (file)
@@ -1,7 +1,7 @@
 i3 User’s Guide
 ===============
 Michael Stapelberg <michael+i3@stapelberg.de>
-August 2011
+September 2011
 
 This document contains all the information you need to configure and use the i3
 window manager. If it does not, please contact us on IRC (preferred) or post your
@@ -431,7 +431,7 @@ change their border style, for example.
 
 *Syntax*:
 -----------------------------
-for_window [criteria] command
+for_window <criteria> command
 -----------------------------
 
 *Examples*:
@@ -478,37 +478,59 @@ configuration file and run it before starting i3 (for example in your
 
 [[assign_workspace]]
 
-Specific windows can be matched by window class and/or window title. It is
-recommended that you match on window classes instead of window titles whenever
-possible because some applications first create their window, and then worry
-about setting the correct title. Firefox with Vimperator comes to mind. The
-window starts up being named Firefox, and only when Vimperator is loaded does
-the title change. As i3 will get the title as soon as the application maps the
+To automatically make a specific window show up on a specific workspace, you
+can use an *assignment*. You can match windows by using any criteria,
+see <<command_criteria>>. It is recommended that you match on window classes
+(and instances, when appropriate) instead of window titles whenever possible
+because some applications first create their window, and then worry about
+setting the correct title. Firefox with Vimperator comes to mind. The window
+starts up being named Firefox, and only when Vimperator is loaded does the
+title change. As i3 will get the title as soon as the application maps the
 window (mapping means actually displaying it on the screen), you’d need to have
 to match on 'Firefox' in this case.
 
-You can prefix or suffix workspaces with a `~` to specify that matching clients
-should be put into floating mode. If you specify only a `~`, the client will
-not be put onto any workspace, but will be set floating on the current one.
-
 *Syntax*:
 ------------------------------------------------------------
-assign ["]window class[/window title]["] [→] [workspace]
+assign <criteria> [→] workspace
 ------------------------------------------------------------
 
 *Examples*:
 ----------------------
-assign urxvt 2
-assign urxvt → 2
-assign urxvt → work
-assign "urxvt" → 2
-assign "urxvt/VIM" → 3
-assign "gecko" → 4
+# Assign URxvt terminals to workspace 2
+assign [class="URxvt"] 2
+
+# Same thing, but more precise (exact match instead of substring)
+assign [class="^URxvt$"] 2
+
+# Same thing, but with a beautiful arrow :)
+assign [class="^URxvt$"] → 2
+
+# Assignment to a named workspace
+assign [class="^URxvt$"] → work
+
+# Start urxvt -name irssi
+assign [class="^URxvt$" instance="^irssi$"] → 3
 ----------------------
 
 Note that the arrow is not required, it just looks good :-). If you decide to
 use it, it has to be a UTF-8 encoded arrow, not `->` or something like that.
 
+To get the class and instance, you can use +xprop+. After clicking on the
+window, you will see the following output:
+
+*xwininfo*:
+-----------------------------------
+WM_CLASS(STRING) = "irssi", "URxvt"
+-----------------------------------
+
+The first part of the WM_CLASS is the instance ("irssi" in this example), the
+second part is the class ("URxvt" in this example).
+
+Should you have any problems with assignments, make sure to check the i3
+logfile first (see http://i3wm.org/docs/debugging.html). It includes more
+details about the matching process and the window’s actual class, instance and
+title when starting up.
+
 === Automatically starting applications on i3 startup
 
 By using the +exec+ keyword outside a keybinding, you can configure
@@ -690,6 +712,31 @@ force_focus_wrapping <yes|no>
 force_focus_wrapping yes
 ------------------------
 
+=== Forcing Xinerama
+
+As explained in-depth in <http://i3wm.org/docs/multi-monitor.html>, some X11
+video drivers (especially the nVidia binary driver) only provide support for
+Xinerama instead of RandR. In such a situation, i3 must be told to use the
+inferior Xinerama API explicitly and therefore don’t provide support for
+reconfiguring your screens on the fly (they are read only once on startup and
+that’s it).
+
+For people who do cannot modify their +~/.xsession+ to add the
++--force-xinerama+ commandline parameter, a configuration option is provided:
+
+*Syntax*:
+-----------------------
+force_xinerama <yes|no>
+-----------------------
+
+*Example*:
+------------------
+force_xinerama yes
+------------------
+
+Also note that your output names are not descriptive (like +HDMI1+) when using
+Xinerama, instead they are counted up, starting at 0: +xinerama-0+, +xinerama-1+, …
+
 == List of commands
 
 Commands are what you bind to specific keypresses. You can also issue commands
@@ -721,6 +768,9 @@ which have the class Firefox, use:
 *Example*:
 ------------------------------------
 bindsym mod+x [class="Firefox"] kill
+
+# same thing, but case-insensitive
+bindsym mod+x [class="(?i)firefox"] kill
 ------------------------------------
 
 The criteria which are currently implemented are:
@@ -729,6 +779,8 @@ class::
        Compares the window class (the second part of WM_CLASS)
 instance::
        Compares the window instance (the first part of WM_CLASS)
+window_role::
+       Compares the window role (WM_WINDOW_ROLE).
 id::
        Compares the X11 window ID, which you can get via +xwininfo+ for example.
 title::
@@ -739,8 +791,9 @@ con_id::
        Compares the i3-internal container ID, which you can get via the IPC
        interface. Handy for scripting.
 
-Note that currently all criteria are compared case-insensitive and do not
-support regular expressions. This is planned to change in the future.
+The criteria +class+, +instance+, +role+, +title+ and +mark+ are actually
+regular expressions (PCRE). See +pcresyntax(3)+ or +perldoc perlre+ for
+information on how to use them.
 
 === Splitting containers
 
@@ -838,6 +891,11 @@ You can also switch to the next and previous workspace with the commands
 workspace 1, 3, 4 and 9 and you want to cycle through them with a single key
 combination.
 
+To move a container to another xrandr output such as +LVDS1+ or +VGA1+, you can
+use the +move output+ command followed by the name of the target output. You
+may also use +left+, +right+, +up+, +down+ instead of the xrandr output name to
+move to the the next output in the specified direction.
+
 *Examples*:
 -------------------------
 bindsym mod+1 workspace 1
index fb2635a274024e1f41199bd3e71dd8a164fd9bb0..7e1a18ff004093dce92d972ff0b3cc6b335b78c8 100644 (file)
@@ -1,9 +1,9 @@
 /*
- * vim:ts=8:expandtab
+ * vim:ts=4:sw=4:expandtab
  *
  * i3 - an improved dynamic tiling window manager
  *
- * © 2009 Michael Stapelberg and contributors
+ * © 2009-2011 Michael Stapelberg and contributors
  *
  * See file LICENSE for license information.
  *
 
 #include "i3-input.h"
 
+/* IPC format string. %s will be replaced with what the user entered, then
+ * the command will be sent to i3 */
+static char *format;
+
 static char *socket_path;
 static int sockfd;
 static xcb_key_symbols_t *symbols;
@@ -48,7 +52,6 @@ static char *glyphs_ucs[512];
 static char *glyphs_utf8[512];
 static int input_position;
 static int font_height;
-static char *command_prefix;
 static char *prompt;
 static int prompt_len;
 static int limit;
@@ -61,33 +64,33 @@ xcb_window_t root;
  *
  */
 static char *socket_path_from_x11() {
-        xcb_connection_t *conn;
-        int screen;
-        if ((conn = xcb_connect(NULL, &screen)) == NULL ||
-            xcb_connection_has_error(conn))
-                return NULL;
-        xcb_screen_t *root_screen = xcb_aux_get_screen(conn, screen);
-        xcb_window_t root = root_screen->root;
-
-        xcb_intern_atom_cookie_t atom_cookie;
-        xcb_intern_atom_reply_t *atom_reply;
-
-        atom_cookie = xcb_intern_atom(conn, 0, strlen("I3_SOCKET_PATH"), "I3_SOCKET_PATH");
-        atom_reply = xcb_intern_atom_reply(conn, atom_cookie, NULL);
-        if (atom_reply == NULL)
-                return NULL;
-
-        xcb_get_property_cookie_t prop_cookie;
-        xcb_get_property_reply_t *prop_reply;
-        prop_cookie = xcb_get_property_unchecked(conn, false, root, atom_reply->atom,
-                                                 XCB_GET_PROPERTY_TYPE_ANY, 0, PATH_MAX);
-        prop_reply = xcb_get_property_reply(conn, prop_cookie, NULL);
-        if (prop_reply == NULL || xcb_get_property_value_length(prop_reply) == 0)
-                return NULL;
-        if (asprintf(&socket_path, "%.*s", xcb_get_property_value_length(prop_reply),
-                     (char*)xcb_get_property_value(prop_reply)) == -1)
-                return NULL;
-        return socket_path;
+    xcb_connection_t *conn;
+    int screen;
+    if ((conn = xcb_connect(NULL, &screen)) == NULL ||
+        xcb_connection_has_error(conn))
+        return NULL;
+    xcb_screen_t *root_screen = xcb_aux_get_screen(conn, screen);
+    xcb_window_t root = root_screen->root;
+
+    xcb_intern_atom_cookie_t atom_cookie;
+    xcb_intern_atom_reply_t *atom_reply;
+
+    atom_cookie = xcb_intern_atom(conn, 0, strlen("I3_SOCKET_PATH"), "I3_SOCKET_PATH");
+    atom_reply = xcb_intern_atom_reply(conn, atom_cookie, NULL);
+    if (atom_reply == NULL)
+        return NULL;
+
+    xcb_get_property_cookie_t prop_cookie;
+    xcb_get_property_reply_t *prop_reply;
+    prop_cookie = xcb_get_property_unchecked(conn, false, root, atom_reply->atom,
+                                             XCB_GET_PROPERTY_TYPE_ANY, 0, PATH_MAX);
+    prop_reply = xcb_get_property_reply(conn, prop_cookie, NULL);
+    if (prop_reply == NULL || xcb_get_property_value_length(prop_reply) == 0)
+        return NULL;
+    if (asprintf(&socket_path, "%.*s", xcb_get_property_value_length(prop_reply),
+                 (char*)xcb_get_property_value(prop_reply)) == -1)
+        return NULL;
+    return socket_path;
 }
 
 /*
@@ -96,21 +99,21 @@ static char *socket_path_from_x11() {
  *
  */
 static uint8_t *concat_strings(char **glyphs, int max) {
-        uint8_t *output = calloc(max+1, 4);
-        uint8_t *walk = output;
-        for (int c = 0; c < max; c++) {
-                printf("at %c\n", glyphs[c][0]);
-                /* if the first byte is 0, this has to be UCS2 */
-                if (glyphs[c][0] == '\0') {
-                        memcpy(walk, glyphs[c], 2);
-                        walk += 2;
-                } else {
-                        strcpy((char*)walk, glyphs[c]);
-                        walk += strlen(glyphs[c]);
-                }
+    uint8_t *output = calloc(max+1, 4);
+    uint8_t *walk = output;
+    for (int c = 0; c < max; c++) {
+        printf("at %c\n", glyphs[c][0]);
+        /* if the first byte is 0, this has to be UCS2 */
+        if (glyphs[c][0] == '\0') {
+            memcpy(walk, glyphs[c], 2);
+            walk += 2;
+        } else {
+            strcpy((char*)walk, glyphs[c]);
+            walk += strlen(glyphs[c]);
         }
-        printf("output = %s\n", output);
-        return output;
+    }
+    printf("output = %s\n", output);
+    return output;
 }
 
 /*
@@ -119,37 +122,37 @@ static uint8_t *concat_strings(char **glyphs, int max) {
  *
  */
 static int handle_expose(void *data, xcb_connection_t *conn, xcb_expose_event_t *event) {
-        printf("expose!\n");
-
-        /* re-draw the background */
-        xcb_rectangle_t border = {0, 0, 500, font_height + 8}, inner = {2, 2, 496, font_height + 8 - 4};
-        xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FOREGROUND, get_colorpixel(conn, "#FF0000"));
-        xcb_poly_fill_rectangle(conn, pixmap, pixmap_gc, 1, &border);
-        xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FOREGROUND, get_colorpixel(conn, "#000000"));
-        xcb_poly_fill_rectangle(conn, pixmap, pixmap_gc, 1, &inner);
-
-        /* restore font color */
-        xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FOREGROUND, get_colorpixel(conn, "#FFFFFF"));
-        uint8_t *con = concat_strings(glyphs_ucs, input_position);
-        char *full_text = (char*)con;
-        if (prompt != NULL) {
-                full_text = malloc((prompt_len + input_position) * 2 + 1);
-                if (full_text == NULL)
-                        err(EXIT_FAILURE, "malloc() failed\n");
-                memcpy(full_text, prompt, prompt_len * 2);
-                memcpy(full_text + (prompt_len * 2), con, input_position * 2);
-        }
-        xcb_image_text_16(conn, input_position + prompt_len, pixmap, pixmap_gc, 4 /* X */,
-                          font_height + 2 /* Y = baseline of font */, (xcb_char2b_t*)full_text);
-
-        /* Copy the contents of the pixmap to the real window */
-        xcb_copy_area(conn, pixmap, win, pixmap_gc, 0, 0, 0, 0, /* */ 500, font_height + 8);
-        xcb_flush(conn);
-        free(con);
-        if (prompt != NULL)
-                free(full_text);
-
-        return 1;
+    printf("expose!\n");
+
+    /* re-draw the background */
+    xcb_rectangle_t border = {0, 0, 500, font_height + 8}, inner = {2, 2, 496, font_height + 8 - 4};
+    xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FOREGROUND, get_colorpixel(conn, "#FF0000"));
+    xcb_poly_fill_rectangle(conn, pixmap, pixmap_gc, 1, &border);
+    xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FOREGROUND, get_colorpixel(conn, "#000000"));
+    xcb_poly_fill_rectangle(conn, pixmap, pixmap_gc, 1, &inner);
+
+    /* restore font color */
+    xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FOREGROUND, get_colorpixel(conn, "#FFFFFF"));
+    uint8_t *con = concat_strings(glyphs_ucs, input_position);
+    char *full_text = (char*)con;
+    if (prompt != NULL) {
+        full_text = malloc((prompt_len + input_position) * 2 + 1);
+        if (full_text == NULL)
+            err(EXIT_FAILURE, "malloc() failed\n");
+        memcpy(full_text, prompt, prompt_len * 2);
+        memcpy(full_text + (prompt_len * 2), con, input_position * 2);
+    }
+    xcb_image_text_16(conn, input_position + prompt_len, pixmap, pixmap_gc, 4 /* X */,
+                      font_height + 2 /* Y = baseline of font */, (xcb_char2b_t*)full_text);
+
+    /* Copy the contents of the pixmap to the real window */
+    xcb_copy_area(conn, pixmap, win, pixmap_gc, 0, 0, 0, 0, /* */ 500, font_height + 8);
+    xcb_flush(conn);
+    free(con);
+    if (prompt != NULL)
+        free(full_text);
+
+    return 1;
 }
 
 /*
@@ -157,37 +160,62 @@ static int handle_expose(void *data, xcb_connection_t *conn, xcb_expose_event_t
  *
  */
 static int handle_key_release(void *ignored, xcb_connection_t *conn, xcb_key_release_event_t *event) {
-        printf("releasing %d, state raw = %d\n", event->detail, event->state);
+    printf("releasing %d, state raw = %d\n", event->detail, event->state);
 
-        /* fix state */
-        event->state &= ~numlockmask;
+    /* fix state */
+    event->state &= ~numlockmask;
 
-        xcb_keysym_t sym = xcb_key_press_lookup_keysym(symbols, event, event->state);
-        if (sym == XK_Mode_switch) {
-                printf("Mode switch disabled\n");
-                modeswitch_active = false;
-        }
+    xcb_keysym_t sym = xcb_key_press_lookup_keysym(symbols, event, event->state);
+    if (sym == XK_Mode_switch) {
+        printf("Mode switch disabled\n");
+        modeswitch_active = false;
+    }
 
-        return 1;
+    return 1;
 }
 
 static void finish_input() {
-        uint8_t *command = concat_strings(glyphs_utf8, input_position);
-        char *full_command = (char*)command;
-        /* prefix the command if a prefix was specified on commandline */
-        if (command_prefix != NULL) {
-                if (asprintf(&full_command, "%s%s", command_prefix, command) == -1)
-                        err(EXIT_FAILURE, "asprintf() failed\n");
+    char *command = (char*)concat_strings(glyphs_utf8, input_position);
+
+    /* count the occurences of %s in the string */
+    int c;
+    int len = strlen(format);
+    int cnt = 0;
+    for (c = 0; c < (len-1); c++)
+        if (format[c] == '%' && format[c+1] == 's')
+            cnt++;
+    printf("occurences = %d\n", cnt);
+
+    /* allocate space for the output */
+    int inputlen = strlen(command);
+    char *full = calloc(1,
+                        strlen(format) - (2 * cnt) /* format without all %s */
+                        + (inputlen * cnt)         /* replaced %s */
+                        + 1);                      /* trailing NUL */
+    char *dest = full;
+    for (c = 0; c < len; c++) {
+        /* if this is not % or it is % but without a following 's',
+         * just copy the character */
+        if (format[c] != '%' || (c == (len-1)) || format[c+1] != 's')
+            *(dest++) = format[c];
+        else {
+            strncat(dest, command, inputlen);
+            dest += inputlen;
+            /* skip the following 's' of '%s' */
+            c++;
         }
-        printf("command = %s\n", full_command);
+    }
+
+    /* prefix the command if a prefix was specified on commandline */
+    printf("command = %s\n", full);
 
-        ipc_send_message(sockfd, strlen(full_command), 0, (uint8_t*)full_command);
+    ipc_send_message(sockfd, strlen(full), 0, (uint8_t*)full);
 
 #if 0
-        free(command);
-        return 1;
+    free(command);
+    return 1;
 #endif
-        exit(0);
+    exit(0);
 }
 
 /*
@@ -200,224 +228,237 @@ static void finish_input() {
  *
  */
 static int handle_key_press(void *ignored, xcb_connection_t *conn, xcb_key_press_event_t *event) {
-        printf("Keypress %d, state raw = %d\n", event->detail, event->state);
-
-        /* fix state */
-        if (modeswitch_active)
-                event->state |= modeswitchmask;
-
-        /* Apparantly, after activating numlock once, the numlock modifier
-         * stays turned on (use xev(1) to verify). So, to resolve useful
-         * keysyms, we remove the numlock flag from the event state */
-        event->state &= ~numlockmask;
-
-        xcb_keysym_t sym = xcb_key_press_lookup_keysym(symbols, event, event->state);
-        if (sym == XK_Mode_switch) {
-                printf("Mode switch enabled\n");
-                modeswitch_active = true;
-                return 1;
-        }
-
-        if (sym == XK_Return)
-                finish_input();
+    printf("Keypress %d, state raw = %d\n", event->detail, event->state);
 
-        if (sym == XK_BackSpace) {
-                if (input_position == 0)
-                        return 1;
+    /* fix state */
+    if (modeswitch_active)
+        event->state |= modeswitchmask;
 
-                input_position--;
-                free(glyphs_ucs[input_position]);
-                free(glyphs_utf8[input_position]);
+    /* Apparantly, after activating numlock once, the numlock modifier
+     * stays turned on (use xev(1) to verify). So, to resolve useful
+     * keysyms, we remove the numlock flag from the event state */
+    event->state &= ~numlockmask;
 
-                handle_expose(NULL, conn, NULL);
-                return 1;
-        }
-        if (sym == XK_Escape) {
-                exit(0);
-        }
+    xcb_keysym_t sym = xcb_key_press_lookup_keysym(symbols, event, event->state);
+    if (sym == XK_Mode_switch) {
+        printf("Mode switch enabled\n");
+        modeswitch_active = true;
+        return 1;
+    }
 
-        /* TODO: handle all of these? */
-        printf("is_keypad_key = %d\n", xcb_is_keypad_key(sym));
-        printf("is_private_keypad_key = %d\n", xcb_is_private_keypad_key(sym));
-        printf("xcb_is_cursor_key = %d\n", xcb_is_cursor_key(sym));
-        printf("xcb_is_pf_key = %d\n", xcb_is_pf_key(sym));
-        printf("xcb_is_function_key = %d\n", xcb_is_function_key(sym));
-        printf("xcb_is_misc_function_key = %d\n", xcb_is_misc_function_key(sym));
-        printf("xcb_is_modifier_key = %d\n", xcb_is_modifier_key(sym));
-
-        if (xcb_is_modifier_key(sym) || xcb_is_cursor_key(sym))
-                return 1;
-
-        printf("sym = %c (%d)\n", sym, sym);
-
-        /* convert the keysym to UCS */
-        uint16_t ucs = keysym2ucs(sym);
-        if ((int16_t)ucs == -1) {
-                fprintf(stderr, "Keysym could not be converted to UCS, skipping\n");
-                return 1;
-        }
+    if (sym == XK_Return)
+        finish_input();
 
-        /* store the UCS into a string */
-        uint8_t inp[3] = {(ucs & 0xFF00) >> 8, (ucs & 0xFF), 0};
+    if (sym == XK_BackSpace) {
+        if (input_position == 0)
+            return 1;
 
-        printf("inp[0] = %02x, inp[1] = %02x, inp[2] = %02x\n", inp[0], inp[1], inp[2]);
-        /* convert it to UTF-8 */
-        char *out = convert_ucs_to_utf8((char*)inp);
-        printf("converted to %s\n", out);
+        input_position--;
+        free(glyphs_ucs[input_position]);
+        free(glyphs_utf8[input_position]);
 
-        glyphs_ucs[input_position] = malloc(3 * sizeof(uint8_t));
-        if (glyphs_ucs[input_position] == NULL)
-                err(EXIT_FAILURE, "malloc() failed\n");
-        memcpy(glyphs_ucs[input_position], inp, 3);
-        glyphs_utf8[input_position] = strdup(out);
-        input_position++;
+        handle_expose(NULL, conn, NULL);
+        return 1;
+    }
+    if (sym == XK_Escape) {
+        exit(0);
+    }
+
+    /* TODO: handle all of these? */
+    printf("is_keypad_key = %d\n", xcb_is_keypad_key(sym));
+    printf("is_private_keypad_key = %d\n", xcb_is_private_keypad_key(sym));
+    printf("xcb_is_cursor_key = %d\n", xcb_is_cursor_key(sym));
+    printf("xcb_is_pf_key = %d\n", xcb_is_pf_key(sym));
+    printf("xcb_is_function_key = %d\n", xcb_is_function_key(sym));
+    printf("xcb_is_misc_function_key = %d\n", xcb_is_misc_function_key(sym));
+    printf("xcb_is_modifier_key = %d\n", xcb_is_modifier_key(sym));
+
+    if (xcb_is_modifier_key(sym) || xcb_is_cursor_key(sym))
+        return 1;
 
-        if (input_position == limit)
-                finish_input();
+    printf("sym = %c (%d)\n", sym, sym);
 
-        handle_expose(NULL, conn, NULL);
+    /* convert the keysym to UCS */
+    uint16_t ucs = keysym2ucs(sym);
+    if ((int16_t)ucs == -1) {
+        fprintf(stderr, "Keysym could not be converted to UCS, skipping\n");
         return 1;
+    }
+
+    /* store the UCS into a string */
+    uint8_t inp[3] = {(ucs & 0xFF00) >> 8, (ucs & 0xFF), 0};
+
+    printf("inp[0] = %02x, inp[1] = %02x, inp[2] = %02x\n", inp[0], inp[1], inp[2]);
+    /* convert it to UTF-8 */
+    char *out = convert_ucs_to_utf8((char*)inp);
+    printf("converted to %s\n", out);
+
+    glyphs_ucs[input_position] = malloc(3 * sizeof(uint8_t));
+    if (glyphs_ucs[input_position] == NULL)
+        err(EXIT_FAILURE, "malloc() failed\n");
+    memcpy(glyphs_ucs[input_position], inp, 3);
+    glyphs_utf8[input_position] = strdup(out);
+    input_position++;
+
+    if (input_position == limit)
+        finish_input();
+
+    handle_expose(NULL, conn, NULL);
+    return 1;
 }
 
 int main(int argc, char *argv[]) {
-        socket_path = getenv("I3SOCK");
-        char *pattern = "-misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1";
-        int o, option_index = 0;
-
-        static struct option long_options[] = {
-                {"socket", required_argument, 0, 's'},
-                {"version", no_argument, 0, 'v'},
-                {"limit", required_argument, 0, 'l'},
-                {"prompt", required_argument, 0, 'P'},
-                {"prefix", required_argument, 0, 'p'},
-                {"font", required_argument, 0, 'f'},
-                {"help", no_argument, 0, 'h'},
-                {0, 0, 0, 0}
-        };
-
-        char *options_string = "s:p:P:f:l:vh";
-
-        while ((o = getopt_long(argc, argv, options_string, long_options, &option_index)) != -1) {
-                switch (o) {
-                        case 's':
-                                FREE(socket_path);
-                                socket_path = strdup(optarg);
-                                break;
-                        case 'v':
-                                printf("i3-input " I3_VERSION);
-                                return 0;
-                        case 'p':
-                                FREE(command_prefix);
-                                command_prefix = strdup(optarg);
-                                break;
-                        case 'l':
-                                limit = atoi(optarg);
-                                break;
-                        case 'P':
-                                FREE(prompt);
-                                prompt = strdup(optarg);
-                                break;
-                        case 'f':
-                                FREE(pattern);
-                                pattern = strdup(optarg);
-                                break;
-                        case 'h':
-                                printf("i3-input " I3_VERSION);
-                                printf("i3-input [-s <socket>] [-p <prefix>] [-l <limit>] [-P <prompt>] [-f <font>] [-v]\n");
-                                return 0;
-                }
+    format = strdup("%s");
+    socket_path = getenv("I3SOCK");
+    char *pattern = "-misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1";
+    int o, option_index = 0;
+
+    static struct option long_options[] = {
+        {"socket", required_argument, 0, 's'},
+        {"version", no_argument, 0, 'v'},
+        {"limit", required_argument, 0, 'l'},
+        {"prompt", required_argument, 0, 'P'},
+        {"prefix", required_argument, 0, 'p'},
+        {"format", required_argument, 0, 'F'},
+        {"font", required_argument, 0, 'f'},
+        {"help", no_argument, 0, 'h'},
+        {0, 0, 0, 0}
+    };
+
+    char *options_string = "s:p:P:f:l:F:vh";
+
+    while ((o = getopt_long(argc, argv, options_string, long_options, &option_index)) != -1) {
+        switch (o) {
+            case 's':
+                FREE(socket_path);
+                socket_path = strdup(optarg);
+                break;
+            case 'v':
+                printf("i3-input " I3_VERSION);
+                return 0;
+            case 'p':
+                /* This option is deprecated, but will still work in i3 v4.1, 4.2 and 4.3 */
+                fprintf(stderr, "i3-input: WARNING: the -p option is DEPRECATED in favor of the -F (format) option\n");
+                FREE(format);
+                asprintf(&format, "%s%%s", optarg);
+                break;
+            case 'l':
+                limit = atoi(optarg);
+                break;
+            case 'P':
+                FREE(prompt);
+                prompt = strdup(optarg);
+                break;
+            case 'f':
+                FREE(pattern);
+                pattern = strdup(optarg);
+                break;
+            case 'F':
+                FREE(format);
+                format = strdup(optarg);
+                break;
+            case 'h':
+                printf("i3-input " I3_VERSION "\n");
+                printf("i3-input [-s <socket>] [-F <format>] [-l <limit>] [-P <prompt>] [-f <font>] [-v]\n");
+                printf("\n");
+                printf("Example:\n");
+                printf("    i3-input -F 'workspace \"%%s\"' -P 'Switch to workspace: '\n");
+                return 0;
         }
+    }
 
-        if (socket_path == NULL)
-                socket_path = socket_path_from_x11();
+    printf("using format \"%s\"\n", format);
 
-        if (socket_path == NULL)
-                socket_path = "/tmp/i3-ipc.sock";
+    if (socket_path == NULL)
+        socket_path = socket_path_from_x11();
 
-        sockfd = connect_ipc(socket_path);
+    if (socket_path == NULL)
+        socket_path = "/tmp/i3-ipc.sock";
 
-        if (prompt != NULL)
-                prompt = convert_utf8_to_ucs2(prompt, &prompt_len);
+    sockfd = connect_ipc(socket_path);
 
-        int screens;
-        xcb_connection_t *conn = xcb_connect(NULL, &screens);
-        if (xcb_connection_has_error(conn))
-                die("Cannot open display\n");
+    if (prompt != NULL)
+        prompt = convert_utf8_to_ucs2(prompt, &prompt_len);
 
-        xcb_screen_t *root_screen = xcb_aux_get_screen(conn, screens);
-        root = root_screen->root;
+    int screens;
+    xcb_connection_t *conn = xcb_connect(NULL, &screens);
+    if (xcb_connection_has_error(conn))
+        die("Cannot open display\n");
 
-        modeswitchmask = get_mod_mask(conn, XK_Mode_switch);
-        numlockmask = get_mod_mask(conn, XK_Num_Lock);
-       symbols = xcb_key_symbols_alloc(conn);
+    xcb_screen_t *root_screen = xcb_aux_get_screen(conn, screens);
+    root = root_screen->root;
 
-        uint32_t font_id = get_font_id(conn, pattern, &font_height);
+    modeswitchmask = get_mod_mask(conn, XK_Mode_switch);
+    numlockmask = get_mod_mask(conn, XK_Num_Lock);
+    symbols = xcb_key_symbols_alloc(conn);
 
-        /* Open an input window */
-        win = open_input_window(conn, 500, font_height + 8);
+    uint32_t font_id = get_font_id(conn, pattern, &font_height);
 
-        /* Create pixmap */
-        pixmap = xcb_generate_id(conn);
-        pixmap_gc = xcb_generate_id(conn);
-        xcb_create_pixmap(conn, root_screen->root_depth, pixmap, win, 500, font_height + 8);
-        xcb_create_gc(conn, pixmap_gc, pixmap, 0, 0);
+    /* Open an input window */
+    win = open_input_window(conn, 500, font_height + 8);
 
-        /* Set input focus (we have override_redirect=1, so the wm will not do
-         * this for us) */
-        xcb_set_input_focus(conn, XCB_INPUT_FOCUS_POINTER_ROOT, win, XCB_CURRENT_TIME);
+    /* Create pixmap */
+    pixmap = xcb_generate_id(conn);
+    pixmap_gc = xcb_generate_id(conn);
+    xcb_create_pixmap(conn, root_screen->root_depth, pixmap, win, 500, font_height + 8);
+    xcb_create_gc(conn, pixmap_gc, pixmap, 0, 0);
 
-        /* Create graphics context */
-        xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FONT, font_id);
+    /* Set input focus (we have override_redirect=1, so the wm will not do
+     * this for us) */
+    xcb_set_input_focus(conn, XCB_INPUT_FOCUS_POINTER_ROOT, win, XCB_CURRENT_TIME);
 
-        /* Grab the keyboard to get all input */
-        xcb_flush(conn);
+    /* Create graphics context */
+    xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FONT, font_id);
 
-        /* Try (repeatedly, if necessary) to grab the keyboard. We might not
-         * get the keyboard at the first attempt because of the keybinding
-         * still being active when started via a wm’s keybinding. */
-        xcb_grab_keyboard_cookie_t cookie;
-        xcb_grab_keyboard_reply_t *reply = NULL;
+    /* Grab the keyboard to get all input */
+    xcb_flush(conn);
 
-        int count = 0;
-        while ((reply == NULL || reply->status != XCB_GRAB_STATUS_SUCCESS) && (count++ < 500)) {
-                cookie = xcb_grab_keyboard(conn, false, win, XCB_CURRENT_TIME, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC);
-                reply = xcb_grab_keyboard_reply(conn, cookie, NULL);
-                usleep(1000);
-        }
+    /* Try (repeatedly, if necessary) to grab the keyboard. We might not
+     * get the keyboard at the first attempt because of the keybinding
+     * still being active when started via a wm’s keybinding. */
+    xcb_grab_keyboard_cookie_t cookie;
+    xcb_grab_keyboard_reply_t *reply = NULL;
 
-        if (reply->status != XCB_GRAB_STATUS_SUCCESS) {
-                fprintf(stderr, "Could not grab keyboard, status = %d\n", reply->status);
-                exit(-1);
-        }
+    int count = 0;
+    while ((reply == NULL || reply->status != XCB_GRAB_STATUS_SUCCESS) && (count++ < 500)) {
+        cookie = xcb_grab_keyboard(conn, false, win, XCB_CURRENT_TIME, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC);
+        reply = xcb_grab_keyboard_reply(conn, cookie, NULL);
+        usleep(1000);
+    }
 
-        xcb_flush(conn);
+    if (reply->status != XCB_GRAB_STATUS_SUCCESS) {
+        fprintf(stderr, "Could not grab keyboard, status = %d\n", reply->status);
+        exit(-1);
+    }
 
-        xcb_generic_event_t *event;
-        while ((event = xcb_wait_for_event(conn)) != NULL) {
-                if (event->response_type == 0) {
-                        fprintf(stderr, "X11 Error received! sequence %x\n", event->sequence);
-                        continue;
-                }
+    xcb_flush(conn);
 
-                /* Strip off the highest bit (set if the event is generated) */
-                int type = (event->response_type & 0x7F);
+    xcb_generic_event_t *event;
+    while ((event = xcb_wait_for_event(conn)) != NULL) {
+        if (event->response_type == 0) {
+            fprintf(stderr, "X11 Error received! sequence %x\n", event->sequence);
+            continue;
+        }
 
-                switch (type) {
-                        case XCB_KEY_PRESS:
-                                handle_key_press(NULL, conn, (xcb_key_press_event_t*)event);
-                                break;
+        /* Strip off the highest bit (set if the event is generated) */
+        int type = (event->response_type & 0x7F);
 
-                        case XCB_KEY_RELEASE:
-                                handle_key_release(NULL, conn, (xcb_key_release_event_t*)event);
-                                break;
+        switch (type) {
+            case XCB_KEY_PRESS:
+                handle_key_press(NULL, conn, (xcb_key_press_event_t*)event);
+                break;
 
-                        case XCB_EXPOSE:
-                                handle_expose(NULL, conn, (xcb_expose_event_t*)event);
-                                break;
-                }
+            case XCB_KEY_RELEASE:
+                handle_key_release(NULL, conn, (xcb_key_release_event_t*)event);
+                break;
 
-                free(event);
+            case XCB_EXPOSE:
+                handle_expose(NULL, conn, (xcb_expose_event_t*)event);
+                break;
         }
 
-        return 0;
+        free(event);
+    }
+
+    return 0;
 }
index 630a345d63c6ce57dc611b19464d8a5b6da0f8cf..2d7cef0e907eb3005b52798f46f4ca39e3d88af8 100644 (file)
@@ -180,9 +180,11 @@ int main(int argc, char *argv[]) {
                 message_type = I3_IPC_MESSAGE_TYPE_GET_OUTPUTS;
             else if (strcasecmp(optarg, "get_tree") == 0)
                 message_type = I3_IPC_MESSAGE_TYPE_GET_TREE;
+            else if (strcasecmp(optarg, "get_marks") == 0)
+                message_type = I3_IPC_MESSAGE_TYPE_GET_MARKS;
             else {
                 printf("Unknown message type\n");
-                printf("Known types: command, get_workspaces, get_outputs, get_tree\n");
+                printf("Known types: command, get_workspaces, get_outputs, get_tree, get_marks\n");
                 exit(EXIT_FAILURE);
             }
         } else if (o == 'q') {
@@ -243,7 +245,7 @@ int main(int argc, char *argv[]) {
     uint32_t reply_length;
     uint8_t *reply;
     ipc_recv_message(sockfd, message_type, &reply_length, &reply);
-    printf("%.*s", reply_length, reply);
+    printf("%.*s\n", reply_length, reply);
     free(reply);
 
     close(sockfd);
diff --git a/i3-sensible-editor b/i3-sensible-editor
new file mode 100755 (executable)
index 0000000..dffe00d
--- /dev/null
@@ -0,0 +1,14 @@
+#!/bin/sh
+# This script tries to exec an editor by trying some known editors if $EDITOR is
+# not set.
+#
+# Distributions/packagers can enhance this script with a
+# distribution-specific mechanism to find the preferred pager.
+which $VISUAL >/dev/null && exec $VISUAL "$@"
+which $EDITOR >/dev/null && exec $EDITOR "$@"
+
+# Hopefully one of these is installed (no flamewars about preference please!):
+which nano >/dev/null && exec nano "$@"
+which vim >/dev/null && exec vim "$@"
+which vi >/dev/null && exec vi "$@"
+which emacs >/dev/null && exec emacs "$@"
diff --git a/i3-sensible-pager b/i3-sensible-pager
new file mode 100755 (executable)
index 0000000..5af8d6b
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/sh
+# This script tries to exec a pager by trying some known pagers if $PAGER is
+# not set.
+#
+# Distributions/packagers can enhance this script with a
+# distribution-specific mechanism to find the preferred pager.
+which $PAGER >/dev/null && exec $PAGER "$@"
+
+# Hopefully one of these is installed:
+which most >/dev/null && exec most "$@"
+which less >/dev/null && exec less "$@"
+# we don't use 'more' because it will exit if the file is 'too short'
+
+# If no pager is installed, try an editor
+exec i3-sensible-editor "$@"
diff --git a/i3-sensible-terminal b/i3-sensible-terminal
new file mode 100755 (executable)
index 0000000..28e6062
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/sh
+# This script tries to exec a terminal emulator by trying some known terminal
+# emulators.
+#
+# 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.
+# Please don't touch the first line, though:
+which $TERMINAL >/dev/null && exec $TERMINAL "$@"
+
+# Hopefully one of these is installed:
+which xterm >/dev/null && exec xterm "$@"
+which urxvt >/dev/null && exec urxvt "$@"
+which rxvt >/dev/null && exec rxvt "$@"
+which roxterm >/dev/null && exec roxterm "$@"
index 52c4a7e16044a3c267614715e83246b85e089a76..7d7fce3301c664cf34326b5b8a1b84c579e77d26 100644 (file)
--- a/i3.config
+++ b/i3.config
@@ -16,7 +16,7 @@ font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
 floating_modifier Mod1
 
 # start a terminal
-bindsym Mod1+Return exec urxvt
+bindsym Mod1+Return exec i3-sensible-terminal
 
 # kill focused window
 bindsym Mod1+Shift+q kill
index 17e7483bb0abd78543f450ac300ce3e37435f861..0f1112dba65fc6d3c05541ab5b2a630c596e33bb 100644 (file)
@@ -17,7 +17,7 @@ font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
 floating_modifier $mod
 
 # start a terminal
-bindcode $mod+36 exec urxvt
+bindcode $mod+36 exec i3-sensible-terminal
 
 # kill focused window
 bindcode $mod+Shift+24 kill
index 22e3ca4337d292aaa120650101e6becaa10f11b4..74bd21528501d3b4701043ef51691c30e2891a61 100644 (file)
@@ -9,18 +9,19 @@
 #ifndef COMMON_H_
 #define COMMON_H_
 
+#include <stdbool.h>
+
 typedef struct rect_t rect;
-typedef int bool;
 
 struct ev_loop* main_loop;
 char            *statusline;
 char            *statusline_buffer;
 
 struct rect_t {
-       int     x;
-       int     y;
-       int     w;
-       int     h;
+    int x;
+    int y;
+    int w;
+    int h;
 };
 
 #include "queue.h"
@@ -29,6 +30,7 @@ struct rect_t {
 #include "outputs.h"
 #include "util.h"
 #include "workspaces.h"
+#include "trayclients.h"
 #include "xcb.h"
 #include "ucs2_to_utf8.h"
 #include "config.h"
index f74048da5d5f2604d09419e1ea75d7b4cfeb63a9..c6402a5b620b987569769862e2d5b02890bb9cba 100644 (file)
@@ -22,33 +22,34 @@ struct outputs_head *outputs;
  * Start parsing the received json-string
  *
  */
-void        parse_outputs_json(char* json);
+void parse_outputs_json(char* json);
 
 /*
  * Initiate the output-list
  *
  */
-void        init_outputs();
+void init_outputs();
 
 /*
  * Returns the output with the given name
  *
  */
-i3_output*  get_output_by_name(char* name);
+i3_output* get_output_by_name(char* name);
 
 struct i3_output {
-       char*           name;         /* Name of the output */
-       bool            active;       /* If the output is active */
-       int             ws;           /* The number of the currently visible ws */
-       rect            rect;         /* The rect (relative to the root-win) */
+    char*          name;          /* Name of the output */
+    bool           active;        /* If the output is active */
+    int            ws;            /* The number of the currently visible ws */
+    rect           rect;          /* The rect (relative to the root-win) */
 
-       xcb_window_t    bar;          /* The id of the bar of the output */
-    xcb_pixmap_t    buffer;       /* An extra pixmap for double-buffering */
-       xcb_gcontext_t  bargc;        /* The graphical context of the bar */
+    xcb_window_t   bar;           /* The id of the bar of the output */
+    xcb_pixmap_t   buffer;        /* An extra pixmap for double-buffering */
+    xcb_gcontext_t bargc;         /* The graphical context of the bar */
 
-       struct ws_head  *workspaces;  /* The workspaces on this output */
+    struct ws_head *workspaces;   /* The workspaces on this output */
+    struct tc_head *trayclients;  /* The tray clients on this output */
 
-       SLIST_ENTRY(i3_output) slist; /* Pointer for the SLIST-Macro */
+    SLIST_ENTRY(i3_output) slist; /* Pointer for the SLIST-Macro */
 };
 
 #endif
diff --git a/i3bar/include/trayclients.h b/i3bar/include/trayclients.h
new file mode 100644 (file)
index 0000000..1113dae
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * i3bar - an xcb-based status- and ws-bar for i3
+ *
+ * © 2010-2011 Axel Wagner and contributors
+ *
+ * See file LICNSE for license information
+ *
+ */
+#ifndef TRAYCLIENT_H_
+#define TRAYCLIENT_H_
+
+#include "common.h"
+
+typedef struct trayclient trayclient;
+
+TAILQ_HEAD(tc_head, trayclient);
+
+struct trayclient {
+    xcb_window_t       win;         /* The window ID of the tray client */
+    bool               mapped;      /* Whether this window is mapped */
+    int                xe_version;  /* The XEMBED version supported by the client */
+
+    TAILQ_ENTRY(trayclient) tailq;  /* Pointer for the TAILQ-Macro */
+};
+
+#endif
index 531fdfe942c76265e0e5b559528b5ccb5e733604..c1b7cc14e3575bd98051ddd3040b89af16fd90aa 100644 (file)
 #include <stdint.h>
 //#include "outputs.h"
 
+#ifdef XCB_COMPAT
+#define XCB_ATOM_CARDINAL CARDINAL
+#endif
+
+#define _NET_SYSTEM_TRAY_ORIENTATION_HORZ 0
+#define _NET_SYSTEM_TRAY_ORIENTATION_VERT 1
+#define SYSTEM_TRAY_REQUEST_DOCK    0
+#define SYSTEM_TRAY_BEGIN_MESSAGE   1
+#define SYSTEM_TRAY_CANCEL_MESSAGE  2
+#define XEMBED_MAPPED                   (1 << 0)
+#define XEMBED_EMBEDDED_NOTIFY         0
+
 struct xcb_color_strings_t {
     char *bar_fg;
     char *bar_bg;
index 5d1688731659a38240ed84b40075e0dd10197e25..b75ceabd538afa3a96aba1fb87fd1a23f06a6403 100644 (file)
@@ -2,4 +2,10 @@ ATOM_DO(_NET_WM_WINDOW_TYPE)
 ATOM_DO(_NET_WM_WINDOW_TYPE_DOCK)
 ATOM_DO(_NET_WM_STRUT_PARTIAL)
 ATOM_DO(I3_SOCKET_PATH)
+ATOM_DO(MANAGER)
+ATOM_DO(_NET_SYSTEM_TRAY_ORIENTATION)
+ATOM_DO(_NET_SYSTEM_TRAY_VISUAL)
+ATOM_DO(_NET_SYSTEM_TRAY_OPCODE)
+ATOM_DO(_XEMBED_INFO)
+ATOM_DO(_XEMBED)
 #undef ATOM_DO
index faab9142ee04d6985f120dc2bd469b8c6f9a2a9d..aa9d65549ca7a5abd259ffbcb6e318b7406111c6 100644 (file)
@@ -90,7 +90,7 @@ void stdin_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) {
         if (rec == buffer_len) {
             buffer_len += STDIN_CHUNK_SIZE;
             buffer = realloc(buffer, buffer_len);
-           }
+        }
     }
     if (*buffer == '\0') {
         FREE(buffer);
index 9daf328d0454463ff482e7bb94eccfdad08de694..464f24a0ea52fa171b5caa1904d6d4708672cd89 100644 (file)
@@ -43,7 +43,7 @@ static int outputs_null_cb(void *params_) {
  * Parse a boolean value (active)
  *
  */
-static int outputs_boolean_cb(void *params_, bool val) {
+static int outputs_boolean_cb(void *params_, int val) {
     struct outputs_json_params *params = (struct outputs_json_params*) params_;
 
     if (strcmp(params->cur_key, "active")) {
@@ -161,6 +161,9 @@ static int outputs_start_map_cb(void *params_) {
         new_output->workspaces = malloc(sizeof(struct ws_head));
         TAILQ_INIT(new_output->workspaces);
 
+        new_output->trayclients = malloc(sizeof(struct tc_head));
+        TAILQ_INIT(new_output->trayclients);
+
         params->outputs_walk = new_output;
 
         return 1;
index 8c79c3f9659b5e23be249b0fd5e3c184e1a139e7..689842276d81eee201d5eb50e92ed9d552141305 100644 (file)
@@ -23,18 +23,18 @@ static iconv_t conversion_descriptor2 = 0;
  *
  */
 char *convert_ucs_to_utf8(char *input) {
-       size_t input_size = 2;
-       /* UTF-8 may consume up to 4 byte */
-       int buffer_size = 8;
+        size_t input_size = 2;
+        /* UTF-8 may consume up to 4 byte */
+        int buffer_size = 8;
 
-       char *buffer = calloc(buffer_size, 1);
+        char *buffer = calloc(buffer_size, 1);
         if (buffer == NULL)
                 err(EXIT_FAILURE, "malloc() failed\n");
-       size_t output_size = buffer_size;
-       /* We need to use an additional pointer, because iconv() modifies it */
-       char *output = buffer;
+        size_t output_size = buffer_size;
+        /* We need to use an additional pointer, because iconv() modifies it */
+        char *output = buffer;
 
-       /* We convert the input into UCS-2 big endian */
+        /* We convert the input into UCS-2 big endian */
         if (conversion_descriptor == 0) {
                 conversion_descriptor = iconv_open("UTF-8", "UCS-2BE");
                 if (conversion_descriptor == 0) {
@@ -43,17 +43,17 @@ char *convert_ucs_to_utf8(char *input) {
                 }
         }
 
-       /* Get the conversion descriptor back to original state */
-       iconv(conversion_descriptor, NULL, NULL, NULL, NULL);
+        /* Get the conversion descriptor back to original state */
+        iconv(conversion_descriptor, NULL, NULL, NULL, NULL);
 
-       /* Convert our text */
-       int rc = iconv(conversion_descriptor, (void*)&input, &input_size, &output, &output_size);
+        /* Convert our text */
+        int rc = iconv(conversion_descriptor, (void*)&input, &input_size, &output, &output_size);
         if (rc == (size_t)-1) {
                 perror("Converting to UCS-2 failed");
                 return NULL;
-       }
+        }
 
-       return buffer;
+        return buffer;
 }
 
 /*
@@ -64,18 +64,18 @@ char *convert_ucs_to_utf8(char *input) {
  *
  */
 char *convert_utf8_to_ucs2(char *input, int *real_strlen) {
-       size_t input_size = strlen(input) + 1;
-       /* UCS-2 consumes exactly two bytes for each glyph */
-       int buffer_size = input_size * 2;
+        size_t input_size = strlen(input) + 1;
+        /* UCS-2 consumes exactly two bytes for each glyph */
+        int buffer_size = input_size * 2;
 
-       char *buffer = malloc(buffer_size);
+        char *buffer = malloc(buffer_size);
         if (buffer == NULL)
                 err(EXIT_FAILURE, "malloc() failed\n");
-       size_t output_size = buffer_size;
-       /* We need to use an additional pointer, because iconv() modifies it */
-       char *output = buffer;
+        size_t output_size = buffer_size;
+        /* We need to use an additional pointer, because iconv() modifies it */
+        char *output = buffer;
 
-       /* We convert the input into UCS-2 big endian */
+        /* We convert the input into UCS-2 big endian */
         if (conversion_descriptor2 == 0) {
                 conversion_descriptor2 = iconv_open("UCS-2BE", "UTF-8");
                 if (conversion_descriptor2 == 0) {
@@ -84,20 +84,20 @@ char *convert_utf8_to_ucs2(char *input, int *real_strlen) {
                 }
         }
 
-       /* Get the conversion descriptor back to original state */
-       iconv(conversion_descriptor2, NULL, NULL, NULL, NULL);
+        /* Get the conversion descriptor back to original state */
+        iconv(conversion_descriptor2, NULL, NULL, NULL, NULL);
 
-       /* Convert our text */
-       int rc = iconv(conversion_descriptor2, (void*)&input, &input_size, &output, &output_size);
+        /* Convert our text */
+        int rc = iconv(conversion_descriptor2, (void*)&input, &input_size, &output, &output_size);
         if (rc == (size_t)-1) {
                 perror("Converting to UCS-2 failed");
                 if (real_strlen != NULL)
-                       *real_strlen = 0;
+                        *real_strlen = 0;
                 return NULL;
-       }
+        }
 
         if (real_strlen != NULL)
-               *real_strlen = ((buffer_size - output_size) / 2) - 1;
+                *real_strlen = ((buffer_size - output_size) / 2) - 1;
 
-       return buffer;
+        return buffer;
 }
index eeb9ca349f459d47a7b0ad9c5eacfa39a5b4c831..a84e152b79e981f2bd71099a27a1a146472716cf 100644 (file)
@@ -29,7 +29,7 @@ struct workspaces_json_params {
  * Parse a boolean value (visible, focused, urgent)
  *
  */
-static int workspaces_boolean_cb(void *params_, bool val) {
+static int workspaces_boolean_cb(void *params_, int val) {
     struct workspaces_json_params *params = (struct workspaces_json_params*) params_;
 
     if (!strcmp(params->cur_key, "visible")) {
index ac48ea5a2e871858e43145b3c1cc489da26baaca..98333113ce80d715ec25f9a689a5f916b12207e6 100644 (file)
@@ -63,6 +63,7 @@ xcb_atom_t               atoms[NUM_ATOMS];
 
 /* Variables, that are the same for all functions at all times */
 xcb_connection_t *xcb_connection;
+int              screen;
 xcb_screen_t     *xcb_screen;
 xcb_window_t     xcb_root;
 xcb_font_t       xcb_font;
@@ -389,6 +390,256 @@ void handle_button(xcb_button_press_event_t *event) {
     i3_send_msg(I3_IPC_MESSAGE_TYPE_COMMAND, buffer);
 }
 
+/*
+ * Configures the x coordinate of all trayclients. To be called after adding a
+ * new tray client or removing an old one.
+ *
+ */
+static void configure_trayclients() {
+    trayclient *trayclient;
+    i3_output *output;
+    SLIST_FOREACH(output, outputs, slist) {
+        if (!output->active)
+            continue;
+
+        int clients = 0;
+        TAILQ_FOREACH_REVERSE(trayclient, output->trayclients, tc_head, tailq) {
+            clients++;
+
+            DLOG("Configuring tray window %08x to x=%d\n",
+                 trayclient->win, output->rect.w - (clients * (font_height + 2)));
+            uint32_t x = output->rect.w - (clients * (font_height + 2));
+            xcb_configure_window(xcb_connection,
+                                 trayclient->win,
+                                 XCB_CONFIG_WINDOW_X,
+                                 &x);
+        }
+    }
+}
+
+/*
+ * Handles ClientMessages (messages sent from another client directly to us).
+ *
+ * At the moment, only the tray window will receive client messages. All
+ * supported client messages currently are _NET_SYSTEM_TRAY_OPCODE.
+ *
+ */
+static void handle_client_message(xcb_client_message_event_t* event) {
+    if (event->type == atoms[_NET_SYSTEM_TRAY_OPCODE] &&
+        event->format == 32) {
+        DLOG("_NET_SYSTEM_TRAY_OPCODE received\n");
+        /* event->data.data32[0] is the timestamp */
+        uint32_t op = event->data.data32[1];
+        uint32_t mask;
+        uint32_t values[2];
+        if (op == SYSTEM_TRAY_REQUEST_DOCK) {
+            xcb_window_t client = event->data.data32[2];
+
+            /* Listen for PropertyNotify events to get the most recent value of
+             * the XEMBED_MAPPED atom, also listen for UnmapNotify events */
+            mask = XCB_CW_EVENT_MASK;
+            values[0] = XCB_EVENT_MASK_PROPERTY_CHANGE |
+                        XCB_EVENT_MASK_STRUCTURE_NOTIFY;
+            xcb_change_window_attributes(xcb_connection,
+                                         client,
+                                         mask,
+                                         values);
+
+            /* Request the _XEMBED_INFO property. The XEMBED specification
+             * (which is referred by the tray specification) says this *has* to
+             * be set, but VLC does not set it… */
+            bool map_it = true;
+            int xe_version = 1;
+            xcb_get_property_cookie_t xembedc;
+            xembedc = xcb_get_property_unchecked(xcb_connection,
+                                                 0,
+                                                 client,
+                                                 atoms[_XEMBED_INFO],
+                                                 XCB_GET_PROPERTY_TYPE_ANY,
+                                                 0,
+                                                 2 * 32);
+
+            xcb_get_property_reply_t *xembedr = xcb_get_property_reply(xcb_connection,
+                                                                       xembedc,
+                                                                       NULL);
+            if (xembedr != NULL && xembedr->length != 0) {
+                DLOG("xembed format = %d, len = %d\n", xembedr->format, xembedr->length);
+                uint32_t *xembed = xcb_get_property_value(xembedr);
+                DLOG("xembed version = %d\n", xembed[0]);
+                DLOG("xembed flags = %d\n", xembed[1]);
+                map_it = ((xembed[1] & XEMBED_MAPPED) == XEMBED_MAPPED);
+                xe_version = xembed[0];
+                if (xe_version > 1)
+                    xe_version = 1;
+                free(xembedr);
+            } else {
+                ELOG("Window %08x violates the XEMBED protocol, _XEMBED_INFO not set\n", client);
+            }
+
+            DLOG("X window %08x requested docking\n", client);
+            i3_output *walk, *output = NULL;
+            SLIST_FOREACH(walk, outputs, slist) {
+                if (!walk->active)
+                    continue;
+                DLOG("using output %s\n", walk->name);
+                output = walk;
+            }
+            if (output == NULL) {
+                ELOG("No output found\n");
+                return;
+            }
+            xcb_reparent_window(xcb_connection,
+                                client,
+                                output->bar,
+                                output->rect.w - font_height - 2,
+                                2);
+            /* We reconfigure the window to use a reasonable size. The systray
+             * specification explicitly says:
+             *   Tray icons may be assigned any size by the system tray, and
+             *   should do their best to cope with any size effectively
+             */
+            mask = XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT;
+            values[0] = font_height;
+            values[1] = font_height;
+            xcb_configure_window(xcb_connection,
+                                 client,
+                                 mask,
+                                 values);
+
+            /* send the XEMBED_EMBEDDED_NOTIFY message */
+            void *event = calloc(32, 1);
+            xcb_client_message_event_t *ev = event;
+            ev->response_type = XCB_CLIENT_MESSAGE;
+            ev->window = client;
+            ev->type = atoms[_XEMBED];
+            ev->format = 32;
+            ev->data.data32[0] = XCB_CURRENT_TIME;
+            ev->data.data32[1] = atoms[XEMBED_EMBEDDED_NOTIFY];
+            ev->data.data32[2] = output->bar;
+            ev->data.data32[3] = xe_version;
+            xcb_send_event(xcb_connection,
+                           0,
+                           client,
+                           XCB_EVENT_MASK_NO_EVENT,
+                           (char*)ev);
+            free(event);
+
+            if (map_it) {
+                DLOG("Mapping dock client\n");
+                xcb_map_window(xcb_connection, client);
+            } else {
+                DLOG("Not mapping dock client yet\n");
+            }
+            trayclient *tc = malloc(sizeof(trayclient));
+            tc->win = client;
+            tc->mapped = map_it;
+            tc->xe_version = xe_version;
+            TAILQ_INSERT_TAIL(output->trayclients, tc, tailq);
+
+            /* Trigger an update to copy the statusline text to the appropriate
+             * position */
+            configure_trayclients();
+            draw_bars();
+        }
+    }
+}
+
+/*
+ * Handles UnmapNotify events. These events happen when a tray window unmaps
+ * itself. We then update our data structure
+ *
+ */
+static void handle_unmap_notify(xcb_unmap_notify_event_t* event) {
+    DLOG("UnmapNotify for window = %08x, event = %08x\n", event->window, event->event);
+
+    i3_output *walk;
+    SLIST_FOREACH(walk, outputs, slist) {
+        if (!walk->active)
+            continue;
+        DLOG("checking output %s\n", walk->name);
+        trayclient *trayclient;
+        TAILQ_FOREACH(trayclient, walk->trayclients, tailq) {
+            if (trayclient->win != event->window)
+                continue;
+
+            DLOG("Removing tray client with window ID %08x\n", event->window);
+            TAILQ_REMOVE(walk->trayclients, trayclient, tailq);
+
+            /* Trigger an update, we now have more space for the statusline */
+            configure_trayclients();
+            draw_bars();
+            return;
+        }
+    }
+}
+
+/*
+ * Handle PropertyNotify messages. Currently only the _XEMBED_INFO property is
+ * handled, which tells us whether a dock client should be mapped or unmapped.
+ *
+ */
+static void handle_property_notify(xcb_property_notify_event_t *event) {
+    DLOG("PropertyNotify\n");
+    if (event->atom == atoms[_XEMBED_INFO] &&
+        event->state == XCB_PROPERTY_NEW_VALUE) {
+        DLOG("xembed_info updated\n");
+        trayclient *trayclient = NULL, *walk;
+        i3_output *o_walk;
+        SLIST_FOREACH(o_walk, outputs, slist) {
+            if (!o_walk->active)
+                continue;
+
+            TAILQ_FOREACH(walk, o_walk->trayclients, tailq) {
+                if (walk->win != event->window)
+                    continue;
+                trayclient = walk;
+                break;
+            }
+
+            if (trayclient)
+                break;
+        }
+        if (!trayclient) {
+            ELOG("PropertyNotify received for unknown window %08x\n",
+                 event->window);
+            return;
+        }
+        xcb_get_property_cookie_t xembedc;
+        xembedc = xcb_get_property_unchecked(xcb_connection,
+                                             0,
+                                             trayclient->win,
+                                             atoms[_XEMBED_INFO],
+                                             XCB_GET_PROPERTY_TYPE_ANY,
+                                             0,
+                                             2 * 32);
+
+        xcb_get_property_reply_t *xembedr = xcb_get_property_reply(xcb_connection,
+                                                                   xembedc,
+                                                                   NULL);
+        if (xembedr == NULL || xembedr->length == 0)
+            return;
+
+        DLOG("xembed format = %d, len = %d\n", xembedr->format, xembedr->length);
+        uint32_t *xembed = xcb_get_property_value(xembedr);
+        DLOG("xembed version = %d\n", xembed[0]);
+        DLOG("xembed flags = %d\n", xembed[1]);
+        bool map_it = ((xembed[1] & XEMBED_MAPPED) == XEMBED_MAPPED);
+        DLOG("map-state now %d\n", map_it);
+        if (trayclient->mapped && !map_it) {
+            /* need to unmap the window */
+            xcb_unmap_window(xcb_connection, trayclient->win);
+            trayclient->mapped = map_it;
+            draw_bars();
+        } else if (!trayclient->mapped && map_it) {
+            /* need to map the window */
+            xcb_map_window(xcb_connection, trayclient->win);
+            trayclient->mapped = map_it;
+            draw_bars();
+        }
+        free(xembedr);
+    }
+}
+
 /*
  * This function is called immediately before the main loop locks. We flush xcb
  * then (and only then)
@@ -425,6 +676,19 @@ void xcb_chk_cb(struct ev_loop *loop, ev_check *watcher, int revents) {
             /* Button-press-events are mouse-buttons clicked on one of our bars */
             handle_button((xcb_button_press_event_t*) event);
             break;
+        case XCB_CLIENT_MESSAGE:
+            /* Client messages are used for client-to-client communication, for
+             * example system tray widgets talk to us directly via client messages. */
+            handle_client_message((xcb_client_message_event_t*) event);
+            break;
+        case XCB_UNMAP_NOTIFY:
+            /* UnmapNotifies are received when a tray window unmaps itself */
+            handle_unmap_notify((xcb_unmap_notify_event_t*) event);
+            break;
+        case XCB_PROPERTY_NOTIFY:
+            /* PropertyNotify */
+            handle_property_notify((xcb_property_notify_event_t*) event);
+            break;
     }
     FREE(event);
 }
@@ -482,7 +746,7 @@ void xkb_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) {
  */
 char *init_xcb(char *fontname) {
     /* FIXME: xcb_connect leaks Memory */
-    xcb_connection = xcb_connect(NULL, NULL);
+    xcb_connection = xcb_connect(NULL, &screen);
     if (xcb_connection_has_error(xcb_connection)) {
         ELOG("Cannot open display\n");
         exit(EXIT_FAILURE);
@@ -646,6 +910,95 @@ char *init_xcb(char *fontname) {
     return path;
 }
 
+/*
+ * Initializes tray support by requesting the appropriate _NET_SYSTEM_TRAY atom
+ * for the X11 display we are running on, then acquiring the selection for this
+ * atom. Afterwards, tray clients will send ClientMessages to our window.
+ *
+ */
+void init_tray() {
+    /* request the tray manager atom for the X11 display we are running on */
+    char atomname[strlen("_NET_SYSTEM_TRAY_S") + 11];
+    snprintf(atomname, strlen("_NET_SYSTEM_TRAY_S") + 11, "_NET_SYSTEM_TRAY_S%d", screen);
+    xcb_intern_atom_cookie_t tray_cookie;
+    xcb_intern_atom_reply_t *tray_reply;
+    tray_cookie = xcb_intern_atom(xcb_connection, 0, strlen(atomname), atomname);
+
+    /* tray support: we need a window to own the selection */
+    xcb_window_t selwin = xcb_generate_id(xcb_connection);
+    uint32_t selmask = XCB_CW_OVERRIDE_REDIRECT;
+    uint32_t selval[] = { 1 };
+    xcb_create_window(xcb_connection,
+                      xcb_screen->root_depth,
+                      selwin,
+                      xcb_root,
+                      -1, -1,
+                      1, 1,
+                      1,
+                      XCB_WINDOW_CLASS_INPUT_OUTPUT,
+                      xcb_screen->root_visual,
+                      selmask,
+                      selval);
+
+    uint32_t orientation = _NET_SYSTEM_TRAY_ORIENTATION_HORZ;
+    /* set the atoms */
+    xcb_change_property(xcb_connection,
+                        XCB_PROP_MODE_REPLACE,
+                        selwin,
+                        atoms[_NET_SYSTEM_TRAY_ORIENTATION],
+                        XCB_ATOM_CARDINAL,
+                        32,
+                        1,
+                        &orientation);
+
+    if (!(tray_reply = xcb_intern_atom_reply(xcb_connection, tray_cookie, NULL))) {
+        ELOG("Could not get atom %s\n", atomname);
+        exit(EXIT_FAILURE);
+    }
+
+    xcb_set_selection_owner(xcb_connection,
+                            selwin,
+                            tray_reply->atom,
+                            XCB_CURRENT_TIME);
+
+    /* Verify that we have the selection */
+    xcb_get_selection_owner_cookie_t selcookie;
+    xcb_get_selection_owner_reply_t *selreply;
+
+    selcookie = xcb_get_selection_owner(xcb_connection, tray_reply->atom);
+    if (!(selreply = xcb_get_selection_owner_reply(xcb_connection, selcookie, NULL))) {
+        ELOG("Could not get selection owner for %s\n", atomname);
+        exit(EXIT_FAILURE);
+    }
+
+    if (selreply->owner != selwin) {
+        ELOG("Could not set the %s selection. " \
+             "Maybe another tray is already running?\n", atomname);
+        /* NOTE that this error is not fatal. We just can’t provide tray
+         * functionality */
+        free(selreply);
+        return;
+    }
+
+    /* Inform clients waiting for a new _NET_SYSTEM_TRAY that we are here */
+    void *event = calloc(32, 1);
+    xcb_client_message_event_t *ev = event;
+    ev->response_type = XCB_CLIENT_MESSAGE;
+    ev->window = xcb_root;
+    ev->type = atoms[MANAGER];
+    ev->format = 32;
+    ev->data.data32[0] = XCB_CURRENT_TIME;
+    ev->data.data32[1] = tray_reply->atom;
+    ev->data.data32[2] = selwin;
+    xcb_send_event(xcb_connection,
+                   0,
+                   xcb_root,
+                   XCB_EVENT_MASK_STRUCTURE_NOTIFY,
+                   (char*)ev);
+    free(event);
+    free(tray_reply);
+}
+
 /*
  * Cleanup the xcb-stuff.
  * Called once, before the program terminates.
@@ -653,15 +1006,27 @@ char *init_xcb(char *fontname) {
  */
 void clean_xcb() {
     i3_output *o_walk;
+    trayclient *trayclient;
     free_workspaces();
     SLIST_FOREACH(o_walk, outputs, slist) {
+        TAILQ_FOREACH(trayclient, o_walk->trayclients, tailq) {
+            /* Unmap, then reparent (to root) the tray client windows */
+            xcb_unmap_window(xcb_connection, trayclient->win);
+            xcb_reparent_window(xcb_connection,
+                                trayclient->win,
+                                xcb_root,
+                                0,
+                                0);
+        }
         destroy_window(o_walk);
+        FREE(o_walk->trayclients);
         FREE(o_walk->workspaces);
         FREE(o_walk->name);
     }
     FREE_SLIST(outputs, i3_output);
     FREE(outputs);
 
+    xcb_flush(xcb_connection);
     xcb_disconnect(xcb_connection);
 
     ev_check_stop(main_loop, xcb_chk);
@@ -771,6 +1136,9 @@ void reconfig_windows() {
         if (walk->bar == XCB_NONE) {
             DLOG("Creating Window for output %s\n", walk->name);
 
+            /* TODO: only call init_tray() if the tray is configured for this output */
+            init_tray();
+
             walk->bar = xcb_generate_id(xcb_connection);
             walk->buffer = xcb_generate_id(xcb_connection);
             mask = XCB_CW_BACK_PIXEL | XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK;
@@ -960,13 +1328,26 @@ void draw_bars() {
             /* Luckily we already prepared a seperate pixmap containing the rendered
              * statusline, we just have to copy the relevant parts to the relevant
              * position */
+            trayclient *trayclient;
+            int traypx = 0;
+            TAILQ_FOREACH(trayclient, outputs_walk->trayclients, tailq) {
+                if (!trayclient->mapped)
+                    continue;
+                /* We assume the tray icons are quadratic (we use the font
+                 * *height* as *width* of the icons) because we configured them
+                 * like this. */
+                traypx += font_height;
+            }
+            /* Add 2px of padding if there are any tray icons */
+            if (traypx > 0)
+                traypx += 2;
             xcb_copy_area(xcb_connection,
                           statusline_pm,
                           outputs_walk->buffer,
                           outputs_walk->bargc,
                           MAX(0, (int16_t)(statusline_width - outputs_walk->rect.w + 4)), 0,
-                          MAX(0, (int16_t)(outputs_walk->rect.w - statusline_width - 4)), 3,
-                          MIN(outputs_walk->rect.w - 4, statusline_width), font_height);
+                          MAX(0, (int16_t)(outputs_walk->rect.w - statusline_width - traypx - 4)), 3,
+                          MIN(outputs_walk->rect.w - traypx - 4, statusline_width), font_height);
         }
 
         if (config.disable_ws) {
index b87be518c1b18193a89a97d11d7bc4b35faa59c9..9c08ebef8e8067c241dfb5f087b45374a5c64cbf 100644 (file)
@@ -64,5 +64,6 @@
 #include "output.h"
 #include "ewmh.h"
 #include "assignments.h"
+#include "regex.h"
 
 #endif
index 300a8abaea48ae0976b1feaacfd9730717e06742..309a3325f8bd3dcc972f0ab21770a32871e7380e 100644 (file)
@@ -21,5 +21,7 @@ xmacro(UTF8_STRING)
 xmacro(WM_STATE)
 xmacro(WM_CLIENT_LEADER)
 xmacro(WM_TAKE_FOCUS)
+xmacro(WM_WINDOW_ROLE)
 xmacro(I3_SOCKET_PATH)
 xmacro(I3_CONFIG_PATH)
+xmacro(I3_SYNC)
index 1021a612e7a98917a211a61d037b688e255c6861..07391a6af6458e4e02d1dfad3c11cf85992f6807 100644 (file)
@@ -123,9 +123,22 @@ struct Config {
          * more often. */
         bool force_focus_wrapping;
 
+        /** By default, use the RandR API for multi-monitor setups.
+         * Unfortunately, the nVidia binary graphics driver doesn't support
+         * this API. Instead, it only support the less powerful Xinerama API,
+         * which can be enabled by this option.
+         *
+         * Note: this option takes only effect on the initial startup (eg.
+         * reconfiguration is not possible). On startup, the list of screens
+         * is fetched once and never updated. */
+        bool force_xinerama;
+
         /** The default border style for new windows. */
         border_style_t default_border;
 
+        /** The default border style for new floating windows. */
+        border_style_t default_floating_border;
+
         /** The modifier which needs to be pressed in combination with your mouse
          * buttons to do things with floating windows (move, resize) */
         uint32_t floating_modifier;
index 5797b7d80d1db633dcb52eb94c4272c9ea55a521..f6052b9f81154ed98aece23cd086e1792dfb33ba 100644 (file)
@@ -10,6 +10,7 @@
 #include <xcb/randr.h>
 #include <xcb/xcb_atom.h>
 #include <stdbool.h>
+#include <pcre.h>
 
 #ifndef _DATA_H
 #define _DATA_H
@@ -137,6 +138,21 @@ struct Ignore_Event {
     SLIST_ENTRY(Ignore_Event) ignore_events;
 };
 
+/**
+ * Regular expression wrapper. It contains the pattern itself as a string (like
+ * ^foo[0-9]$) as well as a pointer to the compiled PCRE expression and the
+ * pcre_extra data returned by pcre_study().
+ *
+ * This makes it easier to have a useful logfile, including the matching or
+ * non-matching pattern.
+ *
+ */
+struct regex {
+    char *pattern;
+    pcre *regex;
+    pcre_extra *extra;
+};
+
 /******************************************************************************
  * Major types
  *****************************************************************************/
@@ -248,6 +264,11 @@ struct Window {
      * application supports _NET_WM_NAME, in COMPOUND_TEXT otherwise). */
     char *name_x;
 
+    /** The WM_WINDOW_ROLE of this window (for example, the pidgin buddy window
+     * sets "buddy list"). Useful to match specific windows in assignments or
+     * for_window. */
+    char *role;
+
     /** Flag to force re-rendering the decoration upon changes */
     bool name_x_changed;
 
@@ -277,12 +298,12 @@ struct Window {
 };
 
 struct Match {
-    char *title;
-    int title_len;
-    char *application;
-    char *class;
-    char *instance;
-    char *mark;
+    struct regex *title;
+    struct regex *application;
+    struct regex *class;
+    struct regex *instance;
+    struct regex *mark;
+    struct regex *role;
     enum {
         M_DONTCHECK = -1,
         M_NODOCK = 0,
index e81f9a155ad5e7254a631c1fb04d7a6a172e6a48..30b2d3044e353cdb4c202a571e6acfde6138326d 100644 (file)
@@ -38,6 +38,8 @@
 /** Requests the tree layout from i3 */
 #define I3_IPC_MESSAGE_TYPE_GET_TREE            4
 
+/** Request the current defined marks from i3 */
+#define I3_IPC_MESSAGE_TYPE_GET_MARKS           5
 
 /*
  * Messages from i3 to clients
@@ -59,6 +61,8 @@
 /** Tree reply type */
 #define I3_IPC_REPLY_TYPE_TREE                  4
 
+/** Marks reply type*/
+#define I3_IPC_REPLY_TYPE_MARKS                 5
 
 /*
  * Events from i3 to clients. Events have the first bit set high.
index 2786c66a8be7792cd25485f48ca7ea971b8d0667..6c0694efeace588462c69fec845d00ba69188261 100644 (file)
@@ -28,4 +28,10 @@ void match_copy(Match *dest, Match *src);
  */
 bool match_matches_window(Match *match, i3Window *window);
 
+/**
+ * Frees the given match. It must not be used afterwards!
+ *
+ */
+void match_free(Match *match);
+
 #endif
diff --git a/include/regex.h b/include/regex.h
new file mode 100644 (file)
index 0000000..adfa665
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ */
+#ifndef _REGEX_H
+#define _REGEX_H
+
+/**
+ * Creates a new 'regex' struct containing the given pattern and a PCRE
+ * compiled regular expression. Also, calls pcre_study because this regex will
+ * most likely be used often (like for every new window and on every relevant
+ * property change of existing windows).
+ *
+ * Returns NULL if the pattern could not be compiled into a regular expression
+ * (and ELOGs an appropriate error message).
+ *
+ */
+struct regex *regex_new(const char *pattern);
+
+/**
+ * Frees the given regular expression. It must not be used afterwards!
+ *
+ */
+void regex_free(struct regex *regex);
+
+/**
+ * Checks if the given regular expression matches the given input and returns
+ * true if it does. In either case, it logs the outcome using LOG(), so it will
+ * be visible without any debug loglevel.
+ *
+ */
+bool regex_matches(struct regex *regex, const char *input);
+
+#endif
diff --git a/include/sd-daemon.h b/include/sd-daemon.h
new file mode 100644 (file)
index 0000000..4b853a1
--- /dev/null
@@ -0,0 +1,265 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+#ifndef foosddaemonhfoo
+#define foosddaemonhfoo
+
+/***
+  Copyright 2010 Lennart Poettering
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation files
+  (the "Software"), to deal in the Software without restriction,
+  including without limitation the rights to use, copy, modify, merge,
+  publish, distribute, sublicense, and/or sell copies of the Software,
+  and to permit persons to whom the Software is furnished to do so,
+  subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+  ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+***/
+
+#include <sys/types.h>
+#include <inttypes.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+  Reference implementation of a few systemd related interfaces for
+  writing daemons. These interfaces are trivial to implement. To
+  simplify porting we provide this reference implementation.
+  Applications are welcome to reimplement the algorithms described
+  here if they do not want to include these two source files.
+
+  The following functionality is provided:
+
+  - Support for logging with log levels on stderr
+  - File descriptor passing for socket-based activation
+  - Daemon startup and status notification
+  - Detection of systemd boots
+
+  You may compile this with -DDISABLE_SYSTEMD to disable systemd
+  support. This makes all those calls NOPs that are directly related to
+  systemd (i.e. only sd_is_xxx() will stay useful).
+
+  Since this is drop-in code we don't want any of our symbols to be
+  exported in any case. Hence we declare hidden visibility for all of
+  them.
+
+  You may find an up-to-date version of these source files online:
+
+  http://cgit.freedesktop.org/systemd/plain/src/sd-daemon.h
+  http://cgit.freedesktop.org/systemd/plain/src/sd-daemon.c
+
+  This should compile on non-Linux systems, too, but with the
+  exception of the sd_is_xxx() calls all functions will become NOPs.
+
+  See sd-daemon(7) for more information.
+*/
+
+#ifndef _sd_printf_attr_
+#if __GNUC__ >= 4
+#define _sd_printf_attr_(a,b) __attribute__ ((format (printf, a, b)))
+#else
+#define _sd_printf_attr_(a,b)
+#endif
+#endif
+
+#ifndef _sd_hidden_
+#if (__GNUC__ >= 4) && !defined(SD_EXPORT_SYMBOLS)
+#define _sd_hidden_ __attribute__ ((visibility("hidden")))
+#else
+#define _sd_hidden_
+#endif
+#endif
+
+/*
+  Log levels for usage on stderr:
+
+          fprintf(stderr, SD_NOTICE "Hello World!\n");
+
+  This is similar to printk() usage in the kernel.
+*/
+#define SD_EMERG   "<0>"  /* system is unusable */
+#define SD_ALERT   "<1>"  /* action must be taken immediately */
+#define SD_CRIT    "<2>"  /* critical conditions */
+#define SD_ERR     "<3>"  /* error conditions */
+#define SD_WARNING "<4>"  /* warning conditions */
+#define SD_NOTICE  "<5>"  /* normal but significant condition */
+#define SD_INFO    "<6>"  /* informational */
+#define SD_DEBUG   "<7>"  /* debug-level messages */
+
+/* The first passed file descriptor is fd 3 */
+#define SD_LISTEN_FDS_START 3
+
+/*
+  Returns how many file descriptors have been passed, or a negative
+  errno code on failure. Optionally, removes the $LISTEN_FDS and
+  $LISTEN_PID file descriptors from the environment (recommended, but
+  problematic in threaded environments). If r is the return value of
+  this function you'll find the file descriptors passed as fds
+  SD_LISTEN_FDS_START to SD_LISTEN_FDS_START+r-1. Returns a negative
+  errno style error code on failure. This function call ensures that
+  the FD_CLOEXEC flag is set for the passed file descriptors, to make
+  sure they are not passed on to child processes. If FD_CLOEXEC shall
+  not be set, the caller needs to unset it after this call for all file
+  descriptors that are used.
+
+  See sd_listen_fds(3) for more information.
+*/
+int sd_listen_fds(int unset_environment) _sd_hidden_;
+
+/*
+  Helper call for identifying a passed file descriptor. Returns 1 if
+  the file descriptor is a FIFO in the file system stored under the
+  specified path, 0 otherwise. If path is NULL a path name check will
+  not be done and the call only verifies if the file descriptor
+  refers to a FIFO. Returns a negative errno style error code on
+  failure.
+
+  See sd_is_fifo(3) for more information.
+*/
+int sd_is_fifo(int fd, const char *path) _sd_hidden_;
+
+/*
+  Helper call for identifying a passed file descriptor. Returns 1 if
+  the file descriptor is a socket of the specified family (AF_INET,
+  ...) and type (SOCK_DGRAM, SOCK_STREAM, ...), 0 otherwise. If
+  family is 0 a socket family check will not be done. If type is 0 a
+  socket type check will not be done and the call only verifies if
+  the file descriptor refers to a socket. If listening is > 0 it is
+  verified that the socket is in listening mode. (i.e. listen() has
+  been called) If listening is == 0 it is verified that the socket is
+  not in listening mode. If listening is < 0 no listening mode check
+  is done. Returns a negative errno style error code on failure.
+
+  See sd_is_socket(3) for more information.
+*/
+int sd_is_socket(int fd, int family, int type, int listening) _sd_hidden_;
+
+/*
+  Helper call for identifying a passed file descriptor. Returns 1 if
+  the file descriptor is an Internet socket, of the specified family
+  (either AF_INET or AF_INET6) and the specified type (SOCK_DGRAM,
+  SOCK_STREAM, ...), 0 otherwise. If version is 0 a protocol version
+  check is not done. If type is 0 a socket type check will not be
+  done. If port is 0 a socket port check will not be done. The
+  listening flag is used the same way as in sd_is_socket(). Returns a
+  negative errno style error code on failure.
+
+  See sd_is_socket_inet(3) for more information.
+*/
+int sd_is_socket_inet(int fd, int family, int type, int listening, uint16_t port) _sd_hidden_;
+
+/*
+  Helper call for identifying a passed file descriptor. Returns 1 if
+  the file descriptor is an AF_UNIX socket of the specified type
+  (SOCK_DGRAM, SOCK_STREAM, ...) and path, 0 otherwise. If type is 0
+  a socket type check will not be done. If path is NULL a socket path
+  check will not be done. For normal AF_UNIX sockets set length to
+  0. For abstract namespace sockets set length to the length of the
+  socket name (including the initial 0 byte), and pass the full
+  socket path in path (including the initial 0 byte). The listening
+  flag is used the same way as in sd_is_socket(). Returns a negative
+  errno style error code on failure.
+
+  See sd_is_socket_unix(3) for more information.
+*/
+int sd_is_socket_unix(int fd, int type, int listening, const char *path, size_t length) _sd_hidden_;
+
+/*
+  Informs systemd about changed daemon state. This takes a number of
+  newline separated environment-style variable assignments in a
+  string. The following variables are known:
+
+     READY=1      Tells systemd that daemon startup is finished (only
+                  relevant for services of Type=notify). The passed
+                  argument is a boolean "1" or "0". Since there is
+                  little value in signaling non-readiness the only
+                  value daemons should send is "READY=1".
+
+     STATUS=...   Passes a single-line status string back to systemd
+                  that describes the daemon state. This is free-from
+                  and can be used for various purposes: general state
+                  feedback, fsck-like programs could pass completion
+                  percentages and failing programs could pass a human
+                  readable error message. Example: "STATUS=Completed
+                  66% of file system check..."
+
+     ERRNO=...    If a daemon fails, the errno-style error code,
+                  formatted as string. Example: "ERRNO=2" for ENOENT.
+
+     BUSERROR=... If a daemon fails, the D-Bus error-style error
+                  code. Example: "BUSERROR=org.freedesktop.DBus.Error.TimedOut"
+
+     MAINPID=...  The main pid of a daemon, in case systemd did not
+                  fork off the process itself. Example: "MAINPID=4711"
+
+  Daemons can choose to send additional variables. However, it is
+  recommended to prefix variable names not listed above with X_.
+
+  Returns a negative errno-style error code on failure. Returns > 0
+  if systemd could be notified, 0 if it couldn't possibly because
+  systemd is not running.
+
+  Example: When a daemon finished starting up, it could issue this
+  call to notify systemd about it:
+
+     sd_notify(0, "READY=1");
+
+  See sd_notifyf() for more complete examples.
+
+  See sd_notify(3) for more information.
+*/
+int sd_notify(int unset_environment, const char *state) _sd_hidden_;
+
+/*
+  Similar to sd_notify() but takes a format string.
+
+  Example 1: A daemon could send the following after initialization:
+
+     sd_notifyf(0, "READY=1\n"
+                   "STATUS=Processing requests...\n"
+                   "MAINPID=%lu",
+                   (unsigned long) getpid());
+
+  Example 2: A daemon could send the following shortly before
+  exiting, on failure:
+
+     sd_notifyf(0, "STATUS=Failed to start up: %s\n"
+                   "ERRNO=%i",
+                   strerror(errno),
+                   errno);
+
+  See sd_notifyf(3) for more information.
+*/
+int sd_notifyf(int unset_environment, const char *format, ...) _sd_printf_attr_(2,3) _sd_hidden_;
+
+/*
+  Returns > 0 if the system was booted with systemd. Returns < 0 on
+  error. Returns 0 if the system was not booted with systemd. Note
+  that all of the functions above handle non-systemd boots just
+  fine. You should NOT protect them with a call to this function. Also
+  note that this function checks whether the system, not the user
+  session is controlled by systemd. However the functions above work
+  for both user and system services.
+
+  See sd_booted(3) for more information.
+*/
+int sd_booted(void) _sd_hidden_;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
index fe282aa08208f2bc8d9e55888b3b1575fb041852..cb9fbbc50c9072b359a3d5f2121dc565a88e6834 100644 (file)
@@ -42,4 +42,10 @@ void window_update_transient_for(i3Window *win, xcb_get_property_reply_t *prop);
  */
 void window_update_strut_partial(i3Window *win, xcb_get_property_reply_t *prop);
 
+/**
+ * Updates the WM_WINDOW_ROLE
+ *
+ */
+void window_update_role(i3Window *win, xcb_get_property_reply_t *prop, bool before_mgmt);
+
 #endif
index cd85c92c622e62d0b82a50c43128e12cb57882ea..df7c4d9ce82bd85845230acc959b9829d71c25fb 100644 (file)
@@ -1,7 +1,7 @@
 i3-input(1)
 =========
 Michael Stapelberg <michael+i3@stapelberg.de>
-v3.delta, November 2009
+v4.1, September 2011
 
 == NAME
 
@@ -9,7 +9,7 @@ i3-input - interactively take a command for i3 window manager
 
 == SYNOPSIS
 
-i3-input [-s <socket>] [-p <prefix>] [-l <limit>] [-P <prompt>] [-f <font>] [-v]
+i3-input [-s <socket>] [-F <format>] [-l <limit>] [-P <prompt>] [-f <font>] [-v]
 
 == DESCRIPTION
 
@@ -17,19 +17,28 @@ i3-input is a tool to take commands (or parts of a command) composed by
 the user, and send it/them to i3. This is useful, for example, for the
 mark/goto command.
 
+The -F option takes a format string. In this string, every occurence of %s is
+replaced by the user input.
+
 == EXAMPLE
 
 ------------------------------------------------
-i3-input -p 'mark ' -l 1 -P 'Mark: '
+i3-input -F 'mark %s' -l 1 -P 'Mark: '
 ------------------------------------------------
 
 == ENVIRONMENT
 
 === I3SOCK
 
-If no ipc-socket is specified on the commandline, this variable is used
-to determine the path, at wich the unix domain socket is expected, on which
-to connect to i3.
+i3-input handles the different sources of socket paths in the following order:
+
+* I3SOCK environment variable
+* I3SOCK gets overwritten by the -s parameter, if specified
+* if neither are available, i3-input reads the socket path from the X11
+  property, which is the recommended way
+* if everything fails, i3-input tries +/tmp/i3-ipc.sock+
+
+The socket path is necessary to connect to i3 and actually issue the command.
 
 == SEE ALSO
 
index 4cf1a1c3777a5fd1f7a115c7cf85b3123764e055..dad5a915520dd60bfe10c9577c0587a180b92d27 100644 (file)
@@ -75,6 +75,14 @@ EOL     (\r?\n)
 
 
 <FOR_WINDOW_COND>"]"            { yy_pop_state(); return ']'; }
+<ASSIGN_COND>"["                {
+                                  /* this is the case for the new assign syntax
+                                   * that uses criteria */
+                                  yy_pop_state();
+                                  yy_push_state(FOR_WINDOW_COND);
+                                  /* afterwards we will be in ASSIGN_TARGET_COND */
+                                  return '[';
+                                }
 <EAT_WHITESPACE>[ \t]*          { yy_pop_state(); }
 <WANT_QSTRING>\"[^\"]+\"        {
                                   yy_pop_state();
@@ -98,13 +106,6 @@ bindsym                         { yy_push_state(BINDSYM_COND); yy_push_state(EAT
 floating_modifier               { BEGIN(INITIAL); return TOKFLOATING_MODIFIER; }
 workspace                       { BEGIN(INITIAL); return TOKWORKSPACE; }
 output                          { yy_push_state(OUTPUT_COND); yy_push_state(EAT_WHITESPACE); return TOKOUTPUT; }
-screen                          {
-                                  /* for compatibility until v3.φ */
-                                  ELOG("Assignments to screens are DEPRECATED and will not work. " \
-                                       "Please replace them with assignments to outputs.\n");
-                                  yy_push_state(OUTPUT_COND); yy_push_state(EAT_WHITESPACE);
-                                  return TOKOUTPUT;
-                                }
 terminal                        { WS_STRING; return TOKTERMINAL; }
 font                            { WS_STRING; return TOKFONT; }
 assign                          { yy_push_state(ASSIGN_TARGET_COND); yy_push_state(ASSIGN_COND); return TOKASSIGN; }
@@ -118,11 +119,13 @@ vertical                        { return TOK_VERT; }
 auto                            { return TOK_AUTO; }
 workspace_layout                { return TOK_WORKSPACE_LAYOUT; }
 new_window                      { return TOKNEWWINDOW; }
+new_float                       { return TOKNEWFLOAT; }
 normal                          { return TOK_NORMAL; }
 none                            { return TOK_NONE; }
 1pixel                          { return TOK_1PIXEL; }
 focus_follows_mouse             { return TOKFOCUSFOLLOWSMOUSE; }
 force_focus_wrapping            { return TOK_FORCE_FOCUS_WRAPPING; }
+force_xinerama                  { return TOK_FORCE_XINERAMA; }
 workspace_bar                   { return TOKWORKSPACEBAR; }
 popup_during_fullscreen         { return TOK_POPUP_DURING_FULLSCREEN; }
 ignore                          { return TOK_IGNORE; }
@@ -168,6 +171,7 @@ shift                           { return TOKSHIFT; }
 
 class                           { yy_push_state(WANT_QSTRING); return TOK_CLASS; }
 instance                        { yy_push_state(WANT_QSTRING); return TOK_INSTANCE; }
+window_role                     { yy_push_state(WANT_QSTRING); return TOK_WINDOW_ROLE; }
 id                              { yy_push_state(WANT_QSTRING); return TOK_ID; }
 con_id                          { yy_push_state(WANT_QSTRING); return TOK_CON_ID; }
 con_mark                        { yy_push_state(WANT_QSTRING); return TOK_MARK; }
@@ -193,7 +197,7 @@ title                           { yy_push_state(WANT_QSTRING); return TOK_TITLE;
                                   yylval.string = copy;
                                   return QUOTEDSTRING;
                                 }
-<ASSIGN_COND>[^ \t\"]+          { BEGIN(ASSIGN_TARGET_COND); yylval.string = sstrdup(yytext); return STR_NG; }
+<ASSIGN_COND>[^ \t\"\[]+        { BEGIN(ASSIGN_TARGET_COND); yylval.string = sstrdup(yytext); return STR_NG; }
 <BINDSYM_COND>[a-zA-Z0-9_]+     { yylval.string = sstrdup(yytext); return WORD; }
 [a-zA-Z]+                       { yylval.string = sstrdup(yytext); return WORD; }
 .                               { return (int)yytext[0]; }
index dc29860cda021f1f23552ed0bfce8cfaf25aa2d6..9a417f2aba0b54aeb65cd059f9947d756e617c6d 100644 (file)
@@ -240,6 +240,23 @@ static void nagbar_exited(EV_P_ ev_child *watcher, int revents) {
     configerror_pid = -1;
 }
 
+/* We need ev >= 4 for the following code. Since it is not *that* important (it
+ * only makes sure that there are no i3-nagbar instances left behind) we still
+ * support old systems with libev 3. */
+#if EV_VERSION_MAJOR >= 4
+/*
+ * Cleanup handler. Will be called when i3 exits. Kills i3-nagbar with signal
+ * SIGKILL (9) to make sure there are no left-over i3-nagbar processes.
+ *
+ */
+static void nagbar_cleanup(EV_P_ ev_cleanup *watcher, int revent) {
+    if (configerror_pid != -1) {
+        LOG("Sending SIGKILL (9) to i3-nagbar with PID %d\n", configerror_pid);
+        kill(configerror_pid, SIGKILL);
+    }
+}
+#endif
+
 /*
  * Starts an i3-nagbar process which alerts the user that his configuration
  * file contains one or more errors. Also offers two buttons: One to launch an
@@ -259,9 +276,9 @@ static void start_configerror_nagbar(const char *config_path) {
     if (configerror_pid == 0) {
         char *editaction,
              *pageraction;
-        if (asprintf(&editaction, TERM_EMU " -e sh -c \"${EDITOR:-vi} \"%s\" && i3-msg reload\"", config_path) == -1)
+        if (asprintf(&editaction, "i3-sensible-terminal -e sh -c \"i3-sensible-editor \\\"%s\\\" && i3-msg reload\"", config_path) == -1)
             exit(1);
-        if (asprintf(&pageraction, TERM_EMU " -e sh -c \"${PAGER:-less} \"%s\"\"", errorfilename) == -1)
+        if (asprintf(&pageraction, "i3-sensible-terminal -e i3-sensible-pager \"%s\"", errorfilename) == -1)
             exit(1);
         char *argv[] = {
             NULL, /* will be replaced by the executable path */
@@ -283,6 +300,17 @@ static void start_configerror_nagbar(const char *config_path) {
     ev_child *child = smalloc(sizeof(ev_child));
     ev_child_init(child, &nagbar_exited, configerror_pid, 0);
     ev_child_start(main_loop, child);
+
+/* We need ev >= 4 for the following code. Since it is not *that* important (it
+ * only makes sure that there are no i3-nagbar instances left behind) we still
+ * support old systems with libev 3. */
+#if EV_VERSION_MAJOR >= 4
+    /* install a cleanup watcher (will be called when i3 exits and i3-nagbar is
+     * still running) */
+    ev_cleanup *cleanup = smalloc(sizeof(ev_cleanup));
+    ev_cleanup_init(cleanup, nagbar_cleanup);
+    ev_cleanup_start(main_loop, cleanup);
+#endif
 }
 
 /*
@@ -311,6 +339,53 @@ void kill_configerror_nagbar(bool wait_for_it) {
     waitpid(configerror_pid, NULL, 0);
 }
 
+/*
+ * Checks for duplicate key bindings (the same keycode or keysym is configured
+ * more than once). If a duplicate binding is found, a message is printed to
+ * stderr and the has_errors variable is set to true, which will start
+ * i3-nagbar.
+ *
+ */
+static void check_for_duplicate_bindings(struct context *context) {
+    Binding *bind, *current;
+    TAILQ_FOREACH(current, bindings, bindings) {
+        TAILQ_FOREACH(bind, bindings, bindings) {
+            /* Abort when we reach the current keybinding, only check the
+             * bindings before */
+            if (bind == current)
+                break;
+
+            /* Check if one is using keysym while the other is using bindsym.
+             * If so, skip. */
+            /* XXX: It should be checked at a later place (when translating the
+             * keysym to keycodes) if there are any duplicates */
+            if ((bind->symbol == NULL && current->symbol != NULL) ||
+                (bind->symbol != NULL && current->symbol == NULL))
+                continue;
+
+            /* If bind is NULL, current has to be NULL, too (see above).
+             * If the keycodes differ, it can't be a duplicate. */
+            if (bind->symbol != NULL &&
+                strcasecmp(bind->symbol, current->symbol) != 0)
+                continue;
+
+            /* Check if the keycodes or modifiers are different. If so, they
+             * can't be duplicate */
+            if (bind->keycode != current->keycode ||
+                bind->mods != current->mods)
+                continue;
+            context->has_errors = true;
+            if (current->keycode != 0) {
+                ELOG("Duplicate keybinding in config file:\n  modmask %d with keycode %d, command \"%s\"\n",
+                     current->mods, current->keycode, current->command);
+            } else {
+                ELOG("Duplicate keybinding in config file:\n  modmask %d with keysym %s, command \"%s\"\n",
+                     current->mods, current->symbol, current->command);
+            }
+        }
+    }
+}
+
 void parse_file(const char *f) {
     SLIST_HEAD(variables_head, Variable) variables = SLIST_HEAD_INITIALIZER(&variables);
     int fd, ret, read_bytes = 0;
@@ -472,6 +547,8 @@ void parse_file(const char *f) {
         exit(1);
     }
 
+    check_for_duplicate_bindings(context);
+
     if (context->has_errors) {
         start_configerror_nagbar(f);
     }
@@ -539,11 +616,13 @@ void parse_file(const char *f) {
 %token                  TOK_AUTO                    "auto"
 %token                  TOK_WORKSPACE_LAYOUT        "workspace_layout"
 %token                  TOKNEWWINDOW                "new_window"
+%token                  TOKNEWFLOAT                 "new_float"
 %token                  TOK_NORMAL                  "normal"
 %token                  TOK_NONE                    "none"
 %token                  TOK_1PIXEL                  "1pixel"
 %token                  TOKFOCUSFOLLOWSMOUSE        "focus_follows_mouse"
 %token                  TOK_FORCE_FOCUS_WRAPPING    "force_focus_wrapping"
+%token                  TOK_FORCE_XINERAMA          "force_xinerama"
 %token                  TOKWORKSPACEBAR             "workspace_bar"
 %token                  TOK_DEFAULT                 "default"
 %token                  TOK_STACKING                "stacking"
@@ -557,6 +636,7 @@ void parse_file(const char *f) {
 %token              TOK_MARK            "mark"
 %token              TOK_CLASS           "class"
 %token              TOK_INSTANCE        "instance"
+%token              TOK_WINDOW_ROLE     "window_role"
 %token              TOK_ID              "id"
 %token              TOK_CON_ID          "con_id"
 %token              TOK_TITLE           "title"
@@ -570,6 +650,7 @@ void parse_file(const char *f) {
 %type   <number>        layout_mode
 %type   <number>        border_style
 %type   <number>        new_window
+%type   <number>        new_float
 %type   <number>        colorpixel
 %type   <number>        bool
 %type   <number>        popup_setting
@@ -594,8 +675,10 @@ line:
     | orientation
     | workspace_layout
     | new_window
+    | new_float
     | focus_follows_mouse
     | force_focus_wrapping
+    | force_xinerama
     | workspace_bar
     | workspace
     | assign
@@ -706,12 +789,20 @@ criterion:
     TOK_CLASS '=' STR
     {
         printf("criteria: class = %s\n", $3);
-        current_match.class = $3;
+        current_match.class = regex_new($3);
+        free($3);
     }
     | TOK_INSTANCE '=' STR
     {
         printf("criteria: instance = %s\n", $3);
-        current_match.instance = $3;
+        current_match.instance = regex_new($3);
+        free($3);
+    }
+    | TOK_WINDOW_ROLE '=' STR
+    {
+        printf("criteria: window_role = %s\n", $3);
+        current_match.role = regex_new($3);
+        free($3);
     }
     | TOK_CON_ID '=' STR
     {
@@ -746,12 +837,14 @@ criterion:
     | TOK_MARK '=' STR
     {
         printf("criteria: mark = %s\n", $3);
-        current_match.mark = $3;
+        current_match.mark = regex_new($3);
+        free($3);
     }
     | TOK_TITLE '=' STR
     {
         printf("criteria: title = %s\n", $3);
-        current_match.title = $3;
+        current_match.title = regex_new($3);
+        free($3);
     }
     ;
 
@@ -888,6 +981,14 @@ new_window:
     }
     ;
 
+new_float:
+    TOKNEWFLOAT border_style
+    {
+       DLOG("new floating windows should start with border style %d\n", $2);
+       config.default_floating_border = $2;
+    }
+    ;
+
 border_style:
     TOK_NORMAL      { $$ = BS_NORMAL; }
     | TOK_NONE      { $$ = BS_NONE; }
@@ -926,6 +1027,14 @@ force_focus_wrapping:
     }
     ;
 
+force_xinerama:
+    TOK_FORCE_XINERAMA bool
+    {
+        DLOG("force xinerama = %d\n", $2);
+        config.force_xinerama = $2;
+    }
+    ;
+
 workspace_bar:
     TOKWORKSPACEBAR bool
     {
@@ -1001,6 +1110,13 @@ workspace_name:
 assign:
     TOKASSIGN window_class STR
     {
+        /* This is the old, deprecated form of assignments. It’s provided for
+         * compatibility in version (4.1, 4.2, 4.3) and will be removed
+         * afterwards. It triggers an i3-nagbar warning starting from 4.1. */
+        ELOG("You are using the old assign syntax (without criteria). "
+             "Please see the User's Guide for the new syntax and fix "
+             "your config file.\n");
+        context->has_errors = true;
         printf("assignment of %s to *%s*\n", $2, $3);
         char *workspace = $3;
         char *criteria = $2;
@@ -1012,15 +1128,27 @@ assign:
         char *separator = NULL;
         if ((separator = strchr(criteria, '/')) != NULL) {
             *(separator++) = '\0';
-            match->title = sstrdup(separator);
+            char *pattern;
+            if (asprintf(&pattern, "(?i)%s", separator) == -1) {
+                ELOG("asprintf failed\n");
+                break;
+            }
+            match->title = regex_new(pattern);
+            free(pattern);
+            printf("  title = %s\n", separator);
+        }
+        if (*criteria != '\0') {
+            char *pattern;
+            if (asprintf(&pattern, "(?i)%s", criteria) == -1) {
+                ELOG("asprintf failed\n");
+                break;
+            }
+            match->class = regex_new(pattern);
+            free(pattern);
+            printf("  class = %s\n", criteria);
         }
-        if (*criteria != '\0')
-            match->class = sstrdup(criteria);
         free(criteria);
 
-        printf("  class = %s\n", match->class);
-        printf("  title = %s\n", match->title);
-
         /* Compatibility with older versions: If the assignment target starts
          * with ~, we create the equivalent of:
          *
@@ -1048,6 +1176,19 @@ assign:
         assignment->dest.workspace = workspace;
         TAILQ_INSERT_TAIL(&assignments, assignment, assignments);
     }
+    | TOKASSIGN match STR
+    {
+        if (match_is_empty(&current_match)) {
+            ELOG("Match is empty, ignoring this assignment\n");
+            break;
+        }
+        printf("new assignment, using above criteria, to workspace %s\n", $3);
+        Assignment *assignment = scalloc(sizeof(Assignment));
+        assignment->match = current_match;
+        assignment->type = A_TO_WORKSPACE;
+        assignment->dest.workspace = $3;
+        TAILQ_INSERT_TAIL(&assignments, assignment, assignments);
+    }
     ;
 
 window_class:
index 6c756b0d42850db11a014c5b1838ec3e21704ec4..968b7e52e512b22845e1ae99ef461e16d193726d 100644 (file)
@@ -123,6 +123,7 @@ floating                        { return TOK_FLOATING; }
 toggle                          { return TOK_TOGGLE; }
 mode_toggle                     { return TOK_MODE_TOGGLE; }
 workspace                       { WS_STRING; return TOK_WORKSPACE; }
+output                          { WS_STRING; return TOK_OUTPUT; }
 focus                           { return TOK_FOCUS; }
 move                            { return TOK_MOVE; }
 open                            { return TOK_OPEN; }
@@ -154,6 +155,7 @@ no                              { return TOK_DISABLE; }
 
 class                           { BEGIN(WANT_QSTRING); return TOK_CLASS; }
 instance                        { BEGIN(WANT_QSTRING); return TOK_INSTANCE; }
+window_role                     { BEGIN(WANT_QSTRING); return TOK_WINDOW_ROLE; }
 id                              { BEGIN(WANT_QSTRING); return TOK_ID; }
 con_id                          { BEGIN(WANT_QSTRING); return TOK_CON_ID; }
 con_mark                        { BEGIN(WANT_QSTRING); return TOK_MARK; }
index bf2fde961c46876bf92135da72bbf6b4cb4e8869..04e8b3ca3f4cb0a8f66337a2132b5e2ceb90223f 100644 (file)
@@ -149,6 +149,7 @@ bool definitelyGreaterThan(float a, float b, float epsilon) {
 %token              TOK_ENABLE          "enable"
 %token              TOK_DISABLE         "disable"
 %token              TOK_WORKSPACE       "workspace"
+%token              TOK_OUTPUT          "output"
 %token              TOK_TOGGLE          "toggle"
 %token              TOK_FOCUS           "focus"
 %token              TOK_MOVE            "move"
@@ -176,6 +177,7 @@ bool definitelyGreaterThan(float a, float b, float epsilon) {
 
 %token              TOK_CLASS           "class"
 %token              TOK_INSTANCE        "instance"
+%token              TOK_WINDOW_ROLE     "window_role"
 %token              TOK_ID              "id"
 %token              TOK_CON_ID          "con_id"
 %token              TOK_TITLE           "title"
@@ -266,10 +268,9 @@ matchend:
 
                 }
             } else if (current_match.mark != NULL && current->con->mark != NULL &&
-                    strcasecmp(current_match.mark, current->con->mark) == 0) {
+                       regex_matches(current_match.mark, current->con->mark)) {
                 printf("match by mark\n");
-                    TAILQ_INSERT_TAIL(&owindows, current, owindows);
-
+                TAILQ_INSERT_TAIL(&owindows, current, owindows);
             } else {
                 if (current->con->window == NULL)
                     continue;
@@ -299,12 +300,20 @@ criterion:
     TOK_CLASS '=' STR
     {
         printf("criteria: class = %s\n", $3);
-        current_match.class = $3;
+        current_match.class = regex_new($3);
+        free($3);
     }
     | TOK_INSTANCE '=' STR
     {
         printf("criteria: instance = %s\n", $3);
-        current_match.instance = $3;
+        current_match.instance = regex_new($3);
+        free($3);
+    }
+    | TOK_WINDOW_ROLE '=' STR
+    {
+        printf("criteria: window_role = %s\n", $3);
+        current_match.role = regex_new($3);
+        free($3);
     }
     | TOK_CON_ID '=' STR
     {
@@ -339,12 +348,14 @@ criterion:
     | TOK_MARK '=' STR
     {
         printf("criteria: mark = %s\n", $3);
-        current_match.mark = $3;
+        current_match.mark = regex_new($3);
+        free($3);
     }
     | TOK_TITLE '=' STR
     {
         printf("criteria: title = %s\n", $3);
-        current_match.title = $3;
+        current_match.title = regex_new($3);
+        free($3);
     }
     ;
 
@@ -704,6 +715,51 @@ move:
 
         tree_render();
     }
+    | TOK_MOVE TOK_OUTPUT STR
+    {
+        owindow *current;
+
+        printf("should move window to output %s", $3);
+
+        HANDLE_EMPTY_MATCH;
+
+        /* get the output */
+        Output *current_output = NULL;
+        Output *output;
+
+        TAILQ_FOREACH(current, &owindows, owindows)
+            current_output = get_output_containing(current->con->rect.x, current->con->rect.y);
+
+        assert(current_output != NULL);
+
+        if (strcasecmp($3, "up") == 0)
+            output = get_output_next(D_UP, current_output);
+        else if (strcasecmp($3, "down") == 0)
+            output = get_output_next(D_DOWN, current_output);
+        else if (strcasecmp($3, "left") == 0)
+            output = get_output_next(D_LEFT, current_output);
+        else if (strcasecmp($3, "right") == 0)
+            output = get_output_next(D_RIGHT, current_output);
+        else
+            output = get_output_by_name($3);
+        free($3);
+
+        if (!output)
+            break;
+
+        /* get visible workspace on output */
+        Con *ws = NULL;
+        GREP_FIRST(ws, output_get_content(output->con), workspace_is_visible(child));
+        if (!ws)
+            break;
+
+        TAILQ_FOREACH(current, &owindows, owindows) {
+            printf("matching: %p / %s\n", current->con, current->con->name);
+            con_move_to_workspace(current->con, ws, true, false);
+        }
+
+        tree_render();
+    }
     ;
 
 append_layout:
@@ -811,6 +867,17 @@ resize:
             double percentage = 1.0 / children;
             LOG("default percentage = %f\n", percentage);
 
+            orientation_t orientation = current->parent->orientation;
+
+            if ((orientation == HORIZ &&
+                 (direction == TOK_UP || direction == TOK_DOWN)) ||
+                (orientation == VERT &&
+                 (direction == TOK_LEFT || direction == TOK_RIGHT))) {
+                LOG("You cannot resize in that direction. Your focus is in a %s split container currently.\n",
+                    (orientation == HORIZ ? "horizontal" : "vertical"));
+                break;
+            }
+
             if (direction == TOK_UP || direction == TOK_LEFT) {
                 other = TAILQ_PREV(current, nodes_head, nodes);
             } else {
@@ -818,7 +885,7 @@ resize:
             }
             if (other == TAILQ_END(workspaces)) {
                 LOG("No other container in this direction found, cannot resize.\n");
-                return 0;
+                break;
             }
             LOG("other->percent = %f\n", other->percent);
             LOG("current->percent before = %f\n", current->percent);
index 14fc6e025922c924f9c11f8f0f80493e44b1ad37..c979d8cdbfcc83ec5bda074a375611688aec4beb 100644 (file)
@@ -257,111 +257,116 @@ static void parse_configuration(const char *override_configpath) {
  *
  */
 void load_configuration(xcb_connection_t *conn, const char *override_configpath, bool reload) {
-        if (reload) {
-                /* First ungrab the keys */
-                ungrab_all_keys(conn);
+    if (reload) {
+        /* First ungrab the keys */
+        ungrab_all_keys(conn);
 
-                struct Mode *mode;
-                Binding *bind;
-                while (!SLIST_EMPTY(&modes)) {
-                        mode = SLIST_FIRST(&modes);
-                        FREE(mode->name);
-
-                        /* Clear the old binding list */
-                        bindings = mode->bindings;
-                        while (!TAILQ_EMPTY(bindings)) {
-                                bind = TAILQ_FIRST(bindings);
-                                TAILQ_REMOVE(bindings, bind, bindings);
-                                FREE(bind->translated_to);
-                                FREE(bind->command);
-                                FREE(bind);
-                        }
-                        FREE(bindings);
-                        SLIST_REMOVE(&modes, mode, Mode, modes);
-                }
+        struct Mode *mode;
+        Binding *bind;
+        while (!SLIST_EMPTY(&modes)) {
+            mode = SLIST_FIRST(&modes);
+            FREE(mode->name);
+
+            /* Clear the old binding list */
+            bindings = mode->bindings;
+            while (!TAILQ_EMPTY(bindings)) {
+                bind = TAILQ_FIRST(bindings);
+                TAILQ_REMOVE(bindings, bind, bindings);
+                FREE(bind->translated_to);
+                FREE(bind->command);
+                FREE(bind);
+            }
+            FREE(bindings);
+            SLIST_REMOVE(&modes, mode, Mode, modes);
+        }
 
-#if 0
-                struct Assignment *assign;
-                while (!TAILQ_EMPTY(&assignments)) {
-                        assign = TAILQ_FIRST(&assignments);
-                        FREE(assign->windowclass_title);
-                        TAILQ_REMOVE(&assignments, assign, assignments);
-                        FREE(assign);
-                }
-#endif
+        struct Assignment *assign;
+        while (!TAILQ_EMPTY(&assignments)) {
+            assign = TAILQ_FIRST(&assignments);
+            if (assign->type == A_TO_WORKSPACE)
+                FREE(assign->dest.workspace);
+            else if (assign->type == A_TO_OUTPUT)
+                FREE(assign->dest.output);
+            else if (assign->type == A_COMMAND)
+                FREE(assign->dest.command);
+            match_free(&(assign->match));
+            TAILQ_REMOVE(&assignments, assign, assignments);
+            FREE(assign);
+        }
 
-                /* Clear workspace names */
+        /* Clear workspace names */
 #if 0
-                Workspace *ws;
-                TAILQ_FOREACH(ws, workspaces, workspaces)
-                        workspace_set_name(ws, NULL);
+        Workspace *ws;
+        TAILQ_FOREACH(ws, workspaces, workspaces)
+            workspace_set_name(ws, NULL);
 #endif
-        }
+    }
 
-        SLIST_INIT(&modes);
+    SLIST_INIT(&modes);
 
-        struct Mode *default_mode = scalloc(sizeof(struct Mode));
-        default_mode->name = sstrdup("default");
-        default_mode->bindings = scalloc(sizeof(struct bindings_head));
-        TAILQ_INIT(default_mode->bindings);
-        SLIST_INSERT_HEAD(&modes, default_mode, modes);
+    struct Mode *default_mode = scalloc(sizeof(struct Mode));
+    default_mode->name = sstrdup("default");
+    default_mode->bindings = scalloc(sizeof(struct bindings_head));
+    TAILQ_INIT(default_mode->bindings);
+    SLIST_INSERT_HEAD(&modes, default_mode, modes);
 
-        bindings = default_mode->bindings;
+    bindings = default_mode->bindings;
 
 #define REQUIRED_OPTION(name) \
-        if (config.name == NULL) \
-                die("You did not specify required configuration option " #name "\n");
+    if (config.name == NULL) \
+        die("You did not specify required configuration option " #name "\n");
 
-        /* Clear the old config or initialize the data structure */
-        memset(&config, 0, sizeof(config));
+    /* Clear the old config or initialize the data structure */
+    memset(&config, 0, sizeof(config));
 
-        /* Initialize default colors */
+    /* Initialize default colors */
 #define INIT_COLOR(x, cborder, cbackground, ctext) \
-        do { \
-                x.border = get_colorpixel(cborder); \
-                x.background = get_colorpixel(cbackground); \
-                x.text = get_colorpixel(ctext); \
-        } while (0)
-
-        config.client.background = get_colorpixel("#000000");
-        INIT_COLOR(config.client.focused, "#4c7899", "#285577", "#ffffff");
-        INIT_COLOR(config.client.focused_inactive, "#333333", "#5f676a", "#ffffff");
-        INIT_COLOR(config.client.unfocused, "#333333", "#222222", "#888888");
-        INIT_COLOR(config.client.urgent, "#2f343a", "#900000", "#ffffff");
-        INIT_COLOR(config.bar.focused, "#4c7899", "#285577", "#ffffff");
-        INIT_COLOR(config.bar.unfocused, "#333333", "#222222", "#888888");
-        INIT_COLOR(config.bar.urgent, "#2f343a", "#900000", "#ffffff");
-
-        config.default_border = BS_NORMAL;
-        /* Set default_orientation to NO_ORIENTATION for auto orientation. */
-        config.default_orientation = NO_ORIENTATION;
-
-        parse_configuration(override_configpath);
-
-        if (reload) {
-                translate_keysyms();
-                grab_all_keys(conn, false);
-        }
+    do { \
+        x.border = get_colorpixel(cborder); \
+        x.background = get_colorpixel(cbackground); \
+        x.text = get_colorpixel(ctext); \
+    } while (0)
+
+    config.client.background = get_colorpixel("#000000");
+    INIT_COLOR(config.client.focused, "#4c7899", "#285577", "#ffffff");
+    INIT_COLOR(config.client.focused_inactive, "#333333", "#5f676a", "#ffffff");
+    INIT_COLOR(config.client.unfocused, "#333333", "#222222", "#888888");
+    INIT_COLOR(config.client.urgent, "#2f343a", "#900000", "#ffffff");
+    INIT_COLOR(config.bar.focused, "#4c7899", "#285577", "#ffffff");
+    INIT_COLOR(config.bar.unfocused, "#333333", "#222222", "#888888");
+    INIT_COLOR(config.bar.urgent, "#2f343a", "#900000", "#ffffff");
+
+    config.default_border = BS_NORMAL;
+    config.default_floating_border = BS_NORMAL;
+    /* Set default_orientation to NO_ORIENTATION for auto orientation. */
+    config.default_orientation = NO_ORIENTATION;
+
+    parse_configuration(override_configpath);
+
+    if (reload) {
+        translate_keysyms();
+        grab_all_keys(conn, false);
+    }
 
-        if (config.font.id == 0) {
-                ELOG("You did not specify required configuration option \"font\"\n");
-                config.font = load_font("fixed", true);
-        }
+    if (config.font.id == 0) {
+        ELOG("You did not specify required configuration option \"font\"\n");
+        config.font = load_font("fixed", true);
+    }
 
 #if 0
-        /* Set an empty name for every workspace which got no name */
-        Workspace *ws;
-        TAILQ_FOREACH(ws, workspaces, workspaces) {
-                if (ws->name != NULL) {
-                        /* If the font was not specified when the workspace name
-                         * was loaded, we need to predict the text width now */
-                        if (ws->text_width == 0)
-                                ws->text_width = predict_text_width(global_conn,
-                                                config.font, ws->name, ws->name_len);
-                        continue;
-                }
-
-                workspace_set_name(ws, NULL);
-        }
+    /* Set an empty name for every workspace which got no name */
+    Workspace *ws;
+    TAILQ_FOREACH(ws, workspaces, workspaces) {
+            if (ws->name != NULL) {
+                    /* If the font was not specified when the workspace name
+                     * was loaded, we need to predict the text width now */
+                    if (ws->text_width == 0)
+                            ws->text_width = predict_text_width(global_conn,
+                                            config.font, ws->name, ws->name_len);
+                    continue;
+            }
+
+            workspace_set_name(ws, NULL);
+    }
 #endif
 }
index b898d7bc6c901823191dc6a676f5681545aa48d1..e43f4703fa4d8ad7876f7500880bd90e26e271f1 100644 (file)
@@ -173,6 +173,10 @@ void floating_enable(Con *con, bool automatic) {
     con->percent = 1.0;
     con->floating = FLOATING_USER_ON;
 
+    /* 4: set the border style as specified with new_float */
+    if (automatic)
+        con->border_style = config.default_floating_border;
+
     TAILQ_INSERT_TAIL(&(nc->nodes_head), con, nodes);
     TAILQ_INSERT_TAIL(&(nc->focus_head), con, focused);
 
@@ -516,31 +520,6 @@ void drag_pointer(Con *con, xcb_button_press_event_t *event, xcb_window_t
 }
 
 #if 0
-/*
- * Changes focus in the given direction for floating clients.
- *
- * Changing to the left/right means going to the previous/next floating client,
- * changing to top/bottom means cycling through the Z-index.
- *
- */
-void floating_focus_direction(xcb_connection_t *conn, Client *currently_focused, direction_t direction) {
-        DLOG("floating focus\n");
-
-        if (direction == D_LEFT || direction == D_RIGHT) {
-                /* Go to the next/previous floating client */
-                Client *client;
-
-                while ((client = (direction == D_LEFT ? TAILQ_PREV(currently_focused, floating_clients_head, floating_clients) :
-                                                        TAILQ_NEXT(currently_focused, floating_clients))) !=
-                       TAILQ_END(&(currently_focused->workspace->floating_clients))) {
-                        if (!client->floating)
-                                continue;
-                        set_focus(conn, client, true);
-                        return;
-                }
-        }
-}
-
 /*
  * Moves the client 10px to the specified direction.
  *
index 336a491f67f6b27300c6cff50226136e9ea3abc6..b3cb1df7b703c28b26b48b46956ec04be96250d2 100644 (file)
@@ -557,6 +557,21 @@ static bool handle_windowname_change_legacy(void *data, xcb_connection_t *conn,
     return true;
 }
 
+/*
+ * Called when a window changes its WM_WINDOW_ROLE.
+ *
+ */
+static bool handle_windowrole_change(void *data, xcb_connection_t *conn, uint8_t state,
+                                     xcb_window_t window, xcb_atom_t atom, xcb_get_property_reply_t *prop) {
+    Con *con;
+    if ((con = con_by_window_id(window)) == NULL || con->window == NULL)
+        return false;
+
+    window_update_role(con->window, prop, false);
+
+    return true;
+}
+
 #if 0
 /*
  * Updates the client’s WM_CLASS property
@@ -633,6 +648,25 @@ static int handle_client_message(xcb_client_message_event_t *event) {
 
         tree_render();
         x_push_changes(croot);
+    } else if (event->type == A_I3_SYNC) {
+        DLOG("i3 sync, yay\n");
+        xcb_window_t window = event->data.data32[0];
+        uint32_t rnd = event->data.data32[1];
+        DLOG("Sending random value %d back to X11 window 0x%08x\n", rnd, window);
+
+        void *reply = scalloc(32);
+        xcb_client_message_event_t *ev = reply;
+
+        ev->response_type = XCB_CLIENT_MESSAGE;
+        ev->window = window;
+        ev->type = A_I3_SYNC;
+        ev->format = 32;
+        ev->data.data32[0] = window;
+        ev->data.data32[1] = rnd;
+
+        xcb_send_event(conn, false, window, XCB_EVENT_MASK_NO_EVENT, (char*)ev);
+        xcb_flush(conn);
+        free(reply);
     } else {
         ELOG("unhandled clientmessage\n");
         return 0;
@@ -933,7 +967,8 @@ static struct property_handler_t property_handlers[] = {
     { 0, 128, handle_windowname_change_legacy },
     { 0, UINT_MAX, handle_normal_hints },
     { 0, UINT_MAX, handle_clientleader_change },
-    { 0, UINT_MAX, handle_transient_for }
+    { 0, UINT_MAX, handle_transient_for },
+    { 0, 128, handle_windowrole_change }
 };
 #define NUM_HANDLERS (sizeof(property_handlers) / sizeof(struct property_handler_t))
 
@@ -949,6 +984,7 @@ void property_handlers_init() {
     property_handlers[3].atom = XCB_ATOM_WM_NORMAL_HINTS;
     property_handlers[4].atom = A_WM_CLIENT_LEADER;
     property_handlers[5].atom = XCB_ATOM_WM_TRANSIENT_FOR;
+    property_handlers[6].atom = A_WM_WINDOW_ROLE;
 }
 
 static void property_notify(uint8_t state, xcb_window_t window, xcb_atom_t atom) {
index e03bdcbd07ccdc712a6c5e90459cf58b810a90b1..031ee9ab5d78bea6369b740bdc84f760fb734c99 100644 (file)
--- a/src/ipc.c
+++ b/src/ipc.c
@@ -205,6 +205,11 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) {
     ystr("urgent");
     y(bool, con->urgent);
 
+    if (con->mark != NULL) {
+        ystr("mark");
+        ystr(con->mark);
+    }
+
     ystr("focused");
     y(bool, (con == focused));
 
@@ -338,6 +343,7 @@ IPC_HANDLER(tree) {
     y(free);
 }
 
+
 /*
  * Formats the reply message for a GET_WORKSPACES request and sends it to the
  * client
@@ -468,6 +474,38 @@ IPC_HANDLER(get_outputs) {
     y(free);
 }
 
+/*
+ * Formats the reply message for a GET_MARKS request and sends it to the
+ * client
+ *
+ */
+IPC_HANDLER(get_marks) {
+#if YAJL_MAJOR >= 2
+    yajl_gen gen = yajl_gen_alloc(NULL);
+#else
+    yajl_gen gen = yajl_gen_alloc(NULL, NULL);
+#endif
+    y(array_open);
+
+    Con *con;
+    TAILQ_FOREACH(con, &all_cons, all_cons)
+        if (con->mark != NULL)
+            ystr(con->mark);
+
+    y(array_close);
+
+    const unsigned char *payload;
+#if YAJL_MAJOR >= 2
+    size_t length;
+#else
+    unsigned int length;
+#endif
+    y(get_buf, &payload, &length);
+
+    ipc_send_message(fd, payload, I3_IPC_REPLY_TYPE_MARKS, length);
+    y(free);
+}
+
 /*
  * Callback for the YAJL parser (will be called when a string is parsed).
  *
@@ -555,12 +593,13 @@ IPC_HANDLER(subscribe) {
 
 /* The index of each callback function corresponds to the numeric
  * value of the message type (see include/i3/ipc.h) */
-handler_t handlers[5] = {
+handler_t handlers[6] = {
     handle_command,
     handle_get_workspaces,
     handle_subscribe,
     handle_get_outputs,
-    handle_tree
+    handle_tree,
+    handle_get_marks
 };
 
 /*
@@ -683,7 +722,7 @@ void ipc_new_client(EV_P_ struct ev_io *w, int revents) {
     ev_io_init(package, ipc_receive_message, client, EV_READ);
     ev_io_start(EV_A_ package);
 
-    DLOG("IPC: new client connected\n");
+    DLOG("IPC: new client connected on fd %d\n", w->fd);
 
     ipc_client *new = scalloc(sizeof(ipc_client));
     new->fd = client;
index b61a0e5c490210a72d30255ebef30ed576469c4c..37322c4ea3857f2b92e85f05c5ee843054f8e975 100644 (file)
@@ -146,6 +146,10 @@ static int json_string(void *ctx, const unsigned char *val, unsigned int len) {
                 json_node->layout = L_OUTPUT;
             else LOG("Unhandled \"layout\": %s\n", buf);
             free(buf);
+        } else if (strcasecmp(last_key, "mark") == 0) {
+            char *buf = NULL;
+            asprintf(&buf, "%.*s", (int)len, val);
+            json_node->mark = buf;
         }
     }
     return 1;
index 77e295fcfbae74647ba5d7fbc02619e988975898..cc60d69cd8fd07a5a1cf4142d7e9510ad0e4016d 100644 (file)
@@ -5,6 +5,8 @@
 #include <fcntl.h>
 #include "all.h"
 
+#include "sd-daemon.h"
+
 static int xkb_event_base;
 
 int xkb_current_group;
@@ -161,6 +163,14 @@ static void xkb_got_event(EV_P_ struct ev_io *w, int revents) {
     DLOG("Done\n");
 }
 
+/*
+ * Exit handler which destroys the main_loop. Will trigger cleanup handlers.
+ *
+ */
+static void i3_exit() {
+    ev_loop_destroy(main_loop);
+}
+
 int main(int argc, char *argv[]) {
     //parse_cmd("[ foo ] attach, attach ; focus");
     int screens;
@@ -422,7 +432,10 @@ int main(int argc, char *argv[]) {
 
     free(greply);
 
-    if (force_xinerama) {
+    /* Force Xinerama (for drivers which don't support RandR yet, esp. the
+     * nVidia binary graphics driver), when specified either in the config
+     * file or on command-line */
+    if (force_xinerama || config.force_xinerama) {
         xinerama_init();
     } else {
         DLOG("Checking for XRandR...\n");
@@ -459,6 +472,21 @@ int main(int argc, char *argv[]) {
         ev_io_start(main_loop, ipc_io);
     }
 
+    /* Also handle the UNIX domain sockets passed via socket activation */
+    int fds = sd_listen_fds(1);
+    if (fds < 0)
+        ELOG("socket activation: Error in sd_listen_fds\n");
+    else if (fds == 0)
+        DLOG("socket activation: no sockets passed\n");
+    else {
+        for (int fd = SD_LISTEN_FDS_START; fd < (SD_LISTEN_FDS_START + fds); fd++) {
+            DLOG("socket activation: also listening on fd %d\n", fd);
+            struct ev_io *ipc_io = scalloc(sizeof(struct ev_io));
+            ev_io_init(ipc_io, ipc_new_client, fd, EV_READ);
+            ev_io_start(main_loop, ipc_io);
+        }
+    }
+
     /* Set up i3 specific atoms like I3_SOCKET_PATH and I3_CONFIG_PATH */
     x_set_i3_atoms();
 
@@ -512,5 +540,9 @@ int main(int argc, char *argv[]) {
         start_application(exec_always->command);
     }
 
+    /* Make sure to destroy the event loop to invoke the cleeanup callbacks
+     * when calling exit() */
+    atexit(i3_exit);
+
     ev_loop(main_loop, 0);
 }
index 3dcdbac87626e049c28a8b66af9236596336fbec..35055d17ea04e7787efe40dde42e6cb6fdf3e751 100644 (file)
@@ -83,7 +83,8 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
 
     xcb_get_property_cookie_t wm_type_cookie, strut_cookie, state_cookie,
                               utf8_title_cookie, title_cookie,
-                              class_cookie, leader_cookie, transient_cookie;
+                              class_cookie, leader_cookie, transient_cookie,
+                              role_cookie;
 
 
     geomc = xcb_get_geometry(conn, d);
@@ -145,6 +146,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
     transient_cookie = GET_PROPERTY(XCB_ATOM_WM_TRANSIENT_FOR, UINT32_MAX);
     title_cookie = GET_PROPERTY(XCB_ATOM_WM_NAME, 128);
     class_cookie = GET_PROPERTY(XCB_ATOM_WM_CLASS, 128);
+    role_cookie = GET_PROPERTY(A_WM_WINDOW_ROLE, 128);
     /* TODO: also get wm_normal_hints here. implement after we got rid of xcb-event */
 
     DLOG("reparenting!\n");
@@ -171,6 +173,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
     window_update_leader(cwindow, xcb_get_property_reply(conn, leader_cookie, NULL));
     window_update_transient_for(cwindow, xcb_get_property_reply(conn, transient_cookie, NULL));
     window_update_strut_partial(cwindow, xcb_get_property_reply(conn, strut_cookie, NULL));
+    window_update_role(cwindow, xcb_get_property_reply(conn, role_cookie, NULL), true);
 
     /* check if the window needs WM_TAKE_FOCUS */
     cwindow->needs_take_focus = window_supports_protocol(cwindow->id, A_WM_TAKE_FOCUS);
@@ -325,7 +328,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
 
     if (want_floating) {
         DLOG("geometry = %d x %d\n", nc->geometry.width, nc->geometry.height);
-        floating_enable(nc, false);
+        floating_enable(nc, true);
     }
 
     /* to avoid getting an UnmapNotify event due to reparenting, we temporarily
index 3a346117340a9c880d66a70f99fcd91bee242df4..745989ba6ef378c5b59755f0eac8a01a6a8a39b5 100644 (file)
@@ -39,6 +39,7 @@ bool match_is_empty(Match *match) {
             match->application == NULL &&
             match->class == NULL &&
             match->instance == NULL &&
+            match->role == NULL &&
             match->id == XCB_NONE &&
             match->con_id == NULL &&
             match->dock == -1 &&
@@ -52,16 +53,20 @@ bool match_is_empty(Match *match) {
 void match_copy(Match *dest, Match *src) {
     memcpy(dest, src, sizeof(Match));
 
-#define STRDUP(field) do { \
+/* The DUPLICATE_REGEX macro creates a new regular expression from the
+ * ->pattern of the old one. It therefore does use a little more memory then
+ *  with a refcounting system, but it’s easier this way. */
+#define DUPLICATE_REGEX(field) do { \
     if (src->field != NULL) \
-        dest->field = sstrdup(src->field); \
+        dest->field = regex_new(src->field->pattern); \
 } while (0)
 
-    STRDUP(title);
-    STRDUP(mark);
-    STRDUP(application);
-    STRDUP(class);
-    STRDUP(instance);
+    DUPLICATE_REGEX(title);
+    DUPLICATE_REGEX(mark);
+    DUPLICATE_REGEX(application);
+    DUPLICATE_REGEX(class);
+    DUPLICATE_REGEX(instance);
+    DUPLICATE_REGEX(role);
 }
 
 /*
@@ -71,9 +76,9 @@ void match_copy(Match *dest, Match *src) {
 bool match_matches_window(Match *match, i3Window *window) {
     LOG("checking window %d (%s)\n", window->id, window->class_class);
 
-    /* TODO: pcre, full matching, … */
     if (match->class != NULL) {
-        if (window->class_class != NULL && strcasecmp(match->class, window->class_class) == 0) {
+        if (window->class_class != NULL &&
+            regex_matches(match->class, window->class_class)) {
             LOG("window class matches (%s)\n", window->class_class);
         } else {
             LOG("window class does not match\n");
@@ -82,7 +87,8 @@ bool match_matches_window(Match *match, i3Window *window) {
     }
 
     if (match->instance != NULL) {
-        if (window->class_instance != NULL && strcasecmp(match->instance, window->class_instance) == 0) {
+        if (window->class_instance != NULL &&
+            regex_matches(match->instance, window->class_instance)) {
             LOG("window instance matches (%s)\n", window->class_instance);
         } else {
             LOG("window instance does not match\n");
@@ -99,9 +105,9 @@ bool match_matches_window(Match *match, i3Window *window) {
         }
     }
 
-    /* TODO: pcre match */
     if (match->title != NULL) {
-        if (window->name_json != NULL && strcasecmp(match->title, window->name_json) == 0) {
+        if (window->name_json != NULL &&
+            regex_matches(match->title, window->name_json)) {
             LOG("title matches (%s)\n", window->name_json);
         } else {
             LOG("title does not match\n");
@@ -109,6 +115,16 @@ bool match_matches_window(Match *match, i3Window *window) {
         }
     }
 
+    if (match->role != NULL) {
+        if (window->role != NULL &&
+            regex_matches(match->role, window->role)) {
+            LOG("window_role matches (%s)\n", window->role);
+        } else {
+            LOG("window_role does not match\n");
+            return false;
+        }
+    }
+
     if (match->dock != -1) {
         LOG("match->dock = %d, window->dock = %d\n", match->dock, window->dock);
         if ((window->dock == W_DOCK_TOP && match->dock == M_DOCK_TOP) ||
@@ -132,3 +148,25 @@ bool match_matches_window(Match *match, i3Window *window) {
 
     return true;
 }
+
+/*
+ * Frees the given match. It must not be used afterwards!
+ *
+ */
+void match_free(Match *match) {
+    /* First step: free the regex fields / patterns */
+    regex_free(match->title);
+    regex_free(match->application);
+    regex_free(match->class);
+    regex_free(match->instance);
+    regex_free(match->mark);
+    regex_free(match->role);
+
+    /* Second step: free the regex helper struct itself */
+    FREE(match->title);
+    FREE(match->application);
+    FREE(match->class);
+    FREE(match->instance);
+    FREE(match->mark);
+    FREE(match->role);
+}
index 6da90070589d18b33e2aacbd79b7b8b71a3935ce..2b8757e5c25dc674647df2728d5f0fb1b1c1f352 100644 (file)
@@ -447,10 +447,12 @@ void init_ws_for_output(Output *output, Con *content) {
         if (!exists) {
             /* Set ->num to the number of the workspace, if the name actually
              * is a number or starts with a number */
-            long parsed_num = strtol(ws->name, NULL, 10);
+            char *endptr = NULL;
+            long parsed_num = strtol(ws->name, &endptr, 10);
             if (parsed_num == LONG_MIN ||
                 parsed_num == LONG_MAX ||
-                parsed_num <= 0)
+                parsed_num < 0 ||
+                endptr == ws->name)
                 ws->num = -1;
             else ws->num = parsed_num;
             LOG("Used number %d for workspace with name %s\n", ws->num, ws->name);
diff --git a/src/regex.c b/src/regex.c
new file mode 100644 (file)
index 0000000..64a2f3a
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ * © 2009-2011 Michael Stapelberg and contributors (see also: LICENSE)
+ *
+ *
+ */
+
+#include "all.h"
+
+/*
+ * Creates a new 'regex' struct containing the given pattern and a PCRE
+ * compiled regular expression. Also, calls pcre_study because this regex will
+ * most likely be used often (like for every new window and on every relevant
+ * property change of existing windows).
+ *
+ * Returns NULL if the pattern could not be compiled into a regular expression
+ * (and ELOGs an appropriate error message).
+ *
+ */
+struct regex *regex_new(const char *pattern) {
+    const char *error;
+    int errorcode, offset;
+
+    struct regex *re = scalloc(sizeof(struct regex));
+    re->pattern = sstrdup(pattern);
+    int options = PCRE_UTF8;
+#ifdef PCRE_HAS_UCP
+    /* We use PCRE_UCP so that \B, \b, \D, \d, \S, \s, \W, \w and some POSIX
+     * character classes play nicely with Unicode */
+    options |= PCRE_UCP;
+#endif
+    while (!(re->regex = pcre_compile2(pattern, options, &errorcode, &error, &offset, NULL))) {
+        /* If the error is that PCRE was not compiled with UTF-8 support we
+         * disable it and try again */
+        if (errorcode == 32) {
+            options &= ~PCRE_UTF8;
+            continue;
+        }
+        ELOG("PCRE regular expression compilation failed at %d: %s\n",
+             offset, error);
+        return NULL;
+    }
+    re->extra = pcre_study(re->regex, 0, &error);
+    /* If an error happened, we print the error message, but continue.
+     * Studying the regular expression leads to faster matching, but it’s not
+     * absolutely necessary. */
+    if (error) {
+        ELOG("PCRE regular expression studying failed: %s\n", error);
+    }
+    return re;
+}
+
+/*
+ * Frees the given regular expression. It must not be used afterwards!
+ *
+ */
+void regex_free(struct regex *regex) {
+    if (!regex)
+        return;
+    FREE(regex->pattern);
+    FREE(regex->regex);
+    FREE(regex->extra);
+}
+
+/*
+ * Checks if the given regular expression matches the given input and returns
+ * true if it does. In either case, it logs the outcome using LOG(), so it will
+ * be visible without any debug loglevel.
+ *
+ */
+bool regex_matches(struct regex *regex, const char *input) {
+    int rc;
+
+    /* We use strlen() because pcre_exec() expects the length of the input
+     * string in bytes */
+    if ((rc = pcre_exec(regex->regex, regex->extra, input, strlen(input), 0, 0, NULL, 0)) == 0) {
+        LOG("Regular expression \"%s\" matches \"%s\"\n",
+            regex->pattern, input);
+        return true;
+    }
+
+    if (rc == PCRE_ERROR_NOMATCH) {
+        LOG("Regular expression \"%s\" does not match \"%s\"\n",
+            regex->pattern, input);
+        return false;
+    }
+
+    ELOG("PCRE error %d while trying to use regular expression \"%s\" on input \"%s\", see pcreapi(3)\n",
+         rc, regex->pattern, input);
+    return false;
+}
diff --git a/src/sd-daemon.c b/src/sd-daemon.c
new file mode 100644 (file)
index 0000000..6d1eebf
--- /dev/null
@@ -0,0 +1,436 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  Copyright 2010 Lennart Poettering
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation files
+  (the "Software"), to deal in the Software without restriction,
+  including without limitation the rights to use, copy, modify, merge,
+  publish, distribute, sublicense, and/or sell copies of the Software,
+  and to permit persons to whom the Software is furnished to do so,
+  subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+  ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+***/
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/fcntl.h>
+#include <netinet/in.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stddef.h>
+
+#include "sd-daemon.h"
+
+int sd_listen_fds(int unset_environment) {
+
+#if defined(DISABLE_SYSTEMD) || !defined(__linux__)
+        return 0;
+#else
+        int r, fd;
+        const char *e;
+        char *p = NULL;
+        unsigned long l;
+
+        if (!(e = getenv("LISTEN_PID"))) {
+                r = 0;
+                goto finish;
+        }
+
+        errno = 0;
+        l = strtoul(e, &p, 10);
+
+        if (errno != 0) {
+                r = -errno;
+                goto finish;
+        }
+
+        if (!p || *p || l <= 0) {
+                r = -EINVAL;
+                goto finish;
+        }
+
+        /* Is this for us? */
+        if (getpid() != (pid_t) l) {
+                r = 0;
+                goto finish;
+        }
+
+        if (!(e = getenv("LISTEN_FDS"))) {
+                r = 0;
+                goto finish;
+        }
+
+        errno = 0;
+        l = strtoul(e, &p, 10);
+
+        if (errno != 0) {
+                r = -errno;
+                goto finish;
+        }
+
+        if (!p || *p) {
+                r = -EINVAL;
+                goto finish;
+        }
+
+        for (fd = SD_LISTEN_FDS_START; fd < SD_LISTEN_FDS_START + (int) l; fd ++) {
+                int flags;
+
+                if ((flags = fcntl(fd, F_GETFD)) < 0) {
+                        r = -errno;
+                        goto finish;
+                }
+
+                if (flags & FD_CLOEXEC)
+                        continue;
+
+                if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) {
+                        r = -errno;
+                        goto finish;
+                }
+        }
+
+        r = (int) l;
+
+finish:
+        if (unset_environment) {
+                unsetenv("LISTEN_PID");
+                unsetenv("LISTEN_FDS");
+        }
+
+        return r;
+#endif
+}
+
+int sd_is_fifo(int fd, const char *path) {
+        struct stat st_fd;
+
+        if (fd < 0)
+                return -EINVAL;
+
+        memset(&st_fd, 0, sizeof(st_fd));
+        if (fstat(fd, &st_fd) < 0)
+                return -errno;
+
+        if (!S_ISFIFO(st_fd.st_mode))
+                return 0;
+
+        if (path) {
+                struct stat st_path;
+
+                memset(&st_path, 0, sizeof(st_path));
+                if (stat(path, &st_path) < 0) {
+
+                        if (errno == ENOENT || errno == ENOTDIR)
+                                return 0;
+
+                        return -errno;
+                }
+
+                return
+                        st_path.st_dev == st_fd.st_dev &&
+                        st_path.st_ino == st_fd.st_ino;
+        }
+
+        return 1;
+}
+
+static int sd_is_socket_internal(int fd, int type, int listening) {
+        struct stat st_fd;
+
+        if (fd < 0 || type < 0)
+                return -EINVAL;
+
+        if (fstat(fd, &st_fd) < 0)
+                return -errno;
+
+        if (!S_ISSOCK(st_fd.st_mode))
+                return 0;
+
+        if (type != 0) {
+                int other_type = 0;
+                socklen_t l = sizeof(other_type);
+
+                if (getsockopt(fd, SOL_SOCKET, SO_TYPE, &other_type, &l) < 0)
+                        return -errno;
+
+                if (l != sizeof(other_type))
+                        return -EINVAL;
+
+                if (other_type != type)
+                        return 0;
+        }
+
+        if (listening >= 0) {
+                int accepting = 0;
+                socklen_t l = sizeof(accepting);
+
+                if (getsockopt(fd, SOL_SOCKET, SO_ACCEPTCONN, &accepting, &l) < 0)
+                        return -errno;
+
+                if (l != sizeof(accepting))
+                        return -EINVAL;
+
+                if (!accepting != !listening)
+                        return 0;
+        }
+
+        return 1;
+}
+
+union sockaddr_union {
+        struct sockaddr sa;
+        struct sockaddr_in in4;
+        struct sockaddr_in6 in6;
+        struct sockaddr_un un;
+        struct sockaddr_storage storage;
+};
+
+int sd_is_socket(int fd, int family, int type, int listening) {
+        int r;
+
+        if (family < 0)
+                return -EINVAL;
+
+        if ((r = sd_is_socket_internal(fd, type, listening)) <= 0)
+                return r;
+
+        if (family > 0) {
+                union sockaddr_union sockaddr;
+                socklen_t l;
+
+                memset(&sockaddr, 0, sizeof(sockaddr));
+                l = sizeof(sockaddr);
+
+                if (getsockname(fd, &sockaddr.sa, &l) < 0)
+                        return -errno;
+
+                if (l < sizeof(sa_family_t))
+                        return -EINVAL;
+
+                return sockaddr.sa.sa_family == family;
+        }
+
+        return 1;
+}
+
+int sd_is_socket_inet(int fd, int family, int type, int listening, uint16_t port) {
+        union sockaddr_union sockaddr;
+        socklen_t l;
+        int r;
+
+        if (family != 0 && family != AF_INET && family != AF_INET6)
+                return -EINVAL;
+
+        if ((r = sd_is_socket_internal(fd, type, listening)) <= 0)
+                return r;
+
+        memset(&sockaddr, 0, sizeof(sockaddr));
+        l = sizeof(sockaddr);
+
+        if (getsockname(fd, &sockaddr.sa, &l) < 0)
+                return -errno;
+
+        if (l < sizeof(sa_family_t))
+                return -EINVAL;
+
+        if (sockaddr.sa.sa_family != AF_INET &&
+            sockaddr.sa.sa_family != AF_INET6)
+                return 0;
+
+        if (family > 0)
+                if (sockaddr.sa.sa_family != family)
+                        return 0;
+
+        if (port > 0) {
+                if (sockaddr.sa.sa_family == AF_INET) {
+                        if (l < sizeof(struct sockaddr_in))
+                                return -EINVAL;
+
+                        return htons(port) == sockaddr.in4.sin_port;
+                } else {
+                        if (l < sizeof(struct sockaddr_in6))
+                                return -EINVAL;
+
+                        return htons(port) == sockaddr.in6.sin6_port;
+                }
+        }
+
+        return 1;
+}
+
+int sd_is_socket_unix(int fd, int type, int listening, const char *path, size_t length) {
+        union sockaddr_union sockaddr;
+        socklen_t l;
+        int r;
+
+        if ((r = sd_is_socket_internal(fd, type, listening)) <= 0)
+                return r;
+
+        memset(&sockaddr, 0, sizeof(sockaddr));
+        l = sizeof(sockaddr);
+
+        if (getsockname(fd, &sockaddr.sa, &l) < 0)
+                return -errno;
+
+        if (l < sizeof(sa_family_t))
+                return -EINVAL;
+
+        if (sockaddr.sa.sa_family != AF_UNIX)
+                return 0;
+
+        if (path) {
+                if (length <= 0)
+                        length = strlen(path);
+
+                if (length <= 0)
+                        /* Unnamed socket */
+                        return l == offsetof(struct sockaddr_un, sun_path);
+
+                if (path[0])
+                        /* Normal path socket */
+                        return
+                                (l >= offsetof(struct sockaddr_un, sun_path) + length + 1) &&
+                                memcmp(path, sockaddr.un.sun_path, length+1) == 0;
+                else
+                        /* Abstract namespace socket */
+                        return
+                                (l == offsetof(struct sockaddr_un, sun_path) + length) &&
+                                memcmp(path, sockaddr.un.sun_path, length) == 0;
+        }
+
+        return 1;
+}
+
+int sd_notify(int unset_environment, const char *state) {
+#if defined(DISABLE_SYSTEMD) || !defined(__linux__) || !defined(SOCK_CLOEXEC)
+        return 0;
+#else
+        int fd = -1, r;
+        struct msghdr msghdr;
+        struct iovec iovec;
+        union sockaddr_union sockaddr;
+        const char *e;
+
+        if (!state) {
+                r = -EINVAL;
+                goto finish;
+        }
+
+        if (!(e = getenv("NOTIFY_SOCKET")))
+                return 0;
+
+        /* Must be an abstract socket, or an absolute path */
+        if ((e[0] != '@' && e[0] != '/') || e[1] == 0) {
+                r = -EINVAL;
+                goto finish;
+        }
+
+        if ((fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0)) < 0) {
+                r = -errno;
+                goto finish;
+        }
+
+        memset(&sockaddr, 0, sizeof(sockaddr));
+        sockaddr.sa.sa_family = AF_UNIX;
+        strncpy(sockaddr.un.sun_path, e, sizeof(sockaddr.un.sun_path));
+
+        if (sockaddr.un.sun_path[0] == '@')
+                sockaddr.un.sun_path[0] = 0;
+
+        memset(&iovec, 0, sizeof(iovec));
+        iovec.iov_base = (char*) state;
+        iovec.iov_len = strlen(state);
+
+        memset(&msghdr, 0, sizeof(msghdr));
+        msghdr.msg_name = &sockaddr;
+        msghdr.msg_namelen = offsetof(struct sockaddr_un, sun_path) + strlen(e);
+
+        if (msghdr.msg_namelen > sizeof(struct sockaddr_un))
+                msghdr.msg_namelen = sizeof(struct sockaddr_un);
+
+        msghdr.msg_iov = &iovec;
+        msghdr.msg_iovlen = 1;
+
+        if (sendmsg(fd, &msghdr, MSG_NOSIGNAL) < 0) {
+                r = -errno;
+                goto finish;
+        }
+
+        r = 1;
+
+finish:
+        if (unset_environment)
+                unsetenv("NOTIFY_SOCKET");
+
+        if (fd >= 0)
+                close(fd);
+
+        return r;
+#endif
+}
+
+int sd_notifyf(int unset_environment, const char *format, ...) {
+#if defined(DISABLE_SYSTEMD) || !defined(__linux__)
+        return 0;
+#else
+        va_list ap;
+        char *p = NULL;
+        int r;
+
+        va_start(ap, format);
+        r = vasprintf(&p, format, ap);
+        va_end(ap);
+
+        if (r < 0 || !p)
+                return -ENOMEM;
+
+        r = sd_notify(unset_environment, p);
+        free(p);
+
+        return r;
+#endif
+}
+
+int sd_booted(void) {
+#if defined(DISABLE_SYSTEMD) || !defined(__linux__)
+        return 0;
+#else
+
+        struct stat a, b;
+
+        /* We simply test whether the systemd cgroup hierarchy is
+         * mounted */
+
+        if (lstat("/sys/fs/cgroup", &a) < 0)
+                return 0;
+
+        if (lstat("/sys/fs/cgroup/systemd", &b) < 0)
+                return 0;
+
+        return a.st_dev != b.st_dev;
+#endif
+}
index 76530d604bb1f3546abe89695ab141cbf5abfa6e..4baba58eed118118a85d99818c901ac6c603942d 100644 (file)
@@ -429,12 +429,35 @@ static bool _tree_next(Con *con, char way, orientation_t orientation, bool wrap)
         return true;
     }
 
+    Con *parent = con->parent;
+
     if (con->type == CT_FLOATING_CON) {
-        /* TODO: implement focus for floating windows */
-        return false;
-    }
+        /* left/right focuses the previous/next floating container */
+        if (orientation == HORIZ) {
+            Con *next;
+            if (way == 'n')
+                next = TAILQ_NEXT(con, floating_windows);
+            else next = TAILQ_PREV(con, floating_head, floating_windows);
+
+            /* If there is no next/previous container, wrap */
+            if (!next) {
+                if (way == 'n')
+                    next = TAILQ_FIRST(&(parent->floating_head));
+                else next = TAILQ_LAST(&(parent->floating_head), floating_head);
+            }
 
-    Con *parent = con->parent;
+            /* Still no next/previous container? bail out */
+            if (!next)
+                return false;
+
+            con_focus(con_descend_focused(next));
+            return true;
+        } else {
+            /* up/down cycles through the Z-index */
+            /* TODO: implement cycling through the z-index */
+            return false;
+        }
+    }
 
     /* If the orientation does not match or there is no other con to focus, we
      * need to go higher in the hierarchy */
index 3dd6645630757ed5566b1bdc9e763d9d4ed79c0e..4b8b6614a863ed507ef40c7cecb6037402cecaa2 100644 (file)
@@ -215,3 +215,36 @@ void window_update_strut_partial(i3Window *win, xcb_get_property_reply_t *prop)
 
     free(prop);
 }
+
+/*
+ * Updates the WM_WINDOW_ROLE
+ *
+ */
+void window_update_role(i3Window *win, xcb_get_property_reply_t *prop, bool before_mgmt) {
+    if (prop == NULL || xcb_get_property_value_length(prop) == 0) {
+        DLOG("prop == NULL\n");
+        FREE(prop);
+        return;
+    }
+
+    char *new_role;
+    if (asprintf(&new_role, "%.*s", xcb_get_property_value_length(prop),
+                 (char*)xcb_get_property_value(prop)) == -1) {
+        perror("asprintf()");
+        DLOG("Could not get WM_WINDOW_ROLE\n");
+        free(prop);
+        return;
+    }
+    FREE(win->role);
+    win->role = new_role;
+    LOG("WM_WINDOW_ROLE changed to \"%s\"\n", win->role);
+
+    if (before_mgmt) {
+        free(prop);
+        return;
+    }
+
+    run_assignments(win);
+
+    free(prop);
+}
index a7a5a7aad6b551b717c3408861d4f0aa0d5a190f..325644598453e98b337f756ccd988fd21dbb3223 100644 (file)
@@ -49,12 +49,12 @@ Con *workspace_get(const char *num, bool *created) {
         workspace->name = sstrdup(num);
         /* We set ->num to the number if this workspace’s name consists only of
          * a positive number. Otherwise it’s a named ws and num will be -1. */
-        char *end;
-        long parsed_num = strtol(num, &end, 10);
+        char *endptr = NULL;
+        long parsed_num = strtol(num, &endptr, 10);
         if (parsed_num == LONG_MIN ||
             parsed_num == LONG_MAX ||
             parsed_num < 0 ||
-            (end && *end != '\0'))
+            endptr == num)
             workspace->num = -1;
         else workspace->num = parsed_num;
         LOG("num = %d\n", workspace->num);
index c05d6e5b4341dae8594e22fb95fd25eb5e7ef828..52038b33151497fbadbac47f09f8bbf79b42d3ff 100755 (executable)
@@ -27,8 +27,20 @@ use File::Basename qw(basename);
 use AnyEvent::I3 qw(:all);
 use Try::Tiny;
 use Getopt::Long;
-use Time::HiRes qw(sleep);
-use X11::XCB::Connection;
+use Time::HiRes qw(sleep gettimeofday tv_interval);
+use X11::XCB;
+use IO::Socket::UNIX; # core
+use POSIX; # core
+use AnyEvent::Handle;
+
+# open a file so that we get file descriptor 3. we will later close it in the
+# child and dup() the listening socket file descriptor to 3 to pass it to i3
+open(my $reserved, '<', '/dev/null');
+if (fileno($reserved) != 3) {
+    warn "Socket file descriptor is not 3.";
+    warn "Please don't start this script within a subshell of vim or something.";
+    exit 1;
+}
 
 # install a dummy CHLD handler to overwrite the CHLD handler of AnyEvent / EV
 # XXX: we could maybe also use a different loop than the default loop in EV?
@@ -63,16 +75,16 @@ my $result = GetOptions(
 my @conns;
 my @wdisplays;
 for my $display (@displays) {
-    try {
-        my $x = X11::XCB::Connection->new(display => $display);
+    my $screen;
+    my $x = X11::XCB->new($display, $screen);
+    if ($x->has_error) {
+        say STDERR "WARNING: Not using X11 display $display, could not connect";
+    } else {
         push @conns, $x;
         push @wdisplays, $display;
-    } catch {
-        say STDERR "WARNING: Not using X11 display $display, could not connect";
-    };
+    }
 }
 
-my $i3cmd = abs_path("../i3") . " -V -d all --disable-signalhandler";
 my $config = slurp('i3-test.config');
 
 # 1: get a list of all testcases
@@ -116,21 +128,87 @@ take_job($_) for @wdisplays;
 #
 sub take_job {
     my ($display) = @_;
-    my ($fh, $tmpfile) = tempfile();
-    say $fh $config;
-    say $fh "ipc-socket /tmp/nested-$display";
-    close($fh);
 
     my $test = shift @testfiles;
     return unless $test;
-    my $logpath = "$outdir/i3-log-for-" . basename($test);
-    my $cmd = "export DISPLAY=$display; exec $i3cmd -c $tmpfile >$logpath 2>&1";
     my $dont_start = (slurp($test) =~ /# !NO_I3_INSTANCE!/);
+    my $logpath = "$outdir/i3-log-for-" . basename($test);
+
+    my ($fh, $tmpfile) = tempfile('i3-run-cfg.XXXXXX', UNLINK => 1);
+    say $fh $config;
+    say $fh "ipc-socket /tmp/nested-$display";
+    close($fh);
+
+    my $activate_cv = AnyEvent->condvar;
+    my $time_before_start = [gettimeofday];
+    my $start_i3 = sub {
+        # remove the old unix socket
+        unlink("/tmp/nested-$display-activation");
+
+        # pass all file descriptors up to three to the children.
+        # we need to set this flag before opening the socket.
+        open(my $fdtest, '<', '/dev/null');
+        $^F = fileno($fdtest);
+        close($fdtest);
+        my $socket = IO::Socket::UNIX->new(
+            Listen => 1,
+            Local => "/tmp/nested-$display-activation",
+        );
+
+        my $pid = fork;
+        if (!defined($pid)) {
+            die "could not fork()";
+        }
+        if ($pid == 0) {
+            $ENV{LISTEN_PID} = $$;
+            $ENV{LISTEN_FDS} = 1;
+            $ENV{DISPLAY} = $display;
+            $^F = 3;
+
+            close($reserved);
+            POSIX::dup2(fileno($socket), 3);
+
+            # now execute i3
+            my $i3cmd = abs_path("../i3") . " -V -d all --disable-signalhandler";
+            my $cmd = "exec $i3cmd -c $tmpfile >$logpath 2>&1";
+            exec "/bin/sh", '-c', $cmd;
+
+            # if we are still here, i3 could not be found or exec failed. bail out.
+            exit 1;
+        }
 
-    my $process = Proc::Background->new($cmd) unless $dont_start;
-    say "[$display] Running $test with logfile $logpath";
+        my $child_watcher;
+        $child_watcher = AnyEvent->child(pid => $pid, cb => sub {
+            say "child died. pid = $pid";
+            undef $child_watcher;
+        });
+
+        # close the socket, the child process should be the only one which keeps a file
+        # descriptor on the listening socket.
+        $socket->close;
+
+        # We now connect (will succeed immediately) and send a request afterwards.
+        # As soon as the reply is there, i3 is considered ready.
+        my $cl = IO::Socket::UNIX->new(Peer => "/tmp/nested-$display-activation");
+        my $hdl;
+        $hdl = AnyEvent::Handle->new(fh => $cl, on_error => sub { $activate_cv->send(0) });
+
+        # send a get_tree message without payload
+        $hdl->push_write('i3-ipc' . pack("LL", 0, 4));
+
+        # wait for the reply
+        $hdl->push_read(chunk => 1, => sub {
+            my ($h, $line) = @_;
+            $activate_cv->send(1);
+            undef $hdl;
+        });
+
+        return $pid;
+    };
+
+    my $pid;
+    $pid = $start_i3->() unless $dont_start;
 
-    sleep 0.5;
     my $kill_i3 = sub {
         # Don’t bother killing i3 when we haven’t started it
         return if $dont_start;
@@ -148,53 +226,70 @@ sub take_job {
         }
 
         say "[$display] killing i3";
-        kill(9, $process->pid) or die "could not kill i3";
+        kill(9, $pid) or die "could not kill i3";
     };
 
-    my $output;
-    my $parser = TAP::Parser->new({
-        exec => [ 'sh', '-c', "DISPLAY=$display /usr/bin/perl -It/lib $test" ],
-        spool => IO::Scalar->new(\$output),
-        merge => 1,
-    });
+    # This will be called as soon as i3 is running and answered to our
+    # IPC request
+    $activate_cv->cb(sub {
+        my $time_activating = [gettimeofday];
+        my $start_duration = tv_interval($time_before_start, $time_activating);
+        my ($status) = $activate_cv->recv;
+        if ($dont_start) {
+            say "[$display] Not starting i3, testcase does that";
+        } else {
+            say "[$display] i3 startup: took " . sprintf("%.2f", $start_duration) . "s, status = $status";
+        }
 
-    my @watchers;
-    my ($stdout, $stderr) = $parser->get_select_handles;
-    for my $handle ($parser->get_select_handles) {
-        my $w;
-        $w = AnyEvent->io(
-            fh => $handle,
-            poll => 'r',
-            cb => sub {
-                # Ignore activity on stderr (unnecessary with merge => 1,
-                # but let’s keep it in here if we want to use merge => 0
-                # for some reason in the future).
-                return if defined($stderr) and $handle == $stderr;
-
-                my $result = $parser->next;
-                if (defined($result)) {
-                    # TODO: check if we should bail out
-                    return;
+        say "[$display] Running $test with logfile $logpath";
+
+        my $output;
+        my $parser = TAP::Parser->new({
+            exec => [ 'sh', '-c', qq|DISPLAY=$display LOGPATH="$logpath" /usr/bin/perl -It/lib $test| ],
+            spool => IO::Scalar->new(\$output),
+            merge => 1,
+        });
+
+        my @watchers;
+        my ($stdout, $stderr) = $parser->get_select_handles;
+        for my $handle ($parser->get_select_handles) {
+            my $w;
+            $w = AnyEvent->io(
+                fh => $handle,
+                poll => 'r',
+                cb => sub {
+                    # Ignore activity on stderr (unnecessary with merge => 1,
+                    # but let’s keep it in here if we want to use merge => 0
+                    # for some reason in the future).
+                    return if defined($stderr) and $handle == $stderr;
+
+                    my $result = $parser->next;
+                    if (defined($result)) {
+                        # TODO: check if we should bail out
+                        return;
+                    }
+
+                    # $result is not defined, we are done parsing
+                    say "[$display] $test finished";
+                    close($parser->delete_spool);
+                    $aggregator->add($test, $parser);
+                    push @done, [ $test, $output ];
+
+                    $kill_i3->();
+
+                    undef $_ for @watchers;
+                    if (@done == $num) {
+                        $cv->send;
+                    } else {
+                        take_job($display);
+                    }
                 }
+            );
+            push @watchers, $w;
+        }
+    });
 
-                # $result is not defined, we are done parsing
-                say "[$display] $test finished";
-                close($parser->delete_spool);
-                $aggregator->add($test, $parser);
-                push @done, [ $test, $output ];
-
-                $kill_i3->();
-
-                undef $_ for @watchers;
-                if (@done == $num) {
-                    $cv->send;
-                } else {
-                    take_job($display);
-                }
-            }
-        );
-        push @watchers, $w;
-    }
+    $activate_cv->send(1) if $dont_start;
 }
 
 $cv->recv;
index 34e5364e495c821255937cf82d6360b980be96d4..ae8c63f67de6789e0e366878a7dc4702a155f578 100644 (file)
@@ -42,6 +42,7 @@ my $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => $original_rect,
     background_color => '#C0C0C0',
+    event_mask => [ 'structure_notify' ],
 );
 
 isa_ok($window, 'X11::XCB::Window');
@@ -50,7 +51,7 @@ is_deeply($window->rect, $original_rect, "rect unmodified before mapping");
 
 $window->map;
 
-sleep 0.25;
+wait_for_map $x;
 
 # open another container to make the window get only half of the screen
 cmd 'open';
@@ -59,11 +60,9 @@ my $new_rect = $window->rect;
 ok(!eq_deeply($new_rect, $original_rect), "Window got repositioned");
 $original_rect = $new_rect;
 
-sleep 0.25;
-
 $window->fullscreen(1);
 
-sleep 0.25;
+sync_with_i3($x);
 
 $new_rect = $window->rect;
 ok(!eq_deeply($new_rect, $original_rect), "Window got repositioned after fullscreen");
@@ -94,6 +93,7 @@ $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => $original_rect,
     background_color => 61440,
+    event_mask => [ 'structure_notify' ],
 );
 
 is_deeply($window->rect, $original_rect, "rect unmodified before mapping");
@@ -101,7 +101,7 @@ is_deeply($window->rect, $original_rect, "rect unmodified before mapping");
 $window->fullscreen(1);
 $window->map;
 
-sleep(0.25);
+wait_for_map $x;
 
 $new_rect = $window->rect;
 ok(!eq_deeply($new_rect, $original_rect), "Window got repositioned after fullscreen");
@@ -124,10 +124,12 @@ my $swindow = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => $original_rect,
     background_color => '#C0C0C0',
+    event_mask => [ 'structure_notify' ],
 );
 
 $swindow->map;
-sleep 0.25;
+
+sync_with_i3($x);
 
 ok(!$swindow->mapped, 'window not mapped while fullscreen window active');
 
@@ -135,12 +137,12 @@ $new_rect = $swindow->rect;
 ok(!eq_deeply($new_rect, $original_rect), "Window got repositioned");
 
 $swindow->fullscreen(1);
-sleep 0.25;
+sync_with_i3($x);
 
 is(fullscreen_windows(), 1, 'amount of fullscreen windows');
 
 $window->fullscreen(0);
-sleep 0.25;
+sync_with_i3($x);
 is(fullscreen_windows(), 0, 'amount of fullscreen windows');
 
 ok($swindow->mapped, 'window mapped after other fullscreen ended');
@@ -152,7 +154,7 @@ ok($swindow->mapped, 'window mapped after other fullscreen ended');
 ###########################################################################
 
 $swindow->fullscreen(0);
-sleep 0.25;
+sync_with_i3($x);
 
 is(fullscreen_windows(), 0, 'amount of fullscreen windows after disabling');
 
index fcf73f084633b2737b3d7c19a0b01dbd69681883..d605328d491124ca9c4556da56afcf4f0ce85c0b 100644 (file)
@@ -17,13 +17,14 @@ my $window = $x->root->create_child(
     background_color => '#C0C0C0',
     # replace the type with 'utility' as soon as the coercion works again in X11::XCB
     window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'),
+    event_mask => [ 'structure_notify' ],
 );
 
 isa_ok($window, 'X11::XCB::Window');
 
 $window->map;
 
-sleep 0.25;
+wait_for_map $x;
 
 my ($absolute, $top) = $window->rect;
 
@@ -40,13 +41,14 @@ $window = $x->root->create_child(
     rect => [ 1, 1, 80, 90],
     background_color => '#C0C0C0',
     window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'),
+    event_mask => [ 'structure_notify' ],
 );
 
 isa_ok($window, 'X11::XCB::Window');
 
 $window->map;
 
-sleep 0.25;
+wait_for_map $x;
 
 ($absolute, $top) = $window->rect;
 
@@ -70,13 +72,14 @@ $window = $x->root->create_child(
     rect => [ 1, 1, 80, 90],
     background_color => '#C0C0C0',
     #window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'),
+    event_mask => [ 'structure_notify' ],
 );
 
 isa_ok($window, 'X11::XCB::Window');
 
 $window->map;
 
-sleep 0.25;
+wait_for_map $x;
 
 cmd 'floating enable';
 
index a910c930b19a87e377e1ffa2c57b05ff4de43231..982ece7e9a2c7f232dc530d19b4bfce63edeea7a 100644 (file)
@@ -2,11 +2,6 @@
 # vim:ts=4:sw=4:expandtab
 
 use i3test;
-use X11::XCB qw(:all);
-
-BEGIN {
-    use_ok('X11::XCB::Connection') or BAIL_OUT('Cannot load X11::XCB::Connection');
-}
 
 my $x = X11::XCB::Connection->new;
 
@@ -17,17 +12,14 @@ fresh_workspace;
 #####################################################################
 
 # Create a window so we can get a focus different from NULL
-my $window = open_standard_window($x);
-diag("window->id = " . $window->id);
-
-sleep 0.25;
+my $window = open_window($x);
 
 my $focus = $x->input_focus;
-diag("old focus = $focus");
 
 # Switch to another workspace
 fresh_workspace;
 
+sync_with_i3($x);
 my $new_focus = $x->input_focus;
 isnt($focus, $new_focus, "Focus changed");
 
index d357c8a9de2d5009d20c93cc7d37b8793e90ff75..5ded494fbe9fd5e4894c04853d79532cb4a68cd9 100644 (file)
@@ -2,15 +2,9 @@
 # vim:ts=4:sw=4:expandtab
 
 use i3test;
-use X11::XCB qw(:all);
-
-BEGIN {
-    use_ok('X11::XCB::Connection') or BAIL_OUT('Cannot load X11::XCB::Connection');
-}
 
 my $x = X11::XCB::Connection->new;
 
-my $i3 = i3(get_socket_path());
 my $tmp = fresh_workspace;
 
 #####################################################################
@@ -21,14 +15,9 @@ my $tmp = fresh_workspace;
 cmd 'layout default';
 cmd 'split v';
 
-my $top = open_standard_window($x);
-my $mid = open_standard_window($x);
-my $bottom = open_standard_window($x);
-sleep 0.25;
-
-diag("top id = " . $top->id);
-diag("mid id = " . $mid->id);
-diag("bottom id = " . $bottom->id);
+my $top = open_window($x);
+my $mid = open_window($x);
+my $bottom = open_window($x);
 
 #
 # Returns the input focus after sending the given command to i3 via IPC
@@ -37,7 +26,8 @@ diag("bottom id = " . $bottom->id);
 sub focus_after {
     my $msg = shift;
 
-    $i3->command($msg)->recv;
+    cmd $msg;
+    sync_with_i3 $x;
     return $x->input_focus;
 }
 
index f8143979164e777cc19b5e6f9b5ce3d370eb6652..b5be284c3b0cbde3eedc9b6387643a490100d06c 100644 (file)
@@ -4,45 +4,26 @@
 # over an unfocused tiling client and destroying the floating one again.
 
 use i3test;
-use X11::XCB qw(:all);
-
-BEGIN {
-    use_ok('X11::XCB::Window') or BAIL_OUT('Could not load X11::XCB::Window');
-}
 
 my $x = X11::XCB::Connection->new;
 
-my $i3 = i3(get_socket_path());
 fresh_workspace;
 
 cmd 'split h';
-my $tiled_left = open_standard_window($x);
-my $tiled_right = open_standard_window($x);
-
-sleep 0.25;
+my $tiled_left = open_window($x);
+my $tiled_right = open_window($x);
 
 # Get input focus before creating the floating window
 my $focus = $x->input_focus;
 
 # Create a floating window which is smaller than the minimum enforced size of i3
-my $window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 1, 1, 30, 30],
-    background_color => '#C0C0C0',
-    type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'),
-);
-
-isa_ok($window, 'X11::XCB::Window');
-
-$window->map;
+my $window = open_floating_window($x);
 
-sleep 1;
-sleep 0.25;
 is($x->input_focus, $window->id, 'floating window focused');
 
 $window->unmap;
 
-sleep 0.25;
+wait_for_unmap($x);
 
 is($x->input_focus, $focus, 'Focus correctly restored');
 
index 1cb205ed9a192188206303973a90bedee600fcc0..cc285f32788162d946c1dac5a47d2830d725d8ea 100644 (file)
@@ -27,9 +27,7 @@ $i3->command('9')->recv;
 #####################################################################
 
 my $top = i3test::open_standard_window($x);
-sleep(0.25);
 my $mid = i3test::open_standard_window($x);
-sleep(0.25);
 my $bottom = i3test::open_standard_window($x);
 sleep(0.25);
 
index 3f0a5195a70737f4956cf9f3a75f57618864f8bf..cad54c26c81aa17228591437ff735b08aaab7442 100644 (file)
@@ -10,7 +10,6 @@ BEGIN {
 }
 
 my $x = X11::XCB::Connection->new;
-my $i3 = i3(get_socket_path());
 
 #####################################################################
 # verify that there is no dock window yet
@@ -30,17 +29,9 @@ my $screens = $x->screens;
 my $primary = first { $_->primary } @{$screens};
 
 # TODO: focus the primary screen before
-
-my $window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#FF0000',
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
-);
-
-$window->map;
-
-sleep 0.25;
+my $window = open_window($x, {
+        window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
+    });
 
 my $rect = $window->rect;
 is($rect->width, $primary->rect->width, 'dock client is as wide as the screen');
@@ -67,7 +58,7 @@ is($docknode->{rect}->{height}, 30, 'dock node has unchanged height');
 
 $window->rect(X11::XCB::Rect->new(x => 0, y => 0, width => 50, height => 40));
 
-sleep 0.25;
+sync_with_i3 $x;
 
 @docked = get_dock_clients('top');
 is(@docked, 1, 'one dock client found');
@@ -82,7 +73,7 @@ is($docknode->{rect}->{height}, 40, 'dock height changed');
 
 $window->destroy;
 
-sleep 0.25;
+wait_for_unmap $x;
 
 @docked = get_dock_clients();
 is(@docked, 0, 'no more dock clients');
@@ -91,16 +82,11 @@ is(@docked, 0, 'no more dock clients');
 # check if it gets placed on bottom (by coordinates)
 #####################################################################
 
-$window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 1000, 30, 30],
-    background_color => '#FF0000',
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
-);
-
-$window->map;
-
-sleep 0.25;
+$window = open_window($x, {
+        rect => [ 0, 1000, 30, 30 ],
+        background_color => '#FF0000',
+        window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
+    });
 
 my $rect = $window->rect;
 is($rect->width, $primary->rect->width, 'dock client is as wide as the screen');
@@ -111,7 +97,7 @@ is(@docked, 1, 'dock client on bottom');
 
 $window->destroy;
 
-sleep 0.25;
+wait_for_unmap $x;
 
 @docked = get_dock_clients();
 is(@docked, 0, 'no more dock clients');
@@ -120,12 +106,12 @@ is(@docked, 0, 'no more dock clients');
 # check if it gets placed on bottom (by hint)
 #####################################################################
 
-$window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 1000, 30, 30],
-    background_color => '#FF0000',
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
-);
+$window = open_window($x, {
+        dont_map => 1,
+        rect => [ 0, 1000, 30, 30 ],
+        background_color => '#FF0000',
+        window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
+    });
 
 $window->_create();
 
@@ -145,24 +131,24 @@ $x->change_property(
 
 $window->map;
 
-sleep 0.25;
+wait_for_map $x;
 
 @docked = get_dock_clients('top');
 is(@docked, 1, 'dock client on top');
 
 $window->destroy;
 
-sleep 0.25;
+wait_for_unmap $x;
 
 @docked = get_dock_clients();
 is(@docked, 0, 'no more dock clients');
 
-$window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 1000, 30, 30],
-    background_color => '#FF0000',
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
-);
+$window = open_window($x, {
+        dont_map => 1,
+        rect => [ 0, 1000, 30, 30 ],
+        background_color => '#FF0000',
+        window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
+    });
 
 $window->_create();
 
@@ -182,7 +168,7 @@ $x->change_property(
 
 $window->map;
 
-sleep 0.25;
+wait_for_map $x;
 
 @docked = get_dock_clients('bottom');
 is(@docked, 1, 'dock client on bottom');
@@ -194,17 +180,16 @@ $window->destroy;
 # regression test: transient dock client
 #####################################################################
 
-my $fwindow = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#FF0000',
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
-);
+$fwindow = open_window($x, {
+        dont_map => 1,
+        background_color => '#FF0000',
+        window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
+    });
 
 $fwindow->transient_for($window);
 $fwindow->map;
 
-sleep 0.25;
+wait_for_map $x;
 
 does_i3_live;
 
index 542dc828bd146fa316fc881493f603152ada0249..903fa0c400b8c3c8b6dea016cff6e477570dae9c 100644 (file)
@@ -2,16 +2,10 @@
 # vim:ts=4:sw=4:expandtab
 
 use i3test;
-use X11::XCB qw(:all);
-use Digest::SHA1 qw(sha1_base64);
-
-BEGIN {
-    use_ok('X11::XCB::Connection') or BAIL_OUT('Cannot load X11::XCB::Connection');
-}
+use File::Temp;
 
 my $x = X11::XCB::Connection->new;
 
-my $i3 = i3(get_socket_path());
 my $tmp = fresh_workspace;
 
 cmd 'split h';
@@ -20,25 +14,19 @@ cmd 'split h';
 # Create two windows and make sure focus switching works
 #####################################################################
 
-my $top = open_standard_window($x);
-sleep 0.25;
-my $mid = open_standard_window($x);
-sleep 0.25;
-my $bottom = open_standard_window($x);
-sleep 0.25;
-
-diag("top id = " . $top->id);
-diag("mid id = " . $mid->id);
-diag("bottom id = " . $bottom->id);
+my $top = open_window($x);
+my $mid = open_window($x);
+my $bottom = open_window($x);
 
 #
 # Returns the input focus after sending the given command to i3 via IPC
-# end sleeping for half a second to make sure i3 reacted
+# and syncing with i3
 #
 sub focus_after {
     my $msg = shift;
 
     cmd $msg;
+    sync_with_i3($x);
     return $x->input_focus;
 }
 
@@ -52,12 +40,12 @@ is($focus, $mid->id, "Middle window focused");
 # Now goto a mark which does not exist
 #####################################################################
 
-my $random_mark = sha1_base64(rand());
+my $random_mark = mktemp('mark.XXXXXX');
 
 $focus = focus_after(qq|[con_mark="$random_mark"] focus|);
 is($focus, $mid->id, "focus unchanged");
 
-$i3->command("mark $random_mark")->recv;
+cmd "mark $random_mark";
 
 $focus = focus_after('focus left');
 is($focus, $top->id, "Top window focused");
index 09297df0d6015eb6551900f3801215ba3329b10a..ac3387a9ef7afcd69120ca25ca316a2b02005e2d 100644 (file)
@@ -1,8 +1,5 @@
 #!perl
 # vim:ts=4:sw=4:expandtab
-# Beware that this test uses workspace 9 to perform some tests (it expects
-# the workspace to be empty).
-# TODO: skip it by default?
 
 use i3test;
 use X11::XCB qw(:all);
@@ -19,31 +16,22 @@ fresh_workspace;
 # Create a floating window and see if resizing works
 #####################################################################
 
-# Create a floating window
-my $window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#C0C0C0',
-    # replace the type with 'utility' as soon as the coercion works again in X11::XCB
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'),
-);
-
-isa_ok($window, 'X11::XCB::Window');
-
-$window->map;
-sleep 0.25;
+my $window = open_floating_window($x);
 
 # See if configurerequests cause window movements (they should not)
 my ($a, $t) = $window->rect;
 $window->rect(X11::XCB::Rect->new(x => $a->x, y => $a->y, width => $a->width, height => $a->height));
 
-sleep 0.25;
+sync_with_i3($x);
+
 my ($na, $nt) = $window->rect;
 is_deeply($na, $a, 'Rects are equal after configurerequest');
 
 sub test_resize {
     $window->rect(X11::XCB::Rect->new(x => 0, y => 0, width => 100, height => 100));
 
+    sync_with_i3($x);
+
     my ($absolute, $top) = $window->rect;
 
     # Make sure the width/height are different from what we’re gonna test, so
@@ -52,7 +40,8 @@ sub test_resize {
     isnt($absolute->height, 500, 'height != 500');
 
     $window->rect(X11::XCB::Rect->new(x => 0, y => 0, width => 300, height => 500));
-    sleep 0.25;
+
+    sync_with_i3($x);
 
     ($absolute, $top) = $window->rect;
 
index f40b72fb9c7691b9894b1da2d0d02317db9cfd85..7954408fa2bf30efaa30d590f0c16ef1fedf2ea2 100644 (file)
@@ -19,8 +19,8 @@ my $tmp = fresh_workspace;
 
 cmd 'split v';
 
-my $top = open_standard_window($x);
-my $bottom = open_standard_window($x);
+my $top = open_window($x);
+my $bottom = open_window($x);
 
 my @urgent = grep { $_->{urgent} } @{get_ws_content($tmp)};
 is(@urgent, 0, 'no window got the urgent flag');
@@ -31,7 +31,7 @@ is(@urgent, 0, 'no window got the urgent flag');
 # Add the urgency hint, switch to a different workspace and back again
 #####################################################################
 $top->add_hint('urgency');
-sleep 0.5;
+sync_with_i3($x);
 
 @content = @{get_ws_content($tmp)};
 @urgent = grep { $_->{urgent} } @content;
@@ -48,7 +48,7 @@ cmd '[id="' . $top->id . '"] focus';
 is(@urgent, 0, 'no window got the urgent flag after focusing');
 
 $top->add_hint('urgency');
-sleep 0.5;
+sync_with_i3($x);
 
 @urgent = grep { $_->{urgent} } @{get_ws_content($tmp)};
 is(@urgent, 0, 'no window got the urgent flag after re-setting urgency hint');
@@ -62,7 +62,7 @@ ok(!$ws->{urgent}, 'urgent flag not set on workspace');
 my $otmp = fresh_workspace;
 
 $top->add_hint('urgency');
-sleep 0.5;
+sync_with_i3($x);
 
 $ws = get_ws($tmp);
 ok($ws->{urgent}, 'urgent flag set on workspace');
index 98978eb3a76ed50343883d66104ca99d09e4cb50..6f7ffce032e3d440e99552c8e5ef5b9aa97037f5 100644 (file)
@@ -9,7 +9,6 @@ BEGIN {
 }
 
 my $x = X11::XCB::Connection->new;
-my $i3 = i3(get_socket_path());
 
 my $tmp = fresh_workspace;
 
@@ -21,74 +20,42 @@ my $tmp = fresh_workspace;
 # one of both (depending on your screen resolution) will be positioned wrong.
 ####################################################################################
 
-my $left = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [0, 0, 30, 30],
-    background_color => '#FF0000',
-);
-
-$left->name('Left');
-$left->map;
-
-my $right = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [0, 0, 30, 30],
-    background_color => '#FF0000',
-);
-
-$right->name('Right');
-$right->map;
-
-sleep 0.25;
+my $left = open_window($x, { name => 'Left' });
+my $right = open_window($x, { name => 'Right' });
 
 my ($abs, $rgeom) = $right->rect;
 
-my $child = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30 ],
-    background_color => '#C0C0C0',
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'),
-);
-
-$child->name('Child window');
+my $child = open_floating_window($x, {
+        dont_map => 1,
+        name => 'Child window',
+    });
 $child->client_leader($right);
 $child->map;
 
-sleep 0.25;
+ok(wait_for_map($x), 'child window mapped');
 
 my $cgeom;
 ($abs, $cgeom) = $child->rect;
 cmp_ok($cgeom->x, '>=', $rgeom->x, 'Child X >= right container X');
 
-my $child2 = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30 ],
-    background_color => '#C0C0C0',
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'),
-);
-
-$child2->name('Child window 2');
+my $child2 = open_floating_window($x, {
+        dont_map => 1,
+        name => 'Child window 2',
+    });
 $child2->client_leader($left);
 $child2->map;
 
-sleep 0.25;
+ok(wait_for_map($x), 'second child window mapped');
 
 ($abs, $cgeom) = $child2->rect;
 cmp_ok(($cgeom->x + $cgeom->width), '<', $rgeom->x, 'child above left window');
 
 # check wm_transient_for
-
-
-my $fwindow = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#FF0000',
-);
-
+my $fwindow = open_window($x, { dont_map => 1 });
 $fwindow->transient_for($right);
 $fwindow->map;
 
-sleep 0.25;
+ok(wait_for_map($x), 'transient window mapped');
 
 my ($absolute, $top) = $fwindow->rect;
 ok($absolute->{x} != 0 && $absolute->{y} != 0, 'i3 did not map it to (0x0)');
@@ -100,16 +67,10 @@ SKIP: {
 # Create a parent window
 #####################################################################
 
-my $window = $x->root->create_child(
-class => WINDOW_CLASS_INPUT_OUTPUT,
-rect => [ 0, 0, 30, 30 ],
-background_color => '#C0C0C0',
-);
-
-$window->name('Parent window');
+my $window = open_window($x, { dont_map => 1, name => 'Parent window' });
 $window->map;
 
-sleep 0.25;
+ok(wait_for_map($x), 'parent window mapped');
 
 #########################################################################
 # Switch to a different workspace and open a child window. It should be opened
@@ -117,17 +78,11 @@ sleep 0.25;
 #########################################################################
 fresh_workspace;
 
-my $child = $x->root->create_child(
-class => WINDOW_CLASS_INPUT_OUTPUT,
-rect => [ 0, 0, 30, 30 ],
-background_color => '#C0C0C0',
-);
-
-$child->name('Child window');
+my $child = open_window($x, { dont_map => 1, name => 'Child window' });
 $child->client_leader($window);
 $child->map;
 
-sleep 0.25;
+ok(wait_for_map($x), 'child window mapped');
 
 isnt($x->input_focus, $child->id, "Child window focused");
 
index 19e2df3408fa83ba3a99843dfd17d9654d440f76..3c3b6cc675421a510dc8c25b723198c2e8bf5006 100644 (file)
@@ -98,4 +98,23 @@ cmd 'workspace "prev"';
 ok(workspace_exists('prev'), 'workspace "prev" exists');
 is(focused_ws(), 'prev', 'now on workspace prev');
 
+#####################################################################
+# check that the numbers are assigned/recognized correctly
+#####################################################################
+
+cmd "workspace 3: $tmp";
+my $ws = get_ws("3: $tmp");
+ok(defined($ws), "workspace 3: $tmp was created");
+is($ws->{num}, 3, 'workspace number is 3');
+
+cmd "workspace 0: $tmp";
+my $ws = get_ws("0: $tmp");
+ok(defined($ws), "workspace 0: $tmp was created");
+is($ws->{num}, 0, 'workspace number is 0');
+
+cmd "workspace aa: $tmp";
+my $ws = get_ws("aa: $tmp");
+ok(defined($ws), "workspace aa: $tmp was created");
+is($ws->{num}, -1, 'workspace number is -1');
+
 done_testing;
index 2332bc71c1d31170be5898e21993ef54c1206343..8b9d21d3e0da46fbc422bda5041cd9365fcbbb61 100644 (file)
@@ -12,20 +12,7 @@ ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
 
 # Open a new window
 my $x = X11::XCB::Connection->new;
-my $window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30 ],
-    background_color => '#C0C0C0',
-);
-
-$window->map;
-# give it some time to be picked up by the window manager
-# TODO: better check for $window->mapped or something like that?
-# maybe we can even wait for getting mapped?
-my $c = 0;
-while (@{get_ws_content($tmp)} == 0 and $c++ < 5) {
-    sleep 0.25;
-}
+my $window = open_window($x);
 my $content = get_ws_content($tmp);
 ok(@{$content} == 1, 'window mapped');
 my $win = $content->[0];
@@ -34,9 +21,10 @@ my $win = $content->[0];
 # first test that matches which should not match this window really do
 # not match it
 ######################################################################
-# TODO: use PCRE expressions
 # TODO: specify more match types
-cmd q|[class="*"] kill|;
+# we can match on any (non-empty) class here since that window does not have
+# WM_CLASS set
+cmd q|[class=".*"] kill|;
 cmd q|[con_id="99999"] kill|;
 
 $content = get_ws_content($tmp);
@@ -47,8 +35,7 @@ cmd 'nop now killing the window';
 my $id = $win->{id};
 cmd qq|[con_id="$id"] kill|;
 
-# give i3 some time to pick up the UnmapNotify event
-sleep 0.25;
+wait_for_unmap $x;
 
 cmd 'nop checking if its gone';
 $content = get_ws_content($tmp);
@@ -87,25 +74,27 @@ my $left = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#0000ff',
+    event_mask => [ 'structure_notify' ],
 );
 
 $left->_create;
 set_wm_class($left->id, 'special', 'special');
 $left->name('left');
 $left->map;
-sleep 0.25;
+ok(wait_for_map($x), 'left window mapped');
 
 my $right = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#0000ff',
+    event_mask => [ 'structure_notify' ],
 );
 
 $right->_create;
 set_wm_class($right->id, 'special', 'special');
 $right->name('right');
 $right->map;
-sleep 0.25;
+ok(wait_for_map($x), 'right window mapped');
 
 # two windows should be here
 $content = get_ws_content($tmp);
@@ -113,9 +102,69 @@ ok(@{$content} == 2, 'two windows opened');
 
 cmd '[class="special" title="left"] kill';
 
-sleep 0.25;
+sync_with_i3($x);
 
 $content = get_ws_content($tmp);
 is(@{$content}, 1, 'one window still there');
 
+######################################################################
+# check that regular expressions work
+######################################################################
+
+$tmp = fresh_workspace;
+
+$left = $x->root->create_child(
+    class => WINDOW_CLASS_INPUT_OUTPUT,
+    rect => [ 0, 0, 30, 30 ],
+    background_color => '#0000ff',
+    event_mask => [ 'structure_notify' ],
+);
+
+$left->_create;
+set_wm_class($left->id, 'special7', 'special7');
+$left->name('left');
+$left->map;
+ok(wait_for_map($x), 'left window mapped');
+
+# two windows should be here
+$content = get_ws_content($tmp);
+ok(@{$content} == 1, 'window opened');
+
+cmd '[class="^special[0-9]$"] kill';
+
+wait_for_unmap $x;
+
+$content = get_ws_content($tmp);
+is(@{$content}, 0, 'window killed');
+
+######################################################################
+# check that UTF-8 works when matching
+######################################################################
+
+$tmp = fresh_workspace;
+
+$left = $x->root->create_child(
+    class => WINDOW_CLASS_INPUT_OUTPUT,
+    rect => [ 0, 0, 30, 30 ],
+    background_color => '#0000ff',
+    event_mask => [ 'structure_notify' ],
+);
+
+$left->_create;
+set_wm_class($left->id, 'special7', 'special7');
+$left->name('ä 3');
+$left->map;
+ok(wait_for_map($x), 'left window mapped');
+
+# two windows should be here
+$content = get_ws_content($tmp);
+ok(@{$content} == 1, 'window opened');
+
+cmd '[title="^\w [3]$"] kill';
+
+wait_for_unmap $x;
+
+$content = get_ws_content($tmp);
+is(@{$content}, 0, 'window killed');
+
 done_testing;
index ac029eb14f85c1f2b6ade674c1baa7eb2c52f50d..8d22561359234e5643d3815e9fc3081e350c183f 100644 (file)
@@ -102,7 +102,7 @@ $first = open_empty_con($i3);
 $middle = open_empty_con($i3);
 # XXX: the $right empty con will be filled with the x11 window we are creating afterwards
 $right = open_empty_con($i3);
-my $win = open_standard_window($x, '#00ff00');
+my $win = open_window($x, { background_color => '#00ff00' });
 
 cmd qq|[con_id="$middle"] focus|;
 $win->destroy;
index 05897c88aa4e106f59ab575ccab89a9710388601..d2d77e8e0f2ae6abb6b14684776dfcf26b96c62b 100644 (file)
@@ -5,20 +5,13 @@
 #
 use i3test;
 
-my $i3 = i3(get_socket_path());
-
 my $x = X11::XCB::Connection->new;
 
 my $tmp = fresh_workspace;
 
 ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
 
-my $win = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => X11::XCB::Rect->new(x => 0, y => 0, width => 30, height => 30),
-    background_color => '#C0C0C0',
-);
-
+my $win = open_window($x, { dont_map => 1 });
 # XXX: we should check screen size. in screens with an AR of 2.0,
 # this is not a good idea.
 my $aspect = X11::XCB::Sizehints::Aspect->new;
@@ -28,11 +21,11 @@ $aspect->max_num(600);
 $aspect->max_den(300);
 $win->_create;
 $win->map;
-sleep 0.25;
+wait_for_map $x;
 $win->hints->aspect($aspect);
 $x->flush;
 
-sleep 0.25;
+sync_with_i3($x);
 
 my $rect = $win->rect;
 my $ar = $rect->width / $rect->height;
index 3f820ea5c5b92797c379a8e6dc1147b53ce5a816..4c5b562ff12ef21a40ca6df520baaf25bcdffaf6 100644 (file)
@@ -13,8 +13,8 @@ my $tmp = fresh_workspace;
 # 1: see if focus stays the same when toggling tiling/floating mode
 #############################################################################
 
-my $first = open_standard_window($x);
-my $second = open_standard_window($x);
+my $first = open_window($x);
+my $second = open_window($x);
 
 is($x->input_focus, $second->id, 'second window focused');
 
@@ -30,9 +30,9 @@ is($x->input_focus, $second->id, 'second window still focused after mode toggle'
 
 $tmp = fresh_workspace;
 
-$first = open_standard_window($x);    # window 2
-$second = open_standard_window($x);   # window 3
-my $third = open_standard_window($x); # window 4
+$first = open_window($x);    # window 2
+$second = open_window($x);   # window 3
+my $third = open_window($x); # window 4
 
 is($x->input_focus, $third->id, 'last container focused');
 
@@ -40,6 +40,8 @@ cmd 'floating enable';
 
 cmd '[id="' . $second->id . '"] focus';
 
+sync_with_i3($x);
+
 is($x->input_focus, $second->id, 'second con focused');
 
 cmd 'floating enable';
@@ -47,7 +49,8 @@ cmd 'floating enable';
 # now kill the third one (it's floating). focus should stay unchanged
 cmd '[id="' . $third->id . '"] kill';
 
-sleep 0.25;
+# TODO: wait for unmapnotify
+sync_with_i3($x);
 
 is($x->input_focus, $second->id, 'second con still focused after killing third');
 
@@ -59,9 +62,9 @@ is($x->input_focus, $second->id, 'second con still focused after killing third')
 
 $tmp = fresh_workspace;
 
-$first = open_standard_window($x, '#ff0000');    # window 5
-$second = open_standard_window($x, '#00ff00');   # window 6
-my $third = open_standard_window($x, '#0000ff'); # window 7
+$first = open_window($x, '#ff0000');    # window 5
+$second = open_window($x, '#00ff00');   # window 6
+my $third = open_window($x, '#0000ff'); # window 7
 
 is($x->input_focus, $third->id, 'last container focused');
 
@@ -69,6 +72,8 @@ cmd 'floating enable';
 
 cmd '[id="' . $second->id . '"] focus';
 
+sync_with_i3($x);
+
 is($x->input_focus, $second->id, 'second con focused');
 
 cmd 'floating enable';
@@ -77,13 +82,14 @@ cmd 'floating enable';
 # also floating
 cmd 'kill';
 
-sleep 0.25;
+# TODO: wait for unmapnotify
+sync_with_i3($x);
 
 is($x->input_focus, $third->id, 'third con focused');
 
 cmd 'kill';
-
-sleep 0.25;
+# TODO: wait for unmapnotify
+sync_with_i3($x);
 
 is($x->input_focus, $first->id, 'first con focused after killing all floating cons');
 
@@ -93,11 +99,11 @@ is($x->input_focus, $first->id, 'first con focused after killing all floating co
 
 $tmp = fresh_workspace;
 
-$first = open_standard_window($x, '#ff0000');    # window 5
+$first = open_window($x, { background_color => '#ff0000' });    # window 5
 cmd 'split v';
 cmd 'layout stacked';
-$second = open_standard_window($x, '#00ff00');   # window 6
-$third = open_standard_window($x, '#0000ff'); # window 7
+$second = open_window($x, { background_color => '#00ff00' });   # window 6
+$third = open_window($x, { background_color => '#0000ff' }); # window 7
 
 is($x->input_focus, $third->id, 'last container focused');
 
@@ -105,23 +111,26 @@ cmd 'floating enable';
 
 cmd '[id="' . $second->id . '"] focus';
 
+sync_with_i3($x);
+
 is($x->input_focus, $second->id, 'second con focused');
 
 cmd 'floating enable';
 
-sleep 0.5;
+sync_with_i3($x);
 
 # now kill the second one. focus should fall back to the third one, which is
 # also floating
 cmd 'kill';
 
-sleep 0.25;
+# TODO: wait for unmapnotify
+sync_with_i3($x);
 
-is($x->input_focus, $third->id, 'second con focused');
+is($x->input_focus, $third->id, 'third con focused');
 
 cmd 'kill';
-
-sleep 0.25;
+# TODO: wait for unmapnotify
+sync_with_i3($x);
 
 is($x->input_focus, $first->id, 'first con focused after killing all floating cons');
 
@@ -131,8 +140,10 @@ is($x->input_focus, $first->id, 'first con focused after killing all floating co
 
 $tmp = fresh_workspace;
 
-$first = open_standard_window($x, '#ff0000');    # window 8
-$second = open_standard_window($x, '#00ff00');   # window 9
+$first = open_window($x, { background_color => '#ff0000' });    # window 8
+$second = open_window($x, { background_color => '#00ff00' });   # window 9
+
+sync_with_i3($x);
 
 is($x->input_focus, $second->id, 'second container focused');
 
@@ -142,33 +153,76 @@ is($x->input_focus, $second->id, 'second container focused');
 
 cmd 'focus tiling';
 
-sleep 0.25;
+sync_with_i3($x);
 
 is($x->input_focus, $first->id, 'first (tiling) container focused');
 
 cmd 'focus floating';
 
-sleep 0.25;
+sync_with_i3($x);
 
 is($x->input_focus, $second->id, 'second (floating) container focused');
 
 cmd 'focus floating';
 
-sleep 0.25;
+sync_with_i3($x);
 
 is($x->input_focus, $second->id, 'second (floating) container still focused');
 
 cmd 'focus mode_toggle';
 
-sleep 0.25;
+sync_with_i3($x);
 
 is($x->input_focus, $first->id, 'first (tiling) container focused');
 
 cmd 'focus mode_toggle';
 
-sleep 0.25;
+sync_with_i3($x);
 
 is($x->input_focus, $second->id, 'second (floating) container focused');
 
+#############################################################################
+# 6: see if switching floating focus using the focus left/right command works
+#############################################################################
+
+$tmp = fresh_workspace;
+
+$first = open_floating_window($x, { background_color => '#ff0000' });# window 10
+$second = open_floating_window($x, { background_color => '#00ff00' }); # window 11
+$third = open_floating_window($x, { background_color => '#0000ff' }); # window 12
+
+sync_with_i3($x);
+
+is($x->input_focus, $third->id, 'third container focused');
+
+cmd 'focus left';
+
+sync_with_i3($x);
+
+is($x->input_focus, $second->id, 'second container focused');
+
+cmd 'focus left';
+
+sync_with_i3($x);
+
+is($x->input_focus, $first->id, 'first container focused');
+
+cmd 'focus left';
+
+sync_with_i3($x);
+
+is($x->input_focus, $third->id, 'focus wrapped to third container');
+
+cmd 'focus right';
+
+sync_with_i3($x);
+
+is($x->input_focus, $first->id, 'focus wrapped to first container');
+
+cmd 'focus right';
+
+sync_with_i3($x);
+
+is($x->input_focus, $second->id, 'focus on second container');
 
 done_testing;
index f33d04dbcd1b093a433bdcdb433ebe843e8a083d..a6e0e405e867de092fa8a42035a3dcc45576fb89 100644 (file)
@@ -22,20 +22,7 @@ ok(workspace_exists($tmp), "workspace $tmp exists");
 my $x = X11::XCB::Connection->new;
 
 # Create a floating window which is smaller than the minimum enforced size of i3
-my $window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#C0C0C0',
-    # replace the type with 'utility' as soon as the coercion works again in X11::XCB
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'),
-);
-
-isa_ok($window, 'X11::XCB::Window');
-
-$window->map;
-
-sleep 0.25;
-
+my $window = open_floating_window($x);
 ok($window->mapped, 'Window is mapped');
 
 # switch to a different workspace, see if the window is still mapped?
index 3ae4b12d95a2e3a50030d53405f0147434021793..ab1a33d313e7d9478476e30258a0828baf437278 100644 (file)
@@ -21,27 +21,14 @@ my $tmp = fresh_workspace;
 my $x = X11::XCB::Connection->new;
 
 # Create a floating window which is smaller than the minimum enforced size of i3
-my $window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#C0C0C0',
-    # replace the type with 'utility' as soon as the coercion works again in X11::XCB
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'),
-);
-
-isa_ok($window, 'X11::XCB::Window');
-
-$window->map;
-
-sleep 0.25;
-
+my $window = open_floating_window($x);
 ok($window->mapped, 'Window is mapped');
 
 # switch to a different workspace, see if the window is still mapped?
 
 my $otmp = fresh_workspace;
 
-sleep 0.25;
+sync_with_i3($x);
 
 ok(!$window->mapped, 'Window is not mapped after switching ws');
 
index 31bddafde61df1a71fbff77e315d3541713ee79f..b08190a22036ba320f40848fcb83294411917fd3 100644 (file)
@@ -5,7 +5,6 @@
 
 use i3test;
 use X11::XCB qw(:all);
-use Time::HiRes qw(sleep);
 
 BEGIN {
     use_ok('X11::XCB::Window');
@@ -22,20 +21,7 @@ my $tmp = fresh_workspace;
 my $x = X11::XCB::Connection->new;
 
 # Create a floating window
-my $window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#C0C0C0',
-    # replace the type with 'utility' as soon as the coercion works again in X11::XCB
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'),
-);
-
-isa_ok($window, 'X11::XCB::Window');
-
-$window->map;
-
-sleep 0.25;
-
+my $window = open_floating_window($x);
 ok($window->mapped, 'Window is mapped');
 
 my $ws = get_ws($tmp);
@@ -45,17 +31,7 @@ is(@{$ws->{floating_nodes}}, 1, 'one floating node');
 is(@{$nodes}, 0, 'no tiling nodes');
 
 # Create a tiling window
-my $twindow = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#C0C0C0',
-);
-
-isa_ok($twindow, 'X11::XCB::Window');
-
-$twindow->map;
-
-sleep 0.25;
+my $twindow = open_window($x);
 
 ($nodes, $focus) = get_ws_content($tmp);
 
@@ -68,8 +44,8 @@ is(@{$nodes}, 1, 'one tiling node');
 
 $tmp = fresh_workspace;
 
-my $first = open_standard_window($x);
-my $second = open_standard_window($x);
+my $first = open_window($x);
+my $second = open_window($x);
 
 cmd 'layout stacked';
 
@@ -78,27 +54,14 @@ is(@{$ws->{floating_nodes}}, 0, 'no floating nodes so far');
 is(@{$ws->{nodes}}, 1, 'one tiling node (stacked con)');
 
 # Create a floating window
-my $window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#C0C0C0',
-    # replace the type with 'utility' as soon as the coercion works again in X11::XCB
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'),
-);
-
-isa_ok($window, 'X11::XCB::Window');
-
-$window->map;
-
-sleep 0.25;
-
+my $window = open_floating_window($x);
 ok($window->mapped, 'Window is mapped');
 
 $ws = get_ws($tmp);
 is(@{$ws->{floating_nodes}}, 1, 'one floating nodes');
 is(@{$ws->{nodes}}, 1, 'one tiling node (stacked con)');
 
-my $third = open_standard_window($x);
+my $third = open_window($x);
 
 
 $ws = get_ws($tmp);
index 3afd281b2329a6310ea6982e8c89b504588d9cd6..31f013efd8cb2646d436f3f5d2e1bf9de13b0371 100644 (file)
@@ -30,7 +30,7 @@ check_order('workspace order alright before testing');
 
 cmd "workspace 93";
 
-open_standard_window($x);
+open_window($x);
 
 my @ws = @{$i3->get_workspaces->recv};
 my @f = grep { defined($_->{num}) && $_->{num} == 93 } @ws;
@@ -38,23 +38,23 @@ is(@f, 1, 'ws 93 found by num');
 check_order('workspace order alright after opening 93');
 
 cmd "workspace 92";
-open_standard_window($x);
+open_window($x);
 check_order('workspace order alright after opening 92');
 
 cmd "workspace 94";
-open_standard_window($x);
+open_window($x);
 check_order('workspace order alright after opening 94');
 
 cmd "workspace 96";
-open_standard_window($x);
+open_window($x);
 check_order('workspace order alright after opening 96');
 
 cmd "workspace foo";
-open_standard_window($x);
+open_window($x);
 check_order('workspace order alright after opening foo');
 
 cmd "workspace 91";
-open_standard_window($x);
+open_window($x);
 check_order('workspace order alright after opening 91');
 
 done_testing;
index 9df220d16dab4920ddee4f3bb882f889b889330d..fb77f01e83b75ebefb943da352e4cfde2af09162 100644 (file)
@@ -4,7 +4,6 @@
 # bug introduced by 77d0d42ed2d7ac8cafe267c92b35a81c1b9491eb
 use i3test;
 use X11::XCB qw(:all);
-use Time::HiRes qw(sleep);
 
 BEGIN {
     use_ok('X11::XCB::Window');
@@ -25,12 +24,11 @@ sub check_order {
 
 my $tmp = fresh_workspace;
 
-my $left = open_standard_window($x);
-sleep 0.25;
-my $mid = open_standard_window($x);
-sleep 0.25;
-my $right = open_standard_window($x);
-sleep 0.25;
+my $left = open_window($x);
+my $mid = open_window($x);
+my $right = open_window($x);
+
+sync_with_i3($x);
 
 diag("left = " . $left->id . ", mid = " . $mid->id . ", right = " . $right->id);
 
index 1d1b1206667c42de6c1ce04566535b87609780d5..8691a044fda77ffd3501e19a633a302a59cc5551 100644 (file)
@@ -14,10 +14,10 @@ my $tmp = fresh_workspace;
 
 cmd 'split v';
 
-my $top = open_standard_window($x);
-sleep 0.25;
-my $bottom = open_standard_window($x);
-sleep 0.25;
+my $top = open_window($x);
+my $bottom = open_window($x);
+
+sync_with_i3($x);
 
 diag("top = " . $top->id . ", bottom = " . $bottom->id);
 
@@ -54,10 +54,8 @@ $tmp = fresh_workspace;
 
 cmd 'split v';
 
-$top = open_standard_window($x);
-sleep 0.25;
-$bottom = open_standard_window($x);
-sleep 0.25;
+$top = open_window($x);
+$bottom = open_window($x);
 
 cmd 'split h';
 cmd 'layout stacked';
@@ -78,8 +76,7 @@ is($nodes->[1]->{percent}, 0.75, 'bottom window got 75%');
 
 $tmp = fresh_workspace;
 
-$top = open_standard_window($x);
-sleep 0.25;
+$top = open_window($x);
 
 cmd 'floating enable';
 
index 98b93ef1793cb2457b5c70da98e8dd1f322c2d14..904252e74c16fcf503e15144892d308bdddfca95 100644 (file)
@@ -17,12 +17,9 @@ my $x = X11::XCB::Connection->new;
 
 my $tmp = fresh_workspace;
 
-my $left = open_standard_window($x);
-sleep 0.25;
-my $mid = open_standard_window($x);
-sleep 0.25;
-my $right = open_standard_window($x);
-sleep 0.25;
+my $left = open_window($x);
+my $mid = open_window($x);
+my $right = open_window($x);
 
 cmd 'move before v';
 cmd 'move after h';
index a4e90d4d1bfa96202b80e8f52e55eac90db5b63c..bc1302bbe645d571107ea60719e0f636ba5fec39 100644 (file)
@@ -2,7 +2,6 @@
 # vim:ts=4:sw=4:expandtab
 #
 use X11::XCB qw(:all);
-use Time::HiRes qw(sleep);
 use i3test;
 
 BEGIN {
@@ -13,14 +12,11 @@ my $x = X11::XCB::Connection->new;
 
 my $tmp = fresh_workspace;
 
-my $left = open_standard_window($x);
-sleep 0.25;
-my $mid = open_standard_window($x);
-sleep 0.25;
+my $left = open_window($x);
+my $mid = open_window($x);
 
 cmd 'split v';
-my $bottom = open_standard_window($x);
-sleep 0.25;
+my $bottom = open_window($x);
 
 my ($nodes, $focus) = get_ws_content($tmp);
 
@@ -28,23 +24,8 @@ my ($nodes, $focus) = get_ws_content($tmp);
 # 1: open a floating window, get it mapped
 #############################################################################
 
-my $x = X11::XCB::Connection->new;
-
 # Create a floating window
-my $window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#C0C0C0',
-    # replace the type with 'utility' as soon as the coercion works again in X11::XCB
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY'),
-);
-
-isa_ok($window, 'X11::XCB::Window');
-
-$window->map;
-
-sleep 0.25;
-
+my $window = open_floating_window($x);
 ok($window->mapped, 'Window is mapped');
 
 ($nodes, $focus) = get_ws_content($tmp);
index 6e04916d98edddd05af68e1c97b632a823e57000..771ace320f59f6b1f7b9754b646ec63e41d4d15c 100644 (file)
@@ -16,12 +16,9 @@ my $x = X11::XCB::Connection->new;
 
 my $tmp = fresh_workspace;
 
-my $left = open_standard_window($x);
-sleep 0.25;
-my $mid = open_standard_window($x);
-sleep 0.25;
-my $right = open_standard_window($x);
-sleep 0.25;
+my $left = open_window($x);
+my $mid = open_window($x);
+my $right = open_window($x);
 
 # go to workspace level
 cmd 'level up';
index 0bec541832ba18de45c79361eacd0d272f6bd8a7..6f9dfc40d47a36ceb2de6b888ec01a8fd68e8d70 100644 (file)
@@ -16,21 +16,21 @@ my $x = X11::XCB::Connection->new;
 my $tmp = fresh_workspace;
 
 # open a tiling window on the first workspace
-open_standard_window($x);
-sleep 0.25;
+open_window($x);
+#sleep 0.25;
 my $first = get_focused($tmp);
 
 # on a different ws, open a floating window
 my $otmp = fresh_workspace;
-open_standard_window($x);
-sleep 0.25;
+open_window($x);
+#sleep 0.25;
 my $float = get_focused($otmp);
 cmd 'mode toggle';
-sleep 0.25;
+#sleep 0.25;
 
 # move the floating con to first workspace
 cmd "move workspace $tmp";
-sleep 0.25;
+#sleep 0.25;
 
 # switch to the first ws and check focus
 is(get_focused($tmp), $float, 'floating client correctly focused');
index a4f7bebc597611d2b16dca817aba70aefdba3fb4..e294e6bd3de9fe70dc312f72bf19011577adfcc4 100644 (file)
@@ -11,7 +11,6 @@ BEGIN {
 }
 
 my $x = X11::XCB::Connection->new;
-my $i3 = i3(get_socket_path());
 
 my $tmp = fresh_workspace;
 
@@ -26,16 +25,10 @@ is(@docked, 0, 'no dock clients yet');
 
 # open a dock client
 
-my $window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#FF0000',
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
-);
-
-$window->map;
-
-sleep 0.25;
+my $window = open_window($x, {
+        background_color => '#FF0000',
+        window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
+    });
 
 #####################################################################
 # check that we can find it in the layout tree at the expected position
@@ -69,7 +62,7 @@ is($docknode->{rect}->{height}, 30, 'dock node has unchanged height after restar
 
 $window->destroy;
 
-sleep 0.25;
+wait_for_unmap $x;
 
 @docked = get_dock_clients;
 is(@docked, 0, 'no dock clients found');
@@ -78,17 +71,12 @@ is(@docked, 0, 'no dock clients found');
 # create a dock client with a 1px border
 #####################################################################
 
-$window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    border => 1,
-    rect => [ 0, 0, 30, 20],
-    background_color => '#00FF00',
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
-);
-
-$window->map;
-
-sleep 0.25;
+$window = open_window($x, {
+        border => 1,
+        rect => [ 0, 0, 30, 20 ],
+        background_color => '#00FF00',
+        window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
+    });
 
 @docked = get_dock_clients;
 is(@docked, 1, 'one dock client found');
index 16e62c2078da89334c7f04788bd460b69c245bed..db0b6e9c63649bab76d70e31bc45b42dee62b9f2 100644 (file)
@@ -11,17 +11,7 @@ my $tmp = fresh_workspace;
 my $x = X11::XCB::Connection->new;
 
 # Create a floating window which is smaller than the minimum enforced size of i3
-my $window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 400, 150],
-    background_color => '#C0C0C0',
-);
-
-isa_ok($window, 'X11::XCB::Window');
-
-$window->map;
-
-sleep 0.25;
+my $window = open_window($x, { rect => [ 0, 0, 400, 150 ] });
 
 my ($absolute, $top) = $window->rect;
 
@@ -30,7 +20,7 @@ cmp_ok($absolute->{width}, '>', 400, 'i3 raised the width');
 cmp_ok($absolute->{height}, '>', 150, 'i3 raised the height');
 
 cmd 'floating toggle';
-sleep 0.25;
+sync_with_i3($x);
 
 ($absolute, $top) = $window->rect;
 
index 36070db1b124b05ba29ef003aa52d5cfd218fc99..21cb96967ba246ae5a03bca05520d7819e61b3df 100644 (file)
@@ -11,7 +11,6 @@ BEGIN {
 }
 
 my $x = X11::XCB::Connection->new;
-my $i3 = i3(get_socket_path());
 
 my $tmp = fresh_workspace;
 
@@ -28,31 +27,19 @@ is(@docked, 0, 'no dock clients yet');
 # open a dock client
 #####################################################################
 
-my $first = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#FF0000',
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
-);
-
-$first->map;
-
-sleep 0.25;
+my $first = open_window($x, {
+        background_color => '#FF0000',
+        window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
+    });
 
 #####################################################################
 # Open a second dock client
 #####################################################################
 
-my $second = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30],
-    background_color => '#FF0000',
-    window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
-);
-
-$second->map;
-
-sleep 0.25;
+my $second = open_window($x, {
+        background_color => '#FF0000',
+        window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
+    });
 
 #####################################################################
 # Kill the second dock client
index ecffbb12fa2c3ddba57f2a2a57be598f76baf4b5..5de05e8b2dceb44fb55f9becef9f9d8ab7df71d0 100644 (file)
@@ -12,7 +12,6 @@ BEGIN {
 }
 
 my $x = X11::XCB::Connection->new;
-my $i3 = i3(get_socket_path());
 
 my $tmp = fresh_workspace;
 
@@ -20,29 +19,19 @@ my $tmp = fresh_workspace;
 # open a window with 200x80
 #####################################################################
 
-my $first = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 200, 80],
-    background_color => '#FF0000',
-);
-
-$first->map;
-
-sleep 0.25;
+my $first = open_window($x, {
+        rect => [ 0, 0, 200, 80],
+        background_color => '#FF0000',
+    });
 
 #####################################################################
 # Open a second window with 300x90
 #####################################################################
 
-my $second = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 300, 90],
-    background_color => '#00FF00',
-);
-
-$second->map;
-
-sleep 0.25;
+my $second = open_window($x, {
+        rect => [ 0, 0, 300, 90],
+        background_color => '#00FF00',
+    });
 
 #####################################################################
 # Set the parent to floating
index ee60dc7ae0bc5e17d49edc6d6332030a88fa08c5..a559b5a5134cb81c4238924167d7bc760797b9d3 100644 (file)
@@ -20,7 +20,7 @@ my $tmp = fresh_workspace;
 # open the left window
 #####################################################################
 
-my $left = open_standard_window($x, '#ff0000');
+my $left = open_window($x, { background_color => '#ff0000' });
 
 is($x->input_focus, $left->id, 'left window focused');
 
@@ -30,7 +30,7 @@ diag("left = " . $left->id);
 # Open the right window
 #####################################################################
 
-my $right = open_standard_window($x, '#00ff00');
+my $right = open_window($x, { background_color => '#00ff00' });
 
 diag("right = " . $right->id);
 
@@ -44,7 +44,15 @@ cmd 'fullscreen';
 # Open a third window
 #####################################################################
 
-my $third = open_standard_window($x, '#0000ff');
+my $third = open_window($x, {
+        background_color => '#0000ff',
+        name => 'Third window',
+        dont_map => 1,
+    });
+
+$third->map;
+
+sync_with_i3 $x;
 
 diag("third = " . $third->id);
 
@@ -56,7 +64,7 @@ cmd "move workspace $tmp2";
 
 # verify that the third window has the focus
 
-sleep 0.25;
+sync_with_i3($x);
 
 is($x->input_focus, $third->id, 'third window focused');
 
index 3e0b2fe156c28a82f44855e5ec1137c0e77bc858..7a101dbc8cf0dd777602262283ac098a01ed80fe 100644 (file)
@@ -11,7 +11,6 @@ BEGIN {
 }
 
 my $x = X11::XCB::Connection->new;
-my $i3 = i3(get_socket_path());
 
 my $tmp = fresh_workspace;
 
@@ -19,7 +18,7 @@ my $tmp = fresh_workspace;
 # open a window, verify it’s not in fullscreen mode
 #####################################################################
 
-my $win = open_standard_window($x);
+my $win = open_window($x);
 
 my $nodes = get_ws_content $tmp;
 is(@$nodes, 1, 'exactly one client');
index 04c785ae30983fd5b28ab238d95e082e61964f07..a90ce1c3d04641cb6e18a8167cad7a7b4931f13e 100644 (file)
@@ -7,106 +7,35 @@ use X11::XCB qw(:all);
 use i3test;
 use v5.10;
 
-BEGIN {
-    use_ok('EV');
-    use_ok('AnyEvent');
-    use_ok('X11::XCB::Window');
-    use_ok('X11::XCB::Event::Generic');
-    use_ok('X11::XCB::Event::MapNotify');
-    use_ok('X11::XCB::Event::ClientMessage');
-}
-
 my $x = X11::XCB::Connection->new;
-my $i3 = i3(get_socket_path());
 
 subtest 'Window without WM_TAKE_FOCUS', sub {
+    fresh_workspace;
 
-    my $tmp = fresh_workspace;
-
-    my $window = $x->root->create_child(
-        class => WINDOW_CLASS_INPUT_OUTPUT,
-        rect => [ 0, 0, 30, 30 ],
-        background_color => '#00ff00',
-        event_mask => [ 'structure_notify' ],
-    );
-
-    $window->name('Window 1');
-    $window->map;
-
-    my $cv = AE::cv;
-
-    my $prep = EV::prepare sub {
-        $x->flush;
-    };
-
-    my $check = EV::check sub {
-        while (defined(my $event = $x->poll_for_event)) {
-            if ($event->response_type == 161) {
-                # clientmessage
-                $cv->send(0);
-            }
-        }
-    };
+    my $window = open_window($x);
 
-    my $w = EV::io $x->get_file_descriptor, EV::READ, sub {
-        # do nothing, we only need this watcher so that EV picks up the events
-    };
-
-    # Trigger timeout after 1 second
-    my $t = AE::timer 1, 0, sub {
-        $cv->send(1);
-    };
-
-    my $result = $cv->recv;
-    ok($result, 'cv result');
+    ok(!wait_for_event($x, 1, sub { $_[0]->{response_type} == 161 }), 'did not receive ClientMessage');
 
     done_testing;
 };
 
 subtest 'Window with WM_TAKE_FOCUS', sub {
+    fresh_workspace;
 
-    my $tmp = fresh_workspace;
+    my $take_focus = $x->atom(name => 'WM_TAKE_FOCUS');
 
-    my $window = $x->root->create_child(
-        class => WINDOW_CLASS_INPUT_OUTPUT,
-        rect => [ 0, 0, 30, 30 ],
-        background_color => '#00ff00',
-        event_mask => [ 'structure_notify' ],
-        protocols => [ $x->atom(name => 'WM_TAKE_FOCUS') ],
-    );
+    my $window = open_window($x, {
+        dont_map => 1,
+        protocols => [ $take_focus ],
+    });
 
-    $window->name('Window 1');
     $window->map;
 
-    my $cv = AE::cv;
-
-    my $prep = EV::prepare sub {
-        $x->flush;
-    };
-
-    my $check = EV::check sub {
-        while (defined(my $event = $x->poll_for_event)) {
-            if ($event->response_type == 161) {
-                $cv->send($event->data);
-            }
-        }
-    };
-
-    my $w = EV::io $x->get_file_descriptor, EV::READ, sub {
-        # do nothing, we only need this watcher so that EV picks up the events
-    };
-
-    my $t = AE::timer 1, 0, sub {
-        say "timer!";
-        $cv->send(undef);
-    };
-
-    my $result = $cv->recv;
-    ok(defined($result), 'got a ClientMessage');
-    if (defined($result)) {
-        my ($data, $time) = unpack("L2", $result);
-        is($data, $x->atom(name => 'WM_TAKE_FOCUS')->id, 'first uint32_t contains WM_TAKE_FOCUS atom');
-    }
+    ok(wait_for_event($x, 1, sub {
+        return 0 unless $_[0]->{response_type} == 161;
+        my ($data, $time) = unpack("L2", $_[0]->{data});
+        return ($data == $take_focus->id);
+    }), 'got ClientMessage with WM_TAKE_FOCUS atom');
 
     done_testing;
 };
index 33350927ff705d5cf1d90bef7f0bddf1ed49901b..36c99087fff5f284c1c3ac0416ea4231cc12430a 100644 (file)
@@ -18,7 +18,7 @@ my $i3_path = abs_path("../i3");
 # default case: socket will be created in /tmp/i3-<username>/ipc-socket.<pid>
 #####################################################################
 
-my ($fh, $tmpfile) = tempfile();
+my ($fh, $tmpfile) = tempfile('/tmp/i3-test-config.XXXXXX', UNLINK => 1);
 say $fh "# i3 config file (v4)";
 say $fh "font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1";
 close($fh);
@@ -67,7 +67,7 @@ my $tmpdir = tempdir(CLEANUP => 1);
 $socketpath = $tmpdir . "/config.sock";
 ok(! -e $socketpath, "$socketpath does not exist yet");
 
-($fh, $tmpfile) = tempfile();
+($fh, $tmpfile) = tempfile('/tmp/i3-test-config.XXXXXX', UNLINK => 1);
 say $fh "# i3 config file (v4)";
 say $fh "font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1";
 say $fh "ipc-socket $socketpath";
index 1acf6c66fdf4824493eaa4220485dad9df37ee6b..c5e3ef8080c2cd2224cee9815d931f0a98e71dab 100644 (file)
@@ -12,7 +12,7 @@ use i3test;
 my $x = X11::XCB::Connection->new;
 my $i3 = i3(get_socket_path());
 my $tmp = fresh_workspace;
-my $window = open_standard_window($x);
+my $window = open_window($x);
 
 sub get_border_style {
     my @content = @{get_ws_content($tmp)};
index 8d188738a537901bb0e3dc5d7aa1d24dcba80dd9..5fb88129ea3c6e320e1f1916f603809680a9641d 100644 (file)
@@ -52,7 +52,7 @@ is($docknode->{rect}->{height}, 30, 'dock node has unchanged height');
 
 $window->add_hint('urgency');
 
-sleep 0.25;
+sync_with_i3($x);
 
 does_i3_live;
 
index 7e983289aba5d64cf66ef806a9246b95da4640df..e55d86824de934d97cab3fb0b21f5773618c3e0d 100644 (file)
@@ -7,34 +7,17 @@
 use X11::XCB qw(:all);
 use i3test;
 
-BEGIN {
-    use_ok('X11::XCB::Window');
-    use_ok('X11::XCB::Event::Generic');
-    use_ok('X11::XCB::Event::MapNotify');
-    use_ok('X11::XCB::Event::ClientMessage');
-}
-
 my $x = X11::XCB::Connection->new;
 
-my $window = $x->root->create_child(
-    class => WINDOW_CLASS_INPUT_OUTPUT,
-    rect => [ 0, 0, 30, 30 ],
-    background_color => '#00ff00',
-    event_mask => [ 'structure_notify' ],
-);
-
-$window->name('Window 1');
-$window->map;
-
-diag('window mapped');
+my $window = open_window($x);
 
-sleep 0.5;
+sync_with_i3($x);
 
 is($window->state, ICCCM_WM_STATE_NORMAL, 'WM_STATE normal');
 
 $window->unmap;
 
-sleep 0.5;
+wait_for_unmap $x;
 
 is($window->state, ICCCM_WM_STATE_WITHDRAWN, 'WM_STATE withdrawn');
 
index 2e0669babd6cf078d05c78cec82dc45d76b1b0b5..ef45a789923e94556bbbffd8b10aab0d6a3c693a 100644 (file)
@@ -4,8 +4,6 @@
 # Tests if WM_STATE is WM_STATE_NORMAL when mapped and WM_STATE_WITHDRAWN when
 # unmapped.
 #
-use X11::XCB qw(:all);
-use X11::XCB::Connection;
 use i3test;
 
 my $x = X11::XCB::Connection->new;
@@ -15,8 +13,10 @@ sub two_windows {
 
     ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
 
-    my $first = open_standard_window($x);
-    my $second = open_standard_window($x);
+    my $first = open_window($x);
+    my $second = open_window($x);
+
+    sync_with_i3 $x;
 
     is($x->input_focus, $second->id, 'second window focused');
     ok(@{get_ws_content($tmp)} == 2, 'two containers opened');
index fb4c28123242141a70f79a41d9674614b7a0168f..36a20ea9e4394272d1a7de188b5d9d039f9b4d15 100644 (file)
@@ -32,18 +32,19 @@ my $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#00ff00',
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->name('Border window');
 $window->map;
-sleep 0.25;
+wait_for_map $x;
 
 my @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 1, 'one node on this workspace now');
 is($content[0]->{border}, 'normal', 'normal border');
 
 $window->unmap;
-sleep 0.25;
+wait_for_unmap $x;
 
 my @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 0, 'no more nodes');
@@ -53,6 +54,7 @@ $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#00ff00',
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->_create;
@@ -79,14 +81,14 @@ sub set_wm_class {
 set_wm_class($window->id, 'borderless', 'borderless');
 $window->name('Borderless window');
 $window->map;
-sleep 0.25;
+wait_for_map $x;
 
 @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 1, 'one node on this workspace now');
 is($content[0]->{border}, 'none', 'no border');
 
 $window->unmap;
-sleep 0.25;
+wait_for_unmap $x;
 
 @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 0, 'no more nodes');
@@ -113,24 +115,25 @@ $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#00ff00',
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->name('special title');
 $window->map;
-sleep 0.25;
+wait_for_map $x;
 
 @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 1, 'one node on this workspace now');
 is($content[0]->{border}, 'normal', 'normal border');
 
 $window->name('special borderless title');
-sleep 0.25;
+sync_with_i3 $x;
 
 @content = @{get_ws_content($tmp)};
 is($content[0]->{border}, 'none', 'no border');
 
 $window->name('special title');
-sleep 0.25;
+sync_with_i3 $x;
 
 cmd 'border normal';
 
@@ -138,13 +141,13 @@ cmd 'border normal';
 is($content[0]->{border}, 'normal', 'border reset to normal');
 
 $window->name('special borderless title');
-sleep 0.25;
+sync_with_i3 $x;
 
 @content = @{get_ws_content($tmp)};
 is($content[0]->{border}, 'normal', 'still normal border');
 
 $window->unmap;
-sleep 0.25;
+wait_for_unmap $x;
 
 @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 0, 'no more nodes');
@@ -172,17 +175,18 @@ $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#00ff00',
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->name('special mark title');
 $window->map;
-sleep 0.25;
+wait_for_map $x;
 
 @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 1, 'one node on this workspace now');
 is($content[0]->{border}, 'none', 'no border');
 
-my $other = open_standard_window($x);
+my $other = open_window($x);
 
 @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 2, 'two nodes');
@@ -215,6 +219,7 @@ $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#00ff00',
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->_create;
@@ -222,14 +227,14 @@ $window->_create;
 set_wm_class($window->id, 'borderless', 'borderless');
 $window->name('usethis');
 $window->map;
-sleep 0.25;
+wait_for_map $x;
 
 @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 1, 'one node on this workspace now');
 is($content[0]->{border}, 'none', 'no border');
 
 $window->unmap;
-sleep 0.25;
+wait_for_unmap $x;
 
 @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 0, 'no nodes on this workspace now');
@@ -237,7 +242,7 @@ cmp_ok(@content, '==', 0, 'no nodes on this workspace now');
 set_wm_class($window->id, 'borderless', 'borderless');
 $window->name('notthis');
 $window->map;
-sleep 0.25;
+wait_for_map $x;
 
 @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 1, 'one node on this workspace now');
@@ -264,6 +269,7 @@ $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#00ff00',
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->_create;
@@ -271,7 +277,7 @@ $window->_create;
 set_wm_class($window->id, 'bar', 'foo');
 $window->name('usethis');
 $window->map;
-sleep 0.25;
+wait_for_map $x;
 
 @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 1, 'one node on this workspace now');
@@ -298,6 +304,7 @@ $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#00ff00',
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->_create;
@@ -305,7 +312,7 @@ $window->_create;
 set_wm_class($window->id, 'bar', 'foo');
 $window->name('usethis');
 $window->map;
-sleep 0.25;
+wait_for_map $x;
 
 @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 1, 'one node on this workspace now');
@@ -334,6 +341,7 @@ $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#00ff00',
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->_create;
@@ -341,7 +349,7 @@ $window->_create;
 set_wm_class($window->id, 'bar', 'foo');
 $window->name('usethis');
 $window->map;
-sleep 0.25;
+wait_for_map $x;
 
 @content = @{get_ws_content($tmp)};
 cmp_ok(@content, '==', 1, 'one node on this workspace now');
@@ -349,5 +357,110 @@ is($content[0]->{border}, 'normal', 'normal border');
 
 exit_gracefully($process->pid);
 
+##############################################################
+# 8: check that the role criterion works properly
+##############################################################
+
+# this configuration is broken because "asdf" is not a valid integer
+# the for_window should therefore recognize this error and don’t add the
+# assignment
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+for_window [window_role="i3test"] border none
+EOT
+
+$process = launch_with_config($config);
+
+$tmp = fresh_workspace;
+
+$window = $x->root->create_child(
+    class => WINDOW_CLASS_INPUT_OUTPUT,
+    rect => [ 0, 0, 30, 30 ],
+    background_color => '#00ff00',
+    event_mask => [ 'structure_notify' ],
+);
+
+$window->_create;
+
+my $atomname = $x->atom(name => 'WM_WINDOW_ROLE');
+my $atomtype = $x->atom(name => 'STRING');
+$x->change_property(
+  PROP_MODE_REPLACE,
+  $window->id,
+  $atomname->id,
+  $atomtype->id,
+  8,
+  length("i3test") + 1,
+  "i3test\x00"
+);
+
+$window->name('usethis');
+$window->map;
+wait_for_map $x;
+
+@content = @{get_ws_content($tmp)};
+cmp_ok(@content, '==', 1, 'one node on this workspace now');
+is($content[0]->{border}, 'none', 'no border (window_role)');
+
+exit_gracefully($process->pid);
+
+##############################################################
+# 9: another test for the window_role, but this time it changes
+#    *after* the window has been mapped
+##############################################################
+
+# this configuration is broken because "asdf" is not a valid integer
+# the for_window should therefore recognize this error and don’t add the
+# assignment
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+for_window [window_role="i3test"] border none
+EOT
+
+$process = launch_with_config($config);
+
+$tmp = fresh_workspace;
+
+$window = $x->root->create_child(
+    class => WINDOW_CLASS_INPUT_OUTPUT,
+    rect => [ 0, 0, 30, 30 ],
+    background_color => '#00ff00',
+    event_mask => [ 'structure_notify' ],
+);
+
+$window->_create;
+
+$window->name('usethis');
+$window->map;
+wait_for_map $x;
+
+@content = @{get_ws_content($tmp)};
+cmp_ok(@content, '==', 1, 'one node on this workspace now');
+is($content[0]->{border}, 'normal', 'normal border (window_role 2)');
+
+$atomname = $x->atom(name => 'WM_WINDOW_ROLE');
+$atomtype = $x->atom(name => 'STRING');
+$x->change_property(
+  PROP_MODE_REPLACE,
+  $window->id,
+  $atomname->id,
+  $atomtype->id,
+  8,
+  length("i3test") + 1,
+  "i3test\x00"
+);
+
+$x->flush;
+
+sync_with_i3 $x;
+
+@content = @{get_ws_content($tmp)};
+cmp_ok(@content, '==', 1, 'one node on this workspace now');
+is($content[0]->{border}, 'none', 'no border (window_role 2)');
+
+exit_gracefully($process->pid);
+
 
 done_testing;
index 776710e7604af4706478718a1cb5fecd43193c8b..cc41b7af4c2f9f8e2640a7c9781bb6a27e9fe2c1 100644 (file)
@@ -50,13 +50,14 @@ my $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#0000ff',
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->_create;
 set_wm_class($window->id, 'special', 'special');
 $window->name('special window');
 $window->map;
-sleep 0.25;
+wait_for_map $x;
 
 ok(@{get_ws_content($tmp)} == 1, 'special window got managed to current (random) workspace');
 
@@ -64,8 +65,6 @@ exit_gracefully($process->pid);
 
 $window->destroy;
 
-sleep 0.25;
-
 #####################################################################
 # start a window and see that it gets assigned to a formerly unused
 # workspace
@@ -89,13 +88,14 @@ my $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#0000ff',
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->_create;
 set_wm_class($window->id, 'special', 'special');
 $window->name('special window');
 $window->map;
-sleep 0.25;
+wait_for_map $x;
 
 ok(@{get_ws_content($tmp)} == 0, 'still no containers');
 ok("targetws" ~~ @{get_workspace_names()}, 'targetws exists');
@@ -128,13 +128,18 @@ my $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#0000ff',
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->_create;
 set_wm_class($window->id, 'special', 'special');
 $window->name('special window');
 $window->map;
-sleep 0.25;
+
+# We use sync_with_i3 instead of wait_for_map here because i3 will not actually
+# map the window -- it will be assigned to a different workspace and will only
+# be mapped once you switch to that workspace
+sync_with_i3 $x;
 
 ok(@{get_ws_content($tmp)} == 0, 'still no containers');
 ok(@{get_ws_content('targetws')} == 2, 'two containers on targetws');
@@ -164,14 +169,56 @@ my $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#0000ff',
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->_create;
 set_wm_class($window->id, 'special', 'special');
 $window->name('special window');
 $window->map;
+wait_for_map $x;
+
+my $content = get_ws($tmp);
+ok(@{$content->{nodes}} == 0, 'no tiling cons');
+ok(@{$content->{floating_nodes}} == 1, 'one floating con');
+
+$window->destroy;
+
+exit_gracefully($process->pid);
+
 sleep 0.25;
 
+#####################################################################
+# make sure that assignments are case-insensitive in the old syntax.
+#####################################################################
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+assign "special" → ~
+EOT
+
+$process = launch_with_config($config);
+
+$tmp = fresh_workspace;
+
+ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
+my $workspaces = get_workspace_names;
+ok(!("targetws" ~~ @{$workspaces}), 'targetws does not exist yet');
+
+my $window = $x->root->create_child(
+    class => WINDOW_CLASS_INPUT_OUTPUT,
+    rect => [ 0, 0, 30, 30 ],
+    background_color => '#0000ff',
+    event_mask => [ 'structure_notify' ],
+);
+
+$window->_create;
+set_wm_class($window->id, 'SPEcial', 'SPEcial');
+$window->name('special window');
+$window->map;
+wait_for_map $x;
+
 my $content = get_ws($tmp);
 ok(@{$content->{nodes}} == 0, 'no tiling cons');
 ok(@{$content->{floating_nodes}} == 1, 'one floating con');
@@ -200,26 +247,29 @@ $tmp = fresh_workspace;
 
 ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
 my @docked = get_dock_clients;
-is(@docked, 0, 'no dock clients yet');
+# We expect i3-nagbar as the first dock client due to using the old assign
+# syntax
+is(@docked, 1, 'one dock client yet');
 
 my $window = $x->root->create_child(
     class => WINDOW_CLASS_INPUT_OUTPUT,
     rect => [ 0, 0, 30, 30 ],
     background_color => '#0000ff',
     window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'),
+    event_mask => [ 'structure_notify' ],
 );
 
 $window->_create;
 set_wm_class($window->id, 'special', 'special');
 $window->name('special window');
 $window->map;
-sleep 0.25;
+wait_for_map $x;
 
 my $content = get_ws($tmp);
 ok(@{$content->{nodes}} == 0, 'no tiling cons');
 ok(@{$content->{floating_nodes}} == 0, 'one floating con');
 @docked = get_dock_clients;
-is(@docked, 1, 'no dock clients yet');
+is(@docked, 2, 'two dock clients now');
 
 $window->destroy;
 
index 2b9f6e5656cb757484bd1d3299c23043bb00e18e..6ff3f15b2bd28d86c9e3601c9936d303e5bd0b0b 100644 (file)
@@ -27,8 +27,10 @@ my $tmp = fresh_workspace;
 
 ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
 
-my $first = open_standard_window($x);
-my $second = open_standard_window($x);
+my $first = open_window($x);
+my $second = open_window($x);
+
+sync_with_i3($x);
 
 is($x->input_focus, $second->id, 'second window focused');
 ok(@{get_ws_content($tmp)} == 2, 'two containers opened');
@@ -54,8 +56,10 @@ $tmp = fresh_workspace;
 
 ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
 
-$first = open_standard_window($x);
-$second = open_standard_window($x);
+$first = open_window($x);
+$second = open_window($x);
+
+sync_with_i3($x);
 
 is($x->input_focus, $second->id, 'second window focused');
 my @content = @{get_ws_content($tmp)};
@@ -68,8 +72,8 @@ is($content[0]->{layout}, 'stacked', 'layout stacked');
 #####################################################################
 
 cmd 'focus parent';
-my $right_top = open_standard_window($x);
-my $right_bot = open_standard_window($x);
+my $right_top = open_window($x);
+my $right_bot = open_window($x);
 
 @content = @{get_ws_content($tmp)};
 is(@content, 2, 'two cons at workspace level after focus parent');
index fcc9ac7e12f5de0b6281151eaa15ee3862f22302..1418b402d38bff307f2386b13f34254e643a3b5a 100644 (file)
@@ -11,8 +11,8 @@ my $x = X11::XCB::Connection->new;
 
 fresh_workspace;
 
-open_standard_window($x);
-open_standard_window($x);
+open_window($x);
+open_window($x);
 
 cmd 'layout stacking';
 sleep 1;
index f2dfc18eb6a9a30510471e915d170e89b34afc99..2aa5407d57b337f78cfe5d30a77b72d80881022e 100644 (file)
@@ -25,13 +25,13 @@ my $tmp = fresh_workspace;
 
 ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
 
-my $first = open_standard_window($x);
-my $second = open_standard_window($x);
+my $first = open_window($x);
+my $second = open_window($x);
 
 cmd 'layout tabbed';
 cmd 'focus parent';
 
-my $third = open_standard_window($x);
+my $third = open_window($x);
 is($x->input_focus, $third->id, 'third window focused');
 
 cmd 'focus left';
@@ -66,13 +66,16 @@ $tmp = fresh_workspace;
 
 ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
 
-$first = open_standard_window($x);
-$second = open_standard_window($x);
+$first = open_window($x);
+$second = open_window($x);
 
 cmd 'layout tabbed';
 cmd 'focus parent';
 
-$third = open_standard_window($x);
+$third = open_window($x);
+
+sync_with_i3($x);
+
 is($x->input_focus, $third->id, 'third window focused');
 
 cmd 'focus left';
index 6b41f2c17997c7f267895d9ce39c60b54f3def5c..561538e5459b74a74f1f88697fae5845d42190df 100644 (file)
@@ -23,7 +23,7 @@ sub slurp {
 sub migrate_config {
     my ($config) = @_;
 
-    my ($fh, $tmpfile) = tempfile();
+    my ($fh, $tmpfile) = tempfile('/tmp/i3-migrate-cfg.XXXXXX', UNLINK => 1);
     print $fh $config;
     close($fh);
 
diff --git a/testcases/t/73-get-marks.t b/testcases/t/73-get-marks.t
new file mode 100644 (file)
index 0000000..e74c233
--- /dev/null
@@ -0,0 +1,50 @@
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# checks if the IPC message type get_marks works correctly
+#
+use i3test;
+
+sub get_marks {
+    return i3(get_socket_path())->get_marks->recv;
+}
+
+##############################################################
+# 1: check that get_marks returns no marks yet
+##############################################################
+
+my $tmp = fresh_workspace;
+
+my $marks = get_marks();
+cmp_deeply($marks, [], 'no marks set so far');
+
+##############################################################
+# 2: check that setting a mark is reflected in the get_marks reply
+##############################################################
+
+cmd 'open';
+cmd 'mark foo';
+
+cmp_deeply(get_marks(), [ 'foo' ], 'mark foo set');
+
+##############################################################
+# 3: check that the mark is gone after killing the container
+##############################################################
+
+cmd 'kill';
+
+cmp_deeply(get_marks(), [ ], 'mark gone');
+
+##############################################################
+# 4: check that duplicate marks are included twice in the get_marks reply
+##############################################################
+
+cmd 'open';
+cmd 'mark bar';
+
+cmd 'open';
+cmd 'mark bar';
+
+cmp_deeply(get_marks(), [ 'bar', 'bar' ], 'duplicate mark found twice');
+
+done_testing;
diff --git a/testcases/t/74-border-config.t b/testcases/t/74-border-config.t
new file mode 100644 (file)
index 0000000..617e37b
--- /dev/null
@@ -0,0 +1,116 @@
+#!perl
+# vim:ts=4:sw=4:expandtab
+# !NO_I3_INSTANCE! will prevent complete-run.pl from starting i3
+#
+# Tests the new_window and new_float config option.
+#
+
+use i3test;
+
+my $x = X11::XCB::Connection->new;
+
+#####################################################################
+# 1: check that new windows start with 'normal' border unless configured
+# otherwise
+#####################################################################
+
+my $config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+EOT
+
+my $process = launch_with_config($config);
+
+my $tmp = fresh_workspace;
+
+ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
+
+my $first = open_window($x);
+
+my @content = @{get_ws_content($tmp)};
+ok(@content == 1, 'one container opened');
+is($content[0]->{border}, 'normal', 'border normal by default');
+
+exit_gracefully($process->pid);
+
+#####################################################################
+# 2: check that new tiling windows start with '1pixel' border when
+# configured
+#####################################################################
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+new_window 1pixel
+EOT
+
+$process = launch_with_config($config);
+
+$tmp = fresh_workspace;
+
+ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
+
+$first = open_window($x);
+
+@content = @{get_ws_content($tmp)};
+ok(@content == 1, 'one container opened');
+is($content[0]->{border}, '1pixel', 'border normal by default');
+
+exit_gracefully($process->pid);
+
+#####################################################################
+# 3: check that new floating windows start with 'normal' border unless
+# configured otherwise
+#####################################################################
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+EOT
+
+$process = launch_with_config($config);
+
+$tmp = fresh_workspace;
+
+ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
+
+$first = open_floating_window($x);
+
+my $wscontent = get_ws($tmp);
+my @floating = @{$wscontent->{floating_nodes}};
+ok(@floating == 1, 'one floating container opened');
+my $floatingcon = $floating[0];
+is($floatingcon->{nodes}->[0]->{border}, 'normal', 'border normal by default');
+
+exit_gracefully($process->pid);
+
+#####################################################################
+# 4: check that new floating windows start with '1pixel' border when
+# configured
+#####################################################################
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+new_float 1pixel
+EOT
+
+$process = launch_with_config($config);
+
+$tmp = fresh_workspace;
+
+ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
+
+$first = open_floating_window($x);
+
+$wscontent = get_ws($tmp);
+@floating = @{$wscontent->{floating_nodes}};
+ok(@floating == 1, 'one floating container opened');
+$floatingcon = $floating[0];
+is($floatingcon->{nodes}->[0]->{border}, '1pixel', 'border normal by default');
+
+exit_gracefully($process->pid);
+
+done_testing;
index 054bb2ae56de6676246c48e433e0cfc0587e5aa3..c890693c5644504af6d38e2f8fdeaad25e442742 100644 (file)
@@ -7,6 +7,7 @@ use X11::XCB::Rect;
 use X11::XCB::Window;
 use X11::XCB qw(:all);
 use AnyEvent::I3;
+use EV;
 use List::Util qw(first);
 use List::MoreUtils qw(lastval);
 use Time::HiRes qw(sleep);
@@ -17,10 +18,33 @@ use Proc::Background;
 use v5.10;
 
 use Exporter ();
-our @EXPORT = qw(get_workspace_names get_unused_workspace fresh_workspace get_ws_content get_ws get_focused open_empty_con open_standard_window get_dock_clients cmd does_i3_live exit_gracefully workspace_exists focused_ws get_socket_path launch_with_config);
+our @EXPORT = qw(
+    get_workspace_names
+    get_unused_workspace
+    fresh_workspace
+    get_ws_content
+    get_ws
+    get_focused
+    open_empty_con
+    open_window
+    open_floating_window
+    get_dock_clients
+    cmd
+    sync_with_i3
+    does_i3_live
+    exit_gracefully
+    workspace_exists
+    focused_ws
+    get_socket_path
+    launch_with_config
+    wait_for_event
+    wait_for_map
+    wait_for_unmap
+);
 
 my $tester = Test::Builder->new();
 my $_cached_socket_path = undef;
+my $_sync_window = undef;
 my $tmp_socket_path = undef;
 
 BEGIN {
@@ -47,25 +71,107 @@ use warnings;
     goto \&Exporter::import;
 }
 
-sub open_standard_window {
-    my ($x, $color) = @_;
+#
+# Waits for the next event and calls the given callback for every event to
+# determine if this is the event we are waiting for.
+#
+# Can be used to wait until a window is mapped, until a ClientMessage is
+# received, etc.
+#
+# wait_for_event $x, 0.25, sub { $_[0]->{response_type} == MAP_NOTIFY };
+#
+sub wait_for_event {
+    my ($x, $timeout, $cb) = @_;
 
-    $color ||= '#c0c0c0';
+    my $cv = AE::cv;
 
-    my $window = $x->root->create_child(
-        class => WINDOW_CLASS_INPUT_OUTPUT,
-        rect => [ 0, 0, 30, 30 ],
-        background_color => $color,
-    );
+    my $prep = EV::prepare sub {
+        $x->flush;
+    };
 
-    $window->name('Window ' . counter_window());
-    $window->map;
+    my $check = EV::check sub {
+        while (defined(my $event = $x->poll_for_event)) {
+            if ($cb->($event)) {
+                $cv->send(1);
+                last;
+            }
+        }
+    };
+
+    my $watcher = EV::io $x->get_file_descriptor, EV::READ, sub {
+        # do nothing, we only need this watcher so that EV picks up the events
+    };
+
+    # Trigger timeout after $timeout seconds (can be fractional)
+    my $timeout = AE::timer $timeout, 0, sub { warn "timeout"; $cv->send(0) };
 
-    sleep(0.25);
+    my $result = $cv->recv;
+    return $result;
+}
+
+# thin wrapper around wait_for_event which waits for MAP_NOTIFY
+# make sure to include 'structure_notify' in the window’s event_mask attribute
+sub wait_for_map {
+    my ($x) = @_;
+    wait_for_event $x, 1, sub { $_[0]->{response_type} == MAP_NOTIFY };
+}
+
+# Wrapper around wait_for_event which waits for UNMAP_NOTIFY. Also calls
+# sync_with_i3 to make sure i3 also picked up and processed the UnmapNotify
+# event.
+sub wait_for_unmap {
+    my ($x) = @_;
+    wait_for_event $x, 1, sub { $_[0]->{response_type} == UNMAP_NOTIFY };
+    sync_with_i3($x);
+}
+
+#
+# Opens a new window (see X11::XCB::Window), maps it, waits until it got mapped
+# and synchronizes with i3.
+#
+# set dont_map to a true value to avoid mapping
+#
+# default values:
+#     class => WINDOW_CLASS_INPUT_OUTPUT
+#     rect => [ 0, 0, 30, 30 ]
+#     background_color => '#c0c0c0'
+#     event_mask => [ 'structure_notify' ]
+#     name => 'Window <n>'
+#
+sub open_window {
+    my ($x, $args) = @_;
+    my %args = ($args ? %$args : ());
 
+    my $dont_map = delete $args{dont_map};
+
+    $args{class} //= WINDOW_CLASS_INPUT_OUTPUT;
+    $args{rect} //= [ 0, 0, 30, 30 ];
+    $args{background_color} //= '#c0c0c0';
+    $args{event_mask} //= [ 'structure_notify' ];
+    $args{name} //= 'Window ' . counter_window();
+
+    my $window = $x->root->create_child(%args);
+
+    return $window if $dont_map;
+
+    $window->map;
+    wait_for_map($x);
+    # We sync with i3 here to make sure $x->input_focus is updated.
+    sync_with_i3($x);
     return $window;
 }
 
+# Thin wrapper around open_window which sets window_type to
+# _NET_WM_WINDOW_TYPE_UTILITY to make the window floating.
+sub open_floating_window {
+    my ($x, $args) = @_;
+    my %args = ($args ? %$args : ());
+
+    $args{window_type} = $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY');
+
+    return open_window($x, \%args);
+}
+
 sub open_empty_con {
     my ($i3) = @_;
 
@@ -190,6 +296,69 @@ sub focused_ws {
     }
 }
 
+#
+# Sends an I3_SYNC ClientMessage with a random value to the root window.
+# i3 will reply with the same value, but, due to the order of events it
+# processes, only after all other events are done.
+#
+# This can be used to ensure the results of a cmd 'focus left' are pushed to
+# X11 and that $x->input_focus returns the correct value afterwards.
+#
+# See also docs/testsuite for a long explanation
+#
+sub sync_with_i3 {
+    my ($x) = @_;
+
+    # Since we need a (mapped) window for receiving a ClientMessage, we create
+    # one on the first call of sync_with_i3. It will be re-used in all
+    # subsequent calls.
+    if (!defined($_sync_window)) {
+        $_sync_window = $x->root->create_child(
+            class => WINDOW_CLASS_INPUT_OUTPUT,
+            rect => X11::XCB::Rect->new(x => -15, y => -15, width => 10, height => 10 ),
+            override_redirect => 1,
+            background_color => '#ff0000',
+            event_mask => [ 'structure_notify' ],
+        );
+
+        $_sync_window->map;
+
+        wait_for_event $x, 0.5, sub { $_[0]->{response_type} == MAP_NOTIFY };
+    }
+
+    my $root = $x->get_root_window();
+    # Generate a random number to identify this particular ClientMessage.
+    my $myrnd = int(rand(255)) + 1;
+
+    # Generate a ClientMessage, see xcb_client_message_t
+    my $msg = pack "CCSLLLLLLL",
+         CLIENT_MESSAGE, # response_type
+         32,     # format
+         0,      # sequence
+         $root,  # destination window
+         $x->atom(name => 'I3_SYNC')->id,
+
+         $_sync_window->id,    # data[0]: our own window id
+         $myrnd, # data[1]: a random value to identify the request
+         0,
+         0,
+         0;
+
+    # Send it to the root window -- since i3 uses the SubstructureRedirect
+    # event mask, it will get the ClientMessage.
+    $x->send_event(0, $root, EVENT_MASK_SUBSTRUCTURE_REDIRECT, $msg);
+
+    # now wait until the reply is here
+    return wait_for_event $x, 1, sub {
+        my ($event) = @_;
+        # TODO: const
+        return 0 unless $event->{response_type} == 161;
+
+        my ($win, $rnd) = unpack "LL", $event->{data};
+        return ($rnd == $myrnd);
+    };
+}
+
 sub does_i3_live {
     my $tree = i3(get_socket_path())->get_tree->recv;
     my @nodes = @{$tree->{nodes}};
@@ -213,6 +382,10 @@ sub exit_gracefully {
     if (!$exited) {
         kill(9, $pid) or die "could not kill i3";
     }
+
+    if ($socketpath =~ m,^/tmp/i3-test-socket-,) {
+        unlink($socketpath);
+    }
 }
 
 # Gets the socket path from the I3_SOCKET_PATH atom stored on the X11 root window
@@ -252,9 +425,12 @@ sub launch_with_config {
     say $fh "ipc-socket $tmp_socket_path";
     close($fh);
 
-    my $i3cmd = "exec " . abs_path("../i3") . " -V -d all --disable-signalhandler -c $tmpfile >/dev/null 2>/dev/null";
+    # Use $ENV{LOGPATH}, gets set in complete-run.pl. We append instead of
+    # overwrite because there might be multiple instances of i3 running during
+    # one test case.
+    my $i3cmd = "exec " . abs_path("../i3") . " -V -d all --disable-signalhandler -c $tmpfile >>$ENV{LOGPATH} 2>&1";
     my $process = Proc::Background->new($i3cmd);
-    sleep 1;
+    sleep 1.25;
 
     # force update of the cached socket path in lib/i3test
     get_socket_path(0);