diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..c25745f8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{c,h}] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true diff --git a/.travis.yml b/.travis.yml index de9ff3fc..87c996fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,7 +42,6 @@ script: - ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME_UBUNTU} ./travis/debian-build.sh deb/ubuntu-amd64/DIST - ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME_386} linux32 ./travis/debian-build.sh deb/debian-i386/DIST - ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME_UBUNTU_386} linux32 ./travis/debian-build.sh deb/ubuntu-i386/DIST - - ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME} ./travis/clang-analyze.sh - ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME} ./travis/docs.sh - ./travis/skip-pkg.sh || travis/prep-bintray.sh diff --git a/AnyEvent-I3/lib/AnyEvent/I3.pm b/AnyEvent-I3/lib/AnyEvent/I3.pm index 53ad8215..198c41c9 100644 --- a/AnyEvent-I3/lib/AnyEvent/I3.pm +++ b/AnyEvent-I3/lib/AnyEvent/I3.pm @@ -9,6 +9,7 @@ use AnyEvent::Socket; use AnyEvent; use Encode; use Scalar::Util qw(tainted); +use Carp; =head1 NAME @@ -98,11 +99,12 @@ use constant TYPE_GET_BAR_CONFIG => 6; use constant TYPE_GET_VERSION => 7; use constant TYPE_GET_BINDING_MODES => 8; use constant TYPE_GET_CONFIG => 9; +use constant TYPE_SEND_TICK => 10; our %EXPORT_TAGS = ( 'all' => [ qw(i3 TYPE_RUN_COMMAND TYPE_COMMAND TYPE_GET_WORKSPACES TYPE_SUBSCRIBE TYPE_GET_OUTPUTS TYPE_GET_TREE TYPE_GET_MARKS TYPE_GET_BAR_CONFIG TYPE_GET_VERSION - TYPE_GET_BINDING_MODES TYPE_GET_CONFIG) + TYPE_GET_BINDING_MODES TYPE_GET_CONFIG TYPE_SEND_TICK) ] ); our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } ); @@ -119,6 +121,7 @@ my %events = ( barconfig_update => ($event_mask | 4), binding => ($event_mask | 5), shutdown => ($event_mask | 6), + tick => ($event_mask | 7), _error => 0xFFFFFFFF, ); @@ -187,7 +190,7 @@ sub new { # We use getpwuid() instead of $ENV{HOME} because the latter is tainted # and thus produces warnings when running tests with perl -T my $home = (getpwuid($<))[7]; - die "Could not get home directory" unless $home and -d $home; + confess "Could not get home directory" unless $home and -d $home; $path =~ s/~/$home/g; } @@ -331,9 +334,9 @@ scalar), if specified. sub message { my ($self, $type, $content) = @_; - die "No message type specified" unless defined($type); + confess "No message type specified" unless defined($type); - die "No connection to i3" unless defined($self->{ipchdl}); + confess "No connection to i3" unless defined($self->{ipchdl}); my $payload = ""; if ($content) { @@ -374,7 +377,7 @@ sub _ensure_connection { return if defined($self->{ipchdl}); - $self->connect->recv or die "Unable to connect to i3 (socket path " . $self->{path} . ")"; + $self->connect->recv or confess "Unable to connect to i3 (socket path " . $self->{path} . ")"; } =head2 get_workspaces @@ -518,6 +521,18 @@ sub get_config { $self->message(TYPE_GET_CONFIG); } +=head2 send_tick + +Sends a tick event. Requires i3 >= 4.15 + +=cut +sub send_tick { + my ($self, $payload) = @_; + + $self->_ensure_connection; + + $self->message(TYPE_SEND_TICK, $payload); +} =head2 command($content) diff --git a/I3_VERSION b/I3_VERSION index 2c940203..0d5ece58 100644 --- a/I3_VERSION +++ b/I3_VERSION @@ -1 +1 @@ -4.14.1-non-git +4.15-non-git diff --git a/Makefile.am b/Makefile.am index b94d3e90..184b0734 100644 --- a/Makefile.am +++ b/Makefile.am @@ -78,6 +78,11 @@ EXTRA_DIST = \ AnyEvent-I3/t/manifest.t \ AnyEvent-I3/t/pod-coverage.t \ AnyEvent-I3/t/pod.t \ + contrib/dump-asy.pl \ + contrib/gtk-tree-watch.pl \ + contrib/i3-wsbar \ + contrib/per-workspace-layout.pl \ + contrib/trivial-bar-script.sh \ docs/asciidoc-git.conf \ docs/bigpicture.png \ docs/i3-pod2html \ @@ -113,7 +118,7 @@ EXTRA_DIST = \ I3_VERSION \ LICENSE \ PACKAGE-MAINTAINER \ - RELEASE-NOTES-4.14.1 \ + RELEASE-NOTES-4.15 \ generate-command-parser.pl \ parser-specs/commands.spec \ parser-specs/config.spec \ diff --git a/README.md b/README.md new file mode 100644 index 00000000..21b2e24b --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +![Logo](docs/logo-30.png) i3: A tiling window manager +===================================================== + +[![Build Status](https://travis-ci.org/i3/i3.svg?branch=next)](https://travis-ci.org/i3/i3) +[![Issue Stats](http://www.issuestats.com/github/i3/i3/badge/issue?style=flat)](http://www.issuestats.com/github/i3/i3) +[![Pull Request Stats](http://www.issuestats.com/github/i3/i3/badge/pr?style=flat)](http://www.issuestats.com/github/i3/i3) + +i3 is a tiling window manager for X11. + +For more information about i3, please see [the project's website](https://i3wm.org/) and [online documentation](https://i3wm.org/docs/). + +For information about contributing to i3, please see [CONTRIBUTING.md](.github/CONTRIBUTING.md). diff --git a/RELEASE-NOTES-4.15 b/RELEASE-NOTES-4.15 new file mode 100644 index 00000000..0e1f81e4 --- /dev/null +++ b/RELEASE-NOTES-4.15 @@ -0,0 +1,113 @@ + + ┌────────────────────────────┐ + │ Release notes for i3 v4.15 │ + └────────────────────────────┘ + +This is i3 v4.15. This version is considered stable. All users of i3 are +strongly encouraged to upgrade. + +Aside from a number of fixes and documentation improvements, a number of +commands have been extended to be more complete (e.g. “assign”, “resize”). + + ┌────────────────────────────┐ + │ Changes in i3 v4.15 │ + └────────────────────────────┘ + + • build: AnyEvent::I3 moved to the i3 repository, so that its main consumer, + the i3 testsuite, can use new features immediately (such as the tick event, + in this case). + • docs/hacking-howto: promote “using git / sending patches” and “how to + build?” sections + • docs/i3bar-protocol: document that pango markup only works with pango fonts + • docs/ipc: document focus, nodes, floating_nodes + • docs/ipc: urgent: complete the list of container types + • docs/ipc: document how to detect i3’s byte order in memory-safe languages + • docs/ipc: document the GET_CONFIG request + • docs/userguide: fix formatting issue + • docs/userguide: explain why Mod4 is usually preferred as a modifier + • docs/userguide: use more idiomatic english (full-size, so-called) + • docs/userguide: switch from removed goto command to focus + • docs/userguide: mention in focus + • docs/userguide: remove outdated 2013 last-modified date + • dump-asy: add prerequisite checks + • dump-asy: fix warnings about empty container names + • i3-dump-log: enable shmlog on demand + • i3-sensible-terminal: add “kitty”, “guake”, “tilda” + • i3-sensible-editor: add “gvim” + • i3bar: add --release flag for bindsym in bar blocks + • i3bar: add relative coordinates in JSON for click events + • ipc: rename COMMAND to RUN_COMMAND for consistency + • ipc: implement tick event for less flaky tests + • ipc: add error reply to “focus ” + • ipc: send success response for nop + • default config: add $mod+r to toggle resize mode + • default config: use variables for workspace names to avoid repetition + • introduce “assign [→] [workspace] [number] ” + • introduce “assign [→] output left|right|up|down|primary|” + • introduce a “focus_wrapping” option (subsumes “force_focus_wrapping”) + • introduce percentage point resizing for floating containers: + “resize set [px | ppt] [px | ppt]” + • introduce “resize set ppt ppt” for tiling windows + • rename “new_window” and “new_float” to “default_border” and + “default_floating_border” (the old names keep working) + • output names (e.g. “DP2”) can now be used as synonyms for monitor names + (e.g. “Dell UP2414Q”). + • the “swap” command now works with fullscreen windows + • raise floating windows to top when they are focused programmatically + • _NET_ACTIVE_WINDOW: invalidate focus to force SetInputFocus call + • make focus handling consistent when changing focus between outputs + • round non-integer Xft.dpi values + • tiling resize: remove minimum size + + ┌────────────────────────────┐ + │ Bugfixes │ + └────────────────────────────┘ + + • i3bar: fix various memory leaks + • i3bar: fix crash when no status_command is provided + • fix uninitialized variables in init_dpi_end, tree_restore + • fix incorrectly set up signal handling + • fix “swap” debug log message + • fix crash when specifying invalid con_id for “swap” + • fix crash upon restart with window marks + • fix crash when config file does not end in a newline + • fix crash in append_layout + • fix crash in layout toggle command + • fix crash when switching monitors + • fix use-after-free in randr_init error path + • fix move accidentally moving windows across outputs + • fix crash when floating window is tiled while being resized + • fix out-of-bounds memory read + • fix memory leak when config conversion fails + • fix layout toggle split, which didn’t work until enabling tabbed/stack mode + once + • move XCB event handling into xcb_prepare_cb + • avert endless loop on unexpected EOF in ipc messages + • perform proper cleanup for signals with Term action + • don’t match containers in the scratchpad with criteria + • fix “workspace show” related issues + • fix config file conversion with long variable names + • fix config file conversion memory initialization + • prevent access of freed workspace in _workspace_show + • disable fullscreen when required when programmatically focusing windows + • free last_motion_notify + • don’t raise floating windows when focused because of focus_follows_mouse + • correctly set EWMH atoms when closing a workspace + • don’t raise floating windows when workspace is shown + • keep focus order when encapsulating workspaces + • validate layout files before loading + + ┌────────────────────────────┐ + │ Thanks! │ + └────────────────────────────┘ + +Thanks for testing, bugfixes, discussions and everything I forgot go out to: + + Alex Lu, Ben Creasy, Bennett Piater, Cast, chressie, clonejo, Dan Elkouby, + Daniel Mueller, DebianWall, Diki Ananta, Edward Betts, hwangcc23, Ingo Bürk, + Jan Alexander Steffens, Johannes Lange, Kent Fredric, livanh, Martin + T. H. Sandsmark, Michael Siegel, Orestis Floros, Pallav Agarwal, Pawel + S. Veselov, Pietro Cerutti, Theo Buehler, Thomas Praxl, Tyler Brazier, + Vladimir Panteleev, walker0643, Wes Roberts, xzfc + +-- Michael Stapelberg, 2018-03-10 diff --git a/configure.ac b/configure.ac index 01574dd9..8dce4f9f 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Run autoreconf -fi to generate a configure script from this file. AC_PREREQ([2.69]) -AC_INIT([i3], [4.14.1], [https://github.com/i3/i3/issues]) +AC_INIT([i3], [4.15], [https://github.com/i3/i3/issues]) # For AX_EXTEND_SRCDIR AX_ENABLE_BUILDDIR AM_INIT_AUTOMAKE([foreign subdir-objects -Wall no-dist-gzip dist-bzip2]) diff --git a/contrib/dump-asy.pl b/contrib/dump-asy.pl index 3ebdb858..9bb2db3a 100755 --- a/contrib/dump-asy.pl +++ b/contrib/dump-asy.pl @@ -13,7 +13,14 @@ use warnings; use Data::Dumper; use AnyEvent::I3; use File::Temp; +use File::Basename; use v5.10; +use IPC::Cmd qw[can_run]; + +# prerequisites check so we can be specific about failures caused +# by not having these tools in the path +can_run('asy') or die 'Please install asymptote'; +can_run('gv') or die 'Please install gv'; my $i3 = i3(); @@ -30,7 +37,7 @@ sub dump_node { my $o = ($n->{orientation} eq 'none' ? "u" : ($n->{orientation} eq 'horizontal' ? "h" : "v")); my $w = (defined($n->{window}) ? $n->{window} : "N"); - my $na = $n->{name}; + my $na = ($n->{name} or "[Empty]"); $na =~ s/#/\\#/g; $na =~ s/\$/\\\$/g; $na =~ s/&/\\&/g; @@ -38,7 +45,7 @@ sub dump_node { $na =~ s/~/\\textasciitilde{}/g; my $type = 'leaf'; if (!defined($n->{window})) { - $type = $n->{orientation} . '-split'; + $type = $n->{layout}; } my $name = qq|``$na'' ($type)|; @@ -75,4 +82,5 @@ say $tmp "draw(n" . $root->{id} . ", (0, 0));"; close($tmp); my $rep = "$tmp"; $rep =~ s/asy$/eps/; -system("cd /tmp && asy $tmp && gv --scale=-1000 --noresize --widgetless $rep && rm $rep"); +my $tmp_dir = dirname($rep); +system("cd $tmp_dir && asy $tmp && gv --scale=-1000 --noresize --widgetless $rep && rm $rep"); diff --git a/contrib/per-workspace-layout.pl b/contrib/per-workspace-layout.pl index 48590456..4a2b4b9e 100644 --- a/contrib/per-workspace-layout.pl +++ b/contrib/per-workspace-layout.pl @@ -14,6 +14,7 @@ use warnings; use AnyEvent; use AnyEvent::I3; use v5.10; +use utf8; my %layouts = ( '4' => 'tabbed', diff --git a/debian/changelog b/debian/changelog index dae55d7e..8dd823eb 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +i3-wm (4.14.2-1) unstable; urgency=medium + + * New upstream release. + + -- Michael Stapelberg Mon, 25 Sep 2017 08:55:22 +0200 + +i3-wm (4.14.1-1) unstable; urgency=medium + + * New upstream release. + + -- Michael Stapelberg Sun, 24 Sep 2017 19:21:15 +0200 + i3-wm (4.14-1) unstable; urgency=medium * New upstream release. @@ -308,7 +320,7 @@ i3-wm (4.0.2-1) unstable; urgency=low * Bugfix: Use correct format string in load_layout (fixes crash in restart) * Bugfix: Fix border rendering (border lines were "cutting" through) * Bugfix: Raise floating windows immediately when dragging/resizing - * Bugfix: Make focus switching work accross outputs again + * Bugfix: Make focus switching work across outputs again * Bugfix: migration-script: handle resize top/bottom correctly * Bugfix: Fix focus issue when moving containers to workspaces * Bugfix: Warp cursor when changing outputs again diff --git a/debian/i3-wm.install b/debian/i3-wm.install index bff5028c..823d4097 100644 --- a/debian/i3-wm.install +++ b/debian/i3-wm.install @@ -1,2 +1,7 @@ debian/tmp/etc debian/tmp/usr +contrib/dump-asy.pl usr/share/doc/i3-wm/examples/ +contrib/gtk-tree-watch.pl usr/share/doc/i3-wm/examples/ +contrib/i3-wsbar usr/share/doc/i3-wm/examples/ +contrib/per-workspace-layout.pl usr/share/doc/i3-wm/examples/ +contrib/trivial-bar-script.sh usr/share/doc/i3-wm/examples/ diff --git a/debian/rules b/debian/rules index e4da3db8..d5c30686 100755 --- a/debian/rules +++ b/debian/rules @@ -17,5 +17,9 @@ override_dh_auto_configure: # The default is /usr/share/doc/i3 dh_auto_configure -- --docdir=/usr/share/doc/i3-wm +override_dh_builddeb: + # bintray does not support xz currently. + dh_builddeb -- -Zgzip + %: dh $@ --parallel --builddirectory=build --with=autoreconf diff --git a/docs/debugging b/docs/debugging index 07bc13a0..dd26f98d 100644 --- a/docs/debugging +++ b/docs/debugging @@ -153,7 +153,7 @@ When sending bug reports, please attach the *whole* log file. Even if you think you found the section which clearly highlights the problem, additional information might be necessary to completely diagnose the problem. -When debugging with us in IRC, be prepared to use a so called nopaste service +When debugging with us in IRC, be prepared to use a so-called nopaste service such as https://pastebin.com because pasting large amounts of text in IRC sometimes leads to incomplete lines (servers have line length limitations) or flood kicks. diff --git a/docs/i3bar-protocol b/docs/i3bar-protocol index b8c2b5ad..cf86531c 100644 --- a/docs/i3bar-protocol +++ b/docs/i3bar-protocol @@ -177,7 +177,8 @@ separator_block_width:: markup:: A string that indicates how the text of the block should be parsed. Set to +"pango"+ to use https://developer.gnome.org/pango/stable/PangoMarkupFormat.html[Pango markup]. - Set to +"none"+ to not use any markup (default). + Set to +"none"+ to not use any markup (default). Pango markup only works + if you use a pango font. If you want to put in your own entries into a block, prefix the key with an underscore (_). i3bar will ignore all keys it doesn’t understand, and prefixing @@ -236,6 +237,11 @@ x, y:: X11 root window coordinates where the click occurred button:: X11 button ID (for example 1 to 3 for left/middle/right mouse button) +relative_x, relative_y:: + Coordinates where the click occurred, with respect to the top left corner + of the block +width, height:: + Width and height (in px) of the block *Example*: ------------------------------------------ @@ -244,6 +250,10 @@ button:: "instance": "eth0", "button": 1, "x": 1320, - "y": 1400 + "y": 1400, + "relative_x": 12, + "relative_y": 8, + "width": 50, + "height": 22 } ------------------------------------------ diff --git a/docs/ipc b/docs/ipc index 997a3055..8b767ade 100644 --- a/docs/ipc +++ b/docs/ipc @@ -64,6 +64,7 @@ to do that). | 7 | +GET_VERSION+ | <<_version_reply,VERSION>> | Gets the i3 version. | 8 | +GET_BINDING_MODES+ | <<_binding_modes_reply,BINDING_MODES>> | Gets the names of all currently configured binding modes. | 9 | +GET_CONFIG+ | <<_config_reply,CONFIG>> | Returns the last loaded i3 config. +| 10 | +SEND_TICK+ | <<_tick_reply,TICK>> | Sends a tick event with the specified payload. |====================================================== So, a typical message could look like this: @@ -126,6 +127,8 @@ BINDING_MODES (8):: Reply to the GET_BINDING_MODES message. GET_CONFIG (9):: Reply to the GET_CONFIG message. +TICK (10):: + Reply to the SEND_TICK message. [[_command_reply]] === COMMAND reply @@ -637,6 +640,19 @@ which is a string containing the config file as loaded by i3 most recently. { "config": "font pango:monospace 8\nbindsym Mod4+q exit\n" } ------------------- +[[_tick_reply]] +=== TICK reply + +The reply is a map containing the "success" member. After the reply was +received, the tick event has been written to all IPC connections which subscribe +to tick events. UNIX sockets are usually buffered, but you can be certain that +once you receive the tick event you just triggered, you must have received all +events generated prior to the +SEND_TICK+ message (happened-before relation). + +*Example:* +------------------- +{ "success": true } +------------------- == Events @@ -694,6 +710,10 @@ binding (5):: mouse shutdown (6):: Sent when the ipc shuts down because of a restart or exit by user command +tick (7):: + Sent when the ipc client subscribes to the tick event (with +"first": + true+) or when any ipc client sends a SEND_TICK message (with +"first": + false+). *Example:* -------------------------------------------------------------------- @@ -866,6 +886,27 @@ because of a user action such as a +restart+ or +exit+ command. The +change } --------------------------- +=== tick event + +This event is triggered by a subscription to tick events or by a +SEND_TICK+ +message. + +*Example (upon subscription):* +-------------------------------------------------------------------------------- +{ + "first": true, + "payload": "" +} +-------------------------------------------------------------------------------- + +*Example (upon +SEND_TICK+ with a payload of +arbitrary string+):* +-------------------------------------------------------------------------------- +{ + "first": false, + "payload": "arbitrary string" +} +-------------------------------------------------------------------------------- + == See also (existing libraries) [[libraries]] @@ -881,6 +922,7 @@ C++:: * https://github.com/drmgc/i3ipcpp Go:: * https://github.com/mdirkse/i3ipc-go + * https://github.com/i3/go-i3 JavaScript:: * https://github.com/acrisci/i3ipc-gjs Lua:: @@ -958,3 +1000,6 @@ detect the byte order i3 is using: payload. Then, receive the pending +COMMAND+ message reply in big endian. 5. From here on out, send/receive all messages using the detected byte order. + +Find an example implementation of this technique in +https://github.com/i3/go-i3/blob/master/byteorder.go diff --git a/docs/testsuite b/docs/testsuite index bf85cb1f..b535e7c1 100644 --- a/docs/testsuite +++ b/docs/testsuite @@ -113,10 +113,8 @@ containing the appropriate i3 logfile for each testcase. The latest folder can always be found under the symlink +latest/+. Unless told differently, it will run the tests on a separate X server instance (using Xephyr). -Xephyr will open a window where you can inspect the running test. You can run -the tests without an X session with Xvfb, such as with +xvfb-run -./complete-run+. This will also speed up the tests significantly especially on -machines without a powerful video card. +Xephyr will open a window where you can inspect the running test. By default, +tests are run under Xvfb. .Example invocation of +complete-run.pl+ --------------------------------------- diff --git a/docs/userguide b/docs/userguide index ebd0a881..ba314af1 100644 --- a/docs/userguide +++ b/docs/userguide @@ -1,7 +1,6 @@ i3 User’s Guide =============== Michael Stapelberg -March 2013 This document contains all the information you need to configure and use the i3 window manager. If it does not, please check https://www.reddit.com/r/i3wm/ @@ -11,7 +10,7 @@ mailing list. == Default keybindings For the "too long; didn’t read" people, here is an overview of the default -keybindings (click to see the full size image): +keybindings (click to see the full-size image): *Keys to use with $mod (Alt):* @@ -35,7 +34,8 @@ above, just decline i3-config-wizard’s offer and base your config on Throughout this guide, the keyword +$mod+ will be used to refer to the configured modifier. This is the Alt key (+Mod1+) by default, with the Windows -key (+Mod4+) being a popular alternative. +key (+Mod4+) being a popular alternative that largely prevents conflicts with +application-defined shortcuts. === Opening terminals and moving around @@ -196,7 +196,7 @@ out to be complicated to use (snapping), understand and implement. === The tree consists of Containers -The building blocks of our tree are so called +Containers+. A +Container+ can +The building blocks of our tree are so-called +Containers+. A +Container+ can host a window (meaning an X11 window, one that you can actually see and use, like a browser). Alternatively, it could contain one or more +Containers+. A simple example is the workspace: When you start i3 with a single monitor, a @@ -509,7 +509,7 @@ mode "$mode_launcher" { === The floating modifier To move floating windows with your mouse, you can either grab their titlebar -or configure the so called floating modifier which you can then press and +or configure the so-called floating modifier which you can then press and click anywhere in the window itself to move it. The most common setup is to use the same key you use for managing windows (Mod1 for example). Then you can press Mod1, click into a window using your left mouse button, and drag @@ -585,23 +585,26 @@ workspace_layout default|stacking|tabbed workspace_layout tabbed --------------------- -=== Border style for new windows +=== Default border style for new windows This option determines which border style new windows will have. The default is -+normal+. Note that new_float applies only to windows which are starting out as ++normal+. Note that default_floating_border applies only to windows which are starting out as floating windows, e.g., dialog windows, but not windows that are floated later on. *Syntax*: --------------------------------------------- -new_window normal|none|pixel -new_window normal|pixel -new_float normal|none|pixel -new_float normal|pixel +default_border normal|none|pixel +default_border normal|pixel +default_floating_border normal|none|pixel +default_floating_border normal|pixel --------------------------------------------- +Please note that +new_window+ and +new_float+ have been deprecated in favor of the above options +and will be removed in a future release. We strongly recommend using the new options instead. + *Example*: --------------------- -new_window pixel +default_border pixel --------------------- The "normal" and "pixel" border styles support an optional border width in @@ -609,11 +612,11 @@ pixels: *Example*: --------------------- -# The same as new_window none -new_window pixel 0 +# The same as default_border none +default_border pixel 0 # A 3 px border -new_window pixel 3 +default_border pixel 3 --------------------- @@ -760,13 +763,18 @@ 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 also assign a window to show up on a specific output. You can use RandR +names such as +VGA1+ or names relative to the output with the currently focused +workspace such as +left+ and +down+. + Assignments are processed by i3 in the order in which they appear in the config file. The first one which matches the window wins and later assignments are not considered. *Syntax*: ------------------------------------------------------------ -assign [→] [workspace] +assign [→] [workspace] [number] +assign [→] output left|right|up|down|primary| ------------------------------------------------------------ *Examples*: @@ -783,11 +791,28 @@ assign [class="^URxvt$"] → 2 # Assignment to a named workspace assign [class="^URxvt$"] → work +# Assign to the workspace with number 2, regardless of name +assign [class="^URxvt$"] → number 2 + +# You can also specify a number + name. If the workspace with number 2 exists, assign will skip the text part. +assign [class="^URxvt$"] → number "2: work" + # Start urxvt -name irssi assign [class="^URxvt$" instance="^irssi$"] → 3 + +# Assign urxvt to the output right of the current one +assign [class="^URxvt$"] → output right + +# Assign urxvt to the primary output +assign [class="^URxvt$"] → output primary ---------------------- -Note that the arrow is not required, it just looks good :-). If you decide to +Note that you might not have a primary output configured yet. To do so, run: +------------------------- +xrandr --output --primary +------------------------- + +Also, 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 @@ -1033,26 +1058,39 @@ popup_during_fullscreen smart === Focus wrapping -When being in a tabbed or stacked container, the first container will be -focused when you use +focus down+ on the last container -- the focus wraps. If -however there is another stacked/tabbed container in that direction, focus will -be set on that container. This is the default behavior so you can navigate to -all your windows without having to use +focus parent+. +By default, when in a container with several windows or child containers, the +opposite window will be focused when trying to move the focus over the edge of +a container (and there are no other containers in that direction) -- the focus +wraps. + +If desired, you can disable this behavior by setting the +focus_wrapping+ +configuration directive to the value +no+. + +When enabled, focus wrapping does not occur by default if there is another +window or container in the specified direction, and focus will instead be set +on that window or container. This is the default behavior so you can navigate +to all your windows without having to use +focus parent+. If you want the focus to *always* wrap and you are aware of using +focus -parent+ to switch to different containers, you can use the -+force_focus_wrapping+ configuration directive. After enabling it, the focus -will always wrap. +parent+ to switch to different containers, you can instead set +focus_wrapping+ +to the value +force+. *Syntax*: --------------------------- -force_focus_wrapping yes|no +focus_wrapping yes|no|force + +# Legacy syntax, equivalent to "focus_wrapping force" +force_focus_wrapping yes --------------------------- -*Example*: ------------------------- -force_focus_wrapping yes ------------------------- +*Examples*: +----------------- +# Disable focus wrapping +focus_wrapping no + +# Force focus wrapping +focus_wrapping force +----------------- === Forcing Xinerama @@ -1341,7 +1379,7 @@ and will be removed in a future release. We strongly recommend using the more ge *Syntax*: ---------------------------- -bindsym button +bindsym [--release] button ---------------------------- *Example*: @@ -1349,6 +1387,8 @@ bindsym button bar { # disable clicking on workspace buttons bindsym button1 nop + # Take a screenshot by right clicking on the bar + bindsym --release button3 exec --no-startup-id import /tmp/latest-screenshot.png # execute custom script when scrolling downwards bindsym button5 exec ~/.i3/scripts/custom_wheel_down } @@ -1913,6 +1953,9 @@ bindsym $mod+t floating toggle To change focus, you can use the +focus+ command. The following options are available: +:: + Sets focus to the container that matches the specified criteria. + See <>. left|right|up|down:: Sets focus to the nearest container in the given direction. parent:: @@ -1932,6 +1975,7 @@ output:: *Syntax*: ---------------------------------------------- + focus focus left|right|down|up focus parent|child|floating|tiling|mode_toggle focus output left|right|up|down|primary| @@ -1939,6 +1983,9 @@ focus output left|right|up|down|primary| *Examples*: ------------------------------------------------- +# Focus firefox +bindsym $mod+F1 [class="Firefox"] focus + # Focus container on the left, bottom, top, right bindsym $mod+j focus left bindsym $mod+k focus down @@ -2232,7 +2279,6 @@ bindsym $mod+x move container to output VGA1 bindsym $mod+x move container to output primary -------------------------------------------------------- -------------------------------- Note that you might not have a primary output configured yet. To do so, run: ------------------------- xrandr --output --primary @@ -2267,7 +2313,7 @@ If you want to resize containers/windows using your keyboard, you can use the *Syntax*: ------------------------------------------------------- resize grow|shrink [ px [or ppt]] -resize set [px] [px] +resize set [px | ppt] [px | ppt] ------------------------------------------------------- Direction can either be one of +up+, +down+, +left+ or +right+. Or you can be @@ -2276,8 +2322,11 @@ space from all the other containers. The optional pixel argument specifies by how many pixels a *floating container* should be grown or shrunk (the default is 10 pixels). The ppt argument means percentage points and specifies by how many percentage points a *tiling container* should be grown or shrunk (the -default is 10 percentage points). Note that +resize set+ will only work for -floating containers. +default is 10 percentage points). + +Notes about +resize set+: a value of 0 for or means "do +not resize in this direction", and resizing a tiling container by +px+ is not +implemented. It is recommended to define bindings for resizing in a dedicated binding mode. See <> and the example in the i3 @@ -2363,10 +2412,10 @@ TODO: make i3-input replace %s *Examples*: --------------------------------------- # Read 1 character and mark the current window with this character -bindsym $mod+m exec i3-input -p 'mark ' -l 1 -P 'Mark: ' +bindsym $mod+m exec i3-input -F 'mark %s' -l 1 -P 'Mark: ' # Read 1 character and go to the window with the character -bindsym $mod+g exec i3-input -p 'goto ' -l 1 -P 'Goto: ' +bindsym $mod+g exec i3-input -F '[con_mark="%s"] focus' -l 1 -P 'Goto: ' --------------------------------------- Alternatively, if you do not want to mess with +i3-input+, you could create diff --git a/etc/config b/etc/config index 483694c1..3be9831d 100644 --- a/etc/config +++ b/etc/config @@ -22,7 +22,7 @@ font pango:monospace 8 # The font above is very space-efficient, that is, it looks good, sharp and # clear in small sizes. However, its unicode glyph coverage is limited, the old # X core fonts rendering does not support right-to-left and this being a bitmap -# font, it doesn’t scale on retina/hidpi displays. +# font, it doesn't scale on retina/hidpi displays. # use these keys for focus, movement, and resize directions when reaching for # the arrows is not convenient @@ -104,29 +104,43 @@ bindsym Mod1+Shift+minus move scratchpad # If there are multiple scratchpad windows, this command cycles through them. bindsym Mod1+minus scratchpad show +# Define names for default workspaces for which we configure key bindings later on. +# We use variables to avoid repeating the names in multiple places. +set $ws1 "1" +set $ws2 "2" +set $ws3 "3" +set $ws4 "4" +set $ws5 "5" +set $ws6 "6" +set $ws7 "7" +set $ws8 "8" +set $ws9 "9" +set $ws10 "10" + + # switch to workspace -bindsym Mod1+1 workspace 1 -bindsym Mod1+2 workspace 2 -bindsym Mod1+3 workspace 3 -bindsym Mod1+4 workspace 4 -bindsym Mod1+5 workspace 5 -bindsym Mod1+6 workspace 6 -bindsym Mod1+7 workspace 7 -bindsym Mod1+8 workspace 8 -bindsym Mod1+9 workspace 9 -bindsym Mod1+0 workspace 10 +bindsym Mod1+1 workspace $ws1 +bindsym Mod1+2 workspace $ws2 +bindsym Mod1+3 workspace $ws3 +bindsym Mod1+4 workspace $ws4 +bindsym Mod1+5 workspace $ws5 +bindsym Mod1+6 workspace $ws6 +bindsym Mod1+7 workspace $ws7 +bindsym Mod1+8 workspace $ws8 +bindsym Mod1+9 workspace $ws9 +bindsym Mod1+0 workspace $ws10 # move focused container to workspace -bindsym Mod1+Shift+1 move container to workspace 1 -bindsym Mod1+Shift+2 move container to workspace 2 -bindsym Mod1+Shift+3 move container to workspace 3 -bindsym Mod1+Shift+4 move container to workspace 4 -bindsym Mod1+Shift+5 move container to workspace 5 -bindsym Mod1+Shift+6 move container to workspace 6 -bindsym Mod1+Shift+7 move container to workspace 7 -bindsym Mod1+Shift+8 move container to workspace 8 -bindsym Mod1+Shift+9 move container to workspace 9 -bindsym Mod1+Shift+0 move container to workspace 10 +bindsym Mod1+Shift+1 move container to workspace $ws1 +bindsym Mod1+Shift+2 move container to workspace $ws2 +bindsym Mod1+Shift+3 move container to workspace $ws3 +bindsym Mod1+Shift+4 move container to workspace $ws4 +bindsym Mod1+Shift+5 move container to workspace $ws5 +bindsym Mod1+Shift+6 move container to workspace $ws6 +bindsym Mod1+Shift+7 move container to workspace $ws7 +bindsym Mod1+Shift+8 move container to workspace $ws8 +bindsym Mod1+Shift+9 move container to workspace $ws9 +bindsym Mod1+Shift+0 move container to workspace $ws10 # reload the configuration file bindsym Mod1+Shift+c reload @@ -154,9 +168,10 @@ mode "resize" { bindsym Up resize shrink height 10 px or 10 ppt bindsym Right resize grow width 10 px or 10 ppt - # back to normal: Enter or Escape + # back to normal: Enter or Escape or Mod1+r bindsym Return mode "default" bindsym Escape mode "default" + bindsym Mod1+r mode "default" } bindsym Mod1+r mode "resize" diff --git a/etc/config.keycodes b/etc/config.keycodes index 6d10fad2..2d56876c 100644 --- a/etc/config.keycodes +++ b/etc/config.keycodes @@ -91,29 +91,42 @@ bindcode $mod+38 focus parent # focus the child container #bindsym $mod+d focus child +# Define names for default workspaces for which we configure key bindings later on. +# We use variables to avoid repeating the names in multiple places. +set $ws1 "1" +set $ws2 "2" +set $ws3 "3" +set $ws4 "4" +set $ws5 "5" +set $ws6 "6" +set $ws7 "7" +set $ws8 "8" +set $ws9 "9" +set $ws10 "10" + # switch to workspace -bindcode $mod+10 workspace 1 -bindcode $mod+11 workspace 2 -bindcode $mod+12 workspace 3 -bindcode $mod+13 workspace 4 -bindcode $mod+14 workspace 5 -bindcode $mod+15 workspace 6 -bindcode $mod+16 workspace 7 -bindcode $mod+17 workspace 8 -bindcode $mod+18 workspace 9 -bindcode $mod+19 workspace 10 +bindcode $mod+10 workspace $ws1 +bindcode $mod+11 workspace $ws2 +bindcode $mod+12 workspace $ws3 +bindcode $mod+13 workspace $ws4 +bindcode $mod+14 workspace $ws5 +bindcode $mod+15 workspace $ws6 +bindcode $mod+16 workspace $ws7 +bindcode $mod+17 workspace $ws8 +bindcode $mod+18 workspace $ws9 +bindcode $mod+19 workspace $ws10 # move focused container to workspace -bindcode $mod+Shift+10 move container to workspace 1 -bindcode $mod+Shift+11 move container to workspace 2 -bindcode $mod+Shift+12 move container to workspace 3 -bindcode $mod+Shift+13 move container to workspace 4 -bindcode $mod+Shift+14 move container to workspace 5 -bindcode $mod+Shift+15 move container to workspace 6 -bindcode $mod+Shift+16 move container to workspace 7 -bindcode $mod+Shift+17 move container to workspace 8 -bindcode $mod+Shift+18 move container to workspace 9 -bindcode $mod+Shift+19 move container to workspace 10 +bindcode $mod+Shift+10 move container to workspace $ws1 +bindcode $mod+Shift+11 move container to workspace $ws2 +bindcode $mod+Shift+12 move container to workspace $ws3 +bindcode $mod+Shift+13 move container to workspace $ws4 +bindcode $mod+Shift+14 move container to workspace $ws5 +bindcode $mod+Shift+15 move container to workspace $ws6 +bindcode $mod+Shift+16 move container to workspace $ws7 +bindcode $mod+Shift+17 move container to workspace $ws8 +bindcode $mod+Shift+18 move container to workspace $ws9 +bindcode $mod+Shift+19 move container to workspace $ws10 # reload the configuration file bindcode $mod+Shift+54 reload @@ -141,9 +154,10 @@ mode "resize" { bindcode 111 resize shrink height 10 px or 10 ppt bindcode 114 resize grow width 10 px or 10 ppt - # back to normal: Enter or Escape + # back to normal: Enter or Escape or $mod+r bindcode 36 mode "default" bindcode 9 mode "default" + bindcode $mod+27 mode "default" } bindcode $mod+27 mode "resize" diff --git a/generate-command-parser.pl b/generate-command-parser.pl index a7687c7b..4c45b6ed 100755 --- a/generate-command-parser.pl +++ b/generate-command-parser.pl @@ -116,17 +116,16 @@ my @keys = sort { (length($b) <=> length($a)) or ($a cmp $b) } keys %states; open(my $enumfh, '>', "GENERATED_${prefix}_enums.h"); -# XXX: we might want to have a way to do this without a trailing comma, but gcc -# seems to eat it. my %statenum; say $enumfh 'typedef enum {'; my $cnt = 0; for my $state (@keys, '__CALL') { - say $enumfh " $state = $cnt,"; + say $enumfh ',' if $cnt > 0; + print $enumfh " $state = $cnt"; $statenum{$state} = $cnt; $cnt++; } -say $enumfh '} cmdp_state;'; +say $enumfh "\n} cmdp_state;"; close($enumfh); # Third step: Generate the call function. @@ -225,7 +224,7 @@ for my $state (@keys) { $next_state = '__CALL'; } my $identifier = $token->{identifier}; - say $tokfh qq| { "$token_name", "$identifier", $next_state, { $call_identifier } }, |; + say $tokfh qq| { "$token_name", "$identifier", $next_state, { $call_identifier } },|; } say $tokfh '};'; } diff --git a/i3-config-wizard/main.c b/i3-config-wizard/main.c index dd58fd12..b368921f 100644 --- a/i3-config-wizard/main.c +++ b/i3-config-wizard/main.c @@ -58,12 +58,10 @@ #error "SYSCONFDIR not defined" #endif -#define FREE(pointer) \ - do { \ - if (pointer != NULL) { \ - free(pointer); \ - pointer = NULL; \ - } \ +#define FREE(pointer) \ + do { \ + free(pointer); \ + pointer = NULL; \ } while (0) #include "xcb.h" @@ -94,7 +92,7 @@ static xcb_get_modifier_mapping_reply_t *modmap_reply; static i3Font font; static i3Font bold_font; static int char_width; -static char *socket_path; +static char *socket_path = NULL; static xcb_window_t win; static surface_t surface; static xcb_key_symbols_t *symbols; @@ -744,7 +742,6 @@ static void finish() { int main(int argc, char *argv[]) { char *xdg_config_home; - socket_path = getenv("I3SOCK"); char *pattern = "pango:monospace 8"; char *patternbold = "pango:monospace bold 8"; int o, option_index = 0; @@ -824,12 +821,6 @@ int main(int argc, char *argv[]) { &xkb_base_error) != 1) errx(EXIT_FAILURE, "Could not setup XKB extension."); - if (socket_path == NULL) - socket_path = root_atom_contents("I3_SOCKET_PATH", conn, screen); - - if (socket_path == NULL) - socket_path = "/tmp/i3-ipc.sock"; - keysyms = xcb_key_symbols_alloc(conn); xcb_get_modifier_mapping_cookie_t modmap_cookie; modmap_cookie = xcb_get_modifier_mapping(conn); diff --git a/i3-dump-log/main.c b/i3-dump-log/main.c index 478af310..e9901f8e 100644 --- a/i3-dump-log/main.c +++ b/i3-dump-log/main.c @@ -25,6 +25,7 @@ #include #include #include +#include #include "libi3.h" #include "shmlog.h" @@ -38,6 +39,29 @@ static uint32_t wrap_count; static i3_shmlog_header *header; static char *logbuffer, *walk; +static int ipcfd = -1; + +static volatile bool interrupted = false; + +static void sighandler(int signal) { + interrupted = true; +} + +static void disable_shmlog(void) { + const char *disablecmd = "debuglog off; shmlog off"; + if (ipc_send_message(ipcfd, strlen(disablecmd), + I3_IPC_MESSAGE_TYPE_COMMAND, (uint8_t *)disablecmd) != 0) + err(EXIT_FAILURE, "IPC send"); + + /* Ensure the command was sent by waiting for the reply: */ + uint32_t reply_length = 0; + uint8_t *reply = NULL; + if (ipc_recv_message(ipcfd, I3_IPC_REPLY_TYPE_COMMAND, + &reply_length, &reply) != 0) { + err(EXIT_FAILURE, "IPC recv"); + } + free(reply); +} static int check_for_wrap(void) { if (wrap_count == header->wrap_count) @@ -59,6 +83,14 @@ static void print_till_end(void) { walk += len; } +void errorlog(char *fmt, ...) { + va_list args; + + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); +} + int main(int argc, char *argv[]) { int o, option_index = 0; bool verbose = false; @@ -123,15 +155,35 @@ int main(int argc, char *argv[]) { exit(1); } if (root_atom_contents("I3_CONFIG_PATH", conn, screen) != NULL) { - fprintf(stderr, "i3-dump-log: ERROR: i3 is running, but SHM logging is not enabled.\n\n"); - if (!is_debug_build()) { + fprintf(stderr, "i3-dump-log: ERROR: i3 is running, but SHM logging is not enabled. Enabling SHM log until cancelled\n\n"); + ipcfd = ipc_connect(NULL); + const char *enablecmd = "debuglog on; shmlog 5242880"; + if (ipc_send_message(ipcfd, strlen(enablecmd), + I3_IPC_MESSAGE_TYPE_COMMAND, (uint8_t *)enablecmd) != 0) + err(EXIT_FAILURE, "IPC send"); + /* By the time we receive a reply, I3_SHMLOG_PATH is set: */ + uint32_t reply_length = 0; + uint8_t *reply = NULL; + if (ipc_recv_message(ipcfd, I3_IPC_REPLY_TYPE_COMMAND, + &reply_length, &reply) != 0) { + err(EXIT_FAILURE, "IPC recv"); + } + free(reply); + + atexit(disable_shmlog); + + /* Retry: */ + shmname = root_atom_contents("I3_SHMLOG_PATH", NULL, 0); + if (shmname == NULL && !is_debug_build()) { fprintf(stderr, "You seem to be using a release version of i3:\n %s\n\n", I3_VERSION); fprintf(stderr, "Release versions do not use SHM logging by default,\ntherefore i3-dump-log does not work.\n\n"); fprintf(stderr, "Please follow this guide instead:\nhttps://i3wm.org/docs/debugging-release-version.html\n"); exit(1); } } - errx(EXIT_FAILURE, "Cannot get I3_SHMLOG_PATH atom contents. Is i3 running on this display?"); + if (shmname == NULL) { + errx(EXIT_FAILURE, "Cannot get I3_SHMLOG_PATH atom contents. Is i3 running on this display?"); + } } if (*shmname == '\0') @@ -182,22 +234,32 @@ int main(int argc, char *argv[]) { print_till_end(); #if !defined(__OpenBSD__) - if (follow) { - /* Since pthread_cond_wait() expects a mutex, we need to provide one. + if (!follow) { + return 0; + } + + /* Handle SIGINT gracefully to invoke atexit handlers, if any. */ + struct sigaction action; + action.sa_handler = sighandler; + sigemptyset(&action.sa_mask); + action.sa_flags = 0; + sigaction(SIGINT, &action, NULL); + + /* Since pthread_cond_wait() expects a mutex, we need to provide one. * To not lock i3 (that’s bad, mhkay?) we just define one outside of * the shared memory. */ - pthread_mutex_t dummy_mutex = PTHREAD_MUTEX_INITIALIZER; - pthread_mutex_lock(&dummy_mutex); - while (1) { - pthread_cond_wait(&(header->condvar), &dummy_mutex); - /* If this was not a spurious wakeup, print the new lines. */ - if (header->offset_next_write != offset_next_write) { - offset_next_write = header->offset_next_write; - print_till_end(); - } + pthread_mutex_t dummy_mutex = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&dummy_mutex); + while (!interrupted) { + pthread_cond_wait(&(header->condvar), &dummy_mutex); + /* If this was not a spurious wakeup, print the new lines. */ + if (header->offset_next_write != offset_next_write) { + offset_next_write = header->offset_next_write; + print_till_end(); } } -#endif +#endif + exit(0); return 0; } diff --git a/i3-input/i3-input.h b/i3-input/i3-input.h index d347506f..d7aae66b 100644 --- a/i3-input/i3-input.h +++ b/i3-input/i3-input.h @@ -5,12 +5,10 @@ #include #define die(...) errx(EXIT_FAILURE, __VA_ARGS__); -#define FREE(pointer) \ - do { \ - if (pointer != NULL) { \ - free(pointer); \ - pointer = NULL; \ - } \ +#define FREE(pointer) \ + do { \ + free(pointer); \ + pointer = NULL; \ } while (0) extern xcb_window_t root; diff --git a/i3-input/main.c b/i3-input/main.c index 785a133f..efb7b20c 100644 --- a/i3-input/main.c +++ b/i3-input/main.c @@ -41,7 +41,6 @@ * the command will be sent to i3 */ static char *format; -static char *socket_path; static int sockfd; static xcb_key_symbols_t *symbols; static bool modeswitch_active = false; @@ -374,7 +373,7 @@ free_resources: int main(int argc, char *argv[]) { format = sstrdup("%s"); - socket_path = getenv("I3SOCK"); + char *socket_path = NULL; char *pattern = sstrdup("pango:monospace 8"); int o, option_index = 0; @@ -438,12 +437,6 @@ int main(int argc, char *argv[]) { if (!conn || xcb_connection_has_error(conn)) die("Cannot open display\n"); - if (socket_path == NULL) - socket_path = root_atom_contents("I3_SOCKET_PATH", conn, screen); - - if (socket_path == NULL) - socket_path = "/tmp/i3-ipc.sock"; - sockfd = ipc_connect(socket_path); root_screen = xcb_aux_get_screen(conn, screen); diff --git a/i3-msg/main.c b/i3-msg/main.c index 8907a6f7..91a714e5 100644 --- a/i3-msg/main.c +++ b/i3-msg/main.c @@ -38,8 +38,6 @@ #include -static char *socket_path; - /* * Having verboselog() and errorlog() is necessary when using libi3. * @@ -161,11 +159,7 @@ int main(int argc, char *argv[]) { if (pledge("stdio rpath unix", NULL) == -1) err(EXIT_FAILURE, "pledge"); #endif - char *env_socket_path = getenv("I3SOCK"); - if (env_socket_path) - socket_path = sstrdup(env_socket_path); - else - socket_path = NULL; + char *socket_path = NULL; int o, option_index = 0; uint32_t message_type = I3_IPC_MESSAGE_TYPE_RUN_COMMAND; char *payload = NULL; @@ -183,8 +177,7 @@ int main(int argc, char *argv[]) { while ((o = getopt_long(argc, argv, options_string, long_options, &option_index)) != -1) { if (o == 's') { - if (socket_path != NULL) - free(socket_path); + free(socket_path); socket_path = sstrdup(optarg); } else if (o == 't') { if (strcasecmp(optarg, "command") == 0) { @@ -207,9 +200,11 @@ int main(int argc, char *argv[]) { message_type = I3_IPC_MESSAGE_TYPE_GET_VERSION; } else if (strcasecmp(optarg, "get_config") == 0) { message_type = I3_IPC_MESSAGE_TYPE_GET_CONFIG; + } else if (strcasecmp(optarg, "send_tick") == 0) { + message_type = I3_IPC_MESSAGE_TYPE_SEND_TICK; } else { printf("Unknown message type\n"); - printf("Known types: run_command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_binding_modes, get_version, get_config\n"); + printf("Known types: run_command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_binding_modes, get_version, get_config, send_tick\n"); exit(EXIT_FAILURE); } } else if (o == 'q') { @@ -226,13 +221,6 @@ int main(int argc, char *argv[]) { } } - if (socket_path == NULL) - socket_path = root_atom_contents("I3_SOCKET_PATH", NULL, 0); - - /* Fall back to the default socket path */ - if (socket_path == NULL) - socket_path = sstrdup("/tmp/i3-ipc.sock"); - /* Use all arguments, separated by whitespace, as payload. * This way, you don’t have to do i3-msg 'mark foo', you can use * i3-msg mark foo */ @@ -251,17 +239,7 @@ int main(int argc, char *argv[]) { if (!payload) payload = sstrdup(""); - int sockfd = socket(AF_LOCAL, SOCK_STREAM, 0); - if (sockfd == -1) - err(EXIT_FAILURE, "Could not create socket"); - - struct sockaddr_un addr; - memset(&addr, 0, sizeof(struct sockaddr_un)); - addr.sun_family = AF_LOCAL; - strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); - if (connect(sockfd, (const struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) - err(EXIT_FAILURE, "Could not connect to i3 on socket \"%s\"", socket_path); - + int sockfd = ipc_connect(socket_path); if (ipc_send_message(sockfd, strlen(payload), message_type, (uint8_t *)payload) == -1) err(EXIT_FAILURE, "IPC: write()"); free(payload); diff --git a/i3-nagbar/i3-nagbar.h b/i3-nagbar/i3-nagbar.h index c5e94cc6..cb672bea 100644 --- a/i3-nagbar/i3-nagbar.h +++ b/i3-nagbar/i3-nagbar.h @@ -5,12 +5,10 @@ #include #define die(...) errx(EXIT_FAILURE, __VA_ARGS__); -#define FREE(pointer) \ - do { \ - if (pointer != NULL) { \ - free(pointer); \ - pointer = NULL; \ - } \ +#define FREE(pointer) \ + do { \ + free(pointer); \ + pointer = NULL; \ } while (0) #define xmacro(atom) xcb_atom_t A_##atom; diff --git a/i3-nagbar/main.c b/i3-nagbar/main.c index 7d38f731..e4628e30 100644 --- a/i3-nagbar/main.c +++ b/i3-nagbar/main.c @@ -575,7 +575,9 @@ int main(int argc, char *argv[]) { case XCB_CONFIGURE_NOTIFY: { xcb_configure_notify_event_t *configure_notify = (xcb_configure_notify_event_t *)event; - draw_util_surface_set_size(&bar, configure_notify->width, configure_notify->height); + if (configure_notify->width > 0 && configure_notify->height > 0) { + draw_util_surface_set_size(&bar, configure_notify->width, configure_notify->height); + } break; } } diff --git a/i3-sensible-editor b/i3-sensible-editor index ad3f6bdd..dc95865d 100755 --- a/i3-sensible-editor +++ b/i3-sensible-editor @@ -9,7 +9,7 @@ # mechanism to find the preferred editor # Hopefully one of these is installed (no flamewars about preference please!): -for editor in "$VISUAL" "$EDITOR" nano nvim vim vi emacs pico qe mg jed gedit mcedit; do +for editor in "$VISUAL" "$EDITOR" nano nvim vim vi emacs pico qe mg jed gedit mcedit gvim; do if command -v "$editor" > /dev/null 2>&1; then exec "$editor" "$@" fi diff --git a/i3-sensible-terminal b/i3-sensible-terminal index f92ff224..f1eb256e 100755 --- a/i3-sensible-terminal +++ b/i3-sensible-terminal @@ -8,7 +8,7 @@ # We welcome patches that add distribution-specific mechanisms to find the # preferred terminal emulator. On Debian, there is the x-terminal-emulator # symlink for example. -for terminal in "$TERMINAL" x-terminal-emulator urxvt rxvt termit terminator Eterm aterm uxterm xterm gnome-terminal roxterm xfce4-terminal termite lxterminal mate-terminal terminology st qterminal lilyterm tilix terminix konsole; do +for terminal in "$TERMINAL" x-terminal-emulator urxvt rxvt termit terminator Eterm aterm uxterm xterm gnome-terminal roxterm xfce4-terminal termite lxterminal mate-terminal terminology st qterminal lilyterm tilix terminix konsole kitty guake tilda; do if command -v "$terminal" > /dev/null 2>&1; then exec "$terminal" "$@" fi diff --git a/i3bar/include/child.h b/i3bar/include/child.h index 0871c7f4..9479fac1 100644 --- a/i3bar/include/child.h +++ b/i3bar/include/child.h @@ -85,4 +85,4 @@ bool child_want_click_events(void); * Generates a click event, if enabled. * */ -void send_block_clicked(int button, const char *name, const char *instance, int x, int y); +void send_block_clicked(int button, const char *name, const char *instance, int x, int y, int x_rel, int y_rel, int width, int height); diff --git a/i3bar/include/configuration.h b/i3bar/include/configuration.h index e77e891b..61cac7f6 100644 --- a/i3bar/include/configuration.h +++ b/i3bar/include/configuration.h @@ -27,6 +27,7 @@ typedef enum { M_DOCK = 0, typedef struct binding_t { int input_code; char *command; + bool release; TAILQ_ENTRY(binding_t) bindings; diff --git a/i3bar/include/outputs.h b/i3bar/include/outputs.h index de960270..29a7bcd3 100644 --- a/i3bar/include/outputs.h +++ b/i3bar/include/outputs.h @@ -33,6 +33,12 @@ void parse_outputs_json(char* json); */ void init_outputs(void); +/* + * free() all outputs data structures. + * + */ +void free_outputs(void); + /* * Returns the output with the given name * diff --git a/i3bar/include/util.h b/i3bar/include/util.h index 3af79ed7..1f563611 100644 --- a/i3bar/include/util.h +++ b/i3bar/include/util.h @@ -20,12 +20,10 @@ #define STARTS_WITH(string, len, needle) (((len) >= strlen((needle))) && strncasecmp((string), (needle), strlen((needle))) == 0) /* Securely free p */ -#define FREE(p) \ - do { \ - if (p != NULL) { \ - free(p); \ - p = NULL; \ - } \ +#define FREE(p) \ + do { \ + free(p); \ + p = NULL; \ } while (0) /* Securely free single-linked list */ diff --git a/i3bar/include/xcb_atoms.def b/i3bar/include/xcb_atoms.def index 65a147c4..453dd0ce 100644 --- a/i3bar/include/xcb_atoms.def +++ b/i3bar/include/xcb_atoms.def @@ -9,4 +9,5 @@ ATOM_DO(_NET_SYSTEM_TRAY_OPCODE) ATOM_DO(_NET_SYSTEM_TRAY_COLORS) ATOM_DO(_XEMBED_INFO) ATOM_DO(_XEMBED) +ATOM_DO(I3_SYNC) #undef ATOM_DO diff --git a/i3bar/src/child.c b/i3bar/src/child.c index fe989c44..1cd7d512 100644 --- a/i3bar/src/child.c +++ b/i3bar/src/child.c @@ -106,7 +106,7 @@ __attribute__((format(printf, 1, 2))) static void set_statusline_error(const cha va_list args; va_start(args, format); if (vasprintf(&message, format, args) == -1) { - return; + goto finish; } struct status_block *err_block = scalloc(1, sizeof(struct status_block)); @@ -124,6 +124,7 @@ __attribute__((format(printf, 1, 2))) static void set_statusline_error(const cha TAILQ_INSERT_HEAD(&statusline_head, err_block, blocks); TAILQ_INSERT_TAIL(&statusline_head, message_block, blocks); +finish: FREE(message); va_end(args); } @@ -595,7 +596,7 @@ void child_click_events_key(const char *key) { * Generates a click event, if enabled. * */ -void send_block_clicked(int button, const char *name, const char *instance, int x, int y) { +void send_block_clicked(int button, const char *name, const char *instance, int x, int y, int x_rel, int y_rel, int width, int height) { if (!child.click_events) { return; } @@ -623,6 +624,18 @@ void send_block_clicked(int button, const char *name, const char *instance, int child_click_events_key("y"); yajl_gen_integer(gen, y); + child_click_events_key("relative_x"); + yajl_gen_integer(gen, x_rel); + + child_click_events_key("relative_y"); + yajl_gen_integer(gen, y_rel); + + child_click_events_key("width"); + yajl_gen_integer(gen, width); + + child_click_events_key("height"); + yajl_gen_integer(gen, height); + yajl_gen_map_close(gen); child_write_output(); } diff --git a/i3bar/src/config.c b/i3bar/src/config.c index cbe84d50..a58b9bf8 100644 --- a/i3bar/src/config.c +++ b/i3bar/src/config.c @@ -107,34 +107,34 @@ static int config_string_cb(void *params_, const unsigned char *val, size_t _len if (!strcmp(cur_key, "mode")) { DLOG("mode = %.*s, len = %d\n", len, val, len); - config.hide_on_modifier = (len == 4 && !strncmp((const char *)val, "dock", strlen("dock")) ? M_DOCK - : (len == 4 && !strncmp((const char *)val, "hide", strlen("hide")) ? M_HIDE - : M_INVISIBLE)); + config.hide_on_modifier = (len == strlen("dock") && !strncmp((const char *)val, "dock", strlen("dock")) ? M_DOCK + : (len == strlen("hide") && !strncmp((const char *)val, "hide", strlen("hide")) ? M_HIDE + : M_INVISIBLE)); return 1; } if (!strcmp(cur_key, "hidden_state")) { DLOG("hidden_state = %.*s, len = %d\n", len, val, len); - config.hidden_state = (len == 4 && !strncmp((const char *)val, "hide", strlen("hide")) ? S_HIDE : S_SHOW); + config.hidden_state = (len == strlen("hide") && !strncmp((const char *)val, "hide", strlen("hide")) ? S_HIDE : S_SHOW); return 1; } if (!strcmp(cur_key, "modifier")) { DLOG("modifier = %.*s\n", len, val); - if (len == 4 && !strncmp((const char *)val, "none", strlen("none"))) { + if (len == strlen("none") && !strncmp((const char *)val, "none", strlen("none"))) { config.modifier = XCB_NONE; return 1; } - if (len == 5 && !strncmp((const char *)val, "shift", strlen("shift"))) { + if (len == strlen("shift") && !strncmp((const char *)val, "shift", strlen("shift"))) { config.modifier = ShiftMask; return 1; } - if (len == 4 && !strncmp((const char *)val, "ctrl", strlen("ctrl"))) { + if (len == strlen("ctrl") && !strncmp((const char *)val, "ctrl", strlen("ctrl"))) { config.modifier = ControlMask; return 1; } - if (len == 4 && !strncmp((const char *)val, "Mod", strlen("Mod"))) { + if (len == strlen("Mod") + 1 && !strncmp((const char *)val, "Mod", strlen("Mod"))) { switch (val[3]) { case '1': config.modifier = Mod1Mask; @@ -179,7 +179,7 @@ static int config_string_cb(void *params_, const unsigned char *val, size_t _len if (!strcmp(cur_key, "position")) { DLOG("position = %.*s\n", len, val); - config.position = (len == 3 && !strncmp((const char *)val, "top", strlen("top")) ? POS_TOP : POS_BOT); + config.position = (len == strlen("top") && !strncmp((const char *)val, "top", strlen("top")) ? POS_TOP : POS_BOT); return 1; } @@ -192,6 +192,7 @@ static int config_string_cb(void *params_, const unsigned char *val, size_t _len if (!strcmp(cur_key, "font")) { DLOG("font = %.*s\n", len, val); + FREE(config.fontname); sasprintf(&config.fontname, "%.*s", len, val); return 1; } @@ -263,6 +264,21 @@ static int config_string_cb(void *params_, const unsigned char *val, size_t _len * */ static int config_boolean_cb(void *params_, int val) { + if (parsing_bindings) { + if (strcmp(cur_key, "release") == 0) { + binding_t *binding = TAILQ_LAST(&(config.bindings), bindings_head); + if (binding == NULL) { + ELOG("There is no binding to put the current command onto. This is a bug in i3.\n"); + return 0; + } + + binding->release = val; + return 1; + } + + ELOG("Unknown key \"%s\" while parsing bar bindings.\n", cur_key); + } + if (!strcmp(cur_key, "binding_mode_indicator")) { DLOG("binding_mode_indicator = %d\n", val); config.disable_binding_mode_indicator = !val; diff --git a/i3bar/src/ipc.c b/i3bar/src/ipc.c index c932aaf7..cc3563ec 100644 --- a/i3bar/src/ipc.c +++ b/i3bar/src/ipc.c @@ -64,17 +64,14 @@ void got_subscribe_reply(char *reply) { */ void got_output_reply(char *reply) { DLOG("Clearing old output configuration...\n"); - i3_output *o_walk; - SLIST_FOREACH(o_walk, outputs, slist) { - destroy_window(o_walk); - } - FREE_SLIST(outputs, i3_output); + free_outputs(); DLOG("Parsing outputs JSON...\n"); parse_outputs_json(reply); DLOG("Reconfiguring windows...\n"); reconfig_windows(false); + i3_output *o_walk; SLIST_FOREACH(o_walk, outputs, slist) { kick_tray_clients(o_walk); } @@ -177,7 +174,7 @@ void got_bar_config_update(char *event) { /* update the configuration with the received settings */ DLOG("Received bar config update \"%s\"\n", event); - char *old_command = sstrdup(config.command); + char *old_command = config.command ? sstrdup(config.command) : NULL; bar_display_mode_t old_mode = config.hide_on_modifier; parse_config_json(event); if (old_mode != config.hide_on_modifier) { @@ -189,7 +186,7 @@ void got_bar_config_update(char *event) { init_colors(&(config.colors)); /* restart status command process */ - if (strcmp(old_command, config.command) != 0) { + if (old_command && strcmp(old_command, config.command) != 0) { kill_child(); start_child(config.command); } diff --git a/i3bar/src/main.c b/i3bar/src/main.c index 910e9524..069803d4 100644 --- a/i3bar/src/main.c +++ b/i3bar/src/main.c @@ -182,7 +182,5 @@ int main(int argc, char **argv) { clean_xcb(); ev_default_destroy(); - free_workspaces(); - return 0; } diff --git a/i3bar/src/outputs.c b/i3bar/src/outputs.c index bd056a70..4c9ce651 100644 --- a/i3bar/src/outputs.c +++ b/i3bar/src/outputs.c @@ -173,6 +173,12 @@ static int outputs_start_map_cb(void *params_) { return 1; } +static void clear_output(i3_output *output) { + FREE(output->name); + FREE(output->workspaces); + FREE(output->trayclients); +} + /* * We hit the end of a map (rect or a new output) * @@ -199,9 +205,7 @@ static int outputs_end_map_cb(void *params_) { if (!handle_output) { DLOG("Ignoring output \"%s\", not configured to handle it.\n", params->outputs_walk->name); - FREE(params->outputs_walk->name); - FREE(params->outputs_walk->workspaces); - FREE(params->outputs_walk->trayclients); + clear_output(params->outputs_walk); FREE(params->outputs_walk); FREE(params->cur_key); return 1; @@ -217,6 +221,9 @@ static int outputs_end_map_cb(void *params_) { target->primary = params->outputs_walk->primary; target->ws = params->outputs_walk->ws; target->rect = params->outputs_walk->rect; + + clear_output(params->outputs_walk); + FREE(params->outputs_walk); } return 1; } @@ -260,7 +267,6 @@ void init_outputs(void) { */ void parse_outputs_json(char *json) { struct outputs_json_params params; - params.outputs_walk = NULL; params.cur_key = NULL; params.json = json; @@ -286,6 +292,27 @@ void parse_outputs_json(char *json) { yajl_free(handle); } +/* + * free() all outputs data structures. + * + */ +void free_outputs(void) { + free_workspaces(); + + i3_output *outputs_walk; + if (outputs == NULL) { + return; + } + SLIST_FOREACH(outputs_walk, outputs, slist) { + destroy_window(outputs_walk); + if (outputs_walk->trayclients != NULL && !TAILQ_EMPTY(outputs_walk->trayclients)) { + FREE_TAILQ(outputs_walk->trayclients, trayclient); + } + clear_output(outputs_walk); + } + FREE_SLIST(outputs, i3_output); +} + /* * Returns the output with the given name * diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index fed969df..542c86c3 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -83,7 +83,6 @@ int mod_pressed = 0; /* Event watchers, to interact with the user */ ev_prepare *xcb_prep; -ev_check *xcb_chk; ev_io *xcb_io; ev_io *xkb_io; @@ -440,6 +439,18 @@ void init_colors(const struct xcb_color_strings_t *new_colors) { xcb_flush(xcb_connection); } +static bool execute_custom_command(xcb_keycode_t input_code, bool event_is_release) { + binding_t *binding; + TAILQ_FOREACH(binding, &(config.bindings), bindings) { + if ((binding->input_code != input_code) || (binding->release != event_is_release)) + continue; + + i3_send_msg(I3_IPC_MESSAGE_TYPE_RUN_COMMAND, binding->command); + return true; + } + return false; +} + /* * Handle a button press event (i.e. a mouse click on one of our bars). * We determine, whether the click occurred on a workspace button or if the scroll- @@ -461,10 +472,16 @@ void handle_button(xcb_button_press_event_t *event) { return; } - int32_t x = event->event_x >= 0 ? event->event_x : 0; - DLOG("Got button %d\n", event->detail); + /* During button release events, only check for custom commands. */ + const bool event_is_release = (event->response_type & ~0x80) == XCB_BUTTON_RELEASE; + if (event_is_release) { + execute_custom_command(event->detail, event_is_release); + return; + } + + int32_t x = event->event_x >= 0 ? event->event_x : 0; int workspace_width = 0; i3_ws *cur_ws = NULL, *clicked_ws = NULL, *ws_walk; @@ -506,7 +523,8 @@ void handle_button(xcb_button_press_event_t *event) { block_x += render->width + render->x_offset + render->x_append + get_sep_offset(block) + sep_offset_remainder; if (statusline_x <= block_x && statusline_x >= last_block_x) { - send_block_clicked(event->detail, block->name, block->instance, event->root_x, event->root_y); + send_block_clicked(event->detail, block->name, block->instance, + event->root_x, event->root_y, statusline_x - last_block_x, event->event_y, block_x - last_block_x, bar_height); return; } @@ -517,12 +535,7 @@ void handle_button(xcb_button_press_event_t *event) { /* If a custom command was specified for this mouse button, it overrides * the default behavior. */ - binding_t *binding; - TAILQ_FOREACH(binding, &(config.bindings), bindings) { - if (binding->input_code != event->detail) - continue; - - i3_send_msg(I3_IPC_MESSAGE_TYPE_RUN_COMMAND, binding->command); + if (execute_custom_command(event->detail, event_is_release)) { return; } @@ -678,8 +691,26 @@ static void configure_trayclients(void) { * */ static void handle_client_message(xcb_client_message_event_t *event) { - if (event->type == atoms[_NET_SYSTEM_TRAY_OPCODE] && - event->format == 32) { + if (event->type == atoms[I3_SYNC]) { + xcb_window_t window = event->data.data32[0]; + uint32_t rnd = event->data.data32[1]; + DLOG("[i3 sync protocol] Forwarding random value %d, X11 window 0x%08x to i3\n", rnd, window); + + void *reply = scalloc(32, 1); + xcb_client_message_event_t *ev = reply; + + ev->response_type = XCB_CLIENT_MESSAGE; + ev->window = window; + ev->type = atoms[I3_SYNC]; + ev->format = 32; + ev->data.data32[0] = window; + ev->data.data32[1] = rnd; + + xcb_send_event(conn, false, xcb_root, XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, (char *)ev); + xcb_flush(conn); + free(reply); + } else 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]; @@ -1057,21 +1088,11 @@ static void handle_resize_request(xcb_resize_request_event_t *event) { } /* - * This function is called immediately before the main loop locks. We flush xcb - * then (and only then) + * This function is called immediately before the main loop locks. We check for + * events from X11, handle them, then flush our outgoing queue. * */ void xcb_prep_cb(struct ev_loop *loop, ev_prepare *watcher, int revents) { - xcb_flush(xcb_connection); -} - -/* - * This function is called immediately after the main loop locks, so when one - * of the watchers registered an event. - * We check whether an X-Event arrived and handle it. - * - */ -void xcb_chk_cb(struct ev_loop *loop, ev_check *watcher, int revents) { xcb_generic_event_t *event; if (xcb_connection_has_error(xcb_connection)) { @@ -1157,6 +1178,7 @@ void xcb_chk_cb(struct ev_loop *loop, ev_check *watcher, int revents) { } break; + case XCB_BUTTON_RELEASE: case XCB_BUTTON_PRESS: /* Button press events are mouse buttons clicked on one of our bars */ handle_button((xcb_button_press_event_t *)event); @@ -1192,6 +1214,8 @@ void xcb_chk_cb(struct ev_loop *loop, ev_check *watcher, int revents) { } free(event); } + + xcb_flush(xcb_connection); } /* @@ -1249,21 +1273,12 @@ char *init_xcb_early() { /* The various watchers to communicate with xcb */ xcb_io = smalloc(sizeof(ev_io)); xcb_prep = smalloc(sizeof(ev_prepare)); - xcb_chk = smalloc(sizeof(ev_check)); ev_io_init(xcb_io, &xcb_io_cb, xcb_get_file_descriptor(xcb_connection), EV_READ); ev_prepare_init(xcb_prep, &xcb_prep_cb); - ev_check_init(xcb_chk, &xcb_chk_cb); - - /* Within an event loop iteration, run the xcb_chk watcher last: other - * watchers might call xcb_flush(), which, unexpectedly, can also read - * events into the queue (see _xcb_conn_wait). Hence, we need to drain xcb’s - * queue last, otherwise we risk dead-locking. */ - ev_set_priority(xcb_chk, EV_MINPRI); ev_io_start(main_loop, xcb_io); ev_prepare_start(main_loop, xcb_prep); - ev_check_start(main_loop, xcb_chk); /* Now we get the atoms and save them in a nice data structure */ get_atoms(); @@ -1499,16 +1514,7 @@ void init_tray_colors(void) { * */ void clean_xcb(void) { - i3_output *o_walk; - free_workspaces(); - SLIST_FOREACH(o_walk, outputs, slist) { - destroy_window(o_walk); - FREE(o_walk->trayclients); - FREE(o_walk->workspaces); - FREE(o_walk->name); - } - FREE_SLIST(outputs, i3_output); - FREE(outputs); + free_outputs(); free_font(); @@ -1517,11 +1523,9 @@ void clean_xcb(void) { xcb_aux_sync(xcb_connection); xcb_disconnect(xcb_connection); - ev_check_stop(main_loop, xcb_chk); ev_prepare_stop(main_loop, xcb_prep); ev_io_stop(main_loop, xcb_io); - FREE(xcb_chk); FREE(xcb_prep); FREE(xcb_io); } @@ -1689,7 +1693,8 @@ void reconfig_windows(bool redraw_bars) { * */ values[3] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT | - XCB_EVENT_MASK_BUTTON_PRESS; + XCB_EVENT_MASK_BUTTON_PRESS | + XCB_EVENT_MASK_BUTTON_RELEASE; if (config.hide_on_modifier == M_DOCK) { /* If the bar is normally visible, catch visibility change events to suspend * the status process when the bar is obscured by full-screened windows. */ diff --git a/include/all.h b/include/all.h index c26835b9..ecc875d0 100644 --- a/include/all.h +++ b/include/all.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include diff --git a/include/commands.h b/include/commands.h index 9780f788..1057f021 100644 --- a/include/commands.h +++ b/include/commands.h @@ -63,10 +63,10 @@ void cmd_move_con_to_workspace_name(I3_CMD, const char *name, const char *no_aut void cmd_move_con_to_workspace_number(I3_CMD, const char *which, const char *no_auto_back_and_forth); /** - * Implementation of 'resize set [px] [px]'. + * Implementation of 'resize set [px | ppt] [px | ppt]'. * */ -void cmd_resize_set(I3_CMD, long cwidth, long cheight); +void cmd_resize_set(I3_CMD, long cwidth, const char *mode_width, long cheight, const char *mode_height); /** * Implementation of 'resize grow|shrink [ px] [or ppt]'. diff --git a/include/con.h b/include/con.h index 674ee61d..58123a87 100644 --- a/include/con.h +++ b/include/con.h @@ -38,6 +38,12 @@ void con_free(Con *con); */ void con_focus(Con *con); +/** + * Sets input focus to the given container and raises it to the top. + * + */ +void con_activate(Con *con); + /** * Closes the given container. * @@ -222,6 +228,22 @@ void con_unmark(Con *con, const char *name); */ Con *con_for_window(Con *con, i3Window *window, Match **store_match); +/** + * Iterate over the container's focus stack and return an array with the + * containers inside it, ordered from higher focus order to lowest. + * + */ +Con **get_focus_order(Con *con); + +/** + * Clear the container's focus stack and re-add it using the provided container + * array. The function doesn't check if the provided array contains the same + * containers with the previous focus stack but will not add floating containers + * in the new focus stack if container is not a workspace. + * + */ +void set_focus_order(Con *con, Con **focus_order); + /** * Returns the number of children of this container. * @@ -314,7 +336,16 @@ void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, * visible workspace on the given output. * */ -void con_move_to_output(Con *con, Output *output); +void con_move_to_output(Con *con, Output *output, bool fix_coordinates); + +/** + * Moves the given container to the currently focused container on the + * visible workspace on the output specified by the given name. + * The current output for the container is used to resolve relative names + * such as left, right, up, down. + * + */ +bool con_move_to_output_name(Con *con, const char *name, bool fix_coordinates); /** * Moves the given container to the given mark. diff --git a/include/config_directives.h b/include/config_directives.h index f35666f3..187b550c 100644 --- a/include/config_directives.h +++ b/include/config_directives.h @@ -49,6 +49,7 @@ CFGFUN(workspace_layout, const char *layout); CFGFUN(workspace_back_and_forth, const char *value); CFGFUN(focus_follows_mouse, const char *value); CFGFUN(mouse_warping, const char *value); +CFGFUN(focus_wrapping, const char *value); CFGFUN(force_focus_wrapping, const char *value); CFGFUN(force_xinerama, const char *value); CFGFUN(disable_randr15, const char *value); @@ -57,7 +58,8 @@ CFGFUN(force_display_urgency_hint, const long duration_ms); CFGFUN(focus_on_window_activation, const char *mode); CFGFUN(show_marks, const char *value); CFGFUN(hide_edge_borders, const char *borders); -CFGFUN(assign, const char *workspace); +CFGFUN(assign_output, const char *output); +CFGFUN(assign, const char *workspace, bool is_number); CFGFUN(no_focus); CFGFUN(ipc_socket, const char *path); CFGFUN(restart_state, const char *path); @@ -65,7 +67,7 @@ CFGFUN(popup_during_fullscreen, const char *value); CFGFUN(color, const char *colorclass, const char *border, const char *background, const char *text, const char *indicator, const char *child_border); CFGFUN(color_single, const char *colorclass, const char *color); CFGFUN(floating_modifier, const char *modifiers); -CFGFUN(new_window, const char *windowtype, const char *border, const long width); +CFGFUN(default_border, const char *windowtype, const char *border, const long width); CFGFUN(workspace, const char *workspace, const char *output); CFGFUN(binding, const char *bindtype, const char *modifiers, const char *key, const char *release, const char *border, const char *whole_window, const char *exclude_titlebar, const char *command); @@ -82,7 +84,7 @@ CFGFUN(bar_verbose, const char *verbose); CFGFUN(bar_modifier, const char *modifier); CFGFUN(bar_wheel_up_cmd, const char *command); CFGFUN(bar_wheel_down_cmd, const char *command); -CFGFUN(bar_bindsym, const char *button, const char *command); +CFGFUN(bar_bindsym, const char *button, const char *release, const char *command); CFGFUN(bar_position, const char *position); CFGFUN(bar_i3bar_command, const char *i3bar_command); CFGFUN(bar_color, const char *colorclass, const char *border, const char *background, const char *text); diff --git a/include/configuration.h b/include/configuration.h index 4f6e5ce8..ac800159 100644 --- a/include/configuration.h +++ b/include/configuration.h @@ -137,15 +137,24 @@ struct Config { * comes with i3. Thus, you can turn it off entirely. */ bool disable_workspace_bar; - /** Think of the following layout: Horizontal workspace with a tabbed - * con on the left of the screen and a terminal on the right of the - * screen. You are in the second container in the tabbed container and - * focus to the right. By default, i3 will set focus to the terminal on - * the right. If you are in the first container in the tabbed container - * however, focusing to the left will wrap. This option forces i3 to - * always wrap, which will result in you having to use "focus parent" - * more often. */ - bool force_focus_wrapping; + /** When focus wrapping is enabled (the default), attempting to + * move focus past the edge of the screen (in other words, in a + * direction in which there are no more containers to focus) will + * cause the focus to wrap to the opposite edge of the current + * container. When it is disabled, nothing happens; the current + * focus is preserved. + * + * Additionally, focus wrapping may be forced. Think of the + * following layout: Horizontal workspace with a tabbed con on the + * left of the screen and a terminal on the right of the + * screen. You are in the second container in the tabbed container + * and focus to the right. By default, i3 will set focus to the + * terminal on the right. If you are in the first container in the + * tabbed container however, focusing to the left will + * wrap. Setting focus_wrapping to FOCUS_WRAPPING_FORCE forces i3 + * to always wrap, which will result in you having to use "focus + * parent" more often. */ + focus_wrapping_t focus_wrapping; /** By default, use the RandR API for multi-monitor setups. * Unfortunately, the nVidia binary graphics driver doesn't support @@ -375,6 +384,9 @@ struct Barbinding { /** The command which is to be executed for this button. */ char *command; + /** If true, the command will be executed after the button is released. */ + bool release; + TAILQ_ENTRY(Barbinding) bindings; }; diff --git a/include/data.h b/include/data.h index 31ef1dc1..69a46e46 100644 --- a/include/data.h +++ b/include/data.h @@ -133,6 +133,15 @@ typedef enum { POINTER_WARPING_NONE = 1 } warping_t; +/** + * Focus wrapping modes. + */ +typedef enum { + FOCUS_WRAPPING_OFF = 0, + FOCUS_WRAPPING_ON = 1, + FOCUS_WRAPPING_FORCE = 2 +} focus_wrapping_t; + /** * Stores a rectangle, for example the size of a window, the child window etc. * It needs to be packed so that the compiler will not add any padding bytes. @@ -556,7 +565,9 @@ struct Assignment { A_ANY = 0, A_COMMAND = (1 << 0), A_TO_WORKSPACE = (1 << 1), - A_NO_FOCUS = (1 << 2) + A_NO_FOCUS = (1 << 2), + A_TO_WORKSPACE_NUMBER = (1 << 3), + A_TO_OUTPUT = (1 << 4) } type; /** the criteria to check if a window matches */ @@ -566,6 +577,7 @@ struct Assignment { union { char *command; char *workspace; + char *output; } dest; TAILQ_ENTRY(Assignment) diff --git a/include/i3.h b/include/i3.h index 4d13d448..93a7e0a3 100644 --- a/include/i3.h +++ b/include/i3.h @@ -74,3 +74,4 @@ extern bool xcursor_supported, xkb_supported; extern xcb_window_t root; extern struct ev_loop *main_loop; extern bool only_check_config; +extern bool force_xinerama; diff --git a/include/i3/ipc.h b/include/i3/ipc.h index 993a2a24..9e0280c9 100644 --- a/include/i3/ipc.h +++ b/include/i3/ipc.h @@ -60,6 +60,9 @@ typedef struct i3_ipc_header { /** Request the raw last loaded i3 config. */ #define I3_IPC_MESSAGE_TYPE_GET_CONFIG 9 +/** Send a tick event to all subscribers. */ +#define I3_IPC_MESSAGE_TYPE_SEND_TICK 10 + /* * Messages from i3 to clients * @@ -74,6 +77,7 @@ typedef struct i3_ipc_header { #define I3_IPC_REPLY_TYPE_VERSION 7 #define I3_IPC_REPLY_TYPE_BINDING_MODES 8 #define I3_IPC_REPLY_TYPE_CONFIG 9 +#define I3_IPC_REPLY_TYPE_TICK 10 /* * Events from i3 to clients. Events have the first bit set high. @@ -101,3 +105,6 @@ typedef struct i3_ipc_header { /** The shutdown event will be triggered when the ipc shuts down */ #define I3_IPC_EVENT_SHUTDOWN (I3_IPC_EVENT_MASK | 6) + +/** The tick event will be sent upon a tick IPC message */ +#define I3_IPC_EVENT_TICK (I3_IPC_EVENT_MASK | 7) diff --git a/include/ipc.h b/include/ipc.h index 7ffbf7a8..c6ad35c7 100644 --- a/include/ipc.h +++ b/include/ipc.h @@ -31,6 +31,10 @@ typedef struct ipc_client { int num_events; char **events; + /* For clients which subscribe to the tick event: whether the first tick + * event has been sent by i3. */ + bool first_tick_sent; + TAILQ_ENTRY(ipc_client) clients; } ipc_client; diff --git a/include/resize.h b/include/resize.h index 7b33de90..38634156 100644 --- a/include/resize.h +++ b/include/resize.h @@ -11,6 +11,6 @@ #include -bool resize_find_tiling_participants(Con **current, Con **other, direction_t direction); +bool resize_find_tiling_participants(Con **current, Con **other, direction_t direction, bool both_sides); int resize_graphical_handler(Con *first, Con *second, orientation_t orientation, const xcb_button_press_event_t *event); diff --git a/include/util.h b/include/util.h index de6fa568..3547d8d7 100644 --- a/include/util.h +++ b/include/util.h @@ -47,16 +47,21 @@ break; \ } -#define FREE(pointer) \ - do { \ - if (pointer != NULL) { \ - free(pointer); \ - pointer = NULL; \ - } \ +#define FREE(pointer) \ + do { \ + free(pointer); \ + pointer = NULL; \ } while (0) #define CALL(obj, member, ...) obj->member(obj, ##__VA_ARGS__) +#define SWAP(first, second, type) \ + do { \ + type tmp_SWAP = first; \ + first = second; \ + second = tmp_SWAP; \ + } while (0) + int min(int a, int b); int max(int a, int b); bool rect_contains(Rect rect, uint32_t x, uint32_t y); diff --git a/libi3/dpi.c b/libi3/dpi.c index 93a3c6f6..a2c40319 100644 --- a/libi3/dpi.c +++ b/libi3/dpi.c @@ -43,12 +43,13 @@ void init_dpi(void) { } char *endptr; - dpi = strtol(resource, &endptr, 10); - if (dpi == LONG_MAX || dpi == LONG_MIN || dpi < 0 || *endptr != '\0' || endptr == resource) { + double in_dpi = strtod(resource, &endptr); + if (in_dpi == HUGE_VAL || dpi < 0 || *endptr != '\0' || endptr == resource) { ELOG("Xft.dpi = %s is an invalid number and couldn't be parsed.\n", resource); dpi = 0; goto init_dpi_end; } + dpi = (long)round(in_dpi); DLOG("Found Xft.dpi = %ld.\n", dpi); diff --git a/libi3/ipc_connect.c b/libi3/ipc_connect.c index 2e628342..f659a1a4 100644 --- a/libi3/ipc_connect.c +++ b/libi3/ipc_connect.c @@ -22,6 +22,25 @@ * */ int ipc_connect(const char *socket_path) { + char *path = NULL; + if (socket_path != NULL) { + path = sstrdup(socket_path); + } + + if (path == NULL) { + if ((path = getenv("I3SOCK")) != NULL) { + path = sstrdup(path); + } + } + + if (path == NULL) { + path = root_atom_contents("I3_SOCKET_PATH", NULL, 0); + } + + if (path == NULL) { + path = sstrdup("/tmp/i3-ipc.sock"); + } + int sockfd = socket(AF_LOCAL, SOCK_STREAM, 0); if (sockfd == -1) err(EXIT_FAILURE, "Could not create socket"); @@ -31,9 +50,9 @@ int ipc_connect(const char *socket_path) { struct sockaddr_un addr; memset(&addr, 0, sizeof(struct sockaddr_un)); addr.sun_family = AF_LOCAL; - strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); if (connect(sockfd, (const struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) - err(EXIT_FAILURE, "Could not connect to i3"); - + err(EXIT_FAILURE, "Could not connect to i3 on socket %s", path); + free(path); return sockfd; } diff --git a/libi3/ipc_recv_message.c b/libi3/ipc_recv_message.c index 16dda90d..84da5aa3 100644 --- a/libi3/ipc_recv_message.c +++ b/libi3/ipc_recv_message.c @@ -13,6 +13,7 @@ #include #include #include +#include #include @@ -41,14 +42,21 @@ int ipc_recv_message(int sockfd, uint32_t *message_type, if (n == -1) return -1; if (n == 0) { - return -2; + if (read_bytes == 0) { + return -2; + } else { + ELOG("IPC: unexpected EOF while reading header, got %" PRIu32 " bytes, want %" PRIu32 " bytes\n", + read_bytes, to_read); + return -3; + } } read_bytes += n; } if (memcmp(walk, I3_IPC_MAGIC, strlen(I3_IPC_MAGIC)) != 0) { - ELOG("IPC: invalid magic in reply\n"); + ELOG("IPC: invalid magic in header, got \"%.*s\", want \"%s\"\n", + (int)strlen(I3_IPC_MAGIC), walk, I3_IPC_MAGIC); return -3; } @@ -61,13 +69,18 @@ int ipc_recv_message(int sockfd, uint32_t *message_type, *reply = smalloc(*reply_length); read_bytes = 0; - int n; while (read_bytes < *reply_length) { - if ((n = read(sockfd, *reply + read_bytes, *reply_length - read_bytes)) == -1) { + const int n = read(sockfd, *reply + read_bytes, *reply_length - read_bytes); + if (n == -1) { if (errno == EINTR || errno == EAGAIN) continue; return -1; } + if (n == 0) { + ELOG("IPC: unexpected EOF while reading payload, got %" PRIu32 " bytes, want %" PRIu32 " bytes\n", + read_bytes, *reply_length); + return -3; + } read_bytes += n; } diff --git a/man/i3-sensible-editor.man b/man/i3-sensible-editor.man index effae6c0..4f16d6c1 100644 --- a/man/i3-sensible-editor.man +++ b/man/i3-sensible-editor.man @@ -30,6 +30,7 @@ It tries to start one of the following (in that order): * jed * gedit * mcedit +* gvim Please don’t complain about the order: If the user has any preference, they will have $VISUAL or $EDITOR set. diff --git a/man/i3-sensible-terminal.man b/man/i3-sensible-terminal.man index 20a6810c..894af912 100644 --- a/man/i3-sensible-terminal.man +++ b/man/i3-sensible-terminal.man @@ -44,6 +44,9 @@ It tries to start one of the following (in that order): * tilix * terminix * konsole +* kitty +* guake +* tilda Please don’t complain about the order: If the user has any preference, they will have $TERMINAL set or modified their i3 configuration file. diff --git a/parser-specs/commands.spec b/parser-specs/commands.spec index a5873328..0289fa1a 100644 --- a/parser-specs/commands.spec +++ b/parser-specs/commands.spec @@ -258,14 +258,16 @@ state RESIZE_SET: -> RESIZE_WIDTH state RESIZE_WIDTH: - 'px' + mode_width = 'px', 'ppt' -> height = number -> RESIZE_HEIGHT state RESIZE_HEIGHT: - 'px', end - -> call cmd_resize_set(&width, &height) + mode_height = 'px', 'ppt' + -> + end + -> call cmd_resize_set(&width, $mode_width, &height, $mode_height) # rename workspace to # rename workspace to diff --git a/parser-specs/config.spec b/parser-specs/config.spec index 4aa320bf..60a1fc67 100644 --- a/parser-specs/config.spec +++ b/parser-specs/config.spec @@ -29,13 +29,15 @@ state INITIAL: 'floating_modifier' -> FLOATING_MODIFIER 'default_orientation' -> DEFAULT_ORIENTATION 'workspace_layout' -> WORKSPACE_LAYOUT - windowtype = 'new_window', 'new_float' -> NEW_WINDOW + windowtype = 'default_border', 'new_window', 'default_floating_border', 'new_float' + -> DEFAULT_BORDER 'hide_edge_borders' -> HIDE_EDGE_BORDERS 'for_window' -> FOR_WINDOW 'assign' -> ASSIGN 'no_focus' -> NO_FOCUS 'focus_follows_mouse' -> FOCUS_FOLLOWS_MOUSE 'mouse_warping' -> MOUSE_WARPING + 'focus_wrapping' -> FOCUS_WRAPPING 'force_focus_wrapping' -> FORCE_FOCUS_WRAPPING 'force_xinerama', 'force-xinerama' -> FORCE_XINERAMA 'disable_randr15', 'disable-randr15' -> DISABLE_RANDR15 @@ -104,25 +106,25 @@ state WORKSPACE_LAYOUT: layout = 'default', 'stacking', 'stacked', 'tabbed' -> call cfg_workspace_layout($layout) -# new_window -# new_float -state NEW_WINDOW: +# +# +state DEFAULT_BORDER: border = 'normal', 'pixel' - -> NEW_WINDOW_PIXELS + -> DEFAULT_BORDER_PIXELS border = '1pixel', 'none' - -> call cfg_new_window($windowtype, $border, -1) + -> call cfg_default_border($windowtype, $border, -1) -state NEW_WINDOW_PIXELS: +state DEFAULT_BORDER_PIXELS: end - -> call cfg_new_window($windowtype, $border, 2) + -> call cfg_default_border($windowtype, $border, 2) width = number - -> NEW_WINDOW_PIXELS_PX + -> DEFAULT_BORDER_PIXELS_PX -state NEW_WINDOW_PIXELS_PX: +state DEFAULT_BORDER_PIXELS_PX: 'px' -> end - -> call cfg_new_window($windowtype, $border, &width) + -> call cfg_default_border($windowtype, $border, &width) # hide_edge_borders # also hide_edge_borders for compatibility @@ -141,7 +143,7 @@ state FOR_WINDOW_COMMAND: command = string -> call cfg_for_window($command) -# assign [→] workspace +# assign [→] [workspace | output] state ASSIGN: '[' -> call cfg_criteria_init(ASSIGN_WORKSPACE); CRITERIA @@ -149,10 +151,22 @@ state ASSIGN: state ASSIGN_WORKSPACE: '→' -> + 'output' + -> ASSIGN_OUTPUT 'workspace' -> + 'number' + -> ASSIGN_WORKSPACE_NUMBER workspace = string - -> call cfg_assign($workspace) + -> call cfg_assign($workspace, 0) + +state ASSIGN_OUTPUT: + output = string + -> call cfg_assign_output($output) + +state ASSIGN_WORKSPACE_NUMBER: + number = string + -> call cfg_assign($number, 1) # no_focus state NO_FOCUS: @@ -197,6 +211,11 @@ state MOUSE_WARPING: value = 'none', 'output' -> call cfg_mouse_warping($value) +# focus_wrapping +state FOCUS_WRAPPING: + value = '1', 'yes', 'true', 'on', 'enable', 'active', '0', 'no', 'false', 'off', 'disable', 'inactive', 'force' + -> call cfg_focus_wrapping($value) + # force_focus_wrapping state FORCE_FOCUS_WRAPPING: value = word @@ -483,12 +502,16 @@ state BAR_WHEEL_DOWN_CMD: -> call cfg_bar_wheel_down_cmd($command); BAR state BAR_BINDSYM: + release = '--release' + -> button = word -> BAR_BINDSYM_COMMAND state BAR_BINDSYM_COMMAND: + release = '--release' + -> command = string - -> call cfg_bar_bindsym($button, $command); BAR + -> call cfg_bar_bindsym($button, $release, $command); BAR state BAR_POSITION: position = 'top', 'bottom' diff --git a/release.sh b/release.sh index 8f537c0e..cfb4beae 100755 --- a/release.sh +++ b/release.sh @@ -1,9 +1,9 @@ #!/bin/zsh # This script is used to prepare a new release of i3. -export RELEASE_VERSION="4.13" -export PREVIOUS_VERSION="4.12" -export RELEASE_BRANCH="next" +export RELEASE_VERSION="4.14.1" +export PREVIOUS_VERSION="4.14" +export RELEASE_BRANCH="master" if [ ! -e "../i3.github.io" ] then @@ -232,9 +232,9 @@ echo "" echo " cd ${TMPDIR}" echo " sendmail -t < email.txt" echo "" -echo "Update milestones on GitHub:" -echo " Set due date of ${RELEASE_VERSION} to $(date +'%Y-$m-%d') and close the milestone" -echo " Create milestone for the next version with unset due date" +echo "Update milestones on GitHub (only for new major versions):" +echo " Set due date of ${RELEASE_VERSION} to $(date +'%Y-%m-%d') and close the milestone" +echo " Create milestone for the next major version with unset due date" echo "" echo "Announce on:" echo " twitter" diff --git a/src/bindings.c b/src/bindings.c index 42a2b79b..c145b956 100644 --- a/src/bindings.c +++ b/src/bindings.c @@ -910,7 +910,6 @@ static int fill_rmlvo_from_root(struct xkb_rule_names *xkb_names) { int remaining = xcb_get_property_value_length(prop_reply); for (int i = 0; i < 5 && remaining > 0; i++) { const int len = strnlen(walk, remaining); - remaining -= len; switch (i) { case 0: sasprintf((char **)&(xkb_names->rules), "%.*s", len, walk); @@ -930,6 +929,7 @@ static int fill_rmlvo_from_root(struct xkb_rule_names *xkb_names) { } DLOG("component %d of _XKB_RULES_NAMES is \"%.*s\"\n", i, len, walk); walk += (len + 1); + remaining -= (len + 1); } free(atom_reply); diff --git a/src/click.c b/src/click.c index 78af8a03..b036c5f8 100644 --- a/src/click.c +++ b/src/click.c @@ -49,7 +49,7 @@ static bool tiling_resize_for_border(Con *con, border_t border, xcb_button_press break; } - bool res = resize_find_tiling_participants(&first, &second, search_direction); + bool res = resize_find_tiling_participants(&first, &second, search_direction, false); if (!res) { LOG("No second container in this direction found.\n"); return false; @@ -241,7 +241,7 @@ static int route_click(Con *con, xcb_button_press_event_t *event, const bool mod * The splitv container will be focused. */ Con *focused = con->parent; focused = TAILQ_FIRST(&(focused->focus_head)); - con_focus(focused); + con_activate(focused); /* To prevent scrolling from going outside the container (see ticket * #557), we first check if scrolling is possible at all. */ bool scroll_prev_possible = (TAILQ_PREV(focused, nodes_head, nodes) != NULL); @@ -256,7 +256,7 @@ static int route_click(Con *con, xcb_button_press_event_t *event, const bool mod } /* 2: focus this con. */ - con_focus(con); + con_activate(con); /* 3: For floating containers, we also want to raise them on click. * We will skip handling events on floating cons in fullscreen mode */ diff --git a/src/commands.c b/src/commands.c index 2697d6e1..899bbb90 100644 --- a/src/commands.c +++ b/src/commands.c @@ -78,14 +78,6 @@ } \ } while (0) -/* - * Returns true if a is definitely greater than b (using the given epsilon) - * - */ -static bool definitelyGreaterThan(float a, float b, float epsilon) { - return (a - b) > ((fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * epsilon); -} - /* * Checks whether we switched to a new workspace and returns false in that case, * signaling that further workspace switching should be done by the calling function @@ -269,14 +261,20 @@ void cmd_criteria_add(I3_CMD, const char *ctype, const char *cvalue) { match_parse_property(current_match, ctype, cvalue); } +static void move_matches_to_workspace(Con *ws) { + owindow *current; + TAILQ_FOREACH(current, &owindows, owindows) { + DLOG("matching: %p / %s\n", current->con, current->con->name); + con_move_to_workspace(current->con, ws, true, false, false); + } +} + /* * Implementation of 'move [window|container] [to] workspace * next|prev|next_on_output|prev_on_output|current'. * */ void cmd_move_con_to_workspace(I3_CMD, const char *which) { - owindow *current; - DLOG("which=%s\n", which); /* We have nothing to move: @@ -309,10 +307,7 @@ void cmd_move_con_to_workspace(I3_CMD, const char *which) { return; } - TAILQ_FOREACH(current, &owindows, owindows) { - DLOG("matching: %p / %s\n", current->con, current->con->name); - con_move_to_workspace(current->con, ws, true, false, false); - } + move_matches_to_workspace(ws); cmd_output->needs_tree_render = true; // XXX: default reply for now, make this a better reply @@ -324,11 +319,7 @@ void cmd_move_con_to_workspace(I3_CMD, const char *which) { * */ void cmd_move_con_to_workspace_back_and_forth(I3_CMD) { - owindow *current; - Con *ws; - - ws = workspace_back_and_forth_get(); - + Con *ws = workspace_back_and_forth_get(); if (ws == NULL) { yerror("No workspace was previously active."); return; @@ -336,10 +327,7 @@ void cmd_move_con_to_workspace_back_and_forth(I3_CMD) { HANDLE_EMPTY_MATCH; - TAILQ_FOREACH(current, &owindows, owindows) { - DLOG("matching: %p / %s\n", current->con, current->con->name); - con_move_to_workspace(current->con, ws, true, false, false); - } + move_matches_to_workspace(ws); cmd_output->needs_tree_render = true; // XXX: default reply for now, make this a better reply @@ -358,7 +346,6 @@ void cmd_move_con_to_workspace_name(I3_CMD, const char *name, const char *_no_au } const bool no_auto_back_and_forth = (_no_auto_back_and_forth != NULL); - owindow *current; /* We have nothing to move: * when criteria was specified but didn't match any window or @@ -382,10 +369,7 @@ void cmd_move_con_to_workspace_name(I3_CMD, const char *name, const char *_no_au HANDLE_EMPTY_MATCH; - TAILQ_FOREACH(current, &owindows, owindows) { - DLOG("matching: %p / %s\n", current->con, current->con->name); - con_move_to_workspace(current->con, ws, true, false, false); - } + move_matches_to_workspace(ws); cmd_output->needs_tree_render = true; // XXX: default reply for now, make this a better reply @@ -398,7 +382,6 @@ void cmd_move_con_to_workspace_name(I3_CMD, const char *name, const char *_no_au */ void cmd_move_con_to_workspace_number(I3_CMD, const char *which, const char *_no_auto_back_and_forth) { const bool no_auto_back_and_forth = (_no_auto_back_and_forth != NULL); - owindow *current; /* We have nothing to move: * when criteria was specified but didn't match any window or @@ -412,7 +395,7 @@ void cmd_move_con_to_workspace_number(I3_CMD, const char *which, const char *_no LOG("should move window to workspace %s\n", which); /* get the workspace */ - Con *output, *workspace = NULL; + Con *output, *ws = NULL; long parsed_num = ws_name_to_number(which); @@ -423,22 +406,19 @@ void cmd_move_con_to_workspace_number(I3_CMD, const char *which, const char *_no } TAILQ_FOREACH(output, &(croot->nodes_head), nodes) - GREP_FIRST(workspace, output_get_content(output), + GREP_FIRST(ws, output_get_content(output), child->num == parsed_num); - if (!workspace) { - workspace = workspace_get(which, NULL); + if (!ws) { + ws = workspace_get(which, NULL); } if (!no_auto_back_and_forth) - workspace = maybe_auto_back_and_forth_workspace(workspace); + ws = maybe_auto_back_and_forth_workspace(ws); HANDLE_EMPTY_MATCH; - TAILQ_FOREACH(current, &owindows, owindows) { - DLOG("matching: %p / %s\n", current->con, current->con->name); - con_move_to_workspace(current->con, workspace, true, false, false); - } + move_matches_to_workspace(ws); cmd_output->needs_tree_render = true; // XXX: default reply for now, make this a better reply @@ -511,7 +491,7 @@ static bool cmd_resize_tiling_direction(I3_CMD, Con *current, const char *way, c else search_direction = D_DOWN; - bool res = resize_find_tiling_participants(&first, &second, search_direction); + bool res = resize_find_tiling_participants(&first, &second, search_direction, false); if (!res) { LOG("No second container in this direction found.\n"); ysuccess(false); @@ -525,8 +505,8 @@ static bool cmd_resize_tiling_direction(I3_CMD, Con *current, const char *way, c LOG("default percentage = %f\n", percentage); /* resize */ - LOG("second->percent = %f\n", second->percent); LOG("first->percent before = %f\n", first->percent); + LOG("second->percent before = %f\n", second->percent); if (first->percent == 0.0) first->percent = percentage; if (second->percent == 0.0) @@ -535,12 +515,10 @@ static bool cmd_resize_tiling_direction(I3_CMD, Con *current, const char *way, c double new_second_percent = second->percent - ((double)ppt / 100.0); LOG("new_first_percent = %f\n", new_first_percent); LOG("new_second_percent = %f\n", new_second_percent); - /* Ensure that the new percentages are positive and greater than - * 0.05 to have a reasonable minimum size. */ - if (definitelyGreaterThan(new_first_percent, 0.05, DBL_EPSILON) && - definitelyGreaterThan(new_second_percent, 0.05, DBL_EPSILON)) { - first->percent += ((double)ppt / 100.0); - second->percent -= ((double)ppt / 100.0); + /* Ensure that the new percentages are positive. */ + if (new_first_percent > 0.0 && new_second_percent > 0.0) { + first->percent = new_first_percent; + second->percent = new_second_percent; LOG("first->percent after = %f\n", first->percent); LOG("second->percent after = %f\n", second->percent); } else { @@ -552,19 +530,15 @@ static bool cmd_resize_tiling_direction(I3_CMD, Con *current, const char *way, c static bool cmd_resize_tiling_width_height(I3_CMD, Con *current, const char *way, const char *direction, int ppt) { LOG("width/height resize\n"); + /* get the appropriate current container (skip stacked/tabbed cons) */ - while (current->parent->layout == L_STACKED || - current->parent->layout == L_TABBED) - current = current->parent; - - /* Then further go up until we find one with the matching orientation. */ - orientation_t search_orientation = - (strcmp(direction, "width") == 0 ? HORIZ : VERT); - - while (current->type != CT_WORKSPACE && - current->type != CT_FLOATING_CON && - (con_orientation(current->parent) != search_orientation || con_num_children(current->parent) == 1)) - current = current->parent; + Con *dummy = NULL; + direction_t search_direction = (strcmp(direction, "width") == 0 ? D_LEFT : D_DOWN); + bool search_result = resize_find_tiling_participants(¤t, &dummy, search_direction, true); + if (search_result == false) { + ysuccess(false); + return false; + } /* get the default percentage */ int children = con_num_children(current->parent); @@ -572,24 +546,6 @@ static bool cmd_resize_tiling_width_height(I3_CMD, Con *current, const char *way double percentage = 1.0 / children; LOG("default percentage = %f\n", percentage); - orientation_t orientation = con_orientation(current->parent); - - if ((orientation == HORIZ && - strcmp(direction, "height") == 0) || - (orientation == VERT && - strcmp(direction, "width") == 0)) { - LOG("You cannot resize in that direction. Your focus is in a %s split container currently.\n", - (orientation == HORIZ ? "horizontal" : "vertical")); - ysuccess(false); - return false; - } - - if (children == 1) { - LOG("This is the only container, cannot resize.\n"); - ysuccess(false); - return false; - } - /* Ensure all the other children have a percentage set. */ Con *child; TAILQ_FOREACH(child, &(current->parent->nodes_head), nodes) { @@ -602,24 +558,23 @@ static bool cmd_resize_tiling_width_height(I3_CMD, Con *current, const char *way double subtract_percent = ((double)ppt / 100.0) / (children - 1); LOG("new_current_percent = %f\n", new_current_percent); LOG("subtract_percent = %f\n", subtract_percent); - /* Ensure that the new percentages are positive and greater than - * 0.05 to have a reasonable minimum size. */ + /* Ensure that the new percentages are positive. */ TAILQ_FOREACH(child, &(current->parent->nodes_head), nodes) { if (child == current) continue; - if (!definitelyGreaterThan(child->percent - subtract_percent, 0.05, DBL_EPSILON)) { + if (child->percent - subtract_percent <= 0.0) { LOG("Not resizing, already at minimum size (child %p would end up with a size of %.f\n", child, child->percent - subtract_percent); ysuccess(false); return false; } } - if (!definitelyGreaterThan(new_current_percent, 0.05, DBL_EPSILON)) { + if (new_current_percent <= 0.0) { LOG("Not resizing, already at minimum size\n"); ysuccess(false); return false; } - current->percent += ((double)ppt / 100.0); + current->percent = new_current_percent; LOG("current->percent after = %f\n", current->percent); TAILQ_FOREACH(child, &(current->parent->nodes_head), nodes) { @@ -676,31 +631,97 @@ void cmd_resize(I3_CMD, const char *way, const char *direction, long resize_px, } /* - * Implementation of 'resize set [px] [px]'. + * Implementation of 'resize set [px | ppt] [px | ppt]'. * */ -void cmd_resize_set(I3_CMD, long cwidth, long cheight) { - DLOG("resizing to %ldx%ld px\n", cwidth, cheight); - if (cwidth <= 0 || cheight <= 0) { - ELOG("Resize failed: dimensions cannot be negative (was %ldx%ld)\n", cwidth, cheight); +void cmd_resize_set(I3_CMD, long cwidth, const char *mode_width, long cheight, const char *mode_height) { + DLOG("resizing to %ld %s x %ld %s\n", cwidth, mode_width, cheight, mode_height); + if (cwidth < 0 || cheight < 0) { + ELOG("Resize failed: dimensions cannot be negative (was %ld %s x %ld %s)\n", cwidth, mode_width, cheight, mode_height); return; } HANDLE_EMPTY_MATCH; owindow *current; + bool success = true; TAILQ_FOREACH(current, &owindows, owindows) { Con *floating_con; if ((floating_con = con_inside_floating(current->con))) { + Con *output = con_get_output(floating_con); + if (cwidth == 0) { + cwidth = output->rect.width; + } else if (mode_width && strcmp(mode_width, "ppt") == 0) { + cwidth = output->rect.width * ((double)cwidth / 100.0); + } + if (cheight == 0) { + cheight = output->rect.height; + } else if (mode_height && strcmp(mode_height, "ppt") == 0) { + cheight = output->rect.height * ((double)cheight / 100.0); + } floating_resize(floating_con, cwidth, cheight); } else { - ELOG("Resize failed: %p not a floating container\n", current->con); + if (current->con->window && current->con->window->dock) { + DLOG("This is a dock window. Not resizing (con = %p)\n)", current->con); + continue; + } + + if (cwidth > 0 && mode_width && strcmp(mode_width, "ppt") == 0) { + /* get the appropriate current container (skip stacked/tabbed cons) */ + Con *target = current->con; + Con *dummy; + resize_find_tiling_participants(&target, &dummy, D_LEFT, true); + + /* Calculate new size for the target container */ + double current_percent = target->percent; + char *action_string; + long adjustment; + + if (current_percent > cwidth) { + action_string = "shrink"; + adjustment = (int)(current_percent * 100) - cwidth; + } else { + action_string = "grow"; + adjustment = cwidth - (int)(current_percent * 100); + } + + /* perform resizing and report failure if not possible */ + if (!cmd_resize_tiling_width_height(current_match, cmd_output, + target, action_string, "width", adjustment)) { + success = false; + } + } + + if (cheight > 0 && mode_width && strcmp(mode_width, "ppt") == 0) { + /* get the appropriate current container (skip stacked/tabbed cons) */ + Con *target = current->con; + Con *dummy; + resize_find_tiling_participants(&target, &dummy, D_DOWN, true); + + /* Calculate new size for the target container */ + double current_percent = target->percent; + char *action_string; + long adjustment; + + if (current_percent > cheight) { + action_string = "shrink"; + adjustment = (int)(current_percent * 100) - cheight; + } else { + action_string = "grow"; + adjustment = cheight - (int)(current_percent * 100); + } + + /* perform resizing and report failure if not possible */ + if (!cmd_resize_tiling_width_height(current_match, cmd_output, + target, action_string, "height", adjustment)) { + success = false; + } + } } } cmd_output->needs_tree_render = true; - // XXX: default reply for now, make this a better reply - ysuccess(true); + ysuccess(success); } /* @@ -760,6 +781,7 @@ void cmd_nop(I3_CMD, const char *comment) { LOG("-------------------------------------------------\n"); LOG(" NOP: %s\n", comment); LOG("-------------------------------------------------\n"); + ysuccess(true); } /* @@ -1044,25 +1066,7 @@ void cmd_move_con_to_output(I3_CMD, const char *name) { TAILQ_FOREACH(current, &owindows, owindows) { DLOG("matching: %p / %s\n", current->con, current->con->name); - Output *current_output = get_output_for_con(current->con); - assert(current_output != NULL); - - Output *output = get_output_from_string(current_output, name); - if (output == NULL) { - ELOG("Could not find output \"%s\", skipping.\n", name); - had_error = true; - continue; - } - - Con *ws = NULL; - GREP_FIRST(ws, output_get_content(output->con), workspace_is_visible(child)); - if (ws == NULL) { - ELOG("Could not find a visible workspace on output %p.\n", output); - had_error = true; - continue; - } - - con_move_to_workspace(current->con, ws, true, false, false); + had_error |= !con_move_to_output_name(current->con, name, true); } cmd_output->needs_tree_render = true; @@ -1132,6 +1136,10 @@ void cmd_move_workspace_to_output(I3_CMD, const char *name) { owindow *current; TAILQ_FOREACH(current, &owindows, owindows) { Con *ws = con_get_workspace(current->con); + if (con_is_internal(ws)) { + continue; + } + bool success = workspace_move_to_output(ws, name); if (!success) { ELOG("Failed to move workspace to output.\n"); @@ -1257,6 +1265,20 @@ void cmd_focus_direction(I3_CMD, const char *direction) { ysuccess(true); } +/* + * Focus a container and disable any other fullscreen container not permitting the focus. + * + */ +static void cmd_focus_force_focus(Con *con) { + /* Disable fullscreen container in workspace with container to be focused. */ + Con *ws = con_get_workspace(con); + Con *fullscreen_on_ws = (focused && focused->fullscreen_mode == CF_GLOBAL) ? focused : con_get_fullscreen_con(ws, CF_OUTPUT); + if (fullscreen_on_ws && fullscreen_on_ws != con && !con_has_parent(con, fullscreen_on_ws)) { + con_disable_fullscreen(fullscreen_on_ws); + } + con_activate(con); +} + /* * Implementation of 'focus tiling|floating|mode_toggle'. * @@ -1264,28 +1286,34 @@ void cmd_focus_direction(I3_CMD, const char *direction) { void cmd_focus_window_mode(I3_CMD, const char *window_mode) { DLOG("window_mode = %s\n", window_mode); - Con *ws = con_get_workspace(focused); - if (ws != NULL) { - if (strcmp(window_mode, "mode_toggle") == 0) { - if (con_inside_floating(focused)) - window_mode = "tiling"; - else - window_mode = "floating"; - } - Con *current; - TAILQ_FOREACH(current, &(ws->focus_head), focused) { - if ((strcmp(window_mode, "floating") == 0 && current->type != CT_FLOATING_CON) || - (strcmp(window_mode, "tiling") == 0 && current->type == CT_FLOATING_CON)) - continue; - - con_focus(con_descend_focused(current)); - break; - } + bool to_floating = false; + if (strcmp(window_mode, "mode_toggle") == 0) { + to_floating = !con_inside_floating(focused); + } else if (strcmp(window_mode, "floating") == 0) { + to_floating = true; + } else if (strcmp(window_mode, "tiling") == 0) { + to_floating = false; } - cmd_output->needs_tree_render = true; - // XXX: default reply for now, make this a better reply - ysuccess(true); + Con *ws = con_get_workspace(focused); + Con *current; + bool success = false; + TAILQ_FOREACH(current, &(ws->focus_head), focused) { + if ((to_floating && current->type != CT_FLOATING_CON) || + (!to_floating && current->type == CT_FLOATING_CON)) + continue; + + cmd_focus_force_focus(con_descend_focused(current)); + success = true; + break; + } + + if (success) { + cmd_output->needs_tree_render = true; + ysuccess(true); + } else { + yerror("Failed to find a %s container in workspace.", to_floating ? "floating" : "tiling"); + } } /* @@ -1342,13 +1370,6 @@ void cmd_focus(I3_CMD) { if (!ws) continue; - /* Check the fullscreen focus constraints. */ - if (!con_fullscreen_permits_focusing(current->con)) { - LOG("Cannot change focus while in fullscreen mode (fullscreen rules).\n"); - ysuccess(false); - return; - } - /* In case this is a scratchpad window, call scratchpad_show(). */ if (ws == __i3_scratch) { scratchpad_show(current->con); @@ -1372,13 +1393,13 @@ void cmd_focus(I3_CMD) { * So we focus 'current' to make it the currently focused window of * the target workspace, then revert focus. */ Con *currently_focused = focused; - con_focus(current->con); - con_focus(currently_focused); + cmd_focus_force_focus(current->con); + con_activate(currently_focused); /* Now switch to the workspace, then focus */ workspace_show(ws); LOG("focusing %p / %s\n", current->con, current->con->name); - con_focus(current->con); + con_activate(current->con); count++; } @@ -1490,7 +1511,7 @@ void cmd_move_direction(I3_CMD, const char *direction, long move_px) { /* the move command should not disturb focus */ if (focused != initially_focused) - con_focus(initially_focused); + con_activate(initially_focused); // XXX: default reply for now, make this a better reply ysuccess(true); @@ -1615,7 +1636,7 @@ void cmd_open(I3_CMD) { LOG("opening new container\n"); Con *con = tree_open_con(NULL, NULL); con->layout = L_SPLITH; - con_focus(con); + con_activate(con); y(map_open); ystr("success"); @@ -2004,7 +2025,7 @@ void cmd_rename_workspace(I3_CMD, const char *old_name, const char *new_name) { } /* Restore the previous focus since con_attach messes with the focus. */ - con_focus(previously_focused); + con_activate(previously_focused); cmd_output->needs_tree_render = true; ysuccess(true); diff --git a/src/con.c b/src/con.c index fcde4c10..4d0c43e3 100644 --- a/src/con.c +++ b/src/con.c @@ -252,6 +252,27 @@ void con_focus(Con *con) { } } +/* + * Raise container to the top if it is floating or inside some floating + * container. + * + */ +static void con_raise(Con *con) { + Con *floating = con_inside_floating(con); + if (floating) { + floating_raise_con(floating); + } +} + +/* + * Sets input focus to the given container and raises it to the top. + * + */ +void con_activate(Con *con) { + con_focus(con); + con_raise(con); +} + /* * Closes the given container. * @@ -795,6 +816,62 @@ Con *con_for_window(Con *con, i3Window *window, Match **store_match) { return NULL; } +static int num_focus_heads(Con *con) { + int focus_heads = 0; + + Con *current; + TAILQ_FOREACH(current, &(con->focus_head), focused) { + focus_heads++; + } + + return focus_heads; +} + +/* + * Iterate over the container's focus stack and return an array with the + * containers inside it, ordered from higher focus order to lowest. + * + */ +Con **get_focus_order(Con *con) { + const int focus_heads = num_focus_heads(con); + Con **focus_order = smalloc(focus_heads * sizeof(Con *)); + Con *current; + int idx = 0; + TAILQ_FOREACH(current, &(con->focus_head), focused) { + assert(idx < focus_heads); + focus_order[idx++] = current; + } + + return focus_order; +} + +/* + * Clear the container's focus stack and re-add it using the provided container + * array. The function doesn't check if the provided array contains the same + * containers with the previous focus stack but will not add floating containers + * in the new focus stack if container is not a workspace. + * + */ +void set_focus_order(Con *con, Con **focus_order) { + int focus_heads = 0; + while (!TAILQ_EMPTY(&(con->focus_head))) { + Con *current = TAILQ_FIRST(&(con->focus_head)); + + TAILQ_REMOVE(&(con->focus_head), current, focused); + focus_heads++; + } + + for (int idx = 0; idx < focus_heads; idx++) { + /* Useful when encapsulating a workspace. */ + if (con->type != CT_WORKSPACE && con_inside_floating(focus_order[idx])) { + focus_heads++; + continue; + } + + TAILQ_INSERT_TAIL(&(con->focus_head), focus_order[idx], focused); + } +} + /* * Returns the number of children of this container. * @@ -994,9 +1071,9 @@ void con_enable_fullscreen(Con *con, fullscreen_mode_t fullscreen_mode) { Con *old_focused = focused; if (fullscreen_mode == CF_GLOBAL && cur_ws != con_ws) workspace_show(con_ws); - con_focus(con); + con_activate(con); if (fullscreen_mode != CF_GLOBAL && cur_ws != con_ws) - con_focus(old_focused); + con_activate(old_focused); con_set_fullscreen_mode(con, fullscreen_mode); } @@ -1148,11 +1225,11 @@ static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fi * new workspace is hidden and it's necessary to immediately switch * back to the originally-focused workspace. */ Con *old_focus = TAILQ_FIRST(&(output_get_content(dest_output)->focus_head)); - con_focus(con_descend_focused(con)); + con_activate(con_descend_focused(con)); /* Restore focus if the output's focused workspace has changed. */ if (con_get_workspace(focused) != old_focus) - con_focus(old_focus); + con_activate(old_focus); } /* 7: when moving to another workspace, we leave the focus on the current @@ -1172,7 +1249,7 @@ static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fi /* Set focus only if con was on current workspace before moving. * Otherwise we would give focus to some window on different workspace. */ if (!ignore_focus && source_ws == current_ws) - con_focus(con_descend_focused(focus_next)); + con_activate(con_descend_focused(focus_next)); /* 8. If anything within the container is associated with a startup sequence, * delete it so child windows won't be created on the old workspace. */ @@ -1300,12 +1377,33 @@ void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool * visible workspace on the given output. * */ -void con_move_to_output(Con *con, Output *output) { +void con_move_to_output(Con *con, Output *output, bool fix_coordinates) { Con *ws = NULL; GREP_FIRST(ws, output_get_content(output->con), workspace_is_visible(child)); assert(ws != NULL); DLOG("Moving con %p to output %s\n", con, output_primary_name(output)); - con_move_to_workspace(con, ws, false, false, false); + con_move_to_workspace(con, ws, fix_coordinates, false, false); +} + +/* + * Moves the given container to the currently focused container on the + * visible workspace on the output specified by the given name. + * The current output for the container is used to resolve relative names + * such as left, right, up, down. + * + */ +bool con_move_to_output_name(Con *con, const char *name, bool fix_coordinates) { + Output *current_output = get_output_for_con(con); + assert(current_output != NULL); + + Output *output = get_output_from_string(current_output, name); + if (output == NULL) { + ELOG("Could not find output \"%s\"\n", name); + return false; + } + + con_move_to_output(con, output, fix_coordinates); + return true; } /* @@ -1735,7 +1833,7 @@ void con_set_layout(Con *con, layout_t layout) { con->workspace_layout = ws_layout; DLOG("Setting layout to %d\n", layout); con->layout = layout; - } else if (layout == L_STACKED || layout == L_TABBED) { + } else if (layout == L_STACKED || layout == L_TABBED || layout == L_SPLITV || layout == L_SPLITH) { DLOG("Creating new split container\n"); /* 1: create a new split container */ Con *new = con_new(NULL, NULL); @@ -1746,17 +1844,9 @@ void con_set_layout(Con *con, layout_t layout) { new->layout = layout; new->last_split_layout = con->last_split_layout; - /* Save the container that was focused before we move containers - * around, but only if the container is visible (otherwise focus - * will be restored properly automatically when switching). */ - Con *old_focused = TAILQ_FIRST(&(con->focus_head)); - if (old_focused == TAILQ_END(&(con->focus_head))) - old_focused = NULL; - if (old_focused != NULL && - !workspace_is_visible(con_get_workspace(old_focused))) - old_focused = NULL; - /* 3: move the existing cons of this workspace below the new con */ + Con **focus_order = get_focus_order(con); + DLOG("Moving cons\n"); Con *child; while (!TAILQ_EMPTY(&(con->nodes_head))) { @@ -1765,13 +1855,13 @@ void con_set_layout(Con *con, layout_t layout) { con_attach(child, new, true); } + set_focus_order(new, focus_order); + free(focus_order); + /* 4: attach the new split container to the workspace */ DLOG("Attaching new split to ws\n"); con_attach(new, con, false); - if (old_focused) - con_focus(old_focused); - tree_flatten(croot); } con_force_split_parents_redraw(con); @@ -1827,6 +1917,10 @@ void con_toggle_layout(Con *con, const char *toggle_mode) { * change to the opposite split layout. */ if (parent->layout != L_SPLITH && parent->layout != L_SPLITV) { layout = parent->last_split_layout; + /* In case last_split_layout was not initialized… */ + if (layout == L_DEFAULT) { + layout = L_SPLITH; + } } else { layout = (parent->layout == L_SPLITH) ? L_SPLITV : L_SPLITH; } @@ -2045,14 +2139,7 @@ bool con_fullscreen_permits_focusing(Con *con) { /* Allow it only if the container to be focused is contained within the * current fullscreen container. */ - do { - if (con->parent == fs) - return true; - con = con->parent; - } while (con); - - /* Focusing con would hide it behind a fullscreen window, disallow it. */ - return false; + return con_has_parent(con, fs); } /* @@ -2294,15 +2381,14 @@ bool con_swap(Con *first, Con *second) { Con *current_ws = con_get_workspace(old_focus); const bool focused_within_first = (first == old_focus || con_has_parent(old_focus, first)); const bool focused_within_second = (second == old_focus || con_has_parent(old_focus, second)); + fullscreen_mode_t first_fullscreen_mode = first->fullscreen_mode; + fullscreen_mode_t second_fullscreen_mode = second->fullscreen_mode; - if (!con_fullscreen_permits_focusing(first_ws)) { - DLOG("Cannot swap because target workspace \"%s\" is obscured.\n", first_ws->name); - return false; + if (first_fullscreen_mode != CF_NONE) { + con_disable_fullscreen(first); } - - if (!con_fullscreen_permits_focusing(second_ws)) { - DLOG("Cannot swap because target workspace \"%s\" is obscured.\n", second_ws->name); - return false; + if (second_fullscreen_mode != CF_NONE) { + con_disable_fullscreen(second); } double first_percent = first->percent; @@ -2341,7 +2427,7 @@ bool con_swap(Con *first, Con *second) { * We don't need to check this for the second container because we've only * moved the first one at this point.*/ if (first_ws != second_ws && focused_within_first) { - con_focus(con_descend_focused(current_ws)); + con_activate(con_descend_focused(current_ws)); } /* Move second to where first has been originally. */ @@ -2384,15 +2470,15 @@ bool con_swap(Con *first, Con *second) { */ if (focused_within_first) { if (first_ws == second_ws) { - con_focus(old_focus); + con_activate(old_focus); } else { - con_focus(con_descend_focused(second)); + con_activate(con_descend_focused(second)); } } else if (focused_within_second) { if (first_ws == second_ws) { - con_focus(old_focus); + con_activate(old_focus); } else { - con_focus(con_descend_focused(first)); + con_activate(con_descend_focused(first)); } } @@ -2403,7 +2489,17 @@ bool con_swap(Con *first, Con *second) { second->percent = first_percent; fake->percent = 0.0; + SWAP(first_fullscreen_mode, second_fullscreen_mode, fullscreen_mode_t); + swap_end: + /* The two windows exchange their original fullscreen status */ + if (first_fullscreen_mode != CF_NONE) { + con_enable_fullscreen(first, first_fullscreen_mode); + } + if (second_fullscreen_mode != CF_NONE) { + con_enable_fullscreen(second, second_fullscreen_mode); + } + /* We don't actually need this since percentages-wise we haven't changed * anything, but we'll better be safe than sorry and just make sure as we'd * otherwise crash i3. */ diff --git a/src/config.c b/src/config.c index 7e08b520..24c7b541 100644 --- a/src/config.c +++ b/src/config.c @@ -227,6 +227,8 @@ void load_configuration(xcb_connection_t *conn, const char *override_configpath, if (config.workspace_urgency_timer == 0) config.workspace_urgency_timer = 0.5; + config.focus_wrapping = FOCUS_WRAPPING_ON; + parse_configuration(override_configpath, true); if (reload) { diff --git a/src/config_directives.c b/src/config_directives.c index 7ca6e102..ad6d65b5 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -197,7 +197,7 @@ CFGFUN(workspace_layout, const char *layout) { config.default_layout = L_TABBED; } -CFGFUN(new_window, const char *windowtype, const char *border, const long width) { +CFGFUN(default_border, const char *windowtype, const char *border, const long width) { int border_style; int border_width; @@ -215,7 +215,8 @@ CFGFUN(new_window, const char *windowtype, const char *border, const long width) border_width = width; } - if (strcmp(windowtype, "new_window") == 0) { + if ((strcmp(windowtype, "default_border") == 0) || + (strcmp(windowtype, "new_window") == 0)) { DLOG("default tiled border style = %d and border width = %d (%d physical px)\n", border_style, border_width, logical_px(border_width)); config.default_border = border_style; @@ -264,8 +265,27 @@ CFGFUN(disable_randr15, const char *value) { config.disable_randr15 = eval_boolstr(value); } +CFGFUN(focus_wrapping, const char *value) { + if (strcmp(value, "force") == 0) { + config.focus_wrapping = FOCUS_WRAPPING_FORCE; + } else if (eval_boolstr(value)) { + config.focus_wrapping = FOCUS_WRAPPING_ON; + } else { + config.focus_wrapping = FOCUS_WRAPPING_OFF; + } +} + CFGFUN(force_focus_wrapping, const char *value) { - config.force_focus_wrapping = eval_boolstr(value); + /* Legacy syntax. */ + if (eval_boolstr(value)) { + config.focus_wrapping = FOCUS_WRAPPING_FORCE; + } else { + /* For "force_focus_wrapping off", don't enable or disable + * focus wrapping, just ensure it's not forced. */ + if (config.focus_wrapping == FOCUS_WRAPPING_FORCE) { + config.focus_wrapping = FOCUS_WRAPPING_ON; + } + } } CFGFUN(workspace_back_and_forth, const char *value) { @@ -377,15 +397,35 @@ CFGFUN(color, const char *colorclass, const char *border, const char *background #undef APPLY_COLORS } -CFGFUN(assign, const char *workspace) { +CFGFUN(assign_output, const char *output) { if (match_is_empty(current_match)) { ELOG("Match is empty, ignoring this assignment\n"); return; } + + DLOG("New assignment, using above criteria, to output \"%s\".\n", output); + Assignment *assignment = scalloc(1, sizeof(Assignment)); + match_copy(&(assignment->match), current_match); + assignment->type = A_TO_OUTPUT; + assignment->dest.output = sstrdup(output); + TAILQ_INSERT_TAIL(&assignments, assignment, assignments); +} + +CFGFUN(assign, const char *workspace, bool is_number) { + if (match_is_empty(current_match)) { + ELOG("Match is empty, ignoring this assignment\n"); + return; + } + + if (is_number && ws_name_to_number(workspace) == -1) { + ELOG("Could not parse initial part of \"%s\" as a number.\n", workspace); + return; + } + DLOG("New assignment, using above criteria, to workspace \"%s\".\n", workspace); Assignment *assignment = scalloc(1, sizeof(Assignment)); match_copy(&(assignment->match), current_match); - assignment->type = A_TO_WORKSPACE; + assignment->type = is_number ? A_TO_WORKSPACE_NUMBER : A_TO_WORKSPACE; assignment->dest.workspace = sstrdup(workspace); TAILQ_INSERT_TAIL(&assignments, assignment, assignments); } @@ -463,7 +503,7 @@ CFGFUN(bar_modifier, const char *modifier) { current_bar->modifier = M_NONE; } -static void bar_configure_binding(const char *button, const char *command) { +static void bar_configure_binding(const char *button, const char *release, const char *command) { if (strncasecmp(button, "button", strlen("button")) != 0) { ELOG("Bindings for a bar can only be mouse bindings, not \"%s\", ignoring.\n", button); return; @@ -474,16 +514,18 @@ static void bar_configure_binding(const char *button, const char *command) { ELOG("Button \"%s\" does not seem to be in format 'buttonX'.\n", button); return; } + const bool release_bool = release != NULL; struct Barbinding *current; TAILQ_FOREACH(current, &(current_bar->bar_bindings), bindings) { - if (current->input_code == input_code) { + if (current->input_code == input_code && current->release == release_bool) { ELOG("command for button %s was already specified, ignoring.\n", button); return; } } struct Barbinding *new_binding = scalloc(1, sizeof(struct Barbinding)); + new_binding->release = release_bool; new_binding->input_code = input_code; new_binding->command = sstrdup(command); TAILQ_INSERT_TAIL(&(current_bar->bar_bindings), new_binding, bindings); @@ -491,16 +533,16 @@ static void bar_configure_binding(const char *button, const char *command) { CFGFUN(bar_wheel_up_cmd, const char *command) { ELOG("'wheel_up_cmd' is deprecated. Please us 'bindsym button4 %s' instead.\n", command); - bar_configure_binding("button4", command); + bar_configure_binding("button4", NULL, command); } CFGFUN(bar_wheel_down_cmd, const char *command) { ELOG("'wheel_down_cmd' is deprecated. Please us 'bindsym button5 %s' instead.\n", command); - bar_configure_binding("button5", command); + bar_configure_binding("button5", NULL, command); } -CFGFUN(bar_bindsym, const char *button, const char *command) { - bar_configure_binding(button, command); +CFGFUN(bar_bindsym, const char *button, const char *release, const char *command) { + bar_configure_binding(button, release, command); } CFGFUN(bar_position, const char *position) { diff --git a/src/config_parser.c b/src/config_parser.c index 58a5552c..2d3f3bb9 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -743,7 +743,7 @@ static char *migrate_config(char *input, off_t size) { /* read the script’s output */ int conv_size = 65535; - char *converted = smalloc(conv_size); + char *converted = scalloc(conv_size, 1); int read_bytes = 0, ret; do { if (read_bytes == conv_size) { @@ -764,6 +764,7 @@ static char *migrate_config(char *input, off_t size) { wait(&status); if (!WIFEXITED(status)) { fprintf(stderr, "Child did not terminate normally, using old config file (will lead to broken behaviour)\n"); + FREE(converted); return NULL; } @@ -778,6 +779,7 @@ static char *migrate_config(char *input, off_t size) { fprintf(stderr, "# i3 config file (v4)\n"); /* TODO: nag the user with a message to include a hint for i3 in their config file */ } + FREE(converted); return NULL; } @@ -900,7 +902,9 @@ bool parse_file(const char *f, bool use_nagbar) { FREE(current_config); current_config = scalloc(stbuf.st_size + 1, 1); - fread(current_config, 1, stbuf.st_size, fstr); + if ((ssize_t)fread(current_config, 1, stbuf.st_size, fstr) != stbuf.st_size) { + die("Could not fread: %s\n", strerror(errno)); + } rewind(fstr); bool invalid_sets = false; @@ -1061,7 +1065,7 @@ bool parse_file(const char *f, bool use_nagbar) { int version = detect_version(buf); if (version == 3) { /* We need to convert this v3 configuration */ - char *converted = migrate_config(new, stbuf.st_size); + char *converted = migrate_config(new, strlen(new)); if (converted != NULL) { ELOG("\n"); ELOG("****************************************************************\n"); diff --git a/src/display_version.c b/src/display_version.c index 764ee753..2e05cafa 100644 --- a/src/display_version.c +++ b/src/display_version.c @@ -55,10 +55,6 @@ static yajl_callbacks version_callbacks = { * */ void display_running_version(void) { - char *socket_path = root_atom_contents("I3_SOCKET_PATH", conn, conn_screen); - if (socket_path == NULL) - exit(EXIT_SUCCESS); - char *pid_from_atom = root_atom_contents("I3_PID", conn, conn_screen); if (pid_from_atom == NULL) { /* If I3_PID is not set, the running version is older than 4.2-200. */ @@ -71,18 +67,7 @@ void display_running_version(void) { printf("(Getting version from running i3, press ctrl-c to abort…)"); fflush(stdout); - /* TODO: refactor this with the code for sending commands */ - int sockfd = socket(AF_LOCAL, SOCK_STREAM, 0); - if (sockfd == -1) - err(EXIT_FAILURE, "Could not create socket"); - - struct sockaddr_un addr; - memset(&addr, 0, sizeof(struct sockaddr_un)); - addr.sun_family = AF_LOCAL; - strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); - if (connect(sockfd, (const struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) - err(EXIT_FAILURE, "Could not connect to i3"); - + int sockfd = ipc_connect(NULL); if (ipc_send_message(sockfd, 0, I3_IPC_MESSAGE_TYPE_GET_VERSION, (uint8_t *)"") == -1) err(EXIT_FAILURE, "IPC: write()"); @@ -184,5 +169,4 @@ void display_running_version(void) { yajl_free(handle); free(reply); free(pid_from_atom); - free(socket_path); } diff --git a/src/floating.c b/src/floating.c index 5f46dcf9..e958153d 100644 --- a/src/floating.c +++ b/src/floating.c @@ -318,7 +318,7 @@ void floating_enable(Con *con, bool automatic) { render_con(con, false); if (set_focus) - con_focus(con); + con_activate(con); /* Check if we need to re-assign it to a different workspace because of its * coordinates and exit if that was done successfully. */ @@ -382,7 +382,7 @@ void floating_disable(Con *con, bool automatic) { con_fix_percent(con->parent); if (set_focus) - con_focus(con); + con_activate(con); floating_set_hint_atom(con, false); ipc_send_window_event("floating", con); @@ -449,7 +449,8 @@ bool floating_maybe_reassign_ws(Con *con) { Con *ws = TAILQ_FIRST(&(content->focus_head)); DLOG("Moving con %p / %s to workspace %p / %s\n", con, con->name, ws, ws->name); con_move_to_workspace(con, ws, false, true, false); - con_focus(con_descend_focused(con)); + workspace_show(ws); + con_activate(con_descend_focused(con)); return true; } @@ -667,7 +668,7 @@ void floating_resize_window(Con *con, const bool proportional, /* Custom data structure used to track dragging-related events. */ struct drag_x11_cb { - ev_check check; + ev_prepare prepare; /* Whether this modal event loop should be exited and with which result. */ drag_result_t result; @@ -686,7 +687,7 @@ struct drag_x11_cb { const void *extra; }; -static void xcb_drag_check_cb(EV_P_ ev_check *w, int revents) { +static void xcb_drag_prepare_cb(EV_P_ ev_prepare *w, int revents) { struct drag_x11_cb *dragloop = (struct drag_x11_cb *)w->data; xcb_motion_notify_event_t *last_motion_notify = NULL; xcb_generic_event_t *event; @@ -746,8 +747,10 @@ static void xcb_drag_check_cb(EV_P_ ev_check *w, int revents) { if (last_motion_notify != (xcb_motion_notify_event_t *)event) free(event); - if (dragloop->result != DRAGGING) + if (dragloop->result != DRAGGING) { + free(last_motion_notify); return; + } } if (last_motion_notify == NULL) @@ -765,6 +768,8 @@ static void xcb_drag_check_cb(EV_P_ ev_check *w, int revents) { dragloop->extra); } free(last_motion_notify); + + xcb_flush(conn); } /* @@ -831,18 +836,18 @@ drag_result_t drag_pointer(Con *con, const xcb_button_press_event_t *event, xcb_ .callback = callback, .extra = extra, }; - ev_check *check = &loop.check; + ev_prepare *prepare = &loop.prepare; if (con) loop.old_rect = con->rect; - ev_check_init(check, xcb_drag_check_cb); - check->data = &loop; + ev_prepare_init(prepare, xcb_drag_prepare_cb); + prepare->data = &loop; main_set_x11_cb(false); - ev_check_start(main_loop, check); + ev_prepare_start(main_loop, prepare); while (loop.result == DRAGGING) ev_run(main_loop, EVRUN_ONCE); - ev_check_stop(main_loop, check); + ev_prepare_stop(main_loop, prepare); main_set_x11_cb(true); xcb_ungrab_keyboard(conn, XCB_CURRENT_TIME); diff --git a/src/handlers.c b/src/handlers.c index 3140e405..e1671c3b 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -433,7 +433,7 @@ static void handle_configure_request(xcb_configure_request_event_t *event) { if (config.focus_on_window_activation == FOWA_FOCUS || (config.focus_on_window_activation == FOWA_SMART && workspace_is_visible(ws))) { DLOG("Focusing con = %p\n", con); workspace_show(ws); - con_focus(con); + con_activate(con); tree_render(); } else if (config.focus_on_window_activation == FOWA_URGENT || (config.focus_on_window_activation == FOWA_SMART && !workspace_is_visible(ws))) { DLOG("Marking con = %p urgent\n", con); @@ -774,7 +774,9 @@ static void handle_client_message(xcb_client_message_event_t *event) { scratchpad_show(con); } else { workspace_show(ws); - con_focus(con); + /* Re-set focus, even if unchanged from i3’s perspective. */ + focused_id = XCB_NONE; + con_activate(con); } } else { /* Request is from an application. */ @@ -786,7 +788,7 @@ static void handle_client_message(xcb_client_message_event_t *event) { if (config.focus_on_window_activation == FOWA_FOCUS || (config.focus_on_window_activation == FOWA_SMART && workspace_is_visible(ws))) { DLOG("Focusing con = %p\n", con); workspace_show(ws); - con_focus(con); + con_activate(con); } else if (config.focus_on_window_activation == FOWA_URGENT || (config.focus_on_window_activation == FOWA_SMART && !workspace_is_visible(ws))) { DLOG("Marking con = %p urgent\n", con); con_set_urgency(con, true); @@ -1243,7 +1245,7 @@ static void handle_focus_in(xcb_focus_in_event_t *event) { if (ws != con_get_workspace(focused)) workspace_show(ws); - con_focus(con); + con_activate(con); /* We update focused_id because we don’t need to set focus again */ focused_id = event->event; tree_render(); @@ -1262,6 +1264,9 @@ static void handle_configure_notify(xcb_configure_notify_event_t *event) { } DLOG("ConfigureNotify for root window 0x%08x\n", event->event); + if (force_xinerama) { + return; + } randr_query_outputs(); } diff --git a/src/ipc.c b/src/ipc.c index 759665fe..a1a72b1a 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -572,6 +572,8 @@ static void dump_bar_bindings(yajl_gen gen, Barconfig *config) { y(integer, current->input_code); ystr("command"); ystr(current->command); + ystr("release"); + y(bool, current->release == B_UPON_KEYRELEASE); y(map_close); } @@ -1046,8 +1048,9 @@ static int add_subscription(void *extra, const unsigned char *s, memcpy(client->events[event], s, len); DLOG("client is now subscribed to:\n"); - for (int i = 0; i < client->num_events; i++) + for (int i = 0; i < client->num_events; i++) { DLOG("event %s\n", client->events[i]); + } DLOG("(done)\n"); return 1; @@ -1099,6 +1102,25 @@ IPC_HANDLER(subscribe) { yajl_free(p); const char *reply = "{\"success\":true}"; ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_SUBSCRIBE, (const uint8_t *)reply); + + if (client->first_tick_sent) { + return; + } + + bool is_tick = false; + for (int i = 0; i < client->num_events; i++) { + if (strcmp(client->events[i], "tick") == 0) { + is_tick = true; + break; + } + } + if (!is_tick) { + return; + } + + client->first_tick_sent = true; + const char *payload = "{\"first\":true,\"payload\":\"\"}"; + ipc_send_message(client->fd, strlen(payload), I3_IPC_EVENT_TICK, (const uint8_t *)payload); } /* @@ -1122,9 +1144,35 @@ IPC_HANDLER(get_config) { y(free); } +/* + * Sends the tick event from the message payload to subscribers. Establishes a + * synchronization point in event-related tests. + */ +IPC_HANDLER(send_tick) { + yajl_gen gen = ygenalloc(); + + y(map_open); + + ystr("payload"); + yajl_gen_string(gen, (unsigned char *)message, message_size); + + y(map_close); + + const unsigned char *payload; + ylength length; + y(get_buf, &payload, &length); + + ipc_send_event("tick", I3_IPC_EVENT_TICK, (const char *)payload); + y(free); + + const char *reply = "{\"success\":true}"; + ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_TICK, (const uint8_t *)reply); + DLOG("Sent tick event\n"); +} + /* The index of each callback function corresponds to the numeric * value of the message type (see include/i3/ipc.h) */ -handler_t handlers[10] = { +handler_t handlers[11] = { handle_run_command, handle_get_workspaces, handle_subscribe, @@ -1135,6 +1183,7 @@ handler_t handlers[10] = { handle_get_version, handle_get_binding_modes, handle_get_config, + handle_send_tick, }; /* diff --git a/src/load_layout.c b/src/load_layout.c index 071b3ccd..aa7ac03c 100644 --- a/src/load_layout.c +++ b/src/load_layout.c @@ -654,6 +654,6 @@ void tree_append_json(Con *con, const char *buf, const size_t len, char **errorm yajl_free(hand); if (to_focus) { - con_focus(to_focus); + con_activate(to_focus); } } diff --git a/src/main.c b/src/main.c index 0d1457fd..194ef05c 100644 --- a/src/main.c +++ b/src/main.c @@ -35,9 +35,9 @@ struct rlimit original_rlimit_core; /** The number of file descriptors passed via socket activation. */ int listen_fds; -/* We keep the xcb_check watcher around to be able to enable and disable it +/* We keep the xcb_prepare watcher around to be able to enable and disable it * temporarily for drag_pointer(). */ -static struct ev_check *xcb_check; +static struct ev_prepare *xcb_prepare; extern Con *focused; @@ -92,29 +92,26 @@ struct ws_assignments_head ws_assignments = TAILQ_HEAD_INITIALIZER(ws_assignment bool xcursor_supported = true; bool xkb_supported = true; +bool force_xinerama = false; + /* - * This callback is only a dummy, see xcb_prepare_cb and xcb_check_cb. + * This callback is only a dummy, see xcb_prepare_cb. * See also man libev(3): "ev_prepare" and "ev_check" - customise your event loop * */ static void xcb_got_event(EV_P_ struct ev_io *w, int revents) { - /* empty, because xcb_prepare_cb and xcb_check_cb are used */ + /* empty, because xcb_prepare_cb are used */ } /* - * Flush before blocking (and waiting for new events) + * Called just before the event loop sleeps. Ensures xcb’s incoming and outgoing + * queues are empty so that any activity will trigger another event loop + * iteration, and hence another xcb_prepare_cb invocation. * */ static void xcb_prepare_cb(EV_P_ ev_prepare *w, int revents) { - xcb_flush(conn); -} - -/* - * Instead of polling the X connection socket we leave this to - * xcb_poll_for_event() which knows better than we can ever know. - * - */ -static void xcb_check_cb(EV_P_ ev_check *w, int revents) { + /* Process all queued (and possibly new) events before the event loop + sleeps. */ xcb_generic_event_t *event; while ((event = xcb_poll_for_event(conn)) != NULL) { @@ -137,6 +134,9 @@ static void xcb_check_cb(EV_P_ ev_check *w, int revents) { free(event); } + + /* Flush all queued events to X11. */ + xcb_flush(conn); } /* @@ -148,12 +148,12 @@ static void xcb_check_cb(EV_P_ ev_check *w, int revents) { void main_set_x11_cb(bool enable) { DLOG("Setting main X11 callback to enabled=%d\n", enable); if (enable) { - ev_check_start(main_loop, xcb_check); + ev_prepare_start(main_loop, xcb_prepare); /* Trigger the watcher explicitly to handle all remaining X11 events. * drag_pointer()’s event handler exits in the middle of the loop. */ - ev_feed_event(main_loop, xcb_check, 0); + ev_feed_event(main_loop, xcb_prepare, 0); } else { - ev_check_stop(main_loop, xcb_check); + ev_prepare_stop(main_loop, xcb_prepare); } } @@ -174,19 +174,62 @@ static void i3_exit(void) { fflush(stderr); shm_unlink(shmlogname); } + ipc_shutdown(SHUTDOWN_REASON_EXIT); + unlink(config.ipc_socket_path); +} + +/* + * (One-shot) Handler for all signals with default action "Core", see signal(7) + * + * Unlinks the SHM log and re-raises the signal. + * + */ +static void handle_core_signal(int sig, siginfo_t *info, void *data) { + if (*shmlogname != '\0') { + shm_unlink(shmlogname); + } + raise(sig); } /* * (One-shot) Handler for all signals with default action "Term", see signal(7) * - * Unlinks the SHM log and re-raises the signal. + * Exits the program gracefully. * */ -static void handle_signal(int sig, siginfo_t *info, void *data) { - if (*shmlogname != '\0') { - shm_unlink(shmlogname); +static void handle_term_signal(struct ev_loop *loop, ev_signal *signal, int revents) { + /* We exit gracefully here in the sense that cleanup handlers + * installed via atexit are invoked. */ + exit(128 + signal->signum); +} + +/* + * Set up handlers for all signals with default action "Term", see signal(7) + * + */ +static void setup_term_handlers(void) { + static struct ev_signal signal_watchers[6]; + size_t num_watchers = sizeof(signal_watchers) / sizeof(signal_watchers[0]); + + /* We have to rely on libev functionality here and should not use + * sigaction handlers because we need to invoke the exit handlers + * and cannot do so from an asynchronous signal handling context as + * not all code triggered during exit is signal safe (and exiting + * the main loop from said handler is not easily possible). libev's + * signal handlers does not impose such a constraint on us. */ + ev_signal_init(&signal_watchers[0], handle_term_signal, SIGHUP); + ev_signal_init(&signal_watchers[1], handle_term_signal, SIGINT); + ev_signal_init(&signal_watchers[2], handle_term_signal, SIGALRM); + ev_signal_init(&signal_watchers[3], handle_term_signal, SIGTERM); + ev_signal_init(&signal_watchers[4], handle_term_signal, SIGUSR1); + ev_signal_init(&signal_watchers[5], handle_term_signal, SIGUSR1); + for (size_t i = 0; i < num_watchers; i++) { + ev_signal_start(main_loop, &signal_watchers[i]); + /* The signal handlers should not block ev_run from returning + * and so none of the signal handlers should hold a reference to + * the main loop. */ + ev_unref(main_loop); } - raise(sig); } int main(int argc, char *argv[]) { @@ -197,7 +240,6 @@ int main(int argc, char *argv[]) { bool autostart = true; char *layout_path = NULL; bool delete_layout_path = false; - bool force_xinerama = false; bool disable_randr15 = false; char *fake_outputs = NULL; bool disable_signalhandler = false; @@ -550,6 +592,10 @@ int main(int argc, char *argv[]) { config.ipc_socket_path = sstrdup(config.ipc_socket_path); } + if (config.force_xinerama) { + force_xinerama = true; + } + xcb_void_cookie_t cookie; cookie = xcb_change_window_attributes_checked(conn, root, XCB_CW_EVENT_MASK, (uint32_t[]){ROOT_EVENT_MASK}); xcb_generic_error_t *error = xcb_request_check(conn, cookie); @@ -668,7 +714,7 @@ int main(int argc, char *argv[]) { fake_outputs_init(fake_outputs); FREE(fake_outputs); config.fake_outputs = NULL; - } else if (force_xinerama || config.force_xinerama) { + } else 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 */ @@ -720,7 +766,7 @@ int main(int argc, char *argv[]) { output = get_first_output(); } - con_focus(con_descend_focused(output_get_content(output->con))); + con_activate(con_descend_focused(output_get_content(output->con))); free(pointerreply); } @@ -776,15 +822,11 @@ int main(int argc, char *argv[]) { ewmh_update_desktop_viewport(); struct ev_io *xcb_watcher = scalloc(1, sizeof(struct ev_io)); - xcb_check = scalloc(1, sizeof(struct ev_check)); - struct ev_prepare *xcb_prepare = scalloc(1, sizeof(struct ev_prepare)); + xcb_prepare = scalloc(1, sizeof(struct ev_prepare)); ev_io_init(xcb_watcher, xcb_got_event, xcb_get_file_descriptor(conn), EV_READ); ev_io_start(main_loop, xcb_watcher); - ev_check_init(xcb_check, xcb_check_cb); - ev_check_start(main_loop, xcb_check); - ev_prepare_init(xcb_prepare, xcb_prepare_cb); ev_prepare_start(main_loop, xcb_prepare); @@ -854,15 +896,15 @@ int main(int argc, char *argv[]) { err(EXIT_FAILURE, "pledge"); #endif - struct sigaction action; - - action.sa_sigaction = handle_signal; - action.sa_flags = SA_NODEFER | SA_RESETHAND | SA_SIGINFO; - sigemptyset(&action.sa_mask); - if (!disable_signalhandler) setup_signal_handler(); else { + struct sigaction action; + + action.sa_sigaction = handle_core_signal; + action.sa_flags = SA_NODEFER | SA_RESETHAND | SA_SIGINFO; + sigemptyset(&action.sa_mask); + /* Catch all signals with default action "Core", see signal(7) */ if (sigaction(SIGQUIT, &action, NULL) == -1 || sigaction(SIGILL, &action, NULL) == -1 || @@ -872,14 +914,7 @@ int main(int argc, char *argv[]) { ELOG("Could not setup signal handler.\n"); } - /* Catch all signals with default action "Term", see signal(7) */ - if (sigaction(SIGHUP, &action, NULL) == -1 || - sigaction(SIGINT, &action, NULL) == -1 || - sigaction(SIGALRM, &action, NULL) == -1 || - sigaction(SIGUSR1, &action, NULL) == -1 || - sigaction(SIGUSR2, &action, NULL) == -1) - ELOG("Could not setup signal handler.\n"); - + setup_term_handlers(); /* Ignore SIGPIPE to survive errors when an IPC client disconnects * while we are sending them a message */ signal(SIGPIPE, SIG_IGN); @@ -922,7 +957,7 @@ int main(int argc, char *argv[]) { free(command); } - /* Make sure to destroy the event loop to invoke the cleeanup callbacks + /* Make sure to destroy the event loop to invoke the cleanup callbacks * when calling exit() */ atexit(i3_exit); diff --git a/src/manage.c b/src/manage.c index 004e8038..8b306052 100644 --- a/src/manage.c +++ b/src/manage.c @@ -259,9 +259,26 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki Con *wm_desktop_ws = NULL; /* If not, check if it is assigned to a specific workspace */ - if ((assignment = assignment_for(cwindow, A_TO_WORKSPACE))) { + if ((assignment = assignment_for(cwindow, A_TO_WORKSPACE)) || + (assignment = assignment_for(cwindow, A_TO_WORKSPACE_NUMBER))) { DLOG("Assignment matches (%p)\n", match); - Con *assigned_ws = workspace_get(assignment->dest.workspace, NULL); + + Con *assigned_ws = NULL; + if (assignment->type == A_TO_WORKSPACE_NUMBER) { + Con *output = NULL; + long parsed_num = ws_name_to_number(assignment->dest.workspace); + + /* This will only work for workspaces that already exist. */ + TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { + GREP_FIRST(assigned_ws, output_get_content(output), child->num == parsed_num); + } + } + /* A_TO_WORKSPACE type assignment or fallback from A_TO_WORKSPACE_NUMBER + * when the target workspace number does not exist yet. */ + if (!assigned_ws) { + assigned_ws = workspace_get(assignment->dest.workspace, NULL); + } + nc = con_descend_tiling_focused(assigned_ws); DLOG("focused on ws %s: %p / %s\n", assigned_ws->name, nc, nc->name); if (nc->type == CT_WORKSPACE) @@ -305,6 +322,10 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki } else nc = tree_open_con(NULL, cwindow); } + + if ((assignment = assignment_for(cwindow, A_TO_OUTPUT))) { + con_move_to_output_name(nc, assignment->dest.output, true); + } } else { /* M_BELOW inserts the new window as a child of the one which was * matched (e.g. dock areas) */ @@ -367,7 +388,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki * needed e.g. for LibreOffice Impress multi-monitor * presentations to work out of the box. */ if (output != NULL) - con_move_to_output(nc, output); + con_move_to_output(nc, output, false); con_toggle_fullscreen(nc, CF_OUTPUT); } fs = NULL; @@ -625,7 +646,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki * proper window event sequence. */ if (set_focus && nc->mapped) { DLOG("Now setting focus.\n"); - con_focus(nc); + con_activate(nc); } tree_render(); diff --git a/src/move.c b/src/move.c index 3ecc69e4..97ca6d40 100644 --- a/src/move.c +++ b/src/move.c @@ -118,7 +118,7 @@ static void move_to_output_directed(Con *con, direction_t direction) { attach_to_workspace(con, ws, direction); /* fix the focus stack */ - con_focus(con); + con_activate(con); /* force re-painting the indicators */ FREE(con->deco_render_params); diff --git a/src/output.c b/src/output.c index e7690384..c76dfd03 100644 --- a/src/output.c +++ b/src/output.c @@ -99,7 +99,8 @@ void output_push_sticky_windows(Con *to_focus) { continue; if (con_is_sticky(current)) { - con_move_to_workspace(current, visible_ws, true, false, current != to_focus->parent); + bool ignore_focus = (to_focus == NULL) || (current != to_focus->parent); + con_move_to_workspace(current, visible_ws, true, false, ignore_focus); } } } diff --git a/src/randr.c b/src/randr.c index 1d333014..85add08f 100644 --- a/src/randr.c +++ b/src/randr.c @@ -496,7 +496,7 @@ void init_ws_for_output(Output *output, Con *content) { Con *ws = create_workspace_on_output(output, content); /* TODO: Set focus in main.c */ - con_focus(ws); + con_activate(ws); } /* @@ -924,7 +924,7 @@ void randr_query_outputs(void) { continue; DLOG("Focusing primary output %s\n", output_primary_name(output)); - con_focus(con_descend_focused(output->con)); + con_activate(con_descend_focused(output->con)); } /* render_layout flushes */ @@ -987,7 +987,7 @@ void randr_disable_output(Output *output) { if (next) { DLOG("now focusing next = %p\n", next); - con_focus(next); + con_activate(next); workspace_show(con_get_workspace(next)); } diff --git a/src/resize.c b/src/resize.c index f07fcec6..ee50bfbc 100644 --- a/src/resize.c +++ b/src/resize.c @@ -47,7 +47,7 @@ DRAGGING_CB(resize_callback) { xcb_flush(conn); } -bool resize_find_tiling_participants(Con **current, Con **other, direction_t direction) { +bool resize_find_tiling_participants(Con **current, Con **other, direction_t direction, bool both_sides) { DLOG("Find two participants for resizing container=%p in direction=%i\n", other, direction); Con *first = *current; Con *second = NULL; @@ -74,8 +74,14 @@ bool resize_find_tiling_participants(Con **current, Con **other, direction_t dir /* get the counterpart for this resizement */ if (dir_backwards) { second = TAILQ_PREV(first, nodes_head, nodes); + if (second == NULL && both_sides == true) { + second = TAILQ_NEXT(first, nodes); + } } else { second = TAILQ_NEXT(first, nodes); + if (second == NULL && both_sides == true) { + second = TAILQ_PREV(first, nodes_head, nodes); + } } if (second == NULL) { diff --git a/src/restore_layout.c b/src/restore_layout.c index bf16c864..b99a50c1 100644 --- a/src/restore_layout.c +++ b/src/restore_layout.c @@ -39,7 +39,6 @@ static TAILQ_HEAD(state_head, placeholder_state) state_head = static xcb_connection_t *restore_conn; static struct ev_io *xcb_watcher; -static struct ev_check *xcb_check; static struct ev_prepare *xcb_prepare; static void restore_handle_event(int type, xcb_generic_event_t *event); @@ -49,10 +48,6 @@ static void restore_xcb_got_event(EV_P_ struct ev_io *w, int revents) { } static void restore_xcb_prepare_cb(EV_P_ ev_prepare *w, int revents) { - xcb_flush(restore_conn); -} - -static void restore_xcb_check_cb(EV_P_ ev_check *w, int revents) { xcb_generic_event_t *event; if (xcb_connection_has_error(restore_conn)) { @@ -77,6 +72,8 @@ static void restore_xcb_check_cb(EV_P_ ev_check *w, int revents) { free(event); } + + xcb_flush(restore_conn); } /* @@ -91,7 +88,6 @@ void restore_connect(void) { /* This is not the initial connect, but a reconnect, most likely * because our X11 connection was killed (e.g. by a user with xkill. */ ev_io_stop(main_loop, xcb_watcher); - ev_check_stop(main_loop, xcb_check); ev_prepare_stop(main_loop, xcb_prepare); placeholder_state *state; @@ -107,7 +103,6 @@ void restore_connect(void) { */ xcb_disconnect(restore_conn); free(xcb_watcher); - free(xcb_check); free(xcb_prepare); } @@ -124,15 +119,11 @@ void restore_connect(void) { } xcb_watcher = scalloc(1, sizeof(struct ev_io)); - xcb_check = scalloc(1, sizeof(struct ev_check)); xcb_prepare = scalloc(1, sizeof(struct ev_prepare)); ev_io_init(xcb_watcher, restore_xcb_got_event, xcb_get_file_descriptor(restore_conn), EV_READ); ev_io_start(main_loop, xcb_watcher); - ev_check_init(xcb_check, restore_xcb_check_cb); - ev_check_start(main_loop, xcb_check); - ev_prepare_init(xcb_prepare, restore_xcb_prepare_cb); ev_prepare_start(main_loop, xcb_prepare); } diff --git a/src/scratchpad.c b/src/scratchpad.c index 9018ad3f..95154014 100644 --- a/src/scratchpad.c +++ b/src/scratchpad.c @@ -123,7 +123,7 @@ void scratchpad_show(Con *con) { /* use con_descend_tiling_focused to get the last focused * window inside this scratch container in order to * keep the focus the same within this container */ - con_focus(con_descend_tiling_focused(walk_con)); + con_activate(con_descend_tiling_focused(walk_con)); return; } } @@ -205,7 +205,7 @@ void scratchpad_show(Con *con) { workspace_show(active); } - con_focus(con_descend_focused(con)); + con_activate(con_descend_focused(con)); } /* diff --git a/src/tree.c b/src/tree.c index d5f457dd..6c6a614e 100644 --- a/src/tree.c +++ b/src/tree.c @@ -330,6 +330,13 @@ bool tree_close_internal(Con *con, kill_window_t kill_window, bool dont_kill_par DLOG("parent container killed\n"); } + if (ws == con) { + DLOG("Closing a workspace container, updating EWMH atoms\n"); + ewmh_update_number_of_desktops(); + ewmh_update_desktop_names(); + ewmh_update_wm_desktop(); + } + con_free(con); /* in the case of floating windows, we already focused another container @@ -344,12 +351,12 @@ bool tree_close_internal(Con *con, kill_window_t kill_window, bool dont_kill_par DLOG("focusing %p / %s\n", next, next->name); if (next->type == CT_DOCKAREA) { /* Instead of focusing the dockarea, we need to restore focus to the workspace */ - con_focus(con_descend_focused(output_get_content(next->parent))); + con_activate(con_descend_focused(output_get_content(next->parent))); } else { if (!force_set_focus && con != focused) DLOG("not changing focus, the container was not focused before\n"); else - con_focus(next); + con_activate(next); } } else { DLOG("not focusing because we're not killing anybody\n"); @@ -433,7 +440,7 @@ bool level_up(void) { /* Skip over floating containers and go directly to the grandparent * (which should always be a workspace) */ if (focused->parent->type == CT_FLOATING_CON) { - con_focus(focused->parent->parent); + con_activate(focused->parent->parent); return true; } @@ -444,7 +451,7 @@ bool level_up(void) { ELOG("'focus parent': Focus is already on the workspace, cannot go higher than that.\n"); return false; } - con_focus(focused->parent); + con_activate(focused->parent); return true; } @@ -469,7 +476,7 @@ bool level_down(void) { next = TAILQ_FIRST(&(next->focus_head)); } - con_focus(next); + con_activate(next); return true; } @@ -560,26 +567,14 @@ static bool _tree_next(Con *con, char way, orientation_t orientation, bool wrap) if (!workspace) return false; - workspace_show(workspace); - - /* If a workspace has an active fullscreen container, one of its - * children should always be focused. The above workspace_show() - * should be adequate for that, so return. */ - if (con_get_fullscreen_con(workspace, CF_OUTPUT)) - return true; - - Con *focus = con_descend_direction(workspace, direction); - - /* special case: if there was no tiling con to focus and the workspace - * has a floating con in the focus stack, focus the top of the focus - * stack (which may be floating) */ - if (focus == workspace) + Con *focus = con_descend_tiling_focused(workspace); + if (focus == workspace) { focus = con_descend_focused(workspace); - - if (focus) { - con_focus(focus); - x_set_warp_to(&(focus->rect)); } + + workspace_show(workspace); + con_activate(focus); + x_set_warp_to(&(focus->rect)); return true; } @@ -616,7 +611,7 @@ static bool _tree_next(Con *con, char way, orientation_t orientation, bool wrap) TAILQ_INSERT_HEAD(&(parent->floating_head), last, floating_windows); } - con_focus(con_descend_focused(next)); + con_activate(con_descend_focused(next)); return true; } @@ -641,7 +636,7 @@ static bool _tree_next(Con *con, char way, orientation_t orientation, bool wrap) next = TAILQ_PREV(current, nodes_head, nodes); if (!next) { - if (!config.force_focus_wrapping) { + if (config.focus_wrapping != FOCUS_WRAPPING_FORCE) { /* If there is no next/previous container, we check if we can focus one * when going higher (without wrapping, though). If so, we are done, if * not, we wrap */ @@ -665,7 +660,7 @@ static bool _tree_next(Con *con, char way, orientation_t orientation, bool wrap) /* 3: focus choice comes in here. at the moment we will go down * until we find a window */ /* TODO: check for window, atm we only go down as far as possible */ - con_focus(con_descend_focused(next)); + con_activate(con_descend_focused(next)); return true; } @@ -675,7 +670,8 @@ static bool _tree_next(Con *con, char way, orientation_t orientation, bool wrap) * */ void tree_next(char way, orientation_t orientation) { - _tree_next(focused, way, orientation, true); + _tree_next(focused, way, orientation, + config.focus_wrapping != FOCUS_WRAPPING_OFF); } /* diff --git a/src/util.c b/src/util.c index ba0969c7..dc3444f7 100644 --- a/src/util.c +++ b/src/util.c @@ -500,7 +500,7 @@ ssize_t slurp(const char *path, char **buf) { size_t n = fread(*buf, 1, stbuf.st_size, f); fclose(f); if ((ssize_t)n != stbuf.st_size) { - ELOG("File \"%s\" could not be read entirely: got %zd, want %zd\n", path, n, stbuf.st_size); + ELOG("File \"%s\" could not be read entirely: got %zd, want %" PRIi64 "\n", path, n, (int64_t)stbuf.st_size); free(*buf); *buf = NULL; return -1; diff --git a/src/workspace.c b/src/workspace.c index 4b350b82..8c46a949 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -459,6 +459,11 @@ static void _workspace_show(Con *workspace) { y(free); + /* Avoid calling output_push_sticky_windows later with a freed container. */ + if (old == old_focus) { + old_focus = NULL; + } + ewmh_update_number_of_desktops(); ewmh_update_desktop_names(); ewmh_update_desktop_viewport(); @@ -810,9 +815,9 @@ void ws_force_orientation(Con *ws, orientation_t orientation) { /* 2: copy layout from workspace */ split->layout = ws->layout; - Con *old_focused = TAILQ_FIRST(&(ws->focus_head)); - /* 3: move the existing cons of this workspace below the new con */ + Con **focus_order = get_focus_order(ws); + DLOG("Moving cons\n"); while (!TAILQ_EMPTY(&(ws->nodes_head))) { Con *child = TAILQ_FIRST(&(ws->nodes_head)); @@ -820,6 +825,9 @@ void ws_force_orientation(Con *ws, orientation_t orientation) { con_attach(child, split, true); } + set_focus_order(split, focus_order); + free(focus_order); + /* 4: switch workspace layout */ ws->layout = (orientation == HORIZ) ? L_SPLITH : L_SPLITV; DLOG("split->layout = %d, ws->layout = %d\n", split->layout, ws->layout); @@ -830,9 +838,6 @@ void ws_force_orientation(Con *ws, orientation_t orientation) { /* 6: fix the percentages */ con_fix_percent(ws); - - if (old_focused) - con_focus(old_focused); } /* @@ -887,9 +892,10 @@ Con *workspace_encapsulate(Con *ws) { new->parent = ws; new->layout = ws->layout; + Con **focus_order = get_focus_order(ws); + DLOG("Moving children of workspace %p / %s into container %p\n", ws, ws->name, new); - Con *child; while (!TAILQ_EMPTY(&(ws->nodes_head))) { child = TAILQ_FIRST(&(ws->nodes_head)); @@ -897,6 +903,9 @@ Con *workspace_encapsulate(Con *ws) { con_attach(child, new, true); } + set_focus_order(new, focus_order); + free(focus_order); + con_attach(new, ws, true); return new; diff --git a/src/x.c b/src/x.c index 09a60493..7829079b 100644 --- a/src/x.c +++ b/src/x.c @@ -1227,9 +1227,13 @@ void x_set_name(Con *con, const char *name) { * */ void update_shmlog_atom() { - xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, - A_I3_SHMLOG_PATH, A_UTF8_STRING, 8, - strlen(shmlogname), shmlogname); + if (*shmlogname == '\0') { + xcb_delete_property(conn, root, A_I3_SHMLOG_PATH); + } else { + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, + A_I3_SHMLOG_PATH, A_UTF8_STRING, 8, + strlen(shmlogname), shmlogname); + } } /* diff --git a/testcases/complete-run.pl.in b/testcases/complete-run.pl.in index ddd6ccad..96b93bed 100755 --- a/testcases/complete-run.pl.in +++ b/testcases/complete-run.pl.in @@ -38,6 +38,8 @@ binmode STDERR, ':utf8'; # subshell or situations like that. AnyEvent::Util::close_all_fds_except(0, 1, 2); +our @CLEANUP; + # convenience wrapper to write to the log file my $log; sub Log { say $log "@_" } @@ -55,6 +57,7 @@ my %options = ( xtrace => 0, coverage => 0, restart => 0, + xvfb => 1, ); my $keep_xserver_output = 0; @@ -64,6 +67,7 @@ my $result = GetOptions( "valgrind" => \$options{valgrind}, "strace" => \$options{strace}, "xtrace" => \$options{xtrace}, + "xvfb!" => \$options{xvfb}, "display=s" => \@displays, "parallel=i" => \$parallel, "help|?" => \$help, @@ -112,6 +116,44 @@ $ENV{PATH} = join(':', qx(Xephyr -help 2>&1); die "Xephyr was not found in your path. Please install Xephyr (xserver-xephyr on Debian)." if $?; +qx(xvfb-run --help 2>&1); +if ($? && $options{xvfb}) { + say "xvfb-run not found, not running tests under xvfb. Install the xvfb package to speed up tests"; + $options{xvfb} = 0; +} + +if ($options{xvfb}) { + for (my $n = 99; $n < 120; $n++) { + my $path = File::Temp::tmpnam($ENV{TMPDIR} // "/tmp", "i3-testsXXXXXX"); + if (!defined(POSIX::mkfifo($path, 0600))) { + die "mkfifo: $!"; + } + my $pid = fork // die "fork: $!"; + if ($pid == 0) { + # Child + + # Xvfb checks whether the parent ignores USR1 and sends USR1 to the + # parent when ready, so that the wait call will be interrupted. We + # can’t implement this in Perl, as Perl’s waitpid transparently + # handles -EINTR. + exec('/bin/sh', '-c', qq|trap "exit" INT; trap : USR1; (trap '' USR1; exec Xvfb :$n -screen 0 640x480x8 -nolisten tcp) & PID=\$!; wait; if ! kill -0 \$PID 2>/dev/null; then echo 1:\$PID > $path; else echo 0:\$PID > $path; wait \$PID; fi|); + die "exec: $!"; + } + chomp(my $kill = slurp($path)); + unlink($path); + my ($code, $xvfbpid) = ($kill =~ m,^([0-1]):(.*)$,); + next unless $code eq '0'; + + $ENV{DISPLAY} = ":$n"; + say "Running tests under Xvfb display $ENV{DISPLAY}"; + + push(@CLEANUP, sub { + kill(15, $xvfbpid); + }); + last; + } +} + @displays = split(/,/, join(',', @displays)); @displays = map { s/ //g; $_ } @displays; @@ -379,7 +421,7 @@ sub take_job { sub cleanup { my $exitcode = $?; - $_->() for our @CLEANUP; + $_->() for @CLEANUP; exit $exitcode; } @@ -443,6 +485,12 @@ C. Runs i3 under xtrace to trace X11 requests/replies. The output will be available in C. +=item B<--xvfb> + +=item B<--no-xvfb> + +Enable or disable running tests under Xvfb. Enabled by default. + =item B<--coverage-testing> Generates a test coverage report at C. Exits i3 cleanly diff --git a/testcases/inject_randr1.5.c b/testcases/inject_randr1.5.c index 6cccfa76..5506d67e 100644 --- a/testcases/inject_randr1.5.c +++ b/testcases/inject_randr1.5.c @@ -23,6 +23,7 @@ #include #include #include +#include #include static void uds_connection_cb(EV_P_ ev_io *w, int revents); diff --git a/testcases/lib/StartXServer.pm b/testcases/lib/StartXServer.pm index 434ca238..4c5bd6f5 100644 --- a/testcases/lib/StartXServer.pm +++ b/testcases/lib/StartXServer.pm @@ -87,7 +87,7 @@ sub start_xserver { # First get the last used display number, then increment it by one. # Effectively falls back to 1 if no X server is running. - my ($displaynum) = map { /(\d+)$/ } reverse sort glob($x_socketpath . '*'); + my ($displaynum) = reverse sort { $a <=> $b } map{ /(\d+)$/ } glob($x_socketpath . '*'); $displaynum++; say "Starting $parallel Xephyr instances, starting at :$displaynum..."; @@ -105,7 +105,7 @@ sub start_xserver { for (1 .. $parallel) { my $socket = fork_xserver($keep_xserver_output, $displaynum, 'Xephyr', ":$displaynum", '-screen', '1280x800', - '-nolisten', 'tcp'); + '-nolisten', 'tcp', '-name', "i3test"); push(@displays, ":$displaynum"); push(@sockets_waiting, $socket); $displaynum++; diff --git a/testcases/lib/i3test.pm.in b/testcases/lib/i3test.pm.in index a484c91a..e754c0c1 100644 --- a/testcases/lib/i3test.pm.in +++ b/testcases/lib/i3test.pm.in @@ -12,6 +12,7 @@ use AnyEvent::I3; use List::Util qw(first); use Time::HiRes qw(sleep); use Cwd qw(abs_path); +use POSIX ':sys_wait_h'; use Scalar::Util qw(blessed); use SocketActivation; use i3test::Util qw(slurp); @@ -37,6 +38,7 @@ our @EXPORT = qw( cmd sync_with_i3 exit_gracefully + exit_forcefully workspace_exists focused_ws get_socket_path @@ -47,6 +49,8 @@ our @EXPORT = qw( wait_for_unmap $x kill_all_windows + events_for + listen_for_binding ); =head1 NAME @@ -121,7 +125,7 @@ END { } else { kill(-9, $i3_pid) - or $tester->BAIL_OUT("could not kill i3"); + or $tester->BAIL_OUT("could not kill i3: $!"); waitpid $i3_pid, 0; } @@ -131,6 +135,22 @@ sub import { my ($class, %args) = @_; my $pkg = caller; + $x ||= i3test::X11->new; + # set the pointer to a predictable position in case a previous test has + # disturbed it + $x->warp_pointer( + 0, # src_window (None) + $x->get_root_window(), # dst_window (None) + 0, # src_x + 0, # src_y + 0, # src_width + 0, # src_height + 0, # dst_x + 0); # dst_y + # Synchronize with X11 to ensure the pointer has been warped before i3 + # starts up. + $x->get_input_focus_reply($x->get_input_focus()->{sequence}); + $i3_autostart = delete($args{i3_autostart}) // 1; my $i3_config = delete($args{i3_config}) // '-default'; @@ -153,10 +173,6 @@ __ strict->import; warnings->import; - $x ||= i3test::X11->new; - # set the pointer to a predictable position in case a previous test has - # disturbed it - $x->root->warp_pointer(0, 0); $cv->recv if $i3_autostart; @_ = ($class); @@ -179,29 +195,11 @@ received, etc. sub wait_for_event { my ($timeout, $cb) = @_; - my $cv = AE::cv; - $x->flush; - # unfortunately, there is no constant for this - my $ae_read = 0; - - my $guard = AE::io $x->get_file_descriptor, $ae_read, sub { - while (defined(my $event = $x->poll_for_event)) { - if ($cb->($event)) { - $cv->send(1); - last; - } - } - }; - - # Trigger timeout after $timeout seconds (can be fractional) - my $t = AE::timer $timeout, 0, sub { warn "timeout ($timeout secs)"; $cv->send(0) }; - - my $result = $cv->recv; - undef $t; - undef $guard; - return $result; + while (defined(my $event = $x->wait_for_event)) { + return 1 if $cb->($event); + } } =head2 wait_for_map($window) @@ -348,6 +346,12 @@ sub open_window { $window->map; wait_for_map($window); + + # MapWindow is sent before i3 even starts rendering: the window is placed at + # temporary off-screen coordinates first, and x_push_changes() sends further + # X11 requests to set focus etc. Hence, we sync with i3 before continuing. + sync_with_i3(); + return $window; } @@ -686,6 +690,7 @@ sub sync_with_i3 { $_sync_window = open_window( rect => [ -15, -15, 10, 10 ], override_redirect => 1, + dont_map => 1, ); } @@ -756,7 +761,7 @@ sub exit_gracefully { if (!$exited) { kill(9, $pid) - or $tester->BAIL_OUT("could not kill i3"); + or $tester->BAIL_OUT("could not kill i3: $!"); } if ($socketpath =~ m,^/tmp/i3-test-socket-,) { @@ -767,6 +772,47 @@ sub exit_gracefully { undef $i3_pid; } +=head2 exit_forcefully($pid, [ $signal ]) + +Tries to exit i3 forcefully by sending a signal (defaults to SIGTERM). + +You only need to use this function if you want to test signal handling +(in which case you must have launched i3 on your own with +C). + + use i3test i3_autostart => 0; + my $pid = launch_with_config($config); + # … + exit_forcefully($pid); + +=cut +sub exit_forcefully { + my ($pid, $signal) = @_; + $signal ||= 'TERM'; + + # Send the given signal to the i3 instance and wait for up to 10s + # for it to terminate. + kill($signal, $pid) + or $tester->BAIL_OUT("could not kill i3: $!"); + my $status; + my $timeout = 10; + do { + $status = waitpid $pid, WNOHANG; + + if ($status <= 0) { + sleep(1); + $timeout--; + } + } while ($status <= 0 && $timeout > 0); + + if ($status <= 0) { + kill('KILL', $pid) + or $tester->BAIL_OUT("could not kill i3: $!"); + waitpid $pid, 0; + } + undef $i3_pid; +} + =head2 get_socket_path([ $cache ]) Gets the socket path from the C atom stored on the X11 root @@ -900,6 +946,86 @@ sub kill_all_windows { cmd '[title=".*"] kill'; } +=head2 events_for($subscribecb, [ $rettype ], [ $eventcbs ]) + +Helper function which returns an array containing all events of type $rettype +which were generated by i3 while $subscribecb was running. + +Set $eventcbs to subscribe to multiple event types and/or perform your own event +aggregation. + +=cut +sub events_for { + my ($subscribecb, $rettype, $eventcbs) = @_; + + my @events; + $eventcbs //= {}; + if (defined($rettype)) { + $eventcbs->{$rettype} = sub { push @events, shift }; + } + my $subscribed = AnyEvent->condvar; + my $flushed = AnyEvent->condvar; + $eventcbs->{tick} = sub { + my ($event) = @_; + if ($event->{first}) { + $subscribed->send($event); + } else { + $flushed->send($event); + } + }; + my $i3 = i3(get_socket_path(0)); + $i3->connect->recv; + $i3->subscribe($eventcbs)->recv; + $subscribed->recv; + # Subscription established, run the callback. + $subscribecb->(); + # Now generate a tick event, which we know we’ll receive (and at which point + # all other events have been received). + my $nonce = int(rand(255)) + 1; + $i3->send_tick($nonce); + + my $tick = $flushed->recv; + $tester->is_eq($tick->{payload}, $nonce, 'tick nonce received'); + return @events; +} + +=head2 listen_for_binding($cb) + +Helper function to evaluate whether sending KeyPress/KeyRelease events via XTEST +triggers an i3 key binding or not. Expects key bindings to be configured in the +form “bindsym nop ”, e.g. “bindsym Mod4+Return nop +Mod4+Return”. + + is(listen_for_binding( + sub { + xtest_key_press(133); # Super_L + xtest_key_press(36); # Return + xtest_key_release(36); # Return + xtest_key_release(133); # Super_L + xtest_sync_with_i3; + }, + ), + 'Mod4+Return', + 'triggered the "Mod4+Return" keybinding'); + +=cut + +sub listen_for_binding { + my ($cb) = @_; + my $triggered = AnyEvent->condvar; + my @events = events_for( + $cb, + 'binding'); + + $tester->is_eq(scalar @events, 1, 'Received precisely one event'); + $tester->is_eq($events[0]->{change}, 'run', 'change is "run"'); + # We look at the command (which is “nop ”) because that is easier + # than re-assembling the string representation of $event->{binding}. + my $command = $events[0]->{binding}->{command}; + $command =~ s/^nop //g; + return $command; +} + =head1 AUTHOR Michael Stapelberg diff --git a/testcases/lib/i3test/Test.pm b/testcases/lib/i3test/Test.pm index 0253bc2d..552ae8b4 100644 --- a/testcases/lib/i3test/Test.pm +++ b/testcases/lib/i3test/Test.pm @@ -5,6 +5,7 @@ use base 'Test::Builder::Module'; our @EXPORT = qw( is_num_children + is_num_fullscreen cmp_float does_i3_live ); @@ -59,6 +60,25 @@ sub is_num_children { $tb->is_num($got_num_children, $num_children, $name); } +=head2 is_num_fullscreen($workspace, $expected, $test_name) + +Gets the number of fullscreen containers on the given workspace and verifies that +they match the expected amount. + + is_num_fullscreen('1', 0, 'no fullscreen containers on workspace 1'); + +=cut +sub is_num_fullscreen { + my ($workspace, $num_fullscreen, $name) = @_; + my $workspace_content = i3test::get_ws($workspace); + my $tb = $CLASS->builder; + + my $nodes = scalar grep { $_->{fullscreen_mode} != 0 } @{$workspace_content->{nodes}->[0]->{nodes}}; + my $cons = scalar grep { $_->{fullscreen_mode} != 0 } @{$workspace_content->{nodes}}; + my $floating = scalar grep { $_->{fullscreen_mode} != 0 } @{$workspace_content->{floating_nodes}->[0]->{nodes}}; + $tb->is_num($nodes + $cons + $floating, $num_fullscreen, $name); +} + =head2 cmp_float($a, $b) Compares floating point numbers C<$a> and C<$b> and returns true if they differ diff --git a/testcases/lib/i3test/XTEST.pm b/testcases/lib/i3test/XTEST.pm index 1ca964b1..4c464c5e 100644 --- a/testcases/lib/i3test/XTEST.pm +++ b/testcases/lib/i3test/XTEST.pm @@ -14,13 +14,13 @@ use ExtUtils::PkgConfig; use Exporter (); our @EXPORT = qw( inlinec_connect + xtest_sync_with + xtest_sync_with_i3 set_xkb_group xtest_key_press xtest_key_release xtest_button_press xtest_button_release - listen_for_binding - start_binding_capture binding_events ); @@ -38,7 +38,7 @@ i3test::XTEST - Inline::C wrappers for xcb-xtest and xcb-xkb # ineffective. my %sn_config; BEGIN { - %sn_config = ExtUtils::PkgConfig->find('xcb-xkb xcb-xtest'); + %sn_config = ExtUtils::PkgConfig->find('xcb-xkb xcb-xtest xcb-util'); } use Inline C => Config => LIBS => $sn_config{libs}, CCFLAGS => $sn_config{cflags}; @@ -53,8 +53,12 @@ use Inline C => <<'END_OF_C_CODE'; #include #include #include +#include static xcb_connection_t *conn = NULL; +static xcb_window_t sync_window; +static xcb_window_t root_window; +static xcb_atom_t i3_sync_atom; bool inlinec_connect() { int screen; @@ -89,9 +93,94 @@ bool inlinec_connect() { } free(usereply); + xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(conn, xcb_intern_atom(conn, 0, strlen("I3_SYNC"), "I3_SYNC"), NULL); + i3_sync_atom = reply->atom; + free(reply); + + xcb_screen_t *root_screen = xcb_aux_get_screen(conn, screen); + root_window = root_screen->root; + sync_window = xcb_generate_id(conn); + xcb_create_window(conn, + XCB_COPY_FROM_PARENT, // depth + sync_window, // window + root_window, // parent + -15, // x + -15, // y + 1, // width + 1, // height + 0, // border_width + XCB_WINDOW_CLASS_INPUT_OUTPUT, // class + XCB_COPY_FROM_PARENT, // visual + XCB_CW_OVERRIDE_REDIRECT, // value_mask + (uint32_t[]){ + 1, // override_redirect + }); // value_list + return true; } +void xtest_sync_with(int window) { + xcb_client_message_event_t ev; + memset(&ev, '\0', sizeof(xcb_client_message_event_t)); + + const int nonce = rand() % 255; + + ev.response_type = XCB_CLIENT_MESSAGE; + ev.window = sync_window; + ev.type = i3_sync_atom; + ev.format = 32; + ev.data.data32[0] = sync_window; + ev.data.data32[1] = nonce; + + xcb_send_event(conn, false, (xcb_window_t)window, XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, (char *)&ev); + xcb_flush(conn); + + xcb_generic_event_t *event = NULL; + while (1) { + free(event); + if ((event = xcb_wait_for_event(conn)) == NULL) { + break; + } + if (event->response_type == 0) { + fprintf(stderr, "X11 Error received! sequence %x\n", event->sequence); + continue; + } + + /* Strip off the highest bit (set if the event is generated) */ + const int type = (event->response_type & 0x7F); + switch (type) { + case XCB_CLIENT_MESSAGE: { + xcb_client_message_event_t *ev = (xcb_client_message_event_t *)event; + { + const uint32_t got = ev->data.data32[0]; + const uint32_t want = sync_window; + if (got != want) { + fprintf(stderr, "Ignoring ClientMessage: unknown window: got %d, want %d\n", got, want); + continue; + } + } + { + const uint32_t got = ev->data.data32[1]; + const uint32_t want = nonce; + if (got != want) { + fprintf(stderr, "Ignoring ClientMessage: unknown nonce: got %d, want %d\n", got, want); + continue; + } + } + return; + } + default: + fprintf(stderr, "Unexpected X11 event of type %d received (XCB_CLIENT_MESSAGE = %d)\n", type, XCB_CLIENT_MESSAGE); + break; + } + } + free(event); +} + +void xtest_sync_with_i3() { + xtest_sync_with((int)root_window); +} + // NOTE: while |group| should be a uint8_t, Inline::C will not define the // function unless we use an int. bool set_xkb_group(int group) { @@ -170,86 +259,6 @@ sub import { =cut -my $i3; -our @binding_events; - -=head2 start_binding_capture() - -Captures all binding events sent by i3 in the C<@binding_events> symbol, so -that you can verify the correct number of binding events was generated. - - my $pid = launch_with_config($config); - start_binding_capture; - # … - sync_with_i3; - is(scalar @i3test::XTEST::binding_events, 2, 'Received exactly 2 binding events'); - -=cut - -sub start_binding_capture { - # Store a copy of each binding event so that we can count the expected - # events in test cases. - $i3 = i3(get_socket_path()); - $i3->connect()->recv; - $i3->subscribe({ - binding => sub { - my ($event) = @_; - @binding_events = (@binding_events, $event); - }, - })->recv; -} - -=head2 listen_for_binding($cb) - -Helper function to evaluate whether sending KeyPress/KeyRelease events via -XTEST triggers an i3 key binding or not (with a timeout of 0.5s). Expects key -bindings to be configured in the form “bindsym nop ”, e.g. -“bindsym Mod4+Return nop Mod4+Return”. - - is(listen_for_binding( - sub { - xtest_key_press(133); # Super_L - xtest_key_press(36); # Return - xtest_key_release(36); # Return - xtest_key_release(133); # Super_L - }, - ), - 'Mod4+Return', - 'triggered the "Mod4+Return" keybinding'); - -=cut - -sub listen_for_binding { - my ($cb) = @_; - my $triggered = AnyEvent->condvar; - my $i3 = i3(get_socket_path()); - $i3->connect()->recv; - $i3->subscribe({ - binding => sub { - my ($event) = @_; - return unless $event->{change} eq 'run'; - # We look at the command (which is “nop ”) because that is - # easier than re-assembling the string representation of - # $event->{binding}. - $triggered->send($event->{binding}->{command}); - }, - })->recv; - - my $t; - $t = AnyEvent->timer( - after => 0.5, - cb => sub { - $triggered->send('timeout'); - } - ); - - $cb->(); - - my $recv = $triggered->recv; - $recv =~ s/^nop //g; - return $recv; -} - =head2 set_xkb_group($group) Changes the current XKB group from the default of 1 to C<$group>, which must be @@ -283,6 +292,15 @@ Sends a ButtonRelease event via XTEST, with the specified C<$button>. Returns false when there was an X11 error, true otherwise. +=head2 xtest_sync_with($window) + +Ensures the specified window has processed all X11 events which were triggered +by this module, provided the window response to the i3 sync protocol. + +=head2 xtest_sync_with_i3() + +Ensures i3 has processed all X11 events which were triggered by this module. + =head1 AUTHOR Michael Stapelberg diff --git a/testcases/t/100-fullscreen.t b/testcases/t/100-fullscreen.t index f0d0b4c6..d817bee0 100644 --- a/testcases/t/100-fullscreen.t +++ b/testcases/t/100-fullscreen.t @@ -21,13 +21,6 @@ my $i3 = i3(get_socket_path()); my $tmp = fresh_workspace; -sub fullscreen_windows { - my $ws = $tmp; - $ws = shift if @_; - - scalar grep { $_->{fullscreen_mode} != 0 } @{get_ws_content($ws)} -} - # get the output of this workspace my $tree = $i3->get_tree->recv; my @outputs = @{$tree->{nodes}}; @@ -143,11 +136,11 @@ ok(!eq_hash($new_rect, $original_rect), "Window got repositioned"); $swindow->fullscreen(1); sync_with_i3; -is(fullscreen_windows(), 1, 'amount of fullscreen windows'); +is_num_fullscreen($tmp, 1, 'amount of fullscreen windows'); $window->fullscreen(0); sync_with_i3; -is(fullscreen_windows(), 1, 'amount of fullscreen windows'); +is_num_fullscreen($tmp, 1, 'amount of fullscreen windows'); ok($swindow->mapped, 'window mapped after other fullscreen ended'); @@ -160,15 +153,15 @@ ok($swindow->mapped, 'window mapped after other fullscreen ended'); $swindow->fullscreen(0); sync_with_i3; -is(fullscreen_windows(), 0, 'amount of fullscreen windows after disabling'); +is_num_fullscreen($tmp, 0, 'amount of fullscreen windows after disabling'); cmd 'fullscreen'; -is(fullscreen_windows(), 1, 'amount of fullscreen windows after fullscreen command'); +is_num_fullscreen($tmp, 1, 'amount of fullscreen windows after fullscreen command'); cmd 'fullscreen'; -is(fullscreen_windows(), 0, 'amount of fullscreen windows after fullscreen command'); +is_num_fullscreen($tmp, 0, 'amount of fullscreen windows after fullscreen command'); # clean up the workspace so that it will be cleaned when switching away cmd 'kill' for (@{get_ws_content($tmp)}); @@ -221,18 +214,18 @@ $swindow = open_window; cmd 'fullscreen'; -is(fullscreen_windows($tmp2), 1, 'one fullscreen window on second ws'); +is_num_fullscreen($tmp2, 1, 'one fullscreen window on second ws'); cmd "move workspace $tmp"; -is(fullscreen_windows($tmp2), 0, 'no fullscreen windows on second ws'); -is(fullscreen_windows($tmp), 1, 'one fullscreen window on first ws'); +is_num_fullscreen($tmp2, 0, 'no fullscreen windows on second ws'); +is_num_fullscreen($tmp, 1, 'one fullscreen window on first ws'); $swindow->fullscreen(0); sync_with_i3; # Verify that $swindow was the one that initially remained fullscreen. -is(fullscreen_windows($tmp), 0, 'no fullscreen windows on first ws'); +is_num_fullscreen($tmp, 0, 'no fullscreen windows on first ws'); ################################################################################ # Verify that opening a window with _NET_WM_STATE_FULLSCREEN unfullscreens any @@ -245,14 +238,14 @@ $window = open_window(); cmd "fullscreen"; -is(fullscreen_windows($tmp), 1, 'one fullscreen window on ws'); +is_num_fullscreen($tmp, 1, 'one fullscreen window on ws'); is($x->input_focus, $window->id, 'fullscreen window focused'); $swindow = open_window({ fullscreen => 1 }); -is(fullscreen_windows($tmp), 1, 'one fullscreen window on ws'); +is_num_fullscreen($tmp, 1, 'one fullscreen window on ws'); is($x->input_focus, $swindow->id, 'fullscreen window focused'); ################################################################################ @@ -263,19 +256,19 @@ $tmp = fresh_workspace; $window = open_window; is($x->input_focus, $window->id, 'window focused'); -is(fullscreen_windows($tmp), 0, 'no fullscreen window on workspace'); +is_num_fullscreen($tmp, 0, 'no fullscreen window on workspace'); cmd 'fullscreen enable'; is($x->input_focus, $window->id, 'window still focused'); -is(fullscreen_windows($tmp), 1, 'one fullscreen window on workspace'); +is_num_fullscreen($tmp, 1, 'one fullscreen window on workspace'); cmd 'fullscreen enable'; is($x->input_focus, $window->id, 'window still focused'); -is(fullscreen_windows($tmp), 1, 'still one fullscreen window on workspace'); +is_num_fullscreen($tmp, 1, 'still one fullscreen window on workspace'); $window->fullscreen(0); sync_with_i3; -is(fullscreen_windows($tmp), 0, 'no fullscreen window on workspace'); +is_num_fullscreen($tmp, 0, 'no fullscreen window on workspace'); ################################################################################ # Verify that command ‘fullscreen enable global’ works and is idempotent. @@ -285,19 +278,19 @@ $tmp = fresh_workspace; $window = open_window; is($x->input_focus, $window->id, 'window focused'); -is(fullscreen_windows($tmp), 0, 'no fullscreen window on workspace'); +is_num_fullscreen($tmp, 0, 'no fullscreen window on workspace'); cmd 'fullscreen enable global'; is($x->input_focus, $window->id, 'window still focused'); -is(fullscreen_windows($tmp), 1, 'one fullscreen window on workspace'); +is_num_fullscreen($tmp, 1, 'one fullscreen window on workspace'); cmd 'fullscreen enable global'; is($x->input_focus, $window->id, 'window still focused'); -is(fullscreen_windows($tmp), 1, 'still one fullscreen window on workspace'); +is_num_fullscreen($tmp, 1, 'still one fullscreen window on workspace'); $window->fullscreen(0); sync_with_i3; -is(fullscreen_windows($tmp), 0, 'no fullscreen window on workspace'); +is_num_fullscreen($tmp, 0, 'no fullscreen window on workspace'); ################################################################################ # Verify that command ‘fullscreen disable’ works and is idempotent. @@ -307,19 +300,19 @@ $tmp = fresh_workspace; $window = open_window; is($x->input_focus, $window->id, 'window focused'); -is(fullscreen_windows($tmp), 0, 'no fullscreen window on workspace'); +is_num_fullscreen($tmp, 0, 'no fullscreen window on workspace'); $window->fullscreen(1); sync_with_i3; -is(fullscreen_windows($tmp), 1, 'one fullscreen window on workspace'); +is_num_fullscreen($tmp, 1, 'one fullscreen window on workspace'); cmd 'fullscreen disable'; is($x->input_focus, $window->id, 'window still focused'); -is(fullscreen_windows($tmp), 0, 'no fullscreen window on workspace'); +is_num_fullscreen($tmp, 0, 'no fullscreen window on workspace'); cmd 'fullscreen disable'; is($x->input_focus, $window->id, 'window still focused'); -is(fullscreen_windows($tmp), 0, 'still no fullscreen window on workspace'); +is_num_fullscreen($tmp, 0, 'still no fullscreen window on workspace'); ################################################################################ # Verify that command ‘fullscreen toggle’ works. @@ -328,15 +321,15 @@ is(fullscreen_windows($tmp), 0, 'still no fullscreen window on workspace'); $tmp = fresh_workspace; $window = open_window; -is(fullscreen_windows($tmp), 0, 'no fullscreen window on workspace'); +is_num_fullscreen($tmp, 0, 'no fullscreen window on workspace'); cmd 'fullscreen toggle'; is($x->input_focus, $window->id, 'window still focused'); -is(fullscreen_windows($tmp), 1, 'one fullscreen window on workspace'); +is_num_fullscreen($tmp, 1, 'one fullscreen window on workspace'); cmd 'fullscreen toggle'; is($x->input_focus, $window->id, 'window still focused'); -is(fullscreen_windows($tmp), 0, 'no fullscreen window on workspace'); +is_num_fullscreen($tmp, 0, 'no fullscreen window on workspace'); ################################################################################ # Verify that a window’s fullscreen is disabled when another one is enabled @@ -349,15 +342,15 @@ $window = open_window; $other = open_window; is($x->input_focus, $other->id, 'other window focused'); -is(fullscreen_windows($tmp), 0, 'no fullscreen window on workspace'); +is_num_fullscreen($tmp, 0, 'no fullscreen window on workspace'); cmd 'fullscreen enable'; is($x->input_focus, $other->id, 'other window focused'); -is(fullscreen_windows($tmp), 1, 'one fullscreen window on workspace'); +is_num_fullscreen($tmp, 1, 'one fullscreen window on workspace'); cmd '[id="' . $window->id . '"] fullscreen enable'; is($x->input_focus, $window->id, 'window focused'); -is(fullscreen_windows($tmp), 1, 'one fullscreen window on workspace'); +is_num_fullscreen($tmp, 1, 'one fullscreen window on workspace'); ################################################################################ # Verify that when a global fullscreen is enabled the window is focused and diff --git a/testcases/t/113-urgent.t b/testcases/t/113-urgent.t index 9c1507e6..1e2644ad 100644 --- a/testcases/t/113-urgent.t +++ b/testcases/t/113-urgent.t @@ -304,7 +304,7 @@ for ($type = 1; $type <= 2; $type++) { cmd 'move right'; cmd '[id="' . $w3->id . '"] focus'; sync_with_i3; - my $ws = get_ws($tmp); + $ws = get_ws($tmp); ok(!$ws->{urgent}, 'urgent flag not set on workspace'); ############################################################################## diff --git a/testcases/t/115-ipc-workspaces.t b/testcases/t/115-ipc-workspaces.t index 34ce0781..b0c4354e 100644 --- a/testcases/t/115-ipc-workspaces.t +++ b/testcases/t/115-ipc-workspaces.t @@ -16,61 +16,25 @@ use i3test; -my $i3 = i3(get_socket_path()); -$i3->connect()->recv; - -################################ -# Workspaces requests and events -################################ - my $old_ws = get_ws(focused_ws()); -# Events - # We are switching to an empty workpspace from an empty workspace, so we expect # to receive "init", "focus", and "empty". -my $init = AnyEvent->condvar; -my $focus = AnyEvent->condvar; -my $empty = AnyEvent->condvar; -$i3->subscribe({ - workspace => sub { - my ($event) = @_; - if ($event->{change} eq 'init') { - $init->send($event); - } elsif ($event->{change} eq 'focus') { - $focus->send($event); - } elsif ($event->{change} eq 'empty') { - $empty->send($event); - } - } -})->recv; - -cmd 'workspace 2'; - -my $t; -$t = AnyEvent->timer( - after => 0.5, - cb => sub { - $init->send(0); - $focus->send(0); - $empty->send(0); - } -); - -my $init_event = $init->recv; -my $focus_event = $focus->recv; -my $empty_event = $empty->recv; +my @events = events_for( + sub { cmd 'workspace 2' }, + 'workspace'); my $current_ws = get_ws(focused_ws()); -ok($init_event, 'workspace "init" event received'); -is($init_event->{current}->{id}, $current_ws->{id}, 'the "current" property should contain the initted workspace con'); +is(scalar @events, 3, 'Received 3 events'); +is($events[0]->{change}, 'init', 'First event has change = init'); +is($events[0]->{current}->{id}, $current_ws->{id}, 'the "current" property contains the initted workspace con'); -ok($focus_event, 'workspace "focus" event received'); -is($focus_event->{current}->{id}, $current_ws->{id}, 'the "current" property should contain the focused workspace con'); -is($focus_event->{old}->{id}, $old_ws->{id}, 'the "old" property should contain the workspace con that was focused last'); +is($events[1]->{change}, 'focus', 'Second event has change = focus'); +is($events[1]->{current}->{id}, $current_ws->{id}, 'the "current" property should contain the focused workspace con'); +is($events[1]->{old}->{id}, $old_ws->{id}, 'the "old" property should contain the workspace con that was focused last'); -ok($empty_event, 'workspace "empty" event received'); -is($empty_event->{current}->{id}, $old_ws->{id}, 'the "current" property should contain the emptied workspace con'); +is($events[2]->{change}, 'empty', 'Third event has change = empty'); +is($events[2]->{current}->{id}, $old_ws->{id}, 'the "current" property should contain the emptied workspace con'); done_testing; diff --git a/testcases/t/156-fullscreen-focus.t b/testcases/t/156-fullscreen-focus.t index 7a5e38ca..9c396f40 100644 --- a/testcases/t/156-fullscreen-focus.t +++ b/testcases/t/156-fullscreen-focus.t @@ -157,9 +157,6 @@ isnt($x->input_focus, $right2->id, 'bottom right window no longer focused'); cmd 'focus child'; is($x->input_focus, $right2->id, 'bottom right window focused again'); -cmd '[id="' . $left->id . '"] focus'; -is($x->input_focus, $right2->id, 'prevented focus change to left window'); - cmd 'focus up'; is($x->input_focus, $right1->id, 'allowed focus up'); @@ -178,9 +175,6 @@ is($x->input_focus, $right1->id, 'allowed focus wrap (down)'); cmd 'focus up'; is($x->input_focus, $right2->id, 'allowed focus wrap (up)'); -cmd '[id="' . $diff_ws->id . '"] focus'; -is($x->input_focus, $right2->id, 'prevented focus change to different ws'); - ################################################################################ # Same tests when we're in non-global fullscreen mode. It should now be possible # to focus a container in a different workspace. @@ -202,9 +196,6 @@ isnt($x->input_focus, $right2->id, 'bottom right window no longer focused'); cmd 'focus child'; is($x->input_focus, $right2->id, 'bottom right window focused again'); -cmd '[id="' . $left->id . '"] focus'; -is($x->input_focus, $right2->id, 'prevented focus change to left window'); - cmd 'focus up'; is($x->input_focus, $right1->id, 'allowed focus up'); @@ -323,6 +314,105 @@ verify_move(2, 'prevented move to workspace by name'); cmd "move to workspace prev"; verify_move(2, 'prevented move to workspace by position'); -# TODO: Tests for "move to output" and "move workspace to output". +################################################################################ +# Ensure it's possible to focus a window using the focus command despite +# fullscreen window blocking it. Fullscreen window should lose its fullscreen +# mode. +################################################################################ +# first & second tiling, focus using id +kill_all_windows; + +$tmp = fresh_workspace; +my $first = open_window; +my $second = open_window; +cmd 'fullscreen'; +is($x->input_focus, $second->id, 'fullscreen window focused'); +is_num_fullscreen($tmp, 1, '1 fullscreen window'); + +cmd '[id="'. $first->id .'"] focus'; +sync_with_i3; + +is($x->input_focus, $first->id, 'correctly focused using id'); +is_num_fullscreen($tmp, 0, 'no fullscreen windows'); + +# first floating, second tiling, focus using 'focus floating' +kill_all_windows; + +$tmp = fresh_workspace; +$first = open_floating_window; +$second = open_window; +cmd 'fullscreen'; +is($x->input_focus, $second->id, 'fullscreen window focused'); +is_num_fullscreen($tmp, 1, '1 fullscreen window'); + +cmd 'focus floating'; +sync_with_i3; + +is($x->input_focus, $first->id, 'correctly focused using focus floating'); +is_num_fullscreen($tmp, 0, 'no fullscreen windows'); + +# first tiling, second floating, focus using 'focus tiling' +kill_all_windows; + +$tmp = fresh_workspace; +$first = open_window; +$second = open_floating_window; +cmd 'fullscreen'; +is($x->input_focus, $second->id, 'fullscreen window focused'); +is_num_fullscreen($tmp, 1, '1 fullscreen window'); + +cmd 'focus tiling'; +sync_with_i3; + +is($x->input_focus, $first->id, 'correctly focused using focus tiling'); +is_num_fullscreen($tmp, 0, 'no fullscreen windows'); + +################################################################################ +# When the fullscreen window is in an other workspace it should maintain its +# fullscreen mode since it's not blocking the window to be focused. +################################################################################ + +kill_all_windows; + +$tmp = fresh_workspace; +$first = open_window; + +$tmp2 = fresh_workspace; +$second = open_window; +cmd 'fullscreen'; +is($x->input_focus, $second->id, 'fullscreen window focused'); +is_num_fullscreen($tmp2, 1, '1 fullscreen window'); + +cmd '[id="'. $first->id .'"] focus'; +sync_with_i3; + +is($x->input_focus, $first->id, 'correctly focused using focus id'); +is_num_fullscreen($tmp, 0, 'no fullscreen windows on first workspace'); +is_num_fullscreen($tmp2, 1, 'still one fullscreen window on second workspace'); + +################################################################################ +# But a global window in another workspace is blocking the window to be focused. +# Ensure that it loses its fullscreen mode. +################################################################################ + +kill_all_windows; + +$tmp = fresh_workspace; +$first = open_window; + +$tmp2 = fresh_workspace; +$second = open_window; +cmd 'fullscreen global'; +is($x->input_focus, $second->id, 'global window focused'); +is_num_fullscreen($tmp2, 1, '1 fullscreen window'); + +cmd '[id="'. $first->id .'"] focus'; +sync_with_i3; + +is($x->input_focus, $first->id, 'correctly focused using focus id'); +is_num_fullscreen($tmp2, 0, 'no fullscreen windows'); + + +# TODO: Tests for "move to output" and "move workspace to output". done_testing; diff --git a/testcases/t/166-assign.t b/testcases/t/166-assign.t index 21cc5189..f7a08647 100644 --- a/testcases/t/166-assign.t +++ b/testcases/t/166-assign.t @@ -21,18 +21,44 @@ use i3test i3_autostart => 0; sub open_special { my %args = @_; $args{name} //= 'special window'; + $args{wm_class} //= 'special'; # We use dont_map because i3 will not map the window on the current # workspace. Thus, open_window would time out in wait_for_map (2 seconds). my $window = open_window( %args, - wm_class => 'special', dont_map => 1, ); $window->map; return $window; } +sub test_workspace_assignment { + my $target_ws = "@_"; + + # initialize the target workspace, then go to a fresh one + ok(!($target_ws ~~ @{get_workspace_names()}), "$target_ws does not exist yet"); + cmd "workspace $target_ws"; + cmp_ok(@{get_ws_content($target_ws)}, '==', 0, "no containers on $target_ws yet"); + cmd 'open'; + cmp_ok(@{get_ws_content($target_ws)}, '==', 1, "one container on $target_ws"); + my $tmp = fresh_workspace; + + ok(@{get_ws_content($tmp)} == 0, 'no containers yet'); + ok($target_ws ~~ @{get_workspace_names()}, "$target_ws does not exist yet"); + + # 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 + my $window = open_special; + sync_with_i3; + + ok(@{get_ws_content($tmp)} == 0, 'still no containers'); + ok(@{get_ws_content($target_ws)} == 2, "two containers on $target_ws"); + + return $window +} + ##################################################################### # start a window and see that it does not get assigned with an empty config ##################################################################### @@ -87,33 +113,67 @@ $window->destroy; exit_gracefully($pid); ##################################################################### -# start a window and see that it gets assigned to a workspace which has content -# already, next to the existing node. +# start a window and see that it gets assigned to a formerly unused +# numbered workspace +##################################################################### + +my $config_numbered = <destroy; + +exit_gracefully($pid); + +##################################################################### +# start a window and see that it gets assigned to a numbered +# workspace which has content already, next to the existing node. +##################################################################### + +$pid = launch_with_config($config_numbered); + +$window = test_workspace_assignment("2"); +$window->destroy; + +exit_gracefully($pid); + +##################################################################### +# start a window and see that it gets assigned to a numbered workspace with +# a name which has content already, next to the existing node. +##################################################################### + +$pid = launch_with_config($config_numbered); + +cmd 'workspace 2'; # Make sure that we are not testing for "2" again. +$window = test_workspace_assignment("2: targetws"); +$window->destroy; + +exit_gracefully($pid); + +##################################################################### +# start a window and see that it gets assigned to a workspace which +# has content already, next to the existing node. ##################################################################### $pid = launch_with_config($config); -# initialize the target workspace, then go to a fresh one -ok(!("targetws" ~~ @{get_workspace_names()}), 'targetws does not exist yet'); -cmd 'workspace targetws'; -cmp_ok(@{get_ws_content('targetws')}, '==', 0, 'no containers on targetws yet'); -cmd 'open'; -cmp_ok(@{get_ws_content('targetws')}, '==', 1, 'one container on targetws'); -$tmp = fresh_workspace; - -ok(@{get_ws_content($tmp)} == 0, 'no containers yet'); -ok("targetws" ~~ @{get_workspace_names()}, 'targetws does not exist yet'); - - -# 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 -$window = open_special(dont_map => 1); -$window->map; -sync_with_i3; - -ok(@{get_ws_content($tmp)} == 0, 'still no containers'); -ok(@{get_ws_content('targetws')} == 2, 'two containers on targetws'); +test_workspace_assignment("targetws"); exit_gracefully($pid); @@ -143,8 +203,127 @@ my $content = get_ws($tmp); ok(@{$content->{nodes}} == 0, 'no tiling cons'); ok(@{$content->{floating_nodes}} == 1, 'one floating con'); -$window->destroy; +kill_all_windows; +exit_gracefully($pid); +##################################################################### +# test assignments to named outputs +##################################################################### +$config = < $class); + sync_with_i3; + is_num_children($ws, $expected_count, + "after: $expected_count containers on output $output"); +} + +cmd "workspace ws-0"; +open_in_output(0, 1); +my $focused = $x->input_focus; + +open_in_output(1, 1); +is($x->input_focus, $focused, 'focus remains on output fake-0'); + +open_in_output(2, 1); +is($x->input_focus, $focused, 'focus remains on output fake-0'); + +for my $i (1 .. 5){ + open_in_output(3, $i); + is($x->input_focus, $focused, 'focus remains on output fake-0'); +} + +# Check invalid output +$tmp = fresh_workspace; +open_special(wm_class => "special-4"); +sync_with_i3; +is_num_children($tmp, 1, 'window assigned to invalid output opened in current workspace'); +open_special(wm_class => "special-3"); +sync_with_i3; +is_num_children($tmp, 1, 'but window assigned to valid output did not'); + +kill_all_windows; +exit_gracefully($pid); + +##################################################################### +# Test assignments to outputs with relative names +##################################################################### +$config = < 'current'); +} +sync_with_i3; +is_num_children('left-top', 5, 'windows opened in current workspace'); + +is_num_children('right-top', 0, 'no children on right-top'); +open_special(wm_class => 'right'); +sync_with_i3; +is_num_children('right-top', 1, 'one child on right-top'); + +is_num_children('left-bottom', 0, 'no children on left-bottom'); +open_special(wm_class => 'down'); +sync_with_i3; +is_num_children('left-bottom', 1, 'one child on left-bottom'); + +cmd 'workspace right-bottom'; + +open_special(wm_class => 'up'); +sync_with_i3; +is_num_children('right-top', 2, 'two children on right-top'); + +open_special(wm_class => 'left'); +sync_with_i3; +is_num_children('left-bottom', 2, 'two children on left-bottom'); + +kill_all_windows; exit_gracefully($pid); ##################################################################### @@ -181,7 +360,7 @@ $tmp = fresh_workspace; ok(@{get_ws_content($tmp)} == 0, 'no containers yet'); my @docked = get_dock_clients; -is(@docked, 0, 'one dock client yet'); +is(@docked, 0, 'no dock client yet'); $window = open_special( window_type => $x->atom(name => '_NET_WM_WINDOW_TYPE_DOCK'), diff --git a/testcases/t/167-workspace_layout.t b/testcases/t/167-workspace_layout.t index d983eb85..597d545e 100644 --- a/testcases/t/167-workspace_layout.t +++ b/testcases/t/167-workspace_layout.t @@ -375,7 +375,74 @@ ok(@content == 2, 'two containers opened'); isnt($content[0]->{layout}, 'tabbed', 'layout not tabbed'); isnt($content[1]->{layout}, 'tabbed', 'layout not tabbed'); +exit_gracefully($pid); + +##################################################################### +# 16: Check that the command 'layout toggle split' works regardless +# of what layout we're using. +##################################################################### + +$config = <{layout}, 'splitv', 'layout toggles to splitv'); + } else { + is($content[0]->{layout}, 'splith', 'layout toggles to splith'); + } + + cmd '[id="' . $first->id . '"] kill'; + cmd '[id="' . $second->id . '"] kill'; + sync_with_i3; +} exit_gracefully($pid); +##################################################################### +# 17: Check about setting a new layout. +##################################################################### + +$config = <{layout}, $second_layout, 'layout changes to ' . $second_layout); + + cmd '[id="' . $first->id . '"] kill'; + cmd '[id="' . $second->id . '"] kill'; + sync_with_i3; + } +} + done_testing; diff --git a/testcases/t/183-config-variables.t b/testcases/t/183-config-variables.t index 4b225214..d135ed59 100644 --- a/testcases/t/183-config-variables.t +++ b/testcases/t/183-config-variables.t @@ -95,7 +95,19 @@ EOT is(launch_get_border($config), 'none', 'no border'); +##################################################################### +# test that variables with longer name than value don't crash i3 with +# v3 to v4 conversion. +# See: #3076 +##################################################################### +$config = <<'EOT'; +set $var a +EOT + +my $pid = launch_with_config($config); +does_i3_live; +exit_gracefully($pid); done_testing; diff --git a/testcases/t/185-scratchpad.t b/testcases/t/185-scratchpad.t index f94bd75b..147890e1 100644 --- a/testcases/t/185-scratchpad.t +++ b/testcases/t/185-scratchpad.t @@ -429,7 +429,7 @@ does_i3_live; ################################################################################ clear_scratchpad; -my $ws = fresh_workspace; +$ws = fresh_workspace; open_window; my $scratch = get_focused($ws); diff --git a/testcases/t/189-floating-constraints.t b/testcases/t/189-floating-constraints.t index ea4c08de..6b082bfd 100644 --- a/testcases/t/189-floating-constraints.t +++ b/testcases/t/189-floating-constraints.t @@ -190,7 +190,7 @@ exit_gracefully($pid); # 7: check floating_maximum_size with cmd_size ################################################################################ -my $config = < [ 0, 0, 90, 80 ]); +$window = open_floating_window(rect => [ 0, 0, 90, 80 ]); cmd 'border none'; cmd 'resize set 101 91'; sync_with_i3; -my $rect = $window->rect; +$rect = $window->rect; is($rect->{width}, 100, 'width did not exceed maximum width'); is($rect->{height}, 90, 'height did not exceed maximum height'); diff --git a/testcases/t/195-net-active-window.t b/testcases/t/195-net-active-window.t index aee1e03a..f9f883cb 100644 --- a/testcases/t/195-net-active-window.t +++ b/testcases/t/195-net-active-window.t @@ -24,7 +24,7 @@ use i3test; sub send_net_active_window { my ($id, $source) = @_; - $source = ($source eq 'pager' ? 2 : 0); + $source = (((defined $source) && ($source eq 'pager')) ? 2 : 0); my $msg = pack "CCSLLLLLLL", X11::XCB::CLIENT_MESSAGE, # response_type @@ -137,7 +137,7 @@ is($x->input_focus, $win3->id, 'window 3 still focused'); # is received. ################################################################################ -my $scratch = open_window; +$scratch = open_window; is($x->input_focus, $scratch->id, 'to-scratchpad window has focus'); diff --git a/testcases/t/199-ipc-mode-event.t b/testcases/t/199-ipc-mode-event.t index 959ff6c4..0e4f8960 100644 --- a/testcases/t/199-ipc-mode-event.t +++ b/testcases/t/199-ipc-mode-event.t @@ -28,24 +28,11 @@ mode "with spaces" { } EOT -my $i3 = i3(get_socket_path(0)); -$i3->connect->recv; +my @events = events_for( + sub { cmd 'mode "m1"' }, + 'mode'); -my $cv = AnyEvent->condvar; - -$i3->subscribe({ - mode => sub { - my ($event) = @_; - $cv->send($event->{change} eq 'm1'); - } -})->recv; - -cmd 'mode "m1"'; - -# Timeout after 0.5s -my $t; -$t = AnyEvent->timer(after => 0.5, cb => sub { $cv->send(0); }); - -ok($cv->recv, 'Mode event received'); +my @changes = map { $_->{change} } @events; +is_deeply(\@changes, [ 'm1' ], 'Mode event received'); done_testing; diff --git a/testcases/t/201-config-parser.t b/testcases/t/201-config-parser.t index 01b22273..6857b621 100644 --- a/testcases/t/201-config-parser.t +++ b/testcases/t/201-config-parser.t @@ -116,6 +116,7 @@ is(parser_calls($config), $config = <<'EOT'; assign [class="^Chrome"] 4 +assign [class="^Chrome"] workspace number 3 assign [class="^Chrome"] named workspace assign [class="^Chrome"] "quoted named workspace" assign [class="^Chrome"] → "quoted named workspace" @@ -123,13 +124,15 @@ EOT $expected = <<'EOT'; cfg_criteria_add(class, ^Chrome) -cfg_assign(4) +cfg_assign(4, 0) cfg_criteria_add(class, ^Chrome) -cfg_assign(named workspace) +cfg_assign(3, 1) cfg_criteria_add(class, ^Chrome) -cfg_assign(quoted named workspace) +cfg_assign(named workspace, 0) cfg_criteria_add(class, ^Chrome) -cfg_assign(quoted named workspace) +cfg_assign(quoted named workspace, 0) +cfg_criteria_add(class, ^Chrome) +cfg_assign(quoted named workspace, 0) EOT is(parser_calls($config), @@ -142,7 +145,7 @@ is(parser_calls($config), $config = <<'EOT'; floating_minimum_size 80x55 -floating_minimum_size 80 x 55 +floating_minimum_size 80 x 55 floating_maximum_size 73 x 10 EOT @@ -242,8 +245,8 @@ is(parser_calls($config), ################################################################################ $config = <<'EOT'; -workspace "3" output DP-1 -workspace "3" output VGA-1 +workspace "3" output DP-1 +workspace "3" output VGA-1 EOT $expected = <<'EOT'; @@ -263,20 +266,34 @@ $config = <<'EOT'; new_window 1pixel new_window normal new_window none +default_border 1pixel +default_border normal +default_border none new_float 1pixel new_float normal new_float none +default_floating_border 1pixel +default_floating_border normal +default_floating_border none EOT $expected = <<'EOT'; -cfg_new_window(new_window, 1pixel, -1) -cfg_new_window(new_window, normal, 2) -cfg_new_window(new_window, none, -1) -cfg_new_window(new_float, 1pixel, -1) -cfg_new_window(new_float, normal, 2) -cfg_new_window(new_float, none, -1) +cfg_default_border(new_window, 1pixel, -1) +cfg_default_border(new_window, normal, 2) +cfg_default_border(new_window, none, -1) +cfg_default_border(default_border, 1pixel, -1) +cfg_default_border(default_border, normal, 2) +cfg_default_border(default_border, none, -1) +cfg_default_border(new_float, 1pixel, -1) +cfg_default_border(new_float, normal, 2) +cfg_default_border(new_float, none, -1) +cfg_default_border(default_floating_border, 1pixel, -1) +cfg_default_border(default_floating_border, normal, 2) +cfg_default_border(default_floating_border, none, -1) EOT +# TODO: are there no tests for "border pixel 1" etc? + is(parser_calls($config), $expected, 'new_window ok'); @@ -459,7 +476,9 @@ my $expected_all_tokens = "ERROR: CONFIG: Expected one of these tokens: , ' floating_modifier default_orientation workspace_layout + default_border new_window + default_floating_border new_float hide_edge_borders for_window @@ -467,6 +486,7 @@ my $expected_all_tokens = "ERROR: CONFIG: Expected one of these tokens: , ' no_focus focus_follows_mouse mouse_warping + focus_wrapping force_focus_wrapping force_xinerama force-xinerama diff --git a/testcases/t/205-ipc-windows.t b/testcases/t/205-ipc-windows.t index ca7db153..bafd155f 100644 --- a/testcases/t/205-ipc-windows.t +++ b/testcases/t/205-ipc-windows.t @@ -16,46 +16,15 @@ use i3test; -SKIP: { - - skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15; - -my $i3 = i3(get_socket_path()); -$i3->connect()->recv; - -################################ -# Window event -################################ - -# Events - my $new = AnyEvent->condvar; my $focus = AnyEvent->condvar; -$i3->subscribe({ - window => sub { - my ($event) = @_; - if ($event->{change} eq 'new') { - $new->send($event); - } elsif ($event->{change} eq 'focus') { - $focus->send($event); - } - } -})->recv; -open_window; +my @events = events_for( + sub { open_window }, + 'window'); -my $t; -$t = AnyEvent->timer( - after => 0.5, - cb => sub { - $new->send(0); - $focus->send(0); - } -); - -is($new->recv->{container}->{focused}, 0, 'Window "new" event received'); -is($focus->recv->{container}->{focused}, 1, 'Window "focus" event received'); - -} +is(scalar @events, 2, 'Received 2 events'); +is($events[0]->{container}->{focused}, 0, 'Window "new" event received'); +is($events[1]->{container}->{focused}, 1, 'Window "focus" event received'); done_testing; diff --git a/testcases/t/206-fullscreen-scratchpad.t b/testcases/t/206-fullscreen-scratchpad.t index c098f23f..95245099 100644 --- a/testcases/t/206-fullscreen-scratchpad.t +++ b/testcases/t/206-fullscreen-scratchpad.t @@ -21,15 +21,6 @@ use i3test; my $tmp = fresh_workspace; -sub fullscreen_windows { - my $ws = $tmp; - $ws = shift if @_; - - my $nodes = scalar grep { $_->{fullscreen_mode} != 0 } @{get_ws_content($ws)->[0]->{nodes}}; - my $cons = scalar grep { $_->{fullscreen_mode} != 0 } @{get_ws_content($ws)}; - return $nodes + $cons; -} - ########################################################################################## # map two windows in one container, fullscreen one of them and then move it to scratchpad ########################################################################################## @@ -41,7 +32,7 @@ my $second_win = open_window; cmd 'fullscreen'; # see if the window really is in fullscreen mode -is(fullscreen_windows(), 1, 'amount of fullscreen windows after enabling fullscreen'); +is_num_fullscreen($tmp, 1, 'amount of fullscreen windows after enabling fullscreen'); # move window to scratchpad cmd 'move scratchpad'; @@ -57,7 +48,7 @@ cmd 'scratchpad show'; cmd 'floating toggle'; # see if no window is in fullscreen mode -is(fullscreen_windows(), 0, 'amount of fullscreen windows after showing previously fullscreened scratchpad window'); +is_num_fullscreen($tmp, 0, 'amount of fullscreen windows after showing previously fullscreened scratchpad window'); ######################################################################################## # move a window to scratchpad, focus parent container, make it fullscreen, focus a child @@ -79,7 +70,7 @@ cmd 'fullscreen'; cmd 'focus child'; # see if the window really is in fullscreen mode -is(fullscreen_windows(), 1, 'amount of fullscreen windows after enabling fullscreen on parent'); +is_num_fullscreen($tmp, 1, 'amount of fullscreen windows after enabling fullscreen on parent'); ########################################################################## # show a scratchpad window; no window should be in fullscreen mode anymore @@ -89,6 +80,6 @@ is(fullscreen_windows(), 1, 'amount of fullscreen windows after enabling fullscr cmd 'scratchpad show'; # see if no window is in fullscreen mode -is(fullscreen_windows(), 0, 'amount of fullscreen windows after showing a scratchpad window while a parent container was in fullscreen mode'); +is_num_fullscreen($tmp, 0, 'amount of fullscreen windows after showing a scratchpad window while a parent container was in fullscreen mode'); done_testing; diff --git a/testcases/t/207-shmlog.t b/testcases/t/207-shmlog.t index 1351d568..c2b2ebaa 100644 --- a/testcases/t/207-shmlog.t +++ b/testcases/t/207-shmlog.t @@ -54,7 +54,7 @@ like($stderr, qr#^$#, 'stderr empty'); # 3: change size of the shared memory log buffer and verify old content is gone ################################################################################ -cmd 'shmlog ' . (23 * 1024 * 1024); +cmd 'shmlog ' . (1 * 1024 * 1024); run [ 'i3-dump-log' ], '>', \$stdout, diff --git a/testcases/t/219-ipc-window-focus.t b/testcases/t/219-ipc-window-focus.t index ae778187..b1c8ba18 100644 --- a/testcases/t/219-ipc-window-focus.t +++ b/testcases/t/219-ipc-window-focus.t @@ -16,13 +16,6 @@ use i3test; -SKIP: { - - skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15; - -my $i3 = i3(get_socket_path()); -$i3->connect()->recv; - ################################ # Window focus event ################################ @@ -33,62 +26,29 @@ my $win0 = open_window; my $win1 = open_window; my $win2 = open_window; -my $focus = AnyEvent->condvar; - -$i3->subscribe({ - window => sub { - my ($event) = @_; - $focus->send($event); - } -})->recv; - -my $t; -$t = AnyEvent->timer( - after => 0.5, - cb => sub { - $focus->send(0); - } -); - # ensure the rightmost window contains input focus -$i3->command('[id="' . $win2->id . '"] focus')->recv; +cmd '[id="' . $win2->id . '"] focus'; is($x->input_focus, $win2->id, "Window 2 focused"); -cmd 'focus left'; -my $event = $focus->recv; -is($event->{change}, 'focus', 'Focus event received'); -is($focus->recv->{container}->{name}, 'Window 1', 'Window 1 focused'); +sub focus_subtest { + my ($cmd, $name) = @_; -$focus = AnyEvent->condvar; -cmd 'focus left'; -$event = $focus->recv; -is($event->{change}, 'focus', 'Focus event received'); -is($event->{container}->{name}, 'Window 0', 'Window 0 focused'); + my $focus = AnyEvent->condvar; -$focus = AnyEvent->condvar; -cmd 'focus right'; -$event = $focus->recv; -is($event->{change}, 'focus', 'Focus event received'); -is($event->{container}->{name}, 'Window 1', 'Window 1 focused'); - -$focus = AnyEvent->condvar; -cmd 'focus right'; -$event = $focus->recv; -is($event->{change}, 'focus', 'Focus event received'); -is($event->{container}->{name}, 'Window 2', 'Window 2 focused'); - -$focus = AnyEvent->condvar; -cmd 'focus right'; -$event = $focus->recv; -is($event->{change}, 'focus', 'Focus event received'); -is($event->{container}->{name}, 'Window 0', 'Window 0 focused'); - -$focus = AnyEvent->condvar; -cmd 'focus left'; -$event = $focus->recv; -is($event->{change}, 'focus', 'Focus event received'); -is($event->{container}->{name}, 'Window 2', 'Window 2 focused'); + my @events = events_for( + sub { cmd $cmd }, + 'window'); + is(scalar @events, 1, 'Received 1 event'); + is($events[0]->{change}, 'focus', 'Focus event received'); + is($events[0]->{container}->{name}, $name, "$name focused"); } +subtest 'focus left (1)', \&focus_subtest, 'focus left', $win1->name; +subtest 'focus left (2)', \&focus_subtest, 'focus left', $win0->name; +subtest 'focus right (1)', \&focus_subtest, 'focus right', $win1->name; +subtest 'focus right (2)', \&focus_subtest, 'focus right', $win2->name; +subtest 'focus right (3)', \&focus_subtest, 'focus right', $win0->name; +subtest 'focus left', \&focus_subtest, 'focus left', $win2->name; + done_testing; diff --git a/testcases/t/220-ipc-window-title.t b/testcases/t/220-ipc-window-title.t index c751350a..b5d14e23 100644 --- a/testcases/t/220-ipc-window-title.t +++ b/testcases/t/220-ipc-window-title.t @@ -16,42 +16,17 @@ use i3test; -SKIP: { - - skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15; - -my $i3 = i3(get_socket_path()); -$i3->connect()->recv; - -################################ -# Window title event -################################ - my $window = open_window(name => 'Window 0'); -my $title = AnyEvent->condvar; +my @events = events_for( + sub { + $window->name('New Window Title'); + sync_with_i3; + }, + 'window'); -$i3->subscribe({ - window => sub { - my ($event) = @_; - $title->send($event); - } -})->recv; - -$window->name('New Window Title'); - -my $t; -$t = AnyEvent->timer( - after => 0.5, - cb => sub { - $title->send(0); - } -); - -my $event = $title->recv; -is($event->{change}, 'title', 'Window title change event received'); -is($event->{container}->{name}, 'New Window Title', 'Window title changed'); - -} +is(scalar @events, 1, 'Received 1 event'); +is($events[0]->{change}, 'title', 'Window title change event received'); +is($events[0]->{container}->{name}, 'New Window Title', 'Window title changed'); done_testing; diff --git a/testcases/t/225-ipc-window-fullscreen.t b/testcases/t/225-ipc-window-fullscreen.t index aeabe953..bc150546 100644 --- a/testcases/t/225-ipc-window-fullscreen.t +++ b/testcases/t/225-ipc-window-fullscreen.t @@ -19,41 +19,19 @@ # Bug still in: 4.7.2-135-g7deb23c use i3test; -my $i3 = i3(get_socket_path()); -$i3->connect()->recv; +open_window; -my $cv; -my $t; +sub fullscreen_subtest { + my ($want) = @_; + my @events = events_for( + sub { cmd 'fullscreen' }, + 'window'); -sub reset_test { - $cv = AE::cv; - $t = AE::timer(0.5, 0, sub { $cv->send(0); }); + is(scalar @events, 1, 'Received 1 event'); + is($events[0]->{container}->{fullscreen_mode}, $want, "fullscreen_mode now $want"); } -reset_test; - -$i3->subscribe({ - window => sub { - my ($e) = @_; - if ($e->{change} eq 'fullscreen_mode') { - $cv->send($e->{container}); - } - }, - })->recv; - -my $window = open_window; - -cmd 'fullscreen'; -my $con = $cv->recv; - -ok($con, 'got fullscreen window event (on)'); -is($con->{fullscreen_mode}, 1, 'window is fullscreen'); - -reset_test; -cmd 'fullscreen'; -$con = $cv->recv; - -ok($con, 'got fullscreen window event (off)'); -is($con->{fullscreen_mode}, 0, 'window is not fullscreen'); +subtest 'fullscreen on', \&fullscreen_subtest, 1; +subtest 'fullscreen off', \&fullscreen_subtest, 0; done_testing; diff --git a/testcases/t/227-ipc-workspace-empty.t b/testcases/t/227-ipc-workspace-empty.t index fe8e03c0..b1f517ef 100644 --- a/testcases/t/227-ipc-workspace-empty.t +++ b/testcases/t/227-ipc-workspace-empty.t @@ -18,12 +18,8 @@ # use i3test; -SKIP: { - - skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15; - ################################################################################ -# check that the workspace empty event is send upon workspace switch when the +# check that the workspace empty event is sent upon workspace switch when the # old workspace is empty ################################################################################ subtest 'Workspace empty event upon switch', sub { @@ -35,26 +31,17 @@ subtest 'Workspace empty event upon switch', sub { cmd '[id="' . $w1->id . '"] kill'; my $cond = AnyEvent->condvar; - my $client = i3(get_socket_path(0)); - $client->connect()->recv; - $client->subscribe({ - workspace => sub { - my ($event) = @_; - $cond->send($event); - } - })->recv; + my @events = events_for( + sub { cmd "workspace $ws2" }, + 'workspace'); - cmd "workspace $ws2"; - - sync_with_i3; - - my $event = $cond->recv; - is($event->{change}, 'empty', '"Empty" event received upon workspace switch'); - is($event->{current}->{name}, $ws1, '"current" property should be set to the workspace con'); + is(scalar @events, 2, 'Received 2 event'); + is($events[1]->{change}, 'empty', '"Empty" event received upon workspace switch'); + is($events[1]->{current}->{name}, $ws1, '"current" property should be set to the workspace con'); }; ################################################################################ -# check that no workspace empty event is send upon workspace switch if the +# check that no workspace empty event is sent upon workspace switch if the # workspace is not empty ################################################################################ subtest 'No workspace empty event', sub { @@ -63,36 +50,16 @@ subtest 'No workspace empty event', sub { my $ws1 = fresh_workspace; my $w1 = open_window(); - my @events; - my $cond = AnyEvent->condvar; - my $client = i3(get_socket_path(0)); - $client->connect()->recv; - $client->subscribe({ - workspace => sub { - my ($event) = @_; - push @events, $event; - } - })->recv; + my @events = events_for( + sub { cmd "workspace $ws2" }, + 'workspace'); - # Wait for the workspace event on a new connection. Events will be delivered - # to older connections earlier, so by the time it arrives here, it should be - # in @events already. - my $ws_event_block_conn = i3(get_socket_path(0)); - $ws_event_block_conn->connect()->recv; - $ws_event_block_conn->subscribe({ workspace => sub { $cond->send(1) }}); - - cmd "workspace $ws2"; - - sync_with_i3; - - my @expected_events = grep { $_->{change} eq 'focus' } @events; - my @empty_events = grep { $_->{change} eq 'empty' } @events; - is(@expected_events, 1, '"Focus" event received'); - is(@empty_events, 0, 'No "empty" events received'); + is(scalar @events, 1, 'Received 1 event'); + is($events[0]->{change}, 'focus', 'Event change is "focus"'); }; ################################################################################ -# check that workspace empty event is send when the last window has been closed +# check that workspace empty event is sent when the last window has been closed # on invisible workspace ################################################################################ subtest 'Workspace empty event upon window close', sub { @@ -101,25 +68,16 @@ subtest 'Workspace empty event upon window close', sub { my $ws2 = fresh_workspace; my $w2 = open_window(); - my $cond = AnyEvent->condvar; - my $client = i3(get_socket_path(0)); - $client->connect()->recv; - $client->subscribe({ - workspace => sub { - my ($event) = @_; - $cond->send($event); - } - })->recv; + my @events = events_for( + sub { + $w1->unmap; + sync_with_i3; + }, + 'workspace'); - cmd '[id="' . $w1->id . '"] kill'; - - sync_with_i3; - - my $event = $cond->recv; - is($event->{change}, 'empty', '"Empty" event received upon window close'); - is($event->{current}->{name}, $ws1, '"current" property should be set to the workspace con'); + is(scalar @events, 1, 'Received 1 event'); + is($events[0]->{change}, 'empty', '"Empty" event received upon window close'); + is($events[0]->{current}->{name}, $ws1, '"current" property should be set to the workspace con'); }; -} - done_testing; diff --git a/testcases/t/231-ipc-floating-event.t b/testcases/t/231-ipc-floating-event.t index e38a1876..96c94a49 100644 --- a/testcases/t/231-ipc-floating-event.t +++ b/testcases/t/231-ipc-floating-event.t @@ -19,41 +19,22 @@ # Bug still in: 4.8-7-gf4a8253 use i3test; -my $i3 = i3(get_socket_path()); -$i3->connect->recv; +sub floating_subtest { + my ($win, $cmd, $want) = @_; -my $cv = AnyEvent->condvar; + my @events = events_for( + sub { cmd $cmd }, + 'window'); -$i3->subscribe({ - window => sub { - my ($event) = @_; - $cv->send($event) if $event->{change} eq 'floating'; - } - })->recv; - -my $t; -$t = AnyEvent->timer( - after => 0.5, - cb => sub { - $cv->send(0); - } -); + my @floating = grep { $_->{change} eq 'floating' } @events; + is(scalar @floating, 1, 'Received 1 floating event'); + is($floating[0]->{container}->{window}, $win->{id}, "window id matches"); + is($floating[0]->{container}->{floating}, $want, "floating is $want"); +} my $win = open_window(); -cmd '[id="' . $win->{id} . '"] floating enable'; -my $e = $cv->recv; - -isnt($e, 0, 'floating a container should send an ipc window event'); -is($e->{container}->{window}, $win->{id}, 'the event should contain information about the window'); -is($e->{container}->{floating}, 'user_on', 'the container should be floating'); - -$cv = AnyEvent->condvar; -cmd '[id="' . $win->{id} . '"] floating disable'; -$e = $cv->recv; - -isnt($e, 0, 'disabling floating on a container should send an ipc window event'); -is($e->{container}->{window}, $win->{id}, 'the event should contain information about the window'); -is($e->{container}->{floating}, 'user_off', 'the container should not be floating'); +subtest 'floating enable', \&floating_subtest, $win, '[id="' . $win->{id} . '"] floating enable', 'user_on'; +subtest 'floating disable', \&floating_subtest, $win, '[id="' . $win->{id} . '"] floating disable', 'user_off'; done_testing; diff --git a/testcases/t/238-ipc-binding-event.t b/testcases/t/238-ipc-binding-event.t index af3f4d2f..bec95a23 100644 --- a/testcases/t/238-ipc-binding-event.t +++ b/testcases/t/238-ipc-binding-event.t @@ -35,51 +35,38 @@ SKIP: { skip 'xdotool is required to test the binding event. `[apt-get install|pacman -S] xdotool`', 1 if $?; - skip "AnyEvent::I3 too old (need >= 0.16)", 1 if $AnyEvent::I3::VERSION < 0.16; - my $pid = launch_with_config($config); - my $i3 = i3(get_socket_path()); - $i3->connect->recv; + my $cv = AnyEvent->condvar; - my $cv = AE::cv; - my $timer = AE::timer 0.5, 0, sub { $cv->send(0); }; + my @events = events_for( + sub { + # TODO: this is still flaky: we need to synchronize every X11 + # connection with i3. Move to XTEST and synchronize that connection. + qx(xdotool key $binding_symbol); + }, + 'binding'); - $i3->subscribe({ - binding => sub { - $cv->send(shift); - } - })->recv; + is(scalar @events, 1, 'Received 1 event'); - qx(xdotool key $binding_symbol); - - my $e = $cv->recv; - - does_i3_live; - - diag "Event:\n", Dumper($e); - - ok($e, - 'the binding event should emit when user input triggers an i3 binding event'); - - is($e->{change}, 'run', + is($events[0]->{change}, 'run', 'the `change` field should indicate this binding has run'); - ok($e->{binding}, + ok($events[0]->{binding}, 'the `binding` field should be a hash that contains information about the binding'); - is($e->{binding}->{input_type}, 'keyboard', + is($events[0]->{binding}->{input_type}, 'keyboard', 'the input_type field should be the input type of the binding (keyboard or mouse)'); note 'the `mods` field should contain the symbols for the modifiers of the binding'; foreach (@mods) { - ok(grep(/$_/i, @{$e->{binding}->{mods}}), "`mods` contains the modifier $_"); + ok(grep(/$_/i, @{$events[0]->{binding}->{mods}}), "`mods` contains the modifier $_"); } - is($e->{binding}->{command}, $command, + is($events[0]->{binding}->{command}, $command, 'the `command` field should contain the command the binding ran'); - is($e->{binding}->{input_code}, 0, + is($events[0]->{binding}->{input_code}, 0, 'the input_code should be the specified code if the key was bound with bindcode, and otherwise zero'); exit_gracefully($pid); diff --git a/testcases/t/240-focus-on-window-activation.t b/testcases/t/240-focus-on-window-activation.t index cd3989c4..57060753 100644 --- a/testcases/t/240-focus-on-window-activation.t +++ b/testcases/t/240-focus-on-window-activation.t @@ -111,7 +111,7 @@ EOT $pid = launch_with_config($config); -my $ws = fresh_workspace; +$ws = fresh_workspace; $first = open_window; $second = open_window; @@ -165,7 +165,7 @@ EOT $pid = launch_with_config($config); -my $ws = fresh_workspace; +$ws = fresh_workspace; $first = open_window; $second = open_window; diff --git a/testcases/t/252-floating-size.t b/testcases/t/252-floating-size.t index 8d8d4120..2c8edf39 100644 --- a/testcases/t/252-floating-size.t +++ b/testcases/t/252-floating-size.t @@ -17,7 +17,13 @@ # Test behavior of "resize " command. # Ticket: #1727 # Bug still in: 4.10.2-1-gc0dbc5d -use i3test; +use i3test i3_config => <{rect}->{height}, '!=', $oldrect->{width}, 'height changed') cmp_ok($content[0]->{rect}->{width}, '==', 100, 'width changed to 100 px'); cmp_ok($content[0]->{rect}->{height}, '==', 250, 'height changed to 250 px'); +################################################################################ +# Same but with ppt instead of px +################################################################################ + +kill_all_windows; +$tmp = 'ws'; +cmd "workspace $tmp"; +open_floating_window; + +@content = @{get_ws($tmp)->{floating_nodes}}; +is(@content, 1, 'one floating node on this ws'); + +$oldrect = $content[0]->{rect}; + +cmd 'resize set 33 ppt 20 ppt'; +my $expected_width = int(0.33 * 1333); +my $expected_height = int(0.2 * 999); + +@content = @{get_ws($tmp)->{floating_nodes}}; +cmp_ok($content[0]->{rect}->{x}, '==', $oldrect->{x}, 'x untouched'); +cmp_ok($content[0]->{rect}->{y}, '==', $oldrect->{y}, 'y untouched'); +cmp_ok($content[0]->{rect}->{width}, '!=', $oldrect->{width}, 'width changed'); +cmp_ok($content[0]->{rect}->{height}, '!=', $oldrect->{width}, 'height changed'); +cmp_ok($content[0]->{rect}->{width}, '==', $expected_width, "width changed to $expected_width px"); +cmp_ok($content[0]->{rect}->{height}, '==', $expected_height, "height changed to $expected_height px"); + +################################################################################ +# Mix ppt and px in a single resize set command +################################################################################ + +cmd 'resize set 44 ppt 111 px'; +$expected_width = int(0.44 * 1333); +$expected_height = 111; + +@content = @{get_ws($tmp)->{floating_nodes}}; +cmp_ok($content[0]->{rect}->{x}, '==', $oldrect->{x}, 'x untouched'); +cmp_ok($content[0]->{rect}->{y}, '==', $oldrect->{y}, 'y untouched'); +cmp_ok($content[0]->{rect}->{width}, '==', $expected_width, "width changed to $expected_width px"); +cmp_ok($content[0]->{rect}->{height}, '==', $expected_height, "height changed to $expected_height px"); + +cmd 'resize set 222 px 100 ppt'; +$expected_width = 222; +$expected_height = 999; + +@content = @{get_ws($tmp)->{floating_nodes}}; +cmp_ok($content[0]->{rect}->{x}, '==', $oldrect->{x}, 'x untouched'); +cmp_ok($content[0]->{rect}->{y}, '==', $oldrect->{y}, 'y untouched'); +cmp_ok($content[0]->{rect}->{width}, '==', $expected_width, "width changed to $expected_width px"); +cmp_ok($content[0]->{rect}->{height}, '==', $expected_height, "height changed to $expected_height px"); + done_testing; diff --git a/testcases/t/257-keypress-group1-fallback.t b/testcases/t/257-keypress-group1-fallback.t index f9166fad..bc08aa2f 100644 --- a/testcases/t/257-keypress-group1-fallback.t +++ b/testcases/t/257-keypress-group1-fallback.t @@ -36,14 +36,13 @@ SKIP: { skip "setxkbmap not found", 1 if system(q|setxkbmap -print >/dev/null|) != 0; -start_binding_capture; - system(q|setxkbmap us,ru -option grp:alt_shift_toggle|); is(listen_for_binding( sub { xtest_key_press(107); xtest_key_release(107); + xtest_sync_with_i3; }, ), 'Print', @@ -55,6 +54,7 @@ is(listen_for_binding( xtest_key_press(36); # Return xtest_key_release(36); # Return xtest_key_release(133); # Super_L + xtest_sync_with_i3; }, ), 'Mod4+Return', @@ -67,6 +67,7 @@ is(listen_for_binding( sub { xtest_key_press(107); xtest_key_release(107); + xtest_sync_with_i3; }, ), 'Print', @@ -78,14 +79,12 @@ is(listen_for_binding( xtest_key_press(36); # Return xtest_key_release(36); # Return xtest_key_release(133); # Super_L + xtest_sync_with_i3; }, ), 'Mod4+Return', 'triggered the "Mod4+Return" keybinding'); -sync_with_i3; -is(scalar @i3test::XTEST::binding_events, 4, 'Received exactly 4 binding events'); - # Disable the grp:alt_shift_toggle option, as we use Alt+Shift in other testcases. system(q|setxkbmap us -option|); diff --git a/testcases/t/258-keypress-release.t b/testcases/t/258-keypress-release.t index 72bfc862..8bca0d86 100644 --- a/testcases/t/258-keypress-release.t +++ b/testcases/t/258-keypress-release.t @@ -37,12 +37,11 @@ SKIP: { skip "libxcb-xkb too old (need >= 1.11)", 1 unless ExtUtils::PkgConfig->atleast_version('xcb-xkb', '1.11'); -start_binding_capture; - is(listen_for_binding( sub { xtest_key_press(107); # Print xtest_key_release(107); # Print + xtest_sync_with_i3; }, ), 'Print', @@ -54,6 +53,7 @@ is(listen_for_binding( xtest_key_press(107); # Print xtest_key_release(107); # Print xtest_key_release(37); # Control_L + xtest_sync_with_i3; }, ), 'Control+Print', @@ -65,6 +65,7 @@ is(listen_for_binding( xtest_key_press(56); # b xtest_key_release(56); # b xtest_key_release(64); # Alt_L + xtest_sync_with_i3; }, ), 'Mod1+b', @@ -78,14 +79,12 @@ is(listen_for_binding( xtest_key_release(56); # b xtest_key_release(50); # Shift_L xtest_key_release(64); # Alt_L + xtest_sync_with_i3; }, ), 'Mod1+Shift+b release', 'triggered the "Mod1+Shift+b" release keybinding'); -sync_with_i3; -is(scalar @i3test::XTEST::binding_events, 4, 'Received exactly 4 binding events'); - } done_testing; diff --git a/testcases/t/263-config-reload-reverts-bind-mode.t b/testcases/t/263-config-reload-reverts-bind-mode.t index ba95897c..1416b6b9 100644 --- a/testcases/t/263-config-reload-reverts-bind-mode.t +++ b/testcases/t/263-config-reload-reverts-bind-mode.t @@ -28,23 +28,11 @@ EOT cmd 'mode othermode'; -my $i3 = i3(get_socket_path(0)); -$i3->connect->recv; +my @events = events_for( + sub { cmd 'reload' }, + 'mode'); -my $cv = AnyEvent->condvar; -$i3->subscribe({ - mode => sub { - my ($event) = @_; - $cv->send($event->{change} eq 'default'); - } -})->recv; - -cmd 'reload'; - -# Timeout after 0.5s -my $t; -$t = AnyEvent->timer(after => 0.5, cb => sub { $cv->send(0); }); - -ok($cv->recv, 'Mode event received'); +is(scalar @events, 1, 'Received 1 event'); +is($events[0]->{change}, 'default', 'change is "default"'); done_testing; diff --git a/testcases/t/265-ipc-mark.t b/testcases/t/265-ipc-mark.t index 06d8d83d..a101944e 100644 --- a/testcases/t/265-ipc-mark.t +++ b/testcases/t/265-ipc-mark.t @@ -18,27 +18,16 @@ # Ticket: #2501 use i3test; -my ($i3, $timer, $event, $mark); +sub mark_subtest { + my ($cmd) = @_; -$i3 = i3(get_socket_path()); -$i3->connect()->recv; + my @events = events_for( + sub { cmd $cmd }, + 'window'); -$i3->subscribe({ - window => sub { - my ($event) = @_; - return unless defined $mark; - return unless $event->{change} eq 'mark'; - - $mark->send($event); - } -})->recv; - -$timer = AnyEvent->timer( - after => 0.5, - cb => sub { - $mark->send(0); - } -); + my @mark = grep { $_->{change} eq 'mark' } @events; + is(scalar @mark, 1, 'Received 1 window::mark event'); +} ############################################################################### # Marking a container triggers a 'mark' event. @@ -46,11 +35,7 @@ $timer = AnyEvent->timer( fresh_workspace; open_window; -$mark = AnyEvent->condvar; -cmd 'mark x'; - -$event = $mark->recv; -ok($event, 'window::mark event has been received'); +subtest 'mark', \&mark_subtest, 'mark x'; ############################################################################### # Unmarking a container triggers a 'mark' event. @@ -59,11 +44,7 @@ fresh_workspace; open_window; cmd 'mark x'; -$mark = AnyEvent->condvar; -cmd 'unmark x'; - -$event = $mark->recv; -ok($event, 'window::mark event has been received'); +subtest 'unmark', \&mark_subtest, 'unmark x'; ############################################################################### diff --git a/testcases/t/268-ipc-config.t b/testcases/t/268-ipc-config.t index b21ff1b2..9ac749b6 100644 --- a/testcases/t/268-ipc-config.t +++ b/testcases/t/268-ipc-config.t @@ -42,8 +42,8 @@ get_socket_path(0); my $i3 = i3(get_socket_path()); $i3->connect->recv; -my $cv = AE::cv; -my $timer = AE::timer 0.5, 0, sub { $cv->send(0); }; +my $cv = AnyEvent->condvar; +my $timer = AnyEvent->timer(after => 0.5, interval => 0, cb => sub { $cv->send(0); }); my $last_config = $i3->get_config()->recv; chomp($last_config->{config}); diff --git a/testcases/t/275-ipc-window-close.t b/testcases/t/275-ipc-window-close.t index bf9d4f8b..ccaf4440 100644 --- a/testcases/t/275-ipc-window-close.t +++ b/testcases/t/275-ipc-window-close.t @@ -19,34 +19,17 @@ # Bug still in: 4.8-7-gf4a8253 use i3test; -my $i3 = i3(get_socket_path()); -$i3->connect()->recv; - -my $cv; -my $t; - -sub reset_test { - $cv = AE::cv; - $t = AE::timer(0.5, 0, sub { $cv->send(0); }); -} - -reset_test; - -$i3->subscribe({ - window => sub { - my ($e) = @_; - if ($e->{change} eq 'close') { - $cv->send($e->{container}); - } - }, - })->recv; - my $window = open_window; -cmd 'kill'; -my $con = $cv->recv; +my @events = events_for( + sub { + $window->unmap; + sync_with_i3; + }, + 'window'); -ok($con, 'closing a window should send the window::close event'); -is($con->{window}, $window->{id}, 'the event should contain information about the window'); +my @close = grep { $_->{change} eq 'close' } @events; +is(scalar @close, 1, 'Received 1 window::close event'); +is($close[0]->{container}->{window}, $window->{id}, 'the event should contain information about the window'); done_testing; diff --git a/testcases/t/276-ipc-window-move.t b/testcases/t/276-ipc-window-move.t index a3c82534..f3606b4e 100644 --- a/testcases/t/276-ipc-window-move.t +++ b/testcases/t/276-ipc-window-move.t @@ -19,43 +19,22 @@ # Bug still in: 4.8-7-gf4a8253 use i3test; -my $i3 = i3(get_socket_path()); -$i3->connect()->recv; - -my $cv; -my $t; - -sub reset_test { - $cv = AE::cv; - $t = AE::timer(0.5, 0, sub { $cv->send(0); }); -} - -reset_test; - -$i3->subscribe({ - window => sub { - my ($e) = @_; - if ($e->{change} eq 'move') { - $cv->send($e->{container}); - } - }, - })->recv; - my $dummy_window = open_window; my $window = open_window; -cmd 'move right'; -my $con = $cv->recv; +sub move_subtest { + my ($cmd) = @_; + my $cv = AnyEvent->condvar; + my @events = events_for( + sub { cmd $cmd }, + 'window'); -ok($con, 'moving a window should emit the window::move event'); -is($con->{window}, $window->{id}, 'the event should contain info about the window'); + my @move = grep { $_->{change} eq 'move' } @events; + is(scalar @move, 1, 'Received 1 window::move event'); + is($move[0]->{container}->{window}, $window->{id}, 'window id matches'); +} -reset_test; - -cmd 'move to workspace ws_new'; -$con = $cv->recv; - -ok($con, 'moving a window to a different workspace should emit the window::move event'); -is($con->{window}, $window->{id}, 'the event should contain info about the window'); +subtest 'move right', \&move_subtest, 'move right'; +subtest 'move to workspace', \&move_subtest, 'move to workspace ws_new'; done_testing; diff --git a/testcases/t/277-ipc-window-urgent.t b/testcases/t/277-ipc-window-urgent.t index 2af29dac..4eea2cdc 100644 --- a/testcases/t/277-ipc-window-urgent.t +++ b/testcases/t/277-ipc-window-urgent.t @@ -19,50 +19,37 @@ # use i3test; -my $config = <connect()->recv; - -my $cv; -$i3->subscribe({ - window => sub { - my ($event) = @_; - $cv->send($event) if $event->{change} eq 'urgent'; - } -})->recv; - -my $t; -$t = AnyEvent->timer( - after => 0.5, - cb => sub { - $cv->send(0); - } -); - -$cv = AnyEvent->condvar; fresh_workspace; my $win = open_window; my $dummy_win = open_window; -$win->add_hint('urgency'); -my $event = $cv->recv; +sub urgency_subtest { + my ($subscribecb, $win, $want) = @_; -isnt($event, 0, 'an urgent con should emit the window::urgent event'); -is($event->{container}->{window}, $win->{id}, 'the event should contain information about the window'); -is($event->{container}->{urgent}, 1, 'the container should be urgent'); + my @events = events_for( + $subscribecb, + 'window'); -$cv = AnyEvent->condvar; -$win->delete_hint('urgency'); -$event = $cv->recv; + my @urgent = grep { $_->{change} eq 'urgent' } @events; + is(scalar @urgent, 1, 'Received 1 window::urgent event'); + is($urgent[0]->{container}->{window}, $win->{id}, "window id matches"); + is($urgent[0]->{container}->{urgent}, $want, "urgent is $want"); +} -isnt($event, 0, 'an urgent con should emit the window::urgent event'); -is($event->{container}->{window}, $win->{id}, 'the event should contain information about the window'); -is($event->{container}->{urgent}, 0, 'the container should not be urgent'); +subtest "urgency set", \&urgency_subtest, + sub { + $win->add_hint('urgency'); + sync_with_i3; + }, + $win, + 1; + +subtest "urgency unset", \&urgency_subtest, + sub { + $win->delete_hint('urgency'); + sync_with_i3; + }, + $win, + 0; done_testing; diff --git a/testcases/t/286-root-window-mouse-binding.t b/testcases/t/286-root-window-mouse-binding.t index 6c1b5d6a..0da373b7 100644 --- a/testcases/t/286-root-window-mouse-binding.t +++ b/testcases/t/286-root-window-mouse-binding.t @@ -30,7 +30,7 @@ fresh_workspace; xtest_button_press(4, 50, 50); xtest_button_release(4, 50, 50); -sync_with_i3; +xtest_sync_with_i3; is(focused_ws(), 'special', 'the binding was triggered'); diff --git a/testcases/t/289-ipc-shutdown-event.t b/testcases/t/289-ipc-shutdown-event.t index 0cf347aa..606474e2 100644 --- a/testcases/t/289-ipc-shutdown-event.t +++ b/testcases/t/289-ipc-shutdown-event.t @@ -23,14 +23,14 @@ # Bug still in: 4.12-46-g2123888 use i3test; -SKIP: { - skip "AnyEvent::I3 too old (need >= 0.17)", 1 if $AnyEvent::I3::VERSION < 0.17; +# We cannot use events_for in this test as we cannot send events after +# issuing the restart/shutdown command. my $i3 = i3(get_socket_path()); $i3->connect->recv; -my $cv = AE::cv; -my $timer = AE::timer 0.5, 0, sub { $cv->send(0); }; +my $cv = AnyEvent->condvar; +my $timer = AnyEvent->timer(after => 0.5, interval => 0, cb => sub { $cv->send(0); }); $i3->subscribe({ shutdown => sub { @@ -50,8 +50,8 @@ is($e->{change}, 'restart', 'the `change` field should tell the reason for the s $i3 = i3(get_socket_path()); $i3->connect->recv; -$cv = AE::cv; -$timer = AE::timer 0.5, 0, sub { $cv->send(0); }; +$cv = AnyEvent->condvar; +$timer = AnyEvent->timer(after => 0.5, interval => 0, cb => sub { $cv->send(0); }); $i3->subscribe({ shutdown => sub { @@ -66,6 +66,5 @@ $e = $cv->recv; diag "Event:\n", Dumper($e); ok($e, 'the shutdown event should emit when the ipc is exited by command'); is($e->{change}, 'exit', 'the `change` field should tell the reason for the shutdown'); -} done_testing; diff --git a/testcases/t/290-keypress-numlock.t b/testcases/t/290-keypress-numlock.t index 5137c35f..94a5747d 100644 --- a/testcases/t/290-keypress-numlock.t +++ b/testcases/t/290-keypress-numlock.t @@ -51,12 +51,11 @@ EOT my $pid = launch_with_config($config); -start_binding_capture; - is(listen_for_binding( sub { xtest_key_press(87); # KP_End xtest_key_release(87); # KP_End + xtest_sync_with_i3; }, ), 'KP_End', @@ -70,6 +69,7 @@ is(listen_for_binding( xtest_key_release(87); # KP_1 xtest_key_press(77); # disable Num_Lock xtest_key_release(77); # disable Num_Lock + xtest_sync_with_i3; }, ), 'KP_1', @@ -81,6 +81,7 @@ is(listen_for_binding( xtest_key_press(38); # a xtest_key_release(38); # a xtest_key_release(133); # Super_L + xtest_sync_with_i3; }, ), 'a', @@ -96,6 +97,7 @@ is(listen_for_binding( xtest_key_release(133); # Super_L xtest_key_press(77); # disable Num_Lock xtest_key_release(77); # disable Num_Lock + xtest_sync_with_i3; }, ), 'a', @@ -105,6 +107,7 @@ is(listen_for_binding( sub { xtest_key_press(9); # Escape xtest_key_release(9); # Escape + xtest_sync_with_i3; }, ), 'Escape', @@ -118,6 +121,7 @@ is(listen_for_binding( xtest_key_release(9); # Escape xtest_key_press(77); # disable Num_Lock xtest_key_release(77); # disable Num_Lock + xtest_sync_with_i3; }, ), 'Escape', @@ -129,6 +133,7 @@ is(listen_for_binding( xtest_key_press(9); # Escape xtest_key_release(9); # Escape xtest_key_release(50); # Shift_L + xtest_sync_with_i3; }, ), 'Shift+Escape', @@ -144,6 +149,7 @@ is(listen_for_binding( xtest_key_release(50); # Shift_L xtest_key_press(77); # disable Num_Lock xtest_key_release(77); # disable Num_Lock + xtest_sync_with_i3; }, ), 'Shift+Escape', @@ -157,6 +163,7 @@ is(listen_for_binding( xtest_key_release(24); # q xtest_key_release(64); # Alt_L xtest_key_release(50); # Shift_L + xtest_sync_with_i3; }, ), 'Mod1+Shift+q', @@ -174,6 +181,7 @@ is(listen_for_binding( xtest_key_release(50); # Shift_L xtest_key_press(77); # disable Num_Lock xtest_key_release(77); # disable Num_Lock + xtest_sync_with_i3; }, ), 'Mod1+Shift+q', @@ -183,6 +191,7 @@ is(listen_for_binding( sub { xtest_key_press(39); # s xtest_key_release(39); # s + xtest_sync_with_i3; }, ), 's', @@ -196,14 +205,12 @@ is(listen_for_binding( xtest_key_release(39); # s xtest_key_press(77); # disable Num_Lock xtest_key_release(77); # disable Num_Lock + xtest_sync_with_i3; }, ), 's', 'triggered the "s" keybinding with Num_Lock'); -sync_with_i3; -is(scalar @i3test::XTEST::binding_events, 12, 'Received exactly 12 binding events'); - exit_gracefully($pid); ################################################################################ @@ -222,12 +229,11 @@ EOT $pid = launch_with_config($config); -start_binding_capture; - is(listen_for_binding( sub { xtest_key_press(133); # Super_L xtest_key_release(133); # Super_L + xtest_sync_with_i3; }, ), 'Super_L', @@ -241,6 +247,7 @@ is(listen_for_binding( xtest_key_release(133); # Super_L xtest_key_press(77); # disable Num_Lock xtest_key_release(77); # disable Num_Lock + xtest_sync_with_i3; }, ), 'Super_L', @@ -252,6 +259,7 @@ is(listen_for_binding( xtest_key_press(36); # Return xtest_key_release(36); # Return xtest_key_release(133); # Super_L + xtest_sync_with_i3; }, ), 'Return', @@ -267,14 +275,12 @@ is(listen_for_binding( xtest_key_release(133); # Super_L xtest_key_press(77); # disable Num_Lock xtest_key_release(77); # disable Num_Lock + xtest_sync_with_i3; }, ), 'Return', 'triggered the "Return" keybinding with Num_Lock'); -sync_with_i3; -is(scalar @i3test::XTEST::binding_events, 16, 'Received exactly 16 binding events'); - exit_gracefully($pid); ################################################################################ @@ -291,12 +297,11 @@ EOT $pid = launch_with_config($config); -start_binding_capture; - is(listen_for_binding( sub { xtest_key_press(87); # KP_End xtest_key_release(87); # KP_End + xtest_sync_with_i3; }, ), 'KP_End', @@ -306,12 +311,13 @@ is(listen_for_binding( sub { xtest_key_press(88); # KP_Down xtest_key_release(88); # KP_Down + xtest_sync_with_i3; }, ), 'KP_Down', 'triggered the "KP_Down" keybinding'); -is(listen_for_binding( +my @unexpected = events_for( sub { xtest_key_press(77); # enable Num_Lock xtest_key_release(77); # enable Num_Lock @@ -319,12 +325,12 @@ is(listen_for_binding( xtest_key_release(87); # KP_1 xtest_key_press(77); # disable Num_Lock xtest_key_release(77); # disable Num_Lock + xtest_sync_with_i3; }, - ), - 'timeout', - 'Did not trigger the KP_End keybinding with KP_1'); + 'binding'); +is(scalar @unexpected, 0, 'Did not trigger the KP_End keybinding with KP_1'); -is(listen_for_binding( +my @unexpected2 = events_for( sub { xtest_key_press(77); # enable Num_Lock xtest_key_release(77); # enable Num_Lock @@ -332,16 +338,14 @@ is(listen_for_binding( xtest_key_release(88); # KP_2 xtest_key_press(77); # disable Num_Lock xtest_key_release(77); # disable Num_Lock + xtest_sync_with_i3; }, - ), - 'timeout', - 'Did not trigger the KP_Down keybinding with KP_2'); + 'binding'); + +is(scalar @unexpected2, 0, 'Did not trigger the KP_Down keybinding with KP_2'); # TODO: This test does not verify that i3 does _NOT_ grab keycode 87 with Mod2. -sync_with_i3; -is(scalar @i3test::XTEST::binding_events, 18, 'Received exactly 18 binding events'); - exit_gracefully($pid); ################################################################################ @@ -359,8 +363,6 @@ $pid = launch_with_config($config); my $win = open_window; -start_binding_capture; - is(listen_for_binding( sub { xtest_key_press(77); # enable Num_Lock @@ -369,6 +371,7 @@ is(listen_for_binding( xtest_button_release(4, 50, 50); xtest_key_press(77); # disable Num_Lock xtest_key_release(77); # disable Num_Lock + xtest_sync_with_i3; }, ), 'button4', @@ -376,8 +379,9 @@ is(listen_for_binding( is(listen_for_binding( sub { - xtest_button_press(4, 50, 50); - xtest_button_release(4, 50, 50); + xtest_button_press(4, 50, 50); + xtest_button_release(4, 50, 50); + xtest_sync_with_i3; }, ), 'button4', diff --git a/testcases/t/291-swap.t b/testcases/t/291-swap.t index f04d0f3b..3b61fdab 100644 --- a/testcases/t/291-swap.t +++ b/testcases/t/291-swap.t @@ -25,8 +25,9 @@ for_window[class="mark_B"] mark B EOT my ($ws, $ws1, $ws2, $ws3); -my ($nodes, $expected_focus, $A, $B, $F); +my ($node, $nodes, $expected_focus, $A, $B, $F); my ($result); +my @fullscreen_permutations = ([], ["A"], ["B"], ["A", "B"]); my @urgent; ############################################################################### @@ -162,26 +163,115 @@ kill_all_windows; # | Y | B | Focus Stacks: # +---+---+ H2: B, Y ############################################################################### +for my $fullscreen (@fullscreen_permutations){ + $ws1 = fresh_workspace; + $A = open_window(wm_class => 'mark_A'); + $expected_focus = get_focused($ws1); + open_window; + cmd 'focus left'; + + $ws2 = fresh_workspace; + open_window; + $B = open_window(wm_class => 'mark_B'); + + my $A_fullscreen = "A" ~~ @$fullscreen || 0; + my $B_fullscreen = "B" ~~ @$fullscreen || 0; + $A->fullscreen($A_fullscreen); + $B->fullscreen($B_fullscreen); + sync_with_i3; + + cmd '[con_mark=B] swap container with mark A'; + + $nodes = get_ws_content($ws1); + $node = $nodes->[0]; + is($node->{window}, $B->{id}, 'B is on ws1:left'); + is_num_fullscreen($ws1, $A_fullscreen, 'amount of fullscreen windows in ws1'); + is($node->{fullscreen_mode}, $A_fullscreen, 'B got A\'s fullscreen mode'); + + $nodes = get_ws_content($ws2); + $node = $nodes->[1]; + is($node->{window}, $A->{id}, 'A is on ws2:right'); + is(get_focused($ws2), $expected_focus, 'A is focused'); + is_num_fullscreen($ws2, $B_fullscreen, 'amount of fullscreen windows in ws2'); + is($node->{fullscreen_mode}, $B_fullscreen, 'A got B\'s fullscreen mode'); + + kill_all_windows; +} + +############################################################################### +# Swap a non-fullscreen window with a fullscreen one in different workspaces. +# Layout: O1[ W1[ H1 ] W2[ B ] ] +# +# +---+---+ Layout: H1[ A F ] +# | A | F | Focus Stacks: +# +---+---+ H1: F, A +# +# +---+---+ +# | B | +# +---+---+ +############################################################################### $ws1 = fresh_workspace; + $A = open_window(wm_class => 'mark_A'); +$F = open_window(); +$F->fullscreen(1); $expected_focus = get_focused($ws1); -open_window; -cmd 'focus left'; $ws2 = fresh_workspace; -open_window; $B = open_window(wm_class => 'mark_B'); +$B->fullscreen(1); +sync_with_i3; cmd '[con_mark=B] swap container with mark A'; $nodes = get_ws_content($ws1); is($nodes->[0]->{window}, $B->{id}, 'B is on ws1:left'); +is_num_fullscreen($ws1, 1, 'F still fullscreen in ws1'); +is(get_focused($ws1), $expected_focus, 'F is still focused'); $nodes = get_ws_content($ws2); -is($nodes->[1]->{window}, $A->{id}, 'A is on ws1:right'); -is(get_focused($ws2), $expected_focus, 'A is focused'); +is($nodes->[0]->{window}, $A->{id}, 'A is on ws1'); -kill_all_windows; +############################################################################### +# Try a more exotic layout with fullscreen containers. +# A and F are fullscreened as a stack of two vertical containers before the +# swap is performed. +# A is swapped with fullscreened window B which is in another workspace. +# +# +---+---+ Layout: H1[ X V1[ A F ] ] +# | | A | Focus Stacks: +# | X +---+ H1: V1, X +# | | F | V1: F, A +# +---+---+ +############################################################################### +$ws1 = fresh_workspace; + +open_window; +$A = open_window(wm_class => 'mark_A'); +cmd "split v"; +open_window; +cmd "focus parent"; +cmd "fullscreen enable"; +$expected_focus = get_focused($ws1); + +$ws2 = fresh_workspace; +$B = open_window(wm_class => 'mark_B'); +$B->fullscreen(1); +sync_with_i3; + +cmd '[con_mark=B] swap container with mark A'; + +sync_with_i3; +does_i3_live; + +$nodes = get_ws_content($ws1); +is($nodes->[1]->{nodes}->[0]->{window}, $B->{id}, 'B is on top right in ws1'); +is(get_focused($ws1), $expected_focus, 'The container of the stacked windows remains focused in ws1'); +is_num_fullscreen($ws1, 1, 'Same amount of fullscreen windows in ws1'); + +$nodes = get_ws_content($ws2); +is($nodes->[0]->{window}, $A->{id}, 'A is on ws2'); +is_num_fullscreen($ws2, 1, 'A is in fullscreen mode'); ############################################################################### # Swap two non-focused containers within the same workspace. @@ -232,27 +322,41 @@ kill_all_windows; # | F | # +---+ ############################################################################### -$ws1 = fresh_workspace; -$A = open_window(wm_class => 'mark_A'); +for my $fullscreen (@fullscreen_permutations){ + $ws1 = fresh_workspace; + $A = open_window(wm_class => 'mark_A'); -$ws2 = fresh_workspace; -$B = open_window(wm_class => 'mark_B'); + $ws2 = fresh_workspace; + $B = open_window(wm_class => 'mark_B'); -$ws3 = fresh_workspace; -open_window; -$expected_focus = get_focused($ws3); + $ws3 = fresh_workspace; + open_window; + $expected_focus = get_focused($ws3); -cmd '[con_mark=B] swap container with mark A'; + my $A_fullscreen = "A" ~~ @$fullscreen || 0; + my $B_fullscreen = "B" ~~ @$fullscreen || 0; + $A->fullscreen($A_fullscreen); + $B->fullscreen($B_fullscreen); + sync_with_i3; -$nodes = get_ws_content($ws1); -is($nodes->[0]->{window}, $B->{id}, 'B is on the first workspace'); + cmd '[con_mark=B] swap container with mark A'; -$nodes = get_ws_content($ws2); -is($nodes->[0]->{window}, $A->{id}, 'A is on the second workspace'); + $nodes = get_ws_content($ws1); + $node = $nodes->[0]; + is($node->{window}, $B->{id}, 'B is on the first workspace'); + is_num_fullscreen($ws1, $A_fullscreen, 'amount of fullscreen windows in ws1'); + is($node->{fullscreen_mode}, $A_fullscreen, 'B got A\'s fullscreen mode'); -is(get_focused($ws3), $expected_focus, 'F is still focused'); + $nodes = get_ws_content($ws2); + $node = $nodes->[0]; + is($node->{window}, $A->{id}, 'A is on the second workspace'); + is_num_fullscreen($ws2, $B_fullscreen, 'amount of fullscreen windows in ws2'); + is($node->{fullscreen_mode}, $B_fullscreen, 'A got B\'s fullscreen mode'); -kill_all_windows; + is(get_focused($ws3), $expected_focus, 'F is still focused'); + + kill_all_windows; +} ############################################################################### # Swap two non-focused containers with one being on a different workspace. diff --git a/testcases/t/293-focus-follows-mouse.t b/testcases/t/293-focus-follows-mouse.t new file mode 100644 index 00000000..0cd6e5c3 --- /dev/null +++ b/testcases/t/293-focus-follows-mouse.t @@ -0,0 +1,88 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • https://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • https://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • https://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Tests the focus_follows_mouse setting. +use i3test i3_config => <root->warp_pointer($x_px, $y_px); + sync_with_i3; +} + +################################################################### +# Test a simple case with 2 windows. +################################################################### + +synced_warp_pointer(600, 600); +$first = open_window; +$second = open_window; +is($x->input_focus, $second->id, 'second window focused'); + +synced_warp_pointer(0, 0); +is($x->input_focus, $first->id, 'first window focused'); + +################################################################### +# Test that focus isn't changed with tabbed windows. +################################################################### + +fresh_workspace; +synced_warp_pointer(600, 600); +$first = open_window; +cmd 'layout tabbed'; +$second = open_window; +is($x->input_focus, $second->id, 'second (tabbed) window focused'); + +synced_warp_pointer(0, 0); +is($x->input_focus, $second->id, 'second window still focused'); + +################################################################### +# Test that floating windows are focused but not raised to the top. +# See issue #2990. +################################################################### + +my $ws; +my $tmp = fresh_workspace; +my ($first_floating, $second_floating); + +synced_warp_pointer(0, 0); +$first_floating = open_floating_window; +$first_floating->rect(X11::XCB::Rect->new(x => 1, y => 1, width => 100, height => 100)); +$second_floating = open_floating_window; +$second_floating->rect(X11::XCB::Rect->new(x => 50, y => 50, width => 100, height => 100)); +sync_with_i3; +$first = open_window; + +is($x->input_focus, $first->id, 'first (tiling) window focused'); +$ws = get_ws($tmp); +is($ws->{floating_nodes}->[1]->{nodes}->[0]->{window}, $second_floating->id, 'second floating on top'); +is($ws->{floating_nodes}->[0]->{nodes}->[0]->{window}, $first_floating->id, 'first floating behind'); + +synced_warp_pointer(40, 40); +is($x->input_focus, $first_floating->id, 'first floating window focused'); +$ws = get_ws($tmp); +is($ws->{floating_nodes}->[1]->{nodes}->[0]->{window}, $second_floating->id, 'second floating still on top'); +is($ws->{floating_nodes}->[0]->{nodes}->[0]->{window}, $first_floating->id, 'first floating still behind'); + +done_testing; diff --git a/testcases/t/293-sticky-output-crash.t b/testcases/t/293-sticky-output-crash.t new file mode 100644 index 00000000..93ebaee9 --- /dev/null +++ b/testcases/t/293-sticky-output-crash.t @@ -0,0 +1,41 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • https://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • https://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • https://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Verifies that i3 does not crash when opening a floating sticky on one output +# and then switching empty workspaces on the other output. +# Ticket: #3075 +# Bug still in: 4.14-191-g9d2d602d +use i3test i3_config => < 0); +open_window; +cmd 'sticky enable, floating enable'; + +# Switch to the right output and open a new workspace. +my $ws = fresh_workspace(output => 1); +does_i3_live; + +# Verify results. +is(@{get_ws($ws)->{floating_nodes}}, 0, 'workspace in right output is empty'); +$ws = fresh_workspace(output => 0); +is(@{get_ws($ws)->{floating_nodes}}, 1, 'new workspace in left output has the sticky container'); + +done_testing; diff --git a/testcases/t/294-focus-order.t b/testcases/t/294-focus-order.t new file mode 100644 index 00000000..41c0bf07 --- /dev/null +++ b/testcases/t/294-focus-order.t @@ -0,0 +1,109 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • https://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • https://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • https://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Verify that the corrent focus stack order is preserved after various +# operations. +use i3test; + +sub kill_and_confirm_focus { + my $focus = shift; + my $msg = shift; + cmd "kill"; + sync_with_i3; + is($x->input_focus, $focus, $msg); +} + +my @windows; + +sub focus_windows { + for (my $i = $#windows; $i >= 0; $i--) { + cmd '[id=' . $windows[$i]->id . '] focus'; + } +} + +sub confirm_focus { + my $msg = shift; + sync_with_i3; + is($x->input_focus, $windows[0]->id, $msg . ': window 0 focused'); + foreach my $i (1 .. $#windows) { + kill_and_confirm_focus($windows[$i]->id, "$msg: window $i focused"); + } + cmd 'kill'; + @windows = (); +} + +##################################################################### +# Open 5 windows, focus them in a custom order and then change to +# tabbed layout. The focus order should be preserved. +##################################################################### + +fresh_workspace; + +$windows[3] = open_window; +$windows[1] = open_window; +$windows[0] = open_window; +$windows[2] = open_window; +$windows[4] = open_window; +focus_windows; + +cmd 'layout tabbed'; +confirm_focus('tabbed'); + +##################################################################### +# Same as above but with stacked. +##################################################################### + +fresh_workspace; + +$windows[3] = open_window; +$windows[1] = open_window; +$windows[0] = open_window; +$windows[2] = open_window; +$windows[4] = open_window; +focus_windows; + +cmd 'layout stacked'; +confirm_focus('stacked'); + +##################################################################### +# Open 4 windows horizontally, move the last one down. The focus +# order should be preserved. +##################################################################### + +fresh_workspace; +$windows[3] = open_window; +$windows[2] = open_window; +$windows[1] = open_window; +$windows[0] = open_window; + +cmd 'move down'; +confirm_focus('split-h + move'); + +##################################################################### +# Same as above but with a vertical split. +##################################################################### + +fresh_workspace; +$windows[3] = open_window; +cmd 'split v'; +$windows[2] = open_window; +$windows[1] = open_window; +$windows[0] = open_window; + +cmd 'move left'; +confirm_focus('split-v + move'); + +done_testing; diff --git a/testcases/t/294-update-ewmh-atoms.t b/testcases/t/294-update-ewmh-atoms.t new file mode 100644 index 00000000..047cc119 --- /dev/null +++ b/testcases/t/294-update-ewmh-atoms.t @@ -0,0 +1,111 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • https://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • https://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • https://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Verifies _NET_DESKTOP_NAMES, _NET_CURRENT_DESKTOP and _NET_CURRENT_DESKTOP +# are updated properly when closing an inactive workspace container. +# See github issue #3126 + +use i3test; + +sub get_desktop_names { + sync_with_i3; + + my $cookie = $x->get_property( + 0, + $x->get_root_window(), + $x->atom(name => '_NET_DESKTOP_NAMES')->id, + $x->atom(name => 'UTF8_STRING')->id, + 0, + 4096, + ); + + my $reply = $x->get_property_reply($cookie->{sequence}); + + return 0 if $reply->{value_len} == 0; + + # the property is a null-delimited list of utf8 strings ;; + return split /\0/, $reply->{value}; +} + +sub get_num_of_desktops { + sync_with_i3; + + my $cookie = $x->get_property( + 0, + $x->get_root_window(), + $x->atom(name => '_NET_NUMBER_OF_DESKTOPS')->id, + $x->atom(name => 'CARDINAL')->id, + 0, + 4, + ); + + my $reply = $x->get_property_reply($cookie->{sequence}); + + return undef if $reply->{value_len} != 1; + return undef if $reply->{format} != 32; + return undef if $reply->{type} != $x->atom(name => 'CARDINAL')->id,; + + return unpack 'L', $reply->{value}; +} + +sub get_current_desktop { + sync_with_i3; + + my $cookie = $x->get_property( + 0, + $x->get_root_window(), + $x->atom(name => '_NET_CURRENT_DESKTOP')->id, + $x->atom(name => 'CARDINAL')->id, + 0, + 4, + ); + + my $reply = $x->get_property_reply($cookie->{sequence}); + + return undef if $reply->{value_len} != 1; + return undef if $reply->{format} != 32; + return undef if $reply->{type} != $x->atom(name => 'CARDINAL')->id,; + + return unpack 'L', $reply->{value}; +} + +cmd 'workspace 0'; +my $first = open_window; + +cmd 'workspace 1'; +my $second = open_window; + +cmd 'workspace 2'; +my $third = open_window; + +# Sanity check +is(get_current_desktop, 2); +is(get_num_of_desktops, 3); +my @actual_names = get_desktop_names; +my @expected_names = ('0', '1', '2'); +is_deeply(\@actual_names, \@expected_names); + +# Kill first window to close a workspace. +cmd '[id="' . $second->id . '"] kill'; + +is(get_current_desktop, 2, '_NET_CURRENT_DESKTOP should be updated'); +is(get_num_of_desktops, 2, '_NET_NUMBER_OF_DESKTOPS should be updated'); +my @actual_names = get_desktop_names; +my @expected_names = ('0', '2'); +is_deeply(\@actual_names, \@expected_names, '_NET_DESKTOP_NAMES should be updated'); + + +done_testing; diff --git a/testcases/t/504-move-workspace-to-output.t b/testcases/t/504-move-workspace-to-output.t index 8e48bafe..4ec33f66 100644 --- a/testcases/t/504-move-workspace-to-output.t +++ b/testcases/t/504-move-workspace-to-output.t @@ -179,6 +179,36 @@ cmd '[con_mark=marked] move workspace to output current'; ($x0, $x1) = workspaces_per_screen(); ok($ws1 ~~ @$x0, 'ws1 on fake-0'); +################################################################################ +# Verify that '[class=".*"] move workspace to output' doesn't fail when +# containers in the scratchpad are matched. +# See issue: #3064. +################################################################################ +my $__i3_scratch = get_ws('__i3_scratch'); +is(scalar @{$__i3_scratch->{floating_nodes}}, 0, 'scratchpad is empty'); + +$ws0 = fresh_workspace(output => 0); +open_window(wm_class => 'a'); + +$ws1 = fresh_workspace(output => 1); +open_window(wm_class => 'b'); +my $scratchpad_window = open_window(wm_class => 'c'); +cmd 'move to scratchpad'; + +($x0, $x1) = workspaces_per_screen(); +ok($ws0 ~~ @$x0, 'ws0 on fake-0'); +ok($ws1 ~~ @$x1, 'ws1 on fake-1'); + +my $reply = cmd '[class=".*"] move workspace to output fake-1'; +ok($reply->[0]->{success}, 'move successful'); + +($x0, $x1) = workspaces_per_screen(); +ok($ws0 ~~ @$x1, 'ws0 on fake-1'); +ok($ws1 ~~ @$x1, 'ws1 on fake-1'); + +$__i3_scratch = get_ws('__i3_scratch'); +is(scalar @{$__i3_scratch->{floating_nodes}}, 1, 'window still in scratchpad'); + ################################################################################ done_testing; diff --git a/testcases/t/510-focus-across-outputs.t b/testcases/t/510-focus-across-outputs.t index 21169adb..065437f0 100644 --- a/testcases/t/510-focus-across-outputs.t +++ b/testcases/t/510-focus-across-outputs.t @@ -92,7 +92,7 @@ reset_focus $s3_ws; cmd "workspace $s2_ws"; cmd 'focus right'; -is($x->input_focus, $sixth->id, 'sixth window focused'); +is($x->input_focus, $seventh->id, 'seventh window focused'); reset_focus $s3_ws; cmd "workspace $s2_ws"; @@ -110,6 +110,8 @@ is($x->input_focus, $third->id, 'third window focused'); cmd 'focus parent'; cmd 'focus parent'; cmd 'split v'; +# Focus second or else $first gets to the top of the focus stack. +cmd '[id=' . $second->id . '] focus'; reset_focus $s0_ws; cmd "workspace $s3_ws"; @@ -117,6 +119,7 @@ is($x->input_focus, $eighth->id, 'eighth window focused'); cmd 'focus parent'; cmd 'focus parent'; cmd 'split v'; +cmd '[id=' . $sixth->id . '] focus'; reset_focus $s3_ws; cmd "workspace $s1_ws"; @@ -137,4 +140,96 @@ cmd "workspace $s2_ws"; cmd 'focus up'; is($x->input_focus, $second->id, 'second window focused'); +################################################################### +# Test that focus (left|down|right|up), when focusing across +# outputs, doesn't focus the next window in the given direction but +# the most focused window of the container in the given direction. +# In the following layout: +# [ WS1*[ ] WS2[ H[ A B* ] ] ] +# (where the asterisk denotes the focused container within its +# parent) moving right from WS1 should focus B which is focused +# inside WS2, not A which is the next window on the right of WS1. +# See issue #1160. +################################################################### + +kill_all_windows; + +sync_with_i3; +$x->root->warp_pointer(1025, 0); # Second screen. +sync_with_i3; +$s1_ws = fresh_workspace; +$first = open_window; +$second = open_window; + +sync_with_i3; +$x->root->warp_pointer(0, 0); # First screen. +sync_with_i3; +$s0_ws = fresh_workspace; +open_window; +$third = open_window; + +cmd 'focus right'; +is($x->input_focus, $second->id, 'second window (rightmost) focused'); +cmd 'focus left'; +is($x->input_focus, $first->id, 'first window focused'); +cmd 'focus left'; +is($x->input_focus, $third->id, 'third window focused'); + + +################################################################### +# Similar but with a tabbed layout. +################################################################### + +cmd 'layout tabbed'; +$fourth = open_window; +cmd 'focus left'; +is($x->input_focus, $third->id, 'third window (tabbed) focused'); +cmd "workspace $s1_ws"; +cmd 'focus left'; +is($x->input_focus, $third->id, 'third window (tabbed) focused'); + + +################################################################### +# Similar but with a stacked layout on the bottom screen. +################################################################### + +sync_with_i3; +$x->root->warp_pointer(0, 769); # Third screen. +sync_with_i3; +$s2_ws = fresh_workspace; +cmd 'layout stacked'; +$fifth = open_window; +$sixth = open_window; + +cmd "workspace $s0_ws"; +cmd 'focus down'; +is($x->input_focus, $sixth->id, 'sixth window (stacked) focused'); + +################################################################### +# Similar but with a more complex layout. +################################################################### + +sync_with_i3; +$x->root->warp_pointer(1025, 769); # Fourth screen. +sync_with_i3; +$s3_ws = fresh_workspace; +open_window; +open_window; +cmd 'split v'; +open_window; +open_window; +cmd 'split h'; +my $nested = open_window; +open_window; +cmd 'focus left'; +is($x->input_focus, $nested->id, 'nested window focused'); + +cmd "workspace $s1_ws"; +cmd 'focus down'; +is($x->input_focus, $nested->id, 'nested window focused from workspace above'); + +cmd "workspace $s2_ws"; +cmd 'focus right'; +is($x->input_focus, $nested->id, 'nested window focused from workspace on the left'); + done_testing; diff --git a/testcases/t/514-ipc-workspace-multi-monitor.t b/testcases/t/514-ipc-workspace-multi-monitor.t index e3753bec..ac918fe3 100644 --- a/testcases/t/514-ipc-workspace-multi-monitor.t +++ b/testcases/t/514-ipc-workspace-multi-monitor.t @@ -13,7 +13,7 @@ # # • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf # (unless you are already familiar with Perl) -# +# # Ticket: #990 # Bug still in: 4.5.1-23-g82b5978 @@ -23,46 +23,17 @@ font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 fake-outputs 1024x768+0+0,1024x768+1024+0 EOT -my $i3 = i3(get_socket_path()); - -$i3->connect()->recv; - -################################ -# Workspaces requests and events -################################ - my $old_ws = get_ws(focused_ws); -# Events - -# We are switching to an empty workpspace on the output to the right from an empty workspace on the output on the left, so we expect -# to receive "init", "focus", and "empty". my $focus = AnyEvent->condvar; -$i3->subscribe({ - workspace => sub { - my ($event) = @_; - if ($event->{change} eq 'focus') { - $focus->send($event); - } - } -})->recv; - -my $t; -$t = AnyEvent->timer( - after => 0.5, - cb => sub { - $focus->send(0); - } -); - -cmd 'focus output right'; - -my $event = $focus->recv; +my @events = events_for( + sub { cmd 'focus output right' }, + 'workspace'); my $current_ws = get_ws(focused_ws); -ok($event, 'Workspace "focus" event received'); -is($event->{current}->{id}, $current_ws->{id}, 'Event gave correct current workspace'); -is($event->{old}->{id}, $old_ws->{id}, 'Event gave correct old workspace'); +is(scalar @events, 1, 'Received 1 event'); +is($events[0]->{current}->{id}, $current_ws->{id}, 'Event gave correct current workspace'); +is($events[0]->{old}->{id}, $old_ws->{id}, 'Event gave correct old workspace'); done_testing; diff --git a/testcases/t/517-regress-move-direction-ipc.t b/testcases/t/517-regress-move-direction-ipc.t index 217d898c..2f7f2b27 100644 --- a/testcases/t/517-regress-move-direction-ipc.t +++ b/testcases/t/517-regress-move-direction-ipc.t @@ -27,51 +27,29 @@ workspace ws-left output fake-0 workspace ws-right output fake-1 EOT -my $i3 = i3(get_socket_path()); -$i3->connect()->recv; - -# subscribe to the 'focus' ipc event -my $focus = AnyEvent->condvar; -$i3->subscribe({ - workspace => sub { - my ($event) = @_; - if ($event->{change} eq 'focus') { - $focus->send($event); - } - } -})->recv; - -# give up after 0.5 seconds -my $timer = AnyEvent->timer( - after => 0.5, - cb => sub { - $focus->send(0); - } -); - # open two windows on the left output cmd 'workspace ws-left'; open_window; open_window; +sub focus_subtest { + my ($cmd, $want) = @_; + + my @events = events_for( + sub { cmd $cmd }, + 'workspace'); + + my @focus = grep { $_->{change} eq 'focus' } @events; + is(scalar @focus, 1, 'Received 1 workspace::focus event'); + is($focus[0]->{current}->{name}, 'ws-right', 'focus event gave the right workspace'); + is(@{$focus[0]->{current}->{nodes}}, $want, 'focus event gave the right number of windows on the workspace'); +} + # move a window over to the right output -cmd 'move right'; -my $event = $focus->recv; +subtest 'move right (1)', \&focus_subtest, 'move right', 1; -ok($event, 'moving from workspace with two windows triggered focus ipc event'); -is($event->{current}->{name}, 'ws-right', 'focus event gave the right workspace'); -is(@{$event->{current}->{nodes}}, 1, 'focus event gave the right number of windows on the workspace'); - -# reset and try again -$focus = AnyEvent->condvar; +# move another window cmd 'workspace ws-left'; -$focus->recv; - -$focus = AnyEvent->condvar; -cmd 'move right'; -$event = $focus->recv; -ok($event, 'moving from workspace with one window triggered focus ipc event'); -is($event->{current}->{name}, 'ws-right', 'focus event gave the right workspace'); -is(@{$event->{current}->{nodes}}, 2, 'focus event gave the right number of windows on the workspace'); +subtest 'move right (2)', \&focus_subtest, 'move right', 2; done_testing; diff --git a/testcases/t/525-i3bar-mouse-bindings.t b/testcases/t/525-i3bar-mouse-bindings.t index ff34328c..87552785 100644 --- a/testcases/t/525-i3bar-mouse-bindings.t +++ b/testcases/t/525-i3bar-mouse-bindings.t @@ -30,21 +30,19 @@ bar { bindsym button3 focus left bindsym button4 focus right bindsym button5 focus left + bindsym --release button6 focus right + bindsym button7 focus left + bindsym button7 --release focus right } EOT use i3test::XTEST; -my ($cv, $timer); -sub reset_test { - $cv = AE::cv; - $timer = AE::timer(1, 0, sub { $cv->send(0); }); -} - my $i3 = i3(get_socket_path()); $i3->connect()->recv; my $ws = fresh_workspace; -reset_test; +my $cv = AnyEvent->condvar; +my $timer = AnyEvent->timer(after => 1, interval => 0, cb => sub { $cv->send(0) }); $i3->subscribe({ window => sub { my ($event) = @_; @@ -60,15 +58,13 @@ $i3->subscribe({ }, })->recv; -my $con; - sub i3bar_present { my ($nodes) = @_; for my $node (@{$nodes}) { my $props = $node->{window_properties}; if (defined($props) && $props->{class} eq 'i3bar') { - return 1; + return $node->{window}; } } @@ -80,53 +76,112 @@ sub i3bar_present { return i3bar_present(\@children); } -if (i3bar_present($i3->get_tree->recv->{nodes})) { +my $i3bar_window = i3bar_present($i3->get_tree->recv->{nodes}); +if ($i3bar_window) { ok(1, 'i3bar present'); } else { - $con = $cv->recv; + my $con = $cv->recv; ok($con, 'i3bar appeared'); + $i3bar_window = $con->{window}; } +diag('i3bar window = ' . $i3bar_window); + my $left = open_window; my $right = open_window; sync_with_i3; -$con = $cv->recv; -is($con->{window}, $right->{id}, 'focus is initially on the right container'); -reset_test; -xtest_button_press(1, 3, 3); -xtest_button_release(1, 3, 3); -sync_with_i3; -$con = $cv->recv; -is($con->{window}, $left->{id}, 'button 1 moves focus left'); -reset_test; +sub focus_subtest { + my ($subscribecb, $want, $msg) = @_; + my @events = events_for( + $subscribecb, + 'window'); + my @focus = map { $_->{container}->{window} } grep { $_->{change} eq 'focus' } @events; + is_deeply(\@focus, $want, $msg); +} -xtest_button_press(2, 3, 3); -xtest_button_release(2, 3, 3); -sync_with_i3; -$con = $cv->recv; -is($con->{window}, $right->{id}, 'button 2 moves focus right'); -reset_test; +subtest 'button 1 moves focus left', \&focus_subtest, + sub { + xtest_button_press(1, 3, 3); + xtest_button_release(1, 3, 3); + xtest_sync_with($i3bar_window); + }, + [ $left->{id} ], + 'button 1 moves focus left'; -xtest_button_press(3, 3, 3); -xtest_button_release(3, 3, 3); -sync_with_i3; -$con = $cv->recv; -is($con->{window}, $left->{id}, 'button 3 moves focus left'); -reset_test; +subtest 'button 2 moves focus right', \&focus_subtest, + sub { + xtest_button_press(2, 3, 3); + xtest_button_release(2, 3, 3); + xtest_sync_with($i3bar_window); + }, + [ $right->{id} ], + 'button 2 moves focus right'; -xtest_button_press(4, 3, 3); -xtest_button_release(4, 3, 3); -sync_with_i3; -$con = $cv->recv; -is($con->{window}, $right->{id}, 'button 4 moves focus right'); -reset_test; +subtest 'button 3 moves focus left', \&focus_subtest, + sub { + xtest_button_press(3, 3, 3); + xtest_button_release(3, 3, 3); + xtest_sync_with($i3bar_window); + }, + [ $left->{id} ], + 'button 3 moves focus left'; -xtest_button_press(5, 3, 3); -xtest_button_release(5, 3, 3); -sync_with_i3; -$con = $cv->recv; -is($con->{window}, $left->{id}, 'button 5 moves focus left'); -reset_test; +subtest 'button 4 moves focus right', \&focus_subtest, + sub { + xtest_button_press(4, 3, 3); + xtest_button_release(4, 3, 3); + xtest_sync_with($i3bar_window); + }, + [ $right->{id} ], + 'button 4 moves focus right'; + +subtest 'button 5 moves focus left', \&focus_subtest, + sub { + xtest_button_press(5, 3, 3); + xtest_button_release(5, 3, 3); + xtest_sync_with($i3bar_window); + }, + [ $left->{id} ], + 'button 5 moves focus left'; + +# Test --release flag with bar bindsym. +# See issue: #3068. + +my $old_focus = get_focused($ws); +subtest 'button 6 does not move focus while pressed', \&focus_subtest, + sub { + xtest_button_press(6, 3, 3); + xtest_sync_with($i3bar_window); + }, + [], + 'button 6 does not move focus while pressed'; +is(get_focused($ws), $old_focus, 'focus unchanged'); + +subtest 'button 6 release moves focus right', \&focus_subtest, + sub { + xtest_button_release(6, 3, 3); + xtest_sync_with($i3bar_window); + }, + [ $right->{id} ], + 'button 6 release moves focus right'; + +# Test same bindsym button with and without --release. + +subtest 'button 7 press moves focus left', \&focus_subtest, + sub { + xtest_button_press(7, 3, 3); + xtest_sync_with($i3bar_window); + }, + [ $left->{id} ], + 'button 7 press moves focus left'; + +subtest 'button 7 release moves focus right', \&focus_subtest, + sub { + xtest_button_release(7, 3, 3); + xtest_sync_with($i3bar_window); + }, + [ $right->{id} ], + 'button 7 release moves focus right'; done_testing; diff --git a/testcases/t/539-disable_focus_wrapping.t b/testcases/t/539-disable_focus_wrapping.t new file mode 100644 index 00000000..8d2e8472 --- /dev/null +++ b/testcases/t/539-disable_focus_wrapping.t @@ -0,0 +1,51 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • http://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • http://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • http://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Tests that focus does not wrap when focus_wrapping is disabled in +# the configuration. +# Ticket: #2352 +# Bug still in: 4.14-72-g6411130c +use i3test i3_config => <input_focus, $win2->id, "Second window focused initially"); + cmd "focus $prev"; + is($x->input_focus, $win1->id, "First window focused"); + cmd "focus $prev"; + is($x->input_focus, $win1->id, "First window still focused"); + cmd "focus $next"; + is($x->input_focus, $win2->id, "Second window focused"); + cmd "focus $next"; + is($x->input_focus, $win2->id, "Second window still focused"); +} + +test_orientation('v', 'up', 'down'); +test_orientation('h', 'left', 'right'); + +done_testing; diff --git a/testcases/t/540-sigterm-cleanup.t b/testcases/t/540-sigterm-cleanup.t new file mode 100644 index 00000000..5e5b9bf3 --- /dev/null +++ b/testcases/t/540-sigterm-cleanup.t @@ -0,0 +1,35 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • https://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • https://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • https://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Tests that the socket file is cleaned up properly after gracefully +# shutting down i3 via SIGTERM. +# Ticket: #3049 +use i3test i3_autostart => 0; + +my $config = < 1); +my $socket = get_socket_path(); +ok(-S $socket, "socket $socket exists"); + +exit_forcefully($pid, 'TERM'); + +ok(!-e $socket, "socket $socket no longer exists"); + +done_testing; diff --git a/testcases/t/541-resize-set-tiling.t b/testcases/t/541-resize-set-tiling.t new file mode 100644 index 00000000..82267baf --- /dev/null +++ b/testcases/t/541-resize-set-tiling.t @@ -0,0 +1,147 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • https://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • https://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • https://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Tests resizing tiling containers +use i3test; + +############################################################ +# resize horizontally +############################################################ + +my $tmp = fresh_workspace; + +cmd 'split h'; + +my $left = open_window; +my $right = open_window; + +diag("left = " . $left->id . ", right = " . $right->id); + +is($x->input_focus, $right->id, 'Right window focused'); + +cmd 'resize set 75 ppt 0 ppt'; + +my ($nodes, $focus) = get_ws_content($tmp); + +cmp_float($nodes->[0]->{percent}, 0.25, 'left window got only 25%'); +cmp_float($nodes->[1]->{percent}, 0.75, 'right window got 75%'); + +############################################################ +# resize vertically +############################################################ + +my $tmp = fresh_workspace; + +cmd 'split v'; + +my $top = open_window; +my $bottom = open_window; + +diag("top = " . $top->id . ", bottom = " . $bottom->id); + +is($x->input_focus, $bottom->id, 'Bottom window focused'); + +cmd 'resize set 0 ppt 75 ppt'; + +my ($nodes, $focus) = get_ws_content($tmp); + +cmp_float($nodes->[0]->{percent}, 0.25, 'top window got only 25%'); +cmp_float($nodes->[1]->{percent}, 0.75, 'bottom window got 75%'); + + +############################################################ +# resize horizontally and vertically +############################################################ + +my $tmp = fresh_workspace; + +cmd 'split h'; +my $left = open_window; +my $top_right = open_window; +cmd 'split v'; +my $bottom_right = open_window; + +diag("left = " . $left->id . ", top-right = " . $top_right->id . ", bottom-right = " . $bottom_right->id); + +is($x->input_focus, $bottom_right->id, 'Bottom-right window focused'); + +cmd 'resize set 75 ppt 75 ppt'; + +my ($nodes, $focus) = get_ws_content($tmp); + +cmp_float($nodes->[0]->{percent}, 0.25, 'left container got 25%'); +cmp_float($nodes->[1]->{percent}, 0.75, 'right container got 75%'); +cmp_float($nodes->[1]->{nodes}->[0]->{percent}, 0.25, 'top-right window got 25%'); +cmp_float($nodes->[1]->{nodes}->[1]->{percent}, 0.75, 'bottom-right window got 75%'); + + +############################################################ +# resize from inside a tabbed container +############################################################ + +my $tmp = fresh_workspace; + +cmd 'split h'; + +my $left = open_window; +my $right1 = open_window; + +cmd 'split h'; +cmd 'layout tabbed'; + +my $right2 = open_window; + +diag("left = " . $left->id . ", right1 = " . $right1->id . ", right2 = " . $right2->id); + +is($x->input_focus, $right2->id, '2nd right window focused'); + +cmd 'resize set 75 ppt 0 ppt'; + +my ($nodes, $focus) = get_ws_content($tmp); + +cmp_float($nodes->[0]->{percent}, 0.25, 'left container got 25%'); +cmp_float($nodes->[1]->{percent}, 0.75, 'right container got 75%'); + + +############################################################ +# resize from inside a stacked container +############################################################ + +my $tmp = fresh_workspace; + +cmd 'split h'; + +my $left = open_window; +my $right1 = open_window; + +cmd 'split h'; +cmd 'layout stacked'; + +my $right2 = open_window; + +diag("left = " . $left->id . ", right1 = " . $right1->id . ", right2 = " . $right2->id); + +is($x->input_focus, $right2->id, '2nd right window focused'); + +cmd 'resize set 75 ppt 0 ppt'; + +my ($nodes, $focus) = get_ws_content($tmp); + +cmp_float($nodes->[0]->{percent}, 0.25, 'left container got 25%'); +cmp_float($nodes->[1]->{percent}, 0.75, 'right container got 75%'); + + +done_testing; diff --git a/travis/clang-analyze.sh b/travis/clang-analyze.sh deleted file mode 100755 index 5ef390d3..00000000 --- a/travis/clang-analyze.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -set -e -set -x - -mkdir -p deb/DIST-clang/build -tar xf build/*.tar.bz2 -C deb/DIST-clang --strip-components=1 -(cd deb/DIST-clang/build && scan-build -o ../../CLANG ../configure && scan-build -o ../../CLANG --html-title="Analysis of i3 v$(git describe --tags)" make -j8) -mv deb/CLANG/*/* deb/CLANG diff --git a/travis/deploy-github-pages.sh b/travis/deploy-github-pages.sh index f86fbf22..2d454820 100755 --- a/travis/deploy-github-pages.sh +++ b/travis/deploy-github-pages.sh @@ -6,7 +6,6 @@ set -x GITVERSION=$(git describe --tags) mkdir build.i3wm.org -cp -r deb/CLANG build.i3wm.org/clang-analyze cp -r deb/COPY-DOCS build.i3wm.org/docs cd build.i3wm.org echo build.i3wm.org > CNAME diff --git a/travis/run-tests.sh b/travis/run-tests.sh index 44df81d2..eac2ea8a 100755 --- a/travis/run-tests.sh +++ b/travis/run-tests.sh @@ -26,7 +26,7 @@ fi # Try running the tests in parallel so that the common case (tests pass) is # quick, but fall back to running them in sequence to make debugging easier. -if ! xvfb-run make check +if ! make check then - xvfb-run ./testcases/complete-run.pl --parallel=1 || (cat latest/complete-run.log; false) + ./testcases/complete-run.pl --parallel=1 || (cat latest/complete-run.log; false) fi diff --git a/travis/travis-base.Dockerfile b/travis/travis-base.Dockerfile index 5704d8e4..7eafb9fb 100644 --- a/travis/travis-base.Dockerfile +++ b/travis/travis-base.Dockerfile @@ -19,7 +19,7 @@ RUN apt-get update && \ dpkg-dev devscripts git equivs \ clang clang-format-3.8 \ lintian \ - libmodule-install-perl libanyevent-perl libextutils-pkgconfig-perl xcb-proto cpanminus xvfb xserver-xephyr xauth libinline-perl libinline-c-perl libxml-simple-perl libmouse-perl libmousex-nativetraits-perl libextutils-depends-perl perl libtest-deep-perl libtest-exception-perl libxml-parser-perl libtest-simple-perl libtest-fatal-perl libdata-dump-perl libtest-differences-perl libxml-tokeparser-perl libipc-run-perl libxcb-xtest0-dev libx11-xcb-perl libjson-xs-perl && \ + libmodule-install-perl libanyevent-perl libextutils-pkgconfig-perl xcb-proto cpanminus xvfb xserver-xephyr xauth libinline-perl libinline-c-perl libxml-simple-perl libmouse-perl libmousex-nativetraits-perl libextutils-depends-perl perl libtest-deep-perl libtest-exception-perl libxml-parser-perl libtest-simple-perl libtest-fatal-perl libdata-dump-perl libtest-differences-perl libxml-tokeparser-perl libipc-run-perl libxcb-xtest0-dev libx11-xcb-perl libjson-xs-perl x11-xserver-utils && \ rm -rf /var/lib/apt/lists/* # Install i3 build dependencies.