diff --git a/.travis.yml b/.travis.yml index 8098035c..0133fc71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,4 @@ -sudo: false dist: trusty -# TODO: remove “group” once trusty kernel is no longer affected by -# https://github.com/google/sanitizers/issues/837 -group: deprecated-2017Q3 services: - docker language: c diff --git a/I3_VERSION b/I3_VERSION index 8162c4c8..4aa58fe3 100644 --- a/I3_VERSION +++ b/I3_VERSION @@ -1 +1 @@ -4.16.1-non-git +4.17-non-git diff --git a/Makefile.am b/Makefile.am index ee71b7f2..51c4f539 100644 --- a/Makefile.am +++ b/Makefile.am @@ -118,7 +118,7 @@ EXTRA_DIST = \ I3_VERSION \ LICENSE \ PACKAGE-MAINTAINER \ - RELEASE-NOTES-4.16.1 \ + RELEASE-NOTES-4.17 \ generate-command-parser.pl \ parser-specs/commands.spec \ parser-specs/config.spec \ @@ -277,6 +277,7 @@ i3_LDADD = \ libi3_CFLAGS = \ $(AM_CFLAGS) \ + $(GLIBGOBJECT_CFLAGS) \ $(XCB_CFLAGS) \ $(XCB_UTIL_CFLAGS) \ $(XCB_UTIL_XRM_CFLAGS) \ @@ -285,6 +286,7 @@ libi3_CFLAGS = \ libi3_LIBS = \ $(top_builddir)/libi3.a \ + $(GLIBGOBJECT_LIBS) \ $(XCB_LIBS) \ $(XCB_UTIL_LIBS) \ $(XCB_UTIL_XRM_LIBS) \ diff --git a/RELEASE-NOTES-4.17 b/RELEASE-NOTES-4.17 new file mode 100644 index 00000000..d5070705 --- /dev/null +++ b/RELEASE-NOTES-4.17 @@ -0,0 +1,96 @@ + + ┌────────────────────────────┐ + │ Release notes for i3 v4.17 │ + └────────────────────────────┘ + +This is i3 v4.17. This version is considered stable. All users of i3 are +strongly encouraged to upgrade. + +This release contains a number of assorted fixes and improvements across pretty +much all individual components of i3. + + ┌────────────────────────────┐ + │ Changes in i3 v4.17 │ + └────────────────────────────┘ + + • config: make binding modes case-sensitive + • default config: mention ~/.config/i3/config + • default config: start xss-lock, nm-applet, pactl (volume keys) + • docs/userguide: update syntax in strip_workspace_* + • docs/userguide: add a section about hidpi displays + • docs/userguide: document mark --replace + • docs/userguide: uncomment and update mark section example + • docs/userguide: point out differences of normal/pixel title bars + • docs/userguide: clarify which config directives can be used at runtime + • docs/userguide: for_window is a directive, not a command + • docs/ipc: clarify event/reply types + • docs/ipc: mention new i3-ipc++ C++ library + • docs/ipc: clarify restart/exit behavior + • docs/i3bar-protocol: add markup + • man/i3.man: fix config file search order + • ipc: make restart command send a reply once restart completed + • ipc: use queue for all messages + fixes i3bar issues when switching between workspaces with many windows + • i3-dump-log: clarify log message + • i3-msg: exit with status code 2 when i3 returns an error + • render left and right borders of titles in stacked mode + • make swap work with floating windows, fix swap crash + • switch to clang-format-6.0 + • add input and bounding shapes support + (e.g. for the https://github.com/phw/peek screen recorder) + • preserve back_and_forth across restarts + • allow partial UTF-8 to UCS-2 conversion for better handling of + title bar content which cannot be represented (e.g. emoji) + when using bitmap pixel fonts + • check for duplicate key bindings in i3 -C + • i3bar: support transparency via --transparency flag (RGBA) + • i3bar: support for user-defined border widths + + ┌────────────────────────────┐ + │ Bugfixes │ + └────────────────────────────┘ + + • build: correctly depend on glib (for g_utf8_make_valid) + • build: fix build when git is configured to show signatures + • ipc: report correct workspace in init event after workspace move + • ipc: send missing window:focus event + • i3bar: correctly recognize click events with text alignment + • i3bar: fix running without fd 0 + • i3bar: correctly handle button presses on separator + • i3 --moreversion: warn when $DISPLAY is unset + • i3bar: support disabling click events + • release.sh: persist correct version number in docs + • accept output names containing spaces (e.g. in assignment) + • fix cursor resizing positioning + • fix aspect ratio issues (e.g. with mpv) + • fix brief focus flicker when renaming workspaces + • fix crash when canceling i3 via ctrl+c + • fix heap-use-after-free, memory leak + • fix focus bugs in enabling/disabling RandR outputs + • fix crash with popups when fullscreen is non-leaf + • fix crash when moving a second window to mark + • fix crash with programs with splash screen + • fix atoms when closing inactive workspace + • apply title_align to non-leaf containers + • layout loading: correctly mark non-leaf containers + • truncate wm_name utf8 strings to first zero byte + (makes window titles work with buggy clients) + • fix crash in workspace moving + • export I3SOCK environment variable (again) + • fix hanging flaky testcase by using the correct X11 connection + • resize: add missing error replies + • don’t pop up floating windows on the wrong workspace + • remove extra \n from errx and die calls + + ┌────────────────────────────┐ + │ Thanks! │ + └────────────────────────────┘ + +Thanks for testing, bugfixes, discussions and everything I forgot go out to: + + aksel, Albert Safin, Alejandro Angulo, Christopher Hasse, Connor E, Hamish + Macdonald, Ingo Bürk, Iskustvo, Jeffrey Huxen, Jeremy Klotz, Jonathan + Woodlief, lasers, Morten Linderud, nejni-marji, Nguyễn Thái Ngọc Duy, Nils + ANDRÉ-CHANG, Oliver Kraitschy, Orestis Floros, TAL, Vladimir Panteleev + +-- Michael Stapelberg, 2019-08-03 diff --git a/configure.ac b/configure.ac index 6e867c7d..849ae5de 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Run autoreconf -fi to generate a configure script from this file. AC_PREREQ([2.69]) -AC_INIT([i3], [4.16.1], [https://github.com/i3/i3/issues]) +AC_INIT([i3], [4.17], [https://github.com/i3/i3/issues]) # For AX_EXTEND_SRCDIR AX_ENABLE_BUILDDIR AM_INIT_AUTOMAKE([foreign subdir-objects -Wall no-dist-gzip dist-bzip2]) @@ -32,7 +32,7 @@ AX_EXTEND_SRCDIR AS_IF([test -d ${srcdir}/.git], [ VERSION="$(git -C ${srcdir} describe --tags --abbrev=0)" - I3_VERSION="$(git -C ${srcdir} describe --tags --always) ($(git -C ${srcdir} log --pretty=format:%cd --date=short -n1), branch \\\"$(git -C ${srcdir} describe --tags --always --all | sed s:heads/::)\\\")" + I3_VERSION="$(git -C ${srcdir} describe --tags --always) ($(git -C ${srcdir} rev-list --format=%cd --date=short -n1 $(git rev-parse HEAD) | tail -n1), branch \\\"$(git -C ${srcdir} describe --tags --always --all | sed s:heads/::)\\\")" # Mirrors what libi3/is_debug_build.c does: is_release=$(test $(echo "${I3_VERSION}" | cut -d '(' -f 1 | wc -m) -lt 10 && echo yes || echo no) ], @@ -91,7 +91,7 @@ AX_PTHREAD dnl Each prefix corresponds to a source tarball which users might have dnl downloaded in a newer version and would like to overwrite. PKG_CHECK_MODULES([LIBSN], [libstartup-notification-1.0]) -PKG_CHECK_MODULES([XCB], [xcb xcb-xkb xcb-xinerama xcb-randr]) +PKG_CHECK_MODULES([XCB], [xcb xcb-xkb xcb-xinerama xcb-randr xcb-shape]) PKG_CHECK_MODULES([XCB_UTIL], [xcb-event xcb-util]) PKG_CHECK_MODULES([XCB_UTIL_CURSOR], [xcb-cursor]) PKG_CHECK_MODULES([XCB_UTIL_KEYSYMS], [xcb-keysyms]) @@ -101,6 +101,7 @@ PKG_CHECK_MODULES([XKBCOMMON], [xkbcommon xkbcommon-x11]) PKG_CHECK_MODULES([YAJL], [yajl]) PKG_CHECK_MODULES([LIBPCRE], [libpcre >= 8.10]) PKG_CHECK_MODULES([PANGOCAIRO], [cairo >= 1.14.4 pangocairo]) +PKG_CHECK_MODULES([GLIBGOBJECT], [glib-2.0 gobject-2.0]) # Checks for programs. AC_PROG_AWK diff --git a/debian/changelog b/debian/changelog index fc18d850..7333d43e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +i3-wm (4.16.1-1) unstable; urgency=medium + + * New upstream release. + + -- Michael Stapelberg Sun, 04 Nov 2018 14:47:25 +0100 + i3-wm (4.16-1) unstable; urgency=medium * New upstream release. diff --git a/debian/control b/debian/control index 71a2599c..843e6557 100644 --- a/debian/control +++ b/debian/control @@ -13,6 +13,7 @@ Build-Depends: debhelper (>= 9), libxcb-cursor-dev, libxcb-xrm-dev, libxcb-xkb-dev, + libxcb-shape0-dev, libxkbcommon-dev (>= 0.4.0), libxkbcommon-x11-dev (>= 0.4.0), asciidoc (>= 8.4.4), diff --git a/docs/i3bar-protocol b/docs/i3bar-protocol index 826cae53..76c51d80 100644 --- a/docs/i3bar-protocol +++ b/docs/i3bar-protocol @@ -141,6 +141,18 @@ background:: Overrides the background color for this particular block. border:: Overrides the border color for this particular block. +border_top:: + Defines the width (in pixels) of the top border of this block. Defaults + to 1. +border_right:: + Defines the width (in pixels) of the right border of this block. Defaults + to 1. +border_bottom:: + Defines the width (in pixels) of the bottom border of this block. Defaults + to 1. +border_left:: + Defines the width (in pixels) of the left border of this block. Defaults + to 1. min_width:: The minimum width (in pixels) of the block. If the content of the +full_text+ key take less space than the specified min_width, the block @@ -215,13 +227,18 @@ An example of a block which uses all possible entries follows: "color": "#00ff00", "background": "#1c1c1c", "border": "#ee0000", + "border_top": 1, + "border_right": 0, + "border_bottom": 3, + "border_left": 1, "min_width": 300, "align": "right", "urgent": false, "name": "ethernet", "instance": "eth0", "separator": true, - "separator_block_width": 9 + "separator_block_width": 9, + "markup": "none" } ------------------------------------------ diff --git a/docs/ipc b/docs/ipc index bcf8df1a..ea7a5892 100644 --- a/docs/ipc +++ b/docs/ipc @@ -80,7 +80,8 @@ Or, as a hexdump: ------------------------------------------------------------------------------ To generate and send such a message, you could use the following code in Perl: ------------------------------------------------------------- + +------------------------------------------------------------------------------- sub format_ipc_command { my ($msg) = @_; my $len; @@ -90,7 +91,7 @@ sub format_ipc_command { } $sock->write(format_ipc_command("exit")); ------------------------------------------------------------------------------- +------------------------------------------------------------------------------- == Receiving replies from i3 @@ -138,6 +139,20 @@ The reply consists of a list of serialized maps for each command that was parsed. Each has the property +success (bool)+ and may also include a human-readable error message in the property +error (string)+. +NOTE: When sending the `restart` command, you will get a singular reply once the +restart completed. All IPC connection states (e.g. subscriptions) will reset and +all but one socket will be closed. Libraries must be able to cope with this by +aligning their internal states. It is also recommended that libraries close +the last remaining socket(one which replied to `restart` command) to achieve +the full reset. + +NOTE: It is easiest to always send the `restart` command alone: due to i3’s +state reset, the reply messages of preceding commands are lost, and following +commands will not be executed. + +NOTE: When processing the `exit` command, i3 will immediately exit without +sending a reply. Expect the socket to be shut down. + *Example:* ------------------- [{ "success": true }] @@ -484,7 +499,7 @@ JSON dump: } ] } ------------------------- +----------------------- [[_marks_reply]] === MARKS reply @@ -681,9 +696,11 @@ responded to. To get informed when certain things happen in i3, clients can subscribe to events. Events consist of a name (like "workspace") and an event reply type -(like I3_IPC_EVENT_WORKSPACE). The events sent by i3 are in the same format -as replies to specific commands. However, the highest bit of the message type -is set to 1 to indicate that this is an event reply instead of a normal reply. +(like I3_IPC_EVENT_WORKSPACE). Events sent by i3 follow a format similar to +replies but with the highest bit of the message type set to 1 to indicate an +event reply instead of a normal reply. Note that event types and reply types +do not follow the same enumeration scheme (e.g. event type 0 corresponds to the +workspace event however reply type 0 corresponds to the COMMAND reply). Caveat: As soon as you subscribe to an event, it is not guaranteed any longer that the requests to i3 are processed in order. This means, the following @@ -768,7 +785,7 @@ This event consists of a single serialized map containing a property +change (string)+ which indicates the type of the change ("focus", "init", "empty", "urgent", "reload", "rename", "restored", "move"). A +current (object)+ property will be present with the affected workspace -whenever the type of event affects a workspace (otherwise, it will be +null). +whenever the type of event affects a workspace (otherwise, it will be +null+). When the change is "focus", an +old (object)+ property will be present with the previous workspace. When the first switch occurs (when i3 focuses the @@ -948,6 +965,7 @@ C:: * i3 includes a headerfile +i3/ipc.h+ which provides you all constants. * https://github.com/acrisci/i3ipc-glib C++:: + * https://github.com/Iskustvo/i3-ipcpp[i3-ipc++] * https://github.com/drmgc/i3ipcpp Go:: * https://github.com/mdirkse/i3ipc-go diff --git a/docs/userguide b/docs/userguide index 91060ab2..0fda7e80 100644 --- a/docs/userguide +++ b/docs/userguide @@ -603,6 +603,9 @@ This option determines which border style new windows will have. The default is +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. +Setting border style to +pixel+ eliminates title bars. The border style +normal+ allows you to +adjust edge border width while keeping your title bar. + *Syntax*: --------------------------------------------- default_border normal|none|pixel @@ -654,7 +657,7 @@ hide_edge_borders vertical [[for_window]] === Arbitrary commands for specific windows (for_window) -With the +for_window+ command, you can let i3 execute any command when it +With the +for_window+ directive, you can let i3 execute any command when it encounters a specific window. This can be used to set windows to floating or to change their border style, for example. @@ -677,7 +680,8 @@ for_window [class="urxvt"] border pixel 1 for_window [title="x200: ~/work"] floating enable ------------------------------------------------ -The valid criteria are the same as those for commands, see <>. +The valid criteria are the same as those for commands, see <>. Only config +directives with a command equivalent can be executed at runtime, see <>. [[no_focus]] === Don't focus window upon opening @@ -816,7 +820,8 @@ assign [class="^URxvt$"] → work # Assign to the workspace with number 2, regardless of name assign [class="^URxvt$"] → number 2 -# You can also specify a number + name. If the workspace with number 2 exists, assign will skip the text part. +# You can also specify a number + name. If the workspace with number 2 exists, +# assign will skip the text part. assign [class="^URxvt$"] → number "2: work" # Start urxvt -name irssi @@ -1627,14 +1632,16 @@ buttons. This is useful if you want to have a named workspace that stays in order on the bar according to its number without displaying the number prefix. When +strip_workspace_numbers+ is set to +yes+, any workspace that has a name of -the form "[n]:[NAME]" will display only the name. You could use this, for +the form "[n][:][NAME]" will display only the name. You could use this, for instance, to display Roman numerals rather than digits by naming your workspaces to "1:I", "2:II", "3:III", "4:IV", ... When +strip_workspace_name+ is set to +yes+, any workspace that has a name of -the form "[n]:[NAME]" will display only the number. +the form "[n][:][NAME]" will display only the number. -The default is to display the full name within the workspace button. +The default is to display the full name within the workspace button. Be aware +that the colon in the workspace name is optional, so `[n][NAME]` will also +have the the workspace name and number stripped correctly. *Syntax*: ------------------------------ @@ -1737,6 +1744,26 @@ bar { } -------------------------------------- +=== Transparency + +i3bar can support transparency by passing the +--transparency+ flag in the +configuration: + +*Syntax*: +-------------------------------------- +bar { + i3bar_command i3bar --transparency +} +-------------------------------------- + +In the i3bar color configuration and i3bar status block color attribute you can +then use colors in the RGBA format, i.e. the last two (hexadecimal) digits +specify the opacity. For example, +#00000000+ will be completely transparent, +while +#000000FF+ will be a fully opaque black (the same as +#000000+). + +Please note that due to the way the tray specification works, enabling this +flag will cause all tray icons to have a transparent background. + [[list_of_commands]] == List of commands @@ -1840,6 +1867,9 @@ The criteria +class+, +instance+, +role+, +title+, +workspace+ and +mark+ are actually regular expressions (PCRE). See +pcresyntax(3)+ or +perldoc perlre+ for information on how to use them. +Note that config directives listed under <> cannot be changed at runtime +unless they happen to have a command equivalent. + [[exec]] === Executing applications (exec) @@ -2105,8 +2135,7 @@ using one of the following methods: +mark+:: A container with the specified mark, see <>. Note that swapping does not work with all containers. Most notably, swapping -floating containers or containers that have a parent-child relationship to one -another does not work. +containers that have a parent-child relationship to one another does not work. *Syntax*: ---------------------------------------- @@ -2414,8 +2443,9 @@ this mark or add it otherwise. Note that you may need to use this in combination with +--add+ (see below) as any other marks will otherwise be removed. -By default, a window can only have one mark. You can use the +--add+ flag to -put more than one mark on a window. +The +--replace+ flag causes i3 to remove any existing marks, which is also the +default behavior. You can use the +--add+ flag to put more than one mark on a +window. Refer to <> if you don't want marks to be shown in the window decoration. @@ -2426,6 +2456,21 @@ mark [--add|--replace] [--toggle] unmark ---------------------------------------------- +You can use +i3-input+ to prompt for a mark name, then use the +mark+ +and +focus+ commands to create and jump to custom marks: + +*Examples*: +--------------------------------------- +# read 1 character and mark the current window with this character +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 -F '[con_mark="%s"] focus' -l 1 -P 'Goto: ' +--------------------------------------- + +Alternatively, if you do not want to mess with +i3-input+, you could create +separate bindings for a specific set of labels and then only use those labels: + *Example (in a terminal)*: --------------------------------------------------------- # marks the focused container @@ -2441,21 +2486,6 @@ unmark irssi [class="(?i)firefox"] unmark --------------------------------------------------------- -/////////////////////////////////////////////////////////////////// -TODO: make i3-input replace %s -*Examples*: ---------------------------------------- -# Read 1 character and mark the current window with this character -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 -F '[con_mark="%s"] focus' -l 1 -P 'Goto: ' ---------------------------------------- - -Alternatively, if you do not want to mess with +i3-input+, you could create -separate bindings for a specific set of labels and then only use those labels. -/////////////////////////////////////////////////////////////////// - [[pango_markup]] === Window title format @@ -2840,3 +2870,20 @@ and you are in multi-monitor mode (see <>). Because i3 is not a compositing window manager, there is no ability to display a window on two screens at the same time. Instead, your presentation software needs to do this job (that is, open a window on each screen). + +[[hidpi]] +=== High-resolution displays (aka HIDPI displays) + +See https://wiki.archlinux.org/index.php/HiDPI for details on how to enable +scaling in various parts of the Linux desktop. i3 will read the desired DPI from +the `Xft.dpi` property. The property defaults to 96 DPI, so to achieve 200% +scaling, you’d set `Xft.dpi: 192` in `~/.Xresources`. + +If you are a long-time i3 user who just got a new monitor, double-check that: + +* You are using a scalable font (starting with “pango:”) in your i3 config. + +* You are using a terminal emulator which supports scaling. You could + temporarily switch to gnome-terminal, which is known to support scaling out of + the box, until you figure out how to adjust the font size in your favorite + terminal emulator. diff --git a/etc/config b/etc/config index da51d570..a0f3b84d 100644 --- a/etc/config +++ b/etc/config @@ -17,12 +17,21 @@ font pango:monospace 8 # text rendering and scalability on retina/hidpi displays (thanks to pango). #font pango:DejaVu Sans Mono 8 -# Before i3 v4.8, we used to recommend this one as the default: -# font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 -# 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. +# The combination of xss-lock, nm-applet and pactl is a popular choice, so +# they are included here as an example. Modify as you see fit. + +# xss-lock grabs a logind suspend inhibit lock and will use i3lock to lock the +# screen before suspend. +exec --no-startup-id xss-lock --transfer-sleep-lock -- i3lock --nofork + +# NetworkManager is the most popular way to manage wireless networks on Linux, +# and nm-applet is a desktop environment-independent system tray GUI for it. +exec --no-startup-id nm-applet + +# Use pactl to adjust volume in PulseAudio. +bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +10% +bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -10% +bindsym XF86AudioMute exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle # use these keys for focus, movement, and resize directions when reaching for # the arrows is not convenient @@ -187,7 +196,8 @@ bar { # keysym-based config which used their favorite modifier (alt or windows) # # i3-config-wizard will not launch if there already is a config file -# in ~/.i3/config. +# in ~/.config/i3/config (or $XDG_CONFIG_HOME/i3/config if set) or +# ~/.i3/config. # # Please remove the following exec line: ####################################################################### diff --git a/etc/config.keycodes b/etc/config.keycodes index 6fc19426..6045ea68 100644 --- a/etc/config.keycodes +++ b/etc/config.keycodes @@ -18,12 +18,21 @@ font pango:monospace 8 # text rendering and scalability on retina/hidpi displays (thanks to pango). #font pango:DejaVu Sans Mono 8 -# Before i3 v4.8, we used to recommend this one as the default: -# font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 -# 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. +# The combination of xss-lock, nm-applet and pactl is a popular choice, so +# they are included here as an example. Modify as you see fit. + +# xss-lock grabs a logind suspend inhibit lock and will use i3lock to lock the +# screen before suspend. +exec --no-startup-id xss-lock --transfer-sleep-lock -- i3lock --nofork + +# NetworkManager is the most popular way to manage wireless networks on Linux, +# and nm-applet is a desktop environment-independent system tray GUI for it. +exec --no-startup-id nm-applet + +# Use pactl to adjust volume in PulseAudio. +bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +10% +bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -10% +bindsym XF86AudioMute exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle # Use Mouse+$mod to drag floating windows to their wanted position floating_modifier $mod diff --git a/i3-config-wizard/main.c b/i3-config-wizard/main.c index 4b556657..7c1f00a3 100644 --- a/i3-config-wizard/main.c +++ b/i3-config-wizard/main.c @@ -822,7 +822,7 @@ int main(int argc, char *argv[]) { int screen; if ((conn = xcb_connect(NULL, &screen)) == NULL || xcb_connection_has_error(conn)) - errx(1, "Cannot open display\n"); + errx(1, "Cannot open display"); if (xkb_x11_setup_xkb_extension(conn, XKB_X11_MIN_MAJOR_XKB_VERSION, @@ -859,7 +859,7 @@ int main(int argc, char *argv[]) { root = root_screen->root; if (!(modmap_reply = xcb_get_modifier_mapping_reply(conn, modmap_cookie, NULL))) - errx(EXIT_FAILURE, "Could not get modifier mapping\n"); + errx(EXIT_FAILURE, "Could not get modifier mapping"); xcb_numlock_mask = get_mod_mask_for(XCB_NUM_LOCK, symbols, modmap_reply); @@ -899,7 +899,7 @@ int main(int argc, char *argv[]) { do { \ xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(conn, name##_cookie, NULL); \ if (!reply) \ - errx(EXIT_FAILURE, "Could not get atom " #name "\n"); \ + errx(EXIT_FAILURE, "Could not get atom " #name); \ \ A_##name = reply->atom; \ free(reply); \ diff --git a/i3-dump-log/main.c b/i3-dump-log/main.c index e9901f8e..4f15f26f 100644 --- a/i3-dump-log/main.c +++ b/i3-dump-log/main.c @@ -155,7 +155,7 @@ 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. Enabling SHM log until cancelled\n\n"); + fprintf(stderr, "i3-dump-log: i3 is running, but SHM logging is not enabled. Enabling SHM log now while i3-dump-log is running\n\n"); ipcfd = ipc_connect(NULL); const char *enablecmd = "debuglog on; shmlog 5242880"; if (ipc_send_message(ipcfd, strlen(enablecmd), diff --git a/i3-input/main.c b/i3-input/main.c index d1a2efd7..f15a74ab 100644 --- a/i3-input/main.c +++ b/i3-input/main.c @@ -435,7 +435,7 @@ int main(int argc, char *argv[]) { int screen; conn = xcb_connect(NULL, &screen); if (!conn || xcb_connection_has_error(conn)) - die("Cannot open display\n"); + die("Cannot open display"); sockfd = ipc_connect(socket_path); diff --git a/i3-msg/main.c b/i3-msg/main.c index fe111416..3a897416 100644 --- a/i3-msg/main.c +++ b/i3-msg/main.c @@ -67,6 +67,7 @@ typedef struct reply_t { char *errorposition; } reply_t; +static int exit_code = 0; static reply_t last_reply; static int reply_boolean_cb(void *params, int val) { @@ -76,8 +77,8 @@ static int reply_boolean_cb(void *params, int val) { } static int reply_string_cb(void *params, const unsigned char *val, size_t len) { - char *str = scalloc(len + 1, 1); - strncpy(str, (const char *)val, len); + char *str = sstrndup((const char *)val, len); + if (strcmp(last_key, "error") == 0) last_reply.error = str; else if (strcmp(last_key, "input") == 0) @@ -100,14 +101,14 @@ static int reply_end_map_cb(void *params) { fprintf(stderr, "ERROR: %s\n", last_reply.errorposition); } fprintf(stderr, "ERROR: %s\n", last_reply.error); + exit_code = 2; } return 1; } static int reply_map_key_cb(void *params, const unsigned char *keyVal, size_t keyLen) { free(last_key); - last_key = scalloc(keyLen + 1, 1); - strncpy(last_key, (const char *)keyVal, keyLen); + last_key = sstrndup((const char *)keyVal, keyLen); return 1; } @@ -126,8 +127,7 @@ static yajl_callbacks reply_callbacks = { static char *config_last_key = NULL; static int config_string_cb(void *params, const unsigned char *val, size_t len) { - char *str = scalloc(len + 1, 1); - strncpy(str, (const char *)val, len); + char *str = sstrndup((const char *)val, len); if (strcmp(config_last_key, "config") == 0) { fprintf(stdout, "%s", str); } @@ -144,8 +144,7 @@ static int config_end_map_cb(void *params) { } static int config_map_key_cb(void *params, const unsigned char *keyVal, size_t keyLen) { - config_last_key = scalloc(keyLen + 1, 1); - strncpy(config_last_key, (const char *)keyVal, keyLen); + config_last_key = sstrndup((const char *)keyVal, keyLen); return 1; } @@ -326,5 +325,5 @@ int main(int argc, char *argv[]) { close(sockfd); - return 0; + return exit_code; } diff --git a/i3-nagbar/main.c b/i3-nagbar/main.c index 4ce74939..ec3e25fb 100644 --- a/i3-nagbar/main.c +++ b/i3-nagbar/main.c @@ -418,7 +418,7 @@ int main(int argc, char *argv[]) { int screens; if ((conn = xcb_connect(NULL, &screens)) == NULL || xcb_connection_has_error(conn)) - die("Cannot open display\n"); + die("Cannot open display"); /* Place requests for the atoms we need as soon as possible */ #define xmacro(atom) \ @@ -512,7 +512,7 @@ int main(int argc, char *argv[]) { do { \ xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(conn, name##_cookie, NULL); \ if (!reply) \ - die("Could not get atom " #name "\n"); \ + die("Could not get atom " #name); \ \ A_##name = reply->atom; \ free(reply); \ diff --git a/i3bar/include/common.h b/i3bar/include/common.h index 77be3182..fe7444a3 100644 --- a/i3bar/include/common.h +++ b/i3bar/include/common.h @@ -61,6 +61,10 @@ struct status_block { bool urgent; bool no_separator; + uint32_t border_top; + uint32_t border_right; + uint32_t border_bottom; + uint32_t border_left; bool pango_markup; /* The amount of pixels necessary to render a separater after the block. */ diff --git a/i3bar/include/configuration.h b/i3bar/include/configuration.h index b86da2e0..3d875e5d 100644 --- a/i3bar/include/configuration.h +++ b/i3bar/include/configuration.h @@ -48,6 +48,7 @@ typedef struct config_t { position_t position; bool verbose; + bool transparency; struct xcb_color_strings_t colors; bool disable_binding_mode_indicator; bool disable_ws; diff --git a/i3bar/src/child.c b/i3bar/src/child.c index 7c527dc3..83ee26b4 100644 --- a/i3bar/src/child.c +++ b/i3bar/src/child.c @@ -35,6 +35,7 @@ i3bar_child child; /* stdin- and SIGCHLD-watchers */ ev_io *stdin_io; +int stdin_fd; ev_child *child_sig; /* JSON parser for stdin */ @@ -175,6 +176,12 @@ static int stdin_start_map(void *context) { else ctx->block.sep_block_width = logical_px(8) + separator_symbol_width; + /* By default we draw all four borders if a border is set. */ + ctx->block.border_top = 1; + ctx->block.border_right = 1; + ctx->block.border_bottom = 1; + ctx->block.border_left = 1; + return 1; } @@ -261,6 +268,22 @@ static int stdin_integer(void *context, long long val) { ctx->block.sep_block_width = (uint32_t)val; return 1; } + if (strcasecmp(ctx->last_map_key, "border_top") == 0) { + ctx->block.border_top = (uint32_t)val; + return 1; + } + if (strcasecmp(ctx->last_map_key, "border_right") == 0) { + ctx->block.border_right = (uint32_t)val; + return 1; + } + if (strcasecmp(ctx->last_map_key, "border_bottom") == 0) { + ctx->block.border_bottom = (uint32_t)val; + return 1; + } + if (strcasecmp(ctx->last_map_key, "border_left") == 0) { + ctx->block.border_left = (uint32_t)val; + return 1; + } return 1; } @@ -450,7 +473,7 @@ static void stdin_io_first_line_cb(struct ev_loop *loop, ev_io *watcher, int rev } free(buffer); ev_io_stop(main_loop, stdin_io); - ev_io_init(stdin_io, &stdin_io_cb, STDIN_FILENO, EV_READ); + ev_io_init(stdin_io, &stdin_io_cb, stdin_fd, EV_READ); ev_io_start(main_loop, stdin_io); } @@ -562,17 +585,17 @@ void start_child(char *command) { close(pipe_in[1]); close(pipe_out[0]); - dup2(pipe_in[0], STDIN_FILENO); + stdin_fd = pipe_in[0]; child_stdin = pipe_out[1]; break; } /* We set O_NONBLOCK because blocking is evil in event-driven software */ - fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK); + fcntl(stdin_fd, F_SETFL, O_NONBLOCK); stdin_io = smalloc(sizeof(ev_io)); - ev_io_init(stdin_io, &stdin_io_first_line_cb, STDIN_FILENO, EV_READ); + ev_io_init(stdin_io, &stdin_io_first_line_cb, stdin_fd, EV_READ); ev_io_start(main_loop, stdin_io); /* We must cleanup, if the child unexpectedly terminates */ diff --git a/i3bar/src/main.c b/i3bar/src/main.c index a818dd97..26ea0eeb 100644 --- a/i3bar/src/main.c +++ b/i3bar/src/main.c @@ -56,13 +56,14 @@ static char *expand_path(char *path) { } static void print_usage(char *elf_name) { - printf("Usage: %s -b bar_id [-s sock_path] [-h] [-v]\n", elf_name); + printf("Usage: %s -b bar_id [-s sock_path] [-t] [-h] [-v]\n", elf_name); printf("\n"); - printf("-b, --bar_id \tBar ID for which to get the configuration\n"); - printf("-s, --socket \tConnect to i3 via \n"); - printf("-h, --help Display this help message and exit\n"); - printf("-v, --version Display version number and exit\n"); - printf("-V, --verbose Enable verbose mode\n"); + printf("-b, --bar_id \tBar ID for which to get the configuration\n"); + printf("-s, --socket \tConnect to i3 via \n"); + printf("-t, --transparency Enable transparency (RGBA colors)\n"); + printf("-h, --help Display this help message and exit\n"); + printf("-v, --version Display version number and exit\n"); + printf("-V, --verbose Enable verbose mode\n"); printf("\n"); printf(" PLEASE NOTE that i3bar will be automatically started by i3\n" " as soon as there is a 'bar' configuration block in your\n" @@ -105,12 +106,13 @@ int main(int argc, char **argv) { static struct option long_opt[] = { {"socket", required_argument, 0, 's'}, {"bar_id", required_argument, 0, 'b'}, + {"transparency", no_argument, 0, 't'}, {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'v'}, {"verbose", no_argument, 0, 'V'}, {NULL, 0, 0, 0}}; - while ((opt = getopt_long(argc, argv, "b:s:hvV", long_opt, &option_index)) != -1) { + while ((opt = getopt_long(argc, argv, "b:s:thvV", long_opt, &option_index)) != -1) { switch (opt) { case 's': socket_path = expand_path(optarg); @@ -122,6 +124,9 @@ int main(int argc, char **argv) { case 'b': config.bar_id = sstrdup(optarg); break; + case 't': + config.transparency = true; + break; case 'V': config.verbose = true; break; diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index 31ae08f0..412981bc 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -213,7 +213,7 @@ static uint32_t predict_statusline_length(bool use_short_text) { render->width = predict_text_width(text); if (block->border) - render->width += logical_px(2); + render->width += logical_px(block->border_left + block->border_right); /* Compute offset and append for text aligment in min_width. */ if (block->min_width <= render->width) { @@ -287,8 +287,8 @@ static void draw_statusline(i3_output *output, uint32_t clip_left, bool use_focu color_t bg_color = bar_color; - int border_width = (block->border) ? logical_px(1) : 0; int full_render_width = render->width + render->x_offset + render->x_append; + int has_border = block->border ? 1 : 0; if (block->border || block->background || block->urgent) { /* Let's determine the colors first. */ color_t border_color = bar_color; @@ -310,15 +310,16 @@ static void draw_statusline(i3_output *output, uint32_t clip_left, bool use_focu /* Draw the background. */ draw_util_rectangle(&output->statusline_buffer, bg_color, - x + border_width, - logical_px(1) + border_width, - full_render_width - 2 * border_width, - bar_height - 2 * border_width - logical_px(2)); + x + has_border * logical_px(block->border_left), + logical_px(1) + has_border * logical_px(block->border_top), + full_render_width - has_border * logical_px(block->border_right + block->border_left), + bar_height - has_border * logical_px(block->border_bottom + block->border_top) - logical_px(2)); } draw_util_text(text, &output->statusline_buffer, fg_color, bg_color, - x + render->x_offset + border_width, logical_px(ws_voff_px), - render->width - 2 * border_width); + x + render->x_offset + has_border * logical_px(block->border_left), + bar_height / 2 - font.height / 2, + render->width - has_border * logical_px(block->border_left + block->border_right)); x += full_render_width; /* If this is not the last block, draw a separator. */ @@ -454,6 +455,50 @@ static bool execute_custom_command(xcb_keycode_t input_code, bool event_is_relea return false; } +static void child_handle_button(xcb_button_press_event_t *event, i3_output *output, uint32_t statusline_x) { + if (statusline_x > (uint32_t)output->statusline_width) { + return; + } + + /* x of the start of the current block relative to the statusline. */ + uint32_t last_block_x = 0; + struct status_block *block; + TAILQ_FOREACH(block, &statusline_head, blocks) { + i3String *text; + struct status_block_render_desc *render; + if (output->statusline_short_text && block->short_text != NULL) { + text = block->short_text; + render = &block->short_render; + } else { + text = block->full_text; + render = &block->full_render; + } + + if (i3string_get_num_bytes(text) == 0) { + continue; + } + + /* Include the whole block in our calculations: when min_width is + * specified, we have to take padding width into account. */ + const uint32_t full_render_width = render->width + render->x_offset + render->x_append; + /* x of the click event relative to the current block. */ + const uint32_t relative_x = statusline_x - last_block_x; + if (relative_x <= full_render_width) { + send_block_clicked(event->detail, block->name, block->instance, + event->root_x, event->root_y, relative_x, + event->event_y, full_render_width, bar_height, + event->state); + return; + } + + last_block_x += full_render_width + block->sep_block_width; + if (last_block_x > statusline_x) { + /* Click was on a separator. */ + return; + } + } +} + /* * 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- @@ -479,10 +524,6 @@ static void handle_button(xcb_button_press_event_t *event) { /* 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; @@ -499,44 +540,32 @@ static void handle_button(xcb_button_press_event_t *event) { workspace_width += logical_px(ws_spacing_px); } - if (x > workspace_width && child_want_click_events()) { - /* If the child asked for click events, - * check if a status block has been clicked. */ - int tray_width = get_tray_width(walk->trayclients); - int last_block_x = 0; - int offset = walk->rect.w - walk->statusline_width - tray_width - logical_px((tray_width > 0) * sb_hoff_px); - int32_t statusline_x = x - offset; + if (child_want_click_events() && x > workspace_width) { + const int tray_width = get_tray_width(walk->trayclients); + /* Calculate the horizontal coordinate (x) of the start of the + * statusline by subtracting its width and the width of the tray from + * the bar width. */ + const int offset = walk->rect.w - walk->statusline_width - + tray_width - logical_px((tray_width > 0) * sb_hoff_px); + if (x >= offset) { + /* Click was after the start of the statusline, return to avoid + * executing any other actions even if a click event is not + * produced eventually. */ - if (statusline_x >= 0 && statusline_x < walk->statusline_width) { - struct status_block *block; - - TAILQ_FOREACH(block, &statusline_head, blocks) { - i3String *text = block->full_text; - struct status_block_render_desc *render = &block->full_render; - if (walk->statusline_short_text && block->short_text != NULL) { - text = block->short_text; - render = &block->short_render; - } - - if (i3string_get_num_bytes(text) == 0) - continue; - - const int relative_x = statusline_x - last_block_x; - if (relative_x >= 0 && (uint32_t)relative_x <= render->width) { - send_block_clicked(event->detail, block->name, block->instance, - event->root_x, event->root_y, relative_x, event->event_y, render->width, bar_height, - event->state); - return; - } - - last_block_x += render->width + render->x_append + render->x_offset + block->sep_block_width; + if (!event_is_release) { + /* x of the click event relative to the start of the + * statusline. */ + const uint32_t statusline_x = x - offset; + child_handle_button(event, walk, statusline_x); } + + return; } } /* If a custom command was specified for this mouse button, it overrides * the default behavior. */ - if (execute_custom_command(event->detail, event_is_release)) { + if (execute_custom_command(event->detail, event_is_release) || event_is_release) { return; } @@ -1169,7 +1198,21 @@ char *init_xcb_early(void) { depth = root_screen->root_depth; colormap = root_screen->default_colormap; - visual_type = get_visualtype(root_screen); + visual_type = config.transparency ? xcb_aux_find_visual_by_attrs(root_screen, -1, 32) : NULL; + if (visual_type != NULL) { + depth = xcb_aux_get_depth_of_visual(root_screen, visual_type->visual_id); + colormap = xcb_generate_id(xcb_connection); + xcb_void_cookie_t cm_cookie = xcb_create_colormap_checked(xcb_connection, + XCB_COLORMAP_ALLOC_NONE, + colormap, + xcb_root, + visual_type->visual_id); + if (xcb_request_failed(cm_cookie, "Could not allocate colormap")) { + exit(EXIT_FAILURE); + } + } else { + visual_type = get_visualtype(root_screen); + } xcb_cursor_context_t *cursor_ctx; if (xcb_cursor_context_new(conn, root_screen, &cursor_ctx) == 0) { diff --git a/include/commands_parser.h b/include/commands_parser.h index b65ae93f..31333af7 100644 --- a/include/commands_parser.h +++ b/include/commands_parser.h @@ -22,6 +22,10 @@ struct CommandResultIR { /* The JSON generator to append a reply to (may be NULL). */ yajl_gen json_gen; + /* The IPC client connection which sent this command (may be NULL, e.g. for + key bindings). */ + ipc_client *client; + /* The next state to transition to. Passed to the function so that we can * determine the next state as a result of a function call, like * cfg_criteria_pop_state() does. */ @@ -61,7 +65,7 @@ char *parse_string(const char **walk, bool as_word); * * Free the returned CommandResult with command_result_free(). */ -CommandResult *parse_command(const char *input, yajl_gen gen); +CommandResult *parse_command(const char *input, yajl_gen gen, ipc_client *client); /** * Frees a CommandResult diff --git a/include/con.h b/include/con.h index 2c991b0c..09ec7b44 100644 --- a/include/con.h +++ b/include/con.h @@ -378,13 +378,6 @@ orientation_t con_orientation(Con *con); */ Con *con_next_focused(Con *con); -/** - * Get the next/previous container in the specified orientation. This may - * travel up until it finds a container with suitable orientation. - * - */ -Con *con_get_next(Con *con, char way, orientation_t orientation); - /** * Returns the focused con inside this client, descending the tree as far as * possible. This comes in handy when attaching a con to a workspace at the @@ -533,3 +526,10 @@ i3String *con_parse_title_format(Con *con); * */ bool con_swap(Con *first, Con *second); + +/** + * Returns given container's rect size depending on its orientation. + * i.e. its width when horizontal, its height when vertical. + * + */ +uint32_t con_rect_size_in_orientation(Con *con); diff --git a/include/configuration.h b/include/configuration.h index 6f55ac2a..872f11c8 100644 --- a/include/configuration.h +++ b/include/configuration.h @@ -400,28 +400,24 @@ struct tray_output_t { tray_outputs; }; -/** - * Finds the configuration file to use (either the one specified by - * override_configpath), the user’s one or the system default) and calls - * parse_file(). - * - * If you specify override_configpath, only this path is used to look for a - * configuration file. - * - * If use_nagbar is false, don't try to start i3-nagbar but log the errors to - * stdout/stderr instead. - * - */ -bool parse_configuration(const char *override_configpath, bool use_nagbar); +typedef enum { + C_VALIDATE, + C_LOAD, + C_RELOAD, +} config_load_t; /** - * Reads the configuration from ~/.i3/config or /etc/i3/config if not found. + * (Re-)loads the configuration file (sets useful defaults before). * * If you specify override_configpath, only this path is used to look for a * configuration file. * + * load_type specifies the type of loading: C_VALIDATE is used to only verify + * the correctness of the config file (used with the flag -C). C_LOAD will load + * the config for normal use and display errors in the nagbar. C_RELOAD will + * also clear the previous config. */ -void load_configuration(xcb_connection_t *conn, const char *override_configfile, bool reload); +bool load_configuration(const char *override_configfile, config_load_t load_type); /** * Ungrabs all keys, to be called before re-grabbing the keys because of a @@ -435,14 +431,3 @@ void ungrab_all_keys(xcb_connection_t *conn); * */ void update_barconfig(void); - -/** - * Kills the configerror i3-nagbar process, if any. - * - * Called when reloading/restarting. - * - * If wait_for_it is set (restarting), this function will waitpid(), otherwise, - * ev is assumed to handle it (reloading). - * - */ -void kill_configerror_nagbar(bool wait_for_it); diff --git a/include/data.h b/include/data.h index f55e003d..c3cada37 100644 --- a/include/data.h +++ b/include/data.h @@ -482,7 +482,13 @@ struct Window { int max_height; /* aspect ratio from WM_NORMAL_HINTS (MPlayer uses this for example) */ - double aspect_ratio; + double min_aspect_ratio; + double max_aspect_ratio; + + /** The window has a nonrectangular shape. */ + bool shaped; + /** The window has a nonrectangular input shape. */ + bool input_shaped; }; /** diff --git a/include/ewmh.h b/include/ewmh.h index 01ae67f9..f616eb84 100644 --- a/include/ewmh.h +++ b/include/ewmh.h @@ -11,6 +11,12 @@ #include +/** + * Updates all the EWMH desktop properties. + * + */ +void ewmh_update_desktop_properties(void); + /** * Updates _NET_CURRENT_DESKTOP with the current desktop number. * @@ -20,24 +26,6 @@ */ void ewmh_update_current_desktop(void); -/** - * Updates _NET_NUMBER_OF_DESKTOPS which we interpret as the number of - * noninternal workspaces. - */ -void ewmh_update_number_of_desktops(void); - -/** - * Updates _NET_DESKTOP_NAMES: "The names of all virtual desktops. This is a - * list of NULL-terminated strings in UTF-8 encoding" - */ -void ewmh_update_desktop_names(void); - -/** - * Updates _NET_DESKTOP_VIEWPORT, which is an array of pairs of cardinals that - * define the top left corner of each desktop's viewport. - */ -void ewmh_update_desktop_viewport(void); - /** * Updates _NET_WM_DESKTOP for all windows. * A request will only be made if the cached value differs from the calculated value. diff --git a/include/floating.h b/include/floating.h index 4382437b..a7813099 100644 --- a/include/floating.h +++ b/include/floating.h @@ -94,12 +94,17 @@ void floating_drag_window(Con *con, const xcb_button_press_event_t *event); void floating_resize_window(Con *con, const bool proportional, const xcb_button_press_event_t *event); /** - * Called when a floating window is created or resized. - * This function resizes the window if its size is higher or lower than the - * configured maximum/minimum size, respectively. + * Called when a floating window is created or resized. This function resizes + * the window if its size is higher or lower than the configured maximum/minimum + * size, respectively or when adjustments are needed to conform to the + * configured size increments or aspect ratio limits. + * + * When prefer_height is true and the window needs to be resized because of the + * configured aspect ratio, the width is adjusted first, preserving the previous + * height. * */ -void floating_check_size(Con *floating_con); +void floating_check_size(Con *floating_con, bool prefer_height); /** * This is the return value of a drag operation like drag_pointer. @@ -152,7 +157,7 @@ bool floating_reposition(Con *con, Rect newrect); * window's size hints. * */ -void floating_resize(Con *floating_con, int x, int y); +void floating_resize(Con *floating_con, uint32_t x, uint32_t y); /** * Fixes the coordinates of the floating window whenever the window gets diff --git a/include/handlers.h b/include/handlers.h index 1d5a3865..d2c79c59 100644 --- a/include/handlers.h +++ b/include/handlers.h @@ -16,6 +16,7 @@ extern int randr_base; extern int xkb_base; +extern int shape_base; /** * Adds the given sequence to the list of events which are ignored. diff --git a/include/i3.h b/include/i3.h index 93a7e0a3..e7afe7e5 100644 --- a/include/i3.h +++ b/include/i3.h @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -70,7 +71,7 @@ extern uint8_t root_depth; extern xcb_visualid_t visual_id; extern xcb_colormap_t colormap; -extern bool xcursor_supported, xkb_supported; +extern bool xcursor_supported, xkb_supported, shape_supported; extern xcb_window_t root; extern struct ev_loop *main_loop; extern bool only_check_config; diff --git a/include/ipc.h b/include/ipc.h index a7743973..a608ef56 100644 --- a/include/ipc.h +++ b/include/ipc.h @@ -72,6 +72,16 @@ typedef void (*handler_t)(ipc_client *, uint8_t *, int, uint32_t, uint32_t); */ void ipc_new_client(EV_P_ struct ev_io *w, int revents); +/** + * ipc_new_client_on_fd() only sets up the event handler + * for activity on the new connection and inserts the file descriptor into + * the list of clients. + * + * This variant is useful for the inherited IPC connection when restarting. + * + */ +ipc_client *ipc_new_client_on_fd(EV_P_ int fd); + /** * Creates the UNIX domain socket at the given path, sets it to non-blocking * mode, bind()s and listen()s on it. @@ -95,10 +105,13 @@ typedef enum { } shutdown_reason_t; /** - * Calls shutdown() on each socket and closes it. + * Calls shutdown() on each socket and closes it. This function is to be called + * when exiting or restarting only! + * + * exempt_fd is never closed. Set to -1 to close all fds. * */ -void ipc_shutdown(shutdown_reason_t reason); +void ipc_shutdown(shutdown_reason_t reason, int exempt_fd); void dump_node(yajl_gen gen, Con *con, bool inplace_restart); @@ -136,3 +149,8 @@ void ipc_send_binding_event(const char *event_type, Binding *bind); * socket. */ void ipc_set_kill_timeout(ev_tstamp new); + +/** + * Sends a restart reply to the IPC client on the specified fd. + */ +void ipc_confirm_restart(ipc_client *client); diff --git a/include/randr.h b/include/randr.h index ec533a28..ae6a20a9 100644 --- a/include/randr.h +++ b/include/randr.h @@ -48,7 +48,7 @@ void output_init_con(Output *output); * • Create the first unused workspace. * */ -void init_ws_for_output(Output *output, Con *content); +void init_ws_for_output(Output *output); /** * Initializes the specified output, assigning the specified workspace to it. diff --git a/include/render.h b/include/render.h index 2b2c8dad..03751c01 100644 --- a/include/render.h +++ b/include/render.h @@ -40,7 +40,7 @@ typedef struct render_params { * updated in X11. * */ -void render_con(Con *con, bool render_fullscreen); +void render_con(Con *con); /** * Returns the height for the decorations diff --git a/include/resize.h b/include/resize.h index 72dffc0f..162d8f6b 100644 --- a/include/resize.h +++ b/include/resize.h @@ -31,9 +31,3 @@ bool resize_neighboring_cons(Con *first, Con *second, int px, int ppt); * */ double percent_for_1px(Con *con); - -/** - * Calculate the given container's new percent given a change in pixels. - * - */ -double px_resize_to_percent(Con *con, int px_diff); diff --git a/include/tree.h b/include/tree.h index 41a63036..12170f94 100644 --- a/include/tree.h +++ b/include/tree.h @@ -73,10 +73,6 @@ void tree_next(char way, orientation_t orientation); * The dont_kill_parent flag is specified when the function calls itself * recursively while deleting a containers children. * - * The force_set_focus flag is specified in the case of killing a floating - * window: tree_close_internal() will be invoked for the CT_FLOATINGCON (the parent - * container) and focus should be set there. - * */ bool tree_close_internal(Con *con, kill_window_t kill_window, bool dont_kill_parent); diff --git a/include/window.h b/include/window.h index 77e3f48f..b03f9c14 100644 --- a/include/window.h +++ b/include/window.h @@ -70,6 +70,12 @@ void window_update_role(i3Window *win, xcb_get_property_reply_t *prop, bool befo */ void window_update_type(i3Window *window, xcb_get_property_reply_t *reply); +/** + * Updates the WM_NORMAL_HINTS + * + */ +bool window_update_normal_hints(i3Window *win, xcb_get_property_reply_t *reply, xcb_get_geometry_reply_t *geom); + /** * Updates the WM_HINTS (we only care about the input focus handling part). * diff --git a/include/workspace.h b/include/workspace.h index 28d9eb66..69974a2e 100644 --- a/include/workspace.h +++ b/include/workspace.h @@ -24,6 +24,13 @@ #define NET_WM_DESKTOP_NONE 0xFFFFFFF0 #define NET_WM_DESKTOP_ALL 0xFFFFFFFF +/** + * Stores a copy of the name of the last used workspace for the workspace + * back-and-forth switching. + * + */ +extern char *previous_workspace_name; + /** * Returns the workspace with the given name or NULL if such a workspace does * not exist. @@ -211,7 +218,6 @@ Con *workspace_encapsulate(Con *ws); /** * Move the given workspace to the specified output. - * This returns true if and only if moving the workspace was successful. * */ -bool workspace_move_to_output(Con *ws, Output *output); +void workspace_move_to_output(Con *ws, Output *output); diff --git a/include/x.h b/include/x.h index 8b7664f2..d01709ed 100644 --- a/include/x.h +++ b/include/x.h @@ -137,3 +137,8 @@ void x_set_warp_to(Rect *rect); * */ void x_mask_event_mask(uint32_t mask); + +/** + * Enables or disables nonrectangular shape of the container frame. + */ +void x_set_shape(Con *con, xcb_shape_sk_t kind, bool enable); diff --git a/libi3/font.c b/libi3/font.c index c06bae00..32744c0b 100644 --- a/libi3/font.c +++ b/libi3/font.c @@ -163,6 +163,7 @@ i3Font load_font(const char *pattern, const bool fallback) { i3Font font; font.type = FONT_TYPE_NONE; + font.pattern = NULL; /* No XCB connction, return early because we're just validating the * configuration file. */ @@ -435,6 +436,7 @@ static int xcb_query_text_width(const xcb_char2b_t *text, size_t text_len) { * a crash. Plus, the user will see the error in their log. */ fprintf(stderr, "Could not get text extents (X error code %d)\n", error->error_code); + free(error); return savedFont->specific.xcb.info->max_bounds.character_width * text_len; } diff --git a/libi3/ucs2_conversion.c b/libi3/ucs2_conversion.c index 398c1ae5..c651cdb3 100644 --- a/libi3/ucs2_conversion.c +++ b/libi3/ucs2_conversion.c @@ -69,32 +69,41 @@ xcb_char2b_t *convert_utf8_to_ucs2(char *input, size_t *real_strlen) { xcb_char2b_t *buffer = smalloc(buffer_size); /* We need to use an additional pointer, because iconv() modifies it */ - size_t output_size = buffer_size; + size_t output_bytes_left = buffer_size; xcb_char2b_t *output = buffer; if (ucs2_conversion_descriptor == (iconv_t)-1) { - /* Get a new conversion descriptor */ - ucs2_conversion_descriptor = iconv_open("UCS-2BE", "UTF-8"); - if (ucs2_conversion_descriptor == (iconv_t)-1) + /* Get a new conversion descriptor. //IGNORE is a GNU suffix that makes + * iconv to silently discard characters that cannot be represented in + * the target character set. */ + ucs2_conversion_descriptor = iconv_open("UCS-2BE//IGNORE", "UTF-8"); + if (ucs2_conversion_descriptor == (iconv_t)-1) { + ucs2_conversion_descriptor = iconv_open("UCS-2BE", "UTF-8"); + } + if (ucs2_conversion_descriptor == (iconv_t)-1) { err(EXIT_FAILURE, "Error opening the conversion context"); + } } else { /* Reset the existing conversion descriptor */ iconv(ucs2_conversion_descriptor, NULL, NULL, NULL, NULL); } /* Do the conversion */ - size_t rc = iconv(ucs2_conversion_descriptor, &input, &input_size, (char **)&output, &output_size); + size_t rc = iconv(ucs2_conversion_descriptor, &input, &input_size, + (char **)&output, &output_bytes_left); if (rc == (size_t)-1) { + /* Conversion will only be partial. */ perror("Converting to UCS-2 failed"); - free(buffer); - if (real_strlen != NULL) - *real_strlen = 0; - return NULL; } + /* If no bytes where converted, this is equivalent to freeing buffer. */ + buffer_size -= output_bytes_left; + buffer = srealloc(buffer, buffer_size); + /* Return the resulting string's length */ - if (real_strlen != NULL) - *real_strlen = (buffer_size - output_size) / sizeof(xcb_char2b_t); + if (real_strlen != NULL) { + *real_strlen = buffer_size / sizeof(xcb_char2b_t); + } return buffer; } diff --git a/man/i3-msg.man b/man/i3-msg.man index 625131de..ce9b476d 100644 --- a/man/i3-msg.man +++ b/man/i3-msg.man @@ -90,6 +90,15 @@ See the -m option for continuous monitoring. i3-msg is a sample implementation for a client using the unix socket IPC interface to i3. +=== Exit status: + +0: +if OK, +1: +if invalid syntax or unable to connect to ipc-socket +2: +if i3 returned an error processing your command(s) + == EXAMPLES ------------------------------------------------ diff --git a/man/i3.man b/man/i3.man index 640b5ac8..8157ac77 100644 --- a/man/i3.man +++ b/man/i3.man @@ -166,7 +166,7 @@ Exits i3. == FILES -=== \~/.i3/config (or ~/.config/i3/config) +=== \~/.config/i3/config (or ~/.i3/config) When starting, i3 looks for configuration files in the following order: diff --git a/parser-specs/config.spec b/parser-specs/config.spec index 43181c59..a256d8b0 100644 --- a/parser-specs/config.spec +++ b/parser-specs/config.spec @@ -277,11 +277,13 @@ state WORKSPACE: state WORKSPACE_OUTPUT: 'output' - -> WORKSPACE_OUTPUT_STR + -> WORKSPACE_OUTPUT_WORD -state WORKSPACE_OUTPUT_STR: - output = string - -> call cfg_workspace($workspace, $output) +state WORKSPACE_OUTPUT_WORD: + output = word + -> call cfg_workspace($workspace, $output); WORKSPACE_OUTPUT_WORD + end + -> INITIAL # ipc-socket state IPC_SOCKET: diff --git a/release.sh b/release.sh index 0190fcfb..97d91868 100755 --- a/release.sh +++ b/release.sh @@ -1,8 +1,8 @@ #!/bin/zsh # This script is used to prepare a new release of i3. -export RELEASE_VERSION="4.15" -export PREVIOUS_VERSION="4.14" +export RELEASE_VERSION="4.16" +export PREVIOUS_VERSION="4.15" export RELEASE_BRANCH="next" if [ ! -e "../i3.github.io" ] @@ -155,6 +155,12 @@ git checkout ${RELEASE_BRANCH} cd ${TMPDIR} git clone --quiet ${STARTDIR}/../i3.github.io cd i3.github.io + +mkdir docs/${PREVIOUS_VERSION} +tar cf - '--exclude=[0-9]\.[0-9e]*' docs | tar xf - --strip-components=1 -C docs/${PREVIOUS_VERSION} +git add docs/${PREVIOUS_VERSION} +git commit -a -m "save docs for ${PREVIOUS_VERSION}" + cp ${TMPDIR}/i3/i3-${RELEASE_VERSION}.tar.bz2* downloads/ git add downloads/i3-${RELEASE_VERSION}.tar.bz2* cp ${TMPDIR}/i3/RELEASE-NOTES-${RELEASE_VERSION} downloads/RELEASE-NOTES-${RELEASE_VERSION}.txt @@ -166,11 +172,6 @@ sed -i "s,,\n \n ${RELEASE_VERSION}\n dest.command); char *full_command; sasprintf(&full_command, "[id=\"%d\"] %s", window->id, current->dest.command); - CommandResult *result = parse_command(full_command, NULL); + CommandResult *result = parse_command(full_command, NULL, NULL); free(full_command); if (result->needs_tree_render) diff --git a/src/bindings.c b/src/bindings.c index 6704c816..f2921a14 100644 --- a/src/bindings.c +++ b/src/bindings.c @@ -123,7 +123,7 @@ static bool binding_in_current_group(const Binding *bind) { } static void grab_keycode_for_binding(xcb_connection_t *conn, Binding *bind, uint32_t keycode) { -/* Grab the key in all combinations */ + /* Grab the key in all combinations */ #define GRAB_KEY(modifier) \ do { \ xcb_grab_key(conn, 0, root, modifier, keycode, XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC); \ @@ -621,7 +621,7 @@ void switch_mode(const char *new_mode) { DLOG("Switching to mode %s\n", new_mode); SLIST_FOREACH(mode, &modes, modes) { - if (strcasecmp(mode->name, new_mode) != 0) + if (strcmp(mode->name, new_mode) != 0) continue; ungrab_all_keys(conn); @@ -824,7 +824,7 @@ CommandResult *run_binding(Binding *bind, Con *con) { sasprintf(&command, "[con_id=\"%p\"] %s", con, bind->command); Binding *bind_cp = binding_copy(bind); - CommandResult *result = parse_command(command, NULL); + CommandResult *result = parse_command(command, NULL, NULL); free(command); if (result->needs_tree_render) diff --git a/src/click.c b/src/click.c index 1218a4c2..58ebbf3d 100644 --- a/src/click.c +++ b/src/click.c @@ -302,12 +302,6 @@ static int route_click(Con *con, xcb_button_press_event_t *event, const bool mod goto done; } - if (in_stacked) { - /* for stacked/tabbed cons, the resizing applies to the parent - * container */ - con = con->parent; - } - /* 7: floating modifier pressed, initiate a resize */ if (dest == CLICK_INSIDE && mod_pressed && event->detail == XCB_BUTTON_CLICK_RIGHT) { if (floating_mod_on_tiled_client(con, event)) diff --git a/src/commands.c b/src/commands.c index b3c1f5e0..aadf204f 100644 --- a/src/commands.c +++ b/src/commands.c @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include "shmlog.h" @@ -463,7 +465,7 @@ static void cmd_resize_floating(I3_CMD, const char *way, const char *direction_s } else { floating_con->rect.width += px; } - floating_check_size(floating_con); + floating_check_size(floating_con, orientation == VERT); /* Did we actually resize anything or did the size constraints prevent us? * If we could not resize, exit now to not move the window. */ @@ -533,8 +535,9 @@ static bool cmd_resize_tiling_width_height(I3_CMD, Con *current, const char *dir if (ppt != 0.0) { new_current_percent = current->percent + ppt; } else { - new_current_percent = px_resize_to_percent(current, px); - ppt = new_current_percent - current->percent; + /* Convert px change to change in percentages */ + ppt = (double)px / (double)con_rect_size_in_orientation(current->parent); + new_current_percent = current->percent + ppt; } subtract_percent = ppt / (children - 1); if (ppt < 0.0 && new_current_percent < percent_for_1px(current)) { @@ -600,13 +603,17 @@ void cmd_resize(I3_CMD, const char *way, const char *direction, long resize_px, const double ppt = (double)resize_ppt / 100.0; if (!cmd_resize_tiling_width_height(current_match, cmd_output, current->con, direction, - resize_px, ppt)) + resize_px, ppt)) { + yerror("Cannot resize."); return; + } } else { if (!cmd_resize_tiling_direction(current_match, cmd_output, current->con, direction, - resize_px, resize_ppt)) + resize_px, resize_ppt)) { + yerror("Cannot resize."); return; + } } } } @@ -652,7 +659,7 @@ static bool resize_set_tiling(I3_CMD, Con *target, orientation_t resize_orientat 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); + yerror("Dimensions cannot be negative."); return; } @@ -777,6 +784,7 @@ void cmd_append_layout(I3_CMD, const char *cpath) { char *buf = NULL; ssize_t len; if ((len = slurp(path, &buf)) < 0) { + yerror("Could not slurp \"%s\".", path); /* slurp already logged an error. */ goto out; } @@ -826,7 +834,7 @@ void cmd_append_layout(I3_CMD, const char *cpath) { // is not executed yet and will be batched with append_layout’s // needs_tree_render after the parser finished. We should check if that is // necessary at all. - render_con(croot, false); + render_con(croot); restore_open_placeholder_windows(parent); @@ -1108,22 +1116,13 @@ void cmd_move_workspace_to_output(I3_CMD, const char *name) { } Output *current_output = get_output_for_con(ws); - if (current_output == NULL) { - yerror("Cannot get current output. This is a bug in i3."); - return; - } - Output *target_output = get_output_from_string(current_output, name); if (!target_output) { yerror("Could not get output from string \"%s\"", name); return; } - bool success = workspace_move_to_output(ws, target_output); - if (!success) { - yerror("Failed to move workspace to output."); - return; - } + workspace_move_to_output(ws, target_output); } cmd_output->needs_tree_render = true; @@ -1508,7 +1507,7 @@ void cmd_layout(I3_CMD, const char *layout_str) { layout_t layout; if (!layout_from_name(layout_str, &layout)) { - ELOG("Unknown layout \"%s\", this is a mismatch between code and parser spec.\n", layout_str); + yerror("Unknown layout \"%s\", this is a mismatch between code and parser spec.", layout_str); return; } @@ -1576,7 +1575,7 @@ void cmd_reload(I3_CMD) { LOG("reloading\n"); kill_nagbar(&config_error_nagbar_pid, false); kill_nagbar(&command_error_nagbar_pid, false); - load_configuration(conn, NULL, true); + load_configuration(NULL, C_RELOAD); x_set_i3_atoms(); /* Send an IPC event just in case the ws names have changed */ ipc_send_workspace_event("reload", NULL, NULL); @@ -1593,15 +1592,27 @@ void cmd_reload(I3_CMD) { */ void cmd_restart(I3_CMD) { LOG("restarting i3\n"); - ipc_shutdown(SHUTDOWN_REASON_RESTART); + int exempt_fd = -1; + if (cmd_output->client != NULL) { + exempt_fd = cmd_output->client->fd; + LOG("Carrying file descriptor %d across restart\n", exempt_fd); + int flags; + if ((flags = fcntl(exempt_fd, F_GETFD)) < 0 || + fcntl(exempt_fd, F_SETFD, flags & ~FD_CLOEXEC) < 0) { + ELOG("Could not disable FD_CLOEXEC on fd %d\n", exempt_fd); + } + char *fdstr = NULL; + sasprintf(&fdstr, "%d", exempt_fd); + setenv("_I3_RESTART_FD", fdstr, 1); + } + ipc_shutdown(SHUTDOWN_REASON_RESTART, exempt_fd); unlink(config.ipc_socket_path); /* We need to call this manually since atexit handlers don’t get called * when exec()ing */ purge_zerobyte_logfile(); i3_restart(false); - - // XXX: default reply for now, make this a better reply - ysuccess(true); + /* unreached */ + assert(false); } /* @@ -1629,24 +1640,18 @@ void cmd_open(I3_CMD) { * */ void cmd_focus_output(I3_CMD, const char *name) { - owindow *current; - - DLOG("name = %s\n", name); - HANDLE_EMPTY_MATCH; - /* get the output */ - Output *current_output = NULL; - Output *output; + if (TAILQ_EMPTY(&owindows)) { + ysuccess(true); + return; + } - TAILQ_FOREACH(current, &owindows, owindows) - current_output = get_output_for_con(current->con); - assert(current_output != NULL); - - output = get_output_from_string(current_output, name); + Output *current_output = get_output_for_con(TAILQ_FIRST(&owindows)->con); + Output *output = get_output_from_string(current_output, name); if (!output) { - yerror("No such output found."); + yerror("Output %s not found.", name); return; } @@ -1661,7 +1666,6 @@ void cmd_focus_output(I3_CMD, const char *name) { workspace_show(ws); cmd_output->needs_tree_render = true; - // XXX: default reply for now, make this a better reply ysuccess(true); } @@ -2012,9 +2016,7 @@ void cmd_rename_workspace(I3_CMD, const char *old_name, const char *new_name) { cmd_output->needs_tree_render = true; ysuccess(true); - ewmh_update_desktop_names(); - ewmh_update_desktop_viewport(); - ewmh_update_current_desktop(); + ewmh_update_desktop_properties(); startup_sequence_rename_workspace(old_name_copy, new_name); free(old_name_copy); diff --git a/src/commands_parser.c b/src/commands_parser.c index 4299c008..d4ae430b 100644 --- a/src/commands_parser.c +++ b/src/commands_parser.c @@ -181,6 +181,7 @@ static struct CommandResultIR command_output; static void next_state(const cmdp_token *token) { if (token->next_state == __CALL) { subcommand_output.json_gen = command_output.json_gen; + subcommand_output.client = command_output.client; subcommand_output.needs_tree_render = false; GENERATED_call(token->extra.call_identifier, &subcommand_output); state = subcommand_output.next_state; @@ -261,11 +262,13 @@ char *parse_string(const char **walk, bool as_word) { * * Free the returned CommandResult with command_result_free(). */ -CommandResult *parse_command(const char *input, yajl_gen gen) { +CommandResult *parse_command(const char *input, yajl_gen gen, ipc_client *client) { DLOG("COMMAND: *%s*\n", input); state = INITIAL; CommandResult *result = scalloc(1, sizeof(CommandResult)); + command_output.client = client; + /* A YAJL JSON generator used for formatting replies. */ command_output.json_gen = gen; @@ -353,7 +356,7 @@ CommandResult *parse_command(const char *input, yajl_gen gen) { if (*walk == '\0' || *walk == ',' || *walk == ';') { next_state(token); token_handled = true; -/* To make sure we start with an appropriate matching + /* To make sure we start with an appropriate matching * datastructure for commands which do *not* specify any * criteria, we re-initialize the criteria system after * every command. */ @@ -499,7 +502,7 @@ int main(int argc, char *argv[]) { } yajl_gen gen = yajl_gen_alloc(NULL); - CommandResult *result = parse_command(argv[1], gen); + CommandResult *result = parse_command(argv[1], gen, NULL); command_result_free(result); diff --git a/src/con.c b/src/con.c index 21d2f097..10afb4cc 100644 --- a/src/con.c +++ b/src/con.c @@ -46,7 +46,6 @@ Con *con_new_skeleton(Con *parent, i3Window *window) { new->current_border_width = -1; if (window) { new->depth = window->depth; - new->window->aspect_ratio = 0.0; } else { new->depth = root_depth; } @@ -540,7 +539,6 @@ bool con_is_internal(Con *con) { */ bool con_is_floating(Con *con) { assert(con != NULL); - DLOG("checking if con %p is floating\n", con); return (con->floating >= FLOATING_AUTO_ON); } @@ -1401,8 +1399,6 @@ void con_move_to_output(Con *con, Output *output, bool fix_coordinates) { */ bool con_move_to_output_name(Con *con, const char *name, bool fix_coordinates) { Output *current_output = get_output_for_con(con); - assert(current_output != NULL); - Output *output = get_output_from_string(current_output, name); if (output == NULL) { ELOG("Could not find output \"%s\"\n", name); @@ -1486,42 +1482,6 @@ Con *con_next_focused(Con *con) { return next; } -/* - * Get the next/previous container in the specified orientation. This may - * travel up until it finds a container with suitable orientation. - * - */ -Con *con_get_next(Con *con, char way, orientation_t orientation) { - DLOG("con_get_next(way=%c, orientation=%d)\n", way, orientation); - /* 1: get the first parent with the same orientation */ - Con *cur = con; - while (con_orientation(cur->parent) != orientation) { - DLOG("need to go one level further up\n"); - if (cur->parent->type == CT_WORKSPACE) { - LOG("that's a workspace, we can't go further up\n"); - return NULL; - } - cur = cur->parent; - } - - /* 2: chose next (or previous) */ - Con *next; - if (way == 'n') { - next = TAILQ_NEXT(cur, nodes); - /* if we are at the end of the list, we need to wrap */ - if (next == TAILQ_END(&(parent->nodes_head))) - return NULL; - } else { - next = TAILQ_PREV(cur, nodes_head, nodes); - /* if we are at the end of the list, we need to wrap */ - if (next == TAILQ_END(&(cur->nodes_head))) - return NULL; - } - DLOG("next = %p\n", next); - - return next; -} - /* * Returns the focused con inside this client, descending the tree as far as * possible. This comes in handy when attaching a con to a workspace at the @@ -2333,11 +2293,6 @@ bool con_swap(Con *first, Con *second) { return false; } - if (con_is_floating(first) || con_is_floating(second)) { - ELOG("Floating windows cannot be swapped.\n"); - return false; - } - if (first == second) { DLOG("Swapping container %p with itself, nothing to do.\n", first); return false; @@ -2348,132 +2303,80 @@ bool con_swap(Con *first, Con *second) { return false; } - Con *old_focus = focused; - - Con *first_ws = con_get_workspace(first); - Con *second_ws = con_get_workspace(second); - Con *current_ws = con_get_workspace(old_focus); - const bool focused_within_first = (first == old_focus || con_has_parent(old_focus, first)); - const bool focused_within_second = (second == old_focus || con_has_parent(old_focus, second)); - fullscreen_mode_t first_fullscreen_mode = first->fullscreen_mode; - fullscreen_mode_t second_fullscreen_mode = second->fullscreen_mode; - - if (first_fullscreen_mode != CF_NONE) { - con_disable_fullscreen(first); - } - if (second_fullscreen_mode != CF_NONE) { - con_disable_fullscreen(second); + Con *ws1 = con_get_workspace(first); + Con *ws2 = con_get_workspace(second); + Con *restore_focus = NULL; + if (ws1 == ws2 && ws1 == con_get_workspace(focused)) { + /* Preserve focus in the current workspace. */ + restore_focus = focused; + } else if (first == focused || con_has_parent(focused, first)) { + restore_focus = second; + } else if (second == focused || con_has_parent(focused, second)) { + restore_focus = first; } - double first_percent = first->percent; - double second_percent = second->percent; +#define SWAP_CONS_IN_TREE(headname, field) \ + do { \ + struct headname *head1 = &(first->parent->headname); \ + struct headname *head2 = &(second->parent->headname); \ + Con *first_prev = TAILQ_PREV(first, headname, field); \ + Con *second_prev = TAILQ_PREV(second, headname, field); \ + if (second_prev == first) { \ + TAILQ_SWAP(first, second, head1, field); \ + } else if (first_prev == second) { \ + TAILQ_SWAP(second, first, head1, field); \ + } else { \ + TAILQ_REMOVE(head1, first, field); \ + TAILQ_REMOVE(head2, second, field); \ + if (second_prev == NULL) { \ + TAILQ_INSERT_HEAD(head2, first, field); \ + } else { \ + TAILQ_INSERT_AFTER(head2, second_prev, first, field); \ + } \ + if (first_prev == NULL) { \ + TAILQ_INSERT_HEAD(head1, second, field); \ + } else { \ + TAILQ_INSERT_AFTER(head1, first_prev, second, field); \ + } \ + } \ + } while (0) - /* De- and reattaching the containers will insert them at the tail of the - * focus_heads. We will need to fix this. But we need to make sure first - * and second don't get in each other's way if they share the same parent, - * so we select the closest previous focus_head that isn't involved. */ - Con *first_prev_focus_head = first; - while (first_prev_focus_head == first || first_prev_focus_head == second) { - first_prev_focus_head = TAILQ_PREV(first_prev_focus_head, focus_head, focused); - } + SWAP_CONS_IN_TREE(nodes_head, nodes); + SWAP_CONS_IN_TREE(focus_head, focused); + SWAP(first->parent, second->parent, Con *); - Con *second_prev_focus_head = second; - while (second_prev_focus_head == second || second_prev_focus_head == first) { - second_prev_focus_head = TAILQ_PREV(second_prev_focus_head, focus_head, focused); - } - - /* We use a fake container to mark the spot of where the second container needs to go. */ - Con *fake = con_new(NULL, NULL); - fake->layout = L_SPLITH; - _con_attach(fake, first->parent, first, true); - - bool result = true; - /* Swap the containers. We set the ignore_focus flag here because after the - * container is attached, the focus order is not yet correct and would - * result in wrong windows being focused. */ - - /* Move first to second. */ - result &= _con_move_to_con(first, second, false, false, false, true, false); - /* If swapping the containers didn't work we don't need to mess with the focus. */ - if (!result) { - goto swap_end; - } - - /* If we moved the container holding the focused window to another - * workspace we need to ensure the visible workspace has the focused - * container. - * 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_activate(con_descend_focused(current_ws)); - } - - /* Move second to where first has been originally. */ - result &= _con_move_to_con(second, fake, false, false, false, true, false); - if (!result) { - goto swap_end; - } - - /* Swapping will have inserted the containers at the tail of their parents' - * focus head. We fix this now by putting them in the position of the focus - * head the container they swapped with was in. */ - TAILQ_REMOVE(&(first->parent->focus_head), first, focused); - TAILQ_REMOVE(&(second->parent->focus_head), second, focused); - - if (second_prev_focus_head == NULL) { - TAILQ_INSERT_HEAD(&(first->parent->focus_head), first, focused); - } else { - TAILQ_INSERT_AFTER(&(first->parent->focus_head), second_prev_focus_head, first, focused); - } - - if (first_prev_focus_head == NULL) { - TAILQ_INSERT_HEAD(&(second->parent->focus_head), second, focused); - } else { - TAILQ_INSERT_AFTER(&(second->parent->focus_head), first_prev_focus_head, second, focused); - } - - /* If the focus was within any of the swapped containers, do the following: - * - If swapping took place within a workspace, ensure the previously - * focused container stays focused. - * - Otherwise, focus the container that has been swapped in. - * - * To understand why fixing the focus_head previously wasn't enough, - * consider the scenario - * H[ V[ A X ] V[ Y B ] ] - * with B being focused, but X being the focus_head within its parent. If - * we swap A and B now, fixing the focus_head would focus X, but since B - * was the focused container before it should stay focused. - */ - if (focused_within_first) { - if (first_ws == second_ws) { - con_activate(old_focus); - } else { - con_activate(con_descend_focused(second)); - } - } else if (focused_within_second) { - if (first_ws == second_ws) { - con_activate(old_focus); - } else { - con_activate(con_descend_focused(first)); - } - } + /* Floating nodes are children of CT_FLOATING_CONs, they are listed in + * nodes_head and focus_head like all other containers. Thus, we don't need + * to do anything special other than swapping the floating status and the + * relevant rects. */ + SWAP(first->floating, second->floating, int); + SWAP(first->rect, second->rect, Rect); + SWAP(first->window_rect, second->window_rect, Rect); /* We need to copy each other's percentages to ensure that the geometry - * doesn't change during the swap. This needs to happen _before_ we close - * the fake container as closing the tree will recalculate percentages. */ - first->percent = second_percent; - second->percent = first_percent; - fake->percent = 0.0; + * doesn't change during the swap. */ + SWAP(first->percent, second->percent, double); - SWAP(first_fullscreen_mode, second_fullscreen_mode, fullscreen_mode_t); - -swap_end: - /* The two windows exchange their original fullscreen status */ - if (first_fullscreen_mode != CF_NONE) { - con_enable_fullscreen(first, first_fullscreen_mode); + if (restore_focus) { + con_focus(restore_focus); } - if (second_fullscreen_mode != CF_NONE) { - con_enable_fullscreen(second, second_fullscreen_mode); + + /* Update new parents' & workspaces' urgency. */ + con_set_urgency(first, first->urgent); + con_set_urgency(second, second->urgent); + + /* Exchange fullscreen modes, can't use SWAP because we need to call the + * correct functions. */ + fullscreen_mode_t second_fullscreen_mode = second->fullscreen_mode; + if (first->fullscreen_mode == CF_NONE) { + con_disable_fullscreen(second); + } else { + con_enable_fullscreen(second, first->fullscreen_mode); + } + if (second_fullscreen_mode == CF_NONE) { + con_disable_fullscreen(first); + } else { + con_enable_fullscreen(first, second_fullscreen_mode); } /* We don't actually need this since percentages-wise we haven't changed @@ -2482,11 +2385,19 @@ swap_end: con_fix_percent(first->parent); con_fix_percent(second->parent); - /* We can get rid of the fake container again now. */ - con_close(fake, DONT_KILL_WINDOW); - + FREE(first->deco_render_params); + FREE(second->deco_render_params); con_force_split_parents_redraw(first); con_force_split_parents_redraw(second); - return result; + return true; +} + +/* + * Returns container's rect size depending on its orientation. + * i.e. its width when horizontal, its height when vertical. + * + */ +uint32_t con_rect_size_in_orientation(Con *con) { + return (con_orientation(con) == HORIZ ? con->rect.width : con->rect.height); } diff --git a/src/config.c b/src/config.c index 9631b216..81919566 100644 --- a/src/config.c +++ b/src/config.c @@ -39,154 +39,141 @@ void update_barconfig(void) { } } -/* - * Finds the configuration file to use (either the one specified by - * override_configpath), the user’s one or the system default) and calls - * parse_file(). - * - */ -bool parse_configuration(const char *override_configpath, bool use_nagbar) { - char *path = get_config_path(override_configpath, true); - if (path == NULL) { - die("Unable to find the configuration file (looked at " - "$XDG_CONFIG_HOME/i3/config, ~/.i3/config, $XDG_CONFIG_DIRS/i3/config " - "and " SYSCONFDIR "/i3/config)"); +static void free_configuration(void) { + assert(conn != NULL); + + /* If we are currently in a binding mode, we first revert to the default + * since we have no guarantee that the current mode will even still exist + * after parsing the config again. See #2228. */ + switch_mode("default"); + + /* First ungrab the keys */ + ungrab_all_keys(conn); + + struct Mode *mode; + while (!SLIST_EMPTY(&modes)) { + mode = SLIST_FIRST(&modes); + FREE(mode->name); + + /* Clear the old binding list */ + while (!TAILQ_EMPTY(mode->bindings)) { + Binding *bind = TAILQ_FIRST(mode->bindings); + TAILQ_REMOVE(mode->bindings, bind, bindings); + binding_free(bind); + } + FREE(mode->bindings); + + SLIST_REMOVE(&modes, mode, Mode, modes); + FREE(mode); } - LOG("Parsing configfile %s\n", path); - FREE(current_configpath); - current_configpath = path; - - /* initialize default bindings if we're just validating the config file */ - if (!use_nagbar && bindings == NULL) { - bindings = scalloc(1, sizeof(struct bindings_head)); - TAILQ_INIT(bindings); + while (!TAILQ_EMPTY(&assignments)) { + struct Assignment *assign = TAILQ_FIRST(&assignments); + if (assign->type == A_TO_WORKSPACE || assign->type == A_TO_WORKSPACE_NUMBER) + FREE(assign->dest.workspace); + else if (assign->type == A_COMMAND) + FREE(assign->dest.command); + else if (assign->type == A_TO_OUTPUT) + FREE(assign->dest.output); + match_free(&(assign->match)); + TAILQ_REMOVE(&assignments, assign, assignments); + FREE(assign); } - return parse_file(path, use_nagbar); + while (!TAILQ_EMPTY(&ws_assignments)) { + struct Workspace_Assignment *assign = TAILQ_FIRST(&ws_assignments); + FREE(assign->name); + FREE(assign->output); + TAILQ_REMOVE(&ws_assignments, assign, ws_assignments); + FREE(assign); + } + + /* Clear bar configs */ + Barconfig *barconfig; + while (!TAILQ_EMPTY(&barconfigs)) { + barconfig = TAILQ_FIRST(&barconfigs); + FREE(barconfig->id); + for (int c = 0; c < barconfig->num_outputs; c++) + free(barconfig->outputs[c]); + + while (!TAILQ_EMPTY(&(barconfig->bar_bindings))) { + struct Barbinding *binding = TAILQ_FIRST(&(barconfig->bar_bindings)); + FREE(binding->command); + TAILQ_REMOVE(&(barconfig->bar_bindings), binding, bindings); + FREE(binding); + } + + while (!TAILQ_EMPTY(&(barconfig->tray_outputs))) { + struct tray_output_t *tray_output = TAILQ_FIRST(&(barconfig->tray_outputs)); + FREE(tray_output->output); + TAILQ_REMOVE(&(barconfig->tray_outputs), tray_output, tray_outputs); + FREE(tray_output); + } + + FREE(barconfig->outputs); + FREE(barconfig->socket_path); + FREE(barconfig->status_command); + FREE(barconfig->i3bar_command); + FREE(barconfig->font); + FREE(barconfig->colors.background); + FREE(barconfig->colors.statusline); + FREE(barconfig->colors.separator); + FREE(barconfig->colors.focused_background); + FREE(barconfig->colors.focused_statusline); + FREE(barconfig->colors.focused_separator); + FREE(barconfig->colors.focused_workspace_border); + FREE(barconfig->colors.focused_workspace_bg); + FREE(barconfig->colors.focused_workspace_text); + FREE(barconfig->colors.active_workspace_border); + FREE(barconfig->colors.active_workspace_bg); + FREE(barconfig->colors.active_workspace_text); + FREE(barconfig->colors.inactive_workspace_border); + FREE(barconfig->colors.inactive_workspace_bg); + FREE(barconfig->colors.inactive_workspace_text); + FREE(barconfig->colors.urgent_workspace_border); + FREE(barconfig->colors.urgent_workspace_bg); + FREE(barconfig->colors.urgent_workspace_text); + FREE(barconfig->colors.binding_mode_border); + FREE(barconfig->colors.binding_mode_bg); + FREE(barconfig->colors.binding_mode_text); + TAILQ_REMOVE(&barconfigs, barconfig, configs); + FREE(barconfig); + } + + Con *con; + TAILQ_FOREACH(con, &all_cons, all_cons) { + /* Assignments changed, previously ran assignments are invalid. */ + if (con->window) { + con->window->nr_assignments = 0; + FREE(con->window->ran_assignments); + } + /* Invalidate pixmap caches in case font or colors changed. */ + FREE(con->deco_render_params); + } + + /* Get rid of the current font */ + free_font(); + + free(config.ipc_socket_path); + free(config.restart_state_path); + free(config.fake_outputs); } /* * (Re-)loads the configuration file (sets useful defaults before). * + * If you specify override_configpath, only this path is used to look for a + * configuration file. + * + * load_type specifies the type of loading: C_VALIDATE is used to only verify + * the correctness of the config file (used with the flag -C). C_LOAD will load + * the config for normal use and display errors in the nagbar. C_RELOAD will + * also clear the previous config. + * */ -void load_configuration(xcb_connection_t *conn, const char *override_configpath, bool reload) { - if (reload) { - /* If we are currently in a binding mode, we first revert to the - * default since we have no guarantee that the current mode will even - * still exist after parsing the config again. See #2228. */ - switch_mode("default"); - - /* First ungrab the keys */ - ungrab_all_keys(conn); - - struct Mode *mode; - while (!SLIST_EMPTY(&modes)) { - mode = SLIST_FIRST(&modes); - FREE(mode->name); - - /* Clear the old binding list */ - while (!TAILQ_EMPTY(mode->bindings)) { - Binding *bind = TAILQ_FIRST(mode->bindings); - TAILQ_REMOVE(mode->bindings, bind, bindings); - binding_free(bind); - } - FREE(mode->bindings); - - SLIST_REMOVE(&modes, mode, Mode, modes); - FREE(mode); - } - - while (!TAILQ_EMPTY(&assignments)) { - struct Assignment *assign = TAILQ_FIRST(&assignments); - if (assign->type == A_TO_WORKSPACE || assign->type == A_TO_WORKSPACE_NUMBER) - FREE(assign->dest.workspace); - else if (assign->type == A_COMMAND) - FREE(assign->dest.command); - else if (assign->type == A_TO_OUTPUT) - FREE(assign->dest.output); - match_free(&(assign->match)); - TAILQ_REMOVE(&assignments, assign, assignments); - FREE(assign); - } - - while (!TAILQ_EMPTY(&ws_assignments)) { - struct Workspace_Assignment *assign = TAILQ_FIRST(&ws_assignments); - FREE(assign->name); - FREE(assign->output); - TAILQ_REMOVE(&ws_assignments, assign, ws_assignments); - FREE(assign); - } - - /* Clear bar configs */ - Barconfig *barconfig; - while (!TAILQ_EMPTY(&barconfigs)) { - barconfig = TAILQ_FIRST(&barconfigs); - FREE(barconfig->id); - for (int c = 0; c < barconfig->num_outputs; c++) - free(barconfig->outputs[c]); - - while (!TAILQ_EMPTY(&(barconfig->bar_bindings))) { - struct Barbinding *binding = TAILQ_FIRST(&(barconfig->bar_bindings)); - FREE(binding->command); - TAILQ_REMOVE(&(barconfig->bar_bindings), binding, bindings); - FREE(binding); - } - - while (!TAILQ_EMPTY(&(barconfig->tray_outputs))) { - struct tray_output_t *tray_output = TAILQ_FIRST(&(barconfig->tray_outputs)); - FREE(tray_output->output); - TAILQ_REMOVE(&(barconfig->tray_outputs), tray_output, tray_outputs); - FREE(tray_output); - } - - FREE(barconfig->outputs); - FREE(barconfig->socket_path); - FREE(barconfig->status_command); - FREE(barconfig->i3bar_command); - FREE(barconfig->font); - FREE(barconfig->colors.background); - FREE(barconfig->colors.statusline); - FREE(barconfig->colors.separator); - FREE(barconfig->colors.focused_background); - FREE(barconfig->colors.focused_statusline); - FREE(barconfig->colors.focused_separator); - FREE(barconfig->colors.focused_workspace_border); - FREE(barconfig->colors.focused_workspace_bg); - FREE(barconfig->colors.focused_workspace_text); - FREE(barconfig->colors.active_workspace_border); - FREE(barconfig->colors.active_workspace_bg); - FREE(barconfig->colors.active_workspace_text); - FREE(barconfig->colors.inactive_workspace_border); - FREE(barconfig->colors.inactive_workspace_bg); - FREE(barconfig->colors.inactive_workspace_text); - FREE(barconfig->colors.urgent_workspace_border); - FREE(barconfig->colors.urgent_workspace_bg); - FREE(barconfig->colors.urgent_workspace_text); - FREE(barconfig->colors.binding_mode_border); - FREE(barconfig->colors.binding_mode_bg); - FREE(barconfig->colors.binding_mode_text); - TAILQ_REMOVE(&barconfigs, barconfig, configs); - FREE(barconfig); - } - - Con *con; - TAILQ_FOREACH(con, &all_cons, all_cons) { - /* Assignments changed, previously ran assignments are invalid. */ - if (con->window) { - con->window->nr_assignments = 0; - FREE(con->window->ran_assignments); - } - /* Invalidate pixmap caches in case font or colors changed. */ - FREE(con->deco_render_params); - } - - /* Get rid of the current font */ - free_font(); - - free(config.ipc_socket_path); - free(config.restart_state_path); - free(config.fake_outputs); +bool load_configuration(const char *override_configpath, config_load_t load_type) { + if (load_type == C_RELOAD) { + free_configuration(); } SLIST_INIT(&modes); @@ -202,7 +189,7 @@ void load_configuration(xcb_connection_t *conn, const char *override_configpath, /* Clear the old config or initialize the data structure */ memset(&config, 0, sizeof(config)); -/* Initialize default colors */ + /* Initialize default colors */ #define INIT_COLOR(x, cborder, cbackground, ctext, cindicator) \ do { \ x.border = draw_util_hex_to_color(cborder); \ @@ -241,24 +228,32 @@ void load_configuration(xcb_connection_t *conn, const char *override_configpath, config.focus_wrapping = FOCUS_WRAPPING_ON; - parse_configuration(override_configpath, true); - - if (reload) { - translate_keysyms(); - grab_all_keys(conn); - regrab_all_buttons(conn); + FREE(current_configpath); + current_configpath = get_config_path(override_configpath, true); + if (current_configpath == NULL) { + die("Unable to find the configuration file (looked at " + "$XDG_CONFIG_HOME/i3/config, ~/.i3/config, $XDG_CONFIG_DIRS/i3/config " + "and " SYSCONFDIR "/i3/config)"); } + LOG("Parsing configfile %s\n", current_configpath); + const bool result = parse_file(current_configpath, load_type != C_VALIDATE); - if (config.font.type == FONT_TYPE_NONE) { + if (config.font.type == FONT_TYPE_NONE && load_type != C_VALIDATE) { ELOG("You did not specify required configuration option \"font\"\n"); config.font = load_font("fixed", true); set_font(&config.font); } - /* Redraw the currently visible decorations on reload, so that - * the possibly new drawing parameters changed. */ - if (reload) { + if (load_type == C_RELOAD) { + translate_keysyms(); + grab_all_keys(conn); + regrab_all_buttons(conn); + + /* Redraw the currently visible decorations on reload, so that the + * possibly new drawing parameters changed. */ x_deco_recurse(croot); xcb_flush(conn); } + + return result; } diff --git a/src/config_directives.c b/src/config_directives.c index 0b01d54a..fb3fba41 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -122,7 +122,7 @@ CFGFUN(mode_binding, const char *bindtype, const char *modifiers, const char *ke } CFGFUN(enter_mode, const char *pango_markup, const char *modename) { - if (strcasecmp(modename, DEFAULT_BINDING_MODE) == 0) { + if (strcmp(modename, DEFAULT_BINDING_MODE) == 0) { ELOG("You cannot use the name %s for your mode\n", DEFAULT_BINDING_MODE); return; } @@ -334,30 +334,41 @@ CFGFUN(show_marks, const char *value) { config.show_marks = eval_boolstr(value); } -CFGFUN(workspace, const char *workspace, const char *outputs) { - DLOG("Assigning workspace \"%s\" to outputs \"%s\"\n", workspace, outputs); - /* Check for earlier assignments of the same workspace so that we - * don’t have assignments of a single workspace to different - * outputs */ +static char *current_workspace = NULL; + +CFGFUN(workspace, const char *workspace, const char *output) { struct Workspace_Assignment *assignment; - TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) { - if (strcasecmp(assignment->name, workspace) == 0) { - ELOG("You have a duplicate workspace assignment for workspace \"%s\"\n", - workspace); + + /* When a new workspace line is encountered, for the first output word, + * $workspace from the config.spec is non-NULL. Afterwards, the parser calls + * clear_stack() because of the call line. Thus, we have to preserve the + * workspace string. */ + if (workspace) { + FREE(current_workspace); + + TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) { + if (strcasecmp(assignment->name, workspace) == 0) { + ELOG("You have a duplicate workspace assignment for workspace \"%s\"\n", + workspace); + return; + } + } + + current_workspace = sstrdup(workspace); + } else { + if (!current_workspace) { + DLOG("Both workspace and current_workspace are NULL, assuming we had an error before\n"); return; } + workspace = current_workspace; } - char *buf = sstrdup(outputs); - char *output = strtok(buf, " "); - while (output != NULL) { - assignment = scalloc(1, sizeof(struct Workspace_Assignment)); - assignment->name = sstrdup(workspace); - assignment->output = sstrdup(output); - TAILQ_INSERT_TAIL(&ws_assignments, assignment, ws_assignments); - output = strtok(NULL, " "); - } - free(buf); + DLOG("Assigning workspace \"%s\" to output \"%s\"\n", workspace, output); + + assignment = scalloc(1, sizeof(struct Workspace_Assignment)); + assignment->name = sstrdup(workspace); + assignment->output = sstrdup(output); + TAILQ_INSERT_TAIL(&ws_assignments, assignment, ws_assignments); } CFGFUN(ipc_socket, const char *path) { diff --git a/src/config_parser.c b/src/config_parser.c index 9f972fed..c238f1b9 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -409,7 +409,7 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) if (*walk == '\0' || *walk == '\n' || *walk == '\r') { next_state(token); token_handled = true; -/* To make sure we start with an appropriate matching + /* To make sure we start with an appropriate matching * datastructure for commands which do *not* specify any * criteria, we re-initialize the criteria system after * every command. */ diff --git a/src/display_version.c b/src/display_version.c index 2e05cafa..da11bff3 100644 --- a/src/display_version.c +++ b/src/display_version.c @@ -55,6 +55,13 @@ static yajl_callbacks version_callbacks = { * */ void display_running_version(void) { + if (getenv("DISPLAY") == NULL) { + fprintf(stderr, "\nYour DISPLAY environment variable is not set.\n"); + fprintf(stderr, "Are you running i3 via SSH or on a virtual console?\n"); + fprintf(stderr, "Try DISPLAY=:0 i3 --moreversion\n"); + exit(EXIT_FAILURE); + } + 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. */ diff --git a/src/ewmh.c b/src/ewmh.c index e5dcafcb..7bd23fb7 100644 --- a/src/ewmh.c +++ b/src/ewmh.c @@ -11,6 +11,11 @@ xcb_window_t ewmh_window; +#define FOREACH_NONINTERNAL \ + TAILQ_FOREACH(output, &(croot->nodes_head), nodes) \ + TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) \ + if (!con_is_internal(ws)) + /* * Updates _NET_CURRENT_DESKTOP with the current desktop number. * @@ -19,28 +24,34 @@ xcb_window_t ewmh_window; * */ void ewmh_update_current_desktop(void) { + static uint32_t old_idx = NET_WM_DESKTOP_NONE; const uint32_t idx = ewmh_get_workspace_index(focused); - if (idx != NET_WM_DESKTOP_NONE) { - xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_CURRENT_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, &idx); + + if (idx == old_idx || idx == NET_WM_DESKTOP_NONE) { + return; } + old_idx = idx; + + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_CURRENT_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, &idx); } /* * Updates _NET_NUMBER_OF_DESKTOPS which we interpret as the number of * noninternal workspaces. */ -void ewmh_update_number_of_desktops(void) { - Con *output; +static void ewmh_update_number_of_desktops(void) { + Con *output, *ws; + static uint32_t old_idx = 0; uint32_t idx = 0; - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *ws; - TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { - if (STARTS_WITH(ws->name, "__")) - continue; - ++idx; - } + FOREACH_NONINTERNAL { + idx++; + }; + + if (idx == old_idx) { + return; } + old_idx = idx; xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_NUMBER_OF_DESKTOPS, XCB_ATOM_CARDINAL, 32, 1, &idx); @@ -50,33 +61,22 @@ void ewmh_update_number_of_desktops(void) { * Updates _NET_DESKTOP_NAMES: "The names of all virtual desktops. This is a * list of NULL-terminated strings in UTF-8 encoding" */ -void ewmh_update_desktop_names(void) { - Con *output; +static void ewmh_update_desktop_names(void) { + Con *output, *ws; int msg_length = 0; /* count the size of the property message to set */ - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *ws; - TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { - if (STARTS_WITH(ws->name, "__")) - continue; - msg_length += strlen(ws->name) + 1; - } - } + FOREACH_NONINTERNAL { + msg_length += strlen(ws->name) + 1; + }; char desktop_names[msg_length]; int current_position = 0; /* fill the buffer with the names of the i3 workspaces */ - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *ws; - TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { - if (STARTS_WITH(ws->name, "__")) - continue; - - for (size_t i = 0; i < strlen(ws->name) + 1; i++) { - desktop_names[current_position++] = ws->name[i]; - } + FOREACH_NONINTERNAL { + for (size_t i = 0; i < strlen(ws->name) + 1; i++) { + desktop_names[current_position++] = ws->name[i]; } } @@ -88,39 +88,39 @@ void ewmh_update_desktop_names(void) { * Updates _NET_DESKTOP_VIEWPORT, which is an array of pairs of cardinals that * define the top left corner of each desktop's viewport. */ -void ewmh_update_desktop_viewport(void) { - Con *output; +static void ewmh_update_desktop_viewport(void) { + Con *output, *ws; int num_desktops = 0; /* count number of desktops */ - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *ws; - TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { - if (STARTS_WITH(ws->name, "__")) - continue; - - num_desktops++; - } + FOREACH_NONINTERNAL { + num_desktops++; } uint32_t viewports[num_desktops * 2]; int current_position = 0; /* fill the viewport buffer */ - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *ws; - TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { - if (STARTS_WITH(ws->name, "__")) - continue; - - viewports[current_position++] = output->rect.x; - viewports[current_position++] = output->rect.y; - } + FOREACH_NONINTERNAL { + viewports[current_position++] = output->rect.x; + viewports[current_position++] = output->rect.y; } xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_DESKTOP_VIEWPORT, XCB_ATOM_CARDINAL, 32, current_position, &viewports); } +/* + * Updates all the EWMH desktop properties. + * + */ +void ewmh_update_desktop_properties(void) { + ewmh_update_number_of_desktops(); + ewmh_update_desktop_viewport(); + ewmh_update_current_desktop(); + ewmh_update_desktop_names(); + ewmh_update_wm_desktop(); +} + static void ewmh_update_wm_desktop_recursively(Con *con, const uint32_t desktop) { Con *child; @@ -354,18 +354,12 @@ Con *ewmh_get_workspace_by_index(uint32_t idx) { uint32_t current_index = 0; - Con *output; - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *workspace; - TAILQ_FOREACH(workspace, &(output_get_content(output)->nodes_head), nodes) { - if (con_is_internal(workspace)) - continue; - - if (current_index == idx) - return workspace; - - ++current_index; + Con *output, *ws; + FOREACH_NONINTERNAL { + if (current_index == idx) { + return ws; } + current_index++; } return NULL; @@ -381,19 +375,14 @@ Con *ewmh_get_workspace_by_index(uint32_t idx) { uint32_t ewmh_get_workspace_index(Con *con) { uint32_t index = 0; - Con *workspace = con_get_workspace(con); - Con *output; - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *current; - TAILQ_FOREACH(current, &(output_get_content(output)->nodes_head), nodes) { - if (con_is_internal(current)) - continue; - - if (current == workspace) - return index; - - ++index; + Con *target_workspace = con_get_workspace(con); + Con *output, *ws; + FOREACH_NONINTERNAL { + if (ws == target_workspace) { + return index; } + + index++; } return NET_WM_DESKTOP_NONE; diff --git a/src/fake_outputs.c b/src/fake_outputs.c index 39cbd7bb..5f3622d4 100644 --- a/src/fake_outputs.c +++ b/src/fake_outputs.c @@ -74,7 +74,7 @@ void fake_outputs_init(const char *output_spec) { else TAILQ_INSERT_TAIL(&outputs, new_output, outputs); output_init_con(new_output); - init_ws_for_output(new_output, output_get_content(new_output->con)); + init_ws_for_output(new_output); num_screens++; } new_output->primary = primary; diff --git a/src/floating.c b/src/floating.c index 4833466c..79f1d3d3 100644 --- a/src/floating.c +++ b/src/floating.c @@ -60,12 +60,17 @@ static void floating_set_hint_atom(Con *con, bool floating) { } /* - * Called when a floating window is created or resized. - * This function resizes the window if its size is higher or lower than the - * configured maximum/minimum size, respectively. + * Called when a floating window is created or resized. This function resizes + * the window if its size is higher or lower than the configured maximum/minimum + * size, respectively or when adjustments are needed to conform to the + * configured size increments or aspect ratio limits. + * + * When prefer_height is true and the window needs to be resized because of the + * configured aspect ratio, the width is adjusted first, preserving the previous + * height. * */ -void floating_check_size(Con *floating_con) { +void floating_check_size(Con *floating_con, bool prefer_height) { /* Define reasonable minimal and maximal sizes for floating windows */ const int floating_sane_min_height = 50; const int floating_sane_min_width = 75; @@ -83,43 +88,89 @@ void floating_check_size(Con *floating_con) { border_rect.height += render_deco_height(); } - if (focused_con->window != NULL) { - if (focused_con->window->min_width) { + i3Window *window = focused_con->window; + if (window != NULL) { + /* ICCCM says: If a base size is not provided, the minimum size is to be used in its place + * and vice versa. */ + int min_width = (window->min_width ? window->min_width : window->base_width); + int min_height = (window->min_height ? window->min_height : window->base_height); + int base_width = (window->base_width ? window->base_width : window->min_width); + int base_height = (window->base_height ? window->base_height : window->min_height); + + if (min_width) { floating_con->rect.width -= border_rect.width; - floating_con->rect.width = max(floating_con->rect.width, focused_con->window->min_width); + floating_con->rect.width = max(floating_con->rect.width, min_width); floating_con->rect.width += border_rect.width; } - if (focused_con->window->min_height) { + if (min_height) { floating_con->rect.height -= border_rect.height; - floating_con->rect.height = max(floating_con->rect.height, focused_con->window->min_height); + floating_con->rect.height = max(floating_con->rect.height, min_height); floating_con->rect.height += border_rect.height; } - if (focused_con->window->max_width) { + if (window->max_width) { floating_con->rect.width -= border_rect.width; - floating_con->rect.width = min(floating_con->rect.width, focused_con->window->max_width); + floating_con->rect.width = min(floating_con->rect.width, window->max_width); floating_con->rect.width += border_rect.width; } - if (focused_con->window->max_height) { + if (window->max_height) { floating_con->rect.height -= border_rect.height; - floating_con->rect.height = min(floating_con->rect.height, focused_con->window->max_height); + floating_con->rect.height = min(floating_con->rect.height, window->max_height); floating_con->rect.height += border_rect.height; } - if (focused_con->window->height_increment && - floating_con->rect.height >= focused_con->window->base_height + border_rect.height) { - floating_con->rect.height -= focused_con->window->base_height + border_rect.height; - floating_con->rect.height -= floating_con->rect.height % focused_con->window->height_increment; - floating_con->rect.height += focused_con->window->base_height + border_rect.height; + /* Obey the aspect ratio, if any, unless we are in fullscreen mode. + * + * The spec isn’t explicit on whether the aspect ratio hints should be + * respected during fullscreen mode. Other WMs such as Openbox don’t do + * that, and this post suggests that this is the correct way to do it: + * https://mail.gnome.org/archives/wm-spec-list/2003-May/msg00007.html + * + * Ignoring aspect ratio during fullscreen was necessary to fix MPlayer + * subtitle rendering, see https://bugs.i3wm.org/594 */ + const double min_ar = window->min_aspect_ratio; + const double max_ar = window->max_aspect_ratio; + if (floating_con->fullscreen_mode == CF_NONE && (min_ar > 0 || max_ar > 0)) { + /* The ICCCM says to subtract the base size from the window size for + * aspect ratio calculations. However, unlike determining the base + * size itself we must not fall back to using the minimum size in + * this case according to the ICCCM. */ + double width = floating_con->rect.width - window->base_width - border_rect.width; + double height = floating_con->rect.height - window->base_height - border_rect.height; + const double ar = (double)width / (double)height; + double new_ar = -1; + if (min_ar > 0 && ar < min_ar) { + new_ar = min_ar; + } else if (max_ar > 0 && ar > max_ar) { + new_ar = max_ar; + } + if (new_ar > 0) { + if (prefer_height) { + width = round(height * new_ar); + height = round(width / new_ar); + } else { + height = round(width / new_ar); + width = round(height * new_ar); + } + floating_con->rect.width = width + window->base_width + border_rect.width; + floating_con->rect.height = height + window->base_height + border_rect.height; + } } - if (focused_con->window->width_increment && - floating_con->rect.width >= focused_con->window->base_width + border_rect.width) { - floating_con->rect.width -= focused_con->window->base_width + border_rect.width; - floating_con->rect.width -= floating_con->rect.width % focused_con->window->width_increment; - floating_con->rect.width += focused_con->window->base_width + border_rect.width; + if (window->height_increment && + floating_con->rect.height >= base_height + border_rect.height) { + floating_con->rect.height -= base_height + border_rect.height; + floating_con->rect.height -= floating_con->rect.height % window->height_increment; + floating_con->rect.height += base_height + border_rect.height; + } + + if (window->width_increment && + floating_con->rect.width >= base_width + border_rect.width) { + floating_con->rect.width -= base_width + border_rect.width; + floating_con->rect.width -= floating_con->rect.width % window->width_increment; + floating_con->rect.width += base_width + border_rect.width; } } @@ -313,7 +364,7 @@ void floating_enable(Con *con, bool automatic) { nc->rect.height += con->border_width * 2; nc->rect.width += con->border_width * 2; - floating_check_size(nc); + floating_check_size(nc, false); /* Some clients (like GIMP’s color picker window) get mapped * to (0, 0), so we push them to a reasonable position @@ -361,8 +412,7 @@ void floating_enable(Con *con, bool automatic) { DLOG("Corrected y = %d (deco_height = %d)\n", nc->rect.y, deco_height); /* render the cons to get initial window_rect correct */ - render_con(nc, false); - render_con(con, false); + render_con(nc); if (set_focus) con_activate(con); @@ -517,7 +567,7 @@ DRAGGING_CB(drag_window_callback) { con->rect.x = old_rect->x + (new_x - event->root_x); con->rect.y = old_rect->y + (new_y - event->root_y); - render_con(con, false); + render_con(con); x_push_node(con); xcb_flush(conn); @@ -611,7 +661,7 @@ DRAGGING_CB(resize_window_callback) { con->rect = (Rect){dest_x, dest_y, dest_width, dest_height}; /* Obey window size */ - floating_check_size(con); + floating_check_size(con, false); /* If not the lower right corner is grabbed, we must also reposition * the client by exactly the amount we resized it */ @@ -624,9 +674,7 @@ DRAGGING_CB(resize_window_callback) { con->rect.x = dest_x; con->rect.y = dest_y; - /* TODO: don’t re-render the whole tree just because we change - * coordinates of a floating window */ - tree_render(); + render_con(con); x_push_changes(croot); } @@ -844,7 +892,7 @@ drag_result_t drag_pointer(Con *con, const xcb_button_press_event_t *event, xcb_ XCB_CURRENT_TIME, XCB_GRAB_MODE_ASYNC, /* continue processing pointer events as normal */ XCB_GRAB_MODE_ASYNC /* keyboard mode */ - ); + ); if ((keyb_reply = xcb_grab_keyboard_reply(conn, keyb_cookie, &error)) == NULL) { ELOG("Could not grab keyboard (error_code = %d)\n", error->error_code); @@ -919,7 +967,7 @@ bool floating_reposition(Con *con, Rect newrect) { * window's size hints. * */ -void floating_resize(Con *floating_con, int x, int y) { +void floating_resize(Con *floating_con, uint32_t x, uint32_t y) { DLOG("floating resize to %dx%d px\n", x, y); Rect *rect = &floating_con->rect; Con *focused_con = con_descend_focused(floating_con); @@ -929,6 +977,7 @@ void floating_resize(Con *floating_con, int x, int y) { } int wi = focused_con->window->width_increment; int hi = focused_con->window->height_increment; + bool prefer_height = (rect->width == x); rect->width = x; rect->height = y; if (wi) @@ -936,7 +985,7 @@ void floating_resize(Con *floating_con, int x, int y) { if (hi) rect->height += (hi - 1 - rect->height) % hi; - floating_check_size(floating_con); + floating_check_size(floating_con, prefer_height); /* If this is a scratchpad window, don't auto center it from now on. */ if (floating_con->scratchpad_state == SCRATCHPAD_FRESH) diff --git a/src/handlers.c b/src/handlers.c index b9677917..ae42b82e 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -20,6 +20,7 @@ int randr_base = -1; int xkb_base = -1; int xkb_current_group; +int shape_base = -1; /* After mapping/unmapping windows, a notify event is generated. However, we don’t want it, since it’d trigger an infinite loop of switching between the different windows when @@ -974,132 +975,14 @@ static bool handle_normal_hints(void *data, xcb_connection_t *conn, uint8_t stat return false; } - xcb_size_hints_t size_hints; + bool changed = window_update_normal_hints(con->window, reply, NULL); - /* If the hints were already in this event, use them, if not, request them */ - if (reply != NULL) { - xcb_icccm_get_wm_size_hints_from_reply(&size_hints, reply); - } else { - xcb_icccm_get_wm_normal_hints_reply(conn, xcb_icccm_get_wm_normal_hints_unchecked(conn, con->window->id), &size_hints, NULL); - } - - int win_width = con->window_rect.width; - int win_height = con->window_rect.height; - - if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE)) { - DLOG("Minimum size: %d (width) x %d (height)\n", size_hints.min_width, size_hints.min_height); - - con->window->min_width = size_hints.min_width; - con->window->min_height = size_hints.min_height; - } - - if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE)) { - DLOG("Maximum size: %d (width) x %d (height)\n", size_hints.max_width, size_hints.max_height); - - con->window->max_width = size_hints.max_width; - con->window->max_height = size_hints.max_height; - } - - if (con_is_floating(con)) { - win_width = MAX(win_width, con->window->min_width); - win_height = MAX(win_height, con->window->min_height); - win_width = MIN(win_width, con->window->max_width); - win_height = MIN(win_height, con->window->max_height); - } - - bool changed = false; - if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_RESIZE_INC)) { - if (size_hints.width_inc > 0 && size_hints.width_inc < 0xFFFF) { - if (con->window->width_increment != size_hints.width_inc) { - con->window->width_increment = size_hints.width_inc; - changed = true; - } - } - - if (size_hints.height_inc > 0 && size_hints.height_inc < 0xFFFF) { - if (con->window->height_increment != size_hints.height_inc) { - con->window->height_increment = size_hints.height_inc; - changed = true; - } - } - - if (changed) { - DLOG("resize increments changed\n"); - } - } - - bool has_base_size = false; - int base_width = 0; - int base_height = 0; - - /* The base width / height is the desired size of the window. */ - if (size_hints.flags & XCB_ICCCM_SIZE_HINT_BASE_SIZE) { - base_width = size_hints.base_width; - base_height = size_hints.base_height; - has_base_size = true; - } - - /* If the window didn't specify a base size, the ICCCM tells us to fall - * back to the minimum size instead, if available. */ - if (!has_base_size && size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE) { - base_width = size_hints.min_width; - base_height = size_hints.min_height; - } - - // TODO XXX Should we only do this is the base size is > 0? - if (base_width != con->window->base_width || base_height != con->window->base_height) { - con->window->base_width = base_width; - con->window->base_height = base_height; - - DLOG("client's base_height changed to %d\n", base_height); - DLOG("client's base_width changed to %d\n", base_width); - changed = true; - } - - /* If no aspect ratio was set or if it was invalid, we ignore the hints */ - if (!(size_hints.flags & XCB_ICCCM_SIZE_HINT_P_ASPECT) || - (size_hints.min_aspect_num <= 0) || - (size_hints.min_aspect_den <= 0)) { - goto render_and_return; - } - - /* The ICCCM says to subtract the base size from the window size for aspect - * ratio calculations. However, unlike determining the base size itself we - * must not fall back to using the minimum size in this case according to - * the ICCCM. */ - double width = win_width - base_width * has_base_size; - double height = win_height - base_height * has_base_size; - - /* Convert numerator/denominator to a double */ - double min_aspect = (double)size_hints.min_aspect_num / size_hints.min_aspect_den; - double max_aspect = (double)size_hints.max_aspect_num / size_hints.max_aspect_den; - - DLOG("Aspect ratio set: minimum %f, maximum %f\n", min_aspect, max_aspect); - DLOG("width = %f, height = %f\n", width, height); - - /* Sanity checks, this is user-input, in a way */ - if (max_aspect <= 0 || min_aspect <= 0 || height == 0 || (width / height) <= 0) { - goto render_and_return; - } - - /* Check if we need to set proportional_* variables using the correct ratio */ - double aspect_ratio = 0.0; - if ((width / height) < min_aspect) { - aspect_ratio = min_aspect; - } else if ((width / height) > max_aspect) { - aspect_ratio = max_aspect; - } else { - goto render_and_return; - } - - if (fabs(con->window->aspect_ratio - aspect_ratio) > DBL_EPSILON) { - con->window->aspect_ratio = aspect_ratio; - changed = true; - } - -render_and_return: if (changed) { - tree_render(); + Con *floating = con_inside_floating(con); + if (floating) { + floating_check_size(con, false); + tree_render(); + } } FREE(reply); @@ -1517,6 +1400,27 @@ void handle_event(int type, xcb_generic_event_t *event) { return; } + if (shape_supported && type == shape_base + XCB_SHAPE_NOTIFY) { + xcb_shape_notify_event_t *shape = (xcb_shape_notify_event_t *)event; + + DLOG("shape_notify_event for window 0x%08x, shape_kind = %d, shaped = %d\n", + shape->affected_window, shape->shape_kind, shape->shaped); + + Con *con = con_by_window_id(shape->affected_window); + if (con == NULL) { + LOG("Not a managed window 0x%08x, ignoring shape_notify_event\n", + shape->affected_window); + return; + } + + if (shape->shape_kind == XCB_SHAPE_SK_BOUNDING || + shape->shape_kind == XCB_SHAPE_SK_INPUT) { + x_set_shape(con, shape->shape_kind, shape->shaped); + } + + return; + } + switch (type) { case XCB_KEY_PRESS: case XCB_KEY_RELEASE: diff --git a/src/ipc.c b/src/ipc.c index d18675f7..0ffdfebf 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -33,6 +33,9 @@ all_clients = TAILQ_HEAD_INITIALIZER(all_clients); */ static void set_nonblock(int sockfd) { int flags = fcntl(sockfd, F_GETFL, 0); + if (flags & O_NONBLOCK) { + return; + } flags |= O_NONBLOCK; if (fcntl(sockfd, F_SETFL, flags) < 0) err(-1, "Could not set O_NONBLOCK"); @@ -125,9 +128,11 @@ static void ipc_send_client_message(ipc_client *client, size_t size, const uint3 } } -static void free_ipc_client(ipc_client *client) { - DLOG("Disconnecting client on fd %d\n", client->fd); - close(client->fd); +static void free_ipc_client(ipc_client *client, int exempt_fd) { + if (client->fd != exempt_fd) { + DLOG("Disconnecting client on fd %d\n", client->fd); + close(client->fd); + } ev_io_stop(main_loop, client->read_callback); FREE(client->read_callback); @@ -195,15 +200,19 @@ static void ipc_send_shutdown_event(shutdown_reason_t reason) { * Calls shutdown() on each socket and closes it. This function is to be called * when exiting or restarting only! * + * exempt_fd is never closed. Set to -1 to close all fds. + * */ -void ipc_shutdown(shutdown_reason_t reason) { +void ipc_shutdown(shutdown_reason_t reason, int exempt_fd) { ipc_send_shutdown_event(reason); ipc_client *current; while (!TAILQ_EMPTY(&all_clients)) { current = TAILQ_FIRST(&all_clients); - shutdown(current->fd, SHUT_RDWR); - free_ipc_client(current); + if (current->fd != exempt_fd) { + shutdown(current->fd, SHUT_RDWR); + } + free_ipc_client(current, exempt_fd); } } @@ -215,12 +224,11 @@ void ipc_shutdown(shutdown_reason_t reason) { IPC_HANDLER(run_command) { /* To get a properly terminated buffer, we copy * message_size bytes out of the buffer */ - char *command = scalloc(message_size + 1, 1); - strncpy(command, (const char *)message, message_size); + char *command = sstrndup((const char *)message, message_size); LOG("IPC: received: *%s*\n", command); yajl_gen gen = yajl_gen_alloc(NULL); - CommandResult *result = parse_command((const char *)command, gen); + CommandResult *result = parse_command(command, gen, client); free(command); if (result->needs_tree_render) @@ -649,6 +657,11 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) { y(integer, con->depth); } + if (inplace_restart && con->type == CT_ROOT && previous_workspace_name) { + ystr("previous_workspace_name"); + ystr(previous_workspace_name); + } + y(map_close); } @@ -1335,7 +1348,7 @@ static void ipc_receive_message(EV_P_ struct ev_io *w, int revents) { /* If not, there was some kind of error. We don’t bother and close the * connection. Delete the client from the list of clients. */ - free_ipc_client(client); + free_ipc_client(client, -1); FREE(message); return; } @@ -1393,7 +1406,7 @@ end: ELOG("client %p on fd %d timed out, killing\n", client, client->fd); } - free_ipc_client(client); + free_ipc_client(client, -1); } static void ipc_socket_writeable_cb(EV_P_ ev_io *w, int revents) { @@ -1427,6 +1440,18 @@ void ipc_new_client(EV_P_ struct ev_io *w, int revents) { /* Close this file descriptor on exec() */ (void)fcntl(fd, F_SETFD, FD_CLOEXEC); + ipc_new_client_on_fd(EV_A_ fd); +} + +/* + * ipc_new_client_on_fd() only sets up the event handler + * for activity on the new connection and inserts the file descriptor into + * the list of clients. + * + * This variant is useful for the inherited IPC connection when restarting. + * + */ +ipc_client *ipc_new_client_on_fd(EV_P_ int fd) { set_nonblock(fd); ipc_client *client = scalloc(1, sizeof(ipc_client)); @@ -1441,8 +1466,9 @@ void ipc_new_client(EV_P_ struct ev_io *w, int revents) { client->write_callback->data = client; ev_io_init(client->write_callback, ipc_socket_writeable_cb, fd, EV_WRITE); - DLOG("IPC: new client connected on fd %d\n", w->fd); + DLOG("IPC: new client connected on fd %d\n", fd); TAILQ_INSERT_TAIL(&all_clients, client, clients); + return client; } /* @@ -1623,3 +1649,15 @@ void ipc_send_binding_event(const char *event_type, Binding *bind) { y(free); setlocale(LC_NUMERIC, ""); } + +/* + * Sends a restart reply to the IPC client on the specified fd. + */ +void ipc_confirm_restart(ipc_client *client) { + DLOG("ipc_confirm_restart(fd %d)\n", client->fd); + static const char *reply = "[{\"success\":true}]"; + ipc_send_client_message( + client, strlen(reply), I3_IPC_REPLY_TYPE_COMMAND, + (const uint8_t *)reply); + ipc_push_pending(client); +} diff --git a/src/load_layout.c b/src/load_layout.c index 5a340d2c..47daada1 100644 --- a/src/load_layout.c +++ b/src/load_layout.c @@ -31,7 +31,15 @@ static bool parsing_marks; struct Match *current_swallow; static bool swallow_is_empty; static int num_marks; -static char **marks; +/* We need to save each container that needs to be marked if we want to support + * marking non-leaf containers. In their case, the end_map for their children is + * called before their own end_map, so marking json_node would end up marking + * the latest child. We can't just mark containers immediately after we parse a + * mark because of #2511. */ +struct pending_marks { + char *mark; + Con *con_to_be_marked; +} * marks; /* This list is used for reordering the focus stack after parsing the 'focus' * array. */ @@ -144,13 +152,15 @@ static int json_end_map(void *ctx) { } } - floating_check_size(json_node); + floating_check_size(json_node, false); } if (num_marks > 0) { for (int i = 0; i < num_marks; i++) { - con_mark(json_node, marks[i], MM_ADD); - free(marks[i]); + Con *con = marks[i].con_to_be_marked; + char *mark = marks[i].mark; + con_mark(con, mark, MM_ADD); + free(mark); } FREE(marks); @@ -274,8 +284,9 @@ static int json_string(void *ctx, const unsigned char *val, size_t len) { char *mark; sasprintf(&mark, "%.*s", (int)len, val); - marks = srealloc(marks, (++num_marks) * sizeof(char *)); - marks[num_marks - 1] = sstrdup(mark); + marks = srealloc(marks, (++num_marks) * sizeof(struct pending_marks)); + marks[num_marks - 1].mark = sstrdup(mark); + marks[num_marks - 1].con_to_be_marked = json_node; } else { if (strcasecmp(last_key, "name") == 0) { json_node->name = scalloc(len + 1, 1); @@ -409,6 +420,9 @@ static int json_string(void *ctx, const unsigned char *val, size_t len) { else if (strcasecmp(buf, "changed") == 0) json_node->scratchpad_state = SCRATCHPAD_CHANGED; free(buf); + } else if (strcasecmp(last_key, "previous_workspace_name") == 0) { + FREE(previous_workspace_name); + previous_workspace_name = sstrndup((const char *)val, len); } } return 1; diff --git a/src/log.c b/src/log.c index 916085f4..31db8b33 100644 --- a/src/log.c +++ b/src/log.c @@ -124,9 +124,9 @@ void init_logging(void) { */ void open_logbuffer(void) { /* Reserve 1% of the RAM for the logfile, but at max 25 MiB. - * For 512 MiB of RAM this will lead to a 5 MiB log buffer. - * At the moment (2011-12-10), no testcase leads to an i3 log - * of more than ~ 600 KiB. */ + * For 512 MiB of RAM this will lead to a 5 MiB log buffer. + * At the moment (2011-12-10), no testcase leads to an i3 log + * of more than ~ 600 KiB. */ logbuffer_size = min(physical_mem_bytes * 0.01, shmlog_size); #if defined(__FreeBSD__) sasprintf(&shmlogname, "/tmp/i3-log-%d", getpid()); diff --git a/src/main.c b/src/main.c index 5d6a68b9..7ba12bd9 100644 --- a/src/main.c +++ b/src/main.c @@ -89,6 +89,7 @@ struct ws_assignments_head ws_assignments = TAILQ_HEAD_INITIALIZER(ws_assignment /* We hope that those are supported and set them to true */ bool xcursor_supported = true; bool xkb_supported = true; +bool shape_supported = true; bool force_xinerama = false; @@ -165,7 +166,7 @@ static void i3_exit(void) { fflush(stderr); shm_unlink(shmlogname); } - ipc_shutdown(SHUTDOWN_REASON_EXIT); + ipc_shutdown(SHUTDOWN_REASON_EXIT, -1); unlink(config.ipc_socket_path); xcb_disconnect(conn); @@ -235,6 +236,20 @@ static void setup_term_handlers(void) { } } +static int parse_restart_fd(void) { + const char *restart_fd = getenv("_I3_RESTART_FD"); + if (restart_fd == NULL) { + return -1; + } + + long int fd = -1; + if (!parse_long(restart_fd, &fd, 10)) { + ELOG("Malformed _I3_RESTART_FD \"%s\"\n", restart_fd); + return -1; + } + return fd; +} + int main(int argc, char *argv[]) { /* Keep a symbol pointing to the I3_VERSION string constant so that we have * it in gdb backtraces. */ @@ -419,7 +434,7 @@ int main(int argc, char *argv[]) { } if (only_check_config) { - exit(parse_configuration(override_configpath, false) ? 0 : 1); + exit(load_configuration(override_configpath, C_VALIDATE) ? 0 : 1); } /* If the user passes more arguments, we act like i3-msg would: Just send @@ -519,7 +534,7 @@ int main(int argc, char *argv[]) { conn = xcb_connect(NULL, &conn_screen); if (xcb_connection_has_error(conn)) - errx(EXIT_FAILURE, "Cannot open display\n"); + errx(EXIT_FAILURE, "Cannot open display"); sndisplay = sn_xcb_display_new(conn, NULL, NULL); @@ -533,7 +548,7 @@ int main(int argc, char *argv[]) { root_screen = xcb_aux_get_screen(conn, conn_screen); root = root_screen->root; -/* Place requests for the atoms we need as soon as possible */ + /* Place requests for the atoms we need as soon as possible */ #define xmacro(atom) \ xcb_intern_atom_cookie_t atom##_cookie = xcb_intern_atom(conn, 0, strlen(#atom), #atom); #include "atoms.xmacro" @@ -571,7 +586,7 @@ int main(int argc, char *argv[]) { xcb_get_geometry_cookie_t gcookie = xcb_get_geometry(conn, root); xcb_query_pointer_cookie_t pointercookie = xcb_query_pointer(conn, root); -/* Setup NetWM atoms */ + /* Setup NetWM atoms */ #define xmacro(name) \ do { \ xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(conn, name##_cookie, NULL); \ @@ -585,7 +600,7 @@ int main(int argc, char *argv[]) { #include "atoms.xmacro" #undef xmacro - load_configuration(conn, override_configpath, false); + load_configuration(override_configpath, C_LOAD); if (config.ipc_socket_path == NULL) { /* Fall back to a file name in /tmp/ based on the PID */ @@ -627,6 +642,9 @@ int main(int argc, char *argv[]) { xcb_set_root_cursor(XCURSOR_CURSOR_POINTER); const xcb_query_extension_reply_t *extreply; + xcb_prefetch_extension_data(conn, &xcb_xkb_id); + xcb_prefetch_extension_data(conn, &xcb_shape_id); + extreply = xcb_get_extension_data(conn, &xcb_xkb_id); xkb_supported = extreply->present; if (!extreply->present) { @@ -688,6 +706,23 @@ int main(int argc, char *argv[]) { xkb_base = extreply->first_event; } + /* Check for Shape extension. We want to handle input shapes which is + * introduced in 1.1. */ + extreply = xcb_get_extension_data(conn, &xcb_shape_id); + if (extreply->present) { + shape_base = extreply->first_event; + xcb_shape_query_version_cookie_t cookie = xcb_shape_query_version(conn); + xcb_shape_query_version_reply_t *version = + xcb_shape_query_version_reply(conn, cookie, NULL); + shape_supported = version && version->minor_version >= 1; + free(version); + } else { + shape_supported = false; + } + if (!shape_supported) { + DLOG("shape 1.1 is not present on this server\n"); + } + restore_connect(); property_handlers_init(); @@ -826,15 +861,21 @@ int main(int argc, char *argv[]) { } } + { + const int restart_fd = parse_restart_fd(); + if (restart_fd != -1) { + DLOG("serving restart fd %d", restart_fd); + ipc_client *client = ipc_new_client_on_fd(main_loop, restart_fd); + ipc_confirm_restart(client); + } + } + /* Set up i3 specific atoms like I3_SOCKET_PATH and I3_CONFIG_PATH */ x_set_i3_atoms(); ewmh_update_workarea(); /* Set the ewmh desktop properties. */ - ewmh_update_current_desktop(); - ewmh_update_number_of_desktops(); - ewmh_update_desktop_names(); - ewmh_update_desktop_viewport(); + ewmh_update_desktop_properties(); struct ev_io *xcb_watcher = scalloc(1, sizeof(struct ev_io)); xcb_prepare = scalloc(1, sizeof(struct ev_prepare)); diff --git a/src/manage.c b/src/manage.c index c4706b0d..80faa167 100644 --- a/src/manage.c +++ b/src/manage.c @@ -80,6 +80,8 @@ void restore_geometry(void) { */ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cookie, bool needs_to_be_mapped) { + DLOG("window 0x%08x\n", window); + xcb_drawable_t d = {window}; xcb_get_geometry_cookie_t geomc; xcb_get_geometry_reply_t *geom; @@ -163,8 +165,6 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki wm_user_time_cookie = GET_PROPERTY(A__NET_WM_USER_TIME, UINT32_MAX); wm_desktop_cookie = GET_PROPERTY(A__NET_WM_DESKTOP, UINT32_MAX); - DLOG("Managing window 0x%08x\n", window); - i3Window *cwindow = scalloc(1, sizeof(i3Window)); cwindow->id = window; cwindow->depth = get_visual_depth(attr->visual); @@ -185,9 +185,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki window_update_hints(cwindow, xcb_get_property_reply(conn, wm_hints_cookie, NULL), &urgency_hint); border_style_t motif_border_style = BS_NORMAL; window_update_motif_hints(cwindow, xcb_get_property_reply(conn, motif_wm_hints_cookie, NULL), &motif_border_style); - xcb_size_hints_t wm_size_hints; - if (!xcb_icccm_get_wm_size_hints_reply(conn, wm_normal_hints_cookie, &wm_size_hints, NULL)) - memset(&wm_size_hints, '\0', sizeof(xcb_size_hints_t)); + window_update_normal_hints(cwindow, xcb_get_property_reply(conn, wm_normal_hints_cookie, NULL), geom); xcb_get_property_reply_t *type_reply = xcb_get_property_reply(conn, wm_type_cookie, NULL); xcb_get_property_reply_t *state_reply = xcb_get_property_reply(conn, state_cookie, NULL); @@ -437,10 +435,9 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki xcb_reply_contains_atom(type_reply, A__NET_WM_WINDOW_TYPE_TOOLBAR) || xcb_reply_contains_atom(type_reply, A__NET_WM_WINDOW_TYPE_SPLASH) || xcb_reply_contains_atom(state_reply, A__NET_WM_STATE_MODAL) || - (wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE && - wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE && - wm_size_hints.min_height == wm_size_hints.max_height && - wm_size_hints.min_width == wm_size_hints.max_width)) { + (cwindow->max_width > 0 && cwindow->max_height > 0 && + cwindow->min_height == cwindow->max_height && + cwindow->min_width == cwindow->max_width)) { LOG("This window is a dialog window, setting floating\n"); want_floating = true; } @@ -499,29 +496,6 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki if (cwindow->dock) want_floating = false; - /* Plasma windows set their geometry in WM_SIZE_HINTS. */ - if ((wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_US_POSITION || wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_P_POSITION) && - (wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_US_SIZE || wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_P_SIZE)) { - DLOG("We are setting geometry according to wm_size_hints x=%d y=%d w=%d h=%d\n", - wm_size_hints.x, wm_size_hints.y, wm_size_hints.width, wm_size_hints.height); - geom->x = wm_size_hints.x; - geom->y = wm_size_hints.y; - geom->width = wm_size_hints.width; - geom->height = wm_size_hints.height; - } - - if (wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE) { - DLOG("Window specifies minimum size %d x %d\n", wm_size_hints.min_width, wm_size_hints.min_height); - nc->window->min_width = wm_size_hints.min_width; - nc->window->min_height = wm_size_hints.min_height; - } - - if (wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE) { - DLOG("Window specifies maximum size %d x %d\n", wm_size_hints.max_width, wm_size_hints.max_height); - nc->window->max_width = wm_size_hints.max_width; - nc->window->max_height = wm_size_hints.max_height; - } - /* Store the requested geometry. The width/height gets raised to at least * 75x50 when entering floating mode, which is the minimum size for a * window to be useful (smaller windows are usually overlays/toolbars/… @@ -574,6 +548,23 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki * cleanup) */ xcb_change_save_set(conn, XCB_SET_MODE_INSERT, window); + if (shape_supported) { + /* Receive ShapeNotify events whenever the client altered its window + * shape. */ + xcb_shape_select_input(conn, window, true); + + /* Check if the window is shaped. Sadly, we can check only for the + * bounding shape, not for the input shape. */ + xcb_shape_query_extents_cookie_t cookie = + xcb_shape_query_extents(conn, window); + xcb_shape_query_extents_reply_t *reply = + xcb_shape_query_extents_reply(conn, cookie, NULL); + if (reply != NULL && reply->bounding_shaped) { + cwindow->shaped = true; + } + FREE(reply); + } + /* Check if any assignments match */ run_assignments(cwindow); @@ -595,13 +586,13 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki * workspace at all. However, just calling render_con() on the * workspace isn’t enough either — it needs the rect. */ ws->rect = ws->parent->rect; - render_con(ws, true); + render_con(ws); /* Disable setting focus, otherwise we’d move focus to an invisible * workspace, which we generally prevent (e.g. in * con_move_to_workspace). */ set_focus = false; } - render_con(croot, false); + render_con(croot); /* Send an event about window creation */ ipc_send_window_event("new", nc); diff --git a/src/output.c b/src/output.c index ebba5c77..3d931b1b 100644 --- a/src/output.c +++ b/src/output.c @@ -54,16 +54,8 @@ char *output_primary_name(Output *output) { Output *get_output_for_con(Con *con) { Con *output_con = con_get_output(con); - if (output_con == NULL) { - ELOG("Could not get the output container for con = %p.\n", con); - return NULL; - } - Output *output = get_output_by_name(output_con->name, true); - if (output == NULL) { - ELOG("Could not get output from name \"%s\".\n", output_con->name); - return NULL; - } + assert(output != NULL); return output; } diff --git a/src/randr.c b/src/randr.c index 6bb8d9e6..fb127ab5 100644 --- a/src/randr.c +++ b/src/randr.c @@ -420,7 +420,10 @@ void output_init_con(Output *output) { * • Create the first unused workspace. * */ -void init_ws_for_output(Output *output, Con *content) { +void init_ws_for_output(Output *output) { + Con *content = output_get_content(output->con); + Con *previous_focus = con_get_workspace(focused); + /* go through all assignments and move the existing workspaces to this output */ struct Workspace_Assignment *assignment; TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) { @@ -442,49 +445,18 @@ void init_ws_for_output(Output *output, Con *content) { continue; } - /* if so, move it over */ - LOG("Moving workspace \"%s\" from output \"%s\" to \"%s\" due to assignment\n", - workspace->name, workspace_out->name, output_primary_name(output)); - - /* if the workspace is currently visible on that output, we need to - * switch to a different workspace - otherwise the output would end up - * with no active workspace */ - bool visible = workspace_is_visible(workspace); - Con *previous = NULL; - if (visible && (previous = TAILQ_NEXT(workspace, focused))) { - LOG("Switching to previously used workspace \"%s\" on output \"%s\"\n", - previous->name, workspace_out->name); - workspace_show(previous); - } - - /* Render the output on which the workspace was to get correct Rects. - * Then, we need to work with the "content" container, since we cannot - * be sure that the workspace itself was rendered at all (in case it’s - * invisible, it won’t be rendered). */ - render_con(workspace_out, false); - Con *ws_out_content = output_get_content(workspace_out); - - Con *floating_con; - TAILQ_FOREACH(floating_con, &(workspace->floating_head), floating_windows) - /* NB: We use output->con here because content is not yet rendered, - * so it has a rect of {0, 0, 0, 0}. */ - floating_fix_coordinates(floating_con, &(ws_out_content->rect), &(output->con->rect)); - - con_detach(workspace); - con_attach(workspace, content, false); - - /* In case the workspace we just moved was visible but there was no - * other workspace to switch to, we need to initialize the source - * output as well */ - if (visible && previous == NULL) { - LOG("There is no workspace left on \"%s\", re-initializing\n", - workspace_out->name); - init_ws_for_output(get_output_by_name(workspace_out->name, true), - output_get_content(workspace_out)); - DLOG("Done re-initializing, continuing with \"%s\"\n", output_primary_name(output)); - } + DLOG("Moving workspace \"%s\" from output \"%s\" to \"%s\" due to assignment\n", + workspace->name, workspace_out->name, output_primary_name(output)); + /* Need to copy output's rect since content is not yet rendered. We + * can't call render_con here because render_output only proceeds if a + * workspace exists. */ + content->rect = output->con->rect; + workspace_move_to_output(workspace, output); } + /* Temporarily set the focused container, might not be initialized yet. */ + focused = content; + /* if a workspace exists, we are done now */ if (!TAILQ_EMPTY(&(content->nodes_head))) { /* ensure that one of the workspaces is actually visible (in fullscreen @@ -493,10 +465,9 @@ void init_ws_for_output(Output *output, Con *content) { GREP_FIRST(visible, content, child->fullscreen_mode == CF_OUTPUT); if (!visible) { visible = TAILQ_FIRST(&(content->nodes_head)); - focused = content; workspace_show(visible); } - return; + goto restore_focus; } /* otherwise, we create the first assigned ws for this output */ @@ -507,17 +478,18 @@ void init_ws_for_output(Output *output, Con *content) { LOG("Initializing first assigned workspace \"%s\" for output \"%s\"\n", assignment->name, assignment->output); - focused = content; workspace_show_by_name(assignment->name); - return; + goto restore_focus; } /* if there is still no workspace, we create the first free workspace */ DLOG("Now adding a workspace\n"); - Con *ws = create_workspace_on_output(output, content); + workspace_show(create_workspace_on_output(output, content)); - /* TODO: Set focus in main.c */ - con_focus(ws); +restore_focus: + if (previous_focus) { + workspace_show(previous_focus); + } } /* @@ -937,7 +909,7 @@ void randr_query_outputs(void) { if (!TAILQ_EMPTY(&(content->nodes_head))) continue; DLOG("Should add ws for output %s\n", output_primary_name(output)); - init_ws_for_output(output, content); + init_ws_for_output(output); } /* Focus the primary screen, if possible */ @@ -976,13 +948,8 @@ void randr_disable_output(Output *output) { if (output->con != NULL) { /* We need to move the workspaces from the disappearing output to the first output */ - /* 1: Get the con to focus next, if the disappearing ws is focused */ - Con *next = NULL; - if (TAILQ_FIRST(&(croot->focus_head)) == output->con) { - DLOG("This output (%p) was focused! Getting next\n", output->con); - next = focused; - DLOG("next = %p\n", next); - } + /* 1: Get the con to focus next */ + Con *next = focused; /* 2: iterate through workspaces and re-assign them, fixing the coordinates * of floating containers as we go */ @@ -1005,13 +972,12 @@ void randr_disable_output(Output *output) { TAILQ_FOREACH(floating_con, &(current->floating_head), floating_windows) { floating_fix_coordinates(floating_con, &(output->con->rect), &(first->con->rect)); } - DLOG("Done, next\n"); } - DLOG("re-attached all workspaces\n"); + /* Restore focus after con_detach / con_attach. next can be NULL, see #3523. */ if (next) { DLOG("now focusing next = %p\n", next); - con_activate(next); + con_focus(next); workspace_show(con_get_workspace(next)); } @@ -1050,7 +1016,7 @@ void randr_disable_output(Output *output) { static void fallback_to_root_output(void) { root_output->active = true; output_init_con(root_output); - init_ws_for_output(root_output, output_get_content(root_output->con)); + init_ws_for_output(root_output); } /* diff --git a/src/render.c b/src/render.c index 3150fb58..42992f3a 100644 --- a/src/render.c +++ b/src/render.c @@ -37,16 +37,15 @@ int render_deco_height(void) { * updated in X11. * */ -void render_con(Con *con, bool render_fullscreen) { +void render_con(Con *con) { render_params params = { .rect = con->rect, .x = con->rect.x, .y = con->rect.y, .children = con_num_children(con)}; - DLOG("Rendering %snode %p / %s / layout %d / children %d\n", - (render_fullscreen ? "fullscreen " : ""), con, con->name, con->layout, - params.children); + DLOG("Rendering node %p / %s / layout %d / children %d\n", con, con->name, + con->layout, params.children); int i = 0; con->mapped = true; @@ -57,42 +56,14 @@ void render_con(Con *con, bool render_fullscreen) { * needs to be smaller */ Rect *inset = &(con->window_rect); *inset = (Rect){0, 0, con->rect.width, con->rect.height}; - if (!render_fullscreen) + if (con->fullscreen_mode == CF_NONE) { *inset = rect_add(*inset, con_border_style_rect(con)); + } /* Obey x11 border */ inset->width -= (2 * con->border_width); inset->height -= (2 * con->border_width); - /* Obey the aspect ratio, if any, unless we are in fullscreen mode. - * - * The spec isn’t explicit on whether the aspect ratio hints should be - * respected during fullscreen mode. Other WMs such as Openbox don’t do - * that, and this post suggests that this is the correct way to do it: - * https://mail.gnome.org/archives/wm-spec-list/2003-May/msg00007.html - * - * Ignoring aspect ratio during fullscreen was necessary to fix MPlayer - * subtitle rendering, see https://bugs.i3wm.org/594 */ - if (!render_fullscreen && con->window->aspect_ratio > 0.0) { - DLOG("aspect_ratio = %f, current width/height are %d/%d\n", - con->window->aspect_ratio, inset->width, inset->height); - double new_height = inset->height + 1; - int new_width = inset->width; - - while (new_height > inset->height) { - new_height = (1.0 / con->window->aspect_ratio) * new_width; - - if (new_height > inset->height) - new_width--; - } - /* Center the window */ - inset->y += ceil(inset->height / 2) - floor((new_height + .5) / 2); - inset->x += ceil(inset->width / 2) - floor(new_width / 2); - - inset->height = new_height + .5; - inset->width = new_width; - } - /* NB: We used to respect resize increment size hints for tiling * windows up until commit 0db93d9 here. However, since all terminal * emulators cope with ignoring the size hints in a better way than we @@ -110,7 +81,7 @@ void render_con(Con *con, bool render_fullscreen) { if (fullscreen) { fullscreen->rect = params.rect; x_raise_con(fullscreen); - render_con(fullscreen, true); + render_con(fullscreen); /* Fullscreen containers are either global (underneath the CT_ROOT * container) or per-output (underneath the CT_CONTENT container). For * global fullscreen containers, we cannot abort rendering here yet, @@ -153,7 +124,7 @@ void render_con(Con *con, bool render_fullscreen) { DLOG("child at (%d, %d) with (%d x %d)\n", child->rect.x, child->rect.y, child->rect.width, child->rect.height); x_raise_con(child); - render_con(child, false); + render_con(child); i++; } @@ -163,17 +134,17 @@ void render_con(Con *con, bool render_fullscreen) { x_raise_con(child); if ((child = TAILQ_FIRST(&(con->focus_head)))) { /* By rendering the stacked container again, we handle the case - * that we have a non-leaf-container inside the stack. In that - * case, the children of the non-leaf-container need to be raised - * as well. */ - render_con(child, false); + * that we have a non-leaf-container inside the stack. In that + * case, the children of the non-leaf-container need to be + * raised as well. */ + render_con(child); } if (params.children != 1) - /* Raise the stack con itself. This will put the stack decoration on - * top of every stack window. That way, when a new window is opened in - * the stack, the old window will not obscure part of the decoration - * (it’s unmapped afterwards). */ + /* Raise the stack con itself. This will put the stack + * decoration on top of every stack window. That way, when a + * new window is opened in the stack, the old window will not + * obscure part of the decoration (it’s unmapped afterwards). */ x_raise_con(con); } } @@ -192,7 +163,7 @@ static int *precalculate_sizes(Con *con, render_params *p) { Con *child; int i = 0, assigned = 0; - int total = con_orientation(con) == HORIZ ? p->rect.width : p->rect.height; + int total = con_rect_size_in_orientation(con); TAILQ_FOREACH(child, &(con->nodes_head), nodes) { double percentage = child->percent > 0.0 ? child->percent : 1.0 / p->children; assigned += sizes[i++] = lround(percentage * total); @@ -215,7 +186,7 @@ static void render_root(Con *con, Con *fullscreen) { Con *output; if (!fullscreen) { TAILQ_FOREACH(output, &(con->nodes_head), nodes) { - render_con(output, false); + render_con(output); } } @@ -281,7 +252,7 @@ static void render_root(Con *con, Con *fullscreen) { DLOG("floating child at (%d,%d) with %d x %d\n", child->rect.x, child->rect.y, child->rect.width, child->rect.height); x_raise_con(child); - render_con(child, false); + render_con(child); } } } @@ -331,7 +302,7 @@ static void render_output(Con *con) { if (fullscreen) { fullscreen->rect = con->rect; x_raise_con(fullscreen); - render_con(fullscreen, true); + render_con(fullscreen); return; } @@ -371,7 +342,7 @@ static void render_output(Con *con) { DLOG("child at (%d, %d) with (%d x %d)\n", child->rect.x, child->rect.y, child->rect.width, child->rect.height); x_raise_con(child); - render_con(child, false); + render_con(child); } } diff --git a/src/resize.c b/src/resize.c index d746ea22..5ddee5c1 100644 --- a/src/resize.c +++ b/src/resize.c @@ -101,30 +101,16 @@ bool resize_find_tiling_participants(Con **current, Con **other, direction_t dir return true; } -/* - * Calculate the given container's new percent given a change in pixels. - * - */ -double px_resize_to_percent(Con *con, int px_diff) { - Con *parent = con->parent; - const orientation_t o = con_orientation(parent); - const int total = (o == HORIZ ? parent->rect.width : parent->rect.height); - /* deco_rect.height is subtracted from each child in render_con_split */ - const int target = px_diff + (o == HORIZ ? con->rect.width : con->rect.height + con->deco_rect.height); - return ((double)target / (double)total); -} - /* * Calculate the minimum percent needed for the given container to be at least 1 * pixel. * */ double percent_for_1px(Con *con) { - Con *parent = con->parent; - const orientation_t o = con_orientation(parent); - const int total = (o == HORIZ ? parent->rect.width : parent->rect.height); - const int target = (o == HORIZ ? 1 : 1 + con->deco_rect.height); - return ((double)target / (double)total); + const int parent_size = con_rect_size_in_orientation(con->parent); + /* deco_rect.height is subtracted from each child in render_con_split */ + const int min_size = (con_orientation(con->parent) == HORIZ ? 1 : 1 + con->deco_rect.height); + return ((double)min_size / (double)parent_size); } /* @@ -145,8 +131,10 @@ bool resize_neighboring_cons(Con *first, Con *second, int px, int ppt) { new_first_percent = first->percent + ((double)ppt / 100.0); new_second_percent = second->percent - ((double)ppt / 100.0); } else { - new_first_percent = px_resize_to_percent(first, px); - new_second_percent = second->percent + first->percent - new_first_percent; + /* Convert px change to change in percentages */ + const double pct = (double)px / (double)con_rect_size_in_orientation(first->parent); + new_first_percent = first->percent + pct; + new_second_percent = second->percent - pct; } /* Ensure that no container will be less than 1 pixel in the resizing * direction. */ @@ -234,6 +222,11 @@ void resize_graphical_handler(Con *first, Con *second, orientation_t orientation int pixels = (new_position - initial_position); DLOG("Done, pixels = %d\n", pixels); + /* No change; no action needed. */ + if (pixels == 0) { + return; + } + /* if we got thus far, the containers must have valid percentages. */ assert(first->percent > 0.0); assert(second->percent > 0.0); diff --git a/src/restore_layout.c b/src/restore_layout.c index b99a50c1..186c0907 100644 --- a/src/restore_layout.c +++ b/src/restore_layout.c @@ -115,7 +115,7 @@ void restore_connect(void) { #ifdef I3_ASAN_ENABLED __lsan_do_leak_check(); #endif - errx(EXIT_FAILURE, "Cannot open display\n"); + errx(EXIT_FAILURE, "Cannot open display"); } xcb_watcher = scalloc(1, sizeof(struct ev_io)); @@ -180,8 +180,8 @@ static void update_placeholder_contents(placeholder_state *state) { int y = (state->rect.height / 2) - (config.font.height / 2); draw_util_text(line, &(state->surface), foreground, background, x, y, text_width); i3string_free(line); - xcb_flush(conn); - xcb_aux_sync(conn); + xcb_flush(restore_conn); + xcb_aux_sync(restore_conn); } static void open_placeholder_window(Con *con) { @@ -221,7 +221,7 @@ static void open_placeholder_window(Con *con) { state->con = con; state->rect = con->rect; - draw_util_surface_init(conn, &(state->surface), placeholder, get_visualtype(root_screen), state->rect.width, state->rect.height); + draw_util_surface_init(restore_conn, &(state->surface), placeholder, get_visualtype(root_screen), state->rect.width, state->rect.height); update_placeholder_contents(state); TAILQ_INSERT_TAIL(&state_head, state, state); diff --git a/src/scratchpad.c b/src/scratchpad.c index d564bf32..b7fbcc92 100644 --- a/src/scratchpad.c +++ b/src/scratchpad.c @@ -121,8 +121,8 @@ bool scratchpad_show(Con *con) { DLOG("Found an unfocused scratchpad window on this workspace\n"); DLOG("Focusing it: %p\n", walk_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 */ + * window inside this scratch container in order to + * keep the focus the same within this container */ con_activate(con_descend_tiling_focused(walk_con)); return true; } @@ -196,7 +196,7 @@ bool scratchpad_show(Con *con) { Con *output = con_get_output(con); con->rect.width = output->rect.width * 0.5; con->rect.height = output->rect.height * 0.75; - floating_check_size(con); + floating_check_size(con, false); floating_center(con, con_get_workspace(con)->rect); } diff --git a/src/sd-daemon.c b/src/sd-daemon.c index dc33b2e9..28a88bfd 100644 --- a/src/sd-daemon.c +++ b/src/sd-daemon.c @@ -413,8 +413,7 @@ int sd_booted(void) { struct stat a, b; - /* We simply test whether the systemd cgroup hierarchy is - * mounted */ + /* We simply test whether the systemd cgroup hierarchy is mounted */ if (lstat("/sys/fs/cgroup", &a) < 0) return 0; diff --git a/src/startup.c b/src/startup.c index 6302d811..c5b7ad5d 100644 --- a/src/startup.c +++ b/src/startup.c @@ -190,6 +190,7 @@ void start_application(const char *command, bool no_startup_id) { /* Setup the environment variable(s) */ if (!no_startup_id) sn_launcher_context_setup_child_process(context); + setenv("I3SOCK", current_socketpath, 1); execl(_PATH_BSHELL, _PATH_BSHELL, "-c", command, NULL); /* not reached */ diff --git a/src/tree.c b/src/tree.c index e3849873..4057d177 100644 --- a/src/tree.c +++ b/src/tree.c @@ -248,6 +248,11 @@ bool tree_close_internal(Con *con, kill_window_t kill_window, bool dont_kill_par * mapped. See https://bugs.i3wm.org/1617 */ xcb_change_save_set(conn, XCB_SET_MODE_DELETE, con->window->id); + /* Stop receiving ShapeNotify events. */ + if (shape_supported) { + xcb_shape_select_input(conn, con->window->id, false); + } + /* Ignore X11 errors for the ReparentWindow request. * X11 Errors are returned when the window was already destroyed */ add_ignore_event(cookie.sequence, 0); @@ -295,10 +300,8 @@ bool tree_close_internal(Con *con, kill_window_t kill_window, bool dont_kill_par x_con_kill(con); 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(); + DLOG("Closing workspace container %s, updating EWMH atoms\n", ws->name); + ewmh_update_desktop_properties(); } con_free(con); @@ -453,7 +456,7 @@ void tree_render(void) { mark_unmapped(croot); croot->mapped = true; - render_con(croot, false); + render_con(croot); x_push_changes(croot); DLOG("-- END RENDERING --\n"); diff --git a/src/util.c b/src/util.c index 85f359c0..9fe3fa44 100644 --- a/src/util.c +++ b/src/util.c @@ -287,7 +287,7 @@ void i3_restart(bool forget_layout) { restore_geometry(); - ipc_shutdown(SHUTDOWN_REASON_RESTART); + ipc_shutdown(SHUTDOWN_REASON_RESTART, -1); LOG("restarting \"%s\"...\n", start_argv[0]); /* make sure -a is in the argument list or add it */ @@ -465,7 +465,7 @@ void kill_nagbar(pid_t *nagbar_pid, bool wait_for_it) { * if the number could be parsed. */ bool parse_long(const char *str, long *out, int base) { - char *end; + char *end = NULL; long result = strtol(str, &end, base); if (result == LONG_MIN || result == LONG_MAX || result < 0 || (end != NULL && *end != '\0')) { *out = result; diff --git a/src/window.c b/src/window.c index 61282556..8c3ae850 100644 --- a/src/window.c +++ b/src/window.c @@ -51,14 +51,10 @@ void window_update_class(i3Window *win, xcb_get_property_reply_t *prop, bool bef LOG("WM_CLASS changed to %s (instance), %s (class)\n", win->class_instance, win->class_class); - if (before_mgmt) { - free(prop); - return; - } - - run_assignments(win); - free(prop); + if (!before_mgmt) { + run_assignments(win); + } } /* @@ -92,14 +88,10 @@ void window_update_name(i3Window *win, xcb_get_property_reply_t *prop, bool befo win->uses_net_wm_name = true; - if (before_mgmt) { - free(prop); - return; - } - - run_assignments(win); - free(prop); + if (!before_mgmt) { + run_assignments(win); + } } /* @@ -141,14 +133,10 @@ void window_update_name_legacy(i3Window *win, xcb_get_property_reply_t *prop, bo win->name_x_changed = true; - if (before_mgmt) { - free(prop); - return; - } - - run_assignments(win); - free(prop); + if (!before_mgmt) { + run_assignments(win); + } } /* @@ -244,14 +232,10 @@ void window_update_role(i3Window *win, xcb_get_property_reply_t *prop, bool befo win->role = new_role; LOG("WM_WINDOW_ROLE changed to \"%s\"\n", win->role); - if (before_mgmt) { - free(prop); - return; - } - - run_assignments(win); - free(prop); + if (!before_mgmt) { + run_assignments(win); + } } /* @@ -272,6 +256,127 @@ void window_update_type(i3Window *window, xcb_get_property_reply_t *reply) { run_assignments(window); } +/* + * Updates the WM_NORMAL_HINTS + * + */ +bool window_update_normal_hints(i3Window *win, xcb_get_property_reply_t *reply, xcb_get_geometry_reply_t *geom) { + bool changed = false; + xcb_size_hints_t size_hints; + + /* If the hints were already in this event, use them, if not, request them */ + bool success; + if (reply != NULL) { + success = xcb_icccm_get_wm_size_hints_from_reply(&size_hints, reply); + } else { + success = xcb_icccm_get_wm_normal_hints_reply(conn, xcb_icccm_get_wm_normal_hints_unchecked(conn, win->id), &size_hints, NULL); + } + if (!success) { + DLOG("Could not get WM_NORMAL_HINTS\n"); + return false; + } + +#define ASSIGN_IF_CHANGED(original, new) \ + do { \ + if (original != new) { \ + original = new; \ + changed = true; \ + } \ + } while (0) + + if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE)) { + DLOG("Minimum size: %d (width) x %d (height)\n", size_hints.min_width, size_hints.min_height); + + ASSIGN_IF_CHANGED(win->min_width, size_hints.min_width); + ASSIGN_IF_CHANGED(win->min_height, size_hints.min_height); + } + + if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE)) { + DLOG("Maximum size: %d (width) x %d (height)\n", size_hints.max_width, size_hints.max_height); + + int max_width = max(0, size_hints.max_width); + int max_height = max(0, size_hints.max_height); + + ASSIGN_IF_CHANGED(win->max_width, max_width); + ASSIGN_IF_CHANGED(win->max_height, max_height); + } else { + DLOG("Clearing maximum size \n"); + + ASSIGN_IF_CHANGED(win->max_width, 0); + ASSIGN_IF_CHANGED(win->max_height, 0); + } + + if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_RESIZE_INC)) { + DLOG("Size increments: %d (width) x %d (height)\n", size_hints.width_inc, size_hints.height_inc); + + if (size_hints.width_inc > 0 && size_hints.width_inc < 0xFFFF) { + ASSIGN_IF_CHANGED(win->width_increment, size_hints.width_inc); + } else { + ASSIGN_IF_CHANGED(win->width_increment, 0); + } + + if (size_hints.height_inc > 0 && size_hints.height_inc < 0xFFFF) { + ASSIGN_IF_CHANGED(win->height_increment, size_hints.height_inc); + } else { + ASSIGN_IF_CHANGED(win->height_increment, 0); + } + } else { + DLOG("Clearing size increments\n"); + + ASSIGN_IF_CHANGED(win->width_increment, 0); + ASSIGN_IF_CHANGED(win->height_increment, 0); + } + + /* The base width / height is the desired size of the window. */ + if (size_hints.flags & XCB_ICCCM_SIZE_HINT_BASE_SIZE && + (win->base_width >= 0) && (win->base_height >= 0)) { + DLOG("Base size: %d (width) x %d (height)\n", size_hints.base_width, size_hints.base_height); + + ASSIGN_IF_CHANGED(win->base_width, size_hints.base_width); + ASSIGN_IF_CHANGED(win->base_height, size_hints.base_height); + } else { + DLOG("Clearing base size\n"); + + ASSIGN_IF_CHANGED(win->base_width, 0); + ASSIGN_IF_CHANGED(win->base_height, 0); + } + + if (geom != NULL && + (size_hints.flags & XCB_ICCCM_SIZE_HINT_US_POSITION || size_hints.flags & XCB_ICCCM_SIZE_HINT_P_POSITION) && + (size_hints.flags & XCB_ICCCM_SIZE_HINT_US_SIZE || size_hints.flags & XCB_ICCCM_SIZE_HINT_P_SIZE)) { + DLOG("Setting geometry x=%d y=%d w=%d h=%d\n", size_hints.x, size_hints.y, size_hints.width, size_hints.height); + geom->x = size_hints.x; + geom->y = size_hints.y; + geom->width = size_hints.width; + geom->height = size_hints.height; + } + + /* If no aspect ratio was set or if it was invalid, we ignore the hints */ + if (size_hints.flags & XCB_ICCCM_SIZE_HINT_P_ASPECT && + (size_hints.min_aspect_num >= 0) && (size_hints.min_aspect_den > 0) && + (size_hints.max_aspect_num >= 0) && (size_hints.max_aspect_den > 0)) { + /* Convert numerator/denominator to a double */ + double min_aspect = (double)size_hints.min_aspect_num / size_hints.min_aspect_den; + double max_aspect = (double)size_hints.max_aspect_num / size_hints.max_aspect_den; + DLOG("Aspect ratio set: minimum %f, maximum %f\n", min_aspect, max_aspect); + if (fabs(win->min_aspect_ratio - min_aspect) > DBL_EPSILON) { + win->min_aspect_ratio = min_aspect; + changed = true; + } + if (fabs(win->max_aspect_ratio - max_aspect) > DBL_EPSILON) { + win->max_aspect_ratio = max_aspect; + changed = true; + } + } else { + DLOG("Clearing aspect ratios\n"); + + ASSIGN_IF_CHANGED(win->min_aspect_ratio, 0.0); + ASSIGN_IF_CHANGED(win->max_aspect_ratio, 0.0); + } + + return changed; +} + /* * Updates the WM_HINTS (we only care about the input focus handling part). * @@ -318,7 +423,7 @@ void window_update_hints(i3Window *win, xcb_get_property_reply_t *prop, bool *ur * */ void window_update_motif_hints(i3Window *win, xcb_get_property_reply_t *prop, border_style_t *motif_border_style) { -/* This implementation simply mirrors Gnome's Metacity. Official + /* This implementation simply mirrors Gnome's Metacity. Official * documentation of this hint is nowhere to be found. * For more information see: * https://people.gnome.org/~tthurman/docs/metacity/xprops_8h-source.html diff --git a/src/workspace.c b/src/workspace.c index a2d9b0e8..59705798 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -11,9 +11,12 @@ #include "all.h" #include "yajl_utils.h" -/* Stores a copy of the name of the last used workspace for the workspace - * back-and-forth switching. */ -static char *previous_workspace_name = NULL; +/* + * Stores a copy of the name of the last used workspace for the workspace + * back-and-forth switching. + * + */ +char *previous_workspace_name = NULL; /* NULL-terminated list of workspace names (in order) extracted from * keybindings. */ @@ -158,10 +161,7 @@ Con *workspace_get(const char *num, bool *created) { con_attach(workspace, content, false); ipc_send_workspace_event("init", workspace, NULL); - ewmh_update_number_of_desktops(); - ewmh_update_desktop_names(); - ewmh_update_desktop_viewport(); - ewmh_update_wm_desktop(); + ewmh_update_desktop_properties(); if (created != NULL) *created = true; } else if (created != NULL) { @@ -286,6 +286,7 @@ Con *create_workspace_on_output(Output *output, Con *content) { ws->workspace_layout = config.default_layout; _workspace_apply_default_orientation(ws); + ipc_send_workspace_event("init", ws, NULL); return ws; } @@ -516,10 +517,7 @@ void workspace_show(Con *workspace) { old_focus = NULL; } - ewmh_update_number_of_desktops(); - ewmh_update_desktop_names(); - ewmh_update_desktop_viewport(); - ewmh_update_wm_desktop(); + ewmh_update_desktop_properties(); } } @@ -957,26 +955,29 @@ Con *workspace_encapsulate(Con *ws) { /* * Move the given workspace to the specified output. - * This returns true if and only if moving the workspace was successful. */ -bool workspace_move_to_output(Con *ws, Output *output) { - LOG("Trying to move workspace %p / %s to output %p / \"%s\".\n", ws, ws->name, output, output_primary_name(output)); +void workspace_move_to_output(Con *ws, Output *output) { + DLOG("Moving workspace %p / %s to output %p / \"%s\".\n", ws, ws->name, output, output_primary_name(output)); Output *current_output = get_output_for_con(ws); - if (current_output == NULL) { - ELOG("Cannot get current output. This is a bug in i3.\n"); - return false; + Con *content = output_get_content(output->con); + DLOG("got output %p with content %p\n", output, content); + + if (ws->parent == content) { + DLOG("Nothing to do, workspace already there\n"); + return; } - Con *content = output_get_content(output->con); - LOG("got output %p with content %p\n", output, content); - Con *previously_visible_ws = TAILQ_FIRST(&(content->focus_head)); - LOG("Previously visible workspace = %p / %s\n", previously_visible_ws, previously_visible_ws->name); + if (previously_visible_ws) { + DLOG("Previously visible workspace = %p / %s\n", previously_visible_ws, previously_visible_ws->name); + } else { + DLOG("No previously visible workspace on output.\n"); + } bool workspace_was_visible = workspace_is_visible(ws); if (con_num_children(ws->parent) == 1) { - LOG("Creating a new workspace to replace \"%s\" (last on its output).\n", ws->name); + DLOG("Creating a new workspace to replace \"%s\" (last on its output).\n", ws->name); /* check if we can find a workspace assigned to this output */ bool used_assignment = false; @@ -991,19 +992,18 @@ bool workspace_move_to_output(Con *ws, Output *output) { } /* so create the workspace referenced to by this assignment */ - LOG("Creating workspace from assignment %s.\n", assignment->name); + DLOG("Creating workspace from assignment %s.\n", assignment->name); workspace_get(assignment->name, NULL); used_assignment = true; break; } - /* if we couldn't create the workspace using an assignment, create - * it on the output */ - if (!used_assignment) + /* if we couldn't create the workspace using an assignment, create it on + * the output. Workspace init IPC events are sent either by + * workspace_get or create_workspace_on_output. */ + if (!used_assignment) { create_workspace_on_output(current_output, ws->parent); - - /* notify the IPC listeners */ - ipc_send_workspace_event("init", ws, NULL); + } } DLOG("Detaching\n"); @@ -1011,18 +1011,19 @@ bool workspace_move_to_output(Con *ws, Output *output) { Con *old_content = ws->parent; con_detach(ws); if (workspace_was_visible) { - /* The workspace which we just detached was visible, so focus - * the next one in the focus-stack. */ + /* The workspace which we just detached was visible, so focus the next + * one in the focus-stack. */ Con *focus_ws = TAILQ_FIRST(&(old_content->focus_head)); - LOG("workspace was visible, focusing %p / %s now\n", focus_ws, focus_ws->name); + DLOG("workspace was visible, focusing %p / %s now\n", focus_ws, focus_ws->name); workspace_show(focus_ws); } con_attach(ws, content, false); /* fix the coordinates of the floating containers */ Con *floating_con; - TAILQ_FOREACH(floating_con, &(ws->floating_head), floating_windows) - floating_fix_coordinates(floating_con, &(old_content->rect), &(content->rect)); + TAILQ_FOREACH(floating_con, &(ws->floating_head), floating_windows) { + floating_fix_coordinates(floating_con, &(old_content->rect), &(content->rect)); + } ipc_send_workspace_event("move", ws, NULL); if (workspace_was_visible) { @@ -1030,21 +1031,24 @@ bool workspace_move_to_output(Con *ws, Output *output) { workspace_show(ws); } - /* NB: We cannot simply work with previously_visible_ws since it might - * have been cleaned up by workspace_show() already, depending on the - * focus order/number of other workspaces on the output. - * Instead, we loop through the available workspaces and only work with - * previously_visible_ws if we still find it. */ + if (!previously_visible_ws) { + return; + } + + /* NB: We cannot simply work with previously_visible_ws since it might have + * been cleaned up by workspace_show() already, depending on the focus + * order/number of other workspaces on the output. Instead, we loop through + * the available workspaces and only work with previously_visible_ws if we + * still find it. */ TAILQ_FOREACH(ws, &(content->nodes_head), nodes) { - if (ws != previously_visible_ws) + if (ws != previously_visible_ws) { continue; + } /* Call the on_remove_child callback of the workspace which previously - * was visible on the destination output. Since it is no longer - * visible, it might need to get cleaned up. */ + * was visible on the destination output. Since it is no longer visible, + * it might need to get cleaned up. */ CALL(previously_visible_ws, on_remove_child); break; } - - return true; } diff --git a/src/x.c b/src/x.c index 267372f9..e6d875e5 100644 --- a/src/x.c +++ b/src/x.c @@ -51,6 +51,11 @@ typedef struct con_state { bool need_reparent; xcb_window_t old_frame; + /* The container was child of floating container during the previous call of + * x_push_node(). This is used to remove the shape when the container is no + * longer floating. */ + bool was_floating; + Rect rect; Rect window_rect; @@ -94,7 +99,7 @@ static con_state *state_for_frame(xcb_window_t window) { return state; /* TODO: better error handling? */ - ELOG("No state found\n"); + ELOG("No state found for window 0x%08x\n", window); assert(false); return NULL; } @@ -263,6 +268,7 @@ static void _x_con_kill(Con *con) { draw_util_surface_free(conn, &(con->frame)); draw_util_surface_free(conn, &(con->frame_buffer)); xcb_free_pixmap(conn, con->frame_buffer.id); + con->frame_buffer.id = XCB_NONE; state = state_for_frame(con->frame.id); CIRCLEQ_REMOVE(&state_head, state, state); CIRCLEQ_REMOVE(&old_state_head, state, old_state); @@ -271,7 +277,12 @@ static void _x_con_kill(Con *con) { free(state); /* Invalidate focused_id to correctly focus new windows with the same ID */ - focused_id = last_focused = XCB_NONE; + if (con->frame.id == focused_id) { + focused_id = XCB_NONE; + } + if (con->frame.id == last_focused) { + last_focused = XCB_NONE; + } } /* @@ -355,20 +366,22 @@ static void x_draw_title_border(Con *con, struct deco_render_params *p) { assert(con->parent != NULL); Rect *dr = &(con->deco_rect); - adjacent_t borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; - int deco_diff_l = borders_to_hide & ADJ_LEFT_SCREEN_EDGE ? 0 : con->current_border_width; - int deco_diff_r = borders_to_hide & ADJ_RIGHT_SCREEN_EDGE ? 0 : con->current_border_width; - if (con->parent->layout == L_TABBED || - (con->parent->layout == L_STACKED && TAILQ_NEXT(con, nodes) != NULL)) { - deco_diff_l = 0; - deco_diff_r = 0; - } + /* Left */ + draw_util_rectangle(&(con->parent->frame_buffer), p->color->border, + dr->x, dr->y, 1, dr->height); + + /* Right */ + draw_util_rectangle(&(con->parent->frame_buffer), p->color->border, + dr->x + dr->width - 1, dr->y, 1, dr->height); + + /* Top */ draw_util_rectangle(&(con->parent->frame_buffer), p->color->border, dr->x, dr->y, dr->width, 1); + /* Bottom */ draw_util_rectangle(&(con->parent->frame_buffer), p->color->border, - dr->x + deco_diff_l, dr->y + dr->height - 1, dr->width - (deco_diff_l + deco_diff_r), 1); + dr->x, dr->y + dr->height - 1, dr->width, 1); } static void x_draw_decoration_after_title(Con *con, struct deco_render_params *p) { @@ -390,22 +403,62 @@ static void x_draw_decoration_after_title(Con *con, struct deco_render_params *p dr->height); } - /* Draw a 1px separator line before and after every tab, so that tabs can - * be easily distinguished. */ - if (con->parent->layout == L_TABBED) { - /* Left side */ - draw_util_rectangle(&(con->parent->frame_buffer), p->color->border, - dr->x, dr->y, 1, dr->height); - - /* Right side */ - draw_util_rectangle(&(con->parent->frame_buffer), p->color->border, - dr->x + dr->width - 1, dr->y, 1, dr->height); - } - /* Redraw the border. */ x_draw_title_border(con, p); } +/* + * Get rectangles representing the border around the child window. Some borders + * are adjacent to the screen-edge and thus not returned. Return value is the + * number of rectangles. + * + */ +static size_t x_get_border_rectangles(Con *con, xcb_rectangle_t rectangles[4]) { + size_t count = 0; + int border_style = con_border_style(con); + + if (border_style != BS_NONE && con_is_leaf(con)) { + adjacent_t borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; + Rect br = con_border_style_rect(con); + + if (!(borders_to_hide & ADJ_LEFT_SCREEN_EDGE)) { + rectangles[count++] = (xcb_rectangle_t){ + .x = 0, + .y = 0, + .width = br.x, + .height = con->rect.height, + }; + } + if (!(borders_to_hide & ADJ_RIGHT_SCREEN_EDGE)) { + rectangles[count++] = (xcb_rectangle_t){ + .x = con->rect.width + (br.width + br.x), + .y = 0, + .width = -(br.width + br.x), + .height = con->rect.height, + }; + } + if (!(borders_to_hide & ADJ_LOWER_SCREEN_EDGE)) { + rectangles[count++] = (xcb_rectangle_t){ + .x = br.x, + .y = con->rect.height + (br.height + br.y), + .width = con->rect.width + br.width, + .height = -(br.height + br.y), + }; + } + /* pixel border have an additional line at the top */ + if (border_style == BS_PIXEL && !(borders_to_hide & ADJ_UPPER_SCREEN_EDGE)) { + rectangles[count++] = (xcb_rectangle_t){ + .x = br.x, + .y = 0, + .width = con->rect.width + br.width, + .height = br.y, + }; + } + } + + return count; +} + /* * Draws the decoration of the given container onto its parent. * @@ -507,37 +560,24 @@ void x_draw_decoration(Con *con) { /* 3: draw a rectangle in border color around the client */ if (p->border_style != BS_NONE && p->con_is_leaf) { - /* We might hide some borders adjacent to the screen-edge */ - adjacent_t borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; - Rect br = con_border_style_rect(con); - - /* These rectangles represent the border around the child window - * (left, bottom and right part). We don’t just fill the whole - * rectangle because some children are not freely resizable and we want - * their background color to "shine through". */ - if (!(borders_to_hide & ADJ_LEFT_SCREEN_EDGE)) { - draw_util_rectangle(&(con->frame_buffer), p->color->child_border, 0, 0, br.x, r->height); - } - if (!(borders_to_hide & ADJ_RIGHT_SCREEN_EDGE)) { - draw_util_rectangle(&(con->frame_buffer), - p->color->child_border, r->width + (br.width + br.x), 0, - -(br.width + br.x), r->height); - } - if (!(borders_to_hide & ADJ_LOWER_SCREEN_EDGE)) { - draw_util_rectangle(&(con->frame_buffer), - p->color->child_border, br.x, r->height + (br.height + br.y), - r->width + br.width, -(br.height + br.y)); - } - /* pixel border needs an additional line at the top */ - if (p->border_style == BS_PIXEL && !(borders_to_hide & ADJ_UPPER_SCREEN_EDGE)) { - draw_util_rectangle(&(con->frame_buffer), - p->color->child_border, br.x, 0, r->width + br.width, br.y); + /* Fill the border. We don’t just fill the whole rectangle because some + * children are not freely resizable and we want their background color + * to "shine through". */ + xcb_rectangle_t rectangles[4]; + size_t rectangles_count = x_get_border_rectangles(con, rectangles); + for (size_t i = 0; i < rectangles_count; i++) { + draw_util_rectangle(&(con->frame_buffer), p->color->child_border, + rectangles[i].x, + rectangles[i].y, + rectangles[i].width, + rectangles[i].height); } /* Highlight the side of the border at which the next window will be * opened if we are rendering a single window within a split container * (which is undistinguishable from a single window outside a split * container otherwise. */ + Rect br = con_border_style_rect(con); if (TAILQ_NEXT(con, nodes) == NULL && TAILQ_PREV(con, nodes_head, nodes) == NULL && con->parent->type != CT_FLOATING_CON) { @@ -573,7 +613,7 @@ void x_draw_decoration(Con *con) { draw_util_rectangle(&(parent->frame_buffer), p->color->background, con->deco_rect.x, con->deco_rect.y, con->deco_rect.width, con->deco_rect.height); - /* 5: draw two unconnected horizontal lines in border color */ + /* 5: draw title border */ x_draw_title_border(con, p); /* 6: draw the title */ @@ -730,6 +770,71 @@ static void set_hidden_state(Con *con) { state->is_hidden = should_be_hidden; } +/* + * Set the container frame shape as the union of the window shape and the + * shape of the frame borders. + */ +static void x_shape_frame(Con *con, xcb_shape_sk_t shape_kind) { + assert(con->window); + + xcb_shape_combine(conn, XCB_SHAPE_SO_SET, shape_kind, shape_kind, + con->frame.id, + con->window_rect.x + con->border_width, + con->window_rect.y + con->border_width, + con->window->id); + xcb_rectangle_t rectangles[4]; + size_t rectangles_count = x_get_border_rectangles(con, rectangles); + if (rectangles_count) { + xcb_shape_rectangles(conn, XCB_SHAPE_SO_UNION, shape_kind, + XCB_CLIP_ORDERING_UNSORTED, con->frame.id, + 0, 0, rectangles_count, rectangles); + } +} + +/* + * Reset the container frame shape. + */ +static void x_unshape_frame(Con *con, xcb_shape_sk_t shape_kind) { + assert(con->window); + + xcb_shape_mask(conn, XCB_SHAPE_SO_SET, shape_kind, con->frame.id, 0, 0, XCB_PIXMAP_NONE); +} + +/* + * Shape or unshape container frame based on the con state. + */ +static void set_shape_state(Con *con, bool need_reshape) { + if (!shape_supported || con->window == NULL) { + return; + } + + struct con_state *state; + if ((state = state_for_frame(con->frame.id)) == NULL) { + ELOG("window state for con %p not found\n", con); + return; + } + + if (need_reshape && con_is_floating(con)) { + /* We need to reshape the window frame only if it already has shape. */ + if (con->window->shaped) { + x_shape_frame(con, XCB_SHAPE_SK_BOUNDING); + } + if (con->window->input_shaped) { + x_shape_frame(con, XCB_SHAPE_SK_INPUT); + } + } + + if (state->was_floating && !con_is_floating(con)) { + /* Remove the shape when container is no longer floating. */ + if (con->window->shaped) { + x_unshape_frame(con, XCB_SHAPE_SK_BOUNDING); + } + if (con->window->input_shaped) { + x_unshape_frame(con, XCB_SHAPE_SK_INPUT); + } + } +} + /* * This function pushes the properties of each node of the layout tree to * X11 if they have changed (like the map state, position of the window, …). @@ -768,6 +873,8 @@ void x_push_node(Con *con) { con->mapped = false; } + bool need_reshape = false; + /* reparent the child window (when the window was moved due to a sticky * container) */ if (state->need_reparent && con->window != NULL) { @@ -793,8 +900,19 @@ void x_push_node(Con *con) { con->ignore_unmap++; DLOG("ignore_unmap for reparenting of con %p (win 0x%08x) is now %d\n", con, con->window->id, con->ignore_unmap); + + need_reshape = true; } + /* We need to update shape when window frame dimensions is updated. */ + need_reshape |= state->rect.width != rect.width || + state->rect.height != rect.height || + state->window_rect.width != con->window_rect.width || + state->window_rect.height != con->window_rect.height; + + /* We need to set shape when container becomes floating. */ + need_reshape |= con_is_floating(con) && !state->was_floating; + /* The pixmap of a borderless leaf container will not be used except * for the titlebar in a stack or tabs (issue #1013). */ bool is_pixmap_needed = (con->border_style != BS_NONE || @@ -898,6 +1016,8 @@ void x_push_node(Con *con) { fake_notify = true; } + set_shape_state(con, need_reshape); + /* Map if map state changed, also ensure that the child window * is changed if we are mapped and there is a new, unmapped child window. * Unmaps are handled in x_push_node_unmaps(). */ @@ -941,6 +1061,7 @@ void x_push_node(Con *con) { } state->unmap_now = (state->mapped != con->mapped) && !con->mapped; + state->was_floating = con_is_floating(con); if (fake_notify) { DLOG("Sending fake configure notify\n"); @@ -1205,6 +1326,7 @@ void x_push_changes(Con *con) { change_ewmh_focus(XCB_WINDOW_NONE, last_focused); focused_id = ewmh_window; + last_focused = XCB_NONE; } xcb_flush(conn); @@ -1325,3 +1447,37 @@ void x_mask_event_mask(uint32_t mask) { xcb_change_window_attributes(conn, state->id, XCB_CW_EVENT_MASK, values); } } + +/* + * Enables or disables nonrectangular shape of the container frame. + */ +void x_set_shape(Con *con, xcb_shape_sk_t kind, bool enable) { + struct con_state *state; + if ((state = state_for_frame(con->frame.id)) == NULL) { + ELOG("window state for con %p not found\n", con); + return; + } + + switch (kind) { + case XCB_SHAPE_SK_BOUNDING: + con->window->shaped = enable; + break; + case XCB_SHAPE_SK_INPUT: + con->window->input_shaped = enable; + break; + default: + ELOG("Received unknown shape event kind for con %p. This is a bug.\n", + con); + return; + } + + if (con_is_floating(con)) { + if (enable) { + x_shape_frame(con, kind); + } else { + x_unshape_frame(con, kind); + } + + xcb_flush(conn); + } +} diff --git a/src/xinerama.c b/src/xinerama.c index d0651a85..4acfd3cb 100644 --- a/src/xinerama.c +++ b/src/xinerama.c @@ -71,7 +71,7 @@ static void query_screens(xcb_connection_t *conn) { else TAILQ_INSERT_TAIL(&outputs, s, outputs); output_init_con(s); - init_ws_for_output(s, output_get_content(s->con)); + init_ws_for_output(s); num_screens++; } @@ -98,7 +98,7 @@ static void use_root_output(xcb_connection_t *conn) { s->active = true; TAILQ_INSERT_TAIL(&outputs, s, outputs); output_init_con(s); - init_ws_for_output(s, output_get_content(s->con)); + init_ws_for_output(s); } /* diff --git a/testcases/inject_randr1.5.c b/testcases/inject_randr1.5.c index 520b0213..29b6a33d 100644 --- a/testcases/inject_randr1.5.c +++ b/testcases/inject_randr1.5.c @@ -402,7 +402,7 @@ int main(int argc, char *argv[]) { } if (optind >= argc) { - errx(EXIT_FAILURE, "syntax: %s [options] \n", argv[0]); + errx(EXIT_FAILURE, "syntax: %s [options] ", argv[0]); } int fd = socket(AF_LOCAL, SOCK_STREAM, 0); diff --git a/testcases/lib/SocketActivation.pm b/testcases/lib/SocketActivation.pm index 5a5a4484..a2db5453 100644 --- a/testcases/lib/SocketActivation.pm +++ b/testcases/lib/SocketActivation.pm @@ -135,7 +135,7 @@ sub activate_i3 { # We overwrite LISTEN_PID with the correct process ID to make # socket activation work (LISTEN_PID has to match getpid(), # otherwise the LISTEN_FDS will be treated as a left-over). - $cmd = qq|strace -fF -s2048 -v -o "$out" -- | . + $cmd = qq|strace -fvy -s2048 -o "$out" -- | . 'sh -c "export LISTEN_PID=\$\$; ' . $cmd . '"'; } diff --git a/testcases/lib/i3test.pm.in b/testcases/lib/i3test.pm.in index 5734eca7..740e13e9 100644 --- a/testcases/lib/i3test.pm.in +++ b/testcases/lib/i3test.pm.in @@ -53,6 +53,7 @@ our @EXPORT = qw( events_for listen_for_binding is_net_wm_state_focused + cmp_tree ); =head1 NAME @@ -1084,6 +1085,229 @@ sub is_net_wm_state_focused { return 0; } +=head2 cmp_tree([ $args ]) + +Compares the tree layout before and after an operation inside a subtest. + +The following arguments can be passed: + +=over 4 + +=item layout_before + +Required argument. The initial layout to be created. For example, +'H[ V[ a* S[ b c ] d ] e ]' or 'V[a b] T[c d*]'. +The layout will be converted to a JSON file which will be passed to i3's +append_layout command. + +The syntax's rules, assertions and limitations are: + +=over 8 + +=item 1. + +Upper case letters H, V, S, T mean horizontal, vertical, stacked and tabbed +layout respectively. They must be followed by an opening square bracket and must +be closed with a closing square bracket. +Each of the non-leaf containers is marked with their corresponding letter +followed by a number indicating the position of the container relative to other +containers of the same type. For example, 'H[V[xxx] V[xxx] H[xxx]]' will mark +the non-leaf containers as H1, V1, V2, H2. + +=item 2. + +Spaces are ignored. + +=item 3. + +Other alphanumeric characters mean a new window which uses the provided +character for its class and name. Eg 'H[a b]' will open windows with classes 'a' +and 'b' inside a horizontal split. Windows use a single character for their +class, eg 'H[xxx]' will open 3 windows with class 'x'. + +=item 4. + +Asterisks after a window mean that the window must be focused after the layout +is loaded. Currently, focusing non-leaf containers must be done manually, in the +callback (C) function. + +=back + +=item cb + +Subroutine to be called after the layout provided by C is created +but before the resulting layout (C) is checked. + +=item layout_after + +Required argument. The final layout in which the tree is expected to be after +the callback is called. Uses the same syntax with C. +For non-leaf containers, their layout (horizontal, vertical, stacked, tabbed) +is compared with the corresponding letter (H, V, S, T). +For leaf containers, their name is compared with the provided alphanumeric. + +=item ws + +The workspace in which the layout will be created. Will switch focus to it. If +not provided, a new one is created. + +=item msg + +Message to prepend to the subtest's name. If not empty, it will be followed by ': '. + +=item dont_kill + +By default, all windows are killed before the C layout is loaded. +Set to 1 to avoid this. + +=back + +=cut +sub cmp_tree { + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my %args = @_; + my $ws = $args{ws}; + if (defined($ws)) { + cmd "workspace $ws"; + } else { + $ws = fresh_workspace; + } + my $msg = ''; + if ($args{msg}) { + $msg = $args{msg} . ': '; + } + die unless $args{layout_before}; + die unless $args{layout_after}; + + kill_all_windows unless $args{dont_kill}; + my @windows = create_layout($args{layout_before}); + Test::More::subtest $msg . $args{layout_before} . ' -> ' . $args{layout_after} => sub { + $args{cb}->(\@windows) if $args{cb}; + verify_layout($args{layout_after}, $ws); + }; + + return @windows; +} + +sub create_layout { + my $layout = shift; + + my $focus; + my @windows = (); + my $r = ''; + my $depth = 0; + my %layout_counts = (H => 0, V => 0, S => 0, T => 0); + + foreach my $char (split('', $layout)) { + if ($char eq 'H') { + $r = $r . '{"layout": "splith",'; + $r = $r . '"marks": ["H' . ++$layout_counts{H} . '"],'; + } elsif ($char eq 'V') { + $r = $r . '{"layout": "splitv",'; + $r = $r . '"marks": ["V' . ++$layout_counts{V} . '"],'; + } elsif ($char eq 'S') { + $r = $r . '{"layout": "stacked",'; + $r = $r . '"marks": ["S' . ++$layout_counts{S} . '"],'; + } elsif ($char eq 'T') { + $r = $r . '{"layout": "tabbed",'; + $r = $r . '"marks": ["T' . ++$layout_counts{T} . '"],'; + } elsif ($char eq '[') { + $depth++; + $r = $r . '"nodes": ['; + } elsif ($char eq ']') { + # End of nodes array: delete trailing comma. + chop $r; + # When we are at depth 0 we need to split using newlines, making + # multiple "JSON texts". + $depth--; + $r = $r . ']}' . ($depth == 0 ? "\n" : ','); + } elsif ($char eq ' ') { + } elsif ($char eq '*') { + $focus = $windows[$#windows]; + } elsif ($char =~ /[[:alnum:]]/) { + push @windows, $char; + + $r = $r . '{"swallows": [{'; + $r = $r . '"class": "^' . "$char" . '$"'; + $r = $r . '}]},'; + } else { + die "Could not understand $char"; + } + } + + die "Invalid layout, depth is $depth > 0" unless $depth == 0; + + Test::More::diag($r); + my ($fh, $tmpfile) = tempfile("layout-XXXXXX", UNLINK => 1); + print $fh "$r\n"; + close($fh); + + my $return = cmd "append_layout $tmpfile"; + die 'Could not parse layout json file' unless $return->[0]->{success}; + + my @result_windows; + push @result_windows, open_window(wm_class => "$_", name => "$_") foreach @windows; + cmd '[class=' . $focus . '] focus' if $focus; + + return @result_windows; +} + +sub verify_layout { + my ($layout, $ws) = @_; + + my $nodes = get_ws_content($ws); + my %counters; + my $depth = 0; + my $node; + + foreach my $char (split('', $layout)) { + my $node_name; + my $node_layout; + if ($char eq 'H') { + $node_layout = 'splith'; + } elsif ($char eq 'V') { + $node_layout = 'splitv'; + } elsif ($char eq 'S') { + $node_layout = 'stacked'; + } elsif ($char eq 'T') { + $node_layout = 'tabbed'; + } elsif ($char eq '[') { + $depth++; + delete $counters{$depth}; + } elsif ($char eq ']') { + $depth--; + } elsif ($char eq ' ') { + } elsif ($char eq '*') { + $tester->is_eq($node->{focused}, 1, 'Correct node focused'); + } elsif ($char =~ /[[:alnum:]]/) { + $node_name = $char; + } else { + die "Could not understand $char"; + } + + if ($node_layout || $node_name) { + if (exists($counters{$depth})) { + $counters{$depth} = $counters{$depth} + 1; + } else { + $counters{$depth} = 0; + } + + $node = $nodes->[$counters{0}]; + for my $i (1 .. $depth) { + $node = $node->{nodes}->[$counters{$i}]; + } + + if ($node_layout) { + $tester->is_eq($node->{layout}, $node_layout, "Layouts match in depth $depth, node number " . $counters{$depth}); + } else { + $tester->is_eq($node->{name}, $node_name, "Names match in depth $depth, node number " . $counters{$depth}); + } + } + } +} + + =head1 AUTHOR diff --git a/testcases/t/133-size-hints.t b/testcases/t/133-size-hints.t index a9a43cb5..a16c5399 100644 --- a/testcases/t/133-size-hints.t +++ b/testcases/t/133-size-hints.t @@ -16,31 +16,97 @@ # # Checks if size hints are interpreted correctly. # -use i3test; +use i3test i3_config => < [0, 0, 100, 100], + before_map => sub { + my ($window) = @_; + my $aspect = X11::XCB::Sizehints::Aspect->new; + $aspect->min_num($min_num); + $aspect->min_den($min_den); + $aspect->max_num($max_num); + $aspect->max_den($max_den); + $window->hints->aspect($aspect); + }); +} -my $win = open_window({ dont_map => 1 }); -# XXX: we should check screen size. in screens with an AR of 2.0, -# this is not a good idea. -my $aspect = X11::XCB::Sizehints::Aspect->new; -$aspect->min_num(600); -$aspect->min_den(300); -$aspect->max_num(600); -$aspect->max_den(300); -$win->_create; -$win->map; -wait_for_map $win; -$win->hints->aspect($aspect); -$x->flush; +################################################################################ +# Test aspect ratio set exactly to 2.0 +################################################################################ -sync_with_i3; +fresh_workspace; +my $win = open_with_aspect(600, 300, 600, 300); my $rect = $win->rect; my $ar = $rect->width / $rect->height; -diag("Aspect ratio = $ar"); -ok(($ar > 1.90) && ($ar < 2.10), 'Aspect ratio about 2.0'); +cmp_float($ar, 2, 'Window set to floating with aspect ratio 2.0'); + +cmd 'resize set 100'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +cmp_float($ar, 2, 'Window resized with aspect ratio kept to 2.0'); + +cmd 'resize set 400 100'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +cmp_float($ar, 2, 'Window resized with aspect ratio kept to 2.0'); + +# Also check that it is possible to resize by height only +cmd 'resize set height 400'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +is($rect->height, 400, 'Window height is 400px'); +cmp_float($ar, 2, 'Window resized with aspect ratio kept to 2.0'); + +cmd 'resize grow height 10'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +is($rect->height, 410, 'Window grew by 10px'); +cmp_float($ar, 2, 'Window resized with aspect ratio kept to 2.0'); + +################################################################################ +# Test aspect ratio between 0.5 and 2.0 +################################################################################ + +fresh_workspace; +$win = open_with_aspect(1, 2, 2, 1); + +$rect = $win->rect; +$ar = $rect->width / $rect->height; +cmp_float($ar, 1, 'Window set to floating with aspect ratio 1.0'); + +cmd 'resize set 200'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +is($rect->width, 200, 'Window width is 200px'); +is($rect->height, 100, 'Window height stayed 100px'); +cmp_float($ar, 2, 'Window resized, aspect ratio changed to 2.0'); + +cmd 'resize set 100 200'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +is($rect->width, 100, 'Window width is 100px'); +is($rect->height, 200, 'Window height is 200px'); +cmp_float($ar, 0.5, 'Window resized, aspect ratio changed to 0.5'); + +cmd 'resize set 500'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +cmp_float($ar, 2, 'Window resized, aspect ratio changed to maximum 2.0'); + +cmd 'resize set 100 400'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +cmp_float($ar, 0.5, 'Window resized, aspect ratio changed to minimum 0.5'); done_testing; diff --git a/testcases/t/176-workspace-baf.t b/testcases/t/176-workspace-baf.t index 01906e46..133e00fc 100644 --- a/testcases/t/176-workspace-baf.t +++ b/testcases/t/176-workspace-baf.t @@ -163,6 +163,14 @@ cmd 'scratchpad show'; cmd 'workspace back_and_forth'; is(focused_ws, '6: baz', 'workspace 6 now focused'); +################################################################################ +# See if BAF is preserved after restart +################################################################################ + +cmd 'restart'; +cmd 'workspace back_and_forth'; +is(focused_ws, '5: foo', 'workspace 5 focused after restart'); + exit_gracefully($pid); done_testing; diff --git a/testcases/t/201-config-parser.t b/testcases/t/201-config-parser.t index c6ce22eb..65997bc7 100644 --- a/testcases/t/201-config-parser.t +++ b/testcases/t/201-config-parser.t @@ -400,12 +400,18 @@ $config = <<'EOT'; workspace 3 output VGA-1 workspace "4: output" output VGA-2 workspace bleh output LVDS1/I_1 +# See #3646 +workspace foo output a b c "a b c" EOT $expected = <<'EOT'; cfg_workspace(3, VGA-1) cfg_workspace(4: output, VGA-2) cfg_workspace(bleh, LVDS1/I_1) +cfg_workspace(foo, a) +cfg_workspace((null), b) +cfg_workspace((null), c) +cfg_workspace((null), a b c) EOT is(parser_calls($config), diff --git a/testcases/t/207-shmlog.t b/testcases/t/207-shmlog.t index c2b2ebaa..94f4bdea 100644 --- a/testcases/t/207-shmlog.t +++ b/testcases/t/207-shmlog.t @@ -31,7 +31,7 @@ run [ 'i3-dump-log' ], '>', \$stdout, '2>', \$stderr; -like($stderr, qr#^i3-dump-log: ERROR: i3 is running, but SHM logging is not enabled\.#, +like($stderr, qr#^i3-dump-log: i3 is running, but SHM logging is not enabled\.#, 'shm logging not enabled'); ################################################################################ @@ -73,7 +73,7 @@ run [ 'i3-dump-log' ], '>', \$stdout, '2>', \$stderr; -like($stderr, qr#^i3-dump-log: ERROR: i3 is running, but SHM logging is not enabled\.#, +like($stderr, qr#^i3-dump-log: i3 is running, but SHM logging is not enabled\.#, 'shm logging not enabled'); done_testing; diff --git a/testcases/t/219-ipc-window-focus.t b/testcases/t/219-ipc-window-focus.t index b1c8ba18..696fc7b2 100644 --- a/testcases/t/219-ipc-window-focus.t +++ b/testcases/t/219-ipc-window-focus.t @@ -14,14 +14,20 @@ # • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf # (unless you are already familiar with Perl) -use i3test; +use i3test i3_config => < 0); my $win0 = open_window; my $win1 = open_window; my $win2 = open_window; @@ -44,11 +50,52 @@ sub focus_subtest { is($events[0]->{container}->{name}, $name, "$name focused"); } +sub kill_subtest { + my ($cmd, $name) = @_; + + my $focus = AnyEvent->condvar; + + my @events = events_for( + sub { cmd $cmd }, + 'window'); + + is(scalar @events, 1, 'Received 1 event'); + is($events[0]->{change}, 'close', 'Close event received'); + is($events[0]->{container}->{name}, $name, "$name closed"); +} + 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; +subtest 'kill doesn\'t produce focus event', \&kill_subtest, '[id=' . $win1->id . '] kill', $win1->name; + +# See issue #3562. We need to switch to an existing workspace on the second +# output to trigger the bug. +cmd 'workspace X'; +subtest 'workspace focus', \&focus_subtest, "workspace $ws", $win2->name; + +sub scratchpad_subtest { + my ($cmd, $name) = @_; + + my $focus = AnyEvent->condvar; + + my @events = events_for( + sub { cmd $cmd }, + 'window'); + + is(scalar @events, 2, 'Received 2 events'); + is($events[0]->{change}, 'move', 'Move event received'); + is($events[0]->{container}->{nodes}->[0]->{name}, $name, "$name moved"); + is($events[1]->{change}, 'focus', 'Focus event received'); + is($events[1]->{container}->{name}, $name, "$name focused"); +} + +fresh_workspace; +my $win = open_window; +cmd 'move scratchpad'; +subtest 'scratchpad', \&scratchpad_subtest, '[id=' . $win->id . '] scratchpad show', $win->name; done_testing; diff --git a/testcases/t/235-check-config-no-x.t b/testcases/t/235-check-config-no-x.t index ef621142..dce70894 100644 --- a/testcases/t/235-check-config-no-x.t +++ b/testcases/t/235-check-config-no-x.t @@ -59,4 +59,33 @@ EOT is($ret, 0, "exit code == 0"); is($out, "", 'valid config file'); +################################################################################ +# 3: test duplicate keybindings +################################################################################ + +$cfg = < < 0; + +my $config = <{fullscreen_mode} != 0 } @{get_ws_content($ws)} +} + +sub cmp_floating_rect { + my ($window, $rect, $prefix) = @_; + sync_with_i3; + my ($absolute, $top) = $window->rect; + + is($absolute->{width}, $rect->[2], "$prefix: width matches"); + is($absolute->{height}, $rect->[3], "$prefix: height matches"); + + is($top->{x}, $rect->[0], "$prefix: x matches"); + is($top->{y}, $rect->[1], "$prefix: y matches"); +} + ############################################################################### # Invalid con_id should not crash i3 # See issue #2895. @@ -261,7 +284,6 @@ sync_with_i3; cmd '[con_mark=B] swap container with mark A'; -sync_with_i3; does_i3_live; $nodes = get_ws_content($ws1); @@ -502,6 +524,142 @@ cmp_float($nodes->[1]->{nodes}->[0]->{percent}, 0.75, 'A has 75% height'); kill_all_windows; +############################################################################### +# Swap floating windows in the same workspace. Check that they exchange rects. +############################################################################### +$ws = fresh_workspace; + +$A = open_floating_window(wm_class => 'mark_A', rect => $rect_A); +$B = open_floating_window(wm_class => 'mark_B', rect => $rect_B); + +cmd '[con_mark=B] swap container with mark A'; +cmp_floating_rect($A, $rect_B, 'A got B\'s rect'); +cmp_floating_rect($B, $rect_A, 'B got A\'s rect'); + +kill_all_windows; + +############################################################################### +# Swap a fullscreen floating and a normal floating window. +############################################################################### +$ws1 = fresh_workspace; +$A = open_floating_window(wm_class => 'mark_A', rect => $rect_A, fullscreen => 1); +$ws2 = fresh_workspace; +$B = open_floating_window(wm_class => 'mark_B', rect => $rect_B); + +cmd '[con_mark=B] swap container with mark A'; + +cmp_floating_rect($A, $rect_B, 'A got B\'s rect'); + +$nodes = get_ws($ws1); +$node = $nodes->{floating_nodes}->[0]->{nodes}->[0]; +is($node->{window}, $B->{id}, 'B is on the first workspace'); +is($node->{fullscreen_mode}, 1, 'B is now fullscreened'); + +$nodes = get_ws($ws2); +$node = $nodes->{floating_nodes}->[0]->{nodes}->[0]; +is($node->{window}, $A->{id}, 'A is on the second workspace'); +is($node->{fullscreen_mode}, 0, 'A is not fullscreened anymore'); + +kill_all_windows; + +############################################################################### +# Swap a floating window which is in a workspace that has another, regular +# window with a regular window in another workspace. A & B focused. +# +# Before: +# +-----------+ +# | F | +# | +---+ | +# | | A | | +# | +---+ | +# | | +# +-----------+ +# +# +-----------+ +# | | +# | | +# | B | +# | | +# | | +# +-----------+ +# +# After: Same as above but A <-> B. A & B focused. +############################################################################### +$ws1 = fresh_workspace; +open_window; +$A = open_floating_window(wm_class => 'mark_A', rect => $rect_A); +$ws2 = fresh_workspace; +$B = open_window(wm_class => 'mark_B'); +$expected_focus = get_focused($ws2); + +cmd '[con_mark=B] swap container with mark A'; + +$nodes = get_ws($ws1); +$node = $nodes->{floating_nodes}->[0]->{nodes}->[0]; +is($node->{window}, $B->{id}, 'B is floating on the first workspace'); +is(get_focused($ws1), $expected_focus, 'B is focused'); +cmp_floating_rect($B, $rect_A, 'B got A\'s rect'); + +$nodes = get_ws_content($ws2); +$node = $nodes->[0]; +is($node->{window}, $A->{id}, 'A is on the second workspace'); + +kill_all_windows; + +################################################################### +# Test that swapping a floating window maintains the correct +# floating order. +################################################################### +$ws = fresh_workspace; +$A = open_floating_window(wm_class => 'mark_A', rect => $rect_A); +$B = open_window(wm_class => 'mark_B'); +$F = open_floating_window; +$expected_focus = get_focused($ws); + +cmd '[con_mark=B] swap container with mark A'; + +$nodes = get_ws($ws); + +$node = $nodes->{floating_nodes}->[0]->{nodes}->[0]; +is($node->{window}, $B->{id}, 'B is floating, bottom'); +cmp_floating_rect($B, $rect_A, 'B got A\'s rect'); + +$node = $nodes->{floating_nodes}->[1]->{nodes}->[0]; +is($node->{window}, $F->{id}, 'F is floating, top'); +is(get_focused($ws), $expected_focus, 'F still focused'); + +$node = $nodes->{nodes}->[0]; +is($node->{window}, $A->{id}, 'A is tiling'); + +kill_all_windows; + +############################################################################### +# Swap a sticky, floating container A and a floating fullscreen container B. +# A should remain sticky and floating and should be fullscreened. +############################################################################### +$ws1 = fresh_workspace; +open_window; +$A = open_floating_window(wm_class => 'mark_A', rect => $rect_A); +$expected_focus = get_focused($ws1); +cmd 'sticky enable'; +$B = open_floating_window(wm_class => 'mark_B', rect => $rect_B); +cmd 'fullscreen enable'; + +cmd '[con_mark=B] swap container with mark A'; + +is(@{get_ws($ws1)->{floating_nodes}}, 2, '2 fullscreen containers on first workspace'); +is(get_focused($ws1), $expected_focus, 'A is focused'); + +$ws2 = fresh_workspace; +cmd 'fullscreen disable'; +cmp_floating_rect($A, $rect_B, 'A got B\'s rect'); +is(@{get_ws($ws2)->{floating_nodes}}, 1, 'only A in new workspace'); + +cmd "workspace $ws1"; # TODO: Why does rect check fails without switching workspaces? +cmp_floating_rect($B, $rect_A, 'B got A\'s rect'); + +kill_all_windows; + ############################################################################### # Swapping containers moves the urgency hint correctly. ############################################################################### @@ -527,6 +685,64 @@ is(get_ws($ws2)->{urgent}, 0, 'the second workspace is not marked urgent'); kill_all_windows; +############################################################################### +# Swapping an urgent container from another workspace with the focused +# container removes the urgency hint from both the container and the previous +# workspace. +############################################################################### + +$ws2 = fresh_workspace; +$B = open_window(wm_class => 'mark_B'); +$ws1 = fresh_workspace; +$A = open_window(wm_class => 'mark_A'); + +$B->add_hint('urgency'); +sync_with_i3; + +cmd '[con_mark=B] swap container with mark A'; + +@urgent = grep { $_->{urgent} } @{get_ws_content($ws1)}; +is(@urgent, 0, 'B is not marked urgent'); +is(get_ws($ws1)->{urgent}, 0, 'the first workspace is not marked urgent'); + +@urgent = grep { $_->{urgent} } @{get_ws_content($ws2)}; +is(@urgent, 0, 'A is not marked urgent'); +is(get_ws($ws2)->{urgent}, 0, 'the second workspace is not marked urgent'); + +kill_all_windows; + +exit_gracefully($pid); + +############################################################################### +# Test that swapping with workspace_layout doesn't crash i3. +# See issue #3280. +############################################################################### + +$config = <[0]->{window}, $B->{id}, 'B is on the left'); + +exit_gracefully($pid); + ############################################################################### done_testing; diff --git a/testcases/t/294-focus-order.t b/testcases/t/294-focus-order.t index 6b16540c..86adb819 100644 --- a/testcases/t/294-focus-order.t +++ b/testcases/t/294-focus-order.t @@ -210,6 +210,45 @@ cmd '[id=' . $windows[2]->id . '] move to workspace ' . $ws; cmd '[id=' . $windows[1]->id . '] move to workspace ' . $ws; confirm_focus('\'move to workspace\' focus order when moving containers from other workspace'); +###################################################################### +# Swapping sibling containers correctly swaps focus order. +###################################################################### + +sub swap_with_ids { + my ($idx1, $idx2) = @_; + cmd '[id=' . $windows[$idx1]->id . '] swap container with id ' . $windows[$idx2]->id; +} + +$ws = fresh_workspace; +$windows[2] = open_window; +$windows[0] = open_window; +$windows[3] = open_window; +$windows[1] = open_window; + +# If one of the swapees is focused we deliberately preserve its focus, switch +# workspaces to move focus away from the windows. +fresh_workspace; + +# 2 0 3 1 <- focus order in this direction +swap_with_ids(3, 1); +# 2 0 1 3 +swap_with_ids(2, 3); +# 3 0 1 2 +swap_with_ids(0, 2); +# 3 2 1 0 + +# Restore input focus for confirm_focus +cmd "workspace $ws"; + +# Also confirm swap worked +$ws = get_ws($ws); +for my $i (0 .. 3) { + my $node = $ws->{nodes}->[3 - $i]; + is($node->{window}, $windows[$i]->id, "window $i in correct position after swap"); +} + +confirm_focus('\'swap container with id\' focus order'); + ###################################################################### # Test focus order with floating and tiling windows. # See issue: 1975 diff --git a/testcases/t/294-update-ewmh-atoms.t b/testcases/t/294-update-ewmh-atoms.t index 047cc119..f13b1764 100644 --- a/testcases/t/294-update-ewmh-atoms.t +++ b/testcases/t/294-update-ewmh-atoms.t @@ -101,11 +101,19 @@ 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_current_desktop, 1, '_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'); +# Rename workspace to reorder them. +cmd 'rename workspace 0 to 5'; + +is(get_current_desktop, 0, '_NET_CURRENT_DESKTOP should be updated'); +is(get_num_of_desktops, 2, '_NET_NUMBER_OF_DESKTOPS should remain the same'); +my @actual_names = get_desktop_names; +my @expected_names = ('2', '5'); +is_deeply(\@actual_names, \@expected_names, '_NET_DESKTOP_NAMES should be updated'); done_testing; diff --git a/testcases/t/297-assign-workspace-to-output.t b/testcases/t/297-assign-workspace-to-output.t index a7b75be9..eb8f24b2 100644 --- a/testcases/t/297-assign-workspace-to-output.t +++ b/testcases/t/297-assign-workspace-to-output.t @@ -79,7 +79,9 @@ workspace 1 output fake-1 fake-2 workspace 2 output fake-3 fake-4 fake-0 fake-1 workspace 3 output these outputs do not exist but these do: fake-0 fake-3 workspace 4 output whitespace fake-0 -workspace special output doesnotexist1 doesnotexist2 doesnotexist3 +workspace foo output doesnotexist1 doesnotexist2 doesnotexist3 +workspace bar output doesnotexist +workspace bar output fake-0 EOT $pid = launch_with_config($config); @@ -91,8 +93,11 @@ do_test('4', 'fake-0', 'Excessive whitespace is ok'); do_test('5', 'fake-1', 'Numbered initialization for fake-1'); do_test('6', 'fake-2', 'Numbered initialization for fake-2'); -cmd 'focus output fake-0, workspace special'; -check_output('special', 'fake-0', 'Workspace with only non-existing assigned outputs opened in current output.'); +cmd 'focus output fake-0, workspace foo'; +check_output('foo', 'fake-0', 'Workspace with only non-existing assigned outputs opened in current output'); + +cmd 'focus output fake-0, workspace bar'; +check_output('bar', 'fake-0', 'Second workspace assignment line ignored'); # Moving assigned workspaces. cmd 'workspace 2, move workspace to output left'; diff --git a/testcases/t/301-shape.t b/testcases/t/301-shape.t new file mode 100644 index 00000000..ac0ec5a7 --- /dev/null +++ b/testcases/t/301-shape.t @@ -0,0 +1,114 @@ +#!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) +# +# Test shape support. +# Ticket: #2742 +use i3test; +use ExtUtils::PkgConfig; + +my %sn_config; +BEGIN { + %sn_config = ExtUtils::PkgConfig->find('xcb-shape'); +} + +use Inline C => Config => LIBS => $sn_config{libs}, CCFLAGS => $sn_config{cflags}; +use Inline C => <<'END_OF_C_CODE'; +#include + +static xcb_connection_t *conn; + +void init_ctx(void *connptr) { + conn = (xcb_connection_t*)connptr; +} + +/* + * Set the shape for the window consisting of the following zones: + * + * +---+---+ + * | A | B | + * +---+---+ + * | C | + * +-------+ + * + * - Zone A is completly opaque. + * - Zone B is clickable through (input shape). + * - Zone C is completly transparent (bounding shape). + */ +void set_shape(long window_id) { + xcb_rectangle_t bounding_rectangle = { 0, 0, 100, 50 }; + xcb_shape_rectangles(conn, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, + XCB_CLIP_ORDERING_UNSORTED, window_id, + 0, 0, 1, &bounding_rectangle); + xcb_rectangle_t input_rectangle = { 0, 0, 50, 50 }; + xcb_shape_rectangles(conn, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, + XCB_CLIP_ORDERING_UNSORTED, window_id, + 0, 0, 1, &input_rectangle); + xcb_flush(conn); +} +END_OF_C_CODE + +init_ctx($x->get_xcb_conn()); + +my ($ws, $win1, $win1_focus, $win2, $win2_focus); + +################################################################################ +# Case 1: make floating window, then set shape +################################################################################ + +$ws = fresh_workspace; + +$win1 = open_floating_window(rect => [0, 0, 100, 100], background_color => '#ff0000'); +$win1_focus = get_focused($ws); + +$win2 = open_floating_window(rect => [0, 0, 100, 100], background_color => '#00ff00'); +$win2_focus = get_focused($ws); +set_shape($win2->id); + +$win1->warp_pointer(75, 25); +sync_with_i3; +is(get_focused($ws), $win1_focus, 'focus switched to the underlying window'); + +$win1->warp_pointer(25, 25); +sync_with_i3; +is(get_focused($ws), $win2_focus, 'focus switched to the top window'); + +kill_all_windows; + +################################################################################ +# Case 2: set shape first, then make window floating +################################################################################ + +$ws = fresh_workspace; + +$win1 = open_window(rect => [0, 0, 100, 100], background_color => '#ff0000'); +$win1_focus = get_focused($ws); +cmd 'floating toggle'; + +$win2 = open_window(rect => [0, 0, 100, 100], background_color => '#00ff00'); +$win2_focus = get_focused($ws); +set_shape($win2->id); +cmd 'floating toggle'; +sync_with_i3; + +$win1->warp_pointer(75, 25); +sync_with_i3; +is(get_focused($ws), $win1_focus, 'focus switched to the underlying window'); + +$win1->warp_pointer(25, 25); +sync_with_i3; +is(get_focused($ws), $win2_focus, 'focus switched to the top window'); + +done_testing; diff --git a/testcases/t/302-tree.t b/testcases/t/302-tree.t new file mode 100644 index 00000000..a2551a3e --- /dev/null +++ b/testcases/t/302-tree.t @@ -0,0 +1,94 @@ +#!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) +# +# Contains various tests that use the cmp_tree subroutine. +# Ticket: #3503 +use i3test; + +sub sanity_check { + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my ($layout, $focus_idx) = @_; + my @windows = cmp_tree( + msg => 'Sanity check', + layout_before => $layout, + layout_after => $layout); + is($x->input_focus, $windows[$focus_idx]->id, 'Correct window focused') if $focus_idx >= 0; +} + +sanity_check('H[ V[ a* V[ b c ] d ] e ]', 0); +sanity_check('H[ a b c d* ]', 3); +sanity_check('V[a b] V[c d*]', 3); +sanity_check('T[a b] S[c*]', 2); + +cmp_tree( + msg => 'Simple focus test', + layout_before => 'H[a b] V[c* d]', + layout_after => 'H[a* b] V[c d]', + cb => sub { + cmd '[class=a] focus'; + }); + +cmp_tree( + msg => 'Simple move test', + layout_before => 'H[a b] V[c* d]', + layout_after => 'H[a b] V[d c*]', + cb => sub { + cmd 'move down'; + }); + +cmp_tree( + msg => 'Move from horizontal to vertical', + layout_before => 'H[a b] V[c d*]', + layout_after => 'H[b] V[c d a*]', + cb => sub { + cmd '[class=a] focus'; + cmd 'move right, move right'; + }); + +cmp_tree( + msg => 'Move unfocused non-leaf container', + layout_before => 'S[a b] V[c d* T[e f g]]', + layout_after => 'S[a T[e f g] b] V[c d*]', + cb => sub { + cmd '[con_mark=T1] move up, move up, move left, move up'; + }); + +cmp_tree( + msg => 'Simple swap test', + layout_before => 'H[a b] V[c d*]', + layout_after => 'H[a d*] V[c b]', + cb => sub { + cmd '[class=b] swap with id ' . $_[0][3]->{id}; + }); + +cmp_tree( + msg => 'Swap non-leaf containers', + layout_before => 'S[a b] V[c d*]', + layout_after => 'V[c d*] S[a b]', + cb => sub { + cmd '[con_mark=S1] swap with mark V1'; + }); + +cmp_tree( + msg => 'Swap nested non-leaf containers', + layout_before => 'S[a b] V[c d* T[e f g]]', + layout_after => 'T[e f g] V[c d* S[a b]]', + cb => sub { + cmd '[con_mark=S1] swap with mark T1'; + }); + +done_testing; diff --git a/testcases/t/304-ipc-workspace-init.t b/testcases/t/304-ipc-workspace-init.t new file mode 100644 index 00000000..0eb838dc --- /dev/null +++ b/testcases/t/304-ipc-workspace-init.t @@ -0,0 +1,69 @@ +#!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) +# +# Test that the workspace init event is correctly sent. +# Ticket: #3631 +# Bug still in: 4.16-85-g2d6e09a6 + +use i3test i3_config => <{change} eq 'init' } @events; + my $len = scalar @init; + is($len, $num_events, "Received $num_events workspace::init event"); + $num_events = $len if $len < $num_events; + for my $idx (0 .. $num_events - 1) { + my $name = shift; + my $output = shift; + is($init[$idx]->{current}->{name}, $name, "workspace name $name matches"); + is($init[$idx]->{current}->{output}, $output, "workspace output $output matches"); + } +} + +subtest 'focus outputs', \&workspace_init_subtest, 'focus output fake-1, focus output fake-0', 0; +subtest 'new workspaces', \&workspace_init_subtest, + 'workspace a, workspace b, workspace a, workspace a', 3, 'a', + 'fake-0', 'b', 'fake-0', 'a', 'fake-0'; +open_window; # Prevent workspace "a" from being deleted. +subtest 'return on existing workspace', \&workspace_init_subtest, + 'workspace a, workspace b, workspace a', 1, 'b', 'fake-0'; +subtest 'assigned workspace is already open', \&workspace_init_subtest, + 'workspace X, workspace b, workspace a', 1, 'b', 'fake-1'; +subtest 'assigned workspace was deleted and now is initialized again', \&workspace_init_subtest, + 'workspace X, workspace b, workspace a', 2, 'X', 'fake-1', 'b', 'fake-1'; +subtest 'move workspace to output', \&workspace_init_subtest, + 'move workspace to output fake-1, move workspace to output fake-0', 2, '1', 'fake-0', 'X', + 'fake-1'; +subtest 'move window to workspace', \&workspace_init_subtest, 'move to workspace b', 1, 'b', + 'fake-0'; +subtest 'back_and_forth', \&workspace_init_subtest, + 'workspace b, workspace back_and_forth, workspace b', 1, + 'a', 'fake-0'; +subtest 'move window to workspace back_and_forth', \&workspace_init_subtest, + 'move window to workspace back_and_forth', 1, 'a', 'fake-0'; + +done_testing; diff --git a/testcases/t/305-restart-reply.t b/testcases/t/305-restart-reply.t new file mode 100644 index 00000000..0493d4ff --- /dev/null +++ b/testcases/t/305-restart-reply.t @@ -0,0 +1,25 @@ +#!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 restart produces a reply. +# Ticket: #3565 +# Bug still in: 4.16-143-gca82f958 +use i3test; + +my $reply = cmd('restart'); +ok($reply->[0]->{success}, 'restart reply received'); + +done_testing; diff --git a/testcases/t/502-focus-output.t b/testcases/t/502-focus-output.t index 01d8db33..118aba16 100644 --- a/testcases/t/502-focus-output.t +++ b/testcases/t/502-focus-output.t @@ -86,4 +86,33 @@ is(focused_output, 'fake-1', 'focus on second output'); cmd 'focus output fake-0'; is(focused_output, 'fake-0', 'focus on first output'); +################################################################################ +# use 'focus output' with command criteria and verify that i3 does not crash +# when they don't match any window +################################################################################ + +is(focused_output, 'fake-0', 'focus on first output'); + +cmd '[con_mark=doesnotexist] focus output right'; +does_i3_live; +is(focused_output, 'fake-0', 'focus remained on first output'); + +################################################################################ +# use 'focus output' with command criteria and verify that focus gets changed +# appropriately +################################################################################ + +is(focused_output, 'fake-0', 'focus on first output'); + +my $window = open_window; + +cmd 'focus output right'; +is(focused_output, 'fake-1', 'focus on second output'); + +cmd '[id= . ' . $window->id . '] focus output right'; +is(focused_output, 'fake-1', 'focus on second output after command with criteria'); + +cmd 'focus output right'; +is(focused_output, 'fake-0', 'focus on first output after command without criteria'); + done_testing; diff --git a/testcases/t/533-randr15.t b/testcases/t/533-randr15.t index 9bbdd74b..51d1c9f6 100644 --- a/testcases/t/533-randr15.t +++ b/testcases/t/533-randr15.t @@ -130,10 +130,16 @@ $tree = i3->get_tree->recv; is_deeply(\@outputs, [ '__i3', 'default' ], 'outputs are __i3 and default'); SKIP: { - skip 'xrandr --setmonitor failed (xrandr too old?)', 1 unless - system(q|xrandr --setmonitor up2414q 3840/527x2160/296+1280+0 none|) == 0; + my @events = events_for( + sub { + skip 'xrandr --setmonitor failed (xrandr too old?)', 1 + unless system(q|xrandr --setmonitor up2414q 3840/527x2160/296+1280+0 none|) == 0; + }, + "workspace"); - sync_with_i3; + my @init = grep { $_->{change} eq 'init' } @events; + is(scalar @init, 1, 'Received 1 workspace::init event'); + is($init[0]->{current}->{output}, 'up2414q', 'Workspace initialized in up2414q'); $tree = i3->get_tree->recv; @outputs = map { $_->{name} } @{$tree->{nodes}}; diff --git a/travis/bintray-autobuild-ubuntu.json b/travis/bintray-autobuild-ubuntu.json index 948f3dc4..37f2f180 100644 --- a/travis/bintray-autobuild-ubuntu.json +++ b/travis/bintray-autobuild-ubuntu.json @@ -15,7 +15,7 @@ { "includePattern": "build/deb/ubuntu-amd64/(.*\\.deb)$", "matrixParams": { - "deb_distribution": "xenial", + "deb_distribution": "bionic", "deb_component": "main", "deb_architecture": "amd64" }, @@ -24,7 +24,7 @@ { "includePattern": "build/deb/ubuntu-i386/(.*\\.deb)$", "matrixParams": { - "deb_distribution": "xenial", + "deb_distribution": "bionic", "deb_component": "main", "deb_architecture": "i386" }, diff --git a/travis/check-formatting.sh b/travis/check-formatting.sh index d232339c..7aa2f565 100755 --- a/travis/check-formatting.sh +++ b/travis/check-formatting.sh @@ -3,4 +3,4 @@ set -e set -x -clang-format-4.0 -i $(find . -name "*.[ch]" | tr '\n' ' ') && git diff --exit-code || (echo 'Code was not formatted using clang-format!'; false) +clang-format-6.0 -i $(find . -name "*.[ch]" | tr '\n' ' ') && git diff --exit-code || (echo 'Code was not formatted using clang-format!'; false) diff --git a/travis/docker-build-and-push.sh b/travis/docker-build-and-push.sh index 9b654a84..686b81b0 100755 --- a/travis/docker-build-and-push.sh +++ b/travis/docker-build-and-push.sh @@ -15,6 +15,6 @@ docker build --pull --no-cache --rm -t=${BASENAME} -f ${DOCKERFILE} . # the login+push step when the variable isn’t set. if [ -n "${DOCKER_PASS}" ] then - docker login -e ${DOCKER_EMAIL} -u ${DOCKER_USER} -p ${DOCKER_PASS} + docker login -u ${DOCKER_USER} -p ${DOCKER_PASS} docker push ${BASENAME} fi diff --git a/travis/travis-base-386.Dockerfile b/travis/travis-base-386.Dockerfile index e6ad51f8..355c2588 100644 --- a/travis/travis-base-386.Dockerfile +++ b/travis/travis-base-386.Dockerfile @@ -13,12 +13,12 @@ RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry # (3608 kB/s)). Hence, let’s stick with httpredir.debian.org (default) for now. # Install mk-build-deps (for installing the i3 build dependencies), -# clang and clang-format-4.0 (for checking formatting and building with clang), +# clang and clang-format-6.0 (for checking formatting and building with clang), # lintian (for checking spelling errors), RUN linux32 apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ dpkg-dev devscripts git equivs \ - clang clang-format-4.0 \ + clang clang-format-6.0 \ lintian && \ rm -rf /var/lib/apt/lists/* diff --git a/travis/travis-base-ubuntu-386.Dockerfile b/travis/travis-base-ubuntu-386.Dockerfile index 82e5ca29..d52df4b8 100644 --- a/travis/travis-base-ubuntu-386.Dockerfile +++ b/travis/travis-base-ubuntu-386.Dockerfile @@ -1,7 +1,7 @@ # vim:ft=Dockerfile # Same as travis-base.Dockerfile, but without the test suite dependencies since # we only build Debian packages on Ubuntu i386, we don’t run the tests. -FROM i386/ubuntu:xenial +FROM i386/ubuntu:bionic RUN echo force-unsafe-io > /etc/dpkg/dpkg.cfg.d/docker-apt-speedup # Paper over occasional network flakiness of some mirrors. @@ -13,21 +13,17 @@ RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry # (3608 kB/s)). Hence, let’s stick with httpredir.debian.org (default) for now. # Install mk-build-deps (for installing the i3 build dependencies), -# clang and clang-format-4.0 (for checking formatting and building with clang), +# clang and clang-format-6.0 (for checking formatting and building with clang), # lintian (for checking spelling errors), RUN linux32 apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ dpkg-dev devscripts git equivs \ - clang clang-format-4.0 \ + clang clang-format-6.0 \ lintian && \ rm -rf /var/lib/apt/lists/* # Install i3 build dependencies. COPY debian/control /usr/src/i3-debian-packaging/control -RUN echo 'deb http://dl.bintray.com/i3/i3-autobuild-ubuntu xenial main' > /etc/apt/sources.list.d/i3-autobuild.list && \ - linux32 apt-get update && \ - linux32 apt-get --allow-unauthenticated install i3-autobuild-keyring && \ - rm -f /var/lib/apt/lists/dl.bintray.com_i3_i3-autobuild-ubuntu_* && \ - linux32 apt-get update && \ +RUN linux32 apt-get update && \ DEBIAN_FRONTEND=noninteractive mk-build-deps --install --remove --tool 'apt-get --no-install-recommends -y' /usr/src/i3-debian-packaging/control && \ rm -rf /var/lib/apt/lists/* diff --git a/travis/travis-base-ubuntu.Dockerfile b/travis/travis-base-ubuntu.Dockerfile index 05066ec8..d1057a39 100644 --- a/travis/travis-base-ubuntu.Dockerfile +++ b/travis/travis-base-ubuntu.Dockerfile @@ -1,7 +1,7 @@ # vim:ft=Dockerfile # Same as travis-base.Dockerfile, but without the test suite dependencies since # we only build Debian packages on Ubuntu, we don’t run the tests. -FROM ubuntu:xenial +FROM ubuntu:bionic RUN echo force-unsafe-io > /etc/dpkg/dpkg.cfg.d/docker-apt-speedup # Paper over occasional network flakiness of some mirrors. @@ -13,22 +13,18 @@ RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry # (3608 kB/s)). Hence, let’s stick with httpredir.debian.org (default) for now. # Install mk-build-deps (for installing the i3 build dependencies), -# clang and clang-format-4.0 (for checking formatting and building with clang), +# clang and clang-format-6.0 (for checking formatting and building with clang), # lintian (for checking spelling errors), # test suite dependencies (for running tests) RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ dpkg-dev devscripts git equivs \ - clang clang-format-4.0 \ + clang clang-format-6.0 \ lintian && \ rm -rf /var/lib/apt/lists/* # Install i3 build dependencies. COPY debian/control /usr/src/i3-debian-packaging/control -RUN echo 'deb http://dl.bintray.com/i3/i3-autobuild-ubuntu xenial main' > /etc/apt/sources.list.d/i3-autobuild.list && \ - apt-get update && \ - apt-get --allow-unauthenticated install i3-autobuild-keyring && \ - rm -f /var/lib/apt/lists/dl.bintray.com_i3_i3-autobuild-ubuntu_* && \ - apt-get update && \ +RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive mk-build-deps --install --remove --tool 'apt-get --no-install-recommends -y' /usr/src/i3-debian-packaging/control && \ rm -rf /var/lib/apt/lists/* diff --git a/travis/travis-base.Dockerfile b/travis/travis-base.Dockerfile index 907002e8..def7598d 100644 --- a/travis/travis-base.Dockerfile +++ b/travis/travis-base.Dockerfile @@ -11,13 +11,13 @@ RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry # (3608 kB/s)). Hence, let’s stick with httpredir.debian.org (default) for now. # Install mk-build-deps (for installing the i3 build dependencies), -# clang and clang-format-4.0 (for checking formatting and building with clang), +# clang and clang-format-6.0 (for checking formatting and building with clang), # lintian (for checking spelling errors), # test suite dependencies (for running tests) RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ dpkg-dev devscripts git equivs \ - clang clang-format-4.0 \ + clang clang-format-6.0 \ lintian \ libmodule-install-perl libanyevent-perl libextutils-pkgconfig-perl xcb-proto cpanminus xvfb xserver-xephyr xauth libinline-perl libinline-c-perl libxml-simple-perl libmouse-perl libmousex-nativetraits-perl libextutils-depends-perl perl libtest-deep-perl libtest-exception-perl libxml-parser-perl libtest-simple-perl libtest-fatal-perl libdata-dump-perl libtest-differences-perl libxml-tokeparser-perl libipc-run-perl libxcb-xtest0-dev libx11-xcb-perl libjson-xs-perl x11-xserver-utils && \ rm -rf /var/lib/apt/lists/*