diff --git a/AnyEvent-I3/lib/AnyEvent/I3.pm b/AnyEvent-I3/lib/AnyEvent/I3.pm index 08b1c0a71..198c41c9a 100644 --- a/AnyEvent-I3/lib/AnyEvent/I3.pm +++ b/AnyEvent-I3/lib/AnyEvent/I3.pm @@ -99,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} } ); @@ -120,6 +121,7 @@ my %events = ( barconfig_update => ($event_mask | 4), binding => ($event_mask | 5), shutdown => ($event_mask | 6), + tick => ($event_mask | 7), _error => 0xFFFFFFFF, ); @@ -519,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/Makefile.am b/Makefile.am index b94d3e901..dc1b044cb 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 \ diff --git a/README.md b/README.md index e2bfad6c9..b905b4506 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Please refer to the [wiki](https://github.com/Airblader/i3/wiki/Compiling-&-Inst Note: Gaps will only work correctly if you disable window titlebars entirely. Unfortunately this is necessary due to the way i3 creates such bars on windows. You can disable them via `for_window [class="^.*"] border pixel 0` in your config. You can also use any non-zero value as long as you only use pixel-style borders. -Based on the patches provided by o4dev and jeanbroid, i3 supports gaps between containers. I extended those patches further to make changing the gaps size easier during runtime and also to expose more functionality for binding it to keys. Additionally, the gaps patch was fixed such that the gaps between containers and the gaps between containers and the edge of the screen are the same. But I didn't stop there: these gaps are called "inner" gaps. This fork also allows setting "outer" gaps which inset all containers independently. +Based on the patches provided by o4dev and jeanbroid, i3 supports gaps between containers. I extended those patches further to make changing the gaps size easier during runtime and also to expose more functionality for binding it to keys. Additionally, the gaps patch was fixed such that inner gaps (the gaps between adjacent containers) and outer gaps (gaps between the edge of the screen and a container) are the same. But I didn't stop there: these gaps are called "inner" gaps. This fork also allows setting "outer" gaps which inset all containers independently. In your i3 config, you can set a global gap size as shown below. This is the default value that will be used for all workspaces: @@ -130,7 +130,9 @@ Based on the patch from [i3-extras](https://github.com/ashinkarov/i3-extras), sm ``` smart_borders on|no_gaps ``` +### Hide Edge Borders +An additional option `smart_no_gaps` is available for `hide_edge_borders`. This will hide all borders on a container if it is the only container in the worskpace *and* the gap size to the edge is `0`. Otherwise all borders will be drawn normally. ## i3bar diff --git a/contrib/dump-asy.pl b/contrib/dump-asy.pl index 3ebdb858e..9bb2db3aa 100755 --- a/contrib/dump-asy.pl +++ b/contrib/dump-asy.pl @@ -13,7 +13,14 @@ 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 @@ sub find_node_with_name { 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 48590456a..4a2b4b9e2 100644 --- a/contrib/per-workspace-layout.pl +++ b/contrib/per-workspace-layout.pl @@ -14,6 +14,7 @@ use AnyEvent; use AnyEvent::I3; use v5.10; +use utf8; my %layouts = ( '4' => 'tabbed', diff --git a/debian/changelog b/debian/changelog index 3292ea6f8..14d90e0b6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +i3-wm (4.14.2-1) unstable; urgency=medium + + * UNRELEASED + + -- 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. @@ -307,7 +319,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 bff5028cb..823d40979 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 e4da3db85..d5c306867 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 07bc13a04..dd26f98d9 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 b8c2b5ad7..cf86531cc 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 997a3055f..8b767adeb 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 bf85cb1fb..b535e7c14 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 2e5a0d1d0..4875e687e 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 --------------------- @@ -1055,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 -*Example*: ------------------------- +# Legacy syntax, equivalent to "focus_wrapping force" force_focus_wrapping yes ------------------------- +--------------------------- + +*Examples*: +----------------- +# Disable focus wrapping +focus_wrapping no + +# Force focus wrapping +focus_wrapping force +----------------- === Forcing Xinerama @@ -1363,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*: @@ -1371,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 } @@ -1952,6 +1970,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:: @@ -1971,6 +1992,7 @@ output:: *Syntax*: ---------------------------------------------- + focus focus left|right|down|up focus parent|child|floating|tiling|mode_toggle focus output left|right|up|down|primary| @@ -1978,6 +2000,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 @@ -2305,7 +2330,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 @@ -2314,8 +2339,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 @@ -2401,10 +2429,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 483694c1c..3be9831dd 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 6d10fad29..2d56876c2 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 a7687c7bc..4c45b6ed6 100755 --- a/generate-command-parser.pl +++ b/generate-command-parser.pl @@ -116,17 +116,16 @@ sub slurp { 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 @@ sub slurp { $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 dd58fd124..b368921f2 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 478af310c..e9901f8ee 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 d347506fe..d7aae66b5 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 785a133fc..efb7b20c2 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 @@ static xcb_rectangle_t get_window_position(void) { 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 8907a6f7a..91a714e56 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 c5e94cc61..cb672bead 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 7d38f7314..e4628e303 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 ad3f6bddb..dc95865de 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 f92ff224a..f1eb256ec 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 0871c7f49..9479fac1d 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 67db25a77..6d3acac57 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 de960270e..29a7bcd38 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 3af79ed77..1f5636110 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 65a147c47..453dd0ceb 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 aa0ad0b4b..823b0a341 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); } @@ -617,7 +618,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; } @@ -645,6 +646,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 c6a0aa228..07a1b2b8c 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 c932aaf7d..cc3563ec0 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 475c10554..86705906d 100644 --- a/i3bar/src/main.c +++ b/i3bar/src/main.c @@ -186,7 +186,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 bd056a700..4c9ce6513 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 26f3bf202..0bfcf51b0 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; @@ -441,6 +440,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- @@ -462,10 +473,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; @@ -507,7 +524,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; } @@ -518,12 +536,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; } @@ -679,8 +692,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]; @@ -1058,21 +1089,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)) { @@ -1158,6 +1179,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); @@ -1193,6 +1215,8 @@ void xcb_chk_cb(struct ev_loop *loop, ev_check *watcher, int revents) { } free(event); } + + xcb_flush(xcb_connection); } /* @@ -1264,21 +1288,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(); @@ -1522,16 +1537,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(); @@ -1540,11 +1546,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); } @@ -1712,7 +1716,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 c26835b90..ecc875d08 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 c05cc91b4..823c240a1 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 76c77284e..87583be2d 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. * diff --git a/include/config_directives.h b/include/config_directives.h index 95a3fffea..d45a6d06d 100644 --- a/include/config_directives.h +++ b/include/config_directives.h @@ -52,6 +52,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); @@ -69,7 +70,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); @@ -87,7 +88,7 @@ CFGFUN(bar_height, const long height); 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 be6c3b685..eecac80ab 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 @@ -387,6 +396,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 84e4b89a1..4b03ecd11 100644 --- a/include/data.h +++ b/include/data.h @@ -85,7 +85,8 @@ typedef enum { HEBM_NONE = ADJ_NONE, HEBM_VERTICAL = ADJ_LEFT_SCREEN_EDGE | ADJ_RIGHT_SCREEN_EDGE, HEBM_HORIZONTAL = ADJ_UPPER_SCREEN_EDGE | ADJ_LOWER_SCREEN_EDGE, HEBM_BOTH = HEBM_VERTICAL | HEBM_HORIZONTAL, - HEBM_SMART = (1 << 5) } hide_edge_borders_mode_t; + HEBM_SMART = (1 << 5), + HEBM_SMART_NO_GAPS = (1 << 6) } hide_edge_borders_mode_t; typedef enum { MM_REPLACE, MM_ADD } mark_mode_t; @@ -143,6 +144,15 @@ struct gaps_t { int outer; }; +/** + * 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. diff --git a/include/i3.h b/include/i3.h index 4d13d448f..93a7e0a34 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 993a2a248..9e0280c93 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 7ffbf7a83..c6ad35c77 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 7b33de907..386341561 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 526ab8810..3547d8d7b 100644 --- a/include/util.h +++ b/include/util.h @@ -47,12 +47,10 @@ 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__) diff --git a/libi3/dpi.c b/libi3/dpi.c index 93a3c6f6a..a2c40319a 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 2e6283425..f659a1a47 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 16dda90d8..84da5aa36 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 effae6c06..4f16d6c1e 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 20a6810c5..894af9120 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 386c2ca74..40d52ec98 100644 --- a/parser-specs/commands.spec +++ b/parser-specs/commands.spec @@ -276,14 +276,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 4a7922e1e..d7fbf18cc 100644 --- a/parser-specs/config.spec +++ b/parser-specs/config.spec @@ -32,13 +32,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 @@ -129,30 +131,30 @@ 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 +# hide_edge_borders # also hide_edge_borders for compatibility state HIDE_EDGE_BORDERS: - hide_borders = 'none', 'vertical', 'horizontal', 'both', 'smart' + hide_borders = 'none', 'vertical', 'horizontal', 'both', 'smart_no_gaps', 'smart' -> call cfg_hide_edge_borders($hide_borders) hide_borders = '1', 'yes', 'true', 'on', 'enable', 'active' -> call cfg_hide_edge_borders($hide_borders) @@ -234,6 +236,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 @@ -524,12 +531,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 8f537c0e2..cfb4beae4 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 42a2b79b8..c145b9560 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 78af8a035..b036c5f80 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 39d428809..2755761d9 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; + /* get the appropriate current container (skip stacked/tabbed cons) */ + 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); } /* @@ -1114,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"); @@ -1239,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'. * @@ -1246,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); + 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; + } + 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 *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; - con_focus(con_descend_focused(current)); - break; - } + cmd_focus_force_focus(con_descend_focused(current)); + success = true; + break; } - cmd_output->needs_tree_render = true; - // XXX: default reply for now, make this a better reply - ysuccess(true); + if (success) { + cmd_output->needs_tree_render = true; + ysuccess(true); + } else { + yerror("Failed to find a %s container in workspace.", to_floating ? "floating" : "tiling"); + } } /* @@ -1324,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); @@ -1354,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++; } @@ -1472,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); @@ -1597,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"); @@ -1986,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); @@ -2167,7 +2206,7 @@ void cmd_debuglog(I3_CMD, const char *argument) { */ void cmd_gaps(I3_CMD, const char *type, const char *scope, const char *mode, const char *value) { #define CMD_GAPS(type, other) \ - int pixels = atoi(value); \ + int pixels = logical_px(atoi(value)); \ Con *workspace = con_get_workspace(focused); \ \ int current_value = config.gaps.type; \ diff --git a/src/con.c b/src/con.c index 714f31945..230af34c7 100644 --- a/src/con.c +++ b/src/con.c @@ -243,15 +243,29 @@ void con_focus(Con *con) { workspace_update_urgent_flag(con_get_workspace(con)); ipc_send_window_event("urgent", con); } +} - /* Focusing a container with a floating parent should raise it to the top. Since - * con_focus is called recursively for each parent we don't need to use - * con_inside_floating(). */ - if (con->type == CT_FLOATING_CON) { - floating_raise_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 +809,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 +1064,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 +1218,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 +1242,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. */ @@ -1586,7 +1656,8 @@ Con *con_descend_direction(Con *con, direction_t direction) { Rect con_border_style_rect(Con *con) { if ((config.smart_borders == ON && con_num_visible_children(con_get_workspace(con)) <= 1) || (config.smart_borders == NO_GAPS && calculate_effective_gaps(con).outer == 0) || - (config.hide_edge_borders == HEBM_SMART && con_num_visible_children(con_get_workspace(con)) <= 1)) { + (config.hide_edge_borders == HEBM_SMART && con_num_visible_children(con_get_workspace(con)) <= 1) || + (config.hide_edge_borders == HEBM_SMART_NO_GAPS && con_num_visible_children(con_get_workspace(con)) <= 1 && calculate_effective_gaps(con).outer == 0)) { if (!con_is_floating(con)) return (Rect){0, 0, 0, 0}; } @@ -1613,7 +1684,13 @@ Rect con_border_style_rect(Con *con) { result = (Rect){border_width, border_width, -(2 * border_width), -(2 * border_width)}; } - borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; + /* If hide_edge_borders is set to no_gaps and it did not pass the no border check, show all borders */ + if (config.hide_edge_borders == HEBM_SMART_NO_GAPS) { + borders_to_hide = con_adjacent_borders(con) & HEBM_NONE; + } else { + borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; + } + if (borders_to_hide & ADJ_LEFT_SCREEN_EDGE) { result.x -= border_width; result.width += border_width; @@ -1757,7 +1834,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); @@ -1768,17 +1845,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))) { @@ -1787,13 +1856,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); @@ -1849,6 +1918,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; } @@ -2067,14 +2140,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); } /* @@ -2380,7 +2446,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. */ @@ -2423,15 +2489,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)); } } diff --git a/src/config.c b/src/config.c index d60958e4c..38f35d953 100644 --- a/src/config.c +++ b/src/config.c @@ -230,6 +230,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 609b6670c..0625bd4de 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -191,16 +191,17 @@ void create_gaps_assignment(const char *workspace, bool inner, const long value) } CFGFUN(gaps, const char *workspace, const char *scope, const long value) { + int pixels = logical_px(value); if (!strcmp(scope, "inner")) { if (workspace == NULL) - config.gaps.inner = value; + config.gaps.inner = pixels; else - create_gaps_assignment(workspace, true, value - config.gaps.inner); + create_gaps_assignment(workspace, true, pixels - config.gaps.inner); } else if (!strcmp(scope, "outer")) { if (workspace == NULL) - config.gaps.outer = value; + config.gaps.outer = pixels; else - create_gaps_assignment(workspace, false, value - config.gaps.outer); + create_gaps_assignment(workspace, false, pixels - config.gaps.outer); } else { ELOG("Invalid command, cannot process scope %s", scope); } @@ -250,7 +251,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; @@ -268,7 +269,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; @@ -282,7 +284,9 @@ CFGFUN(new_window, const char *windowtype, const char *border, const long width) } CFGFUN(hide_edge_borders, const char *borders) { - if (strcmp(borders, "smart") == 0) + if (strcmp(borders, "smart_no_gaps") == 0) + config.hide_edge_borders = HEBM_SMART_NO_GAPS; + else if (strcmp(borders, "smart") == 0) config.hide_edge_borders = HEBM_SMART; else if (strcmp(borders, "vertical") == 0) config.hide_edge_borders = HEBM_VERTICAL; @@ -317,8 +321,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) { @@ -541,7 +564,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; @@ -552,16 +575,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); @@ -569,16 +594,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 58a5552ce..2d3f3bb91 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 764ee7531..2e05cafa8 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 0abdb2405..be1a8a2f6 100644 --- a/src/floating.c +++ b/src/floating.c @@ -318,7 +318,7 @@ void floating_enable(Con *con, bool automatic) { render_con(con, false, true); 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 3140e4057..e1671c3b4 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 f8e337307..a698c1536 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -584,6 +584,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); } @@ -1063,8 +1065,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; @@ -1116,6 +1119,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); } /* @@ -1139,9 +1161,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, @@ -1152,6 +1200,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 2a34a7358..43e390579 100644 --- a/src/load_layout.c +++ b/src/load_layout.c @@ -666,6 +666,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 0d1457fdc..194ef05c3 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,21 +174,64 @@ 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 "Term", see signal(7) + * (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_signal(int sig, siginfo_t *info, void *data) { +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) + * + * Exits the program gracefully. + * + */ +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); + } +} + int main(int argc, char *argv[]) { /* Keep a symbol pointing to the I3_VERSION string constant so that we have * it in gdb backtraces. */ @@ -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 af7308e7d..f123daccd 100644 --- a/src/manage.c +++ b/src/manage.c @@ -646,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 3ecc69e4c..97ca6d40b 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 e76903844..c76dfd035 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 8fd38413b..dff33124e 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/render.c b/src/render.c index b64c891cc..ec9b859ba 100644 --- a/src/render.c +++ b/src/render.c @@ -535,7 +535,7 @@ bool has_adjacent_container(Con *con, direction_t direction) { Con *first = con; Con *second = NULL; - bool found_neighbor = resize_find_tiling_participants(&first, &second, direction); + bool found_neighbor = resize_find_tiling_participants(&first, &second, direction, false); if (!found_neighbor) return false; diff --git a/src/resize.c b/src/resize.c index f07fcec6a..ee50bfbc6 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 bf16c864c..b99a50c16 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 9018ad3ff..95154014c 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 5e5011fa9..548a6f05a 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 ba0969c72..dc3444f7b 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 c15cedf20..4e475c644 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -480,6 +480,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(); @@ -831,9 +836,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)); @@ -841,6 +846,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); @@ -851,9 +859,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); } /* @@ -908,9 +913,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)); @@ -918,6 +924,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 09a604931..7829079bb 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 ddd6ccadf..96b93bed0 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 6cccfa762..5506d67e7 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/i3test.pm.in b/testcases/lib/i3test.pm.in index a484c91a4..e754c0c17 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 0253bc2d5..552ae8b48 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 1ca964b15..4c464c5e3 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 f0d0b4c69..d817bee03 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 9c1507e67..1e2644ad9 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 34ce07813..b0c4354ec 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 7a5e38ca1..9c396f40f 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/167-workspace_layout.t b/testcases/t/167-workspace_layout.t index d983eb856..597d545eb 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 4b225214a..d135ed59c 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 f94bd75b6..147890e1f 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 ea4c08de0..6b082bfdc 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 aee1e03a6..f9f883cbd 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 959ff6c47..0e4f89600 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 dfce3df6d..1998aaa08 100644 --- a/testcases/t/201-config-parser.t +++ b/testcases/t/201-config-parser.t @@ -145,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 @@ -245,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'; @@ -266,19 +266,33 @@ $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) -EOT +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, @@ -465,7 +479,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 @@ -473,6 +489,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 @@ -519,7 +536,7 @@ client.focused #4c7899 #285577 #ffffff #2e9ef4 EOT $expected = <<'EOT'; -ERROR: CONFIG: Expected one of these tokens: 'none', 'vertical', 'horizontal', 'both', 'smart', '1', 'yes', 'true', 'on', 'enable', 'active' +ERROR: CONFIG: Expected one of these tokens: 'none', 'vertical', 'horizontal', 'both', 'smart_no_gaps', 'smart', '1', 'yes', 'true', 'on', 'enable', 'active' ERROR: CONFIG: (in file ) ERROR: CONFIG: Line 1: hide_edge_borders FOOBAR ERROR: CONFIG: ^^^^^^ diff --git a/testcases/t/205-ipc-windows.t b/testcases/t/205-ipc-windows.t index ca7db1533..bafd155f6 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 $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'); +my @events = events_for( + sub { open_window }, + 'window'); -} +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 c098f23fb..952450993 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 1351d568d..c2b2ebaa3 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 ae7781877..b1c8ba183 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'); - -$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'); - -$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'); +sub focus_subtest { + my ($cmd, $name) = @_; -$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'); + 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 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 c751350a4..b5d14e237 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; - -$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'); +my @events = events_for( + sub { + $window->name('New Window Title'); + sync_with_i3; + }, + 'window'); -} +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 aeabe9539..bc1505469 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 fe8e03c02..b1f517ef5 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; - - cmd "workspace $ws2"; - - sync_with_i3; + my @events = events_for( + sub { cmd "workspace $ws2" }, + 'workspace'); - 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; - - # 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"; + my @events = events_for( + sub { cmd "workspace $ws2" }, + 'workspace'); - 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; - - cmd '[id="' . $w1->id . '"] kill'; - - sync_with_i3; + my @events = events_for( + sub { + $w1->unmap; + sync_with_i3; + }, + 'workspace'); - 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 e38a18760..96c94a497 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 af3f4d2fa..bec95a237 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 = AE::cv; - my $timer = AE::timer 0.5, 0, sub { $cv->send(0); }; - - $i3->subscribe({ - binding => sub { - $cv->send(shift); - } - })->recv; - - qx(xdotool key $binding_symbol); - - my $e = $cv->recv; - - does_i3_live; + my $cv = AnyEvent->condvar; - diag "Event:\n", Dumper($e); + 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'); - ok($e, - 'the binding event should emit when user input triggers an i3 binding event'); + is(scalar @events, 1, 'Received 1 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 cd3989c42..570607539 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 8d8d41203..2c8edf397 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 f9166fadb..bc08aa2f2 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 72bfc8628..8bca0d869 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 ba95897c2..1416b6b9e 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 06d8d83d1..a101944e7 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 b21ff1b2a..9ac749b63 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 bf9d4f8bb..ccaf4440e 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 a3c825348..f3606b4e8 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; - -ok($con, 'moving a window should emit the window::move event'); -is($con->{window}, $window->{id}, 'the event should contain info about the window'); +sub move_subtest { + my ($cmd) = @_; + my $cv = AnyEvent->condvar; + my @events = events_for( + sub { cmd $cmd }, + 'window'); -reset_test; - -cmd 'move to workspace ws_new'; -$con = $cv->recv; + 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'); +} -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 2af29dac9..4eea2cdca 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; - -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'); - -$cv = AnyEvent->condvar; -$win->delete_hint('urgency'); -$event = $cv->recv; - -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'); +sub urgency_subtest { + my ($subscribecb, $win, $want) = @_; + + my @events = events_for( + $subscribecb, + 'window'); + + 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"); +} + +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 6c1b5d6ac..0da373b74 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/287-edge-borders.t b/testcases/t/287-edge-borders.t index 1fd697860..3dfc34077 100644 --- a/testcases/t/287-edge-borders.t +++ b/testcases/t/287-edge-borders.t @@ -160,4 +160,69 @@ is($tilewindow2->rect->width, $tiled[1]->{rect}->{width} - 2*2, 'second tiled bo exit_gracefully($pid); +##################################################################### +# 5: check that the borders are visible on a workspace with one tiled +# window and edge gaps +##################################################################### + +$config = <{nodes}}; +ok(@tiled == 1, 'one tiled container opened'); +is($tiled[0]->{current_border_width}, 2, 'tiled current border width set to 2'); +is($tilewindow->rect->width, $tiled[0]->{rect}->{width} - 2*2, 'single tiled border width 2'); + +exit_gracefully($pid); + +##################################################################### +# 5: check that the borders are hidden on a workspace with one tiled +# window with no gaps +##################################################################### + +$config = <{nodes}}; +ok(@tiled == 1, 'one tiled container opened'); +is($tiled[0]->{current_border_width}, 2, 'tiled current border width set to 2'); +is($tilewindow->rect->width, $tiled[0]->{rect}->{width} - 2*0, 'single tiled border width 0'); + +exit_gracefully($pid); + + + done_testing; diff --git a/testcases/t/289-ipc-shutdown-event.t b/testcases/t/289-ipc-shutdown-event.t index 0cf347aa8..606474e24 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 5137c35fc..94a5747d0 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,15 +338,13 @@ 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'); -# TODO: This test does not verify that i3 does _NOT_ grab keycode 87 with Mod2. +is(scalar @unexpected2, 0, 'Did not trigger the KP_Down keybinding with KP_2'); -sync_with_i3; -is(scalar @i3test::XTEST::binding_events, 18, 'Received exactly 18 binding events'); +# TODO: This test does not verify that i3 does _NOT_ grab keycode 87 with Mod2. 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 d4a43bfb1..3b61fdab3 100644 --- a/testcases/t/291-swap.t +++ b/testcases/t/291-swap.t @@ -30,12 +30,6 @@ my ($result); my @fullscreen_permutations = ([], ["A"], ["B"], ["A", "B"]); my @urgent; -sub fullscreen_windows { - my $ws = shift if @_; - - scalar grep { $_->{fullscreen_mode} != 0 } @{get_ws_content($ws)} -} - ############################################################################### # Invalid con_id should not crash i3 # See issue #2895. @@ -191,14 +185,14 @@ for my $fullscreen (@fullscreen_permutations){ $nodes = get_ws_content($ws1); $node = $nodes->[0]; is($node->{window}, $B->{id}, 'B is on ws1:left'); - is(fullscreen_windows($ws1), $A_fullscreen, 'amount of fullscreen windows in ws1'); + 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(fullscreen_windows($ws2), $B_fullscreen, 'amount of fullscreen windows in ws2'); + 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; @@ -232,7 +226,7 @@ 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(fullscreen_windows($ws1), 1, 'F still fullscreen in ws1'); +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); @@ -258,7 +252,6 @@ cmd "split v"; open_window; cmd "focus parent"; cmd "fullscreen enable"; -$F = fullscreen_windows($ws1); $expected_focus = get_focused($ws1); $ws2 = fresh_workspace; @@ -274,11 +267,11 @@ 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(fullscreen_windows($ws1), $F, 'Same amount of fullscreen windows 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(fullscreen_windows($ws2), 1, 'A is in fullscreen mode'); +is_num_fullscreen($ws2, 1, 'A is in fullscreen mode'); ############################################################################### # Swap two non-focused containers within the same workspace. @@ -351,13 +344,13 @@ for my $fullscreen (@fullscreen_permutations){ $nodes = get_ws_content($ws1); $node = $nodes->[0]; is($node->{window}, $B->{id}, 'B is on the first workspace'); - is(fullscreen_windows($ws1), $A_fullscreen, 'amount of fullscreen windows in ws1'); + 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->[0]; is($node->{window}, $A->{id}, 'A is on the second workspace'); - is(fullscreen_windows($ws2), $B_fullscreen, 'amount of fullscreen windows in ws2'); + is_num_fullscreen($ws2, $B_fullscreen, 'amount of fullscreen windows in ws2'); is($node->{fullscreen_mode}, $B_fullscreen, 'A got B\'s fullscreen mode'); is(get_focused($ws3), $expected_focus, 'F is still focused'); diff --git a/testcases/t/293-focus-follows-mouse.t b/testcases/t/293-focus-follows-mouse.t new file mode 100644 index 000000000..0cd6e5c32 --- /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 000000000..93ebaee92 --- /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 000000000..41c0bf070 --- /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 000000000..047cc1197 --- /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 8e48bafe3..4ec33f663 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 21169adbc..065437f0b 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 e3753bec6..ac918fe37 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 217d898ca..2f7f2b27f 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; -# move a window over to the right output -cmd 'move right'; -my $event = $focus->recv; +sub focus_subtest { + my ($cmd, $want) = @_; -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'); + my @events = events_for( + sub { cmd $cmd }, + 'workspace'); -# reset and try again -$focus = AnyEvent->condvar; -cmd 'workspace ws-left'; -$focus->recv; + 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 +subtest 'move right (1)', \&focus_subtest, 'move right', 1; -$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'); +# move another window +cmd 'workspace ws-left'; +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 ff34328c6..875527850 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; - -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; - -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; - -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; +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(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 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'; + +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'; + +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'; + +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 000000000..8d2e84721 --- /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 000000000..5e5b9bf35 --- /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 000000000..82267baf6 --- /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/run-tests.sh b/travis/run-tests.sh index 44df81d2f..eac2ea8a3 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