diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 40fd839..f033a48 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -4,7 +4,7 @@
Note that bug reports and feature requests for related projects should be filed in the corresponding repositories for [i3status](https://github.com/i3/i3status) and [i3lock](https://github.com/i3/i3lock).
-## i3 bug reports and feature requests
+## i3 bug reports
1. Read the [debugging instructions](https://i3wm.org/docs/debugging.html).
2. Make sure you include a link to your logfile in your report (section 3).
@@ -18,6 +18,20 @@
encountered the issue you are about to report while using a compositor,
please try reproducing it without a compositor.
+## i3 feature requests
+
+1. Read the [project goals](https://i3wm.org) on the website and make sure that
+ they are compatible with the feature you want to suggest.
+2. We are generally happy with the current feature set of i3 and instead focus
+ on maintenance such as stability and fixing bugs. New features will rarely
+ be considered if they require additional configuration and/or commands, or
+ if they add significant complexity (either through the exposed configuration
+ or mental complexity) to the project.
+3. Explain in detail what problem the feature addresses and why existing
+ features fall short.
+4. Consider whether the feature could instead be implemented using the
+ [IPC](https://i3wm.org/docs/ipc.html) or other external tooling.
+
## Pull requests
* Before sending a pull request for new features, please check with us that the
@@ -27,11 +41,14 @@
* Use `clang-format` to format your code.
* Run the [testsuite](https://i3wm.org/docs/testsuite.html)
* If your changes should be reported on the next release's changelog, also
- update the [RELEASE-notes-next](../RELEASE-notes-next) file in the root
- folder. Example of changes that should be reported are bug fixes present in
- the latest stable version of i3 and new enhancements. Example of changes that
- should not be reported are minor code improvements, documentation, regression
- and fixes for bugs that were introduced in the `next` branch.
+ add a small single-line file starting with a number (see examples) containing
+ a short explanation of your change either in the
+ [changes](../release-notes/changes) or the
+ [bugfixes](../release-notes/bugfixes/) folder. Example of changes that should
+ be reported are bug fixes present in the latest stable version of i3 and new
+ enhancements. Example of changes that should not be reported are minor code
+ improvements, documentation, regression and fixes for bugs that were
+ introduced in the `next` branch.
## Finding something to do
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..0225148
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,4 @@
+contact_links:
+ - name: Ask a question or request support for using i3
+ url: https://github.com/i3/i3/discussions/new
+ about: Ask a question or request support for using i3
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index e1c169a..c41d1ca 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -8,7 +8,7 @@
-->
## I'm submitting a…
-
+
[ ] Bug
[x] Feature Request
@@ -28,6 +28,15 @@
e.g., »The window left next to the current window should be focused.«
-->
+## Impact
+
+
+[ ] This feature requires new configuration and/or commands
+
+
## Environment
## I'm submitting a…
-
+
[ ] Bug
[ ] Feature Request
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..22c6132
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,102 @@
+name: GitHub Actions
+
+on:
+ push:
+ branches: [ gaps-next, gaps, actions ]
+ pull_request:
+ branches: [ gaps-next ]
+
+jobs:
+ build:
+ name: build and test
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ compiler: [gcc, clang]
+ env:
+ CC: ${{ matrix.compiler }}
+ DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
+ DOCKER_EMAIL: ${{ secrets.DOCKER_EMAIL }}
+ DOCKER_USER: ${{ secrets.DOCKER_USER }}
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
+ BALTO_TOKEN: ${{ secrets.BALTO_TOKEN }}
+
+ steps:
+ - uses: actions/checkout@v2
+ - run: git fetch --prune --unshallow
+ - name: construct container name
+ run: |
+ echo "BASENAME=i3wm/travis-base:$(date +'%Y-%m')-$(./travis/ha.sh travis/travis-base.Dockerfile)" >> $GITHUB_ENV
+ echo "BASENAME_386=i3wm/travis-base-386:$(date +'%Y-%m')-$(./travis/ha.sh travis/travis-base-386.Dockerfile)" >> $GITHUB_ENV
+ echo "BASENAME_UBUNTU=i3wm/travis-base-ubuntu:$(date +'%Y-%m')-$(./travis/ha.sh travis/travis-base-ubuntu.Dockerfile)" >> $GITHUB_ENV
+ echo "BASENAME_UBUNTU_386=i3wm/travis-base-ubuntu-386:$(date +'%Y-%m')-$(./travis/ha.sh travis/travis-base-ubuntu-386.Dockerfile)" >> $GITHUB_ENV
+ - name: fetch or build Docker container
+ run: |
+ docker pull ${{ env.BASENAME }} || ./travis/docker-build-and-push.sh ${{ env.BASENAME }} travis/travis-base.Dockerfile
+ - name: fetch or build extra Docker containers
+ run: |
+ echo "::group::Ubuntu amd64"
+ ./travis/skip-pkg.sh || docker pull ${{ env.BASENAME_UBUNTU }} || ./travis/docker-build-and-push.sh ${{ env.BASENAME_UBUNTU }} travis/travis-base-ubuntu.Dockerfile
+ echo "::endgroup::"
+ echo "::group::Debian i386"
+ ./travis/skip-pkg.sh || docker pull ${{ env.BASENAME_386 }} || ./travis/docker-build-and-push.sh ${{ env.BASENAME_386 }} travis/travis-base-386.Dockerfile
+ echo "::endgroup::"
+ echo "::group::Ubuntu i386"
+ ./travis/skip-pkg.sh || docker pull ${{ env.BASENAME_UBUNTU_386 }} || ./travis/docker-build-and-push.sh ${{ env.BASENAME_UBUNTU_386 }} travis/travis-base-ubuntu-386.Dockerfile
+ echo "::endgroup::"
+ - name: build i3
+ run: |
+ docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 -e CC ${{ env.BASENAME }} /bin/sh -c 'rm -rf build; mkdir -p build && cd build && CFLAGS="-Wformat -Wformat-security -Wextra -Wno-unused-parameter -Wstrict-prototypes -Wmissing-prototypes -Werror -fno-common" meson .. -Ddocs=true -Dmans=true -Db_sanitize=address && ninja -v'
+ - name: check spelling
+ run: |
+ docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${{ env.BASENAME }} ./travis/check-spelling.pl
+ - name: run i3 tests
+ run: |
+ docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 -e CC ${{ env.BASENAME }} ./travis/run-tests.sh
+ - name: Archive test logs
+ uses: actions/upload-artifact@v2
+ with:
+ name: test-logs
+ path: build/testsuite-*
+ if: ${{ failure() }}
+ - name: build dist tarball
+ run: |
+ docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 -e CC ${{ env.BASENAME }} /bin/sh -c 'rm -rf distbuild; mkdir distbuild && cd distbuild && meson .. -Ddocs=true -Dmans=true && ninja -v dist'
+ - name: build Debian packages
+ run: |
+ echo "::group::Debian amd64"
+ ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${{ env.BASENAME }} ./travis/debian-build.sh deb/debian-amd64/DIST
+ echo "::endgroup::"
+ echo "::group::Ubuntu amd64"
+ ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${{ env.BASENAME_UBUNTU }} ./travis/debian-build.sh deb/ubuntu-amd64/DIST
+ echo "::endgroup::"
+ echo "::group::Debian i386"
+ ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${{ env.BASENAME_386 }} linux32 ./travis/debian-build.sh deb/debian-i386/DIST
+ echo "::endgroup::"
+ echo "::group::Ubuntu i386"
+ ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${{ env.BASENAME_UBUNTU_386 }} linux32 ./travis/debian-build.sh deb/ubuntu-i386/DIST
+ echo "::endgroup::"
+ - name: push Debian packages to balto
+ run: |
+ ./travis/skip-pkg.sh || travis/push-balto.sh
+ - name: build docs
+ run: |
+ ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${{ env.BASENAME }} ./travis/docs.sh
+ - name: push docs to GitHub pages
+ run: |
+ ./travis/skip-pkg.sh || travis/deploy-github-pages.sh
+ formatting:
+ name: Check formatting
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: check & print release notes
+ run: ./release-notes/generator.pl
+ - name: Install dependencies
+ run: |
+ sudo apt-get install -y clang-format-10
+ - name: Check formatting
+ run: clang-format-10 --dry-run --Werror $(git ls-files '*.c' '*.h')
+ - name: Verify safe wrapper functions are used
+ run: ./travis/check-safe-wrappers.sh
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 9935cb6..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,71 +0,0 @@
-dist: trusty
-services:
- - docker
-language: c
-compiler:
- - gcc
- - clang
-addons:
- apt:
- packages:
- # For https support in HTTP::Tiny.
- - libio-socket-ssl-perl
-env:
- global:
- - BASENAME="i3wm/travis-base:$(date +'%Y-%m')-$(./travis/ha.sh travis/travis-base.Dockerfile)"
- - BASENAME_386="i3wm/travis-base-386:$(date +'%Y-%m')-$(./travis/ha.sh travis/travis-base-386.Dockerfile)"
- - BASENAME_UBUNTU="i3wm/travis-base-ubuntu:$(date +'%Y-%m')-$(./travis/ha.sh travis/travis-base-ubuntu.Dockerfile)"
- - BASENAME_UBUNTU_386="i3wm/travis-base-ubuntu-386:$(date +'%Y-%m')-$(./travis/ha.sh travis/travis-base-ubuntu-386.Dockerfile)"
- - secure: "B5IICA8MPx/FKaB50rTPqL8P1NU+Q0yuWl+lElL4+a9xSyLikfm3NzUPHoVwx8lNw2AVK6br7p0OmF7vMFjqAgrgc1cajTtEae5uFRKNUrWLpXM046YgNEYLLIHsQOjInxE+S4O6EFVzsUqsu8aeo2Xhq4sm4iUocG7e5isYgYo=" # DOCKER_PASS
- - secure: "EIvrq8PG7lRjidppG0RCv4F0X4GP3DT9F5+ixVuGPfhK/hZT3jYC2AVY9G+NnUcXVwQEpW92rlqpftQ/qZ13FoyWokC8ZyoyD06fr5FPCfoFF3OczZwAJzZYkObI/hE9+/hXcylx/Os6N4INd2My1ntGk3JPsWL9riopod5EjSg=" # DOCKER_EMAIL
- - secure: "hvhBunS4xXTgnIOsk/BPT7I7FrJhvVwCSt5PfxxvMqNaztOJI9BuK7ZrZ5Cy38KyHwlh3VHAH5AaCygJcPauoSQCV3bpnlbaWn3ruq2F0Q697Q5uNf73liXzyUqb9/Zvfvge4y4WWOhP5tVz1C6ZBe/NfhU7pqKLMA+6ads+99c=" # DOCKER_USER
- - secure: "uJuuefmnJUuEH15ZD8xQilibx7EeBvMHBLoIZ8bgGHeleEImBD0XbD1ypvhYJKpviOmw5BkZmc9bVO8DGDEHYbSlIa2xDlF6vGrwgCEaxcMIhOAhv+dW9C/maJVieLOEPM01/fK2qdKESZaLvlopkWmxZwDyMObI9L7AMW9zQD8=" # BINTRAY_USER
- - secure: "L3aPSNLySPXtWCW+xf8h/AAdquwNgxyTQpYOwexJmTPav82Qx8uQlp1yJkUmt+a+FLZDFfQeMivaHq0311RvuQVmkAJx49DjaddrwqOJut2UPsoVDn1WeuAcSHIXOq/0H+zgFMr/PGY0HXIsw1mTMhgheGJNqg09BvYWROCEAcA=" # BINTRAY_KEY
- - secure: "sBMVn4C/WRWgoAytEFGx4CC5O55Q63h02AcuBnb1jXcBm0RenoBpzUPtxSseJwDPUA1o/UkuEDDjm3PosT5NF+dvED01VDFMsPVE11K0u6+avYy3jYXqyUEDW3G2o6Wo/2aqNjmd++8jskBdS9+Cx9gaFbgxfzSp0Yfu3oJm/4c=" # GH_TOKEN
-install:
- - if [ -a .git/shallow ]; then git fetch --unshallow; fi
- - docker pull ${BASENAME} || ./travis/docker-build-and-push.sh ${BASENAME} travis/travis-base.Dockerfile
- - ./travis/skip-pkg.sh || docker pull ${BASENAME_UBUNTU} || ./travis/docker-build-and-push.sh ${BASENAME_UBUNTU} travis/travis-base-ubuntu.Dockerfile
- - ./travis/skip-pkg.sh || docker pull ${BASENAME_386} || ./travis/docker-build-and-push.sh ${BASENAME_386} travis/travis-base-386.Dockerfile
- - ./travis/skip-pkg.sh || docker pull ${BASENAME_UBUNTU_386} || ./travis/docker-build-and-push.sh ${BASENAME_UBUNTU_386} travis/travis-base-ubuntu-386.Dockerfile
-script:
- - docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME} ./travis/check-safe-wrappers.sh
- - docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME} ./travis/check-formatting.sh
- - docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 -e CC ${BASENAME} /bin/sh -c 'rm -rf build; mkdir -p build && cd build && CFLAGS="-Wformat -Wformat-security -Wextra -Wno-unused-parameter -Wstrict-prototypes -Wmissing-prototypes -Werror -fno-common" meson .. -Ddocs=true -Dmans=true -Db_sanitize=address && ninja -v'
- - docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME} ./travis/check-spelling.pl
- - docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 -e CC ${BASENAME} ./travis/run-tests.sh
- - docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 -e CC ${BASENAME} /bin/sh -c 'rm -rf distbuild; mkdir distbuild && cd distbuild && meson .. -Ddocs=true -Dmans=true && ninja -v dist'
- - ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME} ./travis/debian-build.sh deb/debian-amd64/DIST
- - ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME_UBUNTU} ./travis/debian-build.sh deb/ubuntu-amd64/DIST
- - ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME_386} linux32 ./travis/debian-build.sh deb/debian-i386/DIST
- - ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME_UBUNTU_386} linux32 ./travis/debian-build.sh deb/ubuntu-i386/DIST
- - ./travis/skip-pkg.sh || docker run -v $PWD:/usr/src/i3/ -w /usr/src/i3 ${BASENAME} ./travis/docs.sh
- - ./travis/skip-pkg.sh || travis/prep-bintray.sh
-
-deploy:
- - provider: bintray
- file: travis/bintray-autobuild-debian.json
- user: $BINTRAY_USER
- key: $BINTRAY_KEY
- skip_cleanup: true
- on:
- branch: next
- condition: $CC = gcc
- - provider: bintray
- file: travis/bintray-autobuild-ubuntu.json
- user: $BINTRAY_USER
- key: $BINTRAY_KEY
- skip_cleanup: true
- on:
- branch: next
- condition: $CC = gcc
- - provider: script
- script: travis/deploy-github-pages.sh
- skip_cleanup: true
- on:
- branch: next
- condition: $CC = gcc
-
-after_deploy:
- - travis/cleanup-bintray.pl i3-autobuild
- - travis/cleanup-bintray.pl i3-autobuild-ubuntu
diff --git a/README.md b/README.md
index d94d62c..a470295 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![Travis](https://img.shields.io/travis/Airblader/i3.svg)](https://travis-ci.org/Airblader/i3)
+[![Build Status](https://github.com/Airblader/i3/actions/workflows/main.yml/badge.svg)](https://github.com/Airblader/i3/actions/workflows/main.yml)
[![Issues](https://img.shields.io/github/issues/Airblader/i3.svg)](https://github.com/Airblader/i3/issues)
[![Forks](https://img.shields.io/github/forks/Airblader/i3.svg)](https://github.com/Airblader/i3/network)
[![Stars](https://img.shields.io/github/stars/Airblader/i3.svg)](https://github.com/Airblader/i3/stargazers)
@@ -19,7 +19,7 @@
For bug reports or feature requests regarding i3-gaps specifically, open an issue on [GitHub](https://www.github.com/Airblader/i3). If your issue is with core i3 functionality, please report it [upstream](https://www.github.com/i3/i3).
-For support & all other kinds of questions, you can ask your question on the official [subreddit /r/i3wm](https://www.reddit.com/r/i3wm).
+For support & all other kinds of questions, you can ask your question on [GitHub Discussions](https://github.com/i3/i3/discussions).
# Features
diff --git a/RELEASE-NOTES-4.19.1 b/RELEASE-NOTES-4.19.1
deleted file mode 100644
index 72e479c..0000000
--- a/RELEASE-NOTES-4.19.1
+++ /dev/null
@@ -1,27 +0,0 @@
-
- ┌──────────────────────────────┐
- │ Release notes for i3 v4.19.1 │
- └──────────────────────────────┘
-
-This is i3 v4.19. This version is considered stable. All users of i3 are
-strongly encouraged to upgrade.
-
-This is a bugfix release for v4.19
-
- ┌────────────────────────────┐
- │ Bugfixes │
- └────────────────────────────┘
-
- • fix workspaces not moving to assigned output after output becomes available
- • fix duplicate bindcode after i3-config-wizard
- • fix commented-out rofi call in default i3 config
-
- ┌────────────────────────────┐
- │ Thanks! │
- └────────────────────────────┘
-
-Thanks for testing, bugfixes, discussions and everything I forgot go out to:
-
- Anaël Beutot, Imran Virani, Orestis Floros
-
--- Michael Stapelberg, 2021-02-01
diff --git a/RELEASE-NOTES-4.21.1 b/RELEASE-NOTES-4.21.1
new file mode 100644
index 0000000..4f8a900
--- /dev/null
+++ b/RELEASE-NOTES-4.21.1
@@ -0,0 +1,44 @@
+
+ ┌──────────────────────────────┐
+ │ Release notes for i3 v4.21.1 │
+ └──────────────────────────────┘
+
+This is i3 v4.21.1. This version is considered stable. All users of i3 are
+strongly encouraged to upgrade.
+
+This release fixes a few rough edges with regards to the newly-introduced
+tiling drag feature, which is now configurable:
+https://i3wm.org/docs/userguide.html#config_tiling_drag
+
+ ┌────────────────────────────┐
+ │ Changes in i3 v4.21.1 │
+ └────────────────────────────┘
+
+ • tiling drag: allow configuration
+ • tiling drag: allow click immediately, to focus on decoration click
+ • tiling drag: fix cursor (wrong argument passed)
+ • tiling drag: increase drag threshold, run it through logical_px
+ • tiling drag: left-click needs threshold, mod-click doesn’t
+ • tiling drag: ignore scratchpad windows when locating drop targets
+ • tiling drag: only start when there are drop targets
+ • Raise floating windows when their border is clicked
+
+ ┌────────────────────────────┐
+ │ Bugfixes │
+ └────────────────────────────┘
+
+ • docs/ipc: document sticky field of GET_TREE
+ • man/i3-config-wizard: escape ~ to prevent interpretation as subscript
+ • Motif hints: respect maximum border style configuration set by user
+ • i3-dmenu-desktop: fix quoting bug
+ • Fix segfault during config validation
+
+ ┌────────────────────────────┐
+ │ Thanks! │
+ └────────────────────────────┘
+
+Thanks for testing, bugfixes, discussions and everything I forgot go out to:
+
+ Erich Heine, Matias Goldfeld, Orestis Floros, Tudor Brindus, bodea
+
+-- Michael Stapelberg, 2022-10-24
diff --git a/docs/hacking-howto b/docs/hacking-howto
index cc08cd1..01e5c7f 100644
--- a/docs/hacking-howto
+++ b/docs/hacking-howto
@@ -400,8 +400,8 @@
Then, it looks through all bindings and gets the one which matches the received
event.
-The bound command is parsed by the cmdparse lexer/parser, see +parse_cmd+ in
-+src/cmdparse.y+.
+The bound command is parsed by the i3 parser, see +parse_command+ in
++src/commands_parser.c+.
== Manage windows (src/main.c, manage_window() and reparent_window())
diff --git a/docs/i3-pod2html b/docs/i3-pod2html
index e2de51b..80e98a4 100755
--- a/docs/i3-pod2html
+++ b/docs/i3-pod2html
@@ -83,7 +83,7 @@
diff --git a/docs/i3-sync-working.dia b/docs/i3-sync-working.dia
index 9f1c3bc..2580382 100644
Binary files a/docs/i3-sync-working.dia and b/docs/i3-sync-working.dia differ
diff --git a/docs/i3-sync-working.png b/docs/i3-sync-working.png
index dce44ac..419334d 100644
Binary files a/docs/i3-sync-working.png and b/docs/i3-sync-working.png differ
diff --git a/docs/i3-sync.dia b/docs/i3-sync.dia
index 0945ae2..c8fb835 100644
Binary files a/docs/i3-sync.dia and b/docs/i3-sync.dia differ
diff --git a/docs/i3-sync.png b/docs/i3-sync.png
index b64cce2..c9a5a01 100644
Binary files a/docs/i3-sync.png and b/docs/i3-sync.png differ
diff --git a/docs/i3bar-protocol b/docs/i3bar-protocol
index c1bf8f8..82c2f6c 100644
--- a/docs/i3bar-protocol
+++ b/docs/i3bar-protocol
@@ -191,7 +191,7 @@
is 9 pixels), since the separator line is drawn in the middle.
markup::
A string that indicates how the text of the block should be parsed. Set to
- +"pango"+ to use https://developer.gnome.org/pango/stable/pango-Markup.html[Pango markup].
+ +"pango"+ to use https://developer.gnome.org/pango/1.46/[Pango markup].
Set to +"none"+ to not use any markup (default). Pango markup only works
if you use a pango font.
diff --git a/docs/ipc b/docs/ipc
index bb8719c..0151b24 100644
--- a/docs/ipc
+++ b/docs/ipc
@@ -14,7 +14,8 @@
in +/tmp/i3-%u.XXXXXX/ipc-socket.%p+ where +%u+ is your UNIX username, +%p+ is
the PID of i3 and XXXXXX is a string of random characters from the portable
filename character set (see mkdtemp(3)). You can get the socketpath from i3 by
-calling +i3 --get-socketpath+.
+executing +i3 --get-socketpath+, which will print the path to the standard
+output (plus a newline).
All i3 utilities, like +i3-msg+ and +i3-input+ will read the +I3_SOCKET_PATH+
X11 property, stored on the X11 root window.
@@ -39,12 +40,12 @@
== Sending messages to i3
-To send a message to i3, you have to format in the binary message format which
-i3 expects. This format specifies a magic string in the beginning to ensure
-the integrity of messages (to prevent follow-up errors). Following the magic
-string comes the length of the payload of the message as 32-bit integer, and
-the type of the message as 32-bit integer (the integers are not converted, so
-they are in native byte order).
+To send a message to i3, you have to format it in the binary message format
+which i3 expects. This format specifies a magic string in the beginning to
+ensure the integrity of messages (to prevent follow-up errors). Following the
+magic string comes the length of the payload of the message as a 32-bit
+integer, and the type of the message as a 32-bit integer (the integers are not
+converted, so they are in native byte order).
The magic string currently is "i3-ipc" and will only be changed when a change
in the IPC API is done which breaks compatibility (we hope that we don’t need
@@ -96,17 +97,29 @@
== Receiving replies from i3
-Replies from i3 usually consist of a simple string (the length of the string
-is the message_length, so you can consider them length-prefixed) which in turn
-contain the JSON serialization of a data structure. For example, the
-GET_WORKSPACES message returns an array of workspaces (each workspace is a map
-with certain attributes).
-
-=== Reply format
+Each message sent to i3 will cause exactly one reply to be sent in return. The
+order of the sent replies will always correspond to the order of the sent
+requests. The only exception to this is <>, which (once subscribed to)
+may be sent at any time (though never in the middle of another event or reply).
+
+It is generally safe to send several messages to i3 without first waiting for a
+reply for each one (pipelining) -- though, note that depending on the language /
+network library you use, writing to the socket without also reading from it may
+cause a deadlock due to the socket buffers getting full.
The reply format is identical to the normal message format. There also is
the magic string, then the message length, then the message type and the
payload.
+
+The payload of replies from i3 usually consists of a simple string (the length
+of the string is the message_length, so you can consider them length-prefixed),
+which in turn contain the JSON serialization of a data structure. For example,
+the GET_WORKSPACES message returns an array of workspaces (each workspace is a
+map with certain attributes).
+
+Replies currently have a 1:1 correspondence to messages, with the message type
+of the reply corresponding to the message type of the message which caused the
+reply to be sent.
The following reply types are implemented:
@@ -127,16 +140,30 @@
VERSION (7)::
Reply to the GET_VERSION message.
BINDING_MODES (8)::
- Reply to the GET_BINDING_MODES message.
+ Reply to the GET_BINDING_MODES message.
GET_CONFIG (9)::
Reply to the GET_CONFIG message.
TICK (10)::
Reply to the SEND_TICK message.
+SYNC (11)::
+ Reply to the SYNC message.
GET_BINDING_STATE (12)::
Reply to the GET_BINDING_STATE message.
+== Messages and replies
+
[[_command_reply]]
-=== COMMAND reply
+=== RUN_COMMAND / COMMAND
+
+Run the payload as an https://i3wm.org/docs/userguide.html#list_of_commands[i3
+command] (like the commands you can bind to keys).
+
+*Message:*
+
+The message payload is the string containing the command to execute. There is
+no JSON encoding or trailing newline.
+
+*Reply:*
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
@@ -170,7 +197,15 @@
-------------------
[[_workspaces_reply]]
-=== WORKSPACES reply
+=== GET_WORKSPACES / WORKSPACES
+
+Get the list of current workspaces.
+
+*Message:*
+
+No payload.
+
+*Reply:*
The reply consists of a serialized list of workspaces. Each workspace has the
following properties:
@@ -234,7 +269,16 @@
-------------------
[[_subscribe_reply]]
-=== SUBSCRIBE reply
+=== SUBSCRIBE
+
+Subscribe this IPC connection to the event types specified in the message
+payload. See <>.
+
+*Message:*
+
+A JSON-encoded array of event types to subscribe to.
+
+*Reply:*
The reply consists of a single serialized map. The only property is
+success (bool)+, indicating whether the subscription was successful (the
@@ -246,7 +290,15 @@
-------------------
[[_outputs_reply]]
-=== OUTPUTS reply
+=== GET_OUTPUTS / OUTPUTS
+
+Get the list of current outputs.
+
+*Message:*
+
+No payload.
+
+*Reply:*
The reply consists of a serialized list of outputs. Each output has the
following properties:
@@ -257,7 +309,7 @@
Whether this output is currently active (has a valid mode).
primary (boolean)::
Whether this output is currently the primary output.
-current_workspace (string)::
+current_workspace (string or null)::
The name of the current workspace that is visible on this output. +null+ if
the output is not active.
rect (map)::
@@ -293,7 +345,15 @@
-------------------
[[_tree_reply]]
-=== TREE reply
+=== GET_TREE / TREE
+
+Get the i3 layout tree.
+
+*Message:*
+
+No payload.
+
+*Reply:*
The reply consists of a serialized tree. Each node in the tree (representing
one container) has at least the properties listed below. While the nodes might
@@ -329,7 +389,7 @@
"vertical".
THIS FIELD IS OBSOLETE. It is still present, but your code should not
use it. Instead, rely on the layout field.
-percent (float)::
+percent (float or null)::
The percentage which this container takes in its parent. A value of
+null+ means that the percent property does not make sense for this
container, for example for the root container.
@@ -353,18 +413,19 @@
geometry (map)::
The original geometry the window specified when i3 mapped it. Used when
switching a window to floating mode, for example.
-window (integer)::
+window (integer or null)::
The X11 window ID of the *actual client window* inside this container.
- This field is set to null for split containers or otherwise empty
+ This field is set to +null+ for split containers or otherwise empty
containers. This ID corresponds to what xwininfo(1) and other
X11-related tools display (usually in hex).
window_properties (map)::
This optional field contains all available X11 window properties from the
- following list: *title*, *instance*, *class*, *window_role* and *transient_for*.
+ following list: *title*, *instance*, *class*, *window_role*, *machine*
+ and *transient_for*.
window_type (string)::
- The window type (_NET_WM_WINDOW_TYPE). Possible values are undefined, normal,
- dialog, utility, toolbar, splash, menu, dropdown_menu, popup_menu, tooltip and
- notification.
+ The window type (_NET_WM_WINDOW_TYPE). Possible values are `undefined`,
+ unknown, normal, dialog, utility, toolbar, splash, menu, dropdown_menu,
+ popup_menu, tooltip and notification.
urgent (bool)::
Whether this container (window, split container, floating container or
workspace) has the urgency hint set, directly or indirectly. All parent
@@ -379,6 +440,9 @@
order. Traversing the tree by following the first entry in this array
will result in eventually reaching the one node with +focused+ set to
true.
+sticky (bool)::
+ Whether this window is "sticky". If it is also floating, this window will
+ be present on all workspaces on the same output.
fullscreen_mode (integer)::
Whether this container is in fullscreen state or not.
Possible values are
@@ -386,12 +450,18 @@
+1+ (fullscreened on output) or
+2+ (fullscreened globally).
Note that all workspaces are considered fullscreened on their respective output.
-
+floating (string)::
+ Floating state of container.
+ Can be either "auto_on", "auto_off", "user_on" or "user_off"
nodes (array of node)::
The tiling (i.e. non-floating) child containers of this node.
floating_nodes (array of node)::
The floating child containers of this node. Only non-empty on nodes with
type +workspace+.
+scratchpad_state (string)::
+ Whether the window is not in the scratchpad ("none"), freshly moved to
+ the scratchpad but not yet resized ("fresh") or moved to the scratchpad
+ and resized ("changed").
Please note that in the following example, I have left out some keys/values
which are not relevant for the type of the node. Otherwise, the example would
@@ -532,7 +602,15 @@
-----------------------
[[_marks_reply]]
-=== MARKS reply
+=== GET_MARKS / MARKS
+
+Gets the names of all currently set marks.
+
+*Message:*
+
+No payload.
+
+*Reply:*
The reply consists of a single array of strings for each container that has a
mark. A mark can only be set on one container, so the array is unique.
@@ -541,7 +619,15 @@
If no window has a mark the response will be the empty array [].
[[_bar_config_reply]]
-=== BAR_CONFIG reply
+=== GET_BAR_CONFIG / BAR_CONFIG
+
+Gets the specified bar configuration or the names of all bar configurations if payload is empty.
+
+*Message:*
+
+No payload, or the ID of the bar whose configuration to retrieve.
+
+*Reply:*
This can be used by third-party workspace bars (especially i3bar, but others
are free to implement compatible alternatives) to get the +bar+ block
@@ -641,7 +727,15 @@
--------------
[[_version_reply]]
-=== VERSION reply
+=== GET_VERSION / VERSION
+
+Gets the i3 version.
+
+*Message:*
+
+No payload.
+
+*Reply:*
The reply consists of a single JSON dictionary with the following keys:
@@ -674,7 +768,15 @@
-------------------
[[_binding_modes_reply]]
-=== BINDING_MODES reply
+=== GET_BINDING_MODES / BINDING_MODES
+
+Gets the names of all currently configured binding modes.
+
+*Message:*
+
+No payload.
+
+*Reply:*
The reply consists of an array of all currently configured binding modes.
@@ -684,18 +786,66 @@
---------------------
[[_config_reply]]
-=== CONFIG reply
-
-The config reply is a map which currently only contains the "config" member,
-which is a string containing the config file as loaded by i3 most recently.
-
-*Example:*
--------------------
-{ "config": "font pango:monospace 8\nbindsym Mod4+q exit\n" }
+=== GET_CONFIG / CONFIG
+
+Returns the last loaded i3 config.
+
+*Message:*
+
+No payload.
+
+*Reply:*
+
+The config reply is a map which contains the following fields:
+
+config (string)::
+ The top-level config file contents that i3 has loaded most recently.
+ This field is kept for backwards compatibility. See +included_configs+
+ instead.
+included_configs (array of maps)::
+ i3 adds one entry to this array for each config file it loads, in
+ order. The first entry’s +raw_contents+ are identical to the +config+
+ field.
+
+Each +included_configs+ entry contains the following fields
+
+path (string)::
+ Absolute path name to the config file that i3 loaded.
+raw_contents (string)::
+ The raw contents of the file as i3 read them.
+variable_replaced_contents (string)::
+ The contents of the file after i3 replaced all variables. This is useful
+ for debugging variable replacement.
+
+*Example:*
+-------------------
+{
+ "config": "include font.cfg\n",
+ "included_configs": [
+ {
+ "path": "/home/michael/configfiles/i3/config",
+ "raw_contents": "include font.cfg\n",
+ "variable_replaced_contents": "include font.cfg\n"
+ },
+ {
+ "path": "/home/michael/configfiles/i3/font.cfg",
+ "raw_contents": "set $font pango:monospace 8\nfont $font",
+ "variable_replaced_contents": "set pango:monospace 8 pango:monospace 8\nfont pango:monospace 8\n"
+ }
+ ],
+}
-------------------
[[_tick_reply]]
-=== TICK reply
+=== SEND_TICK / TICK
+
+Sends a tick event with the specified payload.
+
+*Message:*
+
+The payload of the tick event to send to IPC event listeners.
+
+*Reply:*
The reply is a map containing the "success" member. After the reply was
received, the tick event has been written to all IPC connections which subscribe
@@ -709,7 +859,15 @@
-------------------
[[_sync_reply]]
-=== SYNC reply
+=== SYNC
+
+Sends an i3 sync event with the specified random value to the specified window.
+
+*Message:*
+
+A JSON-encoded map with the properties "rnd" and "window" (both integer).
+
+*Reply:*
The reply is a map containing the "success" member. After the reply was
received, the https://i3wm.org/docs/testsuite.html#i3_sync[i3 sync message] was
@@ -721,7 +879,15 @@
-------------------
[[_binding_state_reply]]
-=== GET_BINDING_STATE reply
+=== GET_BINDING_STATE
+
+Request the current binding state, i.e. the currently active binding mode name.
+
+*Message:*
+
+No payload.
+
+*Reply:*
The binding_state reply is a map which currently only contains the "name"
member, which is the name of the currently active binding mode as a string.
diff --git a/docs/layout-saving b/docs/layout-saving
index 4f0ffcc..380ffff 100644
--- a/docs/layout-saving
+++ b/docs/layout-saving
@@ -185,9 +185,9 @@
container. Only if you start Emacs with the proper instance name (+emacs24
--name notmuch+), it will get swallowed.
-You can match on "class", "instance", "window_role" and "title". All values are
-case-sensitive regular expressions (PCRE). Use +xprop(1)+ and click into a
-window to see its properties:
+You can match on "class", "instance", "window_role", "title" and "machine". All
+values are case-sensitive regular expressions (PCRE). Use +xprop(1)+ and click
+into a window to see its properties:
--------------------------------------------------------------------------------
$ xprop
diff --git a/docs/testsuite b/docs/testsuite
index 145da15..ec87429 100644
--- a/docs/testsuite
+++ b/docs/testsuite
@@ -4,10 +4,9 @@
September 2012
This document explains how the i3 testsuite works, how to use it and extend it.
-It is targeted at developers who not necessarily have been doing testing before
-or have not been testing in Perl before. In general, the testsuite is not of
+It is targeted at developers who haven't necessarily done testing before,
+or have not used Perl for testing before. In general, the testsuite is not of
interest for end users.
-
== Introduction
@@ -481,7 +480,7 @@
IPC anymore.
[[i3_sync]]
-== Appendix A: The i3 sync protocol
+== Appendix A: The I3_SYNC protocol
Consider the following situation: You open two windows in your testcase, then
you use +focus left+ and want to verify that the X11 focus has been updated
@@ -499,9 +498,9 @@
However, the test fails. Sometimes. Apparently, there is a race condition in
your test. If you think about it, this is because you are using two different
pieces of software: You tell i3 to update focus, i3 confirms that, and then you
-ask X11 to give you the current focus. There is a certain time i3 needs to
-update the X11 state. If the testcase gets CPU time before X11 processed i3's
-requests, the test will fail.
+ask X11 to give you the current focus. There is a certain time that the X11
+server needs to process the requests from i3. If the testcase's request for the
+input focus is processed before i3's requests, the test will fail.
image::i3-sync.png["Diagram of the race condition", title="Diagram of the race condition"]
@@ -531,10 +530,10 @@
The real solution for this problem is a mechanism which I call "the i3 sync
protocol". The idea is to send a request (which does not modify state) via X11
-to i3 which will then be answered. Due to the request's position in the event
-queue (*after* all previous events), you can be sure that by the time you
-receive the reply, all other events have been dealt with by i3 (and, more
-importantly, X11).
+to i3 which will then be answered, again via X11. Because this answer is
+generated via an X11 request, it will be sent to the X11 server *after* all
+previous requests. Thus, you can be sure that by the time you receive the reply,
+all other events have been dealt with by i3 (and, more importantly, X11).
image::i3-sync-working.png["Diagram of the i3 sync solution", title="Diagram of the i3 sync solution"]
@@ -569,7 +568,35 @@
request. You should use a random value in +data[1]+ and check that you received
the same one when getting the reply.
-== Appendix B: Socket activation
+== Appendix B: The sync IPC command
+
+The above I3_SYNC protocol allows to synchronise with i3. However, it is not
+enough for tests that also involve i3bar: There might still be messages from
+i3bar in-flight even after synchronising with i3. Thus, there also exists a sync
+IPC command, that is however not meant to be used directly. Instead, i3bar uses
+it for implementing the I3_SYNC protocol.
+
+The intended usage works like this:
+
+1. You send an I3_SYNC message to i3bar's window. See <>.
+2. i3bar sends a SYNC IPC command to i3 with payload
+ +{"window":your-window-here,"rnd":your-random-value}+.
+3. i3 reacts to this IPC command as if it received an I3_SYNC request via X11.
+
+This protocol is used, for example, in t/525-i3bar-mouse-bindings.t: A mouse
+button press on i3bar is triggered. i3bar reacts to this by sending IPC commands
+to i3.
+
+The necessary synchronisation is achieved by sending an I3_SYNC event to i3bar:
+Because i3bar reacts with a sync IPC command to i3, all previous IPC commands from
+i3bar will be handled first. Because i3 reacts via X11, all previous X11
+requests from i3 will be handled by the X11 server first.
+
+The actual test also has to sync with i3 first due to how X11 handling works.
+For more details, refer to the documentation for +XAllowEvents+ with mode
++ReplayPointer+.
+
+== Appendix C: Socket activation
Socket activation is a mechanism which was made popular by systemd, an init
replacement. It basically describes creating a listening socket before starting
diff --git a/docs/userguide b/docs/userguide
index 2ad3418..d1bb557 100644
--- a/docs/userguide
+++ b/docs/userguide
@@ -3,9 +3,9 @@
Michael Stapelberg
This document contains all the information you need to configure and use the i3
-window manager. If it does not, please check https://www.reddit.com/r/i3wm/
-first, then contact us on IRC (preferred) or post your question(s) on the
-mailing list.
+window manager. If it does not you can https://i3wm.org/contact/[contact us] on
+https://github.com/i3/i3/discussions[GitHub Discussions], IRC, or the mailing
+list.
== Default keybindings
@@ -53,12 +53,23 @@
image:two_terminals.png[Two terminals]
-To move the focus between the two terminals, you can use the direction keys
-which you might know from the editor +vi+. However, in i3, your homerow is used
-for these keys (in +vi+, the keys are shifted to the left by one for
-compatibility with most keyboard layouts). Therefore, +$mod+j+ is left, +$mod+k+
-is down, +$mod+l+ is up and `$mod+;` is right. So, to switch between the
-terminals, use +$mod+k+ or +$mod+l+. Of course, you can also use the arrow keys.
+To move the focus between the two terminals, you can use the arrow keys. For
+convenience, the arrows are also available directly on the
+https://en.wikipedia.org/wiki/Touch_typing[keyboard’s home row] underneath your
+right hand:
+
+|===
+| `$mod+j` | left
+| `$mod+k` | down
+| `$mod+l` | up
+| `$mod+;` | right
+|===
+
+Note that this differs by one key from the popular text editor `vi`, which was
+https://twitter.com/hillelogram/status/1326600125569961991[developed on an
+ADM-3A terminal and therefore uses `hjkl` instead of `jkl;`] -- i3’s default is
+meant to require minimal finger movement, but some `vi` users change their i3
+config for consistency.
At the moment, your workspace is split (it contains two terminals) in a
specific direction (horizontal by default). Every window can be split
@@ -67,7 +78,7 @@
or browser) and "split container" for containers that consist of one or more
windows.
-TODO: picture of the tree
+//TODO: picture of the tree
To split a window vertically, press +$mod+v+ before you create the new window.
To split it horizontally, press +$mod+h+.
@@ -185,6 +196,49 @@
Floating windows are always on top of tiling windows.
+[[tiling_drag]]
+=== Moving tiling containers with the mouse
+
+Since i3 4.21, it's possible to drag tiling containers using the mouse. The
+drag can be initiated either by dragging the window's titlebar or by pressing
+the <> and dragging the container while holding the
+left-click button.
+
+Once the drag is initiated and the cursor has left the original container, drop
+indicators are created according to the position of the cursor relatively to
+the target container. These indicators help you understand what the resulting
+<> layout is going to be after you release the mouse button.
+
+The possible drop positions are:
+
+Drop on container::
+ This happens when the mouse is relatively near the center of a container.
+ If the mouse is released, the result is exactly as if you had run the
+ +move container to mark+ command. See <>.
+Drop as sibling::
+ This happens when the mouse is relatively near the edge of a container. If
+ the mouse is released, the dragged container will become a sibling of the
+ target container, placed left/right/up/down according to the position of
+ the indicator.
+ This might or might not create a new v-split or h-split according to the
+ previous layout of the target container. For example, if the target
+ container is in an h-split and you drop the dragged container below it, the
+ new layout will have to be a v-split.
+Drop to parent::
+ This happens when the mouse is relatively near the edge of a container (but
+ even closer to the border in comparison to the sibling case above) *and* if
+ that edge is also the parent container's edge. For example, if three
+ containers are in a horizontal layout then edges where this can happen is
+ the left edge of the left container, the right edge of the right container
+ and all bottom and top edges of all three containers.
+ If the mouse is released, the container is first dropped as a sibling to
+ the target container, like in the case above, and then is moved
+ directionally like with the +move left|right|down|up+ command. See
+ <>.
+
+The color of the indicator matches the +client.focused+ setting. See <>.
+
+[[tree]]
== Tree
i3 stores all information about the X11 outputs, workspaces and layout of the
@@ -308,6 +362,90 @@
# i3 config file (v4)
---------------------
+[[include]]
+=== Include directive
+
+Since i3 v4.20, it is possible to include other configuration files from your i3
+configuration.
+
+*Syntax*:
+-----------------
+include
+-----------------
+
+i3 expands `pattern` using shell-like word expansion, specifically using the
+https://manpages.debian.org/wordexp.3[`wordexp(3)` C standard library function].
+
+*Examples*:
+--------------------------------------------------------------------------------
+# Tilde expands to the user’s home directory:
+include ~/.config/i3/assignments.conf
+
+# Environment variables are expanded:
+include $HOME/.config/i3/assignments.conf
+
+# Wildcards are expanded:
+include ~/.config/i3/config.d/*.conf
+
+# Command substitution:
+include ~/.config/i3/`hostname`.conf
+
+# i3 loads each path only once, so including the i3 config will not result
+# in an endless loop, but in an error:
+include ~/.config/i3/config
+
+# i3 changes the working directory while parsing a config file
+# so that relative paths are interpreted relative to the directory
+# of the config file that contains the path:
+include assignments.conf
+--------------------------------------------------------------------------------
+
+If a specified file cannot be read, for example because of a lack of file
+permissions, or because of a dangling symlink, i3 will report an error and
+continue processing your remaining configuration.
+
+To list all loaded configuration files, run `i3 --moreversion`:
+
+--------------------------------------------------------------------------------
+% i3 --moreversion
+Binary i3 version: 4.19.2-87-gfcae64f7+ © 2009 Michael Stapelberg and contributors
+Running i3 version: 4.19.2-87-gfcae64f7+ (pid 963940)
+Loaded i3 config:
+ /tmp/i3.cfg (main) (last modified: 2021-05-13T16:42:31 CEST, 463 seconds ago)
+ /tmp/included.cfg (included) (last modified: 2021-05-13T16:42:43 CEST, 451 seconds ago)
+ /tmp/another.cfg (included) (last modified: 2021-05-13T16:42:46 CEST, 448 seconds ago)
+--------------------------------------------------------------------------------
+
+Variables are shared between all config files, but beware of the following limitation:
+
+* You can define a variable and use it within an included file.
+* You cannot use (in the parent file) a variable that was defined within an included file.
+
+This is a technical limitation: variable expansion happens in a separate stage
+before parsing include directives.
+
+Conceptually, included files can only add to the configuration, not undo the
+effects of already-processed configuration. For example, you can only add new
+key bindings, not overwrite or remove existing key bindings. This means:
+
+* The `include` directive is suitable for organizing large configurations into
+ separate files, possibly selecting files based on conditionals.
+
+* The `include` directive is not suitable for expressing “use the default
+ configuration with the following changes”. For that case, we still recommend
+ copying and modifying the default config.
+
+[NOTE]
+====
+Implementation-wise, i3 does not currently construct one big configuration from
+all `include` directives. Instead, i3’s config file parser interprets all
+configuration directives in its `parse_file()` function. When processing an
+`include` configuration directive, the parser recursively calls `parse_file()`.
+
+This means the evaluation order of files forms a tree, or one could say i3 uses
+depth-first traversal.
+====
+
=== Comments
It is possible and recommended to use comments in your configuration file to
@@ -443,7 +581,7 @@
# The middle button over a titlebar kills the window
bindsym --release button2 kill
-# The middle button and a modifer over any part of the window kills the window
+# The middle button and a modifier over any part of the window kills the window
bindsym --whole-window $mod+button2 kill
# The right button toggles floating
@@ -597,9 +735,10 @@
title_align left|center|right
---------------------------------------------
+[[default_border]]
=== Default border style for new windows
-This option determines which border style new windows will have. The default is
+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.
@@ -945,6 +1084,7 @@
workspace "2: vim" output VGA1
---------------------------
+[[client_colors]]
=== Changing colors
You can change all colors which i3 uses to draw the window decorations.
@@ -961,6 +1101,10 @@
client.focused_inactive::
A client which is the focused one of its container, but it does not have
the focus at the moment.
+client.focused_tab_title::
+ Tab or stack container title that is the parent of the focused container
+ but not directly focused. Defaults to focused_inactive if not specified and
+ does not use the indicator and child_border colors.
client.unfocused::
A client which is not the focused one of its container.
client.urgent::
@@ -1004,7 +1148,7 @@
programs to get information from i3, such as the current workspaces
(to display a workspace bar), and to control i3.
-The IPC socket is enabled by default and will be created in
+By default, an IPC socket will be created in
+$XDG_RUNTIME_DIR/i3/ipc-socket.%p+ if the directory is available, falling back
to +/tmp/i3-%u.XXXXXX/ipc-socket.%p+, where +%u+ is your UNIX username, +%p+ is
the PID of i3 and XXXXXX is a string of random characters from the portable
@@ -1258,6 +1402,29 @@
# this line is not continued \
bindsym Mod1+F fullscreen toggle
-------------------
+
+[[config_tiling_drag]]
+=== Tiling drag
+
+You can configure how to initiate the tiling drag feature (see <>).
+
+*Syntax*:
+--------------------------------
+tiling_drag off
+tiling_drag modifier|titlebar [modifier|titlebar]
+--------------------------------
+
+*Examples*:
+--------------------------------
+# Only initiate a tiling drag when the modifier is held:
+tiling_drag modifier
+
+# Initiate a tiling drag on either titlebar click or held modifier:
+tiling_drag modifier titlebar
+
+# Disable tiling drag altogether
+tiling_drag off
+--------------------------------
== Configuring i3bar
@@ -1880,12 +2047,17 @@
# enable floating mode and move container to workspace 4
for_window [class="^evil-app$"] floating enable, move container to workspace 4
+# enable window icons for all windows with extra horizontal padding of 1px
+for_window [all] title_window_icon padding 1px
+
# move all floating windows to the scratchpad
bindsym $mod+x [floating] move scratchpad
------------------------------------
The criteria which are currently implemented are:
+all::
+ Matches all windows. This criterion requires no value.
class::
Compares the window class (the second part of WM_CLASS). Use the
special value +\_\_focused__+ to match all windows having the same window
@@ -1902,6 +2074,10 @@
Compare the window type (_NET_WM_WINDOW_TYPE). Possible values are
+normal+, +dialog+, +utility+, +toolbar+, +splash+, +menu+, +dropdown_menu+,
+popup_menu+, +tooltip+ and +notification+.
+machine::
+ Compares the name of the machine the client window is running on
+ (WM_CLIENT_MACHINE). Usually, it is equal to the hostname of the local
+ machine, but it may differ if remote X11 apps are used.
id::
Compares the X11 window ID, which you can get via +xwininfo+ for example.
title::
@@ -1939,9 +2115,9 @@
tiling are matched. With "user", only windows that the user made tiling
are matched.
-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.
+The criteria +class+, +instance+, +role+, +title+, +workspace+, +machine+ and
++mark+ are actually regular expressions (PCRE). See +pcresyntax(3)+ or +perldoc
+perlre+ for information on how to use them.
[[exec]]
=== Executing applications (exec)
@@ -2120,7 +2296,7 @@
focus left|right|down|up
focus parent|child|floating|tiling|mode_toggle
focus next|prev [sibling]
-focus output left|right|up|down|primary|
+focus output left|right|down|up|current|primary|next| [output2]…
----------------------------------------------
*Examples*:
@@ -2140,6 +2316,9 @@
# Focus last floating/tiling container
bindsym $mod+g focus mode_toggle
+# Focus the next output (effectively toggles when you only have two outputs)
+bindsym $mod+x move workspace to output next
+
# Focus the output right to the current one
bindsym $mod+x focus output right
@@ -2148,6 +2327,9 @@
# Focus the primary output
bindsym $mod+x focus output primary
+
+# Cycle focus between outputs VGA1 and LVDS1 but not DVI0
+bindsym $mod+x move workspace to output VGA1 LVDS1
-------------------------------------------------
Note that you might not have a primary output configured yet. To do so, run:
@@ -2155,6 +2337,7 @@
xrandr --output --primary
-------------------------
+[[move_direction]]
=== Moving containers
Use the +move+ command to move a container.
@@ -2281,9 +2464,8 @@
See <> for how to move a container/workspace to a different
RandR output.
-Workspace names are parsed as
-https://developer.gnome.org/pango/stable/pango-Markup.html[Pango markup]
-by i3bar.
+Workspace names are parsed as https://developer.gnome.org/pango/1.46/[Pango
+markup] by i3bar.
[[back_and_forth]]
To switch back to the previously focused workspace, use +workspace
@@ -2390,9 +2572,11 @@
If a workspace does not exist, the command +workspace number "1: mail"+ will
create workspace "1: mail".
-If a workspace with number 1 does already exist, the command will switch to this
+If a workspace with number 1 already exists, the command will switch to this
workspace and ignore the text part. So even when the workspace has been renamed
-to "1: web", the above command will still switch to it.
+"1: web", the above command will still switch to it. The command +workspace 1+
+will however create and move to a new workspace "1" alongside the existing
+"1: mail" workspace.
=== Moving workspaces to a different screen
@@ -2408,15 +2592,18 @@
*Syntax*:
------------------------------------------------------------
-move container to output left|right|down|up|current|primary|
-move workspace to output left|right|down|up|current|primary|
-------------------------------------------------------------
+move container to output left|right|down|up|current|primary|next| [output2]…
+move workspace to output left|right|down|up|current|primary|next| [output2]…
+-------------------------------------------------------------------------------------
*Examples*:
--------------------------------------------------------
# Move the current workspace to the next output
# (effectively toggles when you only have two outputs)
-bindsym $mod+x move workspace to output right
+bindsym $mod+x move workspace to output next
+
+# Cycle this workspace between outputs VGA1 and LVDS1 but not DVI0
+bindsym $mod+x move workspace to output VGA1 LVDS1
# Put this window on the presentation output.
bindsym $mod+x move container to output VGA1
@@ -2424,12 +2611,18 @@
# Put this window on the primary output.
bindsym $mod+x move container to output primary
--------------------------------------------------------
+
+If you specify more than one output, the container/workspace is cycled through
+them: If it is already in one of the outputs of the list, it will move to the
+next output in the list. If it is in an output not in the list, it will move to
+the first specified output. Non-existing outputs are skipped.
Note that you might not have a primary output configured yet. To do so, run:
-------------------------
xrandr --output --primary
-------------------------
+[[move_to_mark]]
=== Moving containers/windows to marks
To move a container to another container with a specific mark (see <>),
@@ -2575,9 +2768,8 @@
By default, i3 will simply print the X11 window title. Using +title_format+,
this can be customized by setting the format to the desired output. This
-directive supports
-https://developer.gnome.org/pango/stable/pango-Markup.html[Pango markup]
-and the following placeholders which will be replaced:
+directive supports https://developer.gnome.org/pango/1.46/[Pango markup] and the
+following placeholders which will be replaced:
+%title+::
For normal windows, this is the X11 window title (_NET_WM_NAME or WM_NAME
@@ -2590,6 +2782,9 @@
+%instance+::
The X11 window instance (first part of WM_CLASS). This corresponds to the
+instance+ criterion, see <>.
++%machine+::
+ The X11 name of the machine (WM_CLIENT_MACHINE). This corresponds to the
+ +machine+ criterion, see <>.
Using the <> directive, you can set the title format for any window
based on <>.
@@ -2609,6 +2804,32 @@
# print window titles of firefox windows red
for_window [class="(?i)firefox"] title_format "%title "
+-------------------------------------------------------------------------------------
+
+[[title_window_icon]]
+=== Window title icon
+
+By default, i3 does not display the window icon in the title bar.
+
+Starting with i3 v4.20, you can optionally enable window icons either for
+specific windows or for all windows (using the <> directive).
+
+*Syntax*:
+-----------------------------
+title_window_icon
+title_window_icon
+------------------------------
+
+*Examples*:
+-------------------------------------------------------------------------------------
+# show the window icon for the focused window to make it stand out
+bindsym $mod+p title_window_icon on
+
+# enable window icons for all windows
+for_window [all] title_window_icon on
+
+# enable window icons for all windows with extra horizontal padding
+for_window [all] title_window_icon padding 3px
-------------------------------------------------------------------------------------
=== Changing border style
@@ -2640,9 +2861,15 @@
bindsym $mod+t border normal 0
# use no window title and a thick border
bindsym $mod+y border pixel 3
+# use window title *and* a thick border
+bindsym $mod+y border normal 3
# use neither window title nor border
bindsym $mod+u border none
+# no border on VLC
+for_window [class="vlc"] border none
----------------------------------------------
+
+To change the default for all windows, see the directive <>.
[[shmlog]]
=== Enabling shared memory logging
diff --git a/etc/config b/etc/config
index ce22fd3..40076ca 100644
--- a/etc/config
+++ b/etc/config
@@ -16,6 +16,10 @@
# This font is widely installed, provides lots of unicode glyphs, right-to-left
# text rendering and scalability on retina/hidpi displays (thanks to pango).
#font pango:DejaVu Sans Mono 8
+
+# Start XDG autostart .desktop files using dex. See also
+# https://wiki.archlinux.org/index.php/XDG_Autostart
+exec --no-startup-id dex --autostart --environment i3
# 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.
@@ -44,6 +48,10 @@
# use Mouse+Mod1 to drag floating windows to their wanted position
floating_modifier Mod1
+
+# move tiling windows via drag & drop by left-clicking into the title bar,
+# or left-clicking anywhere into the window while holding the floating modifier.
+tiling_drag modifier titlebar
# start a terminal
bindsym Mod1+Return exec i3-sensible-terminal
diff --git a/etc/config.keycodes b/etc/config.keycodes
index f76d5a7..7bc5d60 100644
--- a/etc/config.keycodes
+++ b/etc/config.keycodes
@@ -17,6 +17,10 @@
# This font is widely installed, provides lots of unicode glyphs, right-to-left
# text rendering and scalability on retina/hidpi displays (thanks to pango).
#font pango:DejaVu Sans Mono 8
+
+# Start XDG autostart .desktop files using dex. See also
+# https://wiki.archlinux.org/index.php/XDG_Autostart
+exec --no-startup-id dex --autostart --environment i3
# 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.
@@ -38,6 +42,10 @@
# Use Mouse+$mod to drag floating windows to their wanted position
floating_modifier $mod
+
+# move tiling windows via drag & drop by left-clicking into the title bar,
+# or left-clicking anywhere into the window while holding the floating modifier.
+tiling_drag modifier titlebar
# start a terminal
bindcode $mod+36 exec i3-sensible-terminal
diff --git a/generate-command-parser.pl b/generate-command-parser.pl
index 77502db..1772873 100755
--- a/generate-command-parser.pl
+++ b/generate-command-parser.pl
@@ -133,7 +133,7 @@
open(my $callfh, '>', "GENERATED_${prefix}_call.h");
my $resultname = uc(substr($prefix, 0, 1)) . substr($prefix, 1) . 'ResultIR';
say $callfh '#pragma once';
-say $callfh "static void GENERATED_call(const int call_identifier, struct $resultname *result) {";
+say $callfh "static void GENERATED_call(Match *current_match, struct stack *stack, const int call_identifier, struct $resultname *result) {";
say $callfh ' switch (call_identifier) {';
my $call_id = 0;
for my $state (@keys) {
@@ -150,8 +150,8 @@
# calls to get_string(). Also replaces state names (like FOR_WINDOW)
# with their ID (useful for cfg_criteria_init(FOR_WINDOW) e.g.).
$cmd =~ s/$_/$statenum{$_}/g for @keys;
- $cmd =~ s/\$([a-z_]+)/get_string("$1")/g;
- $cmd =~ s/\&([a-z_]+)/get_long("$1")/g;
+ $cmd =~ s/\$([a-z_]+)/get_string(stack, "$1")/g;
+ $cmd =~ s/\&([a-z_]+)/get_long(stack, "$1")/g;
# For debugging/testing, we print the call using printf() and thus need
# to generate a format string. The format uses %d for s,
# literal numbers or state IDs and %s for NULL, s and literal
@@ -175,9 +175,9 @@
say $callfh '#ifndef TEST_PARSER';
my $real_cmd = $cmd;
if ($real_cmd =~ /\(\)/) {
- $real_cmd =~ s/\(/(¤t_match, result/;
+ $real_cmd =~ s/\(/(current_match, result/;
} else {
- $real_cmd =~ s/\(/(¤t_match, result, /;
+ $real_cmd =~ s/\(/(current_match, result, /;
}
say $callfh " $real_cmd;";
say $callfh '#else';
@@ -228,8 +228,15 @@
($call_identifier) = ($next_state =~ /^call ([0-9]+)$/);
$next_state = '__CALL';
}
- my $identifier = $token->{identifier};
- say $tokfh qq| { "$token_name", "$identifier", $next_state, { $call_identifier } },|;
+ my $identifier;
+ # Set $identifier to NULL if there is no identifier
+ if ($token->{identifier} eq ""){
+ $identifier = "NULL"
+ }
+ else{
+ $identifier = qq|"$token->{identifier}"|;
+ }
+ say $tokfh qq| { "$token_name", $identifier, $next_state, { $call_identifier } },|;
}
say $tokfh '};';
}
diff --git a/i3-config-wizard/main.c b/i3-config-wizard/main.c
index 25117f2..c5ec071 100644
--- a/i3-config-wizard/main.c
+++ b/i3-config-wizard/main.c
@@ -331,8 +331,6 @@
* separate configuration directives. */
while ((*walk == ' ' || *walk == '\t') && *walk != '\0')
walk++;
-
- //printf("remaining input: %s\n", walk);
cmdp_token_ptr *ptr = &(tokens[state]);
for (c = 0; c < ptr->n; c++) {
@@ -426,15 +424,13 @@
}
if (strcmp(token->name, "end") == 0) {
- //printf("checking for end: *%s*\n", walk);
if (*walk == '\0' || *walk == '\n' || *walk == '\r') {
if ((result = next_state(token)) != NULL)
return result;
- /* To make sure we start with an appropriate matching
- * datastructure for commands which do *not* specify any
+ /* To make sure we start with an appropriate matching data
+ * structure for commands which do *not* specify any
* criteria, we re-initialize the criteria system after
* every command. */
- // TODO: make this testable
walk++;
break;
}
diff --git a/i3-dmenu-desktop b/i3-dmenu-desktop
index 07fe833..bb69335 100755
--- a/i3-dmenu-desktop
+++ b/i3-dmenu-desktop
@@ -154,6 +154,8 @@
},
no_chdir => 1,
follow_fast => 1,
+ # Ignore any duplicate files and directories and proceed normally:
+ follow_skip => 2,
},
@searchdirs
);
@@ -411,7 +413,7 @@
my $location = $app->{_Location};
# Quote as described by “The Exec key”:
-# https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
+# https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s07.html
sub quote {
my ($str) = @_;
$str =~ s/("|`|\$|\\)/\\$1/g;
@@ -422,6 +424,17 @@
$choice = quote($choice);
$location = quote($location);
$name = quote($name);
+
+# https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s07.html:
+#
+# Note that the general escape rule for values of type string states that the
+# backslash character can be escaped as ("\\") as well and that this escape rule
+# is applied before the quoting rule. As such, to unambiguously represent a
+# literal backslash character in a quoted argument in a desktop entry file
+# requires the use of four successive backslash characters ("\\\\"). Likewise, a
+# literal dollar sign in a quoted argument in a desktop entry file is
+# unambiguously represented with ("\\$").
+$exec =~ s/\\\\/\\/g;
# Remove deprecated field codes, as the spec dictates.
$exec =~ s/%[dDnNvm]//g;
@@ -479,8 +492,10 @@
# starts with a double quote ("), everything is parsed as-is until the next
# double quote which is NOT preceded by a backslash (\).
#
- # Therefore, we escape all double quotes (") by replacing them with \"
- $exec =~ s/"/\\"/g;
+ # Therefore, we escape all double quotes (") by replacing them with \".
+ # To not change the meaning of any double quote, backslashes need to be
+ # escaped as well.
+ $exec =~ s/(["\\])/\\$1/g;
if (exists($app->{StartupNotify}) && !$app->{StartupNotify}) {
$nosn = '--no-startup-id';
diff --git a/i3-dump-log/main.c b/i3-dump-log/main.c
index e58b0c3..0ce2226 100644
--- a/i3-dump-log/main.c
+++ b/i3-dump-log/main.c
@@ -25,22 +25,16 @@
#include
#include
#include
-
-#if !defined(__OpenBSD__)
-static uint32_t offset_next_write;
-#endif
+#include
+#include
+#include
+
static uint32_t wrap_count;
static i3_shmlog_header *header;
static char *logbuffer,
*walk;
static int ipcfd = -1;
-
-static volatile bool interrupted = false;
-
-static void sighandler(int signal) {
- interrupted = true;
-}
static void disable_shmlog(void) {
const char *disablecmd = "debuglog off; shmlog off";
@@ -188,22 +182,25 @@
/* NB: While we must never write, we need O_RDWR for the pthread condvar. */
int logbuffer_shm = shm_open(shmname, O_RDWR, 0);
- if (logbuffer_shm == -1)
+ if (logbuffer_shm == -1) {
err(EXIT_FAILURE, "Could not shm_open SHM segment for the i3 log (%s)", shmname);
-
- if (fstat(logbuffer_shm, &statbuf) != 0)
+ }
+
+ if (fstat(logbuffer_shm, &statbuf) != 0) {
err(EXIT_FAILURE, "stat(%s)", shmname);
-
- /* NB: While we must never write, we need PROT_WRITE for the pthread condvar. */
- logbuffer = mmap(NULL, statbuf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, logbuffer_shm, 0);
- if (logbuffer == MAP_FAILED)
+ }
+
+ logbuffer = mmap(NULL, statbuf.st_size, PROT_READ, MAP_SHARED, logbuffer_shm, 0);
+ if (logbuffer == MAP_FAILED) {
err(EXIT_FAILURE, "Could not mmap SHM segment for the i3 log");
+ }
header = (i3_shmlog_header *)logbuffer;
- if (verbose)
+ if (verbose) {
printf("next_write = %d, last_wrap = %d, logbuffer_size = %d, shmname = %s\n",
header->offset_next_write, header->offset_last_wrap, header->size, shmname);
+ }
free(shmname);
walk = logbuffer + header->offset_next_write;
@@ -233,25 +230,38 @@
return 0;
}
- /* Handle SIGINT gracefully to invoke atexit handlers, if any. */
- struct sigaction action;
- action.sa_handler = sighandler;
- sigemptyset(&action.sa_mask);
- action.sa_flags = 0;
- sigaction(SIGINT, &action, NULL);
-
- /* Since pthread_cond_wait() expects a mutex, we need to provide one.
- * To not lock i3 (that’s bad, mhkay?) we just define one outside of
- * the shared memory. */
- pthread_mutex_t dummy_mutex = PTHREAD_MUTEX_INITIALIZER;
- pthread_mutex_lock(&dummy_mutex);
- while (!interrupted) {
- pthread_cond_wait(&(header->condvar), &dummy_mutex);
- /* If this was not a spurious wakeup, print the new lines. */
- if (header->offset_next_write != offset_next_write) {
- offset_next_write = header->offset_next_write;
- print_till_end();
- }
+ char *log_stream_socket_path = root_atom_contents("I3_LOG_STREAM_SOCKET_PATH", NULL, 0);
+ if (log_stream_socket_path == NULL) {
+ errx(EXIT_FAILURE, "could not determine i3 log stream socket path: possible i3-dump-log and i3 version mismatch");
+ }
+
+ int sockfd = socket(AF_LOCAL, SOCK_STREAM, 0);
+ if (sockfd == -1) {
+ err(EXIT_FAILURE, "Could not create socket");
+ }
+
+ (void)fcntl(sockfd, F_SETFD, FD_CLOEXEC);
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(struct sockaddr_un));
+ addr.sun_family = AF_LOCAL;
+ strncpy(addr.sun_path, log_stream_socket_path, sizeof(addr.sun_path) - 1);
+ if (connect(sockfd, (const struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) {
+ err(EXIT_FAILURE, "Could not connect to i3 on socket %s", log_stream_socket_path);
+ }
+
+ /* Same size as the buffer used in log.c vlog(): */
+ char buf[4096];
+ for (;;) {
+ const int n = read(sockfd, buf, sizeof(buf));
+ if (n == -1) {
+ err(EXIT_FAILURE, "read(log-stream-socket):");
+ }
+ if (n == 0) {
+ exit(0); /* i3 closed the socket */
+ }
+ buf[n] = '\0';
+ swrite(STDOUT_FILENO, buf, n);
}
#endif
diff --git a/i3-input/keysym.map b/i3-input/keysym.map
index b198dd6..aaecdfc 100644
--- a/i3-input/keysym.map
+++ b/i3-input/keysym.map
@@ -1,6 +1,6 @@
# This list can be used to convert X11 Keysyms to Unicode 2.1 character.
# The list is not checked for correctness by Unicode officials. Use it
-# at your own risk and the creator is not responsable for any damage that
+# at your own risk and the creator is not responsible for any damage that
# occurred due to using this list.
#
# The list is created by looking at the Keysym names and the Unicode data
diff --git a/i3-msg/main.c b/i3-msg/main.c
index c1c8bb8..239ac46 100644
--- a/i3-msg/main.c
+++ b/i3-msg/main.c
@@ -156,6 +156,7 @@
char *payload = NULL;
bool quiet = false;
bool monitor = false;
+ bool raw_reply = false;
static struct option long_options[] = {
{"socket", required_argument, 0, 's'},
@@ -164,9 +165,10 @@
{"quiet", no_argument, 0, 'q'},
{"monitor", no_argument, 0, 'm'},
{"help", no_argument, 0, 'h'},
+ {"raw", no_argument, 0, 'r'},
{0, 0, 0, 0}};
- char *options_string = "s:t:vhqm";
+ char *options_string = "s:t:vhqmr";
while ((o = getopt_long(argc, argv, options_string, long_options, &option_index)) != -1) {
if (o == 's') {
@@ -217,6 +219,8 @@
return 0;
} else if (o == '?') {
exit(EXIT_FAILURE);
+ } else if (o == 'r') {
+ raw_reply = true;
}
}
@@ -262,32 +266,38 @@
/* For the reply of commands, have a look if that command was successful.
* If not, nicely format the error message. */
if (reply_type == I3_IPC_REPLY_TYPE_COMMAND) {
- yajl_handle handle = yajl_alloc(&reply_callbacks, NULL, NULL);
- yajl_status state = yajl_parse(handle, (const unsigned char *)reply, reply_length);
- yajl_free(handle);
-
- switch (state) {
- case yajl_status_ok:
- break;
- case yajl_status_client_canceled:
- case yajl_status_error:
- errx(EXIT_FAILURE, "IPC: Could not parse JSON reply.");
- }
-
- if (!quiet) {
+ if (!raw_reply) {
+ yajl_handle handle = yajl_alloc(&reply_callbacks, NULL, NULL);
+ yajl_status state = yajl_parse(handle, (const unsigned char *)reply, reply_length);
+ yajl_free(handle);
+
+ switch (state) {
+ case yajl_status_ok:
+ break;
+ case yajl_status_client_canceled:
+ case yajl_status_error:
+ errx(EXIT_FAILURE, "IPC: Could not parse JSON reply.");
+ }
+ }
+
+ if (!quiet || raw_reply) {
printf("%.*s\n", reply_length, reply);
}
} else if (reply_type == I3_IPC_REPLY_TYPE_CONFIG) {
- yajl_handle handle = yajl_alloc(&config_callbacks, NULL, NULL);
- yajl_status state = yajl_parse(handle, (const unsigned char *)reply, reply_length);
- yajl_free(handle);
-
- switch (state) {
- case yajl_status_ok:
- break;
- case yajl_status_client_canceled:
- case yajl_status_error:
- errx(EXIT_FAILURE, "IPC: Could not parse JSON reply.");
+ if (raw_reply) {
+ printf("%.*s\n", reply_length, reply);
+ } else {
+ yajl_handle handle = yajl_alloc(&config_callbacks, NULL, NULL);
+ yajl_status state = yajl_parse(handle, (const unsigned char *)reply, reply_length);
+ yajl_free(handle);
+
+ switch (state) {
+ case yajl_status_ok:
+ break;
+ case yajl_status_client_canceled:
+ case yajl_status_error:
+ errx(EXIT_FAILURE, "IPC: Could not parse JSON reply.");
+ }
}
} else if (reply_type == I3_IPC_REPLY_TYPE_SUBSCRIBE) {
do {
diff --git a/i3-nagbar/main.c b/i3-nagbar/main.c
index b13ee13..7d9c090 100644
--- a/i3-nagbar/main.c
+++ b/i3-nagbar/main.c
@@ -266,13 +266,9 @@
}
/**
- * Return the position and size the i3-nagbar window should use.
- * This will be the primary output or a fallback if it cannot be determined.
- */
-static xcb_rectangle_t get_window_position(void) {
- /* Default values if we cannot determine the primary output or its CRTC info. */
- xcb_rectangle_t result = (xcb_rectangle_t){50, 50, 500, font.height + 2 * MSG_PADDING + BAR_BORDER};
-
+ * Tries to position the rectangle on the primary output.
+ */
+static void set_window_position_primary(xcb_rectangle_t *result) {
xcb_randr_get_screen_resources_current_cookie_t rcookie = xcb_randr_get_screen_resources_current(conn, root);
xcb_randr_get_output_primary_cookie_t pcookie = xcb_randr_get_output_primary(conn, root);
@@ -314,14 +310,61 @@
goto free_resources;
}
- result.x = crtc->x;
- result.y = crtc->y;
+ result->x = crtc->x;
+ result->y = crtc->y;
goto free_resources;
free_resources:
free(res);
free(primary);
- return result;
+}
+
+/**
+ * Tries to position the rectangle on the output with input focus.
+ * If unsuccessful, try to position on primary output.
+ */
+static void set_window_position_focus(xcb_rectangle_t *result) {
+ bool success = false;
+ xcb_get_input_focus_reply_t *input_focus = NULL;
+ xcb_get_geometry_reply_t *geometry = NULL;
+ xcb_translate_coordinates_reply_t *coordinates = NULL;
+
+ /* To avoid the input window disappearing while determining its position */
+ xcb_grab_server(conn);
+
+ input_focus = xcb_get_input_focus_reply(conn, xcb_get_input_focus(conn), NULL);
+ if (input_focus == NULL || input_focus->focus == XCB_NONE) {
+ LOG("Failed to receive the current input focus or no window has the input focus right now.\n");
+ goto free_resources;
+ }
+
+ geometry = xcb_get_geometry_reply(conn, xcb_get_geometry(conn, input_focus->focus), NULL);
+ if (geometry == NULL) {
+ LOG("Failed to received window geometry.\n");
+ goto free_resources;
+ }
+
+ coordinates = xcb_translate_coordinates_reply(
+ conn, xcb_translate_coordinates(conn, input_focus->focus, root, geometry->x, geometry->y), NULL);
+ if (coordinates == NULL) {
+ LOG("Failed to translate coordinates.\n");
+ goto free_resources;
+ }
+
+ LOG("Found current focus at x = %i / y = %i.\n", coordinates->dst_x, coordinates->dst_y);
+ result->x = coordinates->dst_x;
+ result->y = coordinates->dst_y;
+ success = true;
+
+free_resources:
+ xcb_ungrab_server(conn);
+ free(input_focus);
+ free(coordinates);
+ free(geometry);
+ if (!success) {
+ LOG("Could not position on focused output, trying to position on primary output.\n");
+ set_window_position_primary(result);
+ }
}
int main(int argc, char *argv[]) {
@@ -360,6 +403,7 @@
argv0 = argv[0];
+ bool position_on_primary = false;
char *pattern = sstrdup("pango:monospace 8");
int o, option_index = 0;
enum { TYPE_ERROR = 0,
@@ -373,9 +417,10 @@
{"help", no_argument, 0, 'h'},
{"message", required_argument, 0, 'm'},
{"type", required_argument, 0, 't'},
+ {"primary", no_argument, 0, 'p'},
{0, 0, 0, 0}};
- char *options_string = "b:B:f:m:t:vh";
+ char *options_string = "b:B:f:m:t:vhp";
prompt = i3string_from_utf8("Please do not run this program.");
@@ -399,8 +444,11 @@
case 'h':
free(pattern);
printf("i3-nagbar " I3_VERSION "\n");
- printf("i3-nagbar [-m ] [-b ] [-B ] [-t warning|error] [-f ] [-v]\n");
+ printf("i3-nagbar [-m ] [-b ] [-B ] [-t warning|error] [-f ] [-v] [-p]\n");
return 0;
+ case 'p':
+ position_on_primary = true;
+ break;
case 'b':
case 'B':
buttons = srealloc(buttons, sizeof(button_t) * (buttoncnt + 1));
@@ -464,7 +512,13 @@
err(EXIT_FAILURE, "pledge");
#endif
- xcb_rectangle_t win_pos = get_window_position();
+ /* Default values if we cannot determine the preferred window position. */
+ xcb_rectangle_t win_pos = (xcb_rectangle_t){50, 50, 500, font.height + 2 * MSG_PADDING + BAR_BORDER};
+ if (position_on_primary) {
+ set_window_position_primary(&win_pos);
+ } else {
+ set_window_position_focus(&win_pos);
+ }
xcb_cursor_context_t *cursor_ctx;
if (xcb_cursor_context_new(conn, root_screen, &cursor_ctx) < 0) {
diff --git a/i3-sensible-pager b/i3-sensible-pager
index 386e298..2d82468 100755
--- a/i3-sensible-pager
+++ b/i3-sensible-pager
@@ -8,6 +8,11 @@
# Distributions/packagers can enhance this script with a
# distribution-specific mechanism to find the preferred pager.
+# The less -E and -F options exit immediately for short files, strip if present.
+case "$LESS" in
+ *[EF]*) LESS=`echo "$LESS" | tr -d EF`
+esac
+
# Hopefully one of these is installed (no flamewars about preference please!):
# We don't use 'more' because it will exit if the file is too short.
# Worst case scenario we'll open the file in your editor.
diff --git a/i3-sensible-terminal b/i3-sensible-terminal
index c3a2e4e..bee303f 100755
--- a/i3-sensible-terminal
+++ b/i3-sensible-terminal
@@ -8,7 +8,13 @@
# We welcome patches that add distribution-specific mechanisms to find the
# preferred terminal emulator. On Debian, there is the x-terminal-emulator
# symlink for example.
-for terminal in "$TERMINAL" x-terminal-emulator urxvt rxvt termit terminator Eterm aterm uxterm xterm gnome-terminal roxterm xfce4-terminal termite lxterminal mate-terminal terminology st qterminal lilyterm tilix terminix konsole kitty guake tilda alacritty hyper; do
+#
+# Invariants:
+# 1. $TERMINAL must come first
+# 2. Distribution-specific mechanisms come next, e.g. x-terminal-emulator
+# 3. The terminal emulator with best accessibility comes first.
+# 4. No order is guaranteed/desired for the remaining terminal emulators.
+for terminal in "$TERMINAL" x-terminal-emulator mate-terminal gnome-terminal terminator xfce4-terminal urxvt rxvt termit Eterm aterm uxterm xterm roxterm termite lxterminal terminology st qterminal lilyterm tilix terminix konsole kitty guake tilda alacritty hyper wezterm; do
if command -v "$terminal" > /dev/null 2>&1; then
exec "$terminal" "$@"
fi
diff --git a/i3bar/include/child.h b/i3bar/include/child.h
index adc638b..ae523bc 100644
--- a/i3bar/include/child.h
+++ b/i3bar/include/child.h
@@ -43,6 +43,12 @@
} i3bar_child;
/*
+ * Remove all blocks from the given statusline.
+ * If free_resources is set, the fields of each status block will be free'd.
+ */
+void clear_statusline(struct statusline_head *head, bool free_resources);
+
+/*
* Start a child process with the specified command and reroute stdin.
* We actually start a $SHELL to execute the command so we don't have to care
* about arguments and such
diff --git a/i3bar/include/common.h b/i3bar/include/common.h
index 52f77b0..e0f2e7e 100644
--- a/i3bar/include/common.h
+++ b/i3bar/include/common.h
@@ -66,7 +66,7 @@
uint32_t border_left;
bool pango_markup;
- /* The amount of pixels necessary to render a separater after the block. */
+ /* The amount of pixels necessary to render a separator after the block. */
uint32_t sep_block_width;
/* Continuously-updated information on how to render this status block. */
diff --git a/i3bar/include/configuration.h b/i3bar/include/configuration.h
index 589c639..346d5b2 100644
--- a/i3bar/include/configuration.h
+++ b/i3bar/include/configuration.h
@@ -76,6 +76,13 @@
void parse_config_json(char *json);
/**
+ * Start parsing the received bar configuration list. The only usecase right
+ * now is to automatically get the first bar id.
+ *
+ */
+void parse_get_first_i3bar_config(char *json);
+
+/**
* free()s the color strings as soon as they are not needed anymore.
*
*/
diff --git a/i3bar/include/ipc.h b/i3bar/include/ipc.h
index 5056e5f..2ce0721 100644
--- a/i3bar/include/ipc.h
+++ b/i3bar/include/ipc.h
@@ -18,7 +18,7 @@
* socket_path must be a valid path to the ipc_socket of i3
*
*/
-int init_connection(const char *socket_path);
+void init_connection(const char *socket_path);
/*
* Destroy the connection to i3.
diff --git a/i3bar/src/child.c b/i3bar/src/child.c
index bece314..df4c660 100644
--- a/i3bar/src/child.c
+++ b/i3bar/src/child.c
@@ -27,7 +27,7 @@
#include
/* Global variables for child_*() */
-i3bar_child child;
+i3bar_child child = {0};
#define DLOG_CHILD DLOG("%s: pid=%ld stopped=%d stop_signal=%d cont_signal=%d click_events=%d click_events_init=%d\n", \
__func__, (long)child.pid, child.stopped, child.stop_signal, child.cont_signal, child.click_events, child.click_events_init)
@@ -66,7 +66,7 @@
* Remove all blocks from the given statusline.
* If free_resources is set, the fields of each status block will be free'd.
*/
-static void clear_statusline(struct statusline_head *head, bool free_resources) {
+void clear_statusline(struct statusline_head *head, bool free_resources) {
struct status_block *first;
while (!TAILQ_EMPTY(head)) {
first = TAILQ_FIRST(head);
@@ -139,6 +139,10 @@
if (stdin_io != NULL) {
ev_io_stop(main_loop, stdin_io);
FREE(stdin_io);
+ close(stdin_fd);
+ stdin_fd = 0;
+ close(child_stdin);
+ child_stdin = 0;
}
if (child_sig != NULL) {
diff --git a/i3bar/src/config.c b/i3bar/src/config.c
index c962779..efc2f51 100644
--- a/i3bar/src/config.c
+++ b/i3bar/src/config.c
@@ -13,7 +13,6 @@
#include
#include
-#include
#include
config_t config;
@@ -126,31 +125,31 @@
}
if (len == strlen("shift") && !strncmp((const char *)val, "shift", strlen("shift"))) {
- config.modifier = ShiftMask;
+ config.modifier = XCB_MOD_MASK_SHIFT;
return 1;
}
if (len == strlen("ctrl") && !strncmp((const char *)val, "ctrl", strlen("ctrl"))) {
- config.modifier = ControlMask;
+ config.modifier = XCB_MOD_MASK_CONTROL;
return 1;
}
if (len == strlen("Mod") + 1 && !strncmp((const char *)val, "Mod", strlen("Mod"))) {
switch (val[3]) {
case '1':
- config.modifier = Mod1Mask;
+ config.modifier = XCB_MOD_MASK_1;
return 1;
case '2':
- config.modifier = Mod2Mask;
+ config.modifier = XCB_MOD_MASK_2;
return 1;
case '3':
- config.modifier = Mod3Mask;
+ config.modifier = XCB_MOD_MASK_3;
return 1;
case '5':
- config.modifier = Mod5Mask;
+ config.modifier = XCB_MOD_MASK_5;
return 1;
}
}
- config.modifier = Mod4Mask;
+ config.modifier = XCB_MOD_MASK_4;
return 1;
}
@@ -184,7 +183,6 @@
if (!strcmp(cur_key, "status_command")) {
DLOG("command = %.*s\n", len, val);
- FREE(config.command);
sasprintf(&config.command, "%.*s", len, val);
return 1;
}
@@ -373,14 +371,12 @@
*
*/
void parse_config_json(char *json) {
- yajl_handle handle;
- yajl_status state;
- handle = yajl_alloc(&outputs_callbacks, NULL, NULL);
+ yajl_handle handle = yajl_alloc(&outputs_callbacks, NULL, NULL);
TAILQ_INIT(&(config.bindings));
TAILQ_INIT(&(config.tray_outputs));
- state = yajl_parse(handle, (const unsigned char *)json, strlen(json));
+ yajl_status state = yajl_parse(handle, (const unsigned char *)json, strlen(json));
/* FIXME: Proper error handling for JSON parsing */
switch (state) {
@@ -393,6 +389,25 @@
break;
}
+ yajl_free(handle);
+}
+
+static int i3bar_config_string_cb(void *params_, const unsigned char *val, size_t _len) {
+ sasprintf(&config.bar_id, "%.*s", (int)_len, val);
+ return 0; /* Stop parsing */
+}
+
+/*
+ * Start parsing the received bar configuration list. The only usecase right
+ * now is to automatically get the first bar id.
+ *
+ */
+void parse_get_first_i3bar_config(char *json) {
+ yajl_callbacks configs_callbacks = {
+ .yajl_string = i3bar_config_string_cb,
+ };
+ yajl_handle handle = yajl_alloc(&configs_callbacks, NULL, NULL);
+ yajl_parse(handle, (const unsigned char *)json, strlen(json));
yajl_free(handle);
}
diff --git a/i3bar/src/ipc.c b/i3bar/src/ipc.c
index f2c105a..06ddf9b 100644
--- a/i3bar/src/ipc.c
+++ b/i3bar/src/ipc.c
@@ -85,6 +85,20 @@
*
*/
static void got_bar_config(char *reply) {
+ if (!config.bar_id) {
+ DLOG("Received bar list \"%s\"\n", reply);
+ parse_get_first_i3bar_config(reply);
+
+ if (!config.bar_id) {
+ ELOG("No bar configuration found, please configure a bar block in your i3 config file.\n");
+ exit(EXIT_FAILURE);
+ }
+
+ LOG("Using first bar config: %s. Use --bar_id to manually select a different bar configuration.\n", config.bar_id);
+ i3_send_msg(I3_IPC_MESSAGE_TYPE_GET_BAR_CONFIG, config.bar_id);
+ return;
+ }
+
DLOG("Received bar config \"%s\"\n", reply);
/* We initiate the main function by requesting infos about the outputs and
* workspaces. Everything else (creating the bars, showing the right workspace-
@@ -141,9 +155,6 @@
static void got_output_event(char *event) {
DLOG("Got output event!\n");
i3_send_msg(I3_IPC_MESSAGE_TYPE_GET_OUTPUTS, NULL);
- if (!config.disable_ws) {
- i3_send_msg(I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL);
- }
}
/*
@@ -154,6 +165,18 @@
DLOG("Got mode event!\n");
parse_mode_json(event);
draw_bars(false);
+}
+
+static bool strings_differ(char *a, char *b) {
+ const bool a_null = (a == NULL);
+ const bool b_null = (b == NULL);
+ if (a_null != b_null) {
+ return true;
+ }
+ if (a_null && b_null) {
+ return false;
+ }
+ return strcmp(a, b) != 0;
}
/*
@@ -176,8 +199,11 @@
/* update the configuration with the received settings */
DLOG("Received bar config update \"%s\"\n", event);
- char *old_command = config.command ? sstrdup(config.command) : NULL;
+
+ char *old_command = config.command;
+ config.command = NULL;
bar_display_mode_t old_mode = config.hide_on_modifier;
+
parse_config_json(event);
if (old_mode != config.hide_on_modifier) {
reconfig_windows(true);
@@ -188,8 +214,9 @@
init_colors(&(config.colors));
/* restart status command process */
- if (old_command && strcmp(old_command, config.command) != 0) {
+ if (strings_differ(old_command, config.command)) {
kill_child();
+ clear_statusline(&statusline_head, true);
start_child(config.command);
}
free(old_command);
@@ -328,13 +355,12 @@
* socket_path must be a valid path to the ipc_socket of i3
*
*/
-int init_connection(const char *socket_path) {
+void init_connection(const char *socket_path) {
sock_path = socket_path;
int sockfd = ipc_connect(socket_path);
i3_connection = smalloc(sizeof(ev_io));
ev_io_init(i3_connection, &got_data, sockfd, EV_READ);
ev_io_start(main_loop, i3_connection);
- return 1;
}
/*
diff --git a/i3bar/src/main.c b/i3bar/src/main.c
index 4e72934..4e93bb0 100644
--- a/i3bar/src/main.c
+++ b/i3bar/src/main.c
@@ -56,9 +56,9 @@
}
static void print_usage(char *elf_name) {
- printf("Usage: %s -b bar_id [-s sock_path] [-t] [-h] [-v]\n", elf_name);
+ printf("Usage: %s [-b bar_id] [-s sock_path] [-t] [-h] [-v] [-V]\n", elf_name);
printf("\n");
- printf("-b, --bar_id \tBar ID for which to get the configuration\n");
+ printf("-b, --bar_id \tBar ID for which to get the configuration, defaults to the first bar from the i3 config\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");
@@ -128,17 +128,12 @@
break;
default:
print_usage(argv[0]);
- exit(EXIT_SUCCESS);
+ exit(EXIT_FAILURE);
break;
}
}
- if (!config.bar_id) {
- /* TODO: maybe we want -f which will automatically ask i3 for the first
- * configured bar (and error out if there are too many)? */
- ELOG("No bar_id passed. Please let i3 start i3bar or specify --bar_id\n");
- exit(EXIT_FAILURE);
- }
+ LOG("i3bar version " I3_VERSION "\n");
main_loop = ev_default_loop(0); /* needed in init_xcb_early */
char *atom_sock_path = init_xcb_early();
@@ -166,10 +161,13 @@
init_dpi();
init_outputs();
- if (init_connection(socket_path)) {
- /* Request the bar configuration. When it arrives, we fill the config array. */
- i3_send_msg(I3_IPC_MESSAGE_TYPE_GET_BAR_CONFIG, config.bar_id);
- }
+
+ init_connection(socket_path);
+ /* Request the bar configuration. When it arrives, we fill the config
+ * array. In case that config.bar_id is empty, we will receive a list of
+ * available configs and then request the configuration for the first bar.
+ * See got_bar_config for more. */
+ i3_send_msg(I3_IPC_MESSAGE_TYPE_GET_BAR_CONFIG, config.bar_id);
free(socket_path);
/* We listen to SIGTERM/QUIT/INT and try to exit cleanly, by stopping the main loop.
diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c
index 89b3410..7010f90 100644
--- a/i3bar/src/xcb.c
+++ b/i3bar/src/xcb.c
@@ -203,7 +203,7 @@
if (block->border)
render->width += logical_px(block->border_left + block->border_right);
- /* Compute offset and append for text aligment in min_width. */
+ /* Compute offset and append for text alignment in min_width. */
if (block->min_width <= render->width) {
render->x_offset = 0;
render->x_append = 0;
@@ -1540,7 +1540,6 @@
free_font();
xcb_free_cursor(xcb_connection, cursor);
- xcb_flush(xcb_connection);
xcb_aux_sync(xcb_connection);
xcb_disconnect(xcb_connection);
diff --git a/include/all.h b/include/all.h
index 5511194..5941b5e 100644
--- a/include/all.h
+++ b/include/all.h
@@ -56,9 +56,9 @@
#include "render.h"
#include "window.h"
#include "match.h"
-#include "cmdparse.h"
#include "xcursor.h"
#include "resize.h"
+#include "tiling_drag.h"
#include "sighandler.h"
#include "move.h"
#include "output.h"
diff --git a/include/cmdparse.h b/include/cmdparse.h
deleted file mode 100644
index a2cbb17..0000000
--- a/include/cmdparse.h
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * vim:ts=4:sw=4:expandtab
- *
- * i3 - an improved dynamic tiling window manager
- * © 2009 Michael Stapelberg and contributors (see also: LICENSE)
- *
- * cmdparse.y: the parser for commands you send to i3 (or bind on keys)
- *
- */
-#pragma once
-
-#include
-
-char *parse_cmd(const char *new);
diff --git a/include/commands.h b/include/commands.h
index 4b2f99b..2ae2643 100644
--- a/include/commands.h
+++ b/include/commands.h
@@ -138,7 +138,7 @@
* Implementation of 'move [window|container] [to] output '.
*
*/
-void cmd_move_con_to_output(I3_CMD, const char *name);
+void cmd_move_con_to_output(I3_CMD, const char *name, bool move_workspace);
/**
* Implementation of 'move [window|container] [to] mark '.
@@ -153,12 +153,6 @@
void cmd_floating(I3_CMD, const char *floating_mode);
/**
- * Implementation of 'move workspace to [output] '.
- *
- */
-void cmd_move_workspace_to_output(I3_CMD, const char *name);
-
-/**
* Implementation of 'split v|h|t|vertical|horizontal|toggle'.
*
*/
@@ -343,3 +337,9 @@
*
*/
void cmd_gaps(I3_CMD, const char *type, const char *scope, const char *mode, const char *value);
+
+/**
+ * Implementation of 'title_window_icon ' and 'title_window_icon '
+ *
+ */
+void cmd_title_window_icon(I3_CMD, const char *enable, int padding);
diff --git a/include/commands_parser.h b/include/commands_parser.h
index 31333af..7e1c520 100644
--- a/include/commands_parser.h
+++ b/include/commands_parser.h
@@ -14,7 +14,7 @@
#include
/**
- * Holds an intermediate represenation of the result of a call to any command.
+ * Holds an intermediate representation of the result of a call to any command.
* When calling parse_command("floating enable, border none"), the parser will
* internally use this struct when calling cmd_floating and cmd_border.
*/
diff --git a/include/con.h b/include/con.h
index 59e34ee..6577c2d 100644
--- a/include/con.h
+++ b/include/con.h
@@ -207,6 +207,14 @@
*
*/
Con *con_by_mark(const char *mark);
+
+/**
+ * Start from a container and traverse the transient_for linked list. Returns
+ * true if target window is found in the list. Protects againsts potential
+ * cycles.
+ *
+ */
+bool con_find_transient_for_window(Con *start, xcb_window_t target);
/**
* Returns true if and only if the given containers holds the mark.
@@ -363,6 +371,7 @@
*/
bool con_move_to_output_name(Con *con, const char *name, bool fix_coordinates);
+bool con_move_to_target(Con *con, Con *target);
/**
* Moves the given container to the given mark.
*
@@ -442,7 +451,7 @@
* floating window.
*
*/
-void con_set_border_style(Con *con, int border_style, int border_width);
+void con_set_border_style(Con *con, border_style_t border_style, int border_width);
/**
* This function changes the layout of a given container. Use it to handle
diff --git a/include/config_directives.h b/include/config_directives.h
index e8d4bd2..1a996cf 100644
--- a/include/config_directives.h
+++ b/include/config_directives.h
@@ -39,6 +39,7 @@
CFGFUN(criteria_add, const char *ctype, const char *cvalue);
CFGFUN(criteria_pop_state);
+CFGFUN(include, const char *pattern);
CFGFUN(font, const char *font);
CFGFUN(exec, const char *exectype, const char *no_startup_id, const char *command);
CFGFUN(for_window, const char *command);
@@ -67,6 +68,7 @@
CFGFUN(no_focus);
CFGFUN(ipc_socket, const char *path);
CFGFUN(ipc_kill_timeout, const long timeout_ms);
+CFGFUN(tiling_drag, const char *value);
CFGFUN(restart_state, const char *path);
CFGFUN(popup_during_fullscreen, const char *value);
CFGFUN(color, const char *colorclass, const char *border, const char *background, const char *text, const char *indicator, const char *child_border);
diff --git a/include/config_parser.h b/include/config_parser.h
index 009538f..82c5709 100644
--- a/include/config_parser.h
+++ b/include/config_parser.h
@@ -16,28 +16,84 @@
SLIST_HEAD(variables_head, Variable);
extern pid_t config_error_nagbar_pid;
+struct stack_entry {
+ /* Just a pointer, not dynamically allocated. */
+ const char *identifier;
+ enum {
+ STACK_STR = 0,
+ STACK_LONG = 1,
+ } type;
+ union {
+ char *str;
+ long num;
+ } val;
+};
+
+struct stack {
+ struct stack_entry stack[10];
+};
+
+struct parser_ctx {
+ bool use_nagbar;
+ bool assume_v4;
+
+ int state;
+ Match current_match;
+
+ /* A list which contains the states that lead to the current state, e.g.
+ * INITIAL, WORKSPACE_LAYOUT.
+ * When jumping back to INITIAL, statelist_idx will simply be set to 1
+ * (likewise for other states, e.g. MODE or BAR).
+ * This list is used to process the nearest error token. */
+ int statelist[10];
+ /* NB: statelist_idx points to where the next entry will be inserted */
+ int statelist_idx;
+
+ /*******************************************************************************
+ * The (small) stack where identified literals are stored during the parsing
+ * of a single config directive (like $workspace).
+ ******************************************************************************/
+ struct stack *stack;
+
+ struct variables_head variables;
+
+ bool has_errors;
+};
+
/**
- * An intermediate reprsentation of the result of a parse_config call.
+ * An intermediate representation of the result of a parse_config call.
* Currently unused, but the JSON output will be useful in the future when we
* implement a config parsing IPC command.
*
*/
struct ConfigResultIR {
- /* The JSON generator to append a reply to. */
- yajl_gen json_gen;
+ struct parser_ctx *ctx;
/* 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. */
int next_state;
+
+ /* Whether any error happened while processing this config directive. */
+ bool has_errors;
};
-
-struct ConfigResultIR *parse_config(const char *input, struct context *context);
/**
* launch nagbar to indicate errors in the configuration file.
*/
void start_config_error_nagbar(const char *configpath, bool has_errors);
+
+/**
+ * Releases the memory of all variables in ctx.
+ *
+ */
+void free_variables(struct parser_ctx *ctx);
+
+typedef enum {
+ PARSE_FILE_FAILED = -1,
+ PARSE_FILE_SUCCESS = 0,
+ PARSE_FILE_CONFIG_ERRORS = 1,
+} parse_file_result_t;
/**
* Parses the given file by first replacing the variables, then calling
@@ -47,4 +103,4 @@
* parsing.
*
*/
-bool parse_file(const char *f, bool use_nagbar);
+parse_file_result_t parse_file(struct parser_ctx *ctx, const char *f, IncludedFile *included_file);
diff --git a/include/configuration.h b/include/configuration.h
index eff28e6..8d3b60d 100644
--- a/include/configuration.h
+++ b/include/configuration.h
@@ -14,7 +14,9 @@
#include "queue.h"
#include "i3.h"
-
+#include "tiling_drag.h"
+
+typedef struct IncludedFile IncludedFile;
typedef struct Config Config;
typedef struct Barconfig Barconfig;
extern char *current_configpath;
@@ -22,6 +24,7 @@
extern Config config;
extern SLIST_HEAD(modes_head, Mode) modes;
extern TAILQ_HEAD(barconfig_head, Barconfig) barconfigs;
+extern TAILQ_HEAD(includedfiles_head, IncludedFile) included_files;
/**
* Used during the config file lexing/parsing to keep the state of the lexer
@@ -67,6 +70,18 @@
char *next_match;
SLIST_ENTRY(Variable) variables;
+};
+
+/**
+ * List entry struct for an included file.
+ *
+ */
+struct IncludedFile {
+ char *path;
+ char *raw_contents;
+ char *variable_replaced_contents;
+
+ TAILQ_ENTRY(IncludedFile) files;
};
/**
@@ -224,9 +239,11 @@
color_t background;
struct Colortriple focused;
struct Colortriple focused_inactive;
+ struct Colortriple focused_tab_title;
struct Colortriple unfocused;
struct Colortriple urgent;
struct Colortriple placeholder;
+ bool got_focused_tab_title;
} client;
struct config_bar {
struct Colortriple focused;
@@ -250,6 +267,8 @@
/* The number of currently parsed barconfigs */
int number_barconfigs;
+ tiling_drag_t tiling_drag;
+
/* Gap sizes */
gaps_t gaps;
diff --git a/include/data.h b/include/data.h
index 9ea09a8..67a573c 100644
--- a/include/data.h
+++ b/include/data.h
@@ -9,12 +9,15 @@
*/
#pragma once
+#define PCRE2_CODE_UNIT_WIDTH 8
+
#define SN_API_NOT_YET_FROZEN 1
#include
#include
-#include
+#include
#include
+#include
#include "queue.h"
@@ -59,9 +62,11 @@
VERT } orientation_t;
typedef enum { BEFORE,
AFTER } position_t;
-typedef enum { BS_NORMAL = 0,
- BS_NONE = 1,
- BS_PIXEL = 2 } border_style_t;
+typedef enum {
+ BS_NONE = 0,
+ BS_PIXEL = 1,
+ BS_NORMAL = 2,
+} border_style_t;
/** parameter to specify whether tree_close_internal() and x_window_kill() should kill
* only this specific window or the whole X11 client */
@@ -266,8 +271,7 @@
*/
struct regex {
char *pattern;
- pcre *regex;
- pcre_extra *extra;
+ pcre2_code *regex;
};
/**
@@ -433,6 +437,9 @@
* for_window. */
char *role;
+ /** WM_CLIENT_MACHINE of the window */
+ char *machine;
+
/** Flag to force re-rendering the decoration upon changes */
bool name_x_changed;
@@ -486,6 +493,9 @@
/* aspect ratio from WM_NORMAL_HINTS (MPlayer uses this for example) */
double min_aspect_ratio;
double max_aspect_ratio;
+
+ /** Window icon, as Cairo surface */
+ cairo_surface_t *icon;
/** The window has a nonrectangular shape. */
bool shaped;
@@ -519,6 +529,7 @@
struct regex *mark;
struct regex *window_role;
struct regex *workspace;
+ struct regex *machine;
xcb_atom_t window_type;
enum {
U_DONTCHECK = -1,
@@ -541,6 +552,7 @@
WM_FLOATING_USER,
WM_FLOATING } window_mode;
Con *con_id;
+ bool match_all_windows;
/* Where the window looking for a match should be inserted:
*
@@ -674,6 +686,11 @@
/** The format with which the window's name should be displayed. */
char *title_format;
+ /** Whether the window icon should be displayed, and with what padding. -1
+ * means display no window icon (default behavior), 0 means display without
+ * any padding, 1 means display with 1 pixel of padding and so on. */
+ int window_icon_padding;
+
/* a sticky-group is an identifier which bundles several containers to a
* group. The contents are shared between all of them, that is they are
* displayed on whichever of the containers is currently visible */
@@ -728,7 +745,16 @@
* layout in workspace_layout and creates a new split container with that
* layout whenever a new container is attached to the workspace. */
layout_t layout, last_split_layout, workspace_layout;
+
border_style_t border_style;
+ /* When the border style of a con changes because of motif hints, we don't
+ * want to set more decoration that the user wants. The user's preference is determined by these:
+ * 1. For new tiling windows, as set by `default_border`
+ * 2. For new floating windows, as set by `default_floating_border`
+ * 3. For all windows that the user runs the `border` command, whatever is
+ * the result of that command for that window. */
+ border_style_t max_user_border_style;
+
/** floating? (= not in tiling layout) This cannot be simply a bool
* because we want to keep track of whether the status was set by the
* application (by setting _NET_WM_WINDOW_TYPE appropriately) or by the
diff --git a/include/display_version.h b/include/display_version.h
index d8dac30..6996038 100644
--- a/include/display_version.h
+++ b/include/display_version.h
@@ -14,7 +14,7 @@
/**
* Connects to i3 to find out the currently running version. Useful since it
* might be different from the version compiled into this binary (maybe the
- * user didn’t correctly install i3 or forgot te restart it).
+ * user didn’t correctly install i3 or forgot to restart it).
*
* The output looks like this:
* Running i3 version: 4.2-202-gb8e782c (2012-08-12, branch "next") (pid 14804)
diff --git a/include/i3-atoms_rest.xmacro.h b/include/i3-atoms_rest.xmacro.h
index 75a5f23..fa915fd 100644
--- a/include/i3-atoms_rest.xmacro.h
+++ b/include/i3-atoms_rest.xmacro.h
@@ -3,6 +3,7 @@
xmacro(_NET_WM_USER_TIME) \
xmacro(_NET_STARTUP_ID) \
xmacro(_NET_WORKAREA) \
+xmacro(_NET_WM_ICON) \
xmacro(WM_PROTOCOLS) \
xmacro(WM_DELETE_WINDOW) \
xmacro(UTF8_STRING) \
@@ -15,8 +16,10 @@
xmacro(I3_SYNC) \
xmacro(I3_SHMLOG_PATH) \
xmacro(I3_PID) \
+xmacro(I3_LOG_STREAM_SOCKET_PATH) \
xmacro(I3_FLOATING_WINDOW) \
xmacro(_NET_REQUEST_FRAME_EXTENTS) \
xmacro(_NET_FRAME_EXTENTS) \
xmacro(_MOTIF_WM_HINTS) \
-xmacro(WM_CHANGE_STATE)
+xmacro(WM_CHANGE_STATE) \
+xmacro(MANAGER)
diff --git a/include/i3.h b/include/i3.h
index 2c550fa..4e2eb5c 100644
--- a/include/i3.h
+++ b/include/i3.h
@@ -39,6 +39,7 @@
/** The number of file descriptors passed via socket activation. */
extern int listen_fds;
extern int conn_screen;
+extern xcb_atom_t wm_sn;
/**
* The EWMH support window that is used to indicate that an EWMH-compliant
* window manager is present. This window is created when i3 starts and
@@ -55,7 +56,6 @@
extern SnDisplay *sndisplay;
extern xcb_key_symbols_t *keysyms;
extern char **start_argv;
-extern Display *xlibdpy, *xkbdpy;
extern int xkb_current_group;
extern TAILQ_HEAD(bindings_head, Binding) *bindings;
extern const char *current_binding_mode;
diff --git a/include/ipc.h b/include/ipc.h
index 0d19daf..732fa9b 100644
--- a/include/ipc.h
+++ b/include/ipc.h
@@ -81,13 +81,6 @@
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.
- *
- */
-int ipc_create_socket(const char *filename);
-
-/**
* Sends the specified event to all IPC clients which are currently connected
* and subscribed to this kind of event.
*
diff --git a/include/libi3.h b/include/libi3.h
index c0b9ddb..005167c 100644
--- a/include/libi3.h
+++ b/include/libi3.h
@@ -15,6 +15,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -294,6 +295,12 @@
int ipc_connect(const char *socket_path);
/**
+ * Connects to the socket at the given path with no fallback paths. Returns
+ * -1 if connect() fails and die()s for other errors.
+ */
+int ipc_connect_impl(const char *socket_path);
+
+/**
* Formats a message (payload) of the given size and type and sends it to i3 via
* the given socket file descriptor.
*
@@ -445,18 +452,13 @@
* specified coordinates (from the top left corner of the leftmost, uppermost
* glyph) and using the provided gc.
*
+ * The given cairo surface must refer to the specified X drawable.
+ *
* Text must be specified as an i3String.
*
*/
void draw_text(i3String *text, xcb_drawable_t drawable, xcb_gcontext_t gc,
- xcb_visualtype_t *visual, int x, int y, int max_width);
-
-/**
- * ASCII version of draw_text to print static strings.
- *
- */
-void draw_text_ascii(const char *text, xcb_drawable_t drawable,
- xcb_gcontext_t gc, int x, int y, int max_width);
+ cairo_surface_t *surface, int x, int y, int max_width);
/**
* Predict the text width in pixels for the given text. Text must be
@@ -571,8 +573,6 @@
/* A classic XCB graphics context. */
xcb_gcontext_t gc;
- xcb_visualtype_t *visual_type;
-
int width;
int height;
@@ -619,6 +619,11 @@
void draw_util_text(i3String *text, surface_t *surface, color_t fg_color, color_t bg_color, int x, int y, int max_width);
/**
+ * Draw the given image using libi3.
+ */
+void draw_util_image(cairo_surface_t *image, surface_t *surface, int x, int y, int width, int height);
+
+/**
* Draws a filled rectangle.
* This function is a convenience wrapper and takes care of flushing the
* surface as well as restoring the cairo state.
@@ -638,3 +643,46 @@
*/
void draw_util_copy_surface(surface_t *src, surface_t *dest, double src_x, double src_y,
double dest_x, double dest_y, double width, double height);
+
+/**
+ * Puts the given socket file descriptor into non-blocking mode or dies if
+ * setting O_NONBLOCK failed. Non-blocking sockets are a good idea for our
+ * IPC model because we should by no means block the window manager.
+ *
+ */
+void set_nonblock(int sockfd);
+
+/**
+ * Creates the UNIX domain socket at the given path, sets it to non-blocking
+ * mode, bind()s and listen()s on it.
+ *
+ * The full path to the socket is stored in the char* that out_socketpath points
+ * to.
+ *
+ */
+int create_socket(const char *filename, char **out_socketpath);
+
+/**
+ * Checks if the given path exists by calling stat().
+ *
+ */
+bool path_exists(const char *path);
+
+/**
+ * Grab a screenshot of the screen's root window and set it as the wallpaper.
+ */
+void set_screenshot_as_wallpaper(xcb_connection_t *conn, xcb_screen_t *screen);
+
+/**
+ * Test whether the screen's root window has a background set.
+ *
+ * This opens & closes a window and test whether the root window still shows the
+ * content of the window.
+ */
+bool is_background_set(xcb_connection_t *conn, xcb_screen_t *screen);
+
+/**
+ * Reports whether str represents the enabled state (1, yes, true, …).
+ *
+ */
+bool boolstr(const char *str);
diff --git a/include/log.h b/include/log.h
index c275844..3a52c64 100644
--- a/include/log.h
+++ b/include/log.h
@@ -10,6 +10,7 @@
#pragma once
#include
+#include
/* We will include libi3.h which define its own version of LOG, ELOG.
* We want *our* version, so we undef the libi3 one. */
@@ -31,6 +32,7 @@
extern char *errorfilename;
extern char *shmlogname;
extern int shmlog_size;
+extern char *current_log_stream_socket_path;
/**
* Initializes logging by creating an error logfile in /tmp (or
@@ -100,3 +102,5 @@
* failures. This function is invoked automatically when exiting.
*/
void purge_zerobyte_logfile(void);
+
+void log_new_client(EV_P_ struct ev_io *w, int revents);
diff --git a/include/randr.h b/include/randr.h
index ae6a20a..6fd7ea9 100644
--- a/include/randr.h
+++ b/include/randr.h
@@ -49,12 +49,6 @@
*
*/
void init_ws_for_output(Output *output);
-
-/**
- * Initializes the specified output, assigning the specified workspace to it.
- *
- */
-//void initialize_output(xcb_connection_t *conn, Output *output, Workspace *workspace);
/**
* (Re-)queries the outputs via RandR and stores them in the list of outputs.
diff --git a/include/shmlog.h b/include/shmlog.h
index 7b7e133..a30852e 100644
--- a/include/shmlog.h
+++ b/include/shmlog.h
@@ -11,10 +11,6 @@
#pragma once
#include
-
-#if !defined(__OpenBSD__)
-#include
-#endif
/* Default shmlog size if not set by user. */
extern const int default_shmlog_size;
@@ -39,11 +35,4 @@
* coincidentally be exactly the same as previously). Overflows can happen
* and don’t matter — clients use an equality check (==). */
uint32_t wrap_count;
-
-#if !defined(__OpenBSD__)
- /* pthread condvar which will be broadcasted whenever there is a new
- * message in the log. i3-dump-log uses this to implement -f (follow, like
- * tail -f) in an efficient way. */
- pthread_cond_t condvar;
-#endif
} i3_shmlog_header;
diff --git a/include/tiling_drag.h b/include/tiling_drag.h
new file mode 100644
index 0000000..3091b73
--- /dev/null
+++ b/include/tiling_drag.h
@@ -0,0 +1,35 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ * © 2009 Michael Stapelberg and contributors (see also: LICENSE)
+ *
+ * tiling_drag.h: Reposition tiled windows by dragging.
+ *
+ */
+#pragma once
+
+#include "all.h"
+
+/**
+ * Tiling drag initiation modes.
+ */
+typedef enum {
+ TILING_DRAG_OFF = 0,
+ TILING_DRAG_MODIFIER = 1,
+ TILING_DRAG_TITLEBAR = 2,
+ TILING_DRAG_MODIFIER_OR_TITLEBAR = 3
+} tiling_drag_t;
+
+/**
+ * Returns whether there currently are any drop targets.
+ * Used to only initiate a drag when there is something to drop onto.
+ *
+ */
+bool has_drop_targets(void);
+
+/**
+ * Initiates a mouse drag operation on a tiled window.
+ *
+ */
+void tiling_drag(Con *con, xcb_button_press_event_t *event, bool use_threshold);
diff --git a/include/util.h b/include/util.h
index 09ad941..8525b6d 100644
--- a/include/util.h
+++ b/include/util.h
@@ -183,3 +183,15 @@
*
*/
direction_t direction_from_orientation_position(orientation_t orientation, position_t position);
+
+/**
+ * Converts direction to a string representation.
+ *
+ */
+const char *direction_to_string(direction_t direction);
+
+/**
+ * Converts position to a string representation.
+ *
+ */
+const char *position_to_string(position_t position);
diff --git a/include/window.h b/include/window.h
index 6673e83..7b43ab1 100644
--- a/include/window.h
+++ b/include/window.h
@@ -94,4 +94,16 @@
* it is still in use by popular widget toolkits such as GTK+ and Java AWT.
*
*/
-void window_update_motif_hints(i3Window *win, xcb_get_property_reply_t *prop, border_style_t *motif_border_style);
+bool window_update_motif_hints(i3Window *win, xcb_get_property_reply_t *prop, border_style_t *motif_border_style);
+
+/**
+ * Updates the WM_CLIENT_MACHINE
+ *
+ */
+void window_update_machine(i3Window *win, xcb_get_property_reply_t *prop);
+
+/**
+ * Updates the _NET_WM_ICON
+ *
+ */
+void window_update_icon(i3Window *win, xcb_get_property_reply_t *prop);
diff --git a/include/workspace.h b/include/workspace.h
index 2193ed0..fe6d9f8 100644
--- a/include/workspace.h
+++ b/include/workspace.h
@@ -92,7 +92,7 @@
/**
* Returns true if the workspace is currently visible. Especially important for
- * multi-monitor environments, as they can have multiple currenlty active
+ * multi-monitor environments, as they can have multiple currently active
* workspaces.
*
*/
diff --git a/libi3/boolstr.c b/libi3/boolstr.c
new file mode 100644
index 0000000..0fa417d
--- /dev/null
+++ b/libi3/boolstr.c
@@ -0,0 +1,25 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ * © 2009 Michael Stapelberg and contributors (see also: LICENSE)
+ *
+ */
+#include "libi3.h"
+
+#include
+#include
+#include
+
+/*
+ * Reports whether str represents the enabled state (1, yes, true, …).
+ *
+ */
+bool boolstr(const char *str) {
+ return (strcasecmp(str, "1") == 0 ||
+ strcasecmp(str, "yes") == 0 ||
+ strcasecmp(str, "true") == 0 ||
+ strcasecmp(str, "on") == 0 ||
+ strcasecmp(str, "enable") == 0 ||
+ strcasecmp(str, "active") == 0);
+}
diff --git a/libi3/create_socket.c b/libi3/create_socket.c
new file mode 100644
index 0000000..d476f43
--- /dev/null
+++ b/libi3/create_socket.c
@@ -0,0 +1,81 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ * © 2009 Michael Stapelberg and contributors (see also: LICENSE)
+ *
+ */
+#include "libi3.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/*
+ * Creates the UNIX domain socket at the given path, sets it to non-blocking
+ * mode, bind()s and listen()s on it.
+ *
+ * The full path to the socket is stored in the char* that out_socketpath points
+ * to.
+ *
+ */
+int create_socket(const char *filename, char **out_socketpath) {
+ char *resolved = resolve_tilde(filename);
+ DLOG("Creating UNIX socket at %s\n", resolved);
+ char *copy = sstrdup(resolved);
+ const char *dir = dirname(copy);
+ if (!path_exists(dir)) {
+ mkdirp(dir, DEFAULT_DIR_MODE);
+ }
+ free(copy);
+
+ /* Check if the socket is in use by another process (this call does not
+ * succeed if the socket is stale / the owner already exited) */
+ int sockfd = ipc_connect_impl(resolved);
+ if (sockfd != -1) {
+ ELOG("Refusing to create UNIX socket at %s: Socket is already in use\n", resolved);
+ close(sockfd);
+ errno = EEXIST;
+ return -1;
+ }
+
+ /* Unlink the unix domain socket before */
+ unlink(resolved);
+
+ sockfd = socket(AF_LOCAL, SOCK_STREAM, 0);
+ if (sockfd < 0) {
+ perror("socket()");
+ free(resolved);
+ return -1;
+ }
+
+ (void)fcntl(sockfd, F_SETFD, FD_CLOEXEC);
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(struct sockaddr_un));
+ addr.sun_family = AF_LOCAL;
+ strncpy(addr.sun_path, resolved, sizeof(addr.sun_path) - 1);
+ if (bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) {
+ perror("bind()");
+ free(resolved);
+ return -1;
+ }
+
+ set_nonblock(sockfd);
+
+ if (listen(sockfd, 5) < 0) {
+ perror("listen()");
+ free(resolved);
+ return -1;
+ }
+
+ free(*out_socketpath);
+ *out_socketpath = resolved;
+ return sockfd;
+}
diff --git a/libi3/draw_util.c b/libi3/draw_util.c
index 313dc29..30eae65 100644
--- a/libi3/draw_util.c
+++ b/libi3/draw_util.c
@@ -35,9 +35,11 @@
void draw_util_surface_init(xcb_connection_t *conn, surface_t *surface, xcb_drawable_t drawable,
xcb_visualtype_t *visual, int width, int height) {
surface->id = drawable;
- surface->visual_type = ((visual == NULL) ? visual_type : visual);
surface->width = width;
surface->height = height;
+
+ if (visual == NULL)
+ visual = visual_type;
surface->gc = xcb_generate_id(conn);
xcb_void_cookie_t gc_cookie = xcb_create_gc_checked(conn, surface->gc, surface->id, 0, NULL);
@@ -45,9 +47,10 @@
xcb_generic_error_t *error = xcb_request_check(conn, gc_cookie);
if (error != NULL) {
ELOG("Could not create graphical context. Error code: %d. Please report this bug.\n", error->error_code);
- }
-
- surface->surface = cairo_xcb_surface_create(conn, surface->id, surface->visual_type, width, height);
+ free(error);
+ }
+
+ surface->surface = cairo_xcb_surface_create(conn, surface->id, visual, width, height);
surface->cr = cairo_create(surface->surface);
}
@@ -56,6 +59,19 @@
*
*/
void draw_util_surface_free(xcb_connection_t *conn, surface_t *surface) {
+ cairo_status_t status = CAIRO_STATUS_SUCCESS;
+ if (surface->cr) {
+ status = cairo_status(surface->cr);
+ }
+ if (status != CAIRO_STATUS_SUCCESS) {
+ LOG("Found cairo context in an error status while freeing, error %d is %s",
+ status, cairo_status_to_string(status));
+ }
+
+ /* NOTE: This function is also called on uninitialised surface_t instances.
+ * The x11 error from xcb_free_gc(conn, XCB_NONE) is silently ignored
+ * elsewhere.
+ */
xcb_free_gc(conn, surface->gc);
cairo_surface_destroy(surface->surface);
cairo_destroy(surface->cr);
@@ -133,10 +149,34 @@
CAIRO_SURFACE_FLUSH(surface->surface);
set_font_colors(surface->gc, fg_color, bg_color);
- draw_text(text, surface->id, surface->gc, surface->visual_type, x, y, max_width);
+ draw_text(text, surface->id, surface->gc, surface->surface, x, y, max_width);
/* Notify cairo that we (possibly) used another way to draw on the surface. */
cairo_surface_mark_dirty(surface->surface);
+}
+
+/**
+ * Draw the given image using libi3.
+ * This function is a convenience wrapper and takes care of flushing the
+ * surface as well as restoring the cairo state.
+ *
+ */
+void draw_util_image(cairo_surface_t *image, surface_t *surface, int x, int y, int width, int height) {
+ RETURN_UNLESS_SURFACE_INITIALIZED(surface);
+
+ cairo_save(surface->cr);
+
+ cairo_translate(surface->cr, x, y);
+
+ const int src_width = cairo_image_surface_get_width(image);
+ const int src_height = cairo_image_surface_get_height(image);
+ double scale = MIN((double)width / src_width, (double)height / src_height);
+ cairo_scale(surface->cr, scale, scale);
+
+ cairo_set_source_surface(surface->cr, image, 0, 0);
+ cairo_paint(surface->cr);
+
+ cairo_restore(surface->cr);
}
/*
diff --git a/libi3/font.c b/libi3/font.c
index 477509d..10abad0 100644
--- a/libi3/font.c
+++ b/libi3/font.c
@@ -83,12 +83,10 @@
*
*/
static void draw_text_pango(const char *text, size_t text_len,
- xcb_drawable_t drawable, xcb_visualtype_t *visual, int x, int y,
- int max_width, bool pango_markup) {
+ xcb_drawable_t drawable, cairo_surface_t *surface,
+ int x, int y, int max_width, bool pango_markup) {
/* Create the Pango layout */
/* root_visual_type is cached in load_pango_font */
- cairo_surface_t *surface = cairo_xcb_surface_create(conn, drawable,
- visual, x + max_width, y + savedFont->height);
cairo_t *cr = cairo_create(surface);
PangoLayout *layout = create_layout_with_dpi(cr);
gint height;
@@ -116,7 +114,6 @@
/* Free resources */
g_object_unref(layout);
cairo_destroy(cr);
- cairo_surface_destroy(surface);
}
/*
@@ -164,7 +161,7 @@
font.type = FONT_TYPE_NONE;
font.pattern = NULL;
- /* No XCB connction, return early because we're just validating the
+ /* No XCB connection, return early because we're just validating the
* configuration file. */
if (conn == NULL) {
return font;
@@ -360,11 +357,8 @@
*
*/
void draw_text(i3String *text, xcb_drawable_t drawable, xcb_gcontext_t gc,
- xcb_visualtype_t *visual, int x, int y, int max_width) {
+ cairo_surface_t *surface, int x, int y, int max_width) {
assert(savedFont != NULL);
- if (visual == NULL) {
- visual = root_visual_type;
- }
switch (savedFont->type) {
case FONT_TYPE_NONE:
@@ -377,42 +371,7 @@
case FONT_TYPE_PANGO:
/* Render the text using Pango */
draw_text_pango(i3string_as_utf8(text), i3string_get_num_bytes(text),
- drawable, visual, x, y, max_width, i3string_is_markup(text));
- return;
- }
-}
-
-/*
- * ASCII version of draw_text to print static strings.
- *
- */
-void draw_text_ascii(const char *text, xcb_drawable_t drawable,
- xcb_gcontext_t gc, int x, int y, int max_width) {
- assert(savedFont != NULL);
-
- switch (savedFont->type) {
- case FONT_TYPE_NONE:
- /* Nothing to do */
- return;
- case FONT_TYPE_XCB: {
- size_t text_len = strlen(text);
- if (text_len > 255) {
- /* The text is too long to draw it directly to X */
- i3String *str = i3string_from_utf8(text);
- draw_text(str, drawable, gc, NULL, x, y, max_width);
- i3string_free(str);
- } else {
- /* X11 coordinates for fonts start at the baseline */
- int pos_y = y + savedFont->specific.xcb.info->font_ascent;
-
- xcb_image_text_8(conn, text_len, drawable, gc, x, pos_y, text);
- }
- break;
- }
- case FONT_TYPE_PANGO:
- /* Render the text using Pango */
- draw_text_pango(text, strlen(text),
- drawable, root_visual_type, x, y, max_width, false);
+ drawable, surface, x, y, max_width, i3string_is_markup(text));
return;
}
}
diff --git a/libi3/get_config_path.c b/libi3/get_config_path.c
index 4909e11..1f2b4c0 100644
--- a/libi3/get_config_path.c
+++ b/libi3/get_config_path.c
@@ -10,15 +10,6 @@
#include
#include
#include
-
-/*
- * Checks if the given path exists by calling stat().
- *
- */
-static bool path_exists(const char *path) {
- struct stat buf;
- return (stat(path, &buf) == 0);
-}
/*
* Get the path of the first configuration file found. If override_configpath is
diff --git a/libi3/get_process_filename.c b/libi3/get_process_filename.c
index 21429ec..d29f8db 100644
--- a/libi3/get_process_filename.c
+++ b/libi3/get_process_filename.c
@@ -27,15 +27,14 @@
char *tmp;
sasprintf(&tmp, "%s/i3", dir);
dir = tmp;
- struct stat buf;
- if (stat(dir, &buf) != 0) {
- if (mkdir(dir, 0700) == -1) {
- warn("Could not mkdir(%s)", dir);
- errx(EXIT_FAILURE, "Check permissions of $XDG_RUNTIME_DIR = '%s'",
- getenv("XDG_RUNTIME_DIR"));
- perror("mkdir()");
- return NULL;
- }
+ /* mkdirp() should prevent race between multiple i3 instances started
+ * in parallel from causing problem */
+ if (mkdirp(dir, 0700) == -1) {
+ warn("Could not mkdirp(%s)", dir);
+ errx(EXIT_FAILURE, "Check permissions of $XDG_RUNTIME_DIR = '%s'",
+ getenv("XDG_RUNTIME_DIR"));
+ perror("mkdirp()");
+ return NULL;
}
} else {
/* If not, we create a (secure) temp directory using the template
diff --git a/libi3/ipc_connect.c b/libi3/ipc_connect.c
index 871fe08..5da9f12 100644
--- a/libi3/ipc_connect.c
+++ b/libi3/ipc_connect.c
@@ -13,6 +13,7 @@
#include
#include
#include
+#include
/*
* Connects to the i3 IPC socket and returns the file descriptor for the
@@ -39,6 +40,20 @@
path = sstrdup("/tmp/i3-ipc.sock");
}
+ int sockfd = ipc_connect_impl(path);
+ if (sockfd < 0) {
+ err(EXIT_FAILURE, "Could not connect to i3 on socket %s", path);
+ }
+ free(path);
+ return sockfd;
+}
+
+/**
+ * Connects to the socket at the given path with no fallback paths. Returns
+ * -1 if connect() fails and die()s for other errors.
+ *
+ */
+int ipc_connect_impl(const char *socket_path) {
int sockfd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (sockfd == -1)
err(EXIT_FAILURE, "Could not create socket");
@@ -48,9 +63,10 @@
struct sockaddr_un addr;
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_LOCAL;
- strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
- if (connect(sockfd, (const struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0)
- err(EXIT_FAILURE, "Could not connect to i3 on socket %s", path);
- free(path);
+ strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
+ if (connect(sockfd, (const struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) {
+ close(sockfd);
+ return -1;
+ }
return sockfd;
}
diff --git a/libi3/is_background_set.c b/libi3/is_background_set.c
new file mode 100644
index 0000000..b3b6e6a
--- /dev/null
+++ b/libi3/is_background_set.c
@@ -0,0 +1,117 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ * © 2009 Michael Stapelberg and contributors (see also: LICENSE)
+ *
+ */
+#include "libi3.h"
+
+#include
+
+/**
+ * Find the region in the given window that is not covered by a mapped child
+ * window.
+ */
+static cairo_region_t *unobscured_region(xcb_connection_t *conn, xcb_window_t window,
+ uint16_t window_width, uint16_t window_height) {
+ cairo_rectangle_int_t rectangle;
+ cairo_region_t *region;
+
+ rectangle.x = 0;
+ rectangle.y = 0;
+ rectangle.width = window_width;
+ rectangle.height = window_height;
+ region = cairo_region_create_rectangle(&rectangle);
+
+ xcb_query_tree_reply_t *tree = xcb_query_tree_reply(conn, xcb_query_tree_unchecked(conn, window), NULL);
+ if (!tree) {
+ return region;
+ }
+
+ /* Get information about children */
+ uint16_t n_children = tree->children_len;
+ xcb_window_t *children = xcb_query_tree_children(tree);
+
+ xcb_get_geometry_cookie_t geometries[n_children];
+ xcb_get_window_attributes_cookie_t attributes[n_children];
+
+ for (int i = 0; i < n_children; i++) {
+ geometries[i] = xcb_get_geometry_unchecked(conn, children[i]);
+ attributes[i] = xcb_get_window_attributes_unchecked(conn, children[i]);
+ }
+
+ /* Remove every visible child from the region */
+ for (int i = 0; i < n_children; i++) {
+ xcb_get_geometry_reply_t *geom = xcb_get_geometry_reply(conn, geometries[i], NULL);
+ xcb_get_window_attributes_reply_t *attr = xcb_get_window_attributes_reply(conn, attributes[i], NULL);
+
+ if (geom && attr && attr->map_state == XCB_MAP_STATE_VIEWABLE) {
+ rectangle.x = geom->x;
+ rectangle.y = geom->y;
+ rectangle.width = geom->width;
+ rectangle.height = geom->height;
+ cairo_region_subtract_rectangle(region, &rectangle);
+ }
+
+ free(geom);
+ free(attr);
+ }
+
+ free(tree);
+ return region;
+}
+
+static void find_unobscured_pixel(xcb_connection_t *conn, xcb_window_t window,
+ uint16_t window_width, uint16_t window_height,
+ uint16_t *x, uint16_t *y) {
+ cairo_region_t *region = unobscured_region(conn, window, window_width, window_height);
+ if (cairo_region_num_rectangles(region) > 0) {
+ /* Return the top left pixel of the first rectangle */
+ cairo_rectangle_int_t rect;
+ cairo_region_get_rectangle(region, 0, &rect);
+ *x = rect.x;
+ *y = rect.y;
+ } else {
+ /* No unobscured area found */
+ *x = 0;
+ *y = 0;
+ }
+ cairo_region_destroy(region);
+}
+
+static uint32_t flicker_window_at(xcb_connection_t *conn, xcb_screen_t *screen, int16_t x, int16_t y, xcb_window_t window,
+ uint32_t pixel) {
+ xcb_create_window(conn, XCB_COPY_FROM_PARENT, window, screen->root, x, y, 10, 10,
+ 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT,
+ XCB_CW_BACK_PIXEL | XCB_CW_OVERRIDE_REDIRECT, (uint32_t[]){pixel, 1});
+ xcb_map_window(conn, window);
+ xcb_clear_area(conn, 0, window, 0, 0, 0, 0);
+ xcb_aux_sync(conn);
+ xcb_destroy_window(conn, window);
+
+ xcb_get_image_reply_t *img = xcb_get_image_reply(conn,
+ xcb_get_image_unchecked(conn, XCB_IMAGE_FORMAT_Z_PIXMAP, screen->root, x, y, 1, 1, ~0),
+ NULL);
+ uint32_t result = 0;
+ if (img) {
+ uint8_t *data = xcb_get_image_data(img);
+ uint8_t depth = img->depth;
+ for (int i = 0; i < MIN(depth, 4); i++) {
+ result = (result << 8) | data[i];
+ }
+ free(img);
+ }
+ return result;
+}
+
+bool is_background_set(xcb_connection_t *conn, xcb_screen_t *screen) {
+ uint16_t x, y;
+ find_unobscured_pixel(conn, screen->root, screen->width_in_pixels, screen->height_in_pixels, &x, &y);
+
+ xcb_window_t window = xcb_generate_id(conn);
+
+ uint32_t pixel1 = flicker_window_at(conn, screen, x, y, window, screen->black_pixel);
+ uint32_t pixel2 = flicker_window_at(conn, screen, x, y, window, screen->white_pixel);
+ return pixel1 == pixel2;
+}
diff --git a/libi3/nonblock.c b/libi3/nonblock.c
new file mode 100644
index 0000000..987c274
--- /dev/null
+++ b/libi3/nonblock.c
@@ -0,0 +1,21 @@
+#include "libi3.h"
+
+#include
+#include
+
+/*
+ * Puts the given socket file descriptor into non-blocking mode or dies if
+ * setting O_NONBLOCK failed. Non-blocking sockets are a good idea for our
+ * IPC model because we should by no means block the window manager.
+ *
+ */
+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");
+ }
+}
diff --git a/libi3/path_exists.c b/libi3/path_exists.c
new file mode 100644
index 0000000..5451c3b
--- /dev/null
+++ b/libi3/path_exists.c
@@ -0,0 +1,21 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ * © 2009 Michael Stapelberg and contributors (see also: LICENSE)
+ *
+ */
+#include "libi3.h"
+
+#include
+#include
+#include
+
+/*
+ * Checks if the given path exists by calling stat().
+ *
+ */
+bool path_exists(const char *path) {
+ struct stat buf;
+ return (stat(path, &buf) == 0);
+}
diff --git a/libi3/screenshot_wallpaper.c b/libi3/screenshot_wallpaper.c
new file mode 100644
index 0000000..2c115a9
--- /dev/null
+++ b/libi3/screenshot_wallpaper.c
@@ -0,0 +1,27 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ * © 2009 Michael Stapelberg and contributors (see also: LICENSE)
+ *
+ */
+#include "libi3.h"
+
+void set_screenshot_as_wallpaper(xcb_connection_t *conn, xcb_screen_t *screen) {
+ uint16_t width = screen->width_in_pixels;
+ uint16_t height = screen->height_in_pixels;
+ xcb_pixmap_t pixmap = xcb_generate_id(conn);
+ xcb_gcontext_t gc = xcb_generate_id(conn);
+
+ xcb_create_pixmap(conn, screen->root_depth, pixmap, screen->root, width, height);
+
+ xcb_create_gc(conn, gc, screen->root,
+ XCB_GC_FUNCTION | XCB_GC_PLANE_MASK | XCB_GC_FILL_STYLE | XCB_GC_SUBWINDOW_MODE,
+ (uint32_t[]){XCB_GX_COPY, ~0, XCB_FILL_STYLE_SOLID, XCB_SUBWINDOW_MODE_INCLUDE_INFERIORS});
+
+ xcb_copy_area(conn, screen->root, pixmap, gc, 0, 0, 0, 0, width, height);
+ xcb_change_window_attributes(conn, screen->root, XCB_CW_BACK_PIXMAP, (uint32_t[]){pixmap});
+ xcb_free_gc(conn, gc);
+ xcb_free_pixmap(conn, pixmap);
+ xcb_flush(conn);
+}
diff --git a/logo.svg b/logo.svg
index bb3e598..6f87d0c 100644
--- a/logo.svg
+++ b/logo.svg
@@ -1,5 +1,4 @@
-
+ inkscape:version="1.0.1 (c497b03c, 2020-09-10)"
+ sodipodi:docname="logo.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ version="1.1">
@@ -112,10 +112,10 @@
@@ -282,15 +282,17 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
- inkscape:cx="239.17981"
- inkscape:cy="807.75327"
+ inkscape:cx="164.67981"
+ inkscape:cy="285.75327"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1272"
- inkscape:window-height="950"
+ inkscape:window-height="856"
inkscape:window-x="24"
- inkscape:window-y="24" />
+ inkscape:window-y="22"
+ inkscape:document-rotation="0"
+ inkscape:window-maximized="0" />
@@ -495,7 +497,7 @@
::
-Use the specified X11 core font (use +xfontsel+ to chose a font).
+Use the specified X11 core font (use +xfontsel+ to choose a font).
-v::
Show version and exit.
diff --git a/man/i3-msg.man b/man/i3-msg.man
index ce9b476..dfe1f89 100644
--- a/man/i3-msg.man
+++ b/man/i3-msg.man
@@ -9,7 +9,7 @@
== SYNOPSIS
-i3-msg [-q] [-v] [-h] [-s socket] [-t type] [message]
+i3-msg [-q] [-v] [-h] [-s socket] [-t type] [-r] [message]
== OPTIONS
@@ -35,6 +35,10 @@
Instead of exiting right after receiving the first subscribed event,
wait indefinitely for all of them. Can only be used with "-t subscribe".
See the "subscribe" IPC message type below for details.
+
+*-r, --raw*::
+Display the raw JSON reply instead of pretty-printing errors (for commands) or
+displaying the top-level config file contents (for GET_CONFIG).
*message*::
Send ipc message, see below.
diff --git a/man/i3-nagbar.man b/man/i3-nagbar.man
index ef3a354..55696df 100644
--- a/man/i3-nagbar.man
+++ b/man/i3-nagbar.man
@@ -9,7 +9,7 @@
== SYNOPSIS
-i3-nagbar [-m ] [-b ] [-B ] [-t warning|error] [-f ] [-v]
+i3-nagbar [-m ] [-b ] [-B ] [-t warning|error] [-f ] [-v] [-p]
== OPTIONS
@@ -39,6 +39,10 @@
Same as above, but will execute the shell commands directly, without launching a
terminal emulator.
+*-p, --primary*::
+Always opens the i3-nagbar on the primary monitor. By default it opens on the
+focused monitor.
+
== DESCRIPTION
i3-nagbar is used by i3 to tell you about errors in your configuration file
diff --git a/man/i3-sensible-terminal.man b/man/i3-sensible-terminal.man
index bda5a72..1b83e07 100644
--- a/man/i3-sensible-terminal.man
+++ b/man/i3-sensible-terminal.man
@@ -23,20 +23,20 @@
* $TERMINAL (this is a non-standard variable)
* x-terminal-emulator (only present on Debian and derivatives)
+* mate-terminal
+* gnome-terminal
+* terminator
+* xfce4-terminal
* urxvt
* rxvt
* termit
-* terminator
* Eterm
* aterm
* uxterm
* xterm
-* gnome-terminal
* roxterm
-* xfce4-terminal
* termite
* lxterminal
-* mate-terminal
* terminology
* st
* qterminal
diff --git a/man/i3.man b/man/i3.man
index 8157ac7..1974f0b 100644
--- a/man/i3.man
+++ b/man/i3.man
@@ -43,6 +43,9 @@
--shmlog-size ::
Limits the size of the i3 SHM log to bytes. Setting this to 0 disables
SHM logging entirely. The default is 0 bytes.
+
+--replace::
+Replace an existing window manager.
== DESCRIPTION
diff --git a/man/i3bar.man b/man/i3bar.man
index d0b0148..479e10f 100644
--- a/man/i3bar.man
+++ b/man/i3bar.man
@@ -9,7 +9,7 @@
== SYNOPSIS
-*i3bar* [*-s* 'sock_path'] [*-b* 'bar_id'] [*-v*] [*-h*]
+*i3bar* [*-b* 'bar_id'] [*-s* 'sock_path'] [*-t*] [*-h*] [*-v*] [*-V*]
== WARNING
@@ -25,13 +25,20 @@
Overwrites the path to the i3 IPC socket.
*-b, --bar_id* 'bar_id'::
-Specifies the bar ID for which to get the configuration from i3.
+Specifies the bar ID for which to get the configuration from i3. By default,
+i3bar will use the first bar block as configured in i3.
+
+*-t, --transparency*::
+Enable transparency (RGBA colors)
+
+*-h, --help*::
+Display a short help-message and exit
*-v, --version*::
Display version number and exit.
-*-h, --help*::
-Display a short help-message and exit
+*-V*::
+Be verbose.
== DESCRIPTION
diff --git a/meson.build b/meson.build
index b052efa..91587a4 100644
--- a/meson.build
+++ b/meson.build
@@ -6,7 +6,7 @@
project(
'i3',
'c',
- version: '4.19.1',
+ version: '4.21.1',
default_options: [
'c_std=c11',
'warning_level=1', # enable all warnings (-Wall)
@@ -63,7 +63,7 @@
sources: vcs_tag(
input: config_h_in,
output: 'config.h',
- fallback: meson.project_version() + ' (2021-02-01)',
+ fallback: meson.project_version() + ' (2022-10-28)',
)
)
@@ -229,6 +229,8 @@
output: '@BASENAME@.1',
command: [
xmlto,
+ '--stringparam',
+ 'man.th.title.max.length=30',
'man',
'-o',
'@OUTDIR@',
@@ -314,7 +316,7 @@
xkbcommon_dep = dependency('xkbcommon', method: 'pkg-config')
xkbcommon_x11_dep = dependency('xkbcommon-x11', method: 'pkg-config')
yajl_dep = dependency('yajl', method: 'pkg-config')
-libpcre_dep = dependency('libpcre', version: '>=8.10', method: 'pkg-config')
+libpcre_dep = dependency('libpcre2-8', version: '>=10', method: 'pkg-config')
cairo_dep = dependency('cairo', version: '>=1.14.4', method: 'pkg-config')
pangocairo_dep = dependency('pangocairo', method: 'pkg-config')
glib_dep = dependency('glib-2.0', method: 'pkg-config')
@@ -325,6 +327,8 @@
inc = include_directories('include')
libi3srcs = [
+ 'libi3/boolstr.c',
+ 'libi3/create_socket.c',
'libi3/dpi.c',
'libi3/draw_util.c',
'libi3/fake_configure_notify.c',
@@ -341,11 +345,15 @@
'libi3/ipc_recv_message.c',
'libi3/ipc_send_message.c',
'libi3/is_debug_build.c',
+ 'libi3/path_exists.c',
'libi3/resolve_tilde.c',
'libi3/root_atom_contents.c',
'libi3/safewrappers.c',
'libi3/string.c',
'libi3/ucs2_conversion.c',
+ 'libi3/nonblock.c',
+ 'libi3/screenshot_wallpaper.c',
+ 'libi3/is_background_set.c',
]
if not cdata.get('HAVE_STRNDUP')
@@ -401,6 +409,7 @@
'src/sighandler.c',
'src/startup.c',
'src/sync.c',
+ 'src/tiling_drag.c',
'src/tree.c',
'src/util.c',
'src/version.c',
@@ -514,6 +523,7 @@
'i3-config-wizard/i3-config-wizard-atoms.xmacro.h',
'i3-config-wizard/main.c',
'i3-config-wizard/xcb.h',
+ config_parser,
],
install: true,
include_directories: include_directories('include', 'i3-config-wizard'),
@@ -642,7 +652,7 @@
'@OUTPUT@',
],
install: true,
- install_dir: join_paths(get_option('datadir'), 'doc', 'i3'),
+ install_dir: docdir,
)
custom_target(
@@ -655,7 +665,7 @@
'@OUTPUT@',
],
install: true,
- install_dir: join_paths(get_option('datadir'), 'doc', 'i3'),
+ install_dir: docdir,
)
endif
@@ -669,7 +679,10 @@
executable(
'test.commands_parser',
- 'src/commands_parser.c',
+ [
+ 'src/commands_parser.c',
+ command_parser,
+ ],
include_directories: inc,
c_args: '-DTEST_PARSER',
dependencies: common_deps,
@@ -678,7 +691,10 @@
executable(
'test.config_parser',
- 'src/config_parser.c',
+ [
+ 'src/config_parser.c',
+ config_parser,
+ ],
include_directories: inc,
c_args: '-DTEST_PARSER',
dependencies: common_deps,
@@ -706,6 +722,7 @@
anyevent_i3,
i3test_pm,
],
+ timeout: 120, # Default of 30 seconds can cause timeouts on slower machines
)
else
# meson < 0.46.0 does not support the depends arg in test targets.
diff --git a/parser-specs/commands.spec b/parser-specs/commands.spec
index 20de3e4..c618dfd 100644
--- a/parser-specs/commands.spec
+++ b/parser-specs/commands.spec
@@ -40,6 +40,7 @@
'scratchpad' -> SCRATCHPAD
'swap' -> SWAP
'title_format' -> TITLE_FORMAT
+ 'title_window_icon' -> TITLE_WINDOW_ICON
'mode' -> MODE
'bar' -> BAR
'gaps' -> GAPS
@@ -55,7 +56,8 @@
ctype = 'title' -> CRITERION
ctype = 'urgent' -> CRITERION
ctype = 'workspace' -> CRITERION
- ctype = 'tiling', 'floating'
+ ctype = 'machine' -> CRITERION
+ ctype = 'tiling', 'floating', 'all'
-> call cmd_criteria_add($ctype, NULL); CRITERIA
']' -> call cmd_criteria_match_windows(); INITIAL
@@ -182,8 +184,10 @@
-> call cmd_focus_direction($direction)
state FOCUS_OUTPUT:
- output = string
- -> call cmd_focus_output($output)
+ output = word
+ -> call cmd_focus_output($output); FOCUS_OUTPUT
+ end
+ -> call cmd_focus_output(NULL); INITIAL
# kill [window|client]
state KILL:
@@ -402,8 +406,10 @@
-> call cmd_move_con_to_workspace_number($number, $no_auto_back_and_forth)
state MOVE_TO_OUTPUT:
- output = string
- -> call cmd_move_con_to_output($output)
+ output = word
+ -> call cmd_move_con_to_output($output, 0); MOVE_TO_OUTPUT
+ end
+ -> call cmd_move_con_to_output(NULL, 0); INITIAL
state MOVE_TO_MARK:
mark = string
@@ -411,9 +417,13 @@
state MOVE_WORKSPACE_TO_OUTPUT:
'output'
- ->
- output = string
- -> call cmd_move_workspace_to_output($output)
+ -> MOVE_WORKSPACE_TO_OUTPUT_WORD
+
+state MOVE_WORKSPACE_TO_OUTPUT_WORD:
+ output = word
+ -> call cmd_move_con_to_output($output, 1); MOVE_WORKSPACE_TO_OUTPUT_WORD
+ end
+ -> call cmd_move_con_to_output(NULL, 1); INITIAL
state MOVE_TO_ABSOLUTE_POSITION:
'position'
@@ -473,6 +483,22 @@
format = string
-> call cmd_title_format($format)
+state TITLE_WINDOW_ICON:
+ 'padding'
+ -> TITLE_WINDOW_ICON_PADDING
+ enable = 'toggle'
+ -> TITLE_WINDOW_ICON_PADDING
+ enable = '1', 'yes', 'true', 'on', 'enable', 'active', '0', 'no', 'false', 'off', 'disable', 'inactive'
+ -> call cmd_title_window_icon($enable, 0)
+
+state TITLE_WINDOW_ICON_PADDING:
+ end
+ -> call cmd_title_window_icon($enable, &padding)
+ 'px'
+ -> call cmd_title_window_icon($enable, &padding)
+ padding = number
+ ->
+
# bar (hidden_state hide|show|toggle)|(mode dock|hide|invisible|toggle) []
state BAR:
'hidden_state'
diff --git a/parser-specs/config.spec b/parser-specs/config.spec
index 9f06406..a0ac987 100644
--- a/parser-specs/config.spec
+++ b/parser-specs/config.spec
@@ -20,6 +20,7 @@
'set ' -> IGNORE_LINE
'set ' -> IGNORE_LINE
'set_from_resource' -> IGNORE_LINE
+ 'include' -> INCLUDE
bindtype = 'bindsym', 'bindcode', 'bind' -> BINDING
'bar' -> BARBRACE
'font' -> FONT
@@ -55,10 +56,11 @@
'ipc_kill_timeout' -> IPC_KILL_TIMEOUT
'restart_state' -> RESTART_STATE
'popup_during_fullscreen' -> POPUP_DURING_FULLSCREEN
+ 'tiling_drag' -> TILING_DRAG
exectype = 'exec_always', 'exec' -> EXEC
colorclass = 'client.background'
-> COLOR_SINGLE
- colorclass = 'client.focused_inactive', 'client.focused', 'client.unfocused', 'client.urgent', 'client.placeholder'
+ colorclass = 'client.focused_inactive', 'client.focused_tab_title', 'client.focused', 'client.unfocused', 'client.urgent', 'client.placeholder'
-> COLOR_BORDER
# We ignore comments and 'set' lines (variables).
@@ -87,8 +89,15 @@
state SMART_GAPS:
enabled = '1', 'yes', 'true', 'on', 'enable', 'active'
-> call cfg_smart_gaps($enabled)
+ enabled = '0', 'no', 'false', 'off', 'disable', 'inactive'
+ -> call cfg_smart_gaps($enabled)
enabled = 'inverse_outer'
-> call cfg_smart_gaps($enabled)
+
+# include
+state INCLUDE:
+ pattern = string
+ -> call cfg_include($pattern)
# floating_minimum_size x
state FLOATING_MINIMUM_SIZE_WIDTH:
@@ -218,9 +227,10 @@
ctype = 'title' -> CRITERION
ctype = 'urgent' -> CRITERION
ctype = 'workspace' -> CRITERION
+ ctype = 'machine' -> CRITERION
ctype = 'floating_from' -> CRITERION_FROM
ctype = 'tiling_from' -> CRITERION_FROM
- ctype = 'tiling', 'floating'
+ ctype = 'tiling', 'floating', 'all'
-> call cfg_criteria_add($ctype, NULL); CRITERIA
']'
-> call cfg_criteria_pop_state()
@@ -352,6 +362,18 @@
state POPUP_DURING_FULLSCREEN:
value = 'ignore', 'leave_fullscreen', 'smart'
-> call cfg_popup_during_fullscreen($value)
+
+state TILING_DRAG_MODE:
+ value = 'modifier', 'titlebar'
+ ->
+ end
+ -> call cfg_tiling_drag($value)
+
+state TILING_DRAG:
+ off = '0', 'no', 'false', 'off', 'disable', 'inactive'
+ -> call cfg_tiling_drag($off)
+ value = 'modifier', 'titlebar'
+ -> TILING_DRAG_MODE
# client.background
state COLOR_SINGLE:
@@ -589,7 +611,7 @@
-> call cfg_bar_position($position); BAR
state BAR_OUTPUT:
- output = string
+ output = word
-> call cfg_bar_output($output); BAR
state BAR_TRAY_OUTPUT:
diff --git a/release-notes/bugfixes/0-example b/release-notes/bugfixes/0-example
new file mode 100644
index 0000000..2cb1f66
--- /dev/null
+++ b/release-notes/bugfixes/0-example
@@ -0,0 +1 @@
+fix crash with "layout default"
diff --git a/release-notes/bugfixes/1-motifs b/release-notes/bugfixes/1-motifs
new file mode 100644
index 0000000..b5ae083
--- /dev/null
+++ b/release-notes/bugfixes/1-motifs
@@ -0,0 +1 @@
+motif hints: respect maximum border style configuration set by user
diff --git a/release-notes/bugfixes/1-replace-socket b/release-notes/bugfixes/1-replace-socket
new file mode 100644
index 0000000..82c78b6
--- /dev/null
+++ b/release-notes/bugfixes/1-replace-socket
@@ -0,0 +1 @@
+Do not replace existing IPC socket on start
diff --git a/release-notes/bugfixes/2-fix-focus-wrap b/release-notes/bugfixes/2-fix-focus-wrap
new file mode 100644
index 0000000..3971238
--- /dev/null
+++ b/release-notes/bugfixes/2-fix-focus-wrap
@@ -0,0 +1 @@
+fix focus when moving container between outputs with mouse warp and focus_follows_mouse
diff --git a/release-notes/bugfixes/2-focus-click b/release-notes/bugfixes/2-focus-click
new file mode 100644
index 0000000..30c096f
--- /dev/null
+++ b/release-notes/bugfixes/2-focus-click
@@ -0,0 +1 @@
+tiling drag: allow click immediately, to focus on decoration click
diff --git a/release-notes/bugfixes/3-drag-cursor b/release-notes/bugfixes/3-drag-cursor
new file mode 100644
index 0000000..1902dc2
--- /dev/null
+++ b/release-notes/bugfixes/3-drag-cursor
@@ -0,0 +1 @@
+fix tiling drag cursor: should be “move”, accidentally was “top right corner”
diff --git a/release-notes/bugfixes/3-transient_for b/release-notes/bugfixes/3-transient_for
new file mode 100644
index 0000000..c652874
--- /dev/null
+++ b/release-notes/bugfixes/3-transient_for
@@ -0,0 +1 @@
+Fix endless loop with transient_for windows
diff --git a/release-notes/bugfixes/4-drop-scratchpad b/release-notes/bugfixes/4-drop-scratchpad
new file mode 100644
index 0000000..559abd1
--- /dev/null
+++ b/release-notes/bugfixes/4-drop-scratchpad
@@ -0,0 +1 @@
+tiling drag: ignore scratchpad windows when locating drop targets
diff --git a/release-notes/bugfixes/4-failed-workspace-output b/release-notes/bugfixes/4-failed-workspace-output
new file mode 100644
index 0000000..b3d2395
--- /dev/null
+++ b/release-notes/bugfixes/4-failed-workspace-output
@@ -0,0 +1 @@
+fix wrong failed reply on move workspace to output
diff --git a/release-notes/bugfixes/5-fix-wm-registration b/release-notes/bugfixes/5-fix-wm-registration
new file mode 100644
index 0000000..513c96e
--- /dev/null
+++ b/release-notes/bugfixes/5-fix-wm-registration
@@ -0,0 +1 @@
+changed WM registration selection from WM_S_S to WM_S
diff --git a/release-notes/bugfixes/6-fix-graphics-artifacts b/release-notes/bugfixes/6-fix-graphics-artifacts
new file mode 100644
index 0000000..a13e2b0
--- /dev/null
+++ b/release-notes/bugfixes/6-fix-graphics-artifacts
@@ -0,0 +1 @@
+avoid graphics artifacts when changing the layout tree by initializing surfaces to all black
diff --git a/release-notes/bugfixes/7-fix-segfault b/release-notes/bugfixes/7-fix-segfault
new file mode 100644
index 0000000..5c280be
--- /dev/null
+++ b/release-notes/bugfixes/7-fix-segfault
@@ -0,0 +1 @@
+Fix segfault if command in bindsym is empty
diff --git a/release-notes/bugfixes/7-update-parent-con-title-on-sibling-move b/release-notes/bugfixes/7-update-parent-con-title-on-sibling-move
new file mode 100644
index 0000000..7bd57df
--- /dev/null
+++ b/release-notes/bugfixes/7-update-parent-con-title-on-sibling-move
@@ -0,0 +1 @@
+update parent split con titles when child con swaps position with another child con
diff --git a/release-notes/bugfixes/8-bar-font b/release-notes/bugfixes/8-bar-font
new file mode 100644
index 0000000..618d3c1
--- /dev/null
+++ b/release-notes/bugfixes/8-bar-font
@@ -0,0 +1 @@
+fix default font not being applied to bars if defined after bar block
diff --git a/release-notes/bugfixes/8-bar-output-trailing-whitespace b/release-notes/bugfixes/8-bar-output-trailing-whitespace
new file mode 100644
index 0000000..c551d88
--- /dev/null
+++ b/release-notes/bugfixes/8-bar-output-trailing-whitespace
@@ -0,0 +1 @@
+strip trailing whitespace in bar output names
diff --git a/release-notes/bugfixes/8-fix-nested-variables-crash b/release-notes/bugfixes/8-fix-nested-variables-crash
new file mode 100644
index 0000000..44058ce
--- /dev/null
+++ b/release-notes/bugfixes/8-fix-nested-variables-crash
@@ -0,0 +1 @@
+Fix crash if config contains nested variables.
diff --git a/release-notes/bugfixes/8-mode-default-sigsegv b/release-notes/bugfixes/8-mode-default-sigsegv
new file mode 100644
index 0000000..75ce814
--- /dev/null
+++ b/release-notes/bugfixes/8-mode-default-sigsegv
@@ -0,0 +1 @@
+Fix segfault with explicit mode "default" key bindings.
diff --git a/release-notes/bugfixes/9-bs-normal b/release-notes/bugfixes/9-bs-normal
new file mode 100644
index 0000000..0342fbf
--- /dev/null
+++ b/release-notes/bugfixes/9-bs-normal
@@ -0,0 +1 @@
+Restore BS_NORMAL _MOTIF_WM_HINTS correctly
diff --git a/release-notes/changes/0-example b/release-notes/changes/0-example
new file mode 100644
index 0000000..6242e4e
--- /dev/null
+++ b/release-notes/changes/0-example
@@ -0,0 +1 @@
+Acquire the WM_Sn selection when starting as required by ICCCM
diff --git a/release-notes/changes/1-tiling-drag b/release-notes/changes/1-tiling-drag
new file mode 100644
index 0000000..bc16308
--- /dev/null
+++ b/release-notes/changes/1-tiling-drag
@@ -0,0 +1 @@
+tiling drag is now configurable, and defaults to “modifier” for existing configs
diff --git a/release-notes/changes/1-valid-socket b/release-notes/changes/1-valid-socket
new file mode 100644
index 0000000..e605e6d
--- /dev/null
+++ b/release-notes/changes/1-valid-socket
@@ -0,0 +1 @@
+Refuse to start without valid IPC socket
diff --git a/release-notes/changes/2-client.focused_tab_title b/release-notes/changes/2-client.focused_tab_title
new file mode 100644
index 0000000..0480219
--- /dev/null
+++ b/release-notes/changes/2-client.focused_tab_title
@@ -0,0 +1 @@
+Add client.focused_tab_title color option
diff --git a/release-notes/changes/2-tiling-drag-targets b/release-notes/changes/2-tiling-drag-targets
new file mode 100644
index 0000000..9981a19
--- /dev/null
+++ b/release-notes/changes/2-tiling-drag-targets
@@ -0,0 +1 @@
+tiling drag: only initiate when there are drop targets
diff --git a/release-notes/changes/3-focus-outputs b/release-notes/changes/3-focus-outputs
new file mode 100644
index 0000000..cecd5b4
--- /dev/null
+++ b/release-notes/changes/3-focus-outputs
@@ -0,0 +1 @@
+Add support for multiple outputs in focus command
diff --git a/release-notes/changes/3-tiling-drag b/release-notes/changes/3-tiling-drag
new file mode 100644
index 0000000..195fcdb
--- /dev/null
+++ b/release-notes/changes/3-tiling-drag
@@ -0,0 +1 @@
+Allow moving tiling windows with the mouse
diff --git a/release-notes/changes/4-title_window_icon-toggle b/release-notes/changes/4-title_window_icon-toggle
new file mode 100644
index 0000000..f15e7a4
--- /dev/null
+++ b/release-notes/changes/4-title_window_icon-toggle
@@ -0,0 +1 @@
+Add title_window_icon toggle
diff --git a/release-notes/generator.pl b/release-notes/generator.pl
new file mode 100755
index 0000000..d77eb09
--- /dev/null
+++ b/release-notes/generator.pl
@@ -0,0 +1,96 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+use v5.10;
+use Getopt::Long;
+
+my @template = (
+'
+ ┌──────────────────────────────┐
+ │ Release notes for i3 v4.21 │
+ └──────────────────────────────┘
+
+This is i3 v4.21. This version is considered stable. All users of i3 are
+strongly encouraged to upgrade.
+
+
+ ┌────────────────────────────┐
+ │ Changes in i3 v4.21 │
+ └────────────────────────────┘
+
+',
+'
+ ┌────────────────────────────┐
+ │ Bugfixes │
+ └────────────────────────────┘
+
+');
+
+my $print_urls = 0;
+my $result = GetOptions('print-urls' => \$print_urls);
+
+sub get_number {
+ my $s = shift;
+ return $1 if $s =~ m/^(\d+)/;
+ return -1;
+}
+
+sub read_changefiles {
+ my $dirpath = shift;
+ opendir my $dir, $dirpath or die "Cannot open directory $dirpath: $!";
+ my @files = sort { get_number($a) <=> get_number($b) } readdir $dir;
+
+ closedir $dir;
+
+ my $s = '';
+ for my $filename (@files) {
+ next if $filename eq '.';
+ next if $filename eq '..';
+ next if $filename eq '0-example';
+
+ die "Filename $filename should start with a number (e.g. the pull request number)" unless get_number($filename) > 0;
+
+ $filename = $dirpath . '/' . $filename;
+ open my $in, '<', $filename or die "can't open $filename: $!";
+ my @lines = <$in>;
+ close $in or die "can't close $filename: $!";
+
+ my $content = trim(join("\n ", map { trim($_) } @lines));
+ die "$filename can't be empty" unless length($content) > 0;
+
+ my $url = '';
+ if ($print_urls) {
+ my $commit = `git log --diff-filter=A --pretty=format:"%H" $filename`;
+ $commit = trim($commit) if defined($commit);
+ die "$filename: git log failed to find commit" if ($?) || (length($commit) == 0);
+
+ my $pr = find_pr($commit);
+ $url = 'https://github.com/i3/i3/commit/' . $commit;
+ $url = 'https://github.com/i3/i3/pull/' . $pr if defined($pr);
+ $url = $url . "\n";
+ }
+
+ $s = $s . ' • ' . $content . "\n" . $url;
+ }
+ return $s;
+}
+
+sub find_pr {
+ my $hash = shift;
+ my $result = `git log --merges --ancestry-path --oneline $hash..next | grep 'Merge pull request' | tail -n1`;
+ return unless defined($result);
+
+ return unless ($result =~ /Merge pull request .([0-9]+)/);
+ return $1;
+}
+
+sub trim {
+ (my $s = $_[0]) =~ s/^\s+|\s+$//g;
+ return $s;
+}
+
+# Expected to run for i3's git root
+my $changes = read_changefiles('release-notes/changes');
+my $bugfixes = read_changefiles('release-notes/bugfixes');
+
+print $template[0] . $changes . $template[1] . $bugfixes;
diff --git a/release.sh b/release.sh
index c6b045c..5939144 100755
--- a/release.sh
+++ b/release.sh
@@ -1,8 +1,10 @@
#!/bin/zsh
# This script is used to prepare a new release of i3.
-export RELEASE_VERSION="4.19"
-export PREVIOUS_VERSION="4.18"
+set -eu
+
+export RELEASE_VERSION="4.20"
+export PREVIOUS_VERSION="4.19.2"
export RELEASE_BRANCH="next"
if [ ! -e "../i3.github.io" ]
@@ -20,13 +22,8 @@
if [ ! -e "RELEASE-NOTES-${RELEASE_VERSION}" ]
then
- echo "RELEASE-NOTES-${RELEASE_VERSION} not found."
- exit 1
-fi
-
-if git diff-files --quiet --exit-code debian/changelog
-then
- echo "Expected debian/changelog to be changed (containing the changelog for ${RELEASE_VERSION})."
+ echo "RELEASE-NOTES-${RELEASE_VERSION} not found. Here is the output from the generator:"
+ ./release-notes/generator.pl --print-urls
exit 1
fi
@@ -41,8 +38,8 @@
TMPDIR=$(mktemp -d)
cd $TMPDIR
-if ! wget https://i3wm.org/downloads/i3-${PREVIOUS_VERSION}.tar.bz2; then
- echo "Could not download i3-${PREVIOUS_VERSION}.tar.bz2 (required for comparing files)."
+if ! wget https://i3wm.org/downloads/i3-${PREVIOUS_VERSION}.tar.xz; then
+ echo "Could not download i3-${PREVIOUS_VERSION}.tar.xz (required for comparing files)."
exit 1
fi
git clone --quiet --branch "${RELEASE_BRANCH}" https://github.com/i3/i3
@@ -52,28 +49,32 @@
exit 1
fi
git checkout -b release-${RELEASE_VERSION}
+git rm RELEASE-NOTES-*
cp "${STARTDIR}/RELEASE-NOTES-${RELEASE_VERSION}" "RELEASE-NOTES-${RELEASE_VERSION}"
git add RELEASE-NOTES-${RELEASE_VERSION}
-git rm RELEASE-NOTES-${PREVIOUS_VERSION}
-sed -i "s/^\s*version: '${PREVIOUS_VERSION}'/ version: '${RELEASE_VERSION}'/" meson.build
+# Update the release version:
+sed -i "s/^\s*version: '4.[^']*'/ version: '${RELEASE_VERSION}'/" meson.build
+cp meson.build "${TMPDIR}/meson.build"
+# Inject the release date into meson.build for the dist tarball:
+sed -i "s/'-non-git'/' ($(date +'%Y-%m-%d'))'/" meson.build
git commit -a -m "release i3 ${RELEASE_VERSION}"
git tag "${RELEASE_VERSION}" -m "release i3 ${RELEASE_VERSION}" --sign --local-user=0x4AC8EE1D
mkdir build
(cd build && meson .. && ninja dist)
-cp build/meson-build/i3-${RELEASE_VERSION}.tar.xz .
+cp build/meson-dist/i3-${RELEASE_VERSION}.tar.xz .
echo "Differences in the release tarball file lists:"
diff --color -u \
- <(tar tf ../i3-${PREVIOUS_VERSION}.tar.xz | sed "s,i3-${PREVIOUS_VERSION}/,,g" | sort) \
- <(tar tf i3-${RELEASE_VERSION}.tar.xz | sed "s,i3-${RELEASE_VERSION}/,,g" | sort)
+ <(tar tf ../i3-${PREVIOUS_VERSION}.tar.* | sed "s,i3-${PREVIOUS_VERSION}/,,g" | sort) \
+ <(tar tf i3-${RELEASE_VERSION}.tar.xz | sed "s,i3-${RELEASE_VERSION}/,,g" | sort) || true
gpg --armor -b i3-${RELEASE_VERSION}.tar.xz
-echo "${RELEASE_VERSION}-non-git" > I3_VERSION
-git add I3_VERSION
-git commit -a -m "Set non-git version to ${RELEASE_VERSION}-non-git."
+mv "${TMPDIR}/meson.build" .
+git add meson.build
+git commit -a -m "Restore non-git version suffix"
if [ "${RELEASE_BRANCH}" = "stable" ]; then
git checkout stable
@@ -94,7 +95,7 @@
git config --add remote.origin.push "+refs/heads/stable:refs/heads/stable"
################################################################################
-# Section 2: Debian packaging
+# Section 2: Debian packaging (for QA)
################################################################################
cd "${TMPDIR}"
@@ -133,9 +134,6 @@
echo "Content of resulting package’s .changes file:"
cat ${TMPDIR}/debian/*.changes
-# debsign is in devscripts, which is available in fedora and debian
-debsign --no-re-sign -k4AC8EE1D ${TMPDIR}/debian/*.changes
-
# TODO: docker cleanup
################################################################################
@@ -176,7 +174,7 @@
(cd _docs && make)
-for i in $(find _docs -maxdepth 1 -and -type f -and \! -regex ".*\.\(html\|man\)$" -and \! -name "Makefile")
+for i in $(find _docs -maxdepth 1 -and -type f -and \! -regex ".*\.\(html\|man\|css\)$" -and \! -name "Makefile")
do
base="$(basename $i)"
[ -e "${TMPDIR}/i3/docs/${base}" ] && cp "_docs/${base}.html" docs/
@@ -234,6 +232,6 @@
echo ""
echo "Announce on:"
echo " twitter"
-echo " google+"
echo " #i3 topic"
-echo " reddit /r/i3wm"
+echo " reddit /r/i3wm (link post to changelog)"
+echo " GitHub Discussions → Announcements"
diff --git a/src/bindings.c b/src/bindings.c
index d6255e7..0aa960d 100644
--- a/src/bindings.c
+++ b/src/bindings.c
@@ -718,6 +718,40 @@
}
/*
+ * Returns true if a is a key binding for the same key as b.
+ *
+ */
+static bool binding_same_key(Binding *a, Binding *b) {
+ /* Check if the input types are different */
+ if (a->input_type != b->input_type) {
+ return false;
+ }
+
+ /* Check if one is using keysym while the other is using bindsym. */
+ if ((a->symbol == NULL && b->symbol != NULL) ||
+ (a->symbol != NULL && b->symbol == NULL)) {
+ return false;
+ }
+
+ /* If a is NULL, b has to be NULL, too (see previous conditional).
+ * If the keycodes differ, it can't be a duplicate. */
+ if (a->symbol != NULL &&
+ strcasecmp(a->symbol, b->symbol) != 0) {
+ return false;
+ }
+
+ /* Check if the keycodes or modifiers are different. If so, they
+ * can't be duplicate */
+ if (a->keycode != b->keycode ||
+ a->event_state_mask != b->event_state_mask ||
+ a->release != b->release) {
+ return false;
+ }
+
+ return true;
+}
+
+/*
* Checks for duplicate key bindings (the same keycode or keysym is configured
* more than once). If a duplicate binding is found, a message is printed to
* stderr and the has_errors variable is set to true, which will start
@@ -730,31 +764,13 @@
TAILQ_FOREACH (bind, bindings, bindings) {
/* Abort when we reach the current keybinding, only check the
* bindings before */
- if (bind == current)
+ if (bind == current) {
break;
-
- /* Check if the input types are different */
- if (bind->input_type != current->input_type)
+ }
+
+ if (!binding_same_key(bind, current)) {
continue;
-
- /* Check if one is using keysym while the other is using bindsym.
- * If so, skip. */
- if ((bind->symbol == NULL && current->symbol != NULL) ||
- (bind->symbol != NULL && current->symbol == NULL))
- continue;
-
- /* If bind is NULL, current has to be NULL, too (see above).
- * If the keycodes differ, it can't be a duplicate. */
- if (bind->symbol != NULL &&
- strcasecmp(bind->symbol, current->symbol) != 0)
- continue;
-
- /* Check if the keycodes or modifiers are different. If so, they
- * can't be duplicate */
- if (bind->keycode != current->keycode ||
- bind->event_state_mask != current->event_state_mask ||
- bind->release != current->release)
- continue;
+ }
context->has_errors = true;
if (current->keycode != 0) {
diff --git a/src/click.c b/src/click.c
index 7a2c032..e262516 100644
--- a/src/click.c
+++ b/src/click.c
@@ -80,7 +80,7 @@
*/
static bool floating_mod_on_tiled_client(Con *con, xcb_button_press_event_t *event) {
/* The client is in tiling layout. We can still initiate a resize with the
- * right mouse button, by chosing the border which is the most near one to
+ * right mouse button, by choosing the border which is the most near one to
* the position of the mouse pointer */
int to_right = con->rect.width - event->event_x,
to_left = event->event_x,
@@ -141,6 +141,12 @@
return false;
}
+static void allow_replay_pointer(xcb_timestamp_t time) {
+ xcb_allow_events(conn, XCB_ALLOW_REPLAY_POINTER, time);
+ xcb_flush(conn);
+ tree_render();
+}
+
/*
* Being called by handle_button_press, this function calls the appropriate
* functions for resizing/dragging.
@@ -152,8 +158,10 @@
DLOG("type = %d, name = %s\n", con->type, con->name);
/* don’t handle dockarea cons, they must not be focused */
- if (con->parent->type == CT_DOCKAREA)
- goto done;
+ if (con->parent->type == CT_DOCKAREA) {
+ allow_replay_pointer(event->time);
+ return;
+ }
/* if the user has bound an action to this click, it should override the
* default behavior. */
@@ -173,7 +181,8 @@
/* There is no default behavior for button release events so we are done. */
if (event->response_type == XCB_BUTTON_RELEASE) {
- goto done;
+ allow_replay_pointer(event->time);
+ return;
}
/* Any click in a workspace should focus that workspace. If the
@@ -184,12 +193,11 @@
if (!ws) {
ws = TAILQ_FIRST(&(output_get_content(con_get_output(con))->focus_head));
- if (!ws)
- goto done;
- }
-
- if (ws != focused_workspace)
- workspace_show(ws);
+ if (!ws) {
+ allow_replay_pointer(event->time);
+ return;
+ }
+ }
/* get the floating con */
Con *floatingcon = con_inside_floating(con);
@@ -215,23 +223,50 @@
Con *next = get_tree_next_sibling(current, direction);
con_activate(con_descend_focused(next ? next : current));
- goto done;
- }
-
- /* 2: focus this con. */
- con_activate(con);
-
- /* 3: For floating containers, we also want to raise them on click.
+ allow_replay_pointer(event->time);
+ return;
+ }
+
+ /* 2: floating modifier pressed, initiate a drag */
+ if (mod_pressed && is_left_click && !floatingcon &&
+ (config.tiling_drag == TILING_DRAG_MODIFIER ||
+ config.tiling_drag == TILING_DRAG_MODIFIER_OR_TITLEBAR) &&
+ has_drop_targets()) {
+ const bool use_threshold = !mod_pressed;
+ tiling_drag(con, event, use_threshold);
+ allow_replay_pointer(event->time);
+ return;
+ }
+
+ /* 3: focus this con or one of its children. */
+ Con *con_to_focus = con;
+ if (in_stacked && dest == CLICK_DECORATION) {
+ /* If the container is a tab/stacked container and the click happened
+ * on a tab, switch to the tab. If the tab contents were already
+ * focused, focus the tab container itself. If the tab container was
+ * already focused, cycle back to focusing the tab contents. */
+ if (was_focused || !con_has_parent(focused, con)) {
+ while (!TAILQ_EMPTY(&(con_to_focus->focus_head))) {
+ con_to_focus = TAILQ_FIRST(&(con_to_focus->focus_head));
+ }
+ }
+ }
+ if (ws != focused_workspace) {
+ workspace_show(ws);
+ }
+ con_activate(con_to_focus);
+
+ /* 4: For floating containers, we also want to raise them on click.
* We will skip handling events on floating cons in fullscreen mode */
Con *fs = con_get_fullscreen_covering_ws(ws);
if (floatingcon != NULL && fs != con) {
- /* 4: floating_modifier plus left mouse button drags */
+ /* 5: floating_modifier plus left mouse button drags */
if (mod_pressed && is_left_click) {
floating_drag_window(floatingcon, event, false);
return;
}
- /* 5: resize (floating) if this was a (left or right) click on the
+ /* 6: resize (floating) if this was a (left or right) click on the
* left/right/bottom border, or a right click on the decoration.
* also try resizing (tiling) if possible */
if (mod_pressed && is_right_click) {
@@ -244,8 +279,10 @@
is_left_or_right_click) {
/* try tiling resize, but continue if it doesn’t work */
DLOG("tiling resize with fallback\n");
- if (tiling_resize(con, event, dest, dest == CLICK_DECORATION && !was_focused))
- goto done;
+ if (tiling_resize(con, event, dest, dest == CLICK_DECORATION && !was_focused)) {
+ allow_replay_pointer(event->time);
+ return;
+ }
}
if (dest == CLICK_DECORATION && is_right_click) {
@@ -260,17 +297,30 @@
return;
}
- /* 6: dragging, if this was a click on a decoration (which did not lead
+ /* 7: dragging, if this was a click on a decoration (which did not lead
* to a resize) */
if (dest == CLICK_DECORATION && is_left_click) {
floating_drag_window(floatingcon, event, !was_focused);
return;
}
- goto done;
- }
-
- /* 7: floating modifier pressed, initiate a resize */
+ allow_replay_pointer(event->time);
+ return;
+ }
+
+ /* 8: floating modifier pressed, or click in titlebar, initiate a drag */
+ if (is_left_click &&
+ ((config.tiling_drag == TILING_DRAG_TITLEBAR && dest == CLICK_DECORATION) ||
+ (config.tiling_drag == TILING_DRAG_MODIFIER_OR_TITLEBAR &&
+ (mod_pressed || dest == CLICK_DECORATION))) &&
+ has_drop_targets()) {
+ allow_replay_pointer(event->time);
+ const bool use_threshold = !mod_pressed;
+ tiling_drag(con, event, use_threshold);
+ return;
+ }
+
+ /* 9: floating modifier pressed, initiate a resize */
if (dest == CLICK_INSIDE && mod_pressed && is_right_click) {
if (floating_mod_on_tiled_client(con, event)) {
return;
@@ -281,17 +331,14 @@
xcb_flush(conn);
return;
}
- /* 8: otherwise, check for border/decoration clicks and resize */
+ /* 10: otherwise, check for border/decoration clicks and resize */
if ((dest == CLICK_BORDER || dest == CLICK_DECORATION) &&
is_left_or_right_click) {
DLOG("Trying to resize (tiling)\n");
tiling_resize(con, event, dest, dest == CLICK_DECORATION && !was_focused);
}
-done:
- xcb_allow_events(conn, XCB_ALLOW_REPLAY_POINTER, event->time);
- xcb_flush(conn);
- tree_render();
+ allow_replay_pointer(event->time);
}
/*
diff --git a/src/commands.c b/src/commands.c
index 26ea876..52c3fcd 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -747,6 +747,8 @@
return;
}
+ /* User changed the border */
+ current->con->max_user_border_style = border_style;
const int con_border_width = border_width_from_style(border_style, border_width, current->con);
con_set_border_style(current->con, border_style, con_border_width);
}
@@ -1023,24 +1025,125 @@
ysuccess(true);
}
-/*
- * Implementation of 'move [window|container] [to] output '.
- *
- */
-void cmd_move_con_to_output(I3_CMD, const char *name) {
- DLOG("Should move window to output \"%s\".\n", name);
+typedef struct user_output_name {
+ char *name;
+ TAILQ_ENTRY(user_output_name) user_output_names;
+} user_output_name;
+typedef TAILQ_HEAD(user_output_names_head, user_output_name) user_output_names_head;
+
+static void user_output_names_add(user_output_names_head *list, const char *name) {
+ if (strcmp(name, "next") == 0) {
+ /* "next" here works like a wildcard: It "expands" to all available
+ * outputs. */
+ Output *output;
+ TAILQ_FOREACH (output, &outputs, outputs) {
+ user_output_name *co = scalloc(sizeof(user_output_name), 1);
+ co->name = sstrdup(output_primary_name(output));
+ TAILQ_INSERT_TAIL(list, co, user_output_names);
+ }
+ return;
+ }
+
+ user_output_name *co = scalloc(sizeof(user_output_name), 1);
+ co->name = sstrdup(name);
+ TAILQ_INSERT_TAIL(list, co, user_output_names);
+ return;
+}
+
+static Output *user_output_names_find_next(user_output_names_head *names, Output *current_output) {
+ Output *target_output = NULL;
+ user_output_name *uo;
+ TAILQ_FOREACH (uo, names, user_output_names) {
+ if (!target_output) {
+ /* The first available output from the list is used in 2 cases:
+ * 1. When we must wrap around the user list. For example, if user
+ * specifies outputs A B C and C is `current_output`.
+ * 2. When the current output is not in the user list. For example,
+ * user specifies A B C and D is `current_output`. */
+ target_output = get_output_from_string(current_output, uo->name);
+ }
+ if (strcasecmp(output_primary_name(current_output), uo->name) == 0) {
+ /* The current output is in the user list */
+ while (true) {
+ /* This corrupts the outer loop but it is ok since we are going
+ * to break anyway. */
+ uo = TAILQ_NEXT(uo, user_output_names);
+ if (!uo) {
+ /* We reached the end of the list. We should use the first
+ * available output that, if it exists, is already saved in
+ * target_output. */
+ break;
+ }
+ Output *out = get_output_from_string(current_output, uo->name);
+ if (out) {
+ return out;
+ }
+ }
+ break;
+ }
+ }
+ return target_output;
+}
+
+static void user_output_names_free(user_output_names_head *names) {
+ user_output_name *uo;
+ while (!TAILQ_EMPTY(names)) {
+ uo = TAILQ_FIRST(names);
+ free(uo->name);
+ TAILQ_REMOVE(names, uo, user_output_names);
+ free(uo);
+ }
+}
+
+/*
+ * Implementation of 'move [window|container|workspace] [to] output '.
+ *
+ */
+void cmd_move_con_to_output(I3_CMD, const char *name, bool move_workspace) {
+ /* Initialize a data structure that is used to save multiple user-specified
+ * output names since this function is called multiple types for each
+ * command call. */
+ static user_output_names_head names = TAILQ_HEAD_INITIALIZER(names);
+
+ if (name) {
+ user_output_names_add(&names, name);
+ return;
+ }
+
HANDLE_EMPTY_MATCH;
+ if (TAILQ_EMPTY(&names)) {
+ yerror("At least one output must be specified");
+ return;
+ }
+
+ bool success = false;
owindow *current;
- bool had_error = false;
TAILQ_FOREACH (current, &owindows, owindows) {
- DLOG("matching: %p / %s\n", current->con, current->con->name);
-
- had_error |= !con_move_to_output_name(current->con, name, true);
- }
-
- cmd_output->needs_tree_render = true;
- ysuccess(!had_error);
+ Con *ws = con_get_workspace(current->con);
+ if (con_is_internal(ws)) {
+ continue;
+ }
+
+ Output *current_output = get_output_for_con(ws);
+ Output *target_output = user_output_names_find_next(&names, current_output);
+ if (target_output) {
+ if (move_workspace) {
+ workspace_move_to_output(ws, target_output);
+ } else {
+ con_move_to_output(current->con, target_output, true);
+ }
+ success = true;
+ }
+ }
+ user_output_names_free(&names);
+
+ cmd_output->needs_tree_render = success;
+ if (success) {
+ ysuccess(true);
+ } else {
+ yerror("No output matched");
+ }
}
/*
@@ -1091,36 +1194,6 @@
cmd_output->needs_tree_render = true;
// XXX: default reply for now, make this a better reply
- ysuccess(true);
-}
-
-/*
- * Implementation of 'move workspace to [output] '.
- *
- */
-void cmd_move_workspace_to_output(I3_CMD, const char *name) {
- DLOG("should move workspace to output %s\n", name);
-
- HANDLE_EMPTY_MATCH;
-
- owindow *current;
- TAILQ_FOREACH (current, &owindows, owindows) {
- Con *ws = con_get_workspace(current->con);
- if (con_is_internal(ws)) {
- continue;
- }
-
- Output *current_output = get_output_for_con(ws);
- Output *target_output = get_output_from_string(current_output, name);
- if (!target_output) {
- yerror("Could not get output from string \"%s\"", name);
- return;
- }
-
- workspace_move_to_output(ws, target_output);
- }
-
- cmd_output->needs_tree_render = true;
ysuccess(true);
}
@@ -1661,6 +1734,9 @@
}
ipc_shutdown(SHUTDOWN_REASON_RESTART, exempt_fd);
unlink(config.ipc_socket_path);
+ if (current_log_stream_socket_path != NULL) {
+ unlink(current_log_stream_socket_path);
+ }
/* We need to call this manually since atexit handlers don’t get called
* when exec()ing */
purge_zerobyte_logfile();
@@ -1694,6 +1770,17 @@
*
*/
void cmd_focus_output(I3_CMD, const char *name) {
+ static user_output_names_head names = TAILQ_HEAD_INITIALIZER(names);
+ if (name) {
+ user_output_names_add(&names, name);
+ return;
+ }
+
+ if (TAILQ_EMPTY(&names)) {
+ yerror("At least one output must be specified");
+ return;
+ }
+
HANDLE_EMPTY_MATCH;
if (TAILQ_EMPTY(&owindows)) {
@@ -1702,25 +1789,29 @@
}
Output *current_output = get_output_for_con(TAILQ_FIRST(&owindows)->con);
- Output *output = get_output_from_string(current_output, name);
-
- if (!output) {
- yerror("Output %s not found.", name);
- return;
- }
-
- /* get visible workspace on output */
- Con *ws = NULL;
- GREP_FIRST(ws, output_get_content(output->con), workspace_is_visible(child));
- if (!ws) {
- yerror("BUG: No workspace found on output.");
- return;
- }
-
- workspace_show(ws);
-
- cmd_output->needs_tree_render = true;
- ysuccess(true);
+ Output *target_output = user_output_names_find_next(&names, current_output);
+ user_output_names_free(&names);
+ bool success = false;
+ if (target_output) {
+ success = true;
+
+ /* get visible workspace on output */
+ Con *ws = NULL;
+ GREP_FIRST(ws, output_get_content(target_output->con), workspace_is_visible(child));
+ if (!ws) {
+ yerror("BUG: No workspace found on output.");
+ return;
+ }
+
+ workspace_show(ws);
+ }
+
+ cmd_output->needs_tree_render = success;
+ if (success) {
+ ysuccess(true);
+ } else {
+ yerror("No output matched");
+ }
}
/*
@@ -1960,6 +2051,55 @@
ewmh_update_visible_name(current->con->window->id, NULL);
}
}
+
+ if (current->con->window != NULL) {
+ /* Make sure the window title is redrawn immediately. */
+ current->con->window->name_x_changed = true;
+ } else {
+ /* For windowless containers we also need to force the redrawing. */
+ FREE(current->con->deco_render_params);
+ }
+ }
+
+ cmd_output->needs_tree_render = true;
+ ysuccess(true);
+}
+
+/*
+ * Implementation of 'title_window_icon ' and 'title_window_icon padding '
+ *
+ */
+void cmd_title_window_icon(I3_CMD, const char *enable, int padding) {
+ bool is_toggle = false;
+ if (enable != NULL) {
+ if (strcmp(enable, "toggle") == 0) {
+ is_toggle = true;
+ } else if (!boolstr(enable)) {
+ padding = -1;
+ }
+ }
+ DLOG("setting window_icon=%d\n", padding);
+ HANDLE_EMPTY_MATCH;
+
+ owindow *current;
+ TAILQ_FOREACH (current, &owindows, owindows) {
+ if (is_toggle) {
+ const int current_padding = current->con->window_icon_padding;
+ if (padding > 0) {
+ if (current_padding < 0) {
+ current->con->window_icon_padding = padding;
+ } else {
+ /* toggle off, but store padding given */
+ current->con->window_icon_padding = -(padding + 1);
+ }
+ } else {
+ /* Set to negative of (current value+1) to keep old padding when toggling */
+ current->con->window_icon_padding = -(current_padding + 1);
+ }
+ } else {
+ current->con->window_icon_padding = padding;
+ }
+ DLOG("Set window_icon for %p / %s to %d\n", current->con, current->con->name, current->con->window_icon_padding);
if (current->con->window != NULL) {
/* Make sure the window title is redrawn immediately. */
diff --git a/src/commands_parser.c b/src/commands_parser.c
index 6c79141..7cdf6c6 100644
--- a/src/commands_parser.c
+++ b/src/commands_parser.c
@@ -56,40 +56,19 @@
#include "GENERATED_command_tokens.h"
-/*******************************************************************************
- * The (small) stack where identified literals are stored during the parsing
- * of a single command (like $workspace).
- ******************************************************************************/
-
-struct stack_entry {
- /* Just a pointer, not dynamically allocated. */
- const char *identifier;
- enum {
- STACK_STR = 0,
- STACK_LONG = 1,
- } type;
- union {
- char *str;
- long num;
- } val;
-};
-
-/* 10 entries should be enough for everybody. */
-static struct stack_entry stack[10];
-
/*
* Pushes a string (identified by 'identifier') on the stack. We simply use a
* single array, since the number of entries we have to store is very small.
*
*/
-static void push_string(const char *identifier, char *str) {
+static void push_string(struct stack *stack, const char *identifier, char *str) {
for (int c = 0; c < 10; c++) {
- if (stack[c].identifier != NULL)
+ if (stack->stack[c].identifier != NULL)
continue;
/* Found a free slot, let’s store it here. */
- stack[c].identifier = identifier;
- stack[c].val.str = str;
- stack[c].type = STACK_STR;
+ stack->stack[c].identifier = identifier;
+ stack->stack[c].val.str = str;
+ stack->stack[c].type = STACK_STR;
return;
}
@@ -103,15 +82,15 @@
}
// TODO move to a common util
-static void push_long(const char *identifier, long num) {
+static void push_long(struct stack *stack, const char *identifier, long num) {
for (int c = 0; c < 10; c++) {
- if (stack[c].identifier != NULL) {
+ if (stack->stack[c].identifier != NULL) {
continue;
}
- stack[c].identifier = identifier;
- stack[c].val.num = num;
- stack[c].type = STACK_LONG;
+ stack->stack[c].identifier = identifier;
+ stack->stack[c].val.num = num;
+ stack->stack[c].type = STACK_LONG;
return;
}
@@ -125,36 +104,36 @@
}
// TODO move to a common util
-static const char *get_string(const char *identifier) {
+static const char *get_string(struct stack *stack, const char *identifier) {
for (int c = 0; c < 10; c++) {
- if (stack[c].identifier == NULL)
+ if (stack->stack[c].identifier == NULL)
break;
- if (strcmp(identifier, stack[c].identifier) == 0)
- return stack[c].val.str;
+ if (strcmp(identifier, stack->stack[c].identifier) == 0)
+ return stack->stack[c].val.str;
}
return NULL;
}
// TODO move to a common util
-static long get_long(const char *identifier) {
+static long get_long(struct stack *stack, const char *identifier) {
for (int c = 0; c < 10; c++) {
- if (stack[c].identifier == NULL)
+ if (stack->stack[c].identifier == NULL)
break;
- if (strcmp(identifier, stack[c].identifier) == 0)
- return stack[c].val.num;
+ if (strcmp(identifier, stack->stack[c].identifier) == 0)
+ return stack->stack[c].val.num;
}
return 0;
}
// TODO move to a common util
-static void clear_stack(void) {
+static void clear_stack(struct stack *stack) {
for (int c = 0; c < 10; c++) {
- if (stack[c].type == STACK_STR)
- free(stack[c].val.str);
- stack[c].identifier = NULL;
- stack[c].val.str = NULL;
- stack[c].val.num = 0;
+ if (stack->stack[c].type == STACK_STR)
+ free(stack->stack[c].val.str);
+ stack->stack[c].identifier = NULL;
+ stack->stack[c].val.str = NULL;
+ stack->stack[c].val.num = 0;
}
}
@@ -163,9 +142,12 @@
******************************************************************************/
static cmdp_state state;
-#ifndef TEST_PARSER
static Match current_match;
-#endif
+/*******************************************************************************
+ * The (small) stack where identified literals are stored during the parsing
+ * of a single command (like $workspace).
+ ******************************************************************************/
+static struct stack stack;
static struct CommandResultIR subcommand_output;
static struct CommandResultIR command_output;
@@ -176,19 +158,19 @@
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);
+ GENERATED_call(¤t_match, &stack, token->extra.call_identifier, &subcommand_output);
state = subcommand_output.next_state;
/* If any subcommand requires a tree_render(), we need to make the
* whole parser result request a tree_render(). */
if (subcommand_output.needs_tree_render)
command_output.needs_tree_render = true;
- clear_stack();
+ clear_stack(&stack);
return;
}
state = token->next_state;
if (state == INITIAL) {
- clear_stack();
+ clear_stack(&stack);
}
}
@@ -296,8 +278,9 @@
/* A literal. */
if (token->name[0] == '\'') {
if (strncasecmp(walk, token->name + 1, strlen(token->name) - 1) == 0) {
- if (token->identifier != NULL)
- push_string(token->identifier, sstrdup(token->name + 1));
+ if (token->identifier != NULL) {
+ push_string(&stack, token->identifier, sstrdup(token->name + 1));
+ }
walk += strlen(token->name) - 1;
next_state(token);
token_handled = true;
@@ -319,8 +302,9 @@
if (end == walk)
continue;
- if (token->identifier != NULL)
- push_long(token->identifier, num);
+ if (token->identifier != NULL) {
+ push_long(&stack, token->identifier, num);
+ }
/* Set walk to the first non-number character */
walk = end;
@@ -333,8 +317,9 @@
strcmp(token->name, "word") == 0) {
char *str = parse_string(&walk, (token->name[0] != 's'));
if (str != NULL) {
- if (token->identifier)
- push_string(token->identifier, str);
+ if (token->identifier) {
+ push_string(&stack, token->identifier, str);
+ }
/* If we are at the end of a quoted string, skip the ending
* double quote. */
if (*walk == '"')
@@ -405,7 +390,7 @@
free(possible_tokens);
/* Contains the same amount of characters as 'input' has, but with
- * the unparseable part highlighted using ^ characters. */
+ * the unparsable part highlighted using ^ characters. */
char *position = smalloc(len + 1);
for (const char *copywalk = input; *copywalk != '\0'; copywalk++)
position[(copywalk - input)] = (copywalk >= walk ? '^' : ' ');
@@ -436,7 +421,7 @@
y(map_close);
free(position);
- clear_stack();
+ clear_stack(&stack);
break;
}
}
diff --git a/src/con.c b/src/con.c
index 22f87c1..51fdab9 100644
--- a/src/con.c
+++ b/src/con.c
@@ -41,8 +41,9 @@
TAILQ_INSERT_TAIL(&all_cons, new, all_cons);
new->type = CT_CON;
new->window = window;
- new->border_style = config.default_border;
+ new->border_style = new->max_user_border_style = config.default_border;
new->current_border_width = -1;
+ new->window_icon_padding = -1;
if (window) {
new->depth = window->depth;
} else {
@@ -730,6 +731,41 @@
}
/*
+ * Start from a container and traverse the transient_for linked list. Returns
+ * true if target window is found in the list. Protects againsts potential
+ * cycles.
+ *
+ */
+bool con_find_transient_for_window(Con *start, xcb_window_t target) {
+ Con *transient_con = start;
+ int count = con_num_windows(croot);
+ while (transient_con != NULL &&
+ transient_con->window != NULL &&
+ transient_con->window->transient_for != XCB_NONE) {
+ DLOG("transient_con = 0x%08x, transient_con->window->transient_for = 0x%08x, target = 0x%08x\n",
+ transient_con->window->id, transient_con->window->transient_for, target);
+ if (transient_con->window->transient_for == target) {
+ return true;
+ }
+ Con *next_transient = con_by_window_id(transient_con->window->transient_for);
+ if (next_transient == NULL) {
+ break;
+ }
+ /* Some clients (e.g. x11-ssh-askpass) actually set WM_TRANSIENT_FOR to
+ * their own window id, so break instead of looping endlessly. */
+ if (transient_con == next_transient) {
+ break;
+ }
+ transient_con = next_transient;
+
+ if (count-- <= 0) { /* Avoid cycles, see #4404 */
+ break;
+ }
+ }
+ return false;
+}
+
+/*
* Returns true if and only if the given containers holds the mark.
*
*/
@@ -851,8 +887,6 @@
Con *con_for_window(Con *con, i3Window *window, Match **store_match) {
Con *child;
Match *match;
- //DLOG("searching con for window %p starting at con %p\n", window, con);
- //DLOG("class == %s\n", window->class_class);
TAILQ_FOREACH (child, &(con->nodes_head), nodes) {
TAILQ_FOREACH (match, &(child->swallow_head), matches) {
@@ -1011,8 +1045,8 @@
Con *child;
int children = con_num_children(con);
- // calculate how much we have distributed and how many containers
- // with a percentage set we have
+ /* calculate how much we have distributed and how many containers with a
+ * percentage set we have */
double total = 0.0;
int children_with_percent = 0;
TAILQ_FOREACH (child, &(con->nodes_head), nodes) {
@@ -1022,8 +1056,8 @@
}
}
- // if there were children without a percentage set, set to a value that
- // will make those children proportional to all others
+ /* if there were children without a percentage set, set to a value that
+ * will make those children proportional to all others */
if (children_with_percent != children) {
TAILQ_FOREACH (child, &(con->nodes_head), nodes) {
if (child->percent <= 0.0) {
@@ -1036,8 +1070,8 @@
}
}
- // if we got a zero, just distribute the space equally, otherwise
- // distribute according to the proportions we got
+ /* if we got a zero, just distribute the space equally, otherwise
+ * distribute according to the proportions we got */
if (total == 0.0) {
TAILQ_FOREACH (child, &(con->nodes_head), nodes) {
child->percent = 1.0 / children;
@@ -1355,17 +1389,7 @@
return true;
}
-/*
- * Moves the given container to the given mark.
- *
- */
-bool con_move_to_mark(Con *con, const char *mark) {
- Con *target = con_by_mark(mark);
- if (target == NULL) {
- DLOG("found no container with mark \"%s\"\n", mark);
- return false;
- }
-
+bool con_move_to_target(Con *con, Con *target) {
/* For target containers in the scratchpad, we just send the window to the scratchpad. */
if (con_get_workspace(target) == workspace_get("__i3_scratch")) {
DLOG("target container is in the scratchpad, moving container to scratchpad.\n");
@@ -1400,6 +1424,20 @@
}
return _con_move_to_con(con, target, false, true, false, false, true);
+}
+
+/*
+ * Moves the given container to the given mark.
+ *
+ */
+bool con_move_to_mark(Con *con, const char *mark) {
+ Con *target = con_by_mark(mark);
+ if (target == NULL) {
+ DLOG("found no container with mark \"%s\"\n", mark);
+ return false;
+ }
+
+ return con_move_to_target(con, target);
}
/*
@@ -1771,7 +1809,11 @@
* floating window.
*
*/
-void con_set_border_style(Con *con, int border_style, int border_width) {
+void con_set_border_style(Con *con, border_style_t border_style, int border_width) {
+ if (border_style > con->max_user_border_style) {
+ border_style = con->max_user_border_style;
+ }
+
/* Handle the simple case: non-floating containerns */
if (!con_is_floating(con)) {
con->border_style = border_style;
@@ -1784,8 +1826,6 @@
* con->rect represent the absolute position of the window (same for
* parent). Then, we change the border style and subtract the new border
* pixels. For the parent, we do the same also for the decoration. */
- DLOG("This is a floating container\n");
-
Con *parent = con->parent;
Rect bsr = con_border_style_rect(con);
int deco_height = (con->border_style == BS_NORMAL ? render_deco_height() : 0);
@@ -1870,9 +1910,9 @@
con_attach(new, con, false);
tree_flatten(croot);
- }
- con_force_split_parents_redraw(con);
- return;
+ con_force_split_parents_redraw(con);
+ return;
+ }
}
if (layout == L_DEFAULT) {
@@ -2216,7 +2256,6 @@
} else
DLOG("Discarding urgency WM_HINT because timer is running\n");
- //CLIENT_LOG(con);
if (con->window) {
if (con->urgent) {
gettimeofday(&con->window->urgent, NULL);
@@ -2358,20 +2397,25 @@
char *title;
char *class;
char *instance;
+ char *machine;
if (win == NULL) {
title = pango_escape_markup(con_get_tree_representation(con));
class = sstrdup("i3-frame");
instance = sstrdup("i3-frame");
+ machine = sstrdup("");
} else {
title = pango_escape_markup(sstrdup((win->name == NULL) ? "" : i3string_as_utf8(win->name)));
class = pango_escape_markup(sstrdup((win->class_class == NULL) ? "" : win->class_class));
instance = pango_escape_markup(sstrdup((win->class_instance == NULL) ? "" : win->class_instance));
+ machine = pango_escape_markup(sstrdup((win->machine == NULL) ? "" : win->machine));
}
placeholder_t placeholders[] = {
{.name = "%title", .value = title},
{.name = "%class", .value = class},
- {.name = "%instance", .value = instance}};
+ {.name = "%instance", .value = instance},
+ {.name = "%machine", .value = machine},
+ };
const size_t num = sizeof(placeholders) / sizeof(placeholder_t);
char *formatted_str = format_placeholders(con->title_format, &placeholders[0], num);
diff --git a/src/config.c b/src/config.c
index 77ba38c..7cbdad1 100644
--- a/src/config.c
+++ b/src/config.c
@@ -10,13 +10,16 @@
*/
#include "all.h"
+#include
+#include
+
#include
char *current_configpath = NULL;
-char *current_config = NULL;
Config config;
struct modes_head modes;
struct barconfig_head barconfigs = TAILQ_HEAD_INITIALIZER(barconfigs);
+struct includedfiles_head included_files = TAILQ_HEAD_INITIALIZER(included_files);
/*
* Ungrabs all keys, to be called before re-grabbing the keys because of a
@@ -194,6 +197,7 @@
INIT_COLOR(config.client.focused_inactive, "#333333", "#5f676a", "#ffffff", "#484e50");
INIT_COLOR(config.client.unfocused, "#333333", "#222222", "#888888", "#292d2e");
INIT_COLOR(config.client.urgent, "#2f343a", "#900000", "#ffffff", "#900000");
+ config.client.got_focused_tab_title = false;
/* border and indicator color are ignored for placeholder contents */
INIT_COLOR(config.client.placeholder, "#000000", "#0c0c0c", "#ffffff", "#000000");
@@ -224,6 +228,8 @@
config.focus_wrapping = FOCUS_WRAPPING_ON;
+ config.tiling_drag = TILING_DRAG_MODIFIER;
+
FREE(current_configpath);
current_configpath = get_config_path(override_configpath, true);
if (current_configpath == NULL) {
@@ -231,8 +237,43 @@
"$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);
+
+ IncludedFile *file;
+ while (!TAILQ_EMPTY(&included_files)) {
+ file = TAILQ_FIRST(&included_files);
+ FREE(file->path);
+ FREE(file->raw_contents);
+ FREE(file->variable_replaced_contents);
+ TAILQ_REMOVE(&included_files, file, files);
+ FREE(file);
+ }
+
+ char resolved_path[PATH_MAX] = {'\0'};
+ if (realpath(current_configpath, resolved_path) == NULL) {
+ die("realpath(%s): %s", current_configpath, strerror(errno));
+ }
+
+ file = scalloc(1, sizeof(IncludedFile));
+ file->path = sstrdup(resolved_path);
+ TAILQ_INSERT_TAIL(&included_files, file, files);
+
+ LOG("Parsing configfile %s\n", resolved_path);
+ struct stack stack;
+ memset(&stack, '\0', sizeof(struct stack));
+ struct parser_ctx ctx = {
+ .use_nagbar = (load_type != C_VALIDATE),
+ .assume_v4 = false,
+ .stack = &stack,
+ };
+ SLIST_INIT(&(ctx.variables));
+ const int result = parse_file(&ctx, resolved_path, file);
+ free_variables(&ctx);
+ if (result == -1) {
+ die("Could not open configuration file: %s\n", strerror(errno));
+ }
+
+ extract_workspace_names_from_bindings();
+ reorder_bindings();
if (config.font.type == FONT_TYPE_NONE && load_type != C_VALIDATE) {
ELOG("You did not specify required configuration option \"font\"\n");
@@ -240,6 +281,17 @@
set_font(&config.font);
}
+ /* Make bar config blocks without a configured font use the i3-wide font. */
+ Barconfig *current;
+ if (load_type != C_VALIDATE) {
+ TAILQ_FOREACH (current, &barconfigs, configs) {
+ if (current->font != NULL) {
+ continue;
+ }
+ current->font = sstrdup(config.font.pattern);
+ }
+ }
+
if (load_type == C_RELOAD) {
translate_keysyms();
grab_all_keys(conn);
@@ -251,5 +303,5 @@
xcb_flush(conn);
}
- return result;
+ return result == 0;
}
diff --git a/src/config_directives.c b/src/config_directives.c
index 5236254..4fd7329 100644
--- a/src/config_directives.c
+++ b/src/config_directives.c
@@ -9,6 +9,86 @@
*/
#include "all.h"
+#include
+
+/*******************************************************************************
+ * Include functions.
+ ******************************************************************************/
+
+CFGFUN(include, const char *pattern) {
+ DLOG("include %s\n", pattern);
+
+ wordexp_t p;
+ const int ret = wordexp(pattern, &p, 0);
+ if (ret != 0) {
+ ELOG("wordexp(%s): error %d\n", pattern, ret);
+ result->has_errors = true;
+ return;
+ }
+ char **w = p.we_wordv;
+ for (size_t i = 0; i < p.we_wordc; i++) {
+ char resolved_path[PATH_MAX] = {'\0'};
+ if (realpath(w[i], resolved_path) == NULL) {
+ LOG("Skipping %s: %s\n", w[i], strerror(errno));
+ continue;
+ }
+
+ bool skip = false;
+ IncludedFile *file;
+ TAILQ_FOREACH (file, &included_files, files) {
+ if (strcmp(file->path, resolved_path) == 0) {
+ skip = true;
+ break;
+ }
+ }
+ if (skip) {
+ LOG("Skipping file %s (already included)\n", resolved_path);
+ continue;
+ }
+
+ LOG("Including config file %s\n", resolved_path);
+
+ file = scalloc(1, sizeof(IncludedFile));
+ file->path = sstrdup(resolved_path);
+ TAILQ_INSERT_TAIL(&included_files, file, files);
+
+ struct stack stack;
+ memset(&stack, '\0', sizeof(struct stack));
+ struct parser_ctx ctx = {
+ .use_nagbar = result->ctx->use_nagbar,
+ /* The include mechanism was added in v4, so we can skip the
+ * auto-detection and get rid of the risk of detecting the wrong
+ * version in potentially very short include fragments: */
+ .assume_v4 = true,
+ .stack = &stack,
+ .variables = result->ctx->variables,
+ };
+ switch (parse_file(&ctx, resolved_path, file)) {
+ case PARSE_FILE_SUCCESS:
+ break;
+
+ case PARSE_FILE_FAILED:
+ ELOG("including config file %s: %s\n", resolved_path, strerror(errno));
+ /* fallthrough */
+
+ case PARSE_FILE_CONFIG_ERRORS:
+ result->has_errors = true;
+ TAILQ_REMOVE(&included_files, file, files);
+ FREE(file->path);
+ FREE(file->raw_contents);
+ FREE(file->variable_replaced_contents);
+ FREE(file);
+ break;
+
+ default:
+ /* missing case statement */
+ assert(false);
+ break;
+ }
+ }
+ wordfree(&p);
+}
+
/*******************************************************************************
* Criteria functions.
******************************************************************************/
@@ -44,15 +124,6 @@
/*******************************************************************************
* Utility functions
******************************************************************************/
-
-static bool eval_boolstr(const char *str) {
- return (strcasecmp(str, "1") == 0 ||
- strcasecmp(str, "yes") == 0 ||
- strcasecmp(str, "true") == 0 ||
- strcasecmp(str, "on") == 0 ||
- strcasecmp(str, "enable") == 0 ||
- strcasecmp(str, "active") == 0);
-}
/*
* A utility function to convert a string containing the group and modifiers to
@@ -92,15 +163,9 @@
return result;
}
-static char *font_pattern;
-
CFGFUN(font, const char *font) {
config.font = load_font(font, true);
set_font(&config.font);
-
- /* Save the font pattern for using it as bar font later on */
- FREE(font_pattern);
- font_pattern = sstrdup(font);
}
CFGFUN(binding, const char *bindtype, const char *modifiers, const char *key, const char *release, const char *border, const char *whole_window, const char *exclude_titlebar, const char *command) {
@@ -115,6 +180,11 @@
static bool current_mode_pango_markup;
CFGFUN(mode_binding, const char *bindtype, const char *modifiers, const char *key, const char *release, const char *border, const char *whole_window, const char *exclude_titlebar, const char *command) {
+ if (current_mode == NULL) {
+ /* When using an invalid mode name, e.g. “default” */
+ return;
+ }
+
configure_binding(bindtype, modifiers, key, release, border, whole_window, exclude_titlebar, command, current_mode, current_mode_pango_markup);
}
@@ -305,14 +375,14 @@
if (!strcmp(enable, "no_gaps"))
config.smart_borders = SMART_BORDERS_NO_GAPS;
else
- config.smart_borders = eval_boolstr(enable) ? SMART_BORDERS_ON : SMART_BORDERS_OFF;
+ config.smart_borders = boolstr(enable) ? SMART_BORDERS_ON : SMART_BORDERS_OFF;
}
CFGFUN(smart_gaps, const char *enable) {
if (!strcmp(enable, "inverse_outer"))
config.smart_gaps = SMART_GAPS_INVERSE_OUTER;
else
- config.smart_gaps = eval_boolstr(enable) ? SMART_GAPS_ON : SMART_GAPS_OFF;
+ config.smart_gaps = boolstr(enable) ? SMART_GAPS_ON : SMART_GAPS_OFF;
}
CFGFUN(floating_minimum_size, const long width, const long height) {
@@ -393,14 +463,14 @@
config.hide_edge_borders = HEBM_BOTH;
else if (strcmp(borders, "none") == 0)
config.hide_edge_borders = HEBM_NONE;
- else if (eval_boolstr(borders))
+ else if (boolstr(borders))
config.hide_edge_borders = HEBM_VERTICAL;
else
config.hide_edge_borders = HEBM_NONE;
}
CFGFUN(focus_follows_mouse, const char *value) {
- config.disable_focus_follows_mouse = !eval_boolstr(value);
+ config.disable_focus_follows_mouse = !boolstr(value);
}
CFGFUN(mouse_warping, const char *value) {
@@ -411,11 +481,11 @@
}
CFGFUN(force_xinerama, const char *value) {
- config.force_xinerama = eval_boolstr(value);
+ config.force_xinerama = boolstr(value);
}
CFGFUN(disable_randr15, const char *value) {
- config.disable_randr15 = eval_boolstr(value);
+ config.disable_randr15 = boolstr(value);
}
CFGFUN(focus_wrapping, const char *value) {
@@ -423,7 +493,7 @@
config.focus_wrapping = FOCUS_WRAPPING_FORCE;
} else if (strcmp(value, "workspace") == 0) {
config.focus_wrapping = FOCUS_WRAPPING_WORKSPACE;
- } else if (eval_boolstr(value)) {
+ } else if (boolstr(value)) {
config.focus_wrapping = FOCUS_WRAPPING_ON;
} else {
config.focus_wrapping = FOCUS_WRAPPING_OFF;
@@ -432,7 +502,7 @@
CFGFUN(force_focus_wrapping, const char *value) {
/* Legacy syntax. */
- if (eval_boolstr(value)) {
+ if (boolstr(value)) {
config.focus_wrapping = FOCUS_WRAPPING_FORCE;
} else {
/* For "force_focus_wrapping off", don't enable or disable
@@ -444,7 +514,7 @@
}
CFGFUN(workspace_back_and_forth, const char *value) {
- config.workspace_auto_back_and_forth = eval_boolstr(value);
+ config.workspace_auto_back_and_forth = boolstr(value);
}
CFGFUN(fake_outputs, const char *outputs) {
@@ -486,7 +556,7 @@
}
CFGFUN(show_marks, const char *value) {
- config.show_marks = eval_boolstr(value);
+ config.show_marks = boolstr(value);
}
static char *current_workspace = NULL;
@@ -554,24 +624,32 @@
}
CFGFUN(color, const char *colorclass, const char *border, const char *background, const char *text, const char *indicator, const char *child_border) {
-#define APPLY_COLORS(classname) \
- do { \
- if (strcmp(colorclass, "client." #classname) == 0) { \
- config.client.classname.border = draw_util_hex_to_color(border); \
- config.client.classname.background = draw_util_hex_to_color(background); \
- config.client.classname.text = draw_util_hex_to_color(text); \
- if (indicator != NULL) { \
- config.client.classname.indicator = draw_util_hex_to_color(indicator); \
- } \
- if (child_border != NULL) { \
- config.client.classname.child_border = draw_util_hex_to_color(child_border); \
- } else { \
- config.client.classname.child_border = config.client.classname.background; \
- } \
- } \
+#define APPLY_COLORS(classname) \
+ do { \
+ if (strcmp(colorclass, "client." #classname) == 0) { \
+ if (strcmp("focused_tab_title", #classname) == 0) { \
+ config.client.got_focused_tab_title = true; \
+ if (indicator || child_border) { \
+ ELOG("indicator and child_border colors have no effect for client.focused_tab_title\n"); \
+ } \
+ } \
+ config.client.classname.border = draw_util_hex_to_color(border); \
+ config.client.classname.background = draw_util_hex_to_color(background); \
+ config.client.classname.text = draw_util_hex_to_color(text); \
+ if (indicator != NULL) { \
+ config.client.classname.indicator = draw_util_hex_to_color(indicator); \
+ } \
+ if (child_border != NULL) { \
+ config.client.classname.child_border = draw_util_hex_to_color(child_border); \
+ } else { \
+ config.client.classname.child_border = config.client.classname.background; \
+ } \
+ return; \
+ } \
} while (0)
APPLY_COLORS(focused_inactive);
+ APPLY_COLORS(focused_tab_title);
APPLY_COLORS(focused);
APPLY_COLORS(unfocused);
APPLY_COLORS(urgent);
@@ -640,6 +718,20 @@
ipc_set_kill_timeout(timeout_ms / 1000.0);
}
+CFGFUN(tiling_drag, const char *value) {
+ if (strcmp(value, "modifier") == 0) {
+ config.tiling_drag = TILING_DRAG_MODIFIER;
+ } else if (strcmp(value, "titlebar") == 0) {
+ config.tiling_drag = TILING_DRAG_TITLEBAR;
+ } else if (strcmp(value, "modifier,titlebar") == 0 ||
+ strcmp(value, "titlebar,modifier") == 0) {
+ /* Switch the above to strtok() or similar if we ever grow more options */
+ config.tiling_drag = TILING_DRAG_MODIFIER_OR_TITLEBAR;
+ } else {
+ config.tiling_drag = TILING_DRAG_OFF;
+ }
+}
+
/*******************************************************************************
* Bar configuration (i3bar)
******************************************************************************/
@@ -676,7 +768,7 @@
}
CFGFUN(bar_verbose, const char *verbose) {
- current_bar->verbose = eval_boolstr(verbose);
+ current_bar->verbose = boolstr(verbose);
}
CFGFUN(bar_height, const long height) {
@@ -800,11 +892,11 @@
}
CFGFUN(bar_binding_mode_indicator, const char *value) {
- current_bar->hide_binding_mode_indicator = !eval_boolstr(value);
+ current_bar->hide_binding_mode_indicator = !boolstr(value);
}
CFGFUN(bar_workspace_buttons, const char *value) {
- current_bar->hide_workspace_buttons = !eval_boolstr(value);
+ current_bar->hide_workspace_buttons = !boolstr(value);
}
CFGFUN(bar_workspace_min_width, const long width) {
@@ -812,11 +904,11 @@
}
CFGFUN(bar_strip_workspace_numbers, const char *value) {
- current_bar->strip_workspace_numbers = eval_boolstr(value);
+ current_bar->strip_workspace_numbers = boolstr(value);
}
CFGFUN(bar_strip_workspace_name, const char *value) {
- current_bar->strip_workspace_name = eval_boolstr(value);
+ current_bar->strip_workspace_name = boolstr(value);
}
CFGFUN(bar_start) {
@@ -835,10 +927,6 @@
config.number_barconfigs++;
- /* If no font was explicitly set, we use the i3 font as default */
- if (current_bar->font == NULL && font_pattern != NULL)
- current_bar->font = sstrdup(font_pattern);
-
TAILQ_INSERT_TAIL(&barconfigs, current_bar, configs);
/* Simply reset the pointer, but don't free the resources. */
current_bar = NULL;
diff --git a/src/config_parser.c b/src/config_parser.c
index f78e75f..baf5848 100644
--- a/src/config_parser.c
+++ b/src/config_parser.c
@@ -35,18 +35,14 @@
#include
#include
#include
+#include
#include
-
-// Macros to make the YAJL API a bit easier to use.
-#define y(x, ...) yajl_gen_##x(command_output.json_gen, ##__VA_ARGS__)
-#define ystr(str) yajl_gen_string(command_output.json_gen, (unsigned char *)str, strlen(str))
xcb_xrm_database_t *database = NULL;
#ifndef TEST_PARSER
pid_t config_error_nagbar_pid = -1;
-static struct context *context;
#endif
/*******************************************************************************
@@ -76,46 +72,25 @@
#include "GENERATED_config_tokens.h"
-/*******************************************************************************
- * The (small) stack where identified literals are stored during the parsing
- * of a single command (like $workspace).
- ******************************************************************************/
-
-struct stack_entry {
- /* Just a pointer, not dynamically allocated. */
- const char *identifier;
- enum {
- STACK_STR = 0,
- STACK_LONG = 1,
- } type;
- union {
- char *str;
- long num;
- } val;
-};
-
-/* 10 entries should be enough for everybody. */
-static struct stack_entry stack[10];
-
/*
* Pushes a string (identified by 'identifier') on the stack. We simply use a
* single array, since the number of entries we have to store is very small.
*
*/
-static void push_string(const char *identifier, const char *str) {
+static void push_string(struct stack *ctx, const char *identifier, const char *str) {
for (int c = 0; c < 10; c++) {
- if (stack[c].identifier != NULL &&
- strcmp(stack[c].identifier, identifier) != 0)
+ if (ctx->stack[c].identifier != NULL &&
+ strcmp(ctx->stack[c].identifier, identifier) != 0)
continue;
- if (stack[c].identifier == NULL) {
+ if (ctx->stack[c].identifier == NULL) {
/* Found a free slot, let’s store it here. */
- stack[c].identifier = identifier;
- stack[c].val.str = sstrdup(str);
- stack[c].type = STACK_STR;
+ ctx->stack[c].identifier = identifier;
+ ctx->stack[c].val.str = sstrdup(str);
+ ctx->stack[c].type = STACK_STR;
} else {
/* Append the value. */
- char *prev = stack[c].val.str;
- sasprintf(&(stack[c].val.str), "%s,%s", prev, str);
+ char *prev = ctx->stack[c].val.str;
+ sasprintf(&(ctx->stack[c].val.str), "%s,%s", prev, str);
free(prev);
}
return;
@@ -130,14 +105,15 @@
exit(EXIT_FAILURE);
}
-static void push_long(const char *identifier, long num) {
+static void push_long(struct stack *ctx, const char *identifier, long num) {
for (int c = 0; c < 10; c++) {
- if (stack[c].identifier != NULL)
+ if (ctx->stack[c].identifier != NULL) {
continue;
+ }
/* Found a free slot, let’s store it here. */
- stack[c].identifier = identifier;
- stack[c].val.num = num;
- stack[c].type = STACK_LONG;
+ ctx->stack[c].identifier = identifier;
+ ctx->stack[c].val.num = num;
+ ctx->stack[c].type = STACK_LONG;
return;
}
@@ -150,33 +126,33 @@
exit(EXIT_FAILURE);
}
-static const char *get_string(const char *identifier) {
+static const char *get_string(struct stack *ctx, const char *identifier) {
for (int c = 0; c < 10; c++) {
- if (stack[c].identifier == NULL)
+ if (ctx->stack[c].identifier == NULL)
break;
- if (strcmp(identifier, stack[c].identifier) == 0)
- return stack[c].val.str;
+ if (strcmp(identifier, ctx->stack[c].identifier) == 0)
+ return ctx->stack[c].val.str;
}
return NULL;
}
-static long get_long(const char *identifier) {
+static long get_long(struct stack *ctx, const char *identifier) {
for (int c = 0; c < 10; c++) {
- if (stack[c].identifier == NULL)
+ if (ctx->stack[c].identifier == NULL)
break;
- if (strcmp(identifier, stack[c].identifier) == 0)
- return stack[c].val.num;
+ if (strcmp(identifier, ctx->stack[c].identifier) == 0)
+ return ctx->stack[c].val.num;
}
return 0;
}
-static void clear_stack(void) {
+static void clear_stack(struct stack *ctx) {
for (int c = 0; c < 10; c++) {
- if (stack[c].type == STACK_STR)
- free(stack[c].val.str);
- stack[c].identifier = NULL;
- stack[c].val.str = NULL;
- stack[c].val.num = 0;
+ if (ctx->stack[c].type == STACK_STR)
+ free(ctx->stack[c].val.str);
+ ctx->stack[c].identifier = NULL;
+ ctx->stack[c].val.str = NULL;
+ ctx->stack[c].val.num = 0;
}
}
@@ -184,50 +160,40 @@
* The parser itself.
******************************************************************************/
-static cmdp_state state;
-static Match current_match;
-static struct ConfigResultIR subcommand_output;
-static struct ConfigResultIR command_output;
-
-/* A list which contains the states that lead to the current state, e.g.
- * INITIAL, WORKSPACE_LAYOUT.
- * When jumping back to INITIAL, statelist_idx will simply be set to 1
- * (likewise for other states, e.g. MODE or BAR).
- * This list is used to process the nearest error token. */
-static cmdp_state statelist[10] = {INITIAL};
-/* NB: statelist_idx points to where the next entry will be inserted */
-static int statelist_idx = 1;
-
#include "GENERATED_config_call.h"
-static void next_state(const cmdp_token *token) {
+static void next_state(const cmdp_token *token, struct parser_ctx *ctx) {
cmdp_state _next_state = token->next_state;
- //printf("token = name %s identifier %s\n", token->name, token->identifier);
- //printf("next_state = %d\n", token->next_state);
if (token->next_state == __CALL) {
- subcommand_output.json_gen = command_output.json_gen;
- GENERATED_call(token->extra.call_identifier, &subcommand_output);
+ struct ConfigResultIR subcommand_output = {
+ .ctx = ctx,
+ };
+ GENERATED_call(&(ctx->current_match), ctx->stack, token->extra.call_identifier, &subcommand_output);
+ if (subcommand_output.has_errors) {
+ ctx->has_errors = true;
+ }
_next_state = subcommand_output.next_state;
- clear_stack();
- }
-
- state = _next_state;
- if (state == INITIAL) {
- clear_stack();
+ clear_stack(ctx->stack);
+ }
+
+ ctx->state = _next_state;
+ if (ctx->state == INITIAL) {
+ clear_stack(ctx->stack);
}
/* See if we are jumping back to a state in which we were in previously
* (statelist contains INITIAL) and just move statelist_idx accordingly. */
- for (int i = 0; i < statelist_idx; i++) {
- if (statelist[i] != _next_state)
+ for (int i = 0; i < ctx->statelist_idx; i++) {
+ if ((cmdp_state)(ctx->statelist[i]) != _next_state) {
continue;
- statelist_idx = i + 1;
+ }
+ ctx->statelist_idx = i + 1;
return;
}
/* Otherwise, the state is new and we add it to the list */
- statelist[statelist_idx++] = _next_state;
+ ctx->statelist[ctx->statelist_idx++] = _next_state;
}
/*
@@ -257,7 +223,7 @@
return result;
}
-struct ConfigResultIR *parse_config(const char *input, struct context *context) {
+static void parse_config(struct parser_ctx *ctx, const char *input, struct context *context) {
/* Dump the entire config file into the debug log. We cannot just use
* DLOG("%s", input); because one log message must not exceed 4 KiB. */
const char *dumpwalk = input;
@@ -273,13 +239,11 @@
}
linecnt++;
}
- state = INITIAL;
- statelist_idx = 1;
-
- /* A YAJL JSON generator used for formatting replies. */
- command_output.json_gen = yajl_gen_alloc(NULL);
-
- y(array_open);
+ ctx->state = INITIAL;
+ for (int i = 0; i < 10; i++) {
+ ctx->statelist[i] = INITIAL;
+ }
+ ctx->statelist_idx = 1;
const char *walk = input;
const size_t len = strlen(input);
@@ -288,9 +252,11 @@
bool token_handled;
linecnt = 1;
-// TODO: make this testable
#ifndef TEST_PARSER
- cfg_criteria_init(¤t_match, &subcommand_output, INITIAL);
+ struct ConfigResultIR subcommand_output = {
+ .ctx = ctx,
+ };
+ cfg_criteria_init(&(ctx->current_match), &subcommand_output, INITIAL);
#endif
/* The "<=" operator is intentional: We also handle the terminating 0-byte
@@ -301,9 +267,7 @@
while ((*walk == ' ' || *walk == '\t') && *walk != '\0')
walk++;
- //printf("remaining input: %s\n", walk);
-
- cmdp_token_ptr *ptr = &(tokens[state]);
+ cmdp_token_ptr *ptr = &(tokens[ctx->state]);
token_handled = false;
for (c = 0; c < ptr->n; c++) {
token = &(ptr->array[c]);
@@ -311,10 +275,11 @@
/* A literal. */
if (token->name[0] == '\'') {
if (strncasecmp(walk, token->name + 1, strlen(token->name) - 1) == 0) {
- if (token->identifier != NULL)
- push_string(token->identifier, token->name + 1);
+ if (token->identifier != NULL) {
+ push_string(ctx->stack, token->identifier, token->name + 1);
+ }
walk += strlen(token->name) - 1;
- next_state(token);
+ next_state(token, ctx);
token_handled = true;
break;
}
@@ -334,12 +299,13 @@
if (end == walk)
continue;
- if (token->identifier != NULL)
- push_long(token->identifier, num);
+ if (token->identifier != NULL) {
+ push_long(ctx->stack, token->identifier, num);
+ }
/* Set walk to the first non-number character */
walk = end;
- next_state(token);
+ next_state(token, ctx);
token_handled = true;
break;
}
@@ -382,14 +348,15 @@
inpos++;
str[outpos] = beginning[inpos];
}
- if (token->identifier)
- push_string(token->identifier, str);
+ if (token->identifier) {
+ push_string(ctx->stack, token->identifier, str);
+ }
free(str);
/* If we are at the end of a quoted string, skip the ending
* double quote. */
if (*walk == '"')
walk++;
- next_state(token);
+ next_state(token, ctx);
token_handled = true;
break;
}
@@ -398,7 +365,7 @@
if (strcmp(token->name, "line") == 0) {
while (*walk != '\0' && *walk != '\n' && *walk != '\r')
walk++;
- next_state(token);
+ next_state(token, ctx);
token_handled = true;
linecnt++;
walk++;
@@ -406,17 +373,15 @@
}
if (strcmp(token->name, "end") == 0) {
- //printf("checking for end: *%s*\n", walk);
if (*walk == '\0' || *walk == '\n' || *walk == '\r') {
- next_state(token);
+ next_state(token, ctx);
token_handled = true;
/* 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. */
-// TODO: make this testable
#ifndef TEST_PARSER
- cfg_criteria_init(¤t_match, &subcommand_output, INITIAL);
+ cfg_criteria_init(&(ctx->current_match), &subcommand_output, INITIAL);
#endif
linecnt++;
walk++;
@@ -473,7 +438,7 @@
const char *error_line = start_of_line(walk, input);
/* Contains the same amount of characters as 'input' has, but with
- * the unparseable part highlighted using ^ characters. */
+ * the unparsable part highlighted using ^ characters. */
char *position = scalloc(strlen(error_line) + 1, 1);
const char *copywalk;
for (copywalk = error_line;
@@ -515,41 +480,24 @@
context->has_errors = true;
- /* Format this error message as a JSON reply. */
- y(map_open);
- ystr("success");
- y(bool, false);
- /* We set parse_error to true to distinguish this from other
- * errors. i3-nagbar is spawned upon keypresses only for parser
- * errors. */
- ystr("parse_error");
- y(bool, true);
- ystr("error");
- ystr(errormessage);
- ystr("input");
- ystr(input);
- ystr("errorposition");
- ystr(position);
- y(map_close);
-
/* Skip the rest of this line, but continue parsing. */
while ((size_t)(walk - input) <= len && *walk != '\n')
walk++;
free(position);
free(errormessage);
- clear_stack();
+ clear_stack(ctx->stack);
/* To figure out in which state to go (e.g. MODE or INITIAL),
* we find the nearest state which contains an token
* and follow that one. */
bool error_token_found = false;
- for (int i = statelist_idx - 1; (i >= 0) && !error_token_found; i--) {
- cmdp_token_ptr *errptr = &(tokens[statelist[i]]);
+ for (int i = ctx->statelist_idx - 1; (i >= 0) && !error_token_found; i--) {
+ cmdp_token_ptr *errptr = &(tokens[ctx->statelist[i]]);
for (int j = 0; j < errptr->n; j++) {
if (strcmp(errptr->array[j].name, "error") != 0)
continue;
- next_state(&(errptr->array[j]));
+ next_state(&(errptr->array[j]), ctx);
error_token_found = true;
break;
}
@@ -558,10 +506,6 @@
assert(error_token_found);
}
}
-
- y(array_close);
-
- return &command_output;
}
/*******************************************************************************
@@ -612,9 +556,17 @@
fprintf(stderr, "Syntax: %s \n", argv[0]);
return 1;
}
+ struct stack stack;
+ memset(&stack, '\0', sizeof(struct stack));
+ struct parser_ctx ctx = {
+ .use_nagbar = false,
+ .assume_v4 = false,
+ .stack = &stack,
+ };
+ SLIST_INIT(&(ctx.variables));
struct context context;
context.filename = "";
- parse_config(argv[1], &context);
+ parse_config(&ctx, argv[1], &context);
}
#else
@@ -636,6 +588,7 @@
/* check for some v4-only statements */
if (strncasecmp(line, "bindcode", strlen("bindcode")) == 0 ||
+ strncasecmp(line, "include", strlen("include")) == 0 ||
strncasecmp(line, "force_focus_wrapping", strlen("force_focus_wrapping")) == 0 ||
strncasecmp(line, "# i3 config file (v4)", strlen("# i3 config file (v4)")) == 0 ||
strncasecmp(line, "workspace_layout", strlen("workspace_layout")) == 0) {
@@ -878,33 +831,62 @@
}
/*
+ * Releases the memory of all variables in ctx.
+ *
+ */
+void free_variables(struct parser_ctx *ctx) {
+ struct Variable *current;
+ while (!SLIST_EMPTY(&(ctx->variables))) {
+ current = SLIST_FIRST(&(ctx->variables));
+ FREE(current->key);
+ FREE(current->value);
+ SLIST_REMOVE_HEAD(&(ctx->variables), variables);
+ FREE(current);
+ }
+}
+
+/*
* Parses the given file by first replacing the variables, then calling
* parse_config and possibly launching i3-nagbar.
*
*/
-bool parse_file(const char *f, bool use_nagbar) {
- struct variables_head variables = SLIST_HEAD_INITIALIZER(&variables);
+parse_file_result_t parse_file(struct parser_ctx *ctx, const char *f, IncludedFile *included_file) {
int fd;
struct stat stbuf;
char *buf;
FILE *fstr;
char buffer[4096], key[512], value[4096], *continuation = NULL;
- if ((fd = open(f, O_RDONLY)) == -1)
- die("Could not open configuration file: %s\n", strerror(errno));
-
- if (fstat(fd, &stbuf) == -1)
- die("Could not fstat file: %s\n", strerror(errno));
+ char *old_dir = getcwd(NULL, 0);
+ char *dir = NULL;
+ /* dirname(3) might modify the buffer, so make a copy: */
+ char *dirbuf = sstrdup(f);
+ if ((dir = dirname(dirbuf)) != NULL) {
+ LOG("Changing working directory to config file directory %s\n", dir);
+ if (chdir(dir) == -1) {
+ ELOG("chdir(%s) failed: %s\n", dir, strerror(errno));
+ return PARSE_FILE_FAILED;
+ }
+ }
+ free(dirbuf);
+
+ if ((fd = open(f, O_RDONLY)) == -1) {
+ return PARSE_FILE_FAILED;
+ }
+
+ if (fstat(fd, &stbuf) == -1) {
+ return PARSE_FILE_FAILED;
+ }
buf = scalloc(stbuf.st_size + 1, 1);
- if ((fstr = fdopen(fd, "r")) == NULL)
- die("Could not fdopen: %s\n", strerror(errno));
-
- FREE(current_config);
- current_config = scalloc(stbuf.st_size + 1, 1);
- if ((ssize_t)fread(current_config, 1, stbuf.st_size, fstr) != stbuf.st_size) {
- die("Could not fread: %s\n", strerror(errno));
+ if ((fstr = fdopen(fd, "r")) == NULL) {
+ return PARSE_FILE_FAILED;
+ }
+
+ included_file->raw_contents = scalloc(stbuf.st_size + 1, 1);
+ if ((ssize_t)fread(included_file->raw_contents, 1, stbuf.st_size, fstr) != stbuf.st_size) {
+ return PARSE_FILE_FAILED;
}
rewind(fstr);
@@ -916,7 +898,7 @@
if (fgets(continuation, sizeof(buffer) - (continuation - buffer), fstr) == NULL) {
if (feof(fstr))
break;
- die("Could not read configuration file\n");
+ return PARSE_FILE_FAILED;
}
if (buffer[strlen(buffer) - 1] != '\n' && !feof(fstr)) {
ELOG("Your line continuation is too long, it exceeds %zd bytes\n", sizeof(buffer));
@@ -960,7 +942,7 @@
continue;
}
- upsert_variable(&variables, v_key, v_value);
+ upsert_variable(&(ctx->variables), v_key, v_value);
continue;
} else if (strcasecmp(key, "set_from_resource") == 0) {
char res_name[512] = {'\0'};
@@ -993,7 +975,7 @@
res_value = sstrdup(fallback);
}
- upsert_variable(&variables, v_key, res_value);
+ upsert_variable(&(ctx->variables), v_key, res_value);
FREE(res_value);
continue;
}
@@ -1014,14 +996,19 @@
* variables (otherwise we will count them twice, which is bad when
* 'extra' is negative) */
char *bufcopy = sstrdup(buf);
- SLIST_FOREACH (current, &variables, variables) {
+ SLIST_FOREACH (current, &(ctx->variables), variables) {
int extra = (strlen(current->value) - strlen(current->key));
char *next;
for (next = bufcopy;
next < (bufcopy + stbuf.st_size) &&
- (next = strcasestr(next, current->key)) != NULL;
- next += strlen(current->key)) {
- *next = '_';
+ (next = strcasestr(next, current->key)) != NULL;) {
+ /* We need to invalidate variables completely (otherwise we may count
+ * the same variable more than once, thus causing buffer overflow or
+ * allocation failure) with spaces (variable names cannot contain spaces) */
+ char *end = next + strlen(current->key);
+ while (next < end) {
+ *next++ = ' ';
+ }
extra_bytes += extra;
}
}
@@ -1034,12 +1021,12 @@
destwalk = new;
while (walk < (buf + stbuf.st_size)) {
/* Find the next variable */
- SLIST_FOREACH (current, &variables, variables) {
+ SLIST_FOREACH (current, &(ctx->variables), variables) {
current->next_match = strcasestr(walk, current->key);
}
nearest = NULL;
int distance = stbuf.st_size;
- SLIST_FOREACH (current, &variables, variables) {
+ SLIST_FOREACH (current, &(ctx->variables), variables) {
if (current->next_match == NULL)
continue;
if ((current->next_match - walk) < distance) {
@@ -1064,7 +1051,10 @@
/* analyze the string to find out whether this is an old config file (3.x)
* or a new config file (4.x). If it’s old, we run the converter script. */
- int version = detect_version(buf);
+ int version = 4;
+ if (!ctx->assume_v4) {
+ version = detect_version(buf);
+ }
if (version == 3) {
/* We need to convert this v3 configuration */
char *converted = migrate_config(new, strlen(new));
@@ -1090,17 +1080,18 @@
}
}
- context = scalloc(1, sizeof(struct context));
+ included_file->variable_replaced_contents = sstrdup(new);
+
+ struct context *context = scalloc(1, sizeof(struct context));
context->filename = f;
-
- struct ConfigResultIR *config_output = parse_config(new, context);
- yajl_gen_free(config_output->json_gen);
-
- extract_workspace_names_from_bindings();
+ parse_config(ctx, new, context);
+ if (ctx->has_errors) {
+ context->has_errors = true;
+ }
+
check_for_duplicate_bindings(context);
- reorder_bindings();
-
- if (use_nagbar && (context->has_errors || context->has_warnings || invalid_sets)) {
+
+ if (ctx->use_nagbar && (context->has_errors || context->has_warnings || invalid_sets)) {
ELOG("FYI: You are using i3 version %s\n", i3_version);
if (version == 3)
ELOG("Please convert your configfile first, then fix any remaining errors (see above).\n");
@@ -1108,22 +1099,22 @@
start_config_error_nagbar(f, context->has_errors || invalid_sets);
}
- bool has_errors = context->has_errors;
+ const bool has_errors = context->has_errors;
FREE(context->line_copy);
free(context);
free(new);
free(buf);
- while (!SLIST_EMPTY(&variables)) {
- current = SLIST_FIRST(&variables);
- FREE(current->key);
- FREE(current->value);
- SLIST_REMOVE_HEAD(&variables, variables);
- FREE(current);
- }
-
- return !has_errors;
+ if (chdir(old_dir) == -1) {
+ ELOG("chdir(%s) failed: %s\n", old_dir, strerror(errno));
+ return PARSE_FILE_FAILED;
+ }
+ free(old_dir);
+ if (has_errors) {
+ return PARSE_FILE_CONFIG_ERRORS;
+ }
+ return PARSE_FILE_SUCCESS;
}
#endif
diff --git a/src/display_version.c b/src/display_version.c
index 32250c1..bfdbc8d 100644
--- a/src/display_version.c
+++ b/src/display_version.c
@@ -14,22 +14,34 @@
#include
#include
-static bool human_readable_key, loaded_config_file_name_key;
-static char *human_readable_version, *loaded_config_file_name;
+static bool human_readable_key;
+static bool loaded_config_file_name_key;
+static bool included_config_file_names;
+
+static char *human_readable_version;
+static char *loaded_config_file_name;
static int version_string(void *ctx, const unsigned char *val, size_t len) {
- if (human_readable_key)
+ if (human_readable_key) {
sasprintf(&human_readable_version, "%.*s", (int)len, val);
- if (loaded_config_file_name_key)
+ }
+ if (loaded_config_file_name_key) {
sasprintf(&loaded_config_file_name, "%.*s", (int)len, val);
+ }
+ if (included_config_file_names) {
+ IncludedFile *file = scalloc(1, sizeof(IncludedFile));
+ sasprintf(&(file->path), "%.*s", (int)len, val);
+ TAILQ_INSERT_TAIL(&included_files, file, files);
+ }
return 1;
}
static int version_map_key(void *ctx, const unsigned char *stringval, size_t stringlen) {
- human_readable_key = (stringlen == strlen("human_readable") &&
- strncmp((const char *)stringval, "human_readable", strlen("human_readable")) == 0);
- loaded_config_file_name_key = (stringlen == strlen("loaded_config_file_name") &&
- strncmp((const char *)stringval, "loaded_config_file_name", strlen("loaded_config_file_name")) == 0);
+#define KEY_MATCHES(x) (stringlen == strlen(x) && strncmp((const char *)stringval, x, strlen(x)) == 0)
+ human_readable_key = KEY_MATCHES("human_readable");
+ loaded_config_file_name_key = KEY_MATCHES("loaded_config_file_name");
+ included_config_file_names = KEY_MATCHES("included_config_file_names");
+#undef KEY_MATCHES
return 1;
}
@@ -38,10 +50,26 @@
.yajl_map_key = version_map_key,
};
+static void print_config_path(const char *path, const char *role) {
+ struct stat sb;
+ time_t now;
+ char mtime[64];
+
+ printf(" %s (%s)", path, role);
+ if (stat(path, &sb) == -1) {
+ printf("\n");
+ ELOG("Cannot stat config file \"%s\"\n", path);
+ } else {
+ strftime(mtime, sizeof(mtime), "%c", localtime(&(sb.st_mtime)));
+ time(&now);
+ printf(" (last modified: %s, %.f seconds ago)\n", mtime, difftime(now, sb.st_mtime));
+ }
+}
+
/*
* Connects to i3 to find out the currently running version. Useful since it
* might be different from the version compiled into this binary (maybe the
- * user didn’t correctly install i3 or forgot te restart it).
+ * user didn’t correctly install i3 or forgot to restart it).
*
* The output looks like this:
* Running i3 version: 4.2-202-gb8e782c (2012-08-12, branch "next") (pid 14804)
@@ -98,17 +126,11 @@
printf("Running i3 version: %s (pid %s)\n", human_readable_version, pid_from_atom);
if (loaded_config_file_name) {
- struct stat sb;
- time_t now;
- char mtime[64];
- printf("Loaded i3 config: %s", loaded_config_file_name);
- if (stat(loaded_config_file_name, &sb) == -1) {
- printf("\n");
- ELOG("Cannot stat config file \"%s\"\n", loaded_config_file_name);
- } else {
- strftime(mtime, sizeof(mtime), "%c", localtime(&(sb.st_mtime)));
- time(&now);
- printf(" (Last modified: %s, %.f seconds ago)\n", mtime, difftime(now, sb.st_mtime));
+ printf("Loaded i3 config:\n");
+ print_config_path(loaded_config_file_name, "main");
+ IncludedFile *file;
+ TAILQ_FOREACH (file, &included_files, files) {
+ print_config_path(file->path, "included");
}
}
diff --git a/src/drag.c b/src/drag.c
index 67ccff4..582dbb1 100644
--- a/src/drag.c
+++ b/src/drag.c
@@ -42,7 +42,8 @@
static bool threshold_exceeded(uint32_t x1, uint32_t y1,
uint32_t x2, uint32_t y2) {
- const uint32_t threshold = 9;
+ /* The threshold is about the height of one window decoration. */
+ const uint32_t threshold = logical_px(15);
return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) > threshold * threshold;
}
diff --git a/src/ewmh.c b/src/ewmh.c
index c61fb5f..ba91093 100644
--- a/src/ewmh.c
+++ b/src/ewmh.c
@@ -278,10 +278,10 @@
*/
void ewmh_update_sticky(xcb_window_t window, bool sticky) {
if (sticky) {
- DLOG("Setting _NET_WM_STATE_STICKY for window = %d.\n", window);
+ DLOG("Setting _NET_WM_STATE_STICKY for window = %08x.\n", window);
xcb_add_property_atom(conn, window, A__NET_WM_STATE, A__NET_WM_STATE_STICKY);
} else {
- DLOG("Removing _NET_WM_STATE_STICKY for window = %d.\n", window);
+ DLOG("Removing _NET_WM_STATE_STICKY for window = %08x.\n", window);
xcb_remove_property_atom(conn, window, A__NET_WM_STATE, A__NET_WM_STATE_STICKY);
}
}
@@ -292,10 +292,10 @@
*/
void ewmh_update_focused(xcb_window_t window, bool is_focused) {
if (is_focused) {
- DLOG("Setting _NET_WM_STATE_FOCUSED for window = %d.\n", window);
+ DLOG("Setting _NET_WM_STATE_FOCUSED for window = %08x.\n", window);
xcb_add_property_atom(conn, window, A__NET_WM_STATE, A__NET_WM_STATE_FOCUSED);
} else {
- DLOG("Removing _NET_WM_STATE_FOCUSED for window = %d.\n", window);
+ DLOG("Removing _NET_WM_STATE_FOCUSED for window = %08x.\n", window);
xcb_remove_property_atom(conn, window, A__NET_WM_STATE, A__NET_WM_STATE_FOCUSED);
}
}
diff --git a/src/floating.c b/src/floating.c
index 02e6ac3..5eaf639 100644
--- a/src/floating.c
+++ b/src/floating.c
@@ -253,7 +253,7 @@
}
/* Consider the part of the focus stack of our current workspace:
* [ ... S_{i-1} S_{i} S_{i+1} ... ]
- * Where S_{x} is a container tree and the container 'con' that is beeing switched to
+ * Where S_{x} is a container tree and the container 'con' that is being switched to
* floating belongs in S_{i}. The new floating container, 'nc', will have the
* workspace as its parent so it needs to be placed in this stack. If C was focused
* we just need to call con_focus(). Otherwise, nc must be placed before or after S_{i}.
@@ -347,8 +347,9 @@
con->floating = FLOATING_USER_ON;
/* 4: set the border style as specified with new_float */
- if (automatic)
- con->border_style = config.default_floating_border;
+ if (automatic) {
+ con->border_style = con->max_user_border_style = config.default_floating_border;
+ }
/* Add pixels for the decoration. */
Rect border_style_rect = con_border_style_rect(con);
@@ -697,6 +698,10 @@
void floating_resize_window(Con *con, const bool proportional,
const xcb_button_press_event_t *event) {
DLOG("floating_resize_window\n");
+
+ /* Push changes before resizing, so that the window gets raised now and not
+ * after the user releases the mouse button */
+ tree_render();
/* corner saves the nearest corner to the original click. It contains
* a bitmask of the nearest borders (BORDER_LEFT, BORDER_RIGHT, …) */
diff --git a/src/handlers.c b/src/handlers.c
index eba5fe2..b0354d1 100644
--- a/src/handlers.c
+++ b/src/handlers.c
@@ -70,11 +70,9 @@
event->response_type != response_type)
continue;
- /* instead of removing a sequence number we better wait until it gets
- * garbage collected. it may generate multiple events (there are multiple
- * enter_notifies for one configure_request, for example). */
- //SLIST_REMOVE(&ignore_events, event, Ignore_Event, ignore_events);
- //free(event);
+ /* Instead of removing & freeing a sequence number we better wait until
+ * it gets garbage collected. It may generate multiple events (there
+ * are multiple enter_notifies for one configure_request, for example). */
return true;
}
@@ -270,7 +268,7 @@
* Configure requests are received when the application wants to resize windows
* on their own.
*
- * We generate a synthethic configure notify event to signalize the client its
+ * We generate a synthetic configure notify event to signalize the client its
* "new" position.
*
*/
@@ -801,14 +799,14 @@
} else if (event->type == A_WM_CHANGE_STATE) {
/* http://tronche.com/gui/x/icccm/sec-4.html#s-4.1.4 */
if (event->data.data32[0] == XCB_ICCCM_WM_STATE_ICONIC) {
- /* For compatiblity reasons, Wine will request iconic state and cannot ensure that the WM has agreed on it;
+ /* For compatibility reasons, Wine will request iconic state and cannot ensure that the WM has agreed on it;
* immediately revert to normal to avoid being stuck in a paused state. */
- DLOG("Client has requested iconic state, rejecting. (window = %d)\n", event->window);
+ DLOG("Client has requested iconic state, rejecting. (window = %08x)\n", event->window);
long data[] = {XCB_ICCCM_WM_STATE_NORMAL, XCB_NONE};
xcb_change_property(conn, XCB_PROP_MODE_REPLACE, event->window,
A_WM_STATE, A_WM_STATE, 32, 2, data);
} else {
- DLOG("Not handling WM_CHANGE_STATE request. (window = %d, state = %d)\n", event->window, event->data.data32[0]);
+ DLOG("Not handling WM_CHANGE_STATE request. (window = %08x, state = %d)\n", event->window, event->data.data32[0]);
}
} else if (event->type == A__NET_CURRENT_DESKTOP) {
/* This request is used by pagers and bars to change the current
@@ -876,7 +874,7 @@
tree_close_internal(con, KILL_WINDOW, false);
tree_render();
} else {
- DLOG("Couldn't find con for _NET_CLOSE_WINDOW request. (window = %d)\n", event->window);
+ DLOG("Couldn't find con for _NET_CLOSE_WINDOW request. (window = %08x)\n", event->window);
}
} else if (event->type == A__NET_WM_MOVERESIZE) {
/*
@@ -885,7 +883,7 @@
*/
Con *con = con_by_window_id(event->window);
if (!con || !con_is_floating(con)) {
- DLOG("Couldn't find con for _NET_WM_MOVERESIZE request, or con not floating (window = %d)\n", event->window);
+ DLOG("Couldn't find con for _NET_WM_MOVERESIZE request, or con not floating (window = %08x)\n", event->window);
return;
}
DLOG("Handling _NET_WM_MOVERESIZE request (con = %p)\n", con);
@@ -1059,6 +1057,76 @@
}
/*
+ * Log FocusOut events.
+ *
+ */
+static void handle_focus_out(xcb_focus_in_event_t *event) {
+ Con *con = con_by_window_id(event->event);
+ const char *window_name, *mode, *detail;
+
+ if (con != NULL) {
+ window_name = con->name;
+ if (window_name == NULL) {
+ window_name = "";
+ }
+ } else if (event->event == root) {
+ window_name = "";
+ } else {
+ window_name = "";
+ }
+
+ switch (event->mode) {
+ case XCB_NOTIFY_MODE_NORMAL:
+ mode = "Normal";
+ break;
+ case XCB_NOTIFY_MODE_GRAB:
+ mode = "Grab";
+ break;
+ case XCB_NOTIFY_MODE_UNGRAB:
+ mode = "Ungrab";
+ break;
+ case XCB_NOTIFY_MODE_WHILE_GRABBED:
+ mode = "WhileGrabbed";
+ break;
+ default:
+ mode = "";
+ break;
+ }
+
+ switch (event->detail) {
+ case XCB_NOTIFY_DETAIL_ANCESTOR:
+ detail = "Ancestor";
+ break;
+ case XCB_NOTIFY_DETAIL_VIRTUAL:
+ detail = "Virtual";
+ break;
+ case XCB_NOTIFY_DETAIL_INFERIOR:
+ detail = "Inferior";
+ break;
+ case XCB_NOTIFY_DETAIL_NONLINEAR:
+ detail = "Nonlinear";
+ break;
+ case XCB_NOTIFY_DETAIL_NONLINEAR_VIRTUAL:
+ detail = "NonlinearVirtual";
+ break;
+ case XCB_NOTIFY_DETAIL_POINTER:
+ detail = "Pointer";
+ break;
+ case XCB_NOTIFY_DETAIL_POINTER_ROOT:
+ detail = "PointerRoot";
+ break;
+ case XCB_NOTIFY_DETAIL_NONE:
+ detail = "NONE";
+ break;
+ default:
+ detail = "unknown";
+ break;
+ }
+
+ DLOG("focus change out: window 0x%08x (con %p, %s) lost focus with detail=%s, mode=%s\n", event->event, con, window_name, detail, mode);
+}
+
+/*
* Handles ConfigureNotify events for the root window, which are generated when
* the monitor configuration changed.
*
@@ -1074,6 +1142,23 @@
return;
}
randr_query_outputs();
+
+ ipc_send_event("output", I3_IPC_EVENT_OUTPUT, "{\"change\":\"unspecified\"}");
+}
+
+/*
+ * Handles SelectionClear events for the root window, which are generated when
+ * we lose ownership of a selection.
+ */
+static void handle_selection_clear(xcb_selection_clear_event_t *event) {
+ if (event->selection != wm_sn) {
+ DLOG("SelectionClear for unknown selection %d, ignoring\n", event->selection);
+ return;
+ }
+ LOG("Lost WM_Sn selection, exiting.\n");
+ exit(EXIT_SUCCESS);
+
+ /* unreachable */
}
/*
@@ -1087,14 +1172,24 @@
}
/*
- * Handles the _MOTIF_WM_HINTS property of specifing window deocration settings.
+ * Handles the WM_CLIENT_MACHINE property for assignments and criteria selection.
+ *
+ */
+static bool handle_machine_change(Con *con, xcb_get_property_reply_t *prop) {
+ window_update_machine(con->window, prop);
+ con = remanage_window(con);
+ return true;
+}
+
+/*
+ * Handles the _MOTIF_WM_HINTS property of specifying window deocration settings.
*
*/
static bool handle_motif_hints_change(Con *con, xcb_get_property_reply_t *prop) {
border_style_t motif_border_style;
- window_update_motif_hints(con->window, prop, &motif_border_style);
-
- if (motif_border_style != con->border_style && motif_border_style != BS_NORMAL) {
+ bool has_mwm_hints = window_update_motif_hints(con->window, prop, &motif_border_style);
+
+ if (has_mwm_hints && motif_border_style != con->border_style) {
DLOG("Update border style of con %p to %d\n", con, motif_border_style);
con_set_border_style(con, motif_border_style, con->current_border_width);
@@ -1175,6 +1270,14 @@
return true;
}
+static bool handle_windowicon_change(Con *con, xcb_get_property_reply_t *prop) {
+ window_update_icon(con->window, prop);
+
+ x_push_changes(croot);
+
+ return true;
+}
+
/* Returns false if the event could not be processed (e.g. the window could not
* be found), true otherwise */
typedef bool (*cb_property_handler_t)(Con *con, xcb_get_property_reply_t *property);
@@ -1197,7 +1300,9 @@
{0, UINT_MAX, handle_strut_partial_change},
{0, UINT_MAX, handle_window_type},
{0, UINT_MAX, handle_i3_floating},
- {0, 5 * sizeof(uint64_t), handle_motif_hints_change}};
+ {0, 128, handle_machine_change},
+ {0, 5 * sizeof(uint64_t), handle_motif_hints_change},
+ {0, UINT_MAX, handle_windowicon_change}};
#define NUM_HANDLERS (sizeof(property_handlers) / sizeof(struct property_handler_t))
/*
@@ -1219,7 +1324,9 @@
property_handlers[8].atom = A__NET_WM_STRUT_PARTIAL;
property_handlers[9].atom = A__NET_WM_WINDOW_TYPE;
property_handlers[10].atom = A_I3_FLOATING_WINDOW;
- property_handlers[11].atom = A__MOTIF_WM_HINTS;
+ property_handlers[11].atom = XCB_ATOM_WM_CLIENT_MACHINE;
+ property_handlers[12].atom = A__MOTIF_WM_HINTS;
+ property_handlers[13].atom = A__NET_WM_ICON;
}
static void property_notify(uint8_t state, xcb_window_t window, xcb_atom_t atom) {
@@ -1237,7 +1344,7 @@
}
if (handler == NULL) {
- //DLOG("Unhandled property notify for atom %d (0x%08x)\n", atom, atom);
+ /* DLOG("Unhandled property notify for atom %d (0x%08x)\n", atom, atom); */
return;
}
@@ -1395,6 +1502,10 @@
handle_focus_in((xcb_focus_in_event_t *)event);
break;
+ case XCB_FOCUS_OUT:
+ handle_focus_out((xcb_focus_out_event_t *)event);
+ break;
+
case XCB_PROPERTY_NOTIFY: {
xcb_property_notify_event_t *e = (xcb_property_notify_event_t *)event;
last_timestamp = e->time;
@@ -1406,8 +1517,12 @@
handle_configure_notify((xcb_configure_notify_event_t *)event);
break;
+ case XCB_SELECTION_CLEAR:
+ handle_selection_clear((xcb_selection_clear_event_t *)event);
+ break;
+
default:
- //DLOG("Unhandled event of type %d\n", type);
- break;
- }
-}
+ /* DLOG("Unhandled event of type %d\n", type); */
+ break;
+ }
+}
diff --git a/src/ipc.c b/src/ipc.c
index eac4d9b..1473321 100644
--- a/src/ipc.c
+++ b/src/ipc.c
@@ -26,22 +26,6 @@
char *current_socketpath = NULL;
TAILQ_HEAD(ipc_client_head, ipc_client) all_clients = TAILQ_HEAD_INITIALIZER(all_clients);
-
-/*
- * Puts the given socket file descriptor into non-blocking mode or dies if
- * setting O_NONBLOCK failed. Non-blocking sockets are a good idea for our
- * IPC model because we should by no means block the window manager.
- *
- */
-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");
-}
static void ipc_client_timeout(EV_P_ ev_timer *w, int revents);
static void ipc_socket_writeable_cb(EV_P_ struct ev_io *w, int revents);
@@ -251,9 +235,9 @@
ystr(name);
y(map_open);
ystr("x");
- y(integer, r.x);
+ y(integer, (int32_t)r.x);
ystr("y");
- y(integer, r.y);
+ y(integer, (int32_t)r.y);
ystr("width");
y(integer, r.width);
ystr("height");
@@ -536,6 +520,9 @@
ystr(con->title_format);
}
+ ystr("window_icon_padding");
+ y(integer, con->window_icon_padding);
+
if (con->type == CT_WORKSPACE) {
ystr("num");
y(integer, con->num);
@@ -597,6 +584,7 @@
DUMP_PROPERTY("class", class_class);
DUMP_PROPERTY("instance", class_instance);
DUMP_PROPERTY("window_role", role);
+ DUMP_PROPERTY("machine", machine);
if (con->window->name != NULL) {
ystr("title");
@@ -686,6 +674,7 @@
DUMP_REGEX(instance);
DUMP_REGEX(window_role);
DUMP_REGEX(title);
+ DUMP_REGEX(machine);
#undef DUMP_REGEX
y(map_close);
@@ -1083,6 +1072,17 @@
ystr("loaded_config_file_name");
ystr(current_configpath);
+ ystr("included_config_file_names");
+ y(array_open);
+ IncludedFile *file;
+ TAILQ_FOREACH (file, &included_files, files) {
+ if (file == TAILQ_FIRST(&included_files)) {
+ /* Skip the first file, which is current_configpath. */
+ continue;
+ }
+ ystr(file->path);
+ }
+ y(array_close);
y(map_close);
const unsigned char *payload;
@@ -1265,7 +1265,22 @@
y(map_open);
ystr("config");
- ystr(current_config);
+ IncludedFile *file = TAILQ_FIRST(&included_files);
+ ystr(file->raw_contents);
+
+ ystr("included_configs");
+ y(array_open);
+ TAILQ_FOREACH (file, &included_files, files) {
+ y(map_open);
+ ystr("path");
+ ystr(file->path);
+ ystr("raw_contents");
+ ystr(file->raw_contents);
+ ystr("variable_replaced_contents");
+ ystr(file->variable_replaced_contents);
+ y(map_close);
+ }
+ y(array_close);
y(map_close);
@@ -1553,57 +1568,6 @@
}
/*
- * Creates the UNIX domain socket at the given path, sets it to non-blocking
- * mode, bind()s and listen()s on it.
- *
- */
-int ipc_create_socket(const char *filename) {
- int sockfd;
-
- FREE(current_socketpath);
-
- char *resolved = resolve_tilde(filename);
- DLOG("Creating IPC-socket at %s\n", resolved);
- char *copy = sstrdup(resolved);
- const char *dir = dirname(copy);
- if (!path_exists(dir))
- mkdirp(dir, DEFAULT_DIR_MODE);
- free(copy);
-
- /* Unlink the unix domain socket before */
- unlink(resolved);
-
- if ((sockfd = socket(AF_LOCAL, SOCK_STREAM, 0)) < 0) {
- perror("socket()");
- free(resolved);
- return -1;
- }
-
- (void)fcntl(sockfd, F_SETFD, FD_CLOEXEC);
-
- struct sockaddr_un addr;
- memset(&addr, 0, sizeof(struct sockaddr_un));
- addr.sun_family = AF_LOCAL;
- strncpy(addr.sun_path, resolved, sizeof(addr.sun_path) - 1);
- if (bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) {
- perror("bind()");
- free(resolved);
- return -1;
- }
-
- set_nonblock(sockfd);
-
- if (listen(sockfd, 5) < 0) {
- perror("listen()");
- free(resolved);
- return -1;
- }
-
- current_socketpath = resolved;
- return sockfd;
-}
-
-/*
* Generates a json workspace event. Returns a dynamically allocated yajl
* generator. Free with yajl_gen_free().
*/
diff --git a/src/load_layout.c b/src/load_layout.c
index c117f9c..9789fb3 100644
--- a/src/load_layout.c
+++ b/src/load_layout.c
@@ -290,6 +290,9 @@
} else if (strcasecmp(last_key, "title") == 0) {
current_swallow->title = regex_new(sval);
swallow_is_empty = false;
+ } else if (strcasecmp(last_key, "machine") == 0) {
+ current_swallow->machine = regex_new(sval);
+ swallow_is_empty = false;
} else {
ELOG("swallow key %s unknown\n", last_key);
}
@@ -456,6 +459,10 @@
if (strcasecmp(last_key, "current_border_width") == 0)
json_node->current_border_width = val;
+
+ if (strcasecmp(last_key, "window_icon_padding") == 0) {
+ json_node->window_icon_padding = val;
+ }
if (strcasecmp(last_key, "depth") == 0)
json_node->depth = val;
diff --git a/src/log.c b/src/log.c
index 326f82b..010d2a5 100644
--- a/src/log.c
+++ b/src/log.c
@@ -12,6 +12,10 @@
#include "all.h"
#include "shmlog.h"
+#include
+#include
+#include
+#include
#include
#include
#include
@@ -23,9 +27,6 @@
#include
#include
#include
-#if !defined(__OpenBSD__)
-#include
-#endif
#if defined(__APPLE__)
#include
@@ -60,6 +61,18 @@
/* Size (in bytes) of physical memory */
static long long physical_mem_bytes;
+typedef struct log_client {
+ int fd;
+
+ TAILQ_ENTRY(log_client)
+ clients;
+} log_client;
+
+TAILQ_HEAD(log_client_head, log_client)
+log_clients = TAILQ_HEAD_INITIALIZER(log_clients);
+
+void log_broadcast_to_clients(const char *message, size_t len);
+
/*
* Writes the offsets for the next write and for the last wrap to the
* shmlog_header.
@@ -124,7 +137,11 @@
* 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);
+ logbuffer_size = shmlog_size;
+ if (physical_mem_bytes * 0.01 < (long long)shmlog_size) {
+ logbuffer_size = physical_mem_bytes * 0.01;
+ }
+
#if defined(__FreeBSD__)
sasprintf(&shmlogname, "/tmp/i3-log-%d", getpid());
#else
@@ -160,14 +177,6 @@
memset(logbuffer, '\0', logbuffer_size);
header = (i3_shmlog_header *)logbuffer;
-
-#if !defined(__OpenBSD__)
- pthread_condattr_t cond_attr;
- pthread_condattr_init(&cond_attr);
- if (pthread_condattr_setpshared(&cond_attr, PTHREAD_PROCESS_SHARED) != 0)
- fprintf(stderr, "pthread_condattr_setpshared() failed, i3-dump-log -f will not work!\n");
- pthread_cond_init(&(header->condvar), &cond_attr);
-#endif
logwalk = logbuffer + sizeof(i3_shmlog_header);
loglastwrap = logbuffer + logbuffer_size;
@@ -283,13 +292,10 @@
store_log_markers();
-#if !defined(__OpenBSD__)
- /* Wake up all (i3-dump-log) processes waiting for condvar. */
- pthread_cond_broadcast(&(header->condvar));
-#endif
-
if (print)
fwrite(message, len, 1, stdout);
+
+ log_broadcast_to_clients(message, len);
}
}
@@ -370,3 +376,51 @@
rmdir(errorfilename);
}
}
+
+char *current_log_stream_socket_path = NULL;
+
+/*
+ * Handler for activity on the listening socket, meaning that a new client
+ * has just connected and we should accept() them. Sets up the event handler
+ * for activity on the new connection and inserts the file descriptor into
+ * the list of log clients.
+ *
+ */
+void log_new_client(EV_P_ struct ev_io *w, int revents) {
+ struct sockaddr_un peer;
+ socklen_t len = sizeof(struct sockaddr_un);
+ int fd;
+ if ((fd = accept(w->fd, (struct sockaddr *)&peer, &len)) < 0) {
+ if (errno != EINTR) {
+ perror("accept()");
+ }
+ return;
+ }
+
+ /* Close this file descriptor on exec() */
+ (void)fcntl(fd, F_SETFD, FD_CLOEXEC);
+
+ set_nonblock(fd);
+
+ log_client *client = scalloc(1, sizeof(log_client));
+ client->fd = fd;
+ TAILQ_INSERT_TAIL(&log_clients, client, clients);
+
+ DLOG("log: new client connected on fd %d\n", fd);
+}
+
+void log_broadcast_to_clients(const char *message, size_t len) {
+ log_client *current = TAILQ_FIRST(&log_clients);
+ while (current != TAILQ_END(&log_clients)) {
+ /* XXX: In case slow connections turn out to be a problem here
+ * (unlikely as long as i3-dump-log is the only consumer), introduce
+ * buffering, similar to the IPC interface. */
+ ssize_t n = writeall(current->fd, message, len);
+ log_client *previous = current;
+ current = TAILQ_NEXT(current, clients);
+ if (n < 0) {
+ TAILQ_REMOVE(&log_clients, previous, clients);
+ free(previous);
+ }
+ }
+}
diff --git a/src/main.c b/src/main.c
index 2201955..6d6b9c9 100644
--- a/src/main.c
+++ b/src/main.c
@@ -24,6 +24,9 @@
#include
#include
#include
+#include
+#include
+#include
#ifdef I3_ASAN_ENABLED
#include
@@ -62,6 +65,9 @@
xcb_screen_t *root_screen;
xcb_window_t root;
+
+xcb_window_t wm_sn_selection_owner;
+xcb_atom_t wm_sn;
/* Color depth, visual id and colormap to use when creating windows and
* pixmaps. Will use 32 bit depth and an appropriate visual, if available,
@@ -181,6 +187,9 @@
}
ipc_shutdown(SHUTDOWN_REASON_EXIT, -1);
unlink(config.ipc_socket_path);
+ if (current_log_stream_socket_path != NULL) {
+ unlink(current_log_stream_socket_path);
+ }
xcb_disconnect(conn);
/* If a nagbar is active, kill it */
@@ -279,6 +288,7 @@
char *fake_outputs = NULL;
bool disable_signalhandler = false;
bool only_check_config = false;
+ bool replace_wm = false;
static struct option long_options[] = {
{"no-autostart", no_argument, 0, 'a'},
{"config", required_argument, 0, 'c'},
@@ -301,6 +311,7 @@
{"fake_outputs", required_argument, 0, 0},
{"fake-outputs", required_argument, 0, 0},
{"force-old-config-parser-v4.4-only", no_argument, 0, 0},
+ {"replace", no_argument, 0, 'r'},
{0, 0, 0, 0}};
int option_index = 0, opt;
@@ -325,7 +336,7 @@
start_argv = argv;
- while ((opt = getopt_long(argc, argv, "c:CvmaL:hld:V", long_options, &option_index)) != -1) {
+ while ((opt = getopt_long(argc, argv, "c:CvmaL:hld:Vr", long_options, &option_index)) != -1) {
switch (opt) {
case 'a':
LOG("Autostart disabled using -a\n");
@@ -362,6 +373,9 @@
break;
case 'l':
/* DEPRECATED, ignored for the next 3 versions (3.e, 3.f, 3.g) */
+ break;
+ case 'r':
+ replace_wm = true;
break;
case 0:
if (strcmp(long_options[option_index].name, "force-xinerama") == 0 ||
@@ -443,6 +457,9 @@
"\tThe default is %d bytes.\n",
shmlog_size);
fprintf(stderr, "\n");
+ fprintf(stderr, "\t--replace\n"
+ "\tReplace an existing window manager.\n");
+ fprintf(stderr, "\n");
fprintf(stderr, "If you pass plain text arguments, i3 will interpret them as a command\n"
"to send to a currently running i3 (like i3-msg). This allows you to\n"
"use nice and logical commands, such as:\n"
@@ -570,6 +587,21 @@
root_screen = xcb_aux_get_screen(conn, conn_screen);
root = root_screen->root;
+ /* Prefetch X11 extensions that we are interested in. */
+ xcb_prefetch_extension_data(conn, &xcb_xkb_id);
+ xcb_prefetch_extension_data(conn, &xcb_shape_id);
+ /* BIG-REQUESTS is used by libxcb internally. */
+ xcb_prefetch_extension_data(conn, &xcb_big_requests_id);
+ if (force_xinerama) {
+ xcb_prefetch_extension_data(conn, &xcb_xinerama_id);
+ } else {
+ xcb_prefetch_extension_data(conn, &xcb_randr_id);
+ }
+
+ /* Prepare for us to get a current timestamp as recommended by ICCCM */
+ xcb_change_window_attributes(conn, root, XCB_CW_EVENT_MASK, (uint32_t[]){XCB_EVENT_MASK_PROPERTY_CHANGE});
+ xcb_change_property(conn, XCB_PROP_MODE_APPEND, root, XCB_ATOM_SUPERSCRIPT_X, XCB_ATOM_CARDINAL, 32, 0, "");
+
/* 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);
@@ -599,6 +631,8 @@
visual_type = get_visualtype(root_screen);
}
+ xcb_prefetch_maximum_request_length(conn);
+
init_dpi();
DLOG("root_depth = %d, visual_id = 0x%08x.\n", root_depth, visual_type->visual_id);
@@ -608,6 +642,22 @@
xcb_get_geometry_cookie_t gcookie = xcb_get_geometry(conn, root);
xcb_query_pointer_cookie_t pointercookie = xcb_query_pointer(conn, root);
+
+ /* Get the PropertyNotify event we caused above */
+ xcb_flush(conn);
+ {
+ xcb_generic_event_t *event;
+ DLOG("waiting for PropertyNotify event\n");
+ while ((event = xcb_wait_for_event(conn)) != NULL) {
+ if (event->response_type == XCB_PROPERTY_NOTIFY) {
+ last_timestamp = ((xcb_property_notify_event_t *)event)->time;
+ free(event);
+ break;
+ }
+ free(event);
+ }
+ DLOG("got timestamp %d\n", last_timestamp);
+ }
/* Setup NetWM atoms */
#define xmacro(name) \
@@ -633,9 +683,111 @@
else
config.ipc_socket_path = sstrdup(config.ipc_socket_path);
}
+ /* Create the UNIX domain socket for IPC */
+ int ipc_socket = create_socket(config.ipc_socket_path, ¤t_socketpath);
+ if (ipc_socket == -1) {
+ die("Could not create the IPC socket: %s", config.ipc_socket_path);
+ }
if (config.force_xinerama) {
force_xinerama = true;
+ }
+
+ /* Acquire the WM_Sn selection. */
+ {
+ /* Get the WM_Sn atom */
+ char *atom_name = xcb_atom_name_by_screen("WM", conn_screen);
+ wm_sn_selection_owner = xcb_generate_id(conn);
+
+ if (atom_name == NULL) {
+ ELOG("xcb_atom_name_by_screen(\"WM\", %d) failed, exiting\n", conn_screen);
+ return 1;
+ }
+
+ xcb_intern_atom_reply_t *atom_reply;
+ atom_reply = xcb_intern_atom_reply(conn,
+ xcb_intern_atom_unchecked(conn,
+ 0,
+ strlen(atom_name),
+ atom_name),
+ NULL);
+ free(atom_name);
+ if (atom_reply == NULL) {
+ ELOG("Failed to intern the WM_Sn atom, exiting\n");
+ return 1;
+ }
+ wm_sn = atom_reply->atom;
+ free(atom_reply);
+
+ /* Check if the selection is already owned */
+ xcb_get_selection_owner_reply_t *selection_reply =
+ xcb_get_selection_owner_reply(conn,
+ xcb_get_selection_owner(conn, wm_sn),
+ NULL);
+ if (selection_reply && selection_reply->owner != XCB_NONE && !replace_wm) {
+ ELOG("Another window manager is already running (WM_Sn is owned)");
+ return 1;
+ }
+
+ /* Become the selection owner */
+ xcb_create_window(conn,
+ root_screen->root_depth,
+ wm_sn_selection_owner, /* window id */
+ root_screen->root, /* parent */
+ -1, -1, 1, 1, /* geometry */
+ 0, /* border width */
+ XCB_WINDOW_CLASS_INPUT_OUTPUT,
+ root_screen->root_visual,
+ 0, NULL);
+ xcb_change_property(conn,
+ XCB_PROP_MODE_REPLACE,
+ wm_sn_selection_owner,
+ XCB_ATOM_WM_CLASS,
+ XCB_ATOM_STRING,
+ 8,
+ (strlen("i3-WM_Sn") + 1) * 2,
+ "i3-WM_Sn\0i3-WM_Sn\0");
+
+ xcb_set_selection_owner(conn, wm_sn_selection_owner, wm_sn, last_timestamp);
+
+ if (selection_reply && selection_reply->owner != XCB_NONE) {
+ unsigned int usleep_time = 100000; /* 0.1 seconds */
+ int check_rounds = 150; /* Wait for a maximum of 15 seconds */
+ xcb_get_geometry_reply_t *geom_reply = NULL;
+
+ DLOG("waiting for old WM_Sn selection owner to exit");
+ do {
+ free(geom_reply);
+ usleep(usleep_time);
+ if (check_rounds-- == 0) {
+ ELOG("The old window manager is not exiting");
+ return 1;
+ }
+ geom_reply = xcb_get_geometry_reply(conn,
+ xcb_get_geometry(conn, selection_reply->owner),
+ NULL);
+ } while (geom_reply != NULL);
+ }
+ free(selection_reply);
+
+ /* Announce that we are the new owner */
+ /* Every X11 event is 32 bytes long. Therefore, XCB will copy 32 bytes.
+ * In order to properly initialize these bytes, we allocate 32 bytes even
+ * though we only need less for an xcb_client_message_event_t */
+ union {
+ xcb_client_message_event_t message;
+ char storage[32];
+ } event;
+ memset(&event, 0, sizeof(event));
+ event.message.response_type = XCB_CLIENT_MESSAGE;
+ event.message.window = root_screen->root;
+ event.message.format = 32;
+ event.message.type = A_MANAGER;
+ event.message.data.data32[0] = last_timestamp;
+ event.message.data.data32[1] = wm_sn;
+ event.message.data.data32[2] = wm_sn_selection_owner;
+
+ xcb_send_event(conn, 0, root_screen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY, event.storage);
}
xcb_void_cookie_t cookie;
@@ -663,9 +815,6 @@
xcursor_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) {
@@ -844,19 +993,26 @@
tree_render();
- /* Create the UNIX domain socket for IPC */
- int ipc_socket = ipc_create_socket(config.ipc_socket_path);
- if (ipc_socket == -1) {
- ELOG("Could not create the IPC socket, IPC disabled\n");
+ /* Listen to the IPC socket for clients */
+ struct ev_io *ipc_io = scalloc(1, sizeof(struct ev_io));
+ ev_io_init(ipc_io, ipc_new_client, ipc_socket, EV_READ);
+ ev_io_start(main_loop, ipc_io);
+
+ /* Chose a file name in /tmp/ based on the PID */
+ char *log_stream_socket_path = get_process_filename("log-stream-socket");
+ int log_socket = create_socket(log_stream_socket_path, ¤t_log_stream_socket_path);
+ free(log_stream_socket_path);
+ if (log_socket == -1) {
+ ELOG("Could not create the log socket, i3-dump-log -f will not work\n");
} else {
- struct ev_io *ipc_io = scalloc(1, sizeof(struct ev_io));
- ev_io_init(ipc_io, ipc_new_client, ipc_socket, EV_READ);
- ev_io_start(main_loop, ipc_io);
- }
-
- /* Also handle the UNIX domain sockets passed via socket activation. The
- * parameter 1 means "remove the environment variables", we don’t want to
- * pass these to child processes. */
+ struct ev_io *log_io = scalloc(1, sizeof(struct ev_io));
+ ev_io_init(log_io, log_new_client, log_socket, EV_READ);
+ ev_io_start(main_loop, log_io);
+ }
+
+ /* Also handle the UNIX domain sockets passed via socket
+ * activation. The parameter 0 means "do not remove the
+ * environment variables", we need to be able to reexec. */
listen_fds = sd_listen_fds(0);
if (listen_fds < 0)
ELOG("socket activation: Error in sd_listen_fds\n");
@@ -950,24 +1106,23 @@
xcb_ungrab_server(conn);
if (autostart) {
- LOG("This is not an in-place restart, copying root window contents to a pixmap\n");
+ /* When the root's window background is set to NONE, that might mean
+ * that old content stays visible when a window is closed. That has
+ * unpleasant effect of "my terminal (does not seem to) close!".
+ *
+ * There does not seem to be an easy way to query for this problem, so
+ * we test for it: Open & close a window and check if the background is
+ * redrawn or the window contents stay visible.
+ */
+ LOG("This is not an in-place restart, checking if a wallpaper is set.\n");
+
xcb_screen_t *root = xcb_aux_get_screen(conn, conn_screen);
- uint16_t width = root->width_in_pixels;
- uint16_t height = root->height_in_pixels;
- xcb_pixmap_t pixmap = xcb_generate_id(conn);
- xcb_gcontext_t gc = xcb_generate_id(conn);
-
- xcb_create_pixmap(conn, root->root_depth, pixmap, root->root, width, height);
-
- xcb_create_gc(conn, gc, root->root,
- XCB_GC_FUNCTION | XCB_GC_PLANE_MASK | XCB_GC_FILL_STYLE | XCB_GC_SUBWINDOW_MODE,
- (uint32_t[]){XCB_GX_COPY, ~0, XCB_FILL_STYLE_SOLID, XCB_SUBWINDOW_MODE_INCLUDE_INFERIORS});
-
- xcb_copy_area(conn, root->root, pixmap, gc, 0, 0, 0, 0, width, height);
- xcb_change_window_attributes(conn, root->root, XCB_CW_BACK_PIXMAP, (uint32_t[]){pixmap});
- xcb_flush(conn);
- xcb_free_gc(conn, gc);
- xcb_free_pixmap(conn, pixmap);
+ if (is_background_set(conn, root)) {
+ LOG("A wallpaper is set, so no screenshot is necessary.\n");
+ } else {
+ LOG("No wallpaper set, copying root window contents to a pixmap\n");
+ set_screenshot_as_wallpaper(conn, root);
+ }
}
#if defined(__OpenBSD__)
@@ -1041,5 +1196,6 @@
* when calling exit() */
atexit(i3_exit);
+ sd_notify(1, "READY=1");
ev_loop(main_loop, 0);
}
diff --git a/src/manage.c b/src/manage.c
index 82c0ff8..6125af3 100644
--- a/src/manage.c
+++ b/src/manage.c
@@ -116,11 +116,14 @@
utf8_title_cookie, title_cookie,
class_cookie, leader_cookie, transient_cookie,
role_cookie, startup_id_cookie, wm_hints_cookie,
- wm_normal_hints_cookie, motif_wm_hints_cookie, wm_user_time_cookie, wm_desktop_cookie;
+ wm_normal_hints_cookie, motif_wm_hints_cookie, wm_user_time_cookie, wm_desktop_cookie,
+ wm_machine_cookie;
+
+ xcb_get_property_cookie_t wm_icon_cookie;
geomc = xcb_get_geometry(conn, d);
- /* Check if the window is mapped (it could be not mapped when intializing and
+ /* Check if the window is mapped (it could be not mapped when initializing and
calling manage_window() for every window) */
if ((attr = xcb_get_window_attributes_reply(conn, cookie, 0)) == NULL) {
DLOG("Could not get attributes\n");
@@ -189,6 +192,8 @@
motif_wm_hints_cookie = GET_PROPERTY(A__MOTIF_WM_HINTS, 5 * sizeof(uint64_t));
wm_user_time_cookie = GET_PROPERTY(A__NET_WM_USER_TIME, UINT32_MAX);
wm_desktop_cookie = GET_PROPERTY(A__NET_WM_DESKTOP, UINT32_MAX);
+ wm_machine_cookie = GET_PROPERTY(XCB_ATOM_WM_CLIENT_MACHINE, UINT32_MAX);
+ wm_icon_cookie = GET_PROPERTY(A__NET_WM_ICON, UINT32_MAX);
i3Window *cwindow = scalloc(1, sizeof(i3Window));
cwindow->id = window;
@@ -202,15 +207,17 @@
window_update_class(cwindow, xcb_get_property_reply(conn, class_cookie, NULL));
window_update_name_legacy(cwindow, xcb_get_property_reply(conn, title_cookie, NULL));
window_update_name(cwindow, xcb_get_property_reply(conn, utf8_title_cookie, NULL));
+ window_update_icon(cwindow, xcb_get_property_reply(conn, wm_icon_cookie, NULL));
window_update_leader(cwindow, xcb_get_property_reply(conn, leader_cookie, NULL));
window_update_transient_for(cwindow, xcb_get_property_reply(conn, transient_cookie, NULL));
window_update_strut_partial(cwindow, xcb_get_property_reply(conn, strut_cookie, NULL));
window_update_role(cwindow, xcb_get_property_reply(conn, role_cookie, NULL));
bool urgency_hint;
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);
+ border_style_t motif_border_style;
+ bool has_mwm_hints = window_update_motif_hints(cwindow, xcb_get_property_reply(conn, motif_wm_hints_cookie, NULL), &motif_border_style);
window_update_normal_hints(cwindow, xcb_get_property_reply(conn, wm_normal_hints_cookie, NULL), geom);
+ window_update_machine(cwindow, xcb_get_property_reply(conn, wm_machine_cookie, NULL));
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);
@@ -478,34 +485,17 @@
(cwindow->leader != XCB_NONE &&
cwindow->leader != cwindow->id &&
con_by_window_id(cwindow->leader) != NULL)) {
- LOG("This window is transient for another window, setting floating\n");
+ DLOG("This window is transient for another window, setting floating\n");
want_floating = true;
if (config.popup_during_fullscreen == PDF_LEAVE_FULLSCREEN &&
fs != NULL) {
- LOG("There is a fullscreen window, leaving fullscreen mode\n");
+ DLOG("There is a fullscreen window, leaving fullscreen mode\n");
con_toggle_fullscreen(fs, CF_OUTPUT);
} else if (config.popup_during_fullscreen == PDF_SMART &&
fs != NULL &&
fs->window != NULL) {
- i3Window *transient_win = cwindow;
- while (transient_win != NULL &&
- transient_win->transient_for != XCB_NONE) {
- if (transient_win->transient_for == fs->window->id) {
- LOG("This floating window belongs to the fullscreen window (popup_during_fullscreen == smart)\n");
- set_focus = true;
- break;
- }
- Con *next_transient = con_by_window_id(transient_win->transient_for);
- if (next_transient == NULL)
- break;
- /* Some clients (e.g. x11-ssh-askpass) actually set
- * WM_TRANSIENT_FOR to their own window id, so break instead of
- * looping endlessly. */
- if (transient_win == next_transient->window)
- break;
- transient_win = next_transient->window;
- }
+ set_focus = con_find_transient_for_window(nc, fs->window->id);
}
}
@@ -521,23 +511,19 @@
if (nc->geometry.width == 0)
nc->geometry = (Rect){geom->x, geom->y, geom->width, geom->height};
- if (motif_border_style != BS_NORMAL) {
+ if (want_floating) {
+ DLOG("geometry = %d x %d\n", nc->geometry.width, nc->geometry.height);
+ if (floating_enable(nc, true)) {
+ nc->floating = FLOATING_AUTO_ON;
+ }
+ }
+
+ if (has_mwm_hints) {
DLOG("MOTIF_WM_HINTS specifies decorations (border_style = %d)\n", motif_border_style);
if (want_floating) {
con_set_border_style(nc, motif_border_style, config.default_floating_border_width);
} else {
con_set_border_style(nc, motif_border_style, config.default_border_width);
- }
- }
-
- if (want_floating) {
- DLOG("geometry = %d x %d\n", nc->geometry.width, nc->geometry.height);
- /* automatically set the border to the default value if a motif border
- * was not specified */
- bool automatic_border = (motif_border_style == BS_NORMAL);
-
- if (floating_enable(nc, automatic_border)) {
- nc->floating = FLOATING_AUTO_ON;
}
}
diff --git a/src/match.c b/src/match.c
index 6ac312e..34314e2 100644
--- a/src/match.c
+++ b/src/match.c
@@ -47,12 +47,14 @@
match->instance == NULL &&
match->window_role == NULL &&
match->workspace == NULL &&
+ match->machine == NULL &&
match->urgent == U_DONTCHECK &&
match->id == XCB_NONE &&
match->window_type == UINT32_MAX &&
match->con_id == NULL &&
match->dock == M_NODOCK &&
- match->window_mode == WM_ANY);
+ match->window_mode == WM_ANY &&
+ match->match_all_windows == false);
}
/*
@@ -130,6 +132,8 @@
}
}
+ CHECK_WINDOW_FIELD(machine, machine, str);
+
Con *con = NULL;
if (match->urgent == U_LATEST) {
/* if the window isn't urgent, no sense in searching */
@@ -256,6 +260,10 @@
LOG("window_mode matches\n");
}
+
+ /* NOTE: See the comment regarding 'all' in match_parse_property()
+ * for an explanation of why match_all_windows isn't explicitly
+ * checked. */
return true;
}
@@ -273,6 +281,7 @@
regex_free(match->mark);
regex_free(match->window_role);
regex_free(match->workspace);
+ regex_free(match->machine);
}
/*
@@ -390,6 +399,12 @@
return;
}
+ if (strcmp(ctype, "machine") == 0) {
+ regex_free(match->machine);
+ match->machine = regex_new(cvalue);
+ return;
+ }
+
if (strcmp(ctype, "tiling") == 0) {
match->window_mode = WM_TILING;
return;
@@ -428,5 +443,16 @@
return;
}
+ /* match_matches_window() only checks negatively, so match_all_windows
+ * won't actually be used there, but that's OK because if no negative
+ * match is found (e.g. because of a more restrictive criterion) the
+ * return value of match_matches_window() is true.
+ * Setting it here only serves to cause match_is_empty() to return false,
+ * otherwise empty criteria rules apply, and that's not what we want. */
+ if (strcmp(ctype, "all") == 0) {
+ match->match_all_windows = true;
+ return;
+ }
+
ELOG("Unknown criterion: %s\n", ctype);
}
diff --git a/src/move.c b/src/move.c
index 967b4c0..1a16164 100644
--- a/src/move.c
+++ b/src/move.c
@@ -228,10 +228,19 @@
* the focused container, con, is now a child of ws. To work around this
* and still produce the correct workspace focus events (see
* 517-regress-move-direction-ipc.t) we need to temporarily set focused
- * to the old workspace. */
+ * to the old workspace.
+ *
+ * The following happen:
+ * 1. Focus con to push it on the top of the focus stack in its new
+ * workspace
+ * 2. Set focused to the old workspace to force workspace_show to
+ * execute
+ * 3. workspace_show will descend focus and target our con for
+ * focusing. This also ensures that the mouse warps correctly.
+ * See: #3518. */
+ con_focus(con);
focused = old_ws;
workspace_show(ws);
- con_focus(con);
}
/* force re-painting the indicators */
@@ -320,6 +329,9 @@
} else {
TAILQ_SWAP(con, swap, &(swap->parent->nodes_head), nodes);
}
+
+ /* redraw parents to ensure all parent split container titles are updated correctly */
+ con_force_split_parents_redraw(con);
ipc_send_window_event("move", con);
return;
diff --git a/src/randr.c b/src/randr.c
index b4d1d09..26b4ca3 100644
--- a/src/randr.c
+++ b/src/randr.c
@@ -665,11 +665,12 @@
new->primary = monitor_info->primary;
- new->changed =
- update_if_necessary(&(new->rect.x), monitor_info->x) |
- update_if_necessary(&(new->rect.y), monitor_info->y) |
- update_if_necessary(&(new->rect.width), monitor_info->width) |
- update_if_necessary(&(new->rect.height), monitor_info->height);
+ const bool update_x = update_if_necessary(&(new->rect.x), monitor_info->x);
+ const bool update_y = update_if_necessary(&(new->rect.y), monitor_info->y);
+ const bool update_w = update_if_necessary(&(new->rect.width), monitor_info->width);
+ const bool update_h = update_if_necessary(&(new->rect.height), monitor_info->height);
+
+ new->changed = update_x || update_y || update_w || update_h;
DLOG("name %s, x %d, y %d, width %d px, height %d px, width %d mm, height %d mm, primary %d, automatic %d\n",
name,
@@ -743,10 +744,11 @@
return;
}
- bool updated = update_if_necessary(&(new->rect.x), crtc->x) |
- update_if_necessary(&(new->rect.y), crtc->y) |
- update_if_necessary(&(new->rect.width), crtc->width) |
- update_if_necessary(&(new->rect.height), crtc->height);
+ const bool update_x = update_if_necessary(&(new->rect.x), crtc->x);
+ const bool update_y = update_if_necessary(&(new->rect.y), crtc->y);
+ const bool update_w = update_if_necessary(&(new->rect.width), crtc->width);
+ const bool update_h = update_if_necessary(&(new->rect.height), crtc->height);
+ const bool updated = update_x || update_y || update_w || update_h;
free(crtc);
new->active = (new->rect.width != 0 && new->rect.height != 0);
if (!new->active) {
@@ -943,9 +945,11 @@
uint32_t width = min(other->rect.width, output->rect.width);
uint32_t height = min(other->rect.height, output->rect.height);
- if (update_if_necessary(&(output->rect.width), width) |
- update_if_necessary(&(output->rect.height), height))
+ const bool update_w = update_if_necessary(&(output->rect.width), width);
+ const bool update_h = update_if_necessary(&(output->rect.height), height);
+ if (update_w || update_h) {
output->changed = true;
+ }
update_if_necessary(&(other->rect.width), width);
update_if_necessary(&(other->rect.height), height);
diff --git a/src/regex.c b/src/regex.c
index 8f03915..66ae511 100644
--- a/src/regex.c
+++ b/src/regex.c
@@ -20,33 +20,22 @@
*
*/
struct regex *regex_new(const char *pattern) {
- const char *error;
- int errorcode, offset;
+ int errorcode;
+ PCRE2_SIZE offset;
struct regex *re = scalloc(1, sizeof(struct regex));
re->pattern = sstrdup(pattern);
- int options = PCRE_UTF8;
+ uint32_t options = PCRE2_UTF;
/* We use PCRE_UCP so that \B, \b, \D, \d, \S, \s, \W, \w and some POSIX
* character classes play nicely with Unicode */
- options |= PCRE_UCP;
- while (!(re->regex = pcre_compile2(pattern, options, &errorcode, &error, &offset, NULL))) {
- /* If the error is that PCRE was not compiled with UTF-8 support we
- * disable it and try again */
- if (errorcode == 32) {
- options &= ~PCRE_UTF8;
- continue;
- }
- ELOG("PCRE regular expression compilation failed at %d: %s\n",
- offset, error);
+ options |= PCRE2_UCP;
+ if (!(re->regex = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, options, &errorcode, &offset, NULL))) {
+ PCRE2_UCHAR buffer[256];
+ pcre2_get_error_message(errorcode, buffer, sizeof(buffer));
+ ELOG("PCRE regular expression compilation failed at %lu: %s\n",
+ offset, buffer);
regex_free(re);
return NULL;
- }
- re->extra = pcre_study(re->regex, 0, &error);
- /* If an error happened, we print the error message, but continue.
- * Studying the regular expression leads to faster matching, but it’s not
- * absolutely necessary. */
- if (error) {
- ELOG("PCRE regular expression studying failed: %s\n", error);
}
return re;
}
@@ -60,7 +49,6 @@
return;
FREE(regex->pattern);
FREE(regex->regex);
- FREE(regex->extra);
FREE(regex);
}
@@ -71,17 +59,22 @@
*
*/
bool regex_matches(struct regex *regex, const char *input) {
+ pcre2_match_data *match_data;
int rc;
+
+ match_data = pcre2_match_data_create_from_pattern(regex->regex, NULL);
/* We use strlen() because pcre_exec() expects the length of the input
* string in bytes */
- if ((rc = pcre_exec(regex->regex, regex->extra, input, strlen(input), 0, 0, NULL, 0)) == 0) {
+ rc = pcre2_match(regex->regex, (PCRE2_SPTR)input, strlen(input), 0, 0, match_data, NULL);
+ pcre2_match_data_free(match_data);
+ if (rc > 0) {
LOG("Regular expression \"%s\" matches \"%s\"\n",
regex->pattern, input);
return true;
}
- if (rc == PCRE_ERROR_NOMATCH) {
+ if (rc == PCRE2_ERROR_NOMATCH) {
LOG("Regular expression \"%s\" does not match \"%s\"\n",
regex->pattern, input);
return false;
diff --git a/src/render.c b/src/render.c
index 072daeb..6917efd 100644
--- a/src/render.c
+++ b/src/render.c
@@ -255,34 +255,12 @@
}
Con *floating_child = con_descend_focused(child);
- Con *transient_con = floating_child;
- bool is_transient_for = false;
- while (transient_con != NULL &&
- transient_con->window != NULL &&
- transient_con->window->transient_for != XCB_NONE) {
- DLOG("transient_con = 0x%08x, transient_con->window->transient_for = 0x%08x, fullscreen_id = 0x%08x\n",
- transient_con->window->id, transient_con->window->transient_for, fullscreen->window->id);
- if (transient_con->window->transient_for == fullscreen->window->id) {
- is_transient_for = true;
- break;
- }
- Con *next_transient = con_by_window_id(transient_con->window->transient_for);
- if (next_transient == NULL)
- break;
- /* Some clients (e.g. x11-ssh-askpass) actually set
- * WM_TRANSIENT_FOR to their own window id, so break instead of
- * looping endlessly. */
- if (transient_con == next_transient)
- break;
- transient_con = next_transient;
- }
-
- if (!is_transient_for)
- continue;
- else {
+ if (con_find_transient_for_window(floating_child, fullscreen->window->id)) {
DLOG("Rendering floating child even though in fullscreen mode: "
"floating->transient_for (0x%08x) --> fullscreen->id (0x%08x)\n",
floating_child->window->transient_for, fullscreen->window->id);
+ } else {
+ continue;
}
}
DLOG("floating child at (%d,%d) with %d x %d\n",
diff --git a/src/restore_layout.c b/src/restore_layout.c
index c51bfcb..6f35d16 100644
--- a/src/restore_layout.c
+++ b/src/restore_layout.c
@@ -134,7 +134,6 @@
draw_util_clear_surface(&(state->surface), background);
// TODO: make i3font functions per-connection, at least these two for now…?
- xcb_flush(restore_conn);
xcb_aux_sync(restore_conn);
Match *swallows;
@@ -153,6 +152,7 @@
APPEND_REGEX(instance);
APPEND_REGEX(window_role);
APPEND_REGEX(title);
+ APPEND_REGEX(machine);
if (serialized == NULL) {
DLOG("This swallows specification is not serializable?!\n");
@@ -179,7 +179,6 @@
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(restore_conn);
xcb_aux_sync(restore_conn);
}
diff --git a/src/tiling_drag.c b/src/tiling_drag.c
new file mode 100644
index 0000000..a35a66e
--- /dev/null
+++ b/src/tiling_drag.c
@@ -0,0 +1,436 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ * © 2009 Michael Stapelberg and contributors (see also: LICENSE)
+ *
+ * tiling_drag.c: Reposition tiled windows by dragging.
+ *
+ */
+#include "all.h"
+static xcb_window_t create_drop_indicator(Rect rect);
+
+/*
+ * Includes decoration (container title) to the container's rect. This way we
+ * can find the correct drop target if the mouse is on a container's
+ * decoration.
+ *
+ */
+static Rect con_rect_plus_deco_height(Con *con) {
+ Rect rect = con->rect;
+ rect.height += con->deco_rect.height;
+ if (rect.y < con->deco_rect.height) {
+ rect.y = 0;
+ } else {
+ rect.y -= con->deco_rect.height;
+ }
+ return rect;
+}
+
+static bool is_tiling_drop_target(Con *con) {
+ if (!con_has_managed_window(con) ||
+ con_is_floating(con) ||
+ con_is_hidden(con)) {
+ return false;
+ }
+ Con *ws = con_get_workspace(con);
+ if (con_is_internal(ws)) {
+ /* Skip containers on i3-internal containers like the scratchpad, which are
+ technically visible on their pseudo-output. */
+ return false;
+ }
+ if (!workspace_is_visible(ws)) {
+ return false;
+ }
+ Con *fs = con_get_fullscreen_covering_ws(ws);
+ if (fs != NULL && fs != con) {
+ /* Workspace is visible, but con is not visible because some other
+ container is in fullscreen. */
+ return false;
+ }
+ return true;
+}
+
+/*
+ * Returns whether there currently are any drop targets.
+ * Used to only initiate a drag when there is something to drop onto.
+ *
+ */
+bool has_drop_targets(void) {
+ int drop_targets = 0;
+ Con *con;
+ TAILQ_FOREACH (con, &all_cons, all_cons) {
+ if (!is_tiling_drop_target(con)) {
+ continue;
+ }
+ drop_targets++;
+ }
+
+ /* In addition to tiling containers themselves, an visible but empty
+ * workspace (in a multi-monitor scenario) also is a drop target. */
+ Con *output;
+ TAILQ_FOREACH (output, &(croot->focus_head), focused) {
+ if (con_is_internal(output)) {
+ continue;
+ }
+ Con *visible_ws = NULL;
+ GREP_FIRST(visible_ws, output_get_content(output), workspace_is_visible(child));
+ if (visible_ws != NULL && con_num_children(visible_ws) == 0) {
+ drop_targets++;
+ }
+ }
+
+ return drop_targets > 1;
+}
+
+/*
+ * Return an appropriate target at given coordinates.
+ *
+ */
+static Con *find_drop_target(uint32_t x, uint32_t y) {
+ Con *con;
+ TAILQ_FOREACH (con, &all_cons, all_cons) {
+ Rect rect = con_rect_plus_deco_height(con);
+ if (!rect_contains(rect, x, y) ||
+ !is_tiling_drop_target(con)) {
+ continue;
+ }
+ Con *ws = con_get_workspace(con);
+ Con *fs = con_get_fullscreen_covering_ws(ws);
+ return fs ? fs : con;
+ }
+
+ /* Couldn't find leaf container, get a workspace. */
+ Output *output = get_output_containing(x, y);
+ if (!output) {
+ return NULL;
+ }
+ Con *content = output_get_content(output->con);
+ /* Still descend because you can drag to the bar on an non-empty workspace. */
+ return con_descend_tiling_focused(content);
+}
+
+typedef enum { DT_SIBLING,
+ DT_CENTER,
+ DT_PARENT
+} drop_type_t;
+
+struct callback_params {
+ xcb_window_t *indicator;
+ Con **target;
+ direction_t *direction;
+ drop_type_t *drop_type;
+};
+
+static Rect adjust_rect(Rect rect, direction_t direction, uint32_t threshold) {
+ switch (direction) {
+ case D_LEFT:
+ rect.width = threshold;
+ break;
+ case D_UP:
+ rect.height = threshold;
+ break;
+ case D_RIGHT:
+ rect.x += (rect.width - threshold);
+ rect.width = threshold;
+ break;
+ case D_DOWN:
+ rect.y += (rect.height - threshold);
+ rect.height = threshold;
+ break;
+ }
+ return rect;
+}
+
+static bool con_on_side_of_parent(Con *con, direction_t direction) {
+ const orientation_t orientation = orientation_from_direction(direction);
+ direction_t reverse_direction;
+ switch (direction) {
+ case D_LEFT:
+ reverse_direction = D_RIGHT;
+ break;
+ case D_RIGHT:
+ reverse_direction = D_LEFT;
+ break;
+ case D_UP:
+ reverse_direction = D_DOWN;
+ break;
+ case D_DOWN:
+ reverse_direction = D_UP;
+ break;
+ }
+ return (con_orientation(con->parent) != orientation ||
+ con->parent->layout == L_STACKED || con->parent->layout == L_TABBED ||
+ con_descend_direction(con->parent, reverse_direction) == con);
+}
+
+/*
+ * The callback that is executed on every mouse move while dragging. On each
+ * invocation we determine the drop target and the direction in which to insert
+ * the dragged container. The indicator window is updated to show the new
+ * position of the dragged container. The target container and direction are
+ * passed out using the callback params.
+ *
+ */
+DRAGGING_CB(drag_callback) {
+ /* 30% of the container (minus the parent indicator) is used to drop the
+ * dragged container as a sibling to the target */
+ const double sibling_indicator_percent_of_rect = 0.3;
+ /* Use the base decoration height and add a few pixels. This makes the
+ * outer indicator generally thin but at least thick enough to cover
+ * container titles */
+ const double parent_indicator_max_size = render_deco_height() + logical_px(5);
+
+ Con *target = find_drop_target(new_x, new_y);
+ if (target == NULL) {
+ return;
+ }
+
+ Rect rect = con_rect_plus_deco_height(target);
+
+ direction_t direction = 0;
+ drop_type_t drop_type = DT_CENTER;
+ bool draw_window = true;
+ const struct callback_params *params = extra;
+
+ if (target->type == CT_WORKSPACE) {
+ goto create_indicator;
+ }
+
+ /* Define the thresholds in pixels. The drop type depends on the cursor
+ * position. */
+ const uint32_t min_rect_dimension = min(rect.width, rect.height);
+ const uint32_t sibling_indicator_size = max(logical_px(2), (uint32_t)(sibling_indicator_percent_of_rect * min_rect_dimension));
+ const uint32_t parent_indicator_size = min(
+ parent_indicator_max_size,
+ /* For small containers, start where the sibling indicator finishes.
+ * This is always at least 1 pixel. We use min() to not override the
+ * sibling indicator: */
+ sibling_indicator_size - 1);
+
+ /* Find which edge the cursor is closer to. */
+ const uint32_t d_left = new_x - rect.x;
+ const uint32_t d_top = new_y - rect.y;
+ const uint32_t d_right = rect.x + rect.width - new_x;
+ const uint32_t d_bottom = rect.y + rect.height - new_y;
+ const uint32_t d_min = min(min(d_left, d_right), min(d_top, d_bottom));
+ /* And move the container towards that direction. */
+ if (d_left == d_min) {
+ direction = D_LEFT;
+ } else if (d_top == d_min) {
+ direction = D_UP;
+ } else if (d_right == d_min) {
+ direction = D_RIGHT;
+ } else if (d_bottom == d_min) {
+ direction = D_DOWN;
+ } else {
+ /* Keep the compiler happy */
+ ELOG("min() is broken\n");
+ assert(false);
+ }
+ const bool target_parent = (d_min < parent_indicator_size &&
+ con_on_side_of_parent(target, direction));
+ const bool target_sibling = (d_min < sibling_indicator_size);
+ drop_type = target_parent ? DT_PARENT : (target_sibling ? DT_SIBLING : DT_CENTER);
+
+ /* target == con makes sense only when we are moving away from target's parent. */
+ if (drop_type != DT_PARENT && target == con) {
+ draw_window = false;
+ xcb_destroy_window(conn, *(params->indicator));
+ *(params->indicator) = 0;
+ goto create_indicator;
+ }
+
+ switch (drop_type) {
+ case DT_PARENT:
+ while (target->parent->type != CT_WORKSPACE && con_on_side_of_parent(target->parent, direction)) {
+ target = target->parent;
+ }
+ rect = adjust_rect(target->parent->rect, direction, parent_indicator_size);
+ break;
+ case DT_CENTER:
+ rect = target->rect;
+ rect.x += sibling_indicator_size;
+ rect.y += sibling_indicator_size;
+ rect.width -= sibling_indicator_size * 2;
+ rect.height -= sibling_indicator_size * 2;
+ break;
+ case DT_SIBLING:
+ rect = adjust_rect(target->rect, direction, sibling_indicator_size);
+ break;
+ }
+
+create_indicator:
+ if (draw_window) {
+ if (*(params->indicator) == 0) {
+ *(params->indicator) = create_drop_indicator(rect);
+ } else {
+ const uint32_t values[4] = {rect.x, rect.y, rect.width, rect.height};
+ const uint32_t mask = XCB_CONFIG_WINDOW_X |
+ XCB_CONFIG_WINDOW_Y |
+ XCB_CONFIG_WINDOW_WIDTH |
+ XCB_CONFIG_WINDOW_HEIGHT;
+ xcb_configure_window(conn, *(params->indicator), mask, values);
+ }
+ }
+ x_mask_event_mask(~XCB_EVENT_MASK_ENTER_WINDOW);
+ xcb_flush(conn);
+
+ *(params->target) = target;
+ *(params->direction) = direction;
+ *(params->drop_type) = drop_type;
+}
+
+/*
+ * Returns a new drop indicator window with the given initial coordinates.
+ *
+ */
+static xcb_window_t create_drop_indicator(Rect rect) {
+ uint32_t mask = 0;
+ uint32_t values[2];
+
+ mask = XCB_CW_BACK_PIXEL;
+ values[0] = config.client.focused.indicator.colorpixel;
+
+ mask |= XCB_CW_OVERRIDE_REDIRECT;
+ values[1] = 1;
+
+ xcb_window_t indicator = create_window(conn, rect, XCB_COPY_FROM_PARENT, XCB_COPY_FROM_PARENT,
+ XCB_WINDOW_CLASS_INPUT_OUTPUT, XCURSOR_CURSOR_MOVE, false, mask, values);
+ /* Change the window class to "i3-drag", so that it can be matched in a
+ * compositor configuration. Note that the class needs to be changed before
+ * mapping the window. */
+ xcb_change_property(conn,
+ XCB_PROP_MODE_REPLACE,
+ indicator,
+ XCB_ATOM_WM_CLASS,
+ XCB_ATOM_STRING,
+ 8,
+ (strlen("i3-drag") + 1) * 2,
+ "i3-drag\0i3-drag\0");
+ xcb_map_window(conn, indicator);
+ xcb_circulate_window(conn, XCB_CIRCULATE_RAISE_LOWEST, indicator);
+
+ return indicator;
+}
+
+/*
+ * Initiates a mouse drag operation on a tiled window.
+ *
+ */
+void tiling_drag(Con *con, xcb_button_press_event_t *event, bool use_threshold) {
+ DLOG("Start dragging tiled container: con = %p\n", con);
+ bool set_focus = (con == focused);
+ bool set_fs = con->fullscreen_mode != CF_NONE;
+
+ /* Don't change focus while dragging. */
+ x_mask_event_mask(~XCB_EVENT_MASK_ENTER_WINDOW);
+ xcb_flush(conn);
+
+ /* Indicate drop location while dragging. This blocks until the drag is completed. */
+ Con *target = NULL;
+ direction_t direction;
+ drop_type_t drop_type;
+ xcb_window_t indicator = 0;
+ const struct callback_params params = {&indicator, &target, &direction, &drop_type};
+
+ drag_result_t drag_result = drag_pointer(con, event, XCB_NONE, XCURSOR_CURSOR_MOVE, use_threshold, drag_callback, ¶ms);
+
+ /* Dragging is done. We don't need the indicator window any more. */
+ xcb_destroy_window(conn, indicator);
+
+ if (drag_result == DRAG_REVERT ||
+ target == NULL ||
+ (target == con && drop_type != DT_PARENT) ||
+ !con_exists(target)) {
+ DLOG("drop aborted\n");
+ return;
+ }
+
+ const orientation_t orientation = orientation_from_direction(direction);
+ const position_t position = position_from_direction(direction);
+ const layout_t layout = orientation == VERT ? L_SPLITV : L_SPLITH;
+ con_disable_fullscreen(con);
+ switch (drop_type) {
+ case DT_CENTER:
+ /* Also handles workspaces.*/
+ DLOG("drop to center of %p\n", target);
+ con_move_to_target(con, target);
+ break;
+ case DT_SIBLING:
+ DLOG("drop %s %p\n", position_to_string(position), target);
+ if (con_orientation(target->parent) != orientation) {
+ /* If con and target are the only children of the same parent, we can just change
+ * the parent's layout manually and then move con to the correct position.
+ * tree_split checks for a parent with only one child so it would create a new
+ * parent with the new layout. */
+ if (con->parent == target->parent && con_num_children(target->parent) == 2) {
+ target->parent->layout = layout;
+ } else {
+ tree_split(target, orientation);
+ }
+ }
+
+ insert_con_into(con, target, position);
+
+ ipc_send_window_event("move", con);
+ break;
+ case DT_PARENT: {
+ const bool parent_tabbed_or_stacked = (target->parent->layout == L_TABBED || target->parent->layout == L_STACKED);
+ DLOG("drop %s (%s) of %s%p\n",
+ direction_to_string(direction),
+ position_to_string(position),
+ parent_tabbed_or_stacked ? "tabbed/stacked " : "",
+ target);
+ if (parent_tabbed_or_stacked) {
+ /* When dealing with tabbed/stacked the target can be in the
+ * middle of the container. Thus, after a directional move, con
+ * will still be bound to the tabbed/stacked parent. */
+ if (position == BEFORE) {
+ target = TAILQ_FIRST(&(target->parent->nodes_head));
+ } else {
+ target = TAILQ_LAST(&(target->parent->nodes_head), nodes_head);
+ }
+ }
+ if (con != target) {
+ insert_con_into(con, target, position);
+ }
+ /* tree_move can change the focus */
+ Con *old_focus = focused;
+ tree_move(con, direction);
+ if (focused != old_focus) {
+ con_activate(old_focus);
+ }
+ break;
+ }
+ }
+ /* Warning: target might not exist anymore */
+ target = NULL;
+
+ /* Manage fullscreen status. */
+ if (set_focus || set_fs) {
+ Con *fs = con_get_fullscreen_covering_ws(con_get_workspace(con));
+ if (fs == con) {
+ ELOG("dragged container somehow got fullscreen again.\n");
+ assert(false);
+ } else if (fs && set_focus && set_fs) {
+ /* con was focused & fullscreen, disable other fullscreen container. */
+ con_disable_fullscreen(fs);
+ } else if (fs) {
+ /* con was not focused, prefer other fullscreen container. */
+ set_fs = set_focus = false;
+ } else if (!set_focus) {
+ /* con was not focused. If it was fullscreen and we are moving it to the focused
+ * workspace we must focus it. */
+ set_focus = (set_fs && con_get_workspace(focused) == con_get_workspace(con));
+ }
+ }
+ if (set_fs) {
+ con_enable_fullscreen(con, CF_OUTPUT);
+ }
+ if (set_focus) {
+ workspace_show(con_get_workspace(con));
+ con_focus(con);
+ }
+ tree_render();
+}
diff --git a/src/util.c b/src/util.c
index cf7f41c..7b14861 100644
--- a/src/util.c
+++ b/src/util.c
@@ -177,15 +177,6 @@
}
/*
- * Checks if the given path exists by calling stat().
- *
- */
-bool path_exists(const char *path) {
- struct stat buf;
- return (stat(path, &buf) == 0);
-}
-
-/*
* Goes through the list of arguments (for exec()) and add/replace the given option,
* including the option name, its argument, and the option character.
*/
@@ -485,3 +476,35 @@
return position == BEFORE ? D_UP : D_DOWN;
}
}
+
+/*
+ * Converts direction to a string representation.
+ *
+ */
+const char *direction_to_string(direction_t direction) {
+ switch (direction) {
+ case D_LEFT:
+ return "left";
+ case D_RIGHT:
+ return "right";
+ case D_UP:
+ return "up";
+ case D_DOWN:
+ return "down";
+ }
+ return "invalid";
+}
+
+/*
+ * Converts position to a string representation.
+ *
+ */
+const char *position_to_string(position_t position) {
+ switch (position) {
+ case BEFORE:
+ return "before";
+ case AFTER:
+ return "after";
+ }
+ return "invalid";
+}
diff --git a/src/window.c b/src/window.c
index bee3fa6..7c65cee 100644
--- a/src/window.c
+++ b/src/window.c
@@ -18,7 +18,10 @@
void window_free(i3Window *win) {
FREE(win->class_class);
FREE(win->class_instance);
+ FREE(win->role);
+ FREE(win->machine);
i3string_free(win->name);
+ cairo_surface_destroy(win->icon);
FREE(win->ran_assignments);
FREE(win);
}
@@ -412,12 +415,10 @@
* it is still in use by popular widget toolkits such as GTK+ and Java AWT.
*
*/
-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
- * documentation of this hint is nowhere to be found.
- * For more information see:
- * https://people.gnome.org/~tthurman/docs/metacity/xprops_8h-source.html
- * https://stackoverflow.com/questions/13787553/detect-if-a-x11-window-has-decorations
+bool window_update_motif_hints(i3Window *win, xcb_get_property_reply_t *prop, border_style_t *motif_border_style) {
+ /* See `man VendorShell' for more info, `XmNmwmDecorations' section:
+ * https://linux.die.net/man/3/vendorshell
+ * The following constants are adapted from .
*/
#define MWM_HINTS_FLAGS_FIELD 0
#define MWM_HINTS_DECORATIONS_FIELD 2
@@ -432,7 +433,7 @@
if (prop == NULL || xcb_get_property_value_length(prop) == 0) {
FREE(prop);
- return;
+ return false;
}
/* The property consists of an array of 5 uint32_t's. The first value is a
@@ -458,6 +459,7 @@
}
FREE(prop);
+ return true;
#undef MWM_HINTS_FLAGS_FIELD
#undef MWM_HINTS_DECORATIONS_FIELD
@@ -466,3 +468,130 @@
#undef MWM_DECOR_BORDER
#undef MWM_DECOR_TITLE
}
+
+/*
+ * Updates the WM_CLIENT_MACHINE
+ *
+ */
+void window_update_machine(i3Window *win, xcb_get_property_reply_t *prop) {
+ if (prop == NULL || xcb_get_property_value_length(prop) == 0) {
+ DLOG("WM_CLIENT_MACHINE not set.\n");
+ FREE(prop);
+ return;
+ }
+
+ FREE(win->machine);
+ win->machine = sstrndup((char *)xcb_get_property_value(prop), xcb_get_property_value_length(prop));
+ LOG("WM_CLIENT_MACHINE changed to \"%s\"\n", win->machine);
+
+ free(prop);
+}
+
+void window_update_icon(i3Window *win, xcb_get_property_reply_t *prop) {
+ uint32_t *data = NULL;
+ uint32_t width, height;
+ uint64_t len = 0;
+ const uint32_t pref_size = (uint32_t)(render_deco_height() - logical_px(2));
+
+ if (!prop || prop->type != XCB_ATOM_CARDINAL || prop->format != 32) {
+ DLOG("_NET_WM_ICON is not set\n");
+ FREE(prop);
+ return;
+ }
+
+ uint32_t prop_value_len = xcb_get_property_value_length(prop);
+ uint32_t *prop_value = (uint32_t *)xcb_get_property_value(prop);
+
+ /* Find an icon matching the preferred size.
+ * If there is no such icon, take the smallest icon having at least
+ * the preferred size.
+ * If all icons are smaller than the preferred size, chose the largest.
+ */
+ while (prop_value_len > (sizeof(uint32_t) * 2) && prop_value &&
+ prop_value[0] && prop_value[1]) {
+ const uint32_t cur_width = prop_value[0];
+ const uint32_t cur_height = prop_value[1];
+ /* Check that the property is as long as it should be (in bytes),
+ handling integer overflow. "+2" to handle the width and height
+ fields. */
+ const uint64_t cur_len = cur_width * (uint64_t)cur_height;
+ const uint64_t expected_len = (cur_len + 2) * 4;
+
+ if (expected_len > prop_value_len) {
+ break;
+ }
+
+ DLOG("Found _NET_WM_ICON of size: (%d,%d)\n", cur_width, cur_height);
+
+ const bool at_least_preferred_size = (cur_width >= pref_size &&
+ cur_height >= pref_size);
+ const bool smaller_than_current = (cur_width < width ||
+ cur_height < height);
+ const bool larger_than_current = (cur_width > width ||
+ cur_height > height);
+ const bool not_yet_at_preferred = (width < pref_size ||
+ height < pref_size);
+ if (len == 0 ||
+ (at_least_preferred_size &&
+ (smaller_than_current || not_yet_at_preferred)) ||
+ (!at_least_preferred_size &&
+ not_yet_at_preferred &&
+ larger_than_current)) {
+ len = cur_len;
+ width = cur_width;
+ height = cur_height;
+ data = prop_value;
+ }
+
+ if (width == pref_size && height == pref_size) {
+ break;
+ }
+
+ /* Find pointer to next icon in the reply. */
+ prop_value_len -= expected_len;
+ prop_value = (uint32_t *)(((uint8_t *)prop_value) + expected_len);
+ }
+
+ if (!data) {
+ DLOG("Could not get _NET_WM_ICON\n");
+ FREE(prop);
+ return;
+ }
+
+ DLOG("Using icon of size (%d,%d) (preferred size: %d)\n",
+ width, height, pref_size);
+
+ win->name_x_changed = true; /* trigger a redraw */
+
+ uint32_t *icon = smalloc(len * 4);
+
+ for (uint64_t i = 0; i < len; i++) {
+ uint8_t r, g, b, a;
+ const uint32_t pixel = data[2 + i];
+ a = (pixel >> 24) & 0xff;
+ r = (pixel >> 16) & 0xff;
+ g = (pixel >> 8) & 0xff;
+ b = (pixel >> 0) & 0xff;
+
+ /* Cairo uses premultiplied alpha */
+ r = (r * a) / 0xff;
+ g = (g * a) / 0xff;
+ b = (b * a) / 0xff;
+
+ icon[i] = ((uint32_t)a << 24) | (r << 16) | (g << 8) | b;
+ }
+
+ if (win->icon != NULL) {
+ cairo_surface_destroy(win->icon);
+ }
+ win->icon = cairo_image_surface_create_for_data(
+ (unsigned char *)icon,
+ CAIRO_FORMAT_ARGB32,
+ width,
+ height,
+ width * 4);
+ static cairo_user_data_key_t free_data;
+ cairo_surface_set_user_data(win->icon, &free_data, icon, free);
+
+ FREE(prop);
+}
diff --git a/src/workspace.c b/src/workspace.c
index e77d544..7391763 100644
--- a/src/workspace.c
+++ b/src/workspace.c
@@ -139,7 +139,7 @@
/* We set workspace->num to the number if this workspace’s name begins with
* a positive number. Otherwise it’s a named ws and num will be 1. */
- const long parsed_num = ws_name_to_number(num);
+ const int parsed_num = ws_name_to_number(num);
struct Workspace_Assignment *assignment;
TAILQ_FOREACH (assignment, &ws_assignments, ws_assignments) {
@@ -254,7 +254,6 @@
*/
Con *create_workspace_on_output(Output *output, Con *content) {
/* add a workspace to this output */
- char *name;
bool exists = true;
Con *ws = con_new(NULL, NULL);
ws->type = CT_WORKSPACE;
@@ -270,13 +269,16 @@
continue;
}
- exists = (get_existing_workspace_by_name(target_name) != NULL);
+ const int num = ws_name_to_number(target_name);
+ exists = (num == -1)
+ ? get_existing_workspace_by_name(target_name)
+ : get_existing_workspace_by_num(num);
if (!exists) {
ws->name = sstrdup(target_name);
/* Set ->num to the number of the workspace, if the name actually
* is a number or starts with a number */
- ws->num = ws_name_to_number(ws->name);
- LOG("Used number %d for workspace with name %s\n", ws->num, ws->name);
+ ws->num = num;
+ DLOG("Used number %d for workspace with name %s\n", ws->num, ws->name);
break;
}
@@ -306,6 +308,7 @@
con_attach(ws, content, false);
+ char *name;
sasprintf(&name, "[i3 con] workspace %s", ws->name);
x_set_name(ws, name);
free(name);
@@ -321,16 +324,16 @@
/*
* Returns true if the workspace is currently visible. Especially important for
- * multi-monitor environments, as they can have multiple currenlty active
+ * multi-monitor environments, as they can have multiple currently active
* workspaces.
*
*/
bool workspace_is_visible(Con *ws) {
Con *output = con_get_output(ws);
- if (output == NULL)
+ if (output == NULL) {
return false;
+ }
Con *fs = con_get_fullscreen_con(output, CF_OUTPUT);
- LOG("workspace visible? fs = %p, ws = %p\n", fs, ws);
return (fs == ws);
}
@@ -1012,14 +1015,14 @@
bool used_assignment = false;
struct Workspace_Assignment *assignment;
TAILQ_FOREACH (assignment, &ws_assignments, ws_assignments) {
- bool attached;
- int num;
if (!output_triggers_assignment(current_output, assignment)) {
continue;
}
/* check if this workspace's name or num is already attached to the tree */
- num = ws_name_to_number(assignment->name);
- attached = ((num == -1) ? get_existing_workspace_by_name(assignment->name) : get_existing_workspace_by_num(num)) != NULL;
+ const int num = ws_name_to_number(assignment->name);
+ const bool attached = (num == -1)
+ ? get_existing_workspace_by_name(assignment->name)
+ : get_existing_workspace_by_num(num);
if (attached) {
continue;
}
diff --git a/src/x.c b/src/x.c
index 48af5f3..42df996 100644
--- a/src/x.c
+++ b/src/x.c
@@ -490,14 +490,20 @@
struct deco_render_params *p = scalloc(1, sizeof(struct deco_render_params));
/* find out which colors to use */
- if (con->urgent)
+ if (con->urgent) {
p->color = &config.client.urgent;
- else if (con == focused || con_inside_focused(con))
+ } else if (con == focused || con_inside_focused(con)) {
p->color = &config.client.focused;
- else if (con == TAILQ_FIRST(&(parent->focus_head)))
- p->color = &config.client.focused_inactive;
- else
+ } else if (con == TAILQ_FIRST(&(parent->focus_head))) {
+ if (config.client.got_focused_tab_title && !leaf && con_descend_focused(con) == focused) {
+ /* Stacked/tabbed parent of focused container */
+ p->color = &config.client.focused_tab_title;
+ } else {
+ p->color = &config.client.focused_inactive;
+ }
+ } else {
p->color = &config.client.unfocused;
+ }
p->border_style = con_border_style(con);
@@ -537,6 +543,9 @@
/* 2: draw the client.background, but only for the parts around the window_rect */
if (con->window != NULL) {
+ /* Clear visible windows before beginning to draw */
+ draw_util_clear_surface(&(con->frame_buffer), (color_t){.red = 0.0, .green = 0.0, .blue = 0.0});
+
/* top area */
draw_util_rectangle(&(con->frame_buffer), config.client.background,
0, 0, r->width, w->y);
@@ -609,11 +618,14 @@
/* 5: draw title border */
x_draw_title_border(con, p);
- /* 6: draw the title */
+ /* 6: draw the icon and title */
int text_offset_y = (con->deco_rect.height - config.font.height) / 2;
+ struct Window *win = con->window;
+
+ const int deco_width = (int)con->deco_rect.width;
const int title_padding = logical_px(2);
- const int deco_width = (int)con->deco_rect.width;
+
int mark_width = 0;
if (config.show_marks && !TAILQ_EMPTY(&(con->marks_head))) {
char *formatted_mark = sstrdup("");
@@ -652,7 +664,6 @@
}
i3String *title = NULL;
- struct Window *win = con->window;
if (win == NULL) {
if (con->title_format == NULL) {
char *_title;
@@ -672,25 +683,45 @@
goto copy_pixmaps;
}
+ /* icon_padding is applied horizontally only, the icon will always use all
+ * available vertical space. */
+ int icon_size = max(0, con->deco_rect.height - logical_px(2));
+ int icon_padding = logical_px(max(1, con->window_icon_padding));
+ int total_icon_space = icon_size + 2 * icon_padding;
+ const bool has_icon = (con->window_icon_padding > -1) && win && win->icon && (total_icon_space < deco_width);
+ if (!has_icon) {
+ icon_size = icon_padding = total_icon_space = 0;
+ }
+ /* Determine x offsets according to title alignment */
+ int icon_offset_x;
int title_offset_x;
switch (config.title_align) {
case ALIGN_LEFT:
- /* (pad)[text ](pad)[mark + its pad) */
- title_offset_x = title_padding;
+ /* (pad)[(pad)(icon)(pad)][text ](pad)[mark + its pad)
+ * ^ ^--- title_offset_x
+ * ^--- icon_offset_x */
+ icon_offset_x = icon_padding;
+ title_offset_x = title_padding + total_icon_space;
break;
case ALIGN_CENTER:
- /* (pad)[ text ](pad)[mark + its pad)
- * To center the text inside its allocated space, the surface
- * between the brackets, we use the formula
- * (surface_width - predict_text_width) / 2
- * where surface_width = deco_width - 2 * pad - mark_width
- * so, offset = pad + (surface_width - predict_text_width) / 2 =
- * = … = (deco_width - mark_width - predict_text_width) / 2 */
- title_offset_x = max(title_padding, (deco_width - mark_width - predict_text_width(title)) / 2);
+ /* (pad)[ ][(pad)(icon)(pad)][text ](pad)[mark + its pad)
+ * ^ ^--- title_offset_x
+ * ^--- icon_offset_x
+ * Text should come right after the icon (+padding). We calculate
+ * the offset for the icon (white space in the title) by dividing
+ * by two the total available area. That's the decoration width
+ * minus the elements that come after icon_offset_x (icon, its
+ * padding, text, marks). */
+ icon_offset_x = max(icon_padding, (deco_width - icon_padding - icon_size - predict_text_width(title) - title_padding - mark_width) / 2);
+ title_offset_x = max(title_padding, icon_offset_x + icon_padding + icon_size);
break;
case ALIGN_RIGHT:
- /* [mark + its pad](pad)[ text](pad) */
- title_offset_x = max(title_padding + mark_width, deco_width - title_padding - predict_text_width(title));
+ /* [mark + its pad](pad)[ text][(pad)(icon)(pad)](pad)
+ * ^ ^--- icon_offset_x
+ * ^--- title_offset_x */
+ title_offset_x = max(title_padding + mark_width, deco_width - title_padding - predict_text_width(title) - total_icon_space);
+ /* Make sure the icon does not escape title boundaries */
+ icon_offset_x = min(deco_width - icon_size - icon_padding - title_padding, title_offset_x + predict_text_width(title) + icon_padding);
break;
}
@@ -698,7 +729,16 @@
p->color->text, p->color->background,
con->deco_rect.x + title_offset_x,
con->deco_rect.y + text_offset_y,
- deco_width - mark_width - 2 * title_padding);
+ deco_width - mark_width - 2 * title_padding - total_icon_space);
+ if (has_icon) {
+ draw_util_image(
+ win->icon,
+ &(parent->frame_buffer),
+ con->deco_rect.x + icon_offset_x,
+ con->deco_rect.y + logical_px(1),
+ icon_size,
+ icon_size);
+ }
if (win == NULL || con->title_format != NULL) {
I3STRING_FREE(title);
@@ -841,7 +881,6 @@
con_state *state;
Rect rect = con->rect;
- //DLOG("Pushing changes for node %p / %s\n", con, con->name);
state = state_for_frame(con->frame.id);
if (state->name != NULL) {
@@ -954,14 +993,16 @@
win_depth = con->window->depth;
/* Ensure we have valid dimensions for our surface. */
- // TODO This is probably a bug in the condition above as we should never enter this path
- // for height == 0. Also, we should probably handle width == 0 the same way.
+ /* TODO: This is probably a bug in the condition above as we should
+ * never enter this path for height == 0. Also, we should probably
+ * handle width == 0 the same way. */
int width = MAX((int32_t)rect.width, 1);
int height = MAX((int32_t)rect.height, 1);
xcb_create_pixmap(conn, win_depth, con->frame_buffer.id, con->frame.id, width, height);
draw_util_surface_init(conn, &(con->frame_buffer), con->frame_buffer.id,
get_visualtype_by_id(get_visualid_by_depth(win_depth)), width, height);
+ draw_util_clear_surface(&(con->frame_buffer), (color_t){.red = 0.0, .green = 0.0, .blue = 0.0});
/* For the graphics context, we disable GraphicsExposure events.
* Those will be sent when a CopyArea request cannot be fulfilled
@@ -974,8 +1015,8 @@
con->pixmap_recreated = true;
/* Don’t render the decoration for windows inside a stack which are
- * not visible right now */
- // TODO Should this work the same way for L_TABBED?
+ * not visible right now
+ * TODO: Should this work the same way for L_TABBED? */
if (!con->parent ||
con->parent->layout != L_STACKED ||
TAILQ_FIRST(&(con->parent->focus_head)) == con)
@@ -1086,7 +1127,6 @@
Con *current;
con_state *state;
- //DLOG("Pushing changes (with unmaps) for node %p / %s\n", con, con->name);
state = state_for_frame(con->frame.id);
/* map/unmap if map state changed, also ensure that the child window
@@ -1162,7 +1202,6 @@
}
DLOG("-- PUSHING WINDOW STACK --\n");
- //DLOG("Disabling EnterNotify\n");
/* We need to keep SubstructureRedirect around, otherwise clients can send
* ConfigureWindow requests and get them applied directly instead of having
* them become ConfigureRequests that i3 handles. */
@@ -1171,7 +1210,6 @@
if (state->mapped)
xcb_change_window_attributes(conn, state->id, XCB_CW_EVENT_MASK, values);
}
- //DLOG("Done, EnterNotify disabled\n");
bool order_changed = false;
bool stacking_changed = false;
@@ -1201,14 +1239,12 @@
if (con_has_managed_window(state->con))
memcpy(walk++, &(state->con->window->id), sizeof(xcb_window_t));
- //DLOG("stack: 0x%08x\n", state->id);
con_state *prev = CIRCLEQ_PREV(state, state);
con_state *old_prev = CIRCLEQ_PREV(state, old_state);
if (prev != old_prev)
order_changed = true;
if ((state->initial || order_changed) && prev != CIRCLEQ_END(&state_head)) {
stacking_changed = true;
- //DLOG("Stacking 0x%08x above 0x%08x\n", prev->id, state->id);
uint32_t mask = 0;
mask |= XCB_CONFIG_WINDOW_SIBLING;
mask |= XCB_CONFIG_WINDOW_STACK_MODE;
@@ -1261,13 +1297,11 @@
warp_to = NULL;
}
- //DLOG("Re-enabling EnterNotify\n");
values[0] = FRAME_EVENT_MASK;
CIRCLEQ_FOREACH_REVERSE (state, &state_head, state) {
if (state->mapped)
xcb_change_window_attributes(conn, state->id, XCB_CW_EVENT_MASK, values);
}
- //DLOG("Done, EnterNotify re-enabled\n");
x_deco_recurse(con);
@@ -1353,9 +1387,6 @@
CIRCLEQ_REMOVE(&old_state_head, state, old_state);
CIRCLEQ_INSERT_TAIL(&old_state_head, state, old_state);
}
- //CIRCLEQ_FOREACH(state, &old_state_head, old_state) {
- // DLOG("old stack: 0x%08x\n", state->id);
- //}
xcb_flush(conn);
}
@@ -1368,7 +1399,6 @@
void x_raise_con(Con *con) {
con_state *state;
state = state_for_frame(con->frame.id);
- //DLOG("raising in new stack: %p / %s / %s / xid %08x\n", con, con->name, con->window ? con->window->name_json : "", state->id);
CIRCLEQ_REMOVE(&state_head, state, state);
CIRCLEQ_INSERT_HEAD(&state_head, state, state);
@@ -1418,6 +1448,8 @@
xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A_I3_PID, XCB_ATOM_CARDINAL, 32, 1, &pid);
xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A_I3_CONFIG_PATH, A_UTF8_STRING, 8,
strlen(current_configpath), current_configpath);
+ xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A_I3_LOG_STREAM_SOCKET_PATH, A_UTF8_STRING, 8,
+ strlen(current_log_stream_socket_path), current_log_stream_socket_path);
update_shmlog_atom();
}
diff --git a/testcases/inject_randr1.5.c b/testcases/inject_randr1.5.c
index 9c0b73d..75cbf83 100644
--- a/testcases/inject_randr1.5.c
+++ b/testcases/inject_randr1.5.c
@@ -433,7 +433,7 @@
bound = true;
/* Let the user know bind() was successful, so that they know the
* error messages can be disregarded. */
- fprintf(stderr, "Successfuly bound to %s\n", addr.sun_path);
+ fprintf(stderr, "Successfully bound to %s\n", addr.sun_path);
sun_path = sstrdup(addr.sun_path);
break;
}
diff --git a/testcases/lib/i3test.pm.in b/testcases/lib/i3test.pm.in
index aedaa60..6d73afc 100644
--- a/testcases/lib/i3test.pm.in
+++ b/testcases/lib/i3test.pm.in
@@ -42,6 +42,7 @@
exit_forcefully
workspace_exists
focused_ws
+ focused_output
get_socket_path
launch_with_config
get_i3_log
@@ -410,7 +411,7 @@
cmd 'focus output fake-1';
cmd 'workspace 1';
- is(get_output_for_workspace('1', 'fake-0', 'Workspace 1 in output fake-0');
+ is(get_output_for_workspace('1'), 'fake-0', 'Workspace 1 in output fake-0');
=cut
sub get_output_for_workspace {
@@ -419,10 +420,12 @@
my $tree = $i3->get_tree->recv;
my @outputs = @{$tree->{nodes}};
- foreach (grep { not $_->{name} =~ /^__/ } @outputs) {
- my $output = $_->{name};
- foreach (grep { $_->{name} =~ "content" } @{$_->{nodes}}) {
- return $output if $_->{nodes}[0]->{name} =~ $ws_name;
+ for my $output (@outputs) {
+ next if $output->{name} eq '__i3';
+ # get the first CT_CON of each output
+ my $content = first { $_->{type} eq 'con' } @{$output->{nodes}};
+ if (grep {$_->{name} eq $ws_name} @{$content->{nodes}}){
+ return $output->{name};
}
}
}
@@ -662,19 +665,36 @@
(scalar grep { $_ eq $name } @{get_workspace_names()}) > 0;
}
-=head2 focused_ws
-
-Returns the name of the currently focused workspace.
-
- my $ws = focused_ws;
- is($ws, '1', 'i3 starts on workspace 1');
-
-=cut
-sub focused_ws {
+=head2 focused_output
+
+Returns the name of the currently focused output.
+
+ is(focused_output, 'fake-0', 'i3 starts on output 0');
+
+=cut
+sub _focused_output {
my $i3 = i3(get_socket_path());
my $tree = $i3->get_tree->recv;
my $focused = $tree->{focus}->[0];
my $output = first { $_->{id} == $focused } @{$tree->{nodes}};
+ return $output;
+}
+
+sub focused_output {
+ return _focused_output->{name}
+}
+
+=head2 focused_ws
+
+Returns the name of the currently focused workspace.
+
+ my $ws = focused_ws;
+ is($ws, '1', 'i3 starts on workspace 1');
+
+=cut
+
+sub focused_ws {
+ my $output = _focused_output;
my $content = first { $_->{type} eq 'con' } @{$output->{nodes}};
my $first = first { $_->{fullscreen_mode} == 1 } @{$content->{nodes}};
return $first->{name}
@@ -778,6 +798,8 @@
my ($pid, $socketpath) = @_;
$socketpath ||= get_socket_path();
+ $SIG{CHLD} = undef;
+
my $exited = 0;
eval {
say "Exiting i3 cleanly...";
@@ -815,6 +837,8 @@
sub exit_forcefully {
my ($pid, $signal) = @_;
$signal ||= 'TERM';
+
+ $SIG{CHLD} = undef;
# Send the given signal to the i3 instance and wait for up to 10s
# for it to terminate.
@@ -939,6 +963,18 @@
return ${^CHILD_ERROR_NATIVE};
}
+ $SIG{CHLD} = sub {
+ # don't change $! and $? outside handler
+ local ($!, $?);
+
+ my $child = waitpid -1, POSIX::WNOHANG;
+ warn "SIGCHLD, waitpid() = $child";
+ if ($child == $i3_pid) {
+ warn "i3 died, exiting!";
+ exit 1;
+ }
+ };
+
# force update of the cached socket path in lib/i3test
# as soon as i3 has started
$cv->cb(sub { get_socket_path(0) });
@@ -970,6 +1006,8 @@
# Sync in case not all windows are managed by i3 just yet.
sync_with_i3;
cmd '[title=".*"] kill';
+ # Sync to make sure x_window_kill() calls have taken effect.
+ sync_with_i3;
}
=head2 events_for($subscribecb, [ $rettype ], [ $eventcbs ])
diff --git a/testcases/t/116-nestedcons.t b/testcases/t/116-nestedcons.t
index 6a2274d..7b15c5f 100644
--- a/testcases/t/116-nestedcons.t
+++ b/testcases/t/116-nestedcons.t
@@ -73,6 +73,7 @@
workspace_layout => 'default',
current_border_width => -1,
marks => $ignore,
+ window_icon_padding => -1,
};
# a shallow copy is sufficient, since we only ignore values at the root
diff --git a/testcases/t/117-workspace.t b/testcases/t/117-workspace.t
index 48a0dbf..6345ae6 100644
--- a/testcases/t/117-workspace.t
+++ b/testcases/t/117-workspace.t
@@ -14,7 +14,7 @@
# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
# (unless you are already familiar with Perl)
#
-# Tests whether we can switch to a non-existant workspace
+# Tests whether we can switch to a non-existent workspace
# (necessary for further tests)
#
use List::Util qw(first);
diff --git a/testcases/t/171-config-migrate.t b/testcases/t/171-config-migrate.t
index 1363332..99158ea 100644
--- a/testcases/t/171-config-migrate.t
+++ b/testcases/t/171-config-migrate.t
@@ -267,7 +267,7 @@
like($output->[3], qr|^bindsym Mod1\+f resize grow down 23 px$|, 'resize bottom changed');
#####################################################################
-# also resizing, but with indention this time
+# also resizing, but with indentation this time
#####################################################################
like($output->[4], qr|^bindsym Mod1\+f resize grow left 10 px$|, 'resize left changed');
diff --git a/testcases/t/187-commands-parser.t b/testcases/t/187-commands-parser.t
index 723a756..3404df1 100644
--- a/testcases/t/187-commands-parser.t
+++ b/testcases/t/187-commands-parser.t
@@ -54,6 +54,7 @@
'move workspace foobar; ' .
'move workspace torrent; ' .
'move workspace to output LVDS1; ' .
+ 'move to output LVDS1 DVI1; ' .
'move workspace 3: foobar; ' .
'move workspace "3: foobar"; ' .
'move workspace "3: foobar, baz"; '),
@@ -62,7 +63,11 @@
"cmd_move_con_to_workspace_name(3, (null))\n" .
"cmd_move_con_to_workspace_name(foobar, (null))\n" .
"cmd_move_con_to_workspace_name(torrent, (null))\n" .
- "cmd_move_workspace_to_output(LVDS1)\n" .
+ "cmd_move_con_to_output(LVDS1, 1)\n" .
+ "cmd_move_con_to_output(NULL, 1)\n" .
+ "cmd_move_con_to_output(LVDS1, 0)\n" .
+ "cmd_move_con_to_output(DVI1, 0)\n" .
+ "cmd_move_con_to_output(NULL, 0)\n" .
"cmd_move_con_to_workspace_name(3: foobar, (null))\n" .
"cmd_move_con_to_workspace_name(3: foobar, (null))\n" .
"cmd_move_con_to_workspace_name(3: foobar, baz, (null))",
@@ -171,6 +176,7 @@
scratchpad
swap
title_format
+ title_window_icon
mode
bar
gaps
diff --git a/testcases/t/201-config-parser.t b/testcases/t/201-config-parser.t
index 2d8b63c..bab650d 100644
--- a/testcases/t/201-config-parser.t
+++ b/testcases/t/201-config-parser.t
@@ -476,16 +476,18 @@
################################################################################
$config = <<'EOT';
-client.focused #4c7899 #285577 #ffffff #2e9ef4 #b34d4c
-client.focused_inactive #333333 #5f676a #ffffff #484e50
-client.unfocused #333333 #222222 #888888 #292d2e
-client.urgent #2f343a #900000 #ffffff #900000 #c00000
-client.placeholder #000000 #0c0c0c #ffffff #000000
+client.focused #4c7899 #285577 #ffffff #2e9ef4 #b34d4c
+client.focused_inactive #333333 #5f676a #ffffff #484e50
+client.focused_tab_title #444444 #555555 #ffffff
+client.unfocused #333333 #222222 #888888 #292d2e
+client.urgent #2f343a #900000 #ffffff #900000 #c00000
+client.placeholder #000000 #0c0c0c #ffffff #000000
EOT
$expected = <<'EOT';
cfg_color(client.focused, #4c7899, #285577, #ffffff, #2e9ef4, #b34d4c)
cfg_color(client.focused_inactive, #333333, #5f676a, #ffffff, #484e50, NULL)
+cfg_color(client.focused_tab_title, #444444, #555555, #ffffff, NULL, NULL)
cfg_color(client.unfocused, #333333, #222222, #888888, #292d2e, NULL)
cfg_color(client.urgent, #2f343a, #900000, #ffffff, #900000, #c00000)
cfg_color(client.placeholder, #000000, #0c0c0c, #ffffff, #000000, NULL)
@@ -506,6 +508,7 @@
my $expected_all_tokens = "ERROR: CONFIG: Expected one of these tokens: , '#', '" . join("', '", 'set ', 'set ', qw(
set_from_resource
+ include
bindsym
bindcode
bind
@@ -549,10 +552,12 @@
ipc_kill_timeout
restart_state
popup_during_fullscreen
+ tiling_drag
exec_always
exec
client.background
client.focused_inactive
+ client.focused_tab_title
client.focused
client.unfocused
client.urgent
diff --git a/testcases/t/210-mark-unmark.t b/testcases/t/210-mark-unmark.t
index 0d9a073..166ca23 100644
--- a/testcases/t/210-mark-unmark.t
+++ b/testcases/t/210-mark-unmark.t
@@ -71,7 +71,7 @@
cmd 'mark left';
#
-# get_marks replys an array of marks, whose order is undefined,
+# get_marks replies an array of marks, whose order is undefined,
# so we use sort to be able to compare the output
#
diff --git a/testcases/t/219-ipc-window-focus.t b/testcases/t/219-ipc-window-focus.t
index 696fc7b..770536f 100644
--- a/testcases/t/219-ipc-window-focus.t
+++ b/testcases/t/219-ipc-window-focus.t
@@ -18,7 +18,7 @@
# i3 config file (v4)
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
-# fake-1 under fake-0 to not interfere with left/right wraping
+# fake-1 under fake-0 to not interfere with left/right wrapping
fake-outputs 1024x768+0+0,1024x768+0+1024
workspace X output fake-1
EOT
@@ -56,7 +56,11 @@
my $focus = AnyEvent->condvar;
my @events = events_for(
- sub { cmd $cmd },
+ sub {
+ cmd $cmd;
+ # Sync to make sure x_window_kill() calls have taken effect.
+ sync_with_i3;
+ },
'window');
is(scalar @events, 1, 'Received 1 event');
diff --git a/testcases/t/247-config-line-continuation.t b/testcases/t/247-config-line-continuation.t
index 8098b99..c54f1ac 100644
--- a/testcases/t/247-config-line-continuation.t
+++ b/testcases/t/247-config-line-continuation.t
@@ -62,7 +62,7 @@
font \
-misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
-# Use line contiuation with too many lines (>4096 characters).
+# Use line continuation with too many lines (>4096 characters).
# This config is invalid. Use it to ensure no buffer overflow.
bindsym Mod1+b \
0001-This is a very very very very very very very very very very very very very very very very very long cmd \
diff --git a/testcases/t/254-move-to-output-with-criteria.t b/testcases/t/254-move-to-output-with-criteria.t
index 17aa1bb..c129aa7 100644
--- a/testcases/t/254-move-to-output-with-criteria.t
+++ b/testcases/t/254-move-to-output-with-criteria.t
@@ -41,4 +41,12 @@
is_num_children($ws_bottom_left, 0, 'no containers on the lower left workspace');
is_num_children($ws_bottom_right, 1, 'one container on the lower right workspace');
+# Also test with multiple explicit outputs
+cmd '[class="moveme"] move window to output fake-1 fake-2';
+
+is_num_children($ws_top_left, 0, 'no containers on the upper left workspace');
+is_num_children($ws_top_right, 1, 'one container on the upper right workspace');
+is_num_children($ws_bottom_left, 1, 'one container on the lower left workspace');
+is_num_children($ws_bottom_right, 0, 'no containers on the lower right workspace');
+
done_testing;
diff --git a/testcases/t/289-ipc-shutdown-event.t b/testcases/t/289-ipc-shutdown-event.t
index 606474e..a56c4ac 100644
--- a/testcases/t/289-ipc-shutdown-event.t
+++ b/testcases/t/289-ipc-shutdown-event.t
@@ -21,7 +21,9 @@
#
# Ticket: #2318
# Bug still in: 4.12-46-g2123888
-use i3test;
+use i3test i3_autostart => 0;
+
+my $pid = launch_with_config('-default');
# We cannot use events_for in this test as we cannot send events after
# issuing the restart/shutdown command.
@@ -59,7 +61,7 @@
}
})->recv;
-cmd 'exit';
+exit_gracefully($pid);
$e = $cv->recv;
diff --git a/testcases/t/294-focus-order.t b/testcases/t/294-focus-order.t
index ce79001..dfadd0a 100644
--- a/testcases/t/294-focus-order.t
+++ b/testcases/t/294-focus-order.t
@@ -14,7 +14,7 @@
# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
# (unless you are already familiar with Perl)
#
-# Verify that the corrent focus stack order is preserved after various
+# Verify that the current focus stack order is preserved after various
# operations.
use i3test i3_config => < 0;
+
+# starts i3 with the given config, opens a window, returns its border style
+sub launch_get_border {
+ my ($config) = @_;
+
+ my $pid = launch_with_config($config);
+
+ my $i3 = i3(get_socket_path(0));
+ my $tmp = fresh_workspace;
+
+ my $window = open_window(name => 'special title');
+
+ my @content = @{get_ws_content($tmp)};
+ cmp_ok(@content, '==', 1, 'one node on this workspace now');
+ my $border = $content[0]->{border};
+
+ exit_gracefully($pid);
+
+ return $border;
+}
+
+#####################################################################
+# test thet windows get the default border
+#####################################################################
+
+my $config = < 1);
+my $varconfig = <<'EOT';
+set $vartest special title
+for_window [title="$vartest"] border none
+EOT
+print $fh $varconfig;
+$fh->flush;
+
+$config = < 1);
+print $indirectfh <flush;
+
+$config = < 1);
+print $indirectfh2 <flush;
+
+$config = < 1);
+$permissiondeniedfh->flush;
+my $mode = 0055;
+chmod($mode, $permissiondenied);
+
+$config = < 1);
+unlink($dangling);
+symlink("/dangling", $dangling);
+
+$config = < 1);
+print $varfh <<'EOT';
+for_window [title="$vartest"] border none
+
+EOT
+$varfh->flush;
+
+$config = < 1);
+print $varfh <<'EOT';
+set $vartest special title
+EOT
+$varfh->flush;
+
+$config = < 1);
+$wsfh->flush;
+
+$config = <get_workspaces->recv->[0]->{name};
+
+ exit_gracefully($pid);
+
+ return $name;
+}
+
+is(launch_get_workspace_name($config), '1: eggs', 'workspace name');
+
+################################################################################
+# loop prevention
+################################################################################
+
+my ($loopfh1, $loopname1) = tempfile(UNLINK => 1);
+my ($loopfh2, $loopname2) = tempfile(UNLINK => 1);
+
+print $loopfh1 <flush;
+
+print $loopfh2 <flush;
+
+$config = <get_version()->recv;
+my $included = $version->{included_config_file_names};
+
+is_deeply($included, [ $indirectfilename2, $filename ], 'included config file names correct');
+
+exit_gracefully($pid);
+
+################################################################################
+# Verify the GET_CONFIG IPC reply returns the top-level config
+################################################################################
+
+my $tmpdir = tempdir(CLEANUP => 1);
+my $socketpath = $tmpdir . "/config.sock";
+ok(! -e $socketpath, "$socketpath does not exist yet");
+
+my ($indirectfh3, $indirectfilename3) = tempfile(UNLINK => 1);
+my $indirectconfig = <flush;
+
+$config = < 1, dont_create_temp_dir => 1);
+
+my $i3 = i3(get_socket_path(0));
+my $config_reply = $i3->get_config()->recv;
+
+is($config_reply->{config}, $config, 'GET_CONFIG returns the top-level config file');
+
+my $included = $config_reply->{included_configs};
+is(scalar @{$included}, 3, 'included_configs contains all 3 files');
+is($included->[0]->{raw_contents}, $config, 'included_configs->[0]->{raw_contents} contains top-level config');
+is($included->[1]->{raw_contents}, $indirectconfig, 'included_configs->[1]->{raw_contents} contains indirect config');
+is($included->[2]->{raw_contents}, $varconfig, 'included_configs->[2]->{raw_contents} contains variable config');
+
+my $indirect_replaced_config = <[1]->{variable_replaced_contents}, $indirect_replaced_config, 'included_configs->[1]->{variable_replaced_contents} contains config with variables replaced');
+
+exit_gracefully($pid);
+
+
+done_testing;
diff --git a/testcases/t/314-window-icon-padding.t b/testcases/t/314-window-icon-padding.t
new file mode 100755
index 0000000..71cf5bd
--- /dev/null
+++ b/testcases/t/314-window-icon-padding.t
@@ -0,0 +1,92 @@
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Please read the following documents before working on tests:
+# • https://build.i3wm.org/docs/testsuite.html
+# (or docs/testsuite)
+#
+# • https://build.i3wm.org/docs/lib-i3test.html
+# (alternatively: perldoc ./testcases/lib/i3test.pm)
+#
+# • https://build.i3wm.org/docs/ipc.html
+# (or docs/ipc)
+#
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
+# (unless you are already familiar with Perl)
+#
+# Verifies title_window_icon behavior.
+use i3test i3_autostart => 0;
+
+sub window_icon_padding {
+ my ($ws) = @_;
+ my ($nodes, $focus) = get_ws_content($ws);
+ ok(@{$nodes} == 1, 'precisely one container on workspace');
+ return $nodes->[0]->{'window_icon_padding'};
+}
+
+my $config = <<"EOT";
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+EOT
+my $pid = launch_with_config($config);
+
+my $tmp = fresh_workspace;
+
+cmd 'open';
+is(window_icon_padding($tmp), -1, 'window_icon_padding defaults to -1');
+
+cmd 'title_window_icon on';
+isnt(window_icon_padding($tmp), -1, 'window_icon_padding no longer -1');
+
+cmd 'title_window_icon toggle';
+is(window_icon_padding($tmp), -1, 'window_icon_padding back to -1');
+
+cmd 'title_window_icon toggle';
+isnt(window_icon_padding($tmp), -1, 'window_icon_padding no longer -1 again');
+
+cmd 'title_window_icon off';
+is(window_icon_padding($tmp), -1, 'window_icon_padding back to -1');
+
+cmd 'title_window_icon padding 3px';
+is(window_icon_padding($tmp), 3, 'window_icon_padding set to 3');
+
+cmd 'title_window_icon toggle';
+ok(window_icon_padding($tmp) < 0, 'window_icon_padding toggled off');
+
+cmd 'title_window_icon toggle';
+is(window_icon_padding($tmp), 3, 'window_icon_padding toggled back to 3');
+
+cmd 'title_window_icon toggle 5px';
+ok(window_icon_padding($tmp) < 0, 'window_icon_padding toggled off');
+
+cmd 'title_window_icon toggle 5px';
+is(window_icon_padding($tmp), 5, 'window_icon_padding toggled on to 5px');
+
+cmd 'title_window_icon toggle 5px';
+ok(window_icon_padding($tmp) < 0, 'window_icon_padding toggled off');
+
+cmd 'title_window_icon toggle 4px';
+is(window_icon_padding($tmp), 4, 'window_icon_padding toggled on to 4px');
+
+exit_gracefully($pid);
+
+################################################################################
+# Verify title_window_icon can be used with for_window as expected
+################################################################################
+
+$config = <<"EOT";
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+for_window [class=".*"] title_window_icon padding 3px
+EOT
+$pid = launch_with_config($config);
+
+$tmp = fresh_workspace;
+
+open_window;
+is(window_icon_padding($tmp), 3, 'window_icon_padding set to 3');
+
+exit_gracefully($pid);
+
+done_testing;
diff --git a/testcases/t/315-all-criterion.t b/testcases/t/315-all-criterion.t
new file mode 100644
index 0000000..f11410f
--- /dev/null
+++ b/testcases/t/315-all-criterion.t
@@ -0,0 +1,90 @@
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Please read the following documents before working on tests:
+# • https://build.i3wm.org/docs/testsuite.html
+# (or docs/testsuite)
+#
+# • https://build.i3wm.org/docs/lib-i3test.html
+# (alternatively: perldoc ./testcases/lib/i3test.pm)
+#
+# • https://build.i3wm.org/docs/ipc.html
+# (or docs/ipc)
+#
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
+# (unless you are already familiar with Perl)
+#
+# Tests all kinds of matching methods
+#
+use i3test;
+
+my $tmp = fresh_workspace;
+
+ok(@{get_ws_content($tmp)} == 0, 'no containers yet');
+
+# Open a new window
+my $window = open_window;
+my $content = get_ws_content($tmp);
+ok(@{$content} == 1, 'window mapped');
+my $win = $content->[0];
+
+######################################################################
+# check that simple matching works.
+######################################################################
+cmd '[all] kill';
+
+sync_with_i3;
+
+is_num_children($tmp, 0, 'window killed');
+
+######################################################################
+# check that simple matching against multiple windows works.
+######################################################################
+
+$tmp = fresh_workspace;
+
+my $left = open_window;
+ok($left->mapped, 'left window mapped');
+
+my $right = open_window;
+ok($right->mapped, 'right window mapped');
+
+# two windows should be here
+is_num_children($tmp, 2, 'two windows opened');
+
+cmd '[all] kill';
+
+sync_with_i3;
+
+is_num_children($tmp, 0, 'two windows killed');
+
+######################################################################
+# check that multiple criteria work are checked with a logical AND,
+# not a logical OR (that is, matching is not cancelled after the first
+# criterion matches).
+######################################################################
+
+$tmp = fresh_workspace;
+
+my $left = open_window(name => 'left');
+ok($left->mapped, 'left window mapped');
+
+my $right = open_window(name => 'right');
+ok($right->mapped, 'right window mapped');
+
+# two windows should be here
+is_num_children($tmp, 2, 'two windows opened');
+
+cmd '[all title="left"] kill';
+
+sync_with_i3;
+
+is_num_children($tmp, 1, 'one window still there');
+
+cmd '[all] kill';
+
+sync_with_i3;
+
+is_num_children($tmp, 0, 'all windows killed');
+
+done_testing;
diff --git a/testcases/t/315-long-commands.t b/testcases/t/315-long-commands.t
new file mode 100644
index 0000000..3db37da
--- /dev/null
+++ b/testcases/t/315-long-commands.t
@@ -0,0 +1,46 @@
+#!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 commands with more than 10 non-identified words doesn't works
+# 10 is the magic number chosen for the stack size which is why it's used here
+# Ticket: #2968
+# Bug still in: 4.19.2-103-gfc65ca36
+use i3test;
+
+######################################################################
+# 1) run a long command
+######################################################################
+
+my $i3 = i3(get_socket_path());
+my $tmp = fresh_workspace;
+
+my $floatwin = open_floating_window;
+
+
+my ($absolute_before, $top_before) = $floatwin->rect;
+
+cmd 'move window container to window container to window container to left';
+
+sync_with_i3;
+
+my ($absolute, $top) = $floatwin->rect;
+
+is($absolute->x, ($absolute_before->x - 10), 'moved 10 px to the left');
+is($absolute->y, $absolute_before->y, 'y not changed');
+is($absolute->width, $absolute_before->width, 'width not changed');
+is($absolute->height, $absolute_before->height, 'height not changed');
+
+done_testing;
diff --git a/testcases/t/316-drag-container.t b/testcases/t/316-drag-container.t
new file mode 100644
index 0000000..7e2b849
--- /dev/null
+++ b/testcases/t/316-drag-container.t
@@ -0,0 +1,321 @@
+#!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 dragging containers.
+
+my ($width, $height) = (1000, 500);
+
+my $config = <<"EOT";
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+focus_follows_mouse no
+floating_modifier Mod1
+
+# 2 side by side outputs
+fake-outputs ${width}x${height}+0+0P,${width}x${height}+${width}+0
+
+bar {
+ output primary
+}
+EOT
+use i3test i3_autostart => 0;
+use i3test::XTEST;
+my $pid = launch_with_config($config);
+
+sub start_drag {
+ my ($pos_x, $pos_y) = @_;
+ die "Drag outside of bounds!" unless $pos_x < $width * 2 && $pos_y < $height;
+
+ $x->root->warp_pointer($pos_x, $pos_y);
+ sync_with_i3;
+
+ xtest_key_press(64); # Alt_L
+ xtest_button_press(1, $pos_x, $pos_y);
+ xtest_sync_with_i3;
+}
+
+sub end_drag {
+ my ($pos_x, $pos_y) = @_;
+ die "Drag outside of bounds!" unless $pos_x < $width * 2 && $pos_y < $height;
+
+ $x->root->warp_pointer($pos_x, $pos_y);
+ sync_with_i3;
+
+ xtest_button_release(1, $pos_x, $pos_y);
+ xtest_key_release(64); # Alt_L
+ xtest_sync_with_i3;
+}
+
+my ($ws1, $ws2);
+my ($A, $B, $tmp);
+my ($A_id, $B_id);
+
+sub move_subtest {
+ my ($cb, $win) = @_;
+
+ my @events = events_for($cb, 'window');
+ my @move = grep { $_->{change} eq 'move' } @events;
+
+ is(scalar @move, 1, 'Received 1 window::move event');
+ is($move[0]->{container}->{window}, $A->{id}, "window id matches");
+}
+
+###############################################################################
+# Drag floating container onto an empty workspace.
+###############################################################################
+
+$ws2 = fresh_workspace(output => 1);
+$ws1 = fresh_workspace(output => 0);
+$A = open_floating_window(rect => [ 30, 30, 50, 50 ]);
+
+start_drag(40, 40);
+end_drag(1050, 50);
+
+is($x->input_focus, $A->id, 'Floating window moved to the right workspace');
+is($ws2, focused_ws, 'Empty workspace focused after floating window dragged to it');
+
+###############################################################################
+# Drag tiling container onto an empty workspace.
+###############################################################################
+
+subtest "Draging tiling container onto an empty workspace produces move event", \&move_subtest,
+sub {
+
+$ws2 = fresh_workspace(output => 1);
+$ws1 = fresh_workspace(output => 0);
+$A = open_window;
+
+start_drag(50, 50);
+end_drag(1050, 50);
+
+is($x->input_focus, $A->id, 'Tiling window moved to the right workspace');
+is($ws2, focused_ws, 'Empty workspace focused after tiling window dragged to it');
+
+};
+
+###############################################################################
+# Drag tiling container onto a container that closes before the drag is
+# complete.
+###############################################################################
+
+$ws1 = fresh_workspace(output => 0);
+$A = open_window;
+open_window;
+
+start_drag(600, 300); # Start dragging the second window.
+
+# Try to place it on the first window.
+$x->root->warp_pointer(50, 50);
+sync_with_i3;
+
+cmd '[id=' . $A->id . '] kill';
+sync_with_i3;
+end_drag(50, 50);
+
+is(@{get_ws_content($ws1)}, 1, 'One container left in ws1');
+
+###############################################################################
+# Drag tiling container onto a tiling container on an other workspace.
+###############################################################################
+
+subtest "Draging tiling container onto a tiling container on an other workspace produces move event", \&move_subtest,
+sub {
+
+$ws2 = fresh_workspace(output => 1);
+open_window;
+$B_id = get_focused($ws2);
+$ws1 = fresh_workspace(output => 0);
+$A = open_window;
+$A_id = get_focused($ws1);
+
+start_drag(50, 50);
+end_drag(1500, 250); # Center of right output, inner region.
+
+is($ws2, focused_ws, 'Workspace focused after tiling window dragged to it');
+$ws2 = get_ws($ws2);
+is($ws2->{focus}[0], $A_id, 'A focused first, dragged container kept focus');
+is($ws2->{focus}[1], $B_id, 'B focused second');
+
+};
+
+###############################################################################
+# Drag tiling container onto a floating container on an other workspace.
+###############################################################################
+
+subtest "Draging tiling container onto a floating container on an other workspace produces move event", \&move_subtest,
+sub {
+
+$ws2 = fresh_workspace(output => 1);
+open_floating_window;
+$B_id = get_focused($ws2);
+$ws1 = fresh_workspace(output => 0);
+$A = open_window;
+$A_id = get_focused($ws1);
+
+start_drag(50, 50);
+end_drag(1500, 250);
+
+is($ws2, focused_ws, 'Workspace with one floating container focused after tiling window dragged to it');
+$ws2 = get_ws($ws2);
+is($ws2->{focus}[0], $A_id, 'A focused first, dragged container kept focus');
+is($ws2->{floating_nodes}[0]->{nodes}[0]->{id}, $B_id, 'B exists & floating');
+
+};
+
+###############################################################################
+# Drag tiling container onto a bar.
+###############################################################################
+
+subtest "Draging tiling container onto a bar produces move event", \&move_subtest,
+sub {
+
+$ws1 = fresh_workspace(output => 0);
+open_window;
+$B_id = get_focused($ws1);
+$ws2 = fresh_workspace(output => 1);
+$A = open_window;
+$A_id = get_focused($ws2);
+
+start_drag(1500, 250);
+end_drag(1, 498); # Bar on bottom of left output.
+
+is($ws1, focused_ws, 'Workspace focused after tiling window dragged to its bar');
+$ws1 = get_ws($ws1);
+is($ws1->{focus}[0], $A_id, 'B focused first, dragged container kept focus');
+is($ws1->{focus}[1], $B_id, 'A focused second');
+
+};
+
+###############################################################################
+# Drag an unfocused tiling container onto it's self.
+###############################################################################
+
+$ws1 = fresh_workspace(output => 0);
+open_window;
+$A_id = get_focused($ws1);
+open_window;
+$B_id = get_focused($ws1);
+
+start_drag(50, 50);
+end_drag(450, 450);
+
+$ws1 = get_ws($ws1);
+is($ws1->{focus}[0], $B_id, 'B focused first, kept focus');
+is($ws1->{focus}[1], $A_id, 'A focused second, unfocused dragged container didn\'t gain focus');
+
+###############################################################################
+# Drag an unfocused tiling container onto an occupied workspace.
+###############################################################################
+
+subtest "Draging unfocused tiling container onto an occupied workspace produces move event", \&move_subtest,
+sub {
+
+$ws1 = fresh_workspace(output => 0);
+$A = open_window;
+$A_id = get_focused($ws1);
+$ws2 = fresh_workspace(output => 1);
+open_window;
+$B_id = get_focused($ws2);
+
+start_drag(50, 50);
+end_drag(1500, 250); # Center of right output, inner region.
+
+is($ws2, focused_ws, 'Workspace remained focused after dragging unfocused container');
+$ws2 = get_ws($ws2);
+is($ws2->{focus}[0], $B_id, 'B focused first, kept focus');
+is($ws2->{focus}[1], $A_id, 'A focused second, unfocused container didn\'t steal focus');
+
+};
+
+###############################################################################
+# Drag fullscreen container onto window in same workspace.
+###############################################################################
+
+$ws1 = fresh_workspace(output => 0);
+open_window;
+$A = open_window;
+cmd 'fullscreen enable';
+
+start_drag(900, 100); # Second window
+end_drag(50, 50); # To first window
+
+is($ws1, focused_ws, 'Workspace remained focused after dragging fullscreen container');
+is_num_fullscreen($ws1, 1, 'Container still fullscreened');
+is($x->input_focus, $A->id, 'Fullscreen container still focused');
+
+###############################################################################
+# Drag unfocused fullscreen container onto window in other workspace.
+###############################################################################
+
+subtest "Draging unfocused fullscreen container onto window in other workspace produces move event", \&move_subtest,
+sub {
+
+$ws1 = fresh_workspace(output => 0);
+$A = open_window;
+cmd 'fullscreen enable';
+$ws2 = fresh_workspace(output => 1);
+open_window;
+open_window;
+
+start_drag(900, 100);
+end_drag(1000 + 500 * 0.15 + 10, 200); # left of leftmost window
+
+is($ws2, focused_ws, 'Workspace still focused after dragging fullscreen container to it');
+is_num_fullscreen($ws1, 0, 'No fullscreen container in first workspace');
+is_num_fullscreen($ws2, 1, 'Moved container still fullscreened');
+is($x->input_focus, $A->id, 'Fullscreen container now focused');
+$ws2 = get_ws($ws2);
+is($ws2->{nodes}->[0]->{window}, $A->id, 'Fullscreen container now leftmost window in second workspace');
+
+};
+
+###############################################################################
+# Drag unfocused fullscreen container onto left outter region of window in
+# other workspace. The container shouldn't end up in $ws2 because it was
+# dragged onto the outter region of the leftmost window. We must also check
+# that the focus remains on the other window.
+###############################################################################
+
+subtest "Draging unfocused fullscreen container onto left outter region of window in other workspace produces move event", \&move_subtest,
+sub {
+
+$ws1 = fresh_workspace(output => 0);
+open_window for (1..3);
+$A = open_window;
+$tmp = get_focused($ws1);
+cmd 'fullscreen enable';
+$ws2 = fresh_workspace(output => 1);
+$B = open_window;
+
+start_drag(990, 100); # rightmost of $ws1
+end_drag(1004, 100); # outter region of window of $ws2
+
+is($ws2, focused_ws, 'Workspace still focused after dragging fullscreen container to it');
+is_num_fullscreen($ws1, 1, 'Fullscreen container still in first workspace');
+is_num_fullscreen($ws2, 0, 'No fullscreen container in second workspace');
+is($x->input_focus, $B->id, 'Window of second workspace still has focus');
+is(get_focused($ws1), $tmp, 'Fullscreen container still focused in first workspace');
+$ws1 = get_ws($ws1);
+is($ws1->{nodes}->[3]->{window}, $A->id, 'Fullscreen container still rightmost window in first workspace');
+
+};
+
+exit_gracefully($pid);
+
+###############################################################################
+
+done_testing;
diff --git a/testcases/t/316-transient-for-loop.t b/testcases/t/316-transient-for-loop.t
new file mode 100644
index 0000000..336a8d8
--- /dev/null
+++ b/testcases/t/316-transient-for-loop.t
@@ -0,0 +1,42 @@
+#!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 i3 does not get stuck in an endless loop between two windows that
+# set transient_for for each other.
+# Ticket: #4404
+# Bug still in: 4.20-69-g43e805a00
+#
+use i3test i3_config => < 1 });
+my $w2 = open_window({ dont_map => 1 });
+
+$w1->transient_for($w2);
+$w2->transient_for($w1);
+$w1->map;
+$w2->map;
+
+does_i3_live;
+
+done_testing;
diff --git a/testcases/t/317-bar-config-font-order.t b/testcases/t/317-bar-config-font-order.t
new file mode 100644
index 0000000..3c1fdcd
--- /dev/null
+++ b/testcases/t/317-bar-config-font-order.t
@@ -0,0 +1,39 @@
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Please read the following documents before working on tests:
+# • https://build.i3wm.org/docs/testsuite.html
+# (or docs/testsuite)
+#
+# • https://build.i3wm.org/docs/lib-i3test.html
+# (alternatively: perldoc ./testcases/lib/i3test.pm)
+#
+# • https://build.i3wm.org/docs/ipc.html
+# (or docs/ipc)
+#
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
+# (unless you are already familiar with Perl)
+#
+# Verifies that bar config blocks get the i3-wide font configured,
+# regardless of where the font is configured in the config file
+# (before or after the bar config blocks).
+# Ticket: #5031
+# Bug still in: 4.20-105-g4db383e4
+use i3test i3_config => <<'EOT';
+# i3 config file (v4)
+
+bar {
+ # no font directive here, no i3-wide font configured (yet)
+}
+
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+EOT
+
+my $i3 = i3(get_socket_path(0));
+my $bars = $i3->get_bar_config()->recv;
+
+my $bar_id = shift @$bars;
+my $bar_config = $i3->get_bar_config($bar_id)->recv;
+is($bar_config->{font}, 'fixed', 'font ok');
+
+done_testing;
diff --git a/testcases/t/317-bar-output-trailing-space.t b/testcases/t/317-bar-output-trailing-space.t
new file mode 100644
index 0000000..540d30c
--- /dev/null
+++ b/testcases/t/317-bar-output-trailing-space.t
@@ -0,0 +1,71 @@
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Please read the following documents before working on tests:
+# • https://build.i3wm.org/docs/testsuite.html
+# (or docs/testsuite)
+#
+# • https://build.i3wm.org/docs/lib-i3test.html
+# (alternatively: perldoc ./testcases/lib/i3test.pm)
+#
+# • https://build.i3wm.org/docs/ipc.html
+# (or docs/ipc)
+#
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
+# (unless you are already familiar with Perl)
+#
+# Verifies that any trailing whitespace in strings (including in
+# “bar { output }” in particular) is stripped.
+# Ticket: #5064
+# Bug still in: 4.20-105-g4db383e4
+use i3test i3_autostart => 0;
+
+# Test with a single output.
+
+my $config = <get_bar_config()->recv;
+is(@$bars, 1, 'one bar configured');
+my $bar_id = shift @$bars;
+
+my $bar_config = $i3->get_bar_config($bar_id)->recv;
+is_deeply($bar_config->{outputs}, [ 'anything' ], 'outputs do not have trailing whitespace');
+
+exit_gracefully($pid);
+
+# Test with multiple outputs for a single bar.
+
+$config = <get_bar_config()->recv;
+is(@$bars, 1, 'one bar configured');
+$bar_id = shift @$bars;
+
+$bar_config = $i3->get_bar_config($bar_id)->recv;
+is_deeply($bar_config->{outputs}, [ 'nospace', 'singlespace' ], 'outputs do not have trailing whitespace');
+
+exit_gracefully($pid);
+
+
+done_testing;
diff --git a/testcases/t/318-i3-dmenu-desktop.t b/testcases/t/318-i3-dmenu-desktop.t
new file mode 100644
index 0000000..75d983e
--- /dev/null
+++ b/testcases/t/318-i3-dmenu-desktop.t
@@ -0,0 +1,125 @@
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Please read the following documents before working on tests:
+# • https://build.i3wm.org/docs/testsuite.html
+# (or docs/testsuite)
+#
+# • https://build.i3wm.org/docs/lib-i3test.html
+# (alternatively: perldoc ./testcases/lib/i3test.pm)
+#
+# • https://build.i3wm.org/docs/ipc.html
+# (or docs/ipc)
+#
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
+# (unless you are already familiar with Perl)
+#
+# Verifies that i3-dmenu-desktop correctly parses Exec= lines in .desktop files
+# and sends the command to i3 for execution.
+# Ticket: #5152, #5156
+# Bug still in: 4.21-17-g389d555d
+use i3test;
+use i3test::Util qw(slurp);
+use File::Temp qw(tempfile tempdir);
+use POSIX qw(mkfifo);
+use JSON::XS qw(decode_json);
+
+my $desktopdir = tempdir(CLEANUP => 1);
+
+$ENV{XDG_DATA_DIRS} = "$desktopdir";
+
+mkdir("$desktopdir/applications");
+
+# Create an i3-msg executable that dumps command line flags to a FIFO
+my $tmpdir = tempdir(CLEANUP => 1);
+
+$ENV{PATH} = "$tmpdir:" . $ENV{PATH};
+
+mkfifo("$tmpdir/fifo", 0600) or BAIL_OUT "Could not create FIFO: $!";
+
+open(my $i3msg_dump, '>', "$tmpdir/i3-msg");
+say $i3msg_dump <', "$tmpdir/fifo");
+say \$f encode_json(\\\@ARGV);
+close(\$f);
+EOT
+close($i3msg_dump);
+chmod 0755, "$tmpdir/i3-msg";
+
+my $testcnt = 0;
+sub verify_exec {
+ my ($execline, $want_arg) = @_;
+
+ $testcnt++;
+
+ open(my $desktop, '>', "$desktopdir/applications/desktop$testcnt.desktop");
+ say $desktop <get_tree->recv;
- my $focused = $tree->{focus}->[0];
- my $output = first { $_->{id} == $focused } @{$tree->{nodes}};
- return $output->{name};
-}
sync_with_i3;
$x->root->warp_pointer(0, 0);
diff --git a/testcases/t/506-focus-right.t b/testcases/t/506-focus-right.t
index 871bc1c..2d8ed20 100644
--- a/testcases/t/506-focus-right.t
+++ b/testcases/t/506-focus-right.t
@@ -149,32 +149,25 @@
# Ensure that focusing right/left works in the expected order.
############################################################################
-sub get_focused_output {
- my $tree = i3(get_socket_path())->get_tree->recv;
- my ($focused_id) = @{$tree->{focus}};
- my ($output) = grep { $_->{id} == $focused_id } @{$tree->{nodes}};
- return $output->{name};
-}
-
-is(get_focused_output(), 'fake-0', 'focus on fake-0');
+is(focused_output, 'fake-0', 'focus on fake-0');
cmd 'focus output right';
-is(get_focused_output(), 'fake-1', 'focus on fake-1');
+is(focused_output, 'fake-1', 'focus on fake-1');
cmd 'focus output right';
-is(get_focused_output(), 'fake-2', 'focus on fake-2');
+is(focused_output, 'fake-2', 'focus on fake-2');
cmd 'focus output left';
-is(get_focused_output(), 'fake-1', 'focus on fake-1');
+is(focused_output, 'fake-1', 'focus on fake-1');
cmd 'focus output left';
-is(get_focused_output(), 'fake-0', 'focus on fake-0');
+is(focused_output, 'fake-0', 'focus on fake-0');
cmd 'focus output left';
-is(get_focused_output(), 'fake-2', 'focus on fake-2 (wrapping)');
+is(focused_output, 'fake-2', 'focus on fake-2 (wrapping)');
cmd 'focus output right';
-is(get_focused_output(), 'fake-0', 'focus on fake-0 (wrapping)');
+is(focused_output, 'fake-0', 'focus on fake-0 (wrapping)');
exit_gracefully($pid);
diff --git a/testcases/t/531-fullscreen-on-given-output.t b/testcases/t/531-fullscreen-on-given-output.t
index fb14b7a..2459f06 100644
--- a/testcases/t/531-fullscreen-on-given-output.t
+++ b/testcases/t/531-fullscreen-on-given-output.t
@@ -55,7 +55,7 @@
# Check that the windows are on the correct output
is_deeply(scalar $win_on_first_output->rect, $orig_rect1, "first window spans the first output");
-is_deeply(scalar $win_on_second_output->rect, $orig_rect2, "second window spans the sencond output");
+is_deeply(scalar $win_on_second_output->rect, $orig_rect2, "second window spans the second output");
# Check that both windows remained fullscreen
my $tree = i3(get_socket_path())->get_tree->recv;
diff --git a/testcases/t/543-move-workspace-to-multiple-outputs.t b/testcases/t/543-move-workspace-to-multiple-outputs.t
new file mode 100644
index 0000000..7b0f8c2
--- /dev/null
+++ b/testcases/t/543-move-workspace-to-multiple-outputs.t
@@ -0,0 +1,103 @@
+#!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 using multiple outputs for 'move workspace to output …'
+# Ticket: #4337
+use i3test i3_config => < $out_num: $msg");
+}
+
+###############################################################################
+# Test moving workspace to same output
+# See issue #4691
+###############################################################################
+is_ws(1, 0, 'sanity check');
+
+my $reply = cmd '[con_mark=aa] move workspace to output fake-0';
+is_ws(1, 0, 'workspace did not move');
+ok($reply->[0]->{success}, 'reply success');
+
+###############################################################################
+# Test using "next" special keyword
+###############################################################################
+
+is_ws(1, 0, 'sanity check');
+is_ws(3, 2, 'sanity check');
+
+for (my $i = 1; $i < 9; $i++) {
+ cmd '[con_mark=a] move workspace to output next';
+ my $out1 = $i % 4;
+ my $out3 = ($i + 2) % 4;
+
+ is_ws(1, $out1, 'move workspace to next');
+ is_ws(3, $out3, 'move workspace to next');
+}
+
+###############################################################################
+# Same as above but explicitly type all the outputs
+###############################################################################
+
+is_ws(1, 0, 'sanity check');
+is_ws(3, 2, 'sanity check');
+
+for (my $i = 1; $i < 10; $i++) {
+ cmd '[con_mark=a] move workspace to output fake-0 fake-1 fake-2 fake-3';
+ my $out1 = $i % 4;
+ my $out3 = ($i + 2) % 4;
+
+ is_ws(1, $out1, 'cycle through explicit outputs');
+ is_ws(3, $out3, 'cycle through explicit outputs');
+}
+
+###############################################################################
+# Use a subset of the outputs plus some non-existing outputs
+###############################################################################
+
+cmd '[con_mark=aa] move workspace to output fake-1';
+cmd '[con_mark=ab] move workspace to output fake-1';
+is_ws(1, 1, 'start from fake-1 which is not included in output list');
+is_ws(3, 1, 'start from fake-1 which is not included in output list');
+
+my @order = (0, 3, 2);
+for (my $i = 0; $i < 10; $i++) {
+ cmd '[con_mark=a] move workspace to output doesnotexist fake-0 alsodoesnotexist fake-3 fake-2';
+
+ my $out = $order[$i % 3];
+ is_ws(1, $out, 'cycle through shuffled outputs');
+ is_ws(3, $out, 'cycle through shuffled outputs');
+
+}
+
+done_testing;
diff --git a/testcases/t/544-focus-multiple-outputs.t b/testcases/t/544-focus-multiple-outputs.t
new file mode 100644
index 0000000..504b8d1
--- /dev/null
+++ b/testcases/t/544-focus-multiple-outputs.t
@@ -0,0 +1,67 @@
+#!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 using multiple output for 'focus output …'
+# Ticket: #4619
+use i3test i3_config => <new;
+my $reply = $x->get_selection_owner($x->atom(name => 'WM_S0')->id);
+ok($reply, "registration successful");
+done_testing;
diff --git a/testcases/t/546-empty-bindcommand.t b/testcases/t/546-empty-bindcommand.t
new file mode 100644
index 0000000..dae2629
--- /dev/null
+++ b/testcases/t/546-empty-bindcommand.t
@@ -0,0 +1,31 @@
+#!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 i3 doesn't crash if the binding command is empty.
+# Ticket: #5000
+
+use i3test i3_autostart => 0;
+
+my $config = < 0;
+
+my $config = < 0;
+
+#######################################################################
+# Test calloc crash
+#######################################################################
+
+my $config = <<'EOT';
+# i3 config file (v4)
+set $long_variable_name_with_short_value 1
+set $$long_variable_name_with_short_value 2
+set $$$long_variable_name_with_short_value 3
+EOT
+
+my $pid = launch_with_config($config);
+
+# ==2108678==ERROR: AddressSanitizer: requested allocation size 0xffffffffffffffe1 (0x7e8 after adjustments for alignment, red zones etc.) exceeds maximum supported size of 0x10000000000 (thread T0)
+# #0 0x7feaa9cbf411 in __interceptor_calloc /usr/src/debug/gcc/libsanitizer/asan/asan_malloc_linux.cpp:77
+# #1 0x560867c0c13d in scalloc ../libi3/safewrappers.c:29
+# #2 0x560867b7bd63 in parse_file ../src/config_parser.c:1030
+# #3 0x560867b6b1b8 in load_configuration ../src/config.c:261
+# #4 0x560867baf9cf in main ../src/main.c:677
+# #5 0x7feaa95b52cf (/usr/lib/libc.so.6+0x232cf)
+
+does_i3_live;
+
+exit_gracefully($pid);
+
+
+#######################################################################
+# Test buffer overflow
+#######################################################################
+
+$config = <<'EOT';
+# i3 config file (v4)
+set $x 1
+set $$x 2
+EOT
+
+$pid = launch_with_config($config);
+
+# ==2110007==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6070000014f2 at pc 0x558590f680ba bp 0x7ffced72b760 sp 0x7ffced72b750
+# WRITE of size 1 at 0x6070000014f2 thread T0
+# #0 0x558590f680b9 in parse_file ../src/config_parser.c:1051
+# #1 0x558590f571b8 in load_configuration ../src/config.c:261
+# #2 0x558590f9b9cf in main ../src/main.c:677
+# #3 0x7f81c61c82cf (/usr/lib/libc.so.6+0x232cf)
+# #4 0x7f81c61c8389 in __libc_start_main (/usr/lib/libc.so.6+0x23389)
+# #5 0x558590f0bd54 in _start ../sysdeps/x86_64/start.S:115
+
+# 0x6070000014f2 is located 0 bytes to the right of 66-byte region [0x6070000014b0,0x6070000014f2)
+# allocated by thread T0 here:
+# #0 0x7f81c68bf411 in __interceptor_calloc /usr/src/debug/gcc/libsanitizer/asan/asan_malloc_linux.cpp:77
+# #1 0x558590ff813d in scalloc ../libi3/safewrappers.c:29
+# #2 0x558590f67d63 in parse_file ../src/config_parser.c:1030
+# #3 0x558590f571b8 in load_configuration ../src/config.c:261
+# #4 0x558590f9b9cf in main ../src/main.c:677
+# #5 0x7f81c61c82cf (/usr/lib/libc.so.6+0x232cf)
+
+does_i3_live;
+
+exit_gracefully($pid);
+done_testing;
diff --git a/testcases/t/548-motif-hints.t b/testcases/t/548-motif-hints.t
new file mode 100644
index 0000000..99b36b0
--- /dev/null
+++ b/testcases/t/548-motif-hints.t
@@ -0,0 +1,204 @@
+#!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 setting and unsetting motif hints updates window decorations
+# accordingly, respecting user configuration.
+# Ticket: #3678
+# Ticket: #5149
+# Bug still in: 4.21
+use List::Util qw(first);
+use i3test i3_autostart => 0;
+use X11::XCB qw(:all);
+
+my $use_floating;
+sub subtest_with_config {
+ my ($style, $cb) = @_;
+ my $some_other_style = $style eq "normal" ? "pixel" : "normal";
+
+ subtest 'with tiling', sub {
+ my $config = <();
+ exit_gracefully($pid);
+ };
+
+ subtest 'with floating', sub {
+ my $config = <();
+ exit_gracefully($pid);
+ };
+}
+
+sub _change_motif_property {
+ my ($window, $value) = @_;
+ $x->change_property(
+ PROP_MODE_REPLACE,
+ $window->id,
+ $x->atom(name => '_MOTIF_WM_HINTS')->id,
+ $x->atom(name => 'CARDINAL')->id,
+ 32, 5,
+ pack('L5', 2, 0, $value, 0, 0),
+ );
+}
+
+sub open_window_with_motifs {
+ my $value = shift;
+
+ # we don't need other windows anymore, simplifies get_border_style
+ kill_all_windows;
+
+ my $open = \&open_window;
+ if ($use_floating) {
+ $open = \&open_floating_window;
+ }
+
+ my $window = $open->(
+ before_map => sub {
+ my ($window) = @_;
+ _change_motif_property($window, $value);
+ },
+ );
+
+ sync_with_i3;
+
+ return $window;
+}
+
+my $window;
+sub change_motif_property {
+ my $value = shift;
+ _change_motif_property($window, $value);
+ sync_with_i3;
+}
+
+sub get_border_style {
+ if ($use_floating) {
+ my @floating = @{get_ws(focused_ws)->{floating_nodes}};
+ return $floating[0]->{nodes}[0]->{border};
+ }
+
+ return @{get_ws(focused_ws)->{nodes}}[0]->{border};
+}
+
+sub is_border_style {
+ my ($expected, $extra_msg) = @_;
+ my $msg = "border style $expected";
+ if (defined $extra_msg) {
+ $msg = "$msg: $extra_msg";
+ }
+
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ is(get_border_style($window), $expected, $msg);
+}
+
+###############################################################################
+subtest 'with default_border normal', \&subtest_with_config, 'normal',
+sub {
+$window = open_window_with_motifs(0);
+is_border_style('none');
+
+$window = open_window_with_motifs(1 << 0);
+is_border_style('normal');
+
+$window = open_window_with_motifs(1 << 1);
+is_border_style('pixel');
+
+$window = open_window_with_motifs(1 << 3);
+is_border_style('normal');
+
+cmd 'border pixel';
+is_border_style('pixel', 'set by user');
+
+change_motif_property(0);
+is_border_style('none');
+
+change_motif_property(1);
+is_border_style('pixel', 'because of user maximum=pixel');
+
+cmd 'border none';
+is_border_style('none', 'set by user');
+
+change_motif_property(0);
+is_border_style('none');
+
+change_motif_property(1);
+is_border_style('none', 'because of user maximum=none');
+};
+
+subtest 'with default_border pixel', \&subtest_with_config, 'pixel',
+sub {
+$window = open_window_with_motifs(0);
+is_border_style('none');
+
+$window = open_window_with_motifs(1 << 0);
+is_border_style('pixel');
+
+$window = open_window_with_motifs(1 << 1);
+is_border_style('pixel');
+
+$window = open_window_with_motifs(1 << 3);
+is_border_style('pixel');
+
+cmd 'border normal';
+is_border_style('normal', 'set by user');
+
+change_motif_property(0);
+is_border_style('none');
+
+change_motif_property(1);
+is_border_style('normal', 'because of user maximum=normal');
+};
+
+subtest 'with default_border none', \&subtest_with_config, 'none',
+sub {
+$window = open_window_with_motifs(0);
+is_border_style('none');
+
+$window = open_window_with_motifs(1 << 0);
+is_border_style('none');
+
+$window = open_window_with_motifs(1 << 1);
+is_border_style('none');
+
+$window = open_window_with_motifs(1 << 3);
+is_border_style('none');
+
+cmd 'border pixel';
+is_border_style('pixel', 'set by user');
+
+change_motif_property(0);
+is_border_style('none');
+
+change_motif_property(1);
+is_border_style('pixel', 'because of user maximum=pixel');
+};
+
+done_testing;
diff --git a/travis/bintray-autobuild-debian.json b/travis/bintray-autobuild-debian.json
deleted file mode 100644
index d01b7db..0000000
--- a/travis/bintray-autobuild-debian.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "package": {
- "name": "i3-wm",
- "repo": "i3-autobuild",
- "subject": "i3"
- },
-
- "version": {
- "name": "%version%",
- "desc": "TODO",
- "gpgSign": false
- },
-
- "files": [
- {
- "includePattern": "distbuild/deb/debian-amd64/(.*\\.deb)$",
- "matrixParams": {
- "deb_distribution": "sid",
- "deb_component": "main",
- "deb_architecture": "amd64"
- },
- "uploadPattern": "$1"
- },
- {
- "includePattern": "distbuild/deb/debian-i386/(.*\\.deb)$",
- "matrixParams": {
- "deb_distribution": "sid",
- "deb_component": "main",
- "deb_architecture": "i386"
- },
- "uploadPattern": "$1"
- }
- ],
-
- "publish": true
-}
diff --git a/travis/bintray-autobuild-ubuntu.json b/travis/bintray-autobuild-ubuntu.json
deleted file mode 100644
index 4c0d611..0000000
--- a/travis/bintray-autobuild-ubuntu.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "package": {
- "name": "i3-wm",
- "repo": "i3-autobuild-ubuntu",
- "subject": "i3"
- },
-
- "version": {
- "name": "%version%",
- "desc": "TODO",
- "gpgSign": false
- },
-
- "files": [
- {
- "includePattern": "distbuild/deb/ubuntu-amd64/(.*\\.deb)$",
- "matrixParams": {
- "deb_distribution": "bionic",
- "deb_component": "main",
- "deb_architecture": "amd64"
- },
- "uploadPattern": "$1"
- },
- {
- "includePattern": "distbuild/deb/ubuntu-i386/(.*\\.deb)$",
- "matrixParams": {
- "deb_distribution": "bionic",
- "deb_component": "main",
- "deb_architecture": "i386"
- },
- "uploadPattern": "$1"
- }
- ],
-
- "publish": true
-}
diff --git a/travis/check-formatting.sh b/travis/check-formatting.sh
deleted file mode 100755
index 3424513..0000000
--- a/travis/check-formatting.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/sh
-
-set -e
-set -x
-
-clang-format-9 -i $(find . -name "*.[ch]" | tr '\n' ' ') && git diff --exit-code || (echo 'Code was not formatted using clang-format!'; false)
diff --git a/travis/check-spelling.pl b/travis/check-spelling.pl
index f2e79b5..52f84c5 100755
--- a/travis/check-spelling.pl
+++ b/travis/check-spelling.pl
@@ -11,6 +11,7 @@
use v5.10;
use autodie;
use lib 'testcases/lib';
+use lib '/usr/share/lintian/lib';
use i3test::Util qw(slurp);
use Lintian::Spelling qw(check_spelling);
@@ -21,16 +22,16 @@
my $profile = Lintian::Profile->new;
$profile->load('debian', ['/usr/share/lintian']);
-Lintian::Data->set_vendor($profile);
-
my $exitcode = 0;
# Whitelist for spelling errors in manpages, in case the spell checker has
# false-positives.
-my $binary_spelling_exceptions = {
- #'exmaple' => 1, # Example for how to add entries to this whitelist.
- 'betwen' => 1, # asan_flags.inc contains this spelling error.
-};
+my $binary_spelling_exceptions = [
+ #'exmaple', # Example for how to add entries to this whitelist.
+ 'betwen', # asan_flags.inc contains this spelling error.
+ 'dissassemble', # https://reviews.llvm.org/D93902
+ 'oT', # lintian finds this in build/i3bar when built with clang?!
+];
my @binaries = qw(
build/i3
build/i3-config-wizard
@@ -41,7 +42,7 @@
build/i3bar
);
for my $binary (@binaries) {
- check_spelling(slurp($binary), $binary_spelling_exceptions, sub {
+ check_spelling($profile->data, slurp($binary), $binary_spelling_exceptions, sub {
my ($current, $fixed) = @_;
say STDERR qq|Binary "$binary" contains a spelling error: "$current" should be "$fixed"|;
$exitcode = 1;
@@ -50,13 +51,13 @@
# Whitelist for spelling errors in manpages, in case the spell checker has
# false-positives.
-my $manpage_spelling_exceptions = {
-};
+my $manpage_spelling_exceptions = [
+];
for my $name (glob('build/man/*.1')) {
for my $line (split(/\n/, slurp($name))) {
next if $line =~ /^\.\\\"/o;
- check_spelling($line, $manpage_spelling_exceptions, sub {
+ check_spelling($profile->data, $line, $manpage_spelling_exceptions, sub {
my ($current, $fixed) = @_;
say STDERR qq|Manpage "$name" contains a spelling error: "$current" should be "$fixed"|;
$exitcode = 1;
diff --git a/travis/cleanup-bintray.pl b/travis/cleanup-bintray.pl
deleted file mode 100755
index e89efb1..0000000
--- a/travis/cleanup-bintray.pl
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/env perl
-# vim:ts=4:sw=4:expandtab
-
-use strict;
-use warnings;
-use Data::Dumper;
-use HTTP::Tiny; # in core since v5.13.9
-use JSON::PP; # in core since v5.13.9
-use MIME::Base64; # in core since v5.7
-use v5.13;
-
-my $repo = shift;
-
-my $auth = $ENV{'BINTRAY_USER'} . ':' . $ENV{'BINTRAY_KEY'};
-die "BINTRAY_USER and/or BINTRAY_KEY environment variables not set" if $auth eq ':';
-# TODO(stapelberg): switch to putting $auth into the URL once perl-modules ≥
-# 5.20 is available on travis (Ubuntu Wily or newer).
-my $auth_header = 'Basic ' . MIME::Base64::encode_base64($auth, "");
-my $apiurl = 'https://api.bintray.com/packages/i3/' . $repo . '/i3-wm';
-my $client = HTTP::Tiny->new(
- verify_SSL => 1,
- default_headers => {
- 'authorization' => $auth_header,
- });
-my $resp = $client->get($apiurl);
-die "Getting versions failed: HTTP status $resp->{status} (content: $resp->{content})" unless $resp->{success};
-my $decoded = decode_json($resp->{content});
-my @versions = reverse sort {
- (system("/usr/bin/dpkg", "--compare-versions", "$a", "gt", "$b") == 0) ? 1 : -1
-} @{$decoded->{versions}};
-
-# Keep the most recent 5 versions.
-splice(@versions, 0, 5);
-
-for my $version (@versions) {
- say "Deleting old version $version";
- $resp = $client->request('DELETE', "$apiurl/versions/$version");
- die "Deletion of version $version failed: HTTP status $resp->{status} (content: $resp->{content})" unless $resp->{success};
-}
diff --git a/travis/deploy-github-pages.sh b/travis/deploy-github-pages.sh
index 2d45482..b5ead0b 100755
--- a/travis/deploy-github-pages.sh
+++ b/travis/deploy-github-pages.sh
@@ -9,6 +9,10 @@
cp -r deb/COPY-DOCS build.i3wm.org/docs
cd build.i3wm.org
echo build.i3wm.org > CNAME
+# Disallow search engine indexing for build.i3wm.org: users should find the
+# release version instead, and only developers should use build.i3wm.org.
+echo 'User-Agent: *' > robots.txt
+echo 'Disallow: /' >> robots.txt
git init
git config user.name "Travis CI"
diff --git a/travis/prep-bintray.sh b/travis/prep-bintray.sh
deleted file mode 100755
index 4ea56dc..0000000
--- a/travis/prep-bintray.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/sh
-
-set -e
-set -x
-
-sed -i "s,%version%,$(git describe --tags),g" travis/bintray-autobuild-*.json
diff --git a/travis/push-balto.sh b/travis/push-balto.sh
new file mode 100755
index 0000000..ffa5ee1
--- /dev/null
+++ b/travis/push-balto.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+set -e
+
+for fn in distbuild/deb/debian-amd64/*.deb distbuild/deb/debian-i386/*.deb
+do
+ echo "pushing $fn to balto"
+ curl \
+ --header "Authorization: Bearer ${BALTO_TOKEN}" \
+ --form "package=@${fn}" \
+ --form distribution=all \
+ https://i3.baltorepo.com/i3/i3-autobuild/upload/
+done
+
+for fn in distbuild/deb/ubuntu-amd64/*.deb distbuild/deb/ubuntu-i386/*.deb
+do
+ echo "pushing $fn to balto"
+ curl \
+ --header "Authorization: Bearer ${BALTO_TOKEN}" \
+ --form "package=@${fn}" \
+ --form distribution=all \
+ https://i3.baltorepo.com/i3/i3-autobuild-ubuntu/upload/
+done
diff --git a/travis/skip-pkg.sh b/travis/skip-pkg.sh
index 4337120..0ca1d74 100755
--- a/travis/skip-pkg.sh
+++ b/travis/skip-pkg.sh
@@ -2,11 +2,11 @@
# Returns true if Debian/Ubuntu packages should be skipped because this CI run
# was triggered by a pull request.
-# Verify BINTRAY_USER is present (only set on github.com/i3/i3),
+# Verify BALTO_TOKEN is present (only set on github.com/i3/i3),
# otherwise the CI run was triggered by a pull request.
# Verify CC=gcc so that we only build packages once for each commit,
# not twice (with gcc and clang).
-if [ ! -z "$BINTRAY_USER" ] && [ "$CC" = "gcc" ]
+if [ ! -z "$BALTO_TOKEN" ] && [ "$CC" = "gcc" ]
then
exit 1
fi
diff --git a/travis/travis-base-386.Dockerfile b/travis/travis-base-386.Dockerfile
index fc03503..8996e02 100644
--- a/travis/travis-base-386.Dockerfile
+++ b/travis/travis-base-386.Dockerfile
@@ -13,12 +13,11 @@
# (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-9 (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 \
- build-essential clang clang-format-9 \
+ build-essential clang \
lintian && \
rm -rf /var/lib/apt/lists/*
@@ -27,3 +26,8 @@
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/*
+
+# The user outside of Docker (GitHub Actions CI runner) and inside of Docker
+# (root) are different, and newer versions of git error out in that scenario.
+# To fix this, explicitly configure /usr/src/i3 as a safe directory:
+RUN git config --global --add safe.directory /usr/src/i3
diff --git a/travis/travis-base-ubuntu-386.Dockerfile b/travis/travis-base-ubuntu-386.Dockerfile
index 747330a..4a41fe3 100644
--- a/travis/travis-base-ubuntu-386.Dockerfile
+++ b/travis/travis-base-ubuntu-386.Dockerfile
@@ -13,12 +13,11 @@
# (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-9 (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 \
- build-essential clang clang-format-9 \
+ build-essential clang \
lintian && \
rm -rf /var/lib/apt/lists/*
@@ -27,3 +26,8 @@
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/*
+
+# The user outside of Docker (GitHub Actions CI runner) and inside of Docker
+# (root) are different, and newer versions of git error out in that scenario.
+# To fix this, explicitly configure /usr/src/i3 as a safe directory:
+RUN git config --global --add safe.directory /usr/src/i3
diff --git a/travis/travis-base-ubuntu.Dockerfile b/travis/travis-base-ubuntu.Dockerfile
index 8a728af..8523b92 100644
--- a/travis/travis-base-ubuntu.Dockerfile
+++ b/travis/travis-base-ubuntu.Dockerfile
@@ -13,13 +13,12 @@
# (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-9 (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 \
- build-essential clang clang-format-9 \
+ build-essential clang \
lintian && \
rm -rf /var/lib/apt/lists/*
@@ -28,3 +27,8 @@
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/*
+
+# The user outside of Docker (GitHub Actions CI runner) and inside of Docker
+# (root) are different, and newer versions of git error out in that scenario.
+# To fix this, explicitly configure /usr/src/i3 as a safe directory:
+RUN git config --global --add safe.directory /usr/src/i3
diff --git a/travis/travis-base.Dockerfile b/travis/travis-base.Dockerfile
index e5552c1..52ae065 100644
--- a/travis/travis-base.Dockerfile
+++ b/travis/travis-base.Dockerfile
@@ -11,13 +11,12 @@
# (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-9 (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 \
- build-essential clang clang-format-9 \
+ build-essential clang \
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/*
@@ -28,3 +27,8 @@
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/*
+
+# The user outside of Docker (GitHub Actions CI runner) and inside of Docker
+# (root) are different, and newer versions of git error out in that scenario.
+# To fix this, explicitly configure /usr/src/i3 as a safe directory:
+RUN git config --global --add safe.directory /usr/src/i3