├── .github └── workflows │ ├── prepare-release.yml │ ├── publish-testing.yml │ ├── publish-unstable.yml │ └── test-pr.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── debian ├── changelog ├── compat ├── control ├── copyright ├── patches │ └── series ├── rules └── source │ └── format ├── lib ├── .gitmodules └── base64 │ ├── base64.cpp │ └── base64.h └── src └── main.cpp /.github/workflows/prepare-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare a Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | paths: 9 | - debian/changelog 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | release: 17 | runs-on: ubuntu-24.04 18 | container: "ghcr.io/regolith-linux/ci-ubuntu:noble-amd64" 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: "recursive" 24 | 25 | - name: Prepare Release 26 | id: prepare 27 | uses: regolith-linux/actions/prepare-release@main 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.ORG_BROADCAST_TOKEN2 }} 30 | with: 31 | name: "${{ github.event.repository.name }}" 32 | repo: "${{ github.server_url }}/${{ github.repository }}.git" 33 | ref: "${{ github.ref_name }}" 34 | 35 | - name: Push Changes to Voulage 36 | uses: stefanzweifel/git-auto-commit-action@v5 37 | if: ${{ steps.prepare.outputs.release-exists == 'false' }} 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.ORG_BROADCAST_TOKEN2 }} 40 | with: 41 | repository: "${{ steps.prepare.outputs.voulage-path }}" 42 | branch: "main" 43 | file_pattern: "stage/testing/**" 44 | commit_message: "chore: bump ${{ github.event.repository.name }} testing to ${{ steps.prepare.outputs.release-version }}" 45 | commit_user_name: regolith-ci-bot 46 | commit_user_email: bot@regolith-desktop.com 47 | commit_author: "regolith-ci-bot " 48 | 49 | - name: Release Package 50 | uses: softprops/action-gh-release@v2 51 | if: ${{ steps.prepare.outputs.release-exists == 'false' }} 52 | with: 53 | name: ${{ steps.prepare.outputs.release-version }} 54 | tag_name: ${{ steps.prepare.outputs.release-version }} 55 | token: ${{ secrets.ORG_BROADCAST_TOKEN2 }} 56 | target_commitish: "${{ github.sha }}" 57 | generate_release_notes: true 58 | -------------------------------------------------------------------------------- /.github/workflows/publish-testing.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Testing 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | matrix-builder: 14 | runs-on: ubuntu-24.04 15 | outputs: 16 | includes: ${{ steps.builder.outputs.includes }} 17 | runners: ${{ steps.builder.outputs.runners }} 18 | steps: 19 | - name: Build Matrix 20 | id: builder 21 | uses: regolith-linux/actions/build-matrix@main 22 | with: 23 | name: "${{ github.event.repository.name }}" 24 | ref: "${{ github.ref_name }}" 25 | arch: "amd64 arm64" 26 | stage: "testing" 27 | 28 | build: 29 | runs-on: ${{ fromJSON(needs.matrix-builder.outputs.runners)[matrix.arch] }} 30 | needs: matrix-builder 31 | container: "ghcr.io/regolith-linux/ci-${{ matrix.distro }}:${{ matrix.codename }}-${{ matrix.arch }}" 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | include: ${{ fromJSON(needs.matrix-builder.outputs.includes) }} 36 | env: 37 | server-address: "${{ secrets.KAMATERA_HOSTNAME2 }}" 38 | server-username: "${{ secrets.KAMATERA_USERNAME }}" 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | with: 43 | submodules: "recursive" 44 | 45 | - name: Import GPG Key 46 | uses: regolith-linux/actions/import-gpg@main 47 | with: 48 | gpg-key: "${{ secrets.PACKAGE_PRIVATE_KEY2 }}" 49 | 50 | - name: Build Package 51 | id: build 52 | uses: regolith-linux/actions/build-package@main 53 | with: 54 | name: "${{ github.event.repository.name }}" 55 | distro: "${{ matrix.distro }}" 56 | codename: "${{ matrix.codename }}" 57 | stage: "testing" 58 | suite: "testing" 59 | component: "main" 60 | arch: "${{ matrix.arch }}" 61 | 62 | - name: Setup SSH 63 | uses: regolith-linux/actions/setup-ssh@main 64 | with: 65 | ssh-host: "${{ env.server-address }}" 66 | ssh-key: "${{ secrets.KAMATERA_SSH_KEY }}" 67 | 68 | - name: Upload Package 69 | uses: regolith-linux/actions/upload-files@main 70 | with: 71 | upload-to-folder: "${{ github.event.repository.name }}" 72 | 73 | - name: Upload SourceLog 74 | uses: regolith-linux/actions/upload-files@main 75 | with: 76 | upload-from: "${{ steps.build.outputs.buildlog-path }}" 77 | upload-pattern: "SOURCELOG_*.txt" 78 | upload-to-base: "/opt/archives/workspace/" 79 | upload-to-folder: "${{ github.event.repository.name }}" 80 | 81 | sources: 82 | runs-on: ubuntu-24.04 83 | needs: build 84 | container: "ghcr.io/regolith-linux/ci-ubuntu:noble-amd64" 85 | if: ${{ !failure() && !cancelled() }} 86 | env: 87 | server-address: "${{ secrets.KAMATERA_HOSTNAME2 }}" 88 | server-username: "${{ secrets.KAMATERA_USERNAME }}" 89 | steps: 90 | - name: Import GPG Key 91 | uses: regolith-linux/actions/import-gpg@main 92 | with: 93 | gpg-key: "${{ secrets.PACKAGE_PRIVATE_KEY2 }}" 94 | 95 | - name: Setup SSH 96 | uses: regolith-linux/actions/setup-ssh@main 97 | with: 98 | ssh-host: "${{ env.server-address }}" 99 | ssh-key: "${{ secrets.KAMATERA_SSH_KEY }}" 100 | 101 | - name: Rebuild Sources 102 | uses: regolith-linux/actions/rebuild-sources@main 103 | with: 104 | workspace-subfolder: "${{ github.event.repository.name }}" 105 | only-component: "testing" 106 | only-package: "${{ github.event.repository.name }}" 107 | 108 | publish: 109 | runs-on: ubuntu-24.04 110 | needs: sources 111 | container: "ghcr.io/regolith-linux/ci-ubuntu:noble-amd64" 112 | if: ${{ !failure() && !cancelled() }} 113 | env: 114 | server-address: "${{ secrets.KAMATERA_HOSTNAME2 }}" 115 | server-username: "${{ secrets.KAMATERA_USERNAME }}" 116 | steps: 117 | - name: Setup SSH 118 | uses: regolith-linux/actions/setup-ssh@main 119 | with: 120 | ssh-host: "${{ env.server-address }}" 121 | ssh-key: "${{ secrets.KAMATERA_SSH_KEY }}" 122 | 123 | - name: Publish Repo 124 | uses: regolith-linux/actions/publish-repo@main 125 | with: 126 | packages-path-subfolder: "${{ github.event.repository.name }}" 127 | only-component: "testing" 128 | 129 | manifests: 130 | runs-on: ubuntu-24.04 131 | needs: [matrix-builder, publish] 132 | container: "ghcr.io/regolith-linux/ci-ubuntu:noble-amd64" 133 | if: ${{ !failure() && !cancelled() }} 134 | steps: 135 | - name: Update Manifests 136 | uses: regolith-linux/actions/update-manifest@main 137 | env: 138 | GITHUB_TOKEN: ${{ secrets.ORG_BROADCAST_TOKEN2 }} 139 | with: 140 | name: "${{ github.event.repository.name }}" 141 | repo: "${{ github.server_url }}/${{ github.repository }}.git" 142 | ref: "${{ github.ref_name }}" 143 | sha: "${{ github.sha }}" 144 | matrix: "${{ needs.matrix-builder.outputs.includes }}" 145 | suite: "testing" 146 | component: "main" 147 | -------------------------------------------------------------------------------- /.github/workflows/publish-unstable.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Unstable 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | matrix-builder: 15 | runs-on: ubuntu-24.04 16 | outputs: 17 | includes: ${{ steps.builder.outputs.includes }} 18 | runners: ${{ steps.builder.outputs.runners }} 19 | steps: 20 | - name: Build Matrix 21 | id: builder 22 | uses: regolith-linux/actions/build-matrix@main 23 | with: 24 | name: "${{ github.event.repository.name }}" 25 | ref: "${{ github.ref_name }}" 26 | arch: "amd64 arm64" 27 | stage: "unstable" 28 | 29 | build: 30 | runs-on: ${{ fromJSON(needs.matrix-builder.outputs.runners)[matrix.arch] }} 31 | needs: matrix-builder 32 | container: "ghcr.io/regolith-linux/ci-${{ matrix.distro }}:${{ matrix.codename }}-${{ matrix.arch }}" 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | include: ${{ fromJSON(needs.matrix-builder.outputs.includes) }} 37 | env: 38 | server-address: "${{ secrets.KAMATERA_HOSTNAME2 }}" 39 | server-username: "${{ secrets.KAMATERA_USERNAME }}" 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | with: 44 | submodules: "recursive" 45 | 46 | - name: Import GPG Key 47 | uses: regolith-linux/actions/import-gpg@main 48 | with: 49 | gpg-key: "${{ secrets.PACKAGE_PRIVATE_KEY2 }}" 50 | 51 | - name: Build Package 52 | id: build 53 | uses: regolith-linux/actions/build-package@main 54 | with: 55 | name: "${{ github.event.repository.name }}" 56 | distro: "${{ matrix.distro }}" 57 | codename: "${{ matrix.codename }}" 58 | stage: "unstable" 59 | suite: "unstable" 60 | component: "main" 61 | arch: "${{ matrix.arch }}" 62 | 63 | - name: Setup SSH 64 | uses: regolith-linux/actions/setup-ssh@main 65 | with: 66 | ssh-host: "${{ env.server-address }}" 67 | ssh-key: "${{ secrets.KAMATERA_SSH_KEY }}" 68 | 69 | - name: Upload Package 70 | uses: regolith-linux/actions/upload-files@main 71 | with: 72 | upload-to-folder: "${{ github.event.repository.name }}" 73 | 74 | - name: Upload SourceLog 75 | uses: regolith-linux/actions/upload-files@main 76 | with: 77 | upload-from: "${{ steps.build.outputs.buildlog-path }}" 78 | upload-pattern: "SOURCELOG_*.txt" 79 | upload-to-base: "/opt/archives/workspace/" 80 | upload-to-folder: "${{ github.event.repository.name }}" 81 | 82 | sources: 83 | runs-on: ubuntu-24.04 84 | needs: build 85 | container: "ghcr.io/regolith-linux/ci-ubuntu:noble-amd64" 86 | if: ${{ !failure() && !cancelled() }} 87 | env: 88 | server-address: "${{ secrets.KAMATERA_HOSTNAME2 }}" 89 | server-username: "${{ secrets.KAMATERA_USERNAME }}" 90 | steps: 91 | - name: Import GPG Key 92 | uses: regolith-linux/actions/import-gpg@main 93 | with: 94 | gpg-key: "${{ secrets.PACKAGE_PRIVATE_KEY2 }}" 95 | 96 | - name: Setup SSH 97 | uses: regolith-linux/actions/setup-ssh@main 98 | with: 99 | ssh-host: "${{ env.server-address }}" 100 | ssh-key: "${{ secrets.KAMATERA_SSH_KEY }}" 101 | 102 | - name: Rebuild Sources 103 | uses: regolith-linux/actions/rebuild-sources@main 104 | with: 105 | workspace-subfolder: "${{ github.event.repository.name }}" 106 | only-component: "unstable" 107 | only-package: "${{ github.event.repository.name }}" 108 | 109 | publish: 110 | runs-on: ubuntu-24.04 111 | needs: sources 112 | container: "ghcr.io/regolith-linux/ci-ubuntu:noble-amd64" 113 | if: ${{ !failure() && !cancelled() }} 114 | env: 115 | server-address: "${{ secrets.KAMATERA_HOSTNAME2 }}" 116 | server-username: "${{ secrets.KAMATERA_USERNAME }}" 117 | steps: 118 | - name: Setup SSH 119 | uses: regolith-linux/actions/setup-ssh@main 120 | with: 121 | ssh-host: "${{ env.server-address }}" 122 | ssh-key: "${{ secrets.KAMATERA_SSH_KEY }}" 123 | 124 | - name: Publish Repo 125 | uses: regolith-linux/actions/publish-repo@main 126 | with: 127 | packages-path-subfolder: "${{ github.event.repository.name }}" 128 | only-component: "unstable" 129 | 130 | manifests: 131 | runs-on: ubuntu-24.04 132 | needs: [matrix-builder, publish] 133 | container: "ghcr.io/regolith-linux/ci-ubuntu:noble-amd64" 134 | if: ${{ !failure() && !cancelled() }} 135 | steps: 136 | - name: Update Manifests 137 | uses: regolith-linux/actions/update-manifest@main 138 | env: 139 | GITHUB_TOKEN: ${{ secrets.ORG_BROADCAST_TOKEN2 }} 140 | with: 141 | name: "${{ github.event.repository.name }}" 142 | repo: "${{ github.server_url }}/${{ github.repository }}.git" 143 | ref: "${{ github.ref_name }}" 144 | sha: "${{ github.sha }}" 145 | matrix: "${{ needs.matrix-builder.outputs.includes }}" 146 | suite: "unstable" 147 | component: "main" 148 | -------------------------------------------------------------------------------- /.github/workflows/test-pr.yml: -------------------------------------------------------------------------------- 1 | name: Test Pull Request 2 | 3 | on: 4 | pull_request: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | matrix-builder: 12 | runs-on: ubuntu-24.04 13 | outputs: 14 | includes: ${{ steps.builder.outputs.includes }} 15 | runners: ${{ steps.builder.outputs.runners }} 16 | steps: 17 | - name: Build Matrix 18 | id: builder 19 | uses: regolith-linux/actions/build-matrix@main 20 | with: 21 | name: "${{ github.event.repository.name }}" 22 | ref: "${{ github.base_ref }}" # build for target branch of the pull request 23 | arch: "amd64" # only test on amd64 on pull requests 24 | stage: "unstable" 25 | 26 | build: 27 | runs-on: ${{ fromJSON(needs.matrix-builder.outputs.runners)[matrix.arch] }} 28 | needs: matrix-builder 29 | container: "ghcr.io/regolith-linux/ci-${{ matrix.distro }}:${{ matrix.codename }}-${{ matrix.arch }}" 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | include: ${{ fromJSON(needs.matrix-builder.outputs.includes) }} 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | with: 38 | submodules: "recursive" 39 | 40 | - name: Build Package 41 | uses: regolith-linux/actions/build-package@main 42 | with: 43 | only-build: "true" 44 | name: "${{ github.event.repository.name }}" 45 | distro: "${{ matrix.distro }}" 46 | codename: "${{ matrix.codename }}" 47 | stage: "unstable" 48 | suite: "unstable" 49 | component: "main" 50 | arch: "${{ matrix.arch }}" 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | cmake-build-debug 34 | build 35 | .idea 36 | files 37 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/i3ipc++"] 2 | path = lib/i3ipc++ 3 | url = https://github.com/drmgc/i3ipcpp.git 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(i3_snapshot) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | add_subdirectory(lib/i3ipc++) 6 | 7 | include_directories(${I3IPCpp_INCLUDE_DIRS} lib/base64) 8 | link_directories(${I3IPCpp_LIBRARY_DIRS}) 9 | 10 | add_executable(i3-snapshot src/main.cpp lib/base64/base64.cpp) 11 | 12 | target_link_libraries(i3-snapshot ${I3IPCpp_LIBRARIES}) 13 | 14 | install(TARGETS i3-snapshot 15 | RUNTIME DESTINATION bin 16 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Regolith Linux 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # i3-snapshot 2 | Save and restore window and workspace layout within an i3wm instance. 3 | 4 | ## Usage 5 | 6 | i3-snapshot has two modes: 7 | 8 | * record: read i3 current state and emit to stdout 9 | `i3-snapshot > snapshot.txt` 10 | * restore: modify the i3 window state from a previous configuration. 11 | `i3-snapshot < snapshot.txt` 12 | 13 | ## Notes 14 | 15 | This program is intended to be used when window layouts are mangled by display hotplug events or other changes that cause i3wm to restructure the containment layout. 16 | 17 | i3-snapshot employs a 'best effort' and 'fail-fast' strategy. This means that it does little validation and aborts execution upon any failure. 18 | 19 | The output is meant to be somewhat human readable for basic troubleshooting purposes. 20 | 21 | i3-snapshot is not an alternative to i3-save-tree. i3-save-tree is for long-lived workspace structures that are to be populated by users interactively. i3-snapshot only works within a single i3wm instance because it uses the internal ids to reference specific windows. This means that a snapshot cannot be used after the i3wm session it was recorded in exits. 22 | 23 | ## Example 24 | 25 | To save your current window and workspace layout to a file: 26 | ``` 27 | $ i3-snapshot > layout.txt 28 | ``` 29 | 30 | To restore a layout from a file: 31 | ``` 32 | $ i3-snapshot < layout.txt 33 | ``` 34 | 35 | ## Adding to an i3 config 36 | 37 | Bind i3-snapshot to keys such that layouts can be saved and restored like this: 38 | 39 | ``` 40 | bindsym $mod+comma exec /usr/local/bin/i3-snapshot -o > /tmp/i3-snapshot.txt 41 | bindsym $mod+period exec /usr/local/bin/i3-snapshot -c < /tmp/i3-snapshot.txt 42 | ``` 43 | 44 | ## Install 45 | 46 | A Debian package `i3-snapshot` for Ubuntu is available at `ppa:kgilmer/speed-ricer` for Bionic, Disco, and Eoan releases. 47 | 48 | ## How to build 49 | 50 | Direct dependencies are integrated via git submodules. Additionally, the i3/ipc.h header file from the i3 package, `libjsoncpp-dev`, and `libsigc++-2.0-dev` are required. 51 | 52 | ``` 53 | $ git clone https://github.com/regolith-linux/i3-snapshot.git 54 | ...`` 55 | $ cd i3-snapshot 56 | ... 57 | $ git submodule init 58 | $ git submodule update 59 | $ mkdir build && cd build 60 | $ cmake .. 61 | $ make 62 | ... 63 | $ ./i3-snapshot 64 | ``` 65 | 66 | ### and install 67 | 68 | ``` 69 | sudo cp i3-snapshot /usr/local/bin/ 70 | ``` 71 | 72 | ## Expectation of quality 73 | 74 | i3-snapshot should be considered alpha quality. -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | i3-snapshot (1.1-1ubuntu1~ppa1) bionic; urgency=medium 2 | 3 | [ Ken Gilmer ] 4 | * Update readme w/ new install options, build details from https://github.com/regolith-linux/i3-snapshot/issues/1. 5 | 6 | [ Khosrow Moossavi ] 7 | * feat: enable github action to test pull requests 8 | * feat: enable github action to publish to unstable 9 | * feat: enable github action to publish to testing 10 | * feat: enable github action to prepare release 11 | * fix: create release tag from correct git sha 12 | 13 | -- Regolith Linux Tue, 04 Feb 2025 02:29:57 +0000 14 | 15 | i3-snapshot (1.0-1ubuntu1~ppa1) bionic; urgency=medium 16 | 17 | * Handle workspace names with quotes. 18 | * Add option to "dryrun" update i3 for debugging. 19 | 20 | -- Ken Gilmer Sat, 14 Dec 2019 19:00:11 -0800 21 | 22 | i3-snapshot (0.1-1ubuntu1ppa4) bionic; urgency=medium 23 | 24 | * Initial release 25 | 26 | -- Ken Gilmer Sun, 30 Jun 2019 18:56:33 -0700 27 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: i3-snapshot 2 | Section: x11 3 | Priority: extra 4 | Maintainer: Ken Gilmer 5 | Build-Depends: cmake, cmake-data, debhelper (>=9), pkg-config, libsigc++-2.0-dev, libjsoncpp-dev, i3-wm, zlib1g-dev 6 | Standards-Version: 3.9.8 7 | Homepage: https://github.com/regolith-linux/i3-snapshot 8 | 9 | Package: i3-snapshot 10 | Architecture: any 11 | Multi-Arch: foreign 12 | Depends: ${misc:Depends}, ${shlibs:Depends}, i3-wm 13 | Description: Record and restore window and workspace containment structure in i3-wm -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: i3-snapshot 3 | Source: https://github.com/regolith-linux/i3-snapshot 4 | 5 | Files: lib/base64/base64.cpp 6 | lib/base64/base64.h 7 | Copyright: 2004-2017 René Nyffenegger 8 | License: BSD 9 | This source code is provided 'as-is', without any express or implied 10 | warranty. In no event will the author be held liable for any damages 11 | arising from the use of this software. 12 | . 13 | Permission is granted to anyone to use this software for any purpose, 14 | including commercial applications, and to alter it and redistribute it 15 | freely, subject to the following restrictions: 16 | . 17 | 1. The origin of this source code must not be misrepresented; you must not 18 | claim that you wrote the original source code. If you use this source code 19 | in a product, an acknowledgment in the product documentation would be 20 | appreciated but is not required. 21 | . 22 | 2. Altered source versions must be plainly marked as such, and must not be 23 | misrepresented as being the original source code. 24 | . 25 | 3. This notice may not be removed or altered from any source distribution. 26 | . 27 | René Nyffenegger rene.nyffenegger@adp-gmbh.ch 28 | 29 | Files: debian/* 30 | Copyright: 2019 Ken Gilmer 31 | License: GPL-3.0+ 32 | 33 | Files: src/main.cpp 34 | CMakeLists.txt 35 | LICENSE 36 | README.md 37 | Copyright: 2019 Ken Gilmer 38 | License: BSD-4-Clause 39 | Redistribution and use in source and binary forms, with or without 40 | modification, are permitted provided that the following conditions are met: 41 | 1. Redistributions of source code must retain the above copyright 42 | notice, this list of conditions and the following disclaimer. 43 | 2. Redistributions in binary form must reproduce the above copyright 44 | notice, this list of conditions and the following disclaimer in the 45 | documentation and/or other materials provided with the distribution. 46 | 3. All advertising materials mentioning features or use of this software 47 | must display the following acknowledgement: 48 | This product includes software developed by Ken Gilmer. 49 | 4. Neither the name of Ken Gilmer nor the 50 | names of its contributors may be used to endorse or promote products 51 | derived from this software without specific prior written permission. 52 | . 53 | THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY 54 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 56 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 57 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 58 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 59 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 60 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 61 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 62 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 63 | #---------------------------------------------------------------------------- 64 | # License file: LICENSE 65 | BSD 3-Clause License 66 | . 67 | Copyright (c) 2019, Regolith Linux 68 | All rights reserved. 69 | . 70 | Redistribution and use in source and binary forms, with or without 71 | modification, are permitted provided that the following conditions are met: 72 | . 73 | 1. Redistributions of source code must retain the above copyright notice, this 74 | list of conditions and the following disclaimer. 75 | . 76 | 2. Redistributions in binary form must reproduce the above copyright notice, 77 | this list of conditions and the following disclaimer in the documentation 78 | and/or other materials provided with the distribution. 79 | . 80 | 3. Neither the name of the copyright holder nor the names of its 81 | contributors may be used to endorse or promote products derived from 82 | this software without specific prior written permission. 83 | . 84 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 85 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 86 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 87 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 88 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 89 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 90 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 91 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 92 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 93 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 94 | -------------------------------------------------------------------------------- /debian/patches/series: -------------------------------------------------------------------------------- 1 | # You must remove unused comment lines for the released package. 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | export DH_VERBOSE = 1 3 | export DEB_BUILD_MAINT_OPTIONS = hardening=+all 4 | export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic 5 | export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed 6 | 7 | %: 8 | dh $@ 9 | 10 | override_dh_auto_configure: 11 | dh_auto_configure -- \ 12 | -DCMAKE_LIBRARY_ARCHITECTURE="$(DEB_TARGET_MULTIARCH)" 13 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /lib/.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "i3ipc++"] 2 | path = i3ipc++ 3 | url = https://github.com/drmgc/i3ipcpp.git 4 | -------------------------------------------------------------------------------- /lib/base64/base64.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | base64.cpp and base64.h 3 | 4 | base64 encoding and decoding with C++. 5 | 6 | Version: 1.01.00 7 | 8 | Copyright (C) 2004-2017 René Nyffenegger 9 | 10 | This source code is provided 'as-is', without any express or implied 11 | warranty. In no event will the author be held liable for any damages 12 | arising from the use of this software. 13 | 14 | Permission is granted to anyone to use this software for any purpose, 15 | including commercial applications, and to alter it and redistribute it 16 | freely, subject to the following restrictions: 17 | 18 | 1. The origin of this source code must not be misrepresented; you must not 19 | claim that you wrote the original source code. If you use this source code 20 | in a product, an acknowledgment in the product documentation would be 21 | appreciated but is not required. 22 | 23 | 2. Altered source versions must be plainly marked as such, and must not be 24 | misrepresented as being the original source code. 25 | 26 | 3. This notice may not be removed or altered from any source distribution. 27 | 28 | René Nyffenegger rene.nyffenegger@adp-gmbh.ch 29 | 30 | */ 31 | 32 | #include "base64.h" 33 | #include 34 | 35 | static const std::string base64_chars = 36 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 37 | "abcdefghijklmnopqrstuvwxyz" 38 | "0123456789+/"; 39 | 40 | 41 | static inline bool is_base64(unsigned char c) { 42 | return (isalnum(c) || (c == '+') || (c == '/')); 43 | } 44 | 45 | std::string base64_encode(unsigned char const* bytes_to_encode, unsigned int in_len) { 46 | std::string ret; 47 | int i = 0; 48 | int j = 0; 49 | unsigned char char_array_3[3]; 50 | unsigned char char_array_4[4]; 51 | 52 | while (in_len--) { 53 | char_array_3[i++] = *(bytes_to_encode++); 54 | if (i == 3) { 55 | char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; 56 | char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); 57 | char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); 58 | char_array_4[3] = char_array_3[2] & 0x3f; 59 | 60 | for(i = 0; (i <4) ; i++) 61 | ret += base64_chars[char_array_4[i]]; 62 | i = 0; 63 | } 64 | } 65 | 66 | if (i) 67 | { 68 | for(j = i; j < 3; j++) 69 | char_array_3[j] = '\0'; 70 | 71 | char_array_4[0] = ( char_array_3[0] & 0xfc) >> 2; 72 | char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); 73 | char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); 74 | 75 | for (j = 0; (j < i + 1); j++) 76 | ret += base64_chars[char_array_4[j]]; 77 | 78 | while((i++ < 3)) 79 | ret += '='; 80 | 81 | } 82 | 83 | return ret; 84 | 85 | } 86 | 87 | std::string base64_decode(std::string const& encoded_string) { 88 | int in_len = encoded_string.size(); 89 | int i = 0; 90 | int j = 0; 91 | int in_ = 0; 92 | unsigned char char_array_4[4], char_array_3[3]; 93 | std::string ret; 94 | 95 | while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { 96 | char_array_4[i++] = encoded_string[in_]; in_++; 97 | if (i ==4) { 98 | for (i = 0; i <4; i++) 99 | char_array_4[i] = base64_chars.find(char_array_4[i]); 100 | 101 | char_array_3[0] = ( char_array_4[0] << 2 ) + ((char_array_4[1] & 0x30) >> 4); 102 | char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); 103 | char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; 104 | 105 | for (i = 0; (i < 3); i++) 106 | ret += char_array_3[i]; 107 | i = 0; 108 | } 109 | } 110 | 111 | if (i) { 112 | for (j = 0; j < i; j++) 113 | char_array_4[j] = base64_chars.find(char_array_4[j]); 114 | 115 | char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); 116 | char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); 117 | 118 | for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; 119 | } 120 | 121 | return ret; 122 | } 123 | -------------------------------------------------------------------------------- /lib/base64/base64.h: -------------------------------------------------------------------------------- 1 | // 2 | // base64 encoding and decoding with C++. 3 | // Version: 1.01.00 4 | // 5 | 6 | #ifndef BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A 7 | #define BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A 8 | 9 | #include 10 | 11 | std::string base64_encode(unsigned char const* , unsigned int len); 12 | std::string base64_decode(std::string const& s); 13 | 14 | #endif /* BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A */ -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2019, Ken Gilmer 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. All advertising materials mentioning features or use of this software 13 | must display the following acknowledgement: 14 | This product includes software developed by Ken Gilmer. 15 | 4. Neither the name of Ken Gilmer nor the 16 | names of its contributors may be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY 20 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 23 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #include "base64.h" 38 | 39 | using namespace std; 40 | 41 | /** 42 | * Keep track of output and workspace as the i3 container tree is traversed depth-first. 43 | */ 44 | struct TreeState { 45 | string outputName; 46 | string workspaceName; 47 | size_t workspaceId{}; 48 | }; 49 | 50 | enum WindowIdentifier { 51 | I3_ID, WINDOW_TITLE 52 | }; 53 | 54 | struct CommandLineOptions { 55 | bool debug; 56 | bool failFast; 57 | bool forceOutputMode; 58 | bool encodeStrings; 59 | bool dryRun; 60 | WindowIdentifier windowIdentifier; 61 | }; 62 | 63 | /** 64 | * Determine if the i3 container is a window type. 65 | * @param c i3 container 66 | * @return true if container is window, false otherwise. 67 | */ 68 | bool isWindow(const i3ipc::container_t &c) { 69 | return c.type == "con" && c.xwindow_id != 0; 70 | } 71 | 72 | /** 73 | * Determine if container should not be ignored. 74 | * @param c i3 container 75 | * @return true if container is valid, false otherwise. 76 | */ 77 | bool isValidParent(const i3ipc::container_t &c) { 78 | return c.type != "dockarea"; 79 | } 80 | 81 | /** 82 | * Traverse i3 containers and emit relevant info to stdout. 83 | * 84 | * @param c i3 container 85 | * @param treeState storage of current state of tree traversal. 86 | */ 87 | void findWindows(const i3ipc::container_t &c, TreeState &treeState, CommandLineOptions &options) { 88 | if (c.type == "output") { 89 | treeState.outputName = c.name; 90 | } else if (c.type == "workspace") { 91 | treeState.workspaceName = c.name; 92 | treeState.workspaceId = c.id; 93 | } else if (isWindow(c)) { 94 | if (treeState.outputName.empty() || treeState.workspaceName.empty()) { 95 | cout << "Invalid tree state, aborting." << endl; 96 | exit(1); 97 | } 98 | 99 | string outputEncoded; 100 | string workspaceEncoded; 101 | string windowEncoded; 102 | 103 | if (options.encodeStrings) { 104 | outputEncoded = base64_encode(reinterpret_cast(treeState.outputName.c_str()), 105 | treeState.outputName.length()); 106 | workspaceEncoded = base64_encode( 107 | reinterpret_cast(treeState.workspaceName.c_str()), 108 | treeState.workspaceName.length()); 109 | windowEncoded = base64_encode( 110 | reinterpret_cast(c.name.c_str()), 111 | c.name.length()); 112 | } else { 113 | outputEncoded = treeState.outputName; 114 | workspaceEncoded = treeState.workspaceName; 115 | windowEncoded = c.name; 116 | } 117 | 118 | // Output Name, Workspace Name, Workspace Id, Window Id, Window Name 119 | cout << outputEncoded << " " << workspaceEncoded << " " << treeState.workspaceId << " " << c.id << " " 120 | << windowEncoded << endl; 121 | } 122 | 123 | if (isValidParent(c)) 124 | for (auto &node : c.nodes) 125 | findWindows(*node, treeState, options); 126 | } 127 | 128 | /** 129 | * Move a workspace to an out and a window to a workspace. 130 | * @param i3conn i3 connection 131 | * @param windowId i3 window id 132 | * @param outputName system name for output (monitor) 133 | * @param workspaceName i3 name for workspace 134 | * @param workspaceId i3 id for workspace 135 | * @param windowTitle window title 136 | * @return true if operation success, false otherwise. 137 | */ 138 | bool 139 | moveWindow(const i3ipc::connection &i3conn, size_t windowId, const string &outputName, const string &workspaceName, 140 | size_t workspaceId, const string &windowTitle, CommandLineOptions &opts) { 141 | // Move workspace to output 142 | // i3-msg [workspace=" 2 "] move workspace to output "eDP-1" 143 | string wsCmd; 144 | if (opts.windowIdentifier == I3_ID) { 145 | wsCmd = "[con_id=" + to_string(workspaceId) + "] move workspace to output " + outputName; 146 | } else { 147 | wsCmd = "[workspace=\"" + workspaceName + "\"] move workspace to output " + outputName; 148 | } 149 | 150 | if (opts.debug) cout << "i3-msg " << wsCmd << endl; 151 | 152 | if (!opts.dryRun && !i3conn.send_command(wsCmd)) return false; 153 | 154 | // Move window to workspace 155 | string windowCmd; 156 | // https://build.i3wm.org/docs/userguide.html#command_criteria 157 | if (opts.windowIdentifier == I3_ID) { 158 | windowCmd = "[con_id=" + to_string(windowId) + "] move container to workspace " + workspaceName; 159 | } else { 160 | windowCmd = "[title=\"" + windowTitle + "\"] move container to workspace " + workspaceName; 161 | } 162 | 163 | if (opts.debug) cout << "i3-msg " << windowCmd << endl; 164 | 165 | if (opts.dryRun) return true; 166 | 167 | return i3conn.send_command(windowCmd); 168 | } 169 | 170 | /** 171 | * Determine if input is being passed to program from a pipe. 172 | * This determines the mode of the program (read/write). 173 | * @return true if input is from terminal (meaning in write mode). 174 | */ 175 | bool inputFromTerminal() { 176 | return !isatty(fileno(stdin)); 177 | } 178 | 179 | void printHelp() { 180 | cout 181 | << "Save and restore window containment in i3-wm.\n" 182 | << "Usage: i3-snapshot [-d | --debug] [-v | --verbose] [-c | --continue] [-r | --rawstrings] [-t | --title] [-o | --output] [-y | --dryrun]\n" 183 | << "-d: debug -v: version -c: ignore error -r: raw strings -t: match window title -o: force output mode -y: dryrun\n" 184 | << "Generate a snapshot: i3-snapshot > snapshot.txt\n" 185 | << "Replay a snapshot: i3-snapshot < snapshot.txt" 186 | << endl; 187 | } 188 | 189 | void printVersion() { 190 | cout << "Version 0.1" << endl; 191 | } 192 | 193 | /** 194 | * Parse command-line options. 195 | * @param argc 196 | * @param argv 197 | * @return true for debug mode 198 | */ 199 | CommandLineOptions parseOptions(int argc, char **argv) { 200 | CommandLineOptions options{}; 201 | 202 | options.debug = false; 203 | options.failFast = true; 204 | options.forceOutputMode = false; 205 | options.encodeStrings = true; 206 | options.dryRun = false; 207 | options.windowIdentifier = I3_ID; 208 | 209 | for (int i = 1; i < argc; i++) { 210 | if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { 211 | printHelp(); 212 | exit(0); 213 | } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) { 214 | printVersion(); 215 | exit(0); 216 | } else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--continue") == 0) { 217 | options.failFast = false; 218 | } else if (strcmp(argv[i], "-d") == 0 || strcmp(argv[i], "--debug") == 0) { 219 | options.debug = true; 220 | } else if (strcmp(argv[i], "-r") == 0 || strcmp(argv[i], "--rawstrings") == 0) { 221 | options.encodeStrings = false; 222 | } else if (strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--title") == 0) { 223 | options.windowIdentifier = WINDOW_TITLE; 224 | } else if (strcmp(argv[i], "-o") == 0 || strcmp(argv[i], "--output") == 0) { 225 | options.forceOutputMode = true; 226 | } else if (strcmp(argv[i], "-y") == 0 || strcmp(argv[i], "--dryrun") == 0) { 227 | options.dryRun= true; 228 | options.debug = true; 229 | } else { 230 | cout << "Unrecognized command line option: '" << argv[i] << "'. Aborting." << endl; 231 | exit(1); 232 | } 233 | } 234 | 235 | return options; 236 | } 237 | 238 | int main(int argc, char **argv) { 239 | CommandLineOptions opts = parseOptions(argc, argv); 240 | 241 | i3ipc::connection i3connection; 242 | TreeState treeState; 243 | 244 | if (opts.forceOutputMode || !inputFromTerminal()) { 245 | findWindows(*i3connection.get_tree(), treeState, opts); 246 | } else { 247 | string outputNameEnc, workspaceNameEnc, workspaceIdStr, windowIdStr, windowNameEnc; 248 | 249 | while (!cin.eof()) { 250 | cin >> outputNameEnc >> workspaceNameEnc >> workspaceIdStr >> windowIdStr >> windowNameEnc; 251 | 252 | string outputName = base64_decode(outputNameEnc); 253 | string workspaceName = base64_decode(workspaceNameEnc); 254 | size_t workspaceId = stoul(workspaceIdStr); 255 | string windowName = base64_decode(windowNameEnc); 256 | size_t windowId = stoul(windowIdStr); 257 | 258 | std::stringstream escapedWorkspaceName; 259 | escapedWorkspaceName << std::quoted(workspaceName); 260 | 261 | if (!moveWindow(i3connection, windowId, outputName, escapedWorkspaceName.str(), workspaceId, windowName, opts)) { 262 | cerr << "Failed to move " << windowId << " (" << windowName << ")." << endl; 263 | 264 | if (opts.failFast) return 1; 265 | } 266 | } 267 | } 268 | 269 | return 0; 270 | } 271 | --------------------------------------------------------------------------------