├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ ├── config.yml │ └── feature_request.md └── workflows │ ├── ci.yml │ └── ubuntu.dockerfile ├── .gitignore ├── ARCHITECTURE.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── flake.lock ├── flake.nix ├── macros ├── Cargo.toml └── src │ └── lib.rs ├── resources └── xwayland-satellite.service ├── src ├── clientside │ ├── data_device.rs │ ├── mod.rs │ └── xdg_activation.rs ├── lib.rs ├── main.rs ├── server │ ├── dispatch.rs │ ├── event.rs │ ├── mod.rs │ └── tests.rs └── xstate │ ├── mod.rs │ ├── selection.rs │ └── settings.rs ├── tests └── integration.rs ├── testwl ├── Cargo.lock ├── Cargo.toml └── src │ └── lib.rs └── wl_drm ├── Cargo.toml └── src ├── drm.xml └── lib.rs /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ["bug"] 4 | body: 5 | - type: input 6 | id: os 7 | attributes: 8 | label: Operating System 9 | placeholder: Arch Linux, Ubuntu 24.04, Gentoo, etc 10 | validations: 11 | required: true 12 | - type: input 13 | id: comp 14 | attributes: 15 | label: Wayland Compositor 16 | placeholder: Niri, Hyprland, Sway, etc 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: problem 21 | attributes: 22 | label: Describe the issue. 23 | value: | 24 | **Please attach a log if applicable.** 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: steps 29 | attributes: 30 | label: Steps to reproduce 31 | - type: checkboxes 32 | attributes: 33 | label: Please confirm you have reproduced this on the latest commit. 34 | options: 35 | - label: I have reproduced this on the latest commit 36 | required: true 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Q & A 3 | url: https://github.com/Supreeeme/xwayland-satellite/discussions 4 | about: Need help troubleshooting, or have some other question? Ask here 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | REGISTRY: ghcr.io 12 | IMAGE_NAME: ${{ github.repository }} 13 | UBUNTU_DOCKERFILE: .github/workflows/ubuntu.dockerfile 14 | 15 | permissions: 16 | contents: read 17 | packages: read 18 | 19 | jobs: 20 | check-should-build-container: 21 | runs-on: ubuntu-latest 22 | permissions: 23 | pull-requests: read 24 | contents: read 25 | steps: 26 | - if: ${{ github.event_name == 'push' }} 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 2 30 | sparse-checkout: .github 31 | - id: changed 32 | uses: tj-actions/changed-files@v45 33 | with: 34 | files: ${{ env.UBUNTU_DOCKERFILE }} 35 | - id: name 36 | run: echo "container_path=${{ env.REGISTRY }}/${GITHUB_REPOSITORY@L}" >> "$GITHUB_OUTPUT" 37 | outputs: 38 | should-build: ${{ steps.changed.outputs.any_changed }} 39 | container-path: ${{ steps.name.outputs.container_path }} 40 | 41 | ubuntu-build: 42 | runs-on: ubuntu-latest 43 | needs: check-should-build-container 44 | if: ${{ needs.check-should-build-container.outputs.should-build == 'true' }} 45 | permissions: 46 | contents: read 47 | packages: write 48 | id-token: write 49 | 50 | steps: 51 | - uses: docker/setup-buildx-action@v3 52 | - uses: docker/login-action@v3 53 | with: 54 | registry: ${{ env.REGISTRY }} 55 | username: ${{ github.actor }} 56 | password: ${{ secrets.GITHUB_TOKEN }} 57 | - uses: docker/build-push-action@v6 58 | with: 59 | file: ${{ env.UBUNTU_DOCKERFILE }} 60 | push: true 61 | tags: ${{ needs.check-should-build-container.outputs.container-path }}-ubuntu:latest 62 | labels: org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} 63 | cache-from: type=gha 64 | cache-to: type=gha,mode=max 65 | 66 | build: 67 | needs: [ubuntu-build, check-should-build-container] 68 | if: ${{ always() && (needs.ubuntu-build.result == 'success' || needs.ubuntu-build.result == 'skipped') }} 69 | runs-on: ubuntu-latest 70 | container: 71 | image: ${{ needs.check-should-build-container.outputs.container-path }}-ubuntu:latest 72 | credentials: 73 | username: ${{ github.actor }} 74 | password: ${{ secrets.GITHUB_TOKEN }} 75 | steps: 76 | - uses: actions/checkout@v4 77 | - name: Build 78 | run: cargo build --release --verbose 79 | - name: Run tests 80 | run: cargo test --verbose 81 | - name: Format check 82 | run: cargo fmt --check 83 | - name: Clippy 84 | run: cargo clippy 85 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | ENV TZ=Etc/UTC 5 | ENV CARGO_HOME=/cargo RUSTUP_HOME=/rustup 6 | RUN apt-get update \ 7 | && apt-get install -y xwayland libxcb1 clang libxcb-cursor0 libxcb-cursor-dev curl pkg-config libegl1 \ 8 | && rm -r /var/lib/apt/lists/* 9 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ 10 | && . "/cargo/env" \ 11 | && rustup toolchain install stable \ 12 | && rustup default stable 13 | RUN mkdir /run/xwls-test 14 | ENV PATH="/cargo/bin:$PATH" XDG_RUNTIME_DIR="/run/xwls-test" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | This is a high level overview of the architecture of xwayland-satellite. It has a 4 | few different roles that can be confusing to understand if you aren't familiar with 5 | how Wayland and X11 function. 6 | 7 | ## Overview 8 | 9 | ```mermaid 10 | flowchart TD 11 | host["Host compositor 12 | (niri, sway, etc)"] 13 | sat[xwayland-satellite] 14 | Xwayland 15 | client[X client] 16 | 17 | host --> sat 18 | sat -->|Wayland server| Xwayland 19 | Xwayland -->|X11 window manager| sat 20 | sat -->|X11 window manager| client 21 | Xwayland --> client 22 | ``` 23 | 24 | xwayland-satellite grants rootless Xwayland integration to any standard Wayland compositor through standard 25 | Wayland protocols. As such, this means satellite has to function as three different things: 26 | 27 | 1. An X11 window manager 28 | 2. A Wayland client 29 | 3. A Wayland server/compositor 30 | 31 | ### X11 window manager 32 | 33 | The code for the X11 portion of satellite lives in `src/xstate`. Satellite must function largely the same way 34 | as any other standard X11 window manager. This includes: 35 | 36 | - Setting SubstructureRedirect and SubstructureNotify on the root window, to get notifications for when new windows are being created 37 | - Follwing (most of) the [ICCCM](https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html) and [EWMH](https://specifications.freedesktop.org/wm-spec/latest/) specs 38 | 39 | In addition, satellite must do some other things that a normal X11 window manager wouldn't - but a compositor integrating 40 | Xwayland would - such as synchronize X11 and Wayland selections. This is explained further in the Wayland server section. 41 | 42 | The way that satellite manages windows from the X11 point of view is as follows: 43 | 44 | - All toplevels on a monitor are positioned at 0x0 on that monitor. So if you have one monitor at 0x0, 45 | all the windows are located at 0x0. If you have a monitor at 300x600, all the windows on that monitor are at 300x600. 46 | - This offset is needed because all monitors rest in the same coordinate plane in X11, so missing this offset would 47 | would lead to incorrect cursor behavior. 48 | - The current window that the mouse is hovering over is raised to the top of the stack. 49 | - Any window determined to be a popup (override redirect, EWMH properties, etc) has its position respected if there is 50 | an existing toplevel. If there is no existing toplevel, the window is treated as a toplevel. 51 | 52 | This approach seems to work well for most applications. The biggest issues will be applications that rely on creating windows 53 | at specific coordinates - for example Steam's notifications that slide in from the bottom of the screen. 54 | 55 | ### Wayland client 56 | 57 | Since satellite is intended to function on any Wayland compositor implementing the necessary protocols, 58 | it functions as a Wayland client. This is straightforward to think about. Client interfacing code lives in 59 | `src/clientside`, as well as being interspersed throughout `src/server`. 60 | 61 | ### Wayland server 62 | 63 | In order to interface with Xwayland, which itself is a Wayland client, satellite must function as a Wayland server. 64 | The code for this lives in `src/server`. Satellite will re-expose relevant Wayland interfaces from the host compositor 65 | (the compositor that satellite itself is a client to) back to Xwayland. 66 | 67 | A lot of the interfaces that Xwayland is interested in can be exposed directly from the host with no further changes, 68 | in which case satellite is just acting as an interface passthrough. However, some interfaces need to be manipulated 69 | or otherwise intercepted for proper functionality, such as: 70 | 71 | - `wl_surface` - Xwayland simply exposes all X11 windows as `wl_surface`s, but for standard desktop compositors to actually show something, 72 | these surfaces must have roles, such as `xdg_toplevel` and `xdg_popup` and friends 73 | - `xdg_output` - Xwayland will use `xdg_output`'s logical size for sizing the X11 screen, but this leads to the wrong size 74 | when the output is scaled, and blurry windows as you may have seen in other Xwayland integrations. 75 | - `wl_pointer` - For handling scaled mouse output, since we are changing the reported output/surface sizes. 76 | 77 | Note that there may be some interfaces that are only used from satellite's client side, such as `xdg_wm_base`. These 78 | are interfaces that Xwayland is not actually interested in, but satellite itself uses to provide some sort of functionality 79 | - satellite is a normal Wayland client after all. 80 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.8" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell_polyfill", 61 | "windows-sys", 62 | ] 63 | 64 | [[package]] 65 | name = "anyhow" 66 | version = "1.0.98" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 69 | 70 | [[package]] 71 | name = "bindgen" 72 | version = "0.69.5" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" 75 | dependencies = [ 76 | "bitflags 2.9.1", 77 | "cexpr", 78 | "clang-sys", 79 | "itertools", 80 | "lazy_static", 81 | "lazycell", 82 | "log", 83 | "prettyplease", 84 | "proc-macro2", 85 | "quote", 86 | "regex", 87 | "rustc-hash", 88 | "shlex", 89 | "syn", 90 | "which", 91 | ] 92 | 93 | [[package]] 94 | name = "bitflags" 95 | version = "1.3.2" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 98 | 99 | [[package]] 100 | name = "bitflags" 101 | version = "2.9.1" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 104 | 105 | [[package]] 106 | name = "cc" 107 | version = "1.2.24" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" 110 | dependencies = [ 111 | "shlex", 112 | ] 113 | 114 | [[package]] 115 | name = "cexpr" 116 | version = "0.6.0" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 119 | dependencies = [ 120 | "nom", 121 | ] 122 | 123 | [[package]] 124 | name = "cfg-if" 125 | version = "1.0.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 128 | 129 | [[package]] 130 | name = "clang-sys" 131 | version = "1.8.1" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 134 | dependencies = [ 135 | "glob", 136 | "libc", 137 | "libloading", 138 | ] 139 | 140 | [[package]] 141 | name = "colorchoice" 142 | version = "1.0.3" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 145 | 146 | [[package]] 147 | name = "cursor-icon" 148 | version = "1.2.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" 151 | 152 | [[package]] 153 | name = "darling" 154 | version = "0.20.11" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 157 | dependencies = [ 158 | "darling_core", 159 | "darling_macro", 160 | ] 161 | 162 | [[package]] 163 | name = "darling_core" 164 | version = "0.20.11" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 167 | dependencies = [ 168 | "fnv", 169 | "ident_case", 170 | "proc-macro2", 171 | "quote", 172 | "strsim", 173 | "syn", 174 | ] 175 | 176 | [[package]] 177 | name = "darling_macro" 178 | version = "0.20.11" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 181 | dependencies = [ 182 | "darling_core", 183 | "quote", 184 | "syn", 185 | ] 186 | 187 | [[package]] 188 | name = "deranged" 189 | version = "0.4.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 192 | dependencies = [ 193 | "powerfmt", 194 | ] 195 | 196 | [[package]] 197 | name = "derive_builder" 198 | version = "0.20.2" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" 201 | dependencies = [ 202 | "derive_builder_macro", 203 | ] 204 | 205 | [[package]] 206 | name = "derive_builder_core" 207 | version = "0.20.2" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 210 | dependencies = [ 211 | "darling", 212 | "proc-macro2", 213 | "quote", 214 | "syn", 215 | ] 216 | 217 | [[package]] 218 | name = "derive_builder_macro" 219 | version = "0.20.2" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 222 | dependencies = [ 223 | "derive_builder_core", 224 | "syn", 225 | ] 226 | 227 | [[package]] 228 | name = "downcast-rs" 229 | version = "1.2.1" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" 232 | 233 | [[package]] 234 | name = "either" 235 | version = "1.15.0" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 238 | 239 | [[package]] 240 | name = "env_filter" 241 | version = "0.1.3" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 244 | dependencies = [ 245 | "log", 246 | "regex", 247 | ] 248 | 249 | [[package]] 250 | name = "env_logger" 251 | version = "0.10.2" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 254 | dependencies = [ 255 | "humantime", 256 | "is-terminal", 257 | "log", 258 | "regex", 259 | "termcolor", 260 | ] 261 | 262 | [[package]] 263 | name = "env_logger" 264 | version = "0.11.8" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 267 | dependencies = [ 268 | "anstream", 269 | "anstyle", 270 | "env_filter", 271 | "jiff", 272 | "log", 273 | ] 274 | 275 | [[package]] 276 | name = "errno" 277 | version = "0.3.12" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 280 | dependencies = [ 281 | "libc", 282 | "windows-sys", 283 | ] 284 | 285 | [[package]] 286 | name = "fnv" 287 | version = "1.0.7" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 290 | 291 | [[package]] 292 | name = "glob" 293 | version = "0.3.2" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 296 | 297 | [[package]] 298 | name = "hermit-abi" 299 | version = "0.5.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" 302 | 303 | [[package]] 304 | name = "home" 305 | version = "0.5.11" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 308 | dependencies = [ 309 | "windows-sys", 310 | ] 311 | 312 | [[package]] 313 | name = "humantime" 314 | version = "2.2.0" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" 317 | 318 | [[package]] 319 | name = "ident_case" 320 | version = "1.0.1" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 323 | 324 | [[package]] 325 | name = "is-terminal" 326 | version = "0.4.16" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" 329 | dependencies = [ 330 | "hermit-abi", 331 | "libc", 332 | "windows-sys", 333 | ] 334 | 335 | [[package]] 336 | name = "is_terminal_polyfill" 337 | version = "1.70.1" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 340 | 341 | [[package]] 342 | name = "itertools" 343 | version = "0.12.1" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 346 | dependencies = [ 347 | "either", 348 | ] 349 | 350 | [[package]] 351 | name = "itoa" 352 | version = "1.0.15" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 355 | 356 | [[package]] 357 | name = "jiff" 358 | version = "0.2.14" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" 361 | dependencies = [ 362 | "jiff-static", 363 | "log", 364 | "portable-atomic", 365 | "portable-atomic-util", 366 | "serde", 367 | ] 368 | 369 | [[package]] 370 | name = "jiff-static" 371 | version = "0.2.14" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" 374 | dependencies = [ 375 | "proc-macro2", 376 | "quote", 377 | "syn", 378 | ] 379 | 380 | [[package]] 381 | name = "lazy_static" 382 | version = "1.5.0" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 385 | 386 | [[package]] 387 | name = "lazycell" 388 | version = "1.3.0" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 391 | 392 | [[package]] 393 | name = "libc" 394 | version = "0.2.172" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 397 | 398 | [[package]] 399 | name = "libloading" 400 | version = "0.8.8" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" 403 | dependencies = [ 404 | "cfg-if", 405 | "windows-targets 0.53.0", 406 | ] 407 | 408 | [[package]] 409 | name = "linux-raw-sys" 410 | version = "0.4.15" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 413 | 414 | [[package]] 415 | name = "log" 416 | version = "0.4.27" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 419 | 420 | [[package]] 421 | name = "macros" 422 | version = "0.1.0" 423 | dependencies = [ 424 | "quote", 425 | "syn", 426 | ] 427 | 428 | [[package]] 429 | name = "memchr" 430 | version = "2.7.4" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 433 | 434 | [[package]] 435 | name = "memmap2" 436 | version = "0.9.5" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" 439 | dependencies = [ 440 | "libc", 441 | ] 442 | 443 | [[package]] 444 | name = "minimal-lexical" 445 | version = "0.2.1" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 448 | 449 | [[package]] 450 | name = "nom" 451 | version = "7.1.3" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 454 | dependencies = [ 455 | "memchr", 456 | "minimal-lexical", 457 | ] 458 | 459 | [[package]] 460 | name = "num-conv" 461 | version = "0.1.0" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 464 | 465 | [[package]] 466 | name = "num_threads" 467 | version = "0.1.7" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 470 | dependencies = [ 471 | "libc", 472 | ] 473 | 474 | [[package]] 475 | name = "once_cell" 476 | version = "1.21.3" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 479 | 480 | [[package]] 481 | name = "once_cell_polyfill" 482 | version = "1.70.1" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 485 | 486 | [[package]] 487 | name = "pkg-config" 488 | version = "0.3.32" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 491 | 492 | [[package]] 493 | name = "portable-atomic" 494 | version = "1.11.0" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 497 | 498 | [[package]] 499 | name = "portable-atomic-util" 500 | version = "0.2.4" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 503 | dependencies = [ 504 | "portable-atomic", 505 | ] 506 | 507 | [[package]] 508 | name = "powerfmt" 509 | version = "0.2.0" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 512 | 513 | [[package]] 514 | name = "pretty_env_logger" 515 | version = "0.5.0" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" 518 | dependencies = [ 519 | "env_logger 0.10.2", 520 | "log", 521 | ] 522 | 523 | [[package]] 524 | name = "prettyplease" 525 | version = "0.2.32" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" 528 | dependencies = [ 529 | "proc-macro2", 530 | "syn", 531 | ] 532 | 533 | [[package]] 534 | name = "proc-macro2" 535 | version = "1.0.95" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 538 | dependencies = [ 539 | "unicode-ident", 540 | ] 541 | 542 | [[package]] 543 | name = "quick-xml" 544 | version = "0.30.0" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" 547 | dependencies = [ 548 | "memchr", 549 | ] 550 | 551 | [[package]] 552 | name = "quick-xml" 553 | version = "0.37.5" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" 556 | dependencies = [ 557 | "memchr", 558 | ] 559 | 560 | [[package]] 561 | name = "quote" 562 | version = "1.0.40" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 565 | dependencies = [ 566 | "proc-macro2", 567 | ] 568 | 569 | [[package]] 570 | name = "regex" 571 | version = "1.11.1" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 574 | dependencies = [ 575 | "aho-corasick", 576 | "memchr", 577 | "regex-automata", 578 | "regex-syntax", 579 | ] 580 | 581 | [[package]] 582 | name = "regex-automata" 583 | version = "0.4.9" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 586 | dependencies = [ 587 | "aho-corasick", 588 | "memchr", 589 | "regex-syntax", 590 | ] 591 | 592 | [[package]] 593 | name = "regex-syntax" 594 | version = "0.8.5" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 597 | 598 | [[package]] 599 | name = "rustc-hash" 600 | version = "1.1.0" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 603 | 604 | [[package]] 605 | name = "rustix" 606 | version = "0.38.44" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 609 | dependencies = [ 610 | "bitflags 2.9.1", 611 | "errno", 612 | "libc", 613 | "linux-raw-sys", 614 | "windows-sys", 615 | ] 616 | 617 | [[package]] 618 | name = "rustversion" 619 | version = "1.0.21" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 622 | 623 | [[package]] 624 | name = "sd-notify" 625 | version = "0.4.5" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "b943eadf71d8b69e661330cb0e2656e31040acf21ee7708e2c238a0ec6af2bf4" 628 | dependencies = [ 629 | "libc", 630 | ] 631 | 632 | [[package]] 633 | name = "serde" 634 | version = "1.0.219" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 637 | dependencies = [ 638 | "serde_derive", 639 | ] 640 | 641 | [[package]] 642 | name = "serde_derive" 643 | version = "1.0.219" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 646 | dependencies = [ 647 | "proc-macro2", 648 | "quote", 649 | "syn", 650 | ] 651 | 652 | [[package]] 653 | name = "shlex" 654 | version = "1.3.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 657 | 658 | [[package]] 659 | name = "slotmap" 660 | version = "1.0.7" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" 663 | dependencies = [ 664 | "version_check", 665 | ] 666 | 667 | [[package]] 668 | name = "smallvec" 669 | version = "1.15.0" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 672 | 673 | [[package]] 674 | name = "smithay-client-toolkit" 675 | version = "0.19.2" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" 678 | dependencies = [ 679 | "bitflags 2.9.1", 680 | "cursor-icon", 681 | "libc", 682 | "log", 683 | "memmap2", 684 | "rustix", 685 | "thiserror", 686 | "wayland-backend", 687 | "wayland-client", 688 | "wayland-csd-frame", 689 | "wayland-cursor", 690 | "wayland-protocols", 691 | "wayland-protocols-wlr", 692 | "wayland-scanner", 693 | "xkeysym", 694 | ] 695 | 696 | [[package]] 697 | name = "strsim" 698 | version = "0.11.1" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 701 | 702 | [[package]] 703 | name = "syn" 704 | version = "2.0.101" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 707 | dependencies = [ 708 | "proc-macro2", 709 | "quote", 710 | "unicode-ident", 711 | ] 712 | 713 | [[package]] 714 | name = "termcolor" 715 | version = "1.4.1" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 718 | dependencies = [ 719 | "winapi-util", 720 | ] 721 | 722 | [[package]] 723 | name = "testwl" 724 | version = "0.1.0" 725 | dependencies = [ 726 | "rustix", 727 | "wayland-protocols", 728 | "wayland-server", 729 | "wl_drm", 730 | ] 731 | 732 | [[package]] 733 | name = "thiserror" 734 | version = "1.0.69" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 737 | dependencies = [ 738 | "thiserror-impl", 739 | ] 740 | 741 | [[package]] 742 | name = "thiserror-impl" 743 | version = "1.0.69" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 746 | dependencies = [ 747 | "proc-macro2", 748 | "quote", 749 | "syn", 750 | ] 751 | 752 | [[package]] 753 | name = "time" 754 | version = "0.3.41" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 757 | dependencies = [ 758 | "deranged", 759 | "itoa", 760 | "libc", 761 | "num-conv", 762 | "num_threads", 763 | "powerfmt", 764 | "serde", 765 | "time-core", 766 | "time-macros", 767 | ] 768 | 769 | [[package]] 770 | name = "time-core" 771 | version = "0.1.4" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 774 | 775 | [[package]] 776 | name = "time-macros" 777 | version = "0.2.22" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 780 | dependencies = [ 781 | "num-conv", 782 | "time-core", 783 | ] 784 | 785 | [[package]] 786 | name = "unicode-ident" 787 | version = "1.0.18" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 790 | 791 | [[package]] 792 | name = "utf8parse" 793 | version = "0.2.2" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 796 | 797 | [[package]] 798 | name = "vergen" 799 | version = "9.0.6" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "6b2bf58be11fc9414104c6d3a2e464163db5ef74b12296bda593cac37b6e4777" 802 | dependencies = [ 803 | "anyhow", 804 | "derive_builder", 805 | "rustversion", 806 | "vergen-lib", 807 | ] 808 | 809 | [[package]] 810 | name = "vergen-gitcl" 811 | version = "1.0.8" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "b9dfc1de6eb2e08a4ddf152f1b179529638bedc0ea95e6d667c014506377aefe" 814 | dependencies = [ 815 | "anyhow", 816 | "derive_builder", 817 | "rustversion", 818 | "time", 819 | "vergen", 820 | "vergen-lib", 821 | ] 822 | 823 | [[package]] 824 | name = "vergen-lib" 825 | version = "0.1.6" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166" 828 | dependencies = [ 829 | "anyhow", 830 | "derive_builder", 831 | "rustversion", 832 | ] 833 | 834 | [[package]] 835 | name = "version_check" 836 | version = "0.9.5" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 839 | 840 | [[package]] 841 | name = "wayland-backend" 842 | version = "0.3.10" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" 845 | dependencies = [ 846 | "cc", 847 | "downcast-rs", 848 | "rustix", 849 | "smallvec", 850 | "wayland-sys", 851 | ] 852 | 853 | [[package]] 854 | name = "wayland-client" 855 | version = "0.31.10" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" 858 | dependencies = [ 859 | "bitflags 2.9.1", 860 | "rustix", 861 | "wayland-backend", 862 | "wayland-scanner", 863 | ] 864 | 865 | [[package]] 866 | name = "wayland-csd-frame" 867 | version = "0.3.0" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" 870 | dependencies = [ 871 | "bitflags 2.9.1", 872 | "cursor-icon", 873 | "wayland-backend", 874 | ] 875 | 876 | [[package]] 877 | name = "wayland-cursor" 878 | version = "0.31.10" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "a65317158dec28d00416cb16705934070aef4f8393353d41126c54264ae0f182" 881 | dependencies = [ 882 | "rustix", 883 | "wayland-client", 884 | "xcursor", 885 | ] 886 | 887 | [[package]] 888 | name = "wayland-protocols" 889 | version = "0.32.8" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" 892 | dependencies = [ 893 | "bitflags 2.9.1", 894 | "wayland-backend", 895 | "wayland-client", 896 | "wayland-scanner", 897 | "wayland-server", 898 | ] 899 | 900 | [[package]] 901 | name = "wayland-protocols-wlr" 902 | version = "0.3.8" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" 905 | dependencies = [ 906 | "bitflags 2.9.1", 907 | "wayland-backend", 908 | "wayland-client", 909 | "wayland-protocols", 910 | "wayland-scanner", 911 | ] 912 | 913 | [[package]] 914 | name = "wayland-scanner" 915 | version = "0.31.6" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" 918 | dependencies = [ 919 | "proc-macro2", 920 | "quick-xml 0.37.5", 921 | "quote", 922 | ] 923 | 924 | [[package]] 925 | name = "wayland-server" 926 | version = "0.31.9" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "485dfb8ccf0daa0d34625d34e6ac15f99e550a7999b6fd88a0835ccd37655785" 929 | dependencies = [ 930 | "bitflags 2.9.1", 931 | "downcast-rs", 932 | "rustix", 933 | "wayland-backend", 934 | "wayland-scanner", 935 | ] 936 | 937 | [[package]] 938 | name = "wayland-sys" 939 | version = "0.31.6" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" 942 | dependencies = [ 943 | "pkg-config", 944 | ] 945 | 946 | [[package]] 947 | name = "which" 948 | version = "4.4.2" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" 951 | dependencies = [ 952 | "either", 953 | "home", 954 | "once_cell", 955 | "rustix", 956 | ] 957 | 958 | [[package]] 959 | name = "winapi-util" 960 | version = "0.1.9" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 963 | dependencies = [ 964 | "windows-sys", 965 | ] 966 | 967 | [[package]] 968 | name = "windows-sys" 969 | version = "0.59.0" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 972 | dependencies = [ 973 | "windows-targets 0.52.6", 974 | ] 975 | 976 | [[package]] 977 | name = "windows-targets" 978 | version = "0.52.6" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 981 | dependencies = [ 982 | "windows_aarch64_gnullvm 0.52.6", 983 | "windows_aarch64_msvc 0.52.6", 984 | "windows_i686_gnu 0.52.6", 985 | "windows_i686_gnullvm 0.52.6", 986 | "windows_i686_msvc 0.52.6", 987 | "windows_x86_64_gnu 0.52.6", 988 | "windows_x86_64_gnullvm 0.52.6", 989 | "windows_x86_64_msvc 0.52.6", 990 | ] 991 | 992 | [[package]] 993 | name = "windows-targets" 994 | version = "0.53.0" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 997 | dependencies = [ 998 | "windows_aarch64_gnullvm 0.53.0", 999 | "windows_aarch64_msvc 0.53.0", 1000 | "windows_i686_gnu 0.53.0", 1001 | "windows_i686_gnullvm 0.53.0", 1002 | "windows_i686_msvc 0.53.0", 1003 | "windows_x86_64_gnu 0.53.0", 1004 | "windows_x86_64_gnullvm 0.53.0", 1005 | "windows_x86_64_msvc 0.53.0", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "windows_aarch64_gnullvm" 1010 | version = "0.52.6" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1013 | 1014 | [[package]] 1015 | name = "windows_aarch64_gnullvm" 1016 | version = "0.53.0" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 1019 | 1020 | [[package]] 1021 | name = "windows_aarch64_msvc" 1022 | version = "0.52.6" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1025 | 1026 | [[package]] 1027 | name = "windows_aarch64_msvc" 1028 | version = "0.53.0" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 1031 | 1032 | [[package]] 1033 | name = "windows_i686_gnu" 1034 | version = "0.52.6" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1037 | 1038 | [[package]] 1039 | name = "windows_i686_gnu" 1040 | version = "0.53.0" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 1043 | 1044 | [[package]] 1045 | name = "windows_i686_gnullvm" 1046 | version = "0.52.6" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1049 | 1050 | [[package]] 1051 | name = "windows_i686_gnullvm" 1052 | version = "0.53.0" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 1055 | 1056 | [[package]] 1057 | name = "windows_i686_msvc" 1058 | version = "0.52.6" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1061 | 1062 | [[package]] 1063 | name = "windows_i686_msvc" 1064 | version = "0.53.0" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 1067 | 1068 | [[package]] 1069 | name = "windows_x86_64_gnu" 1070 | version = "0.52.6" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1073 | 1074 | [[package]] 1075 | name = "windows_x86_64_gnu" 1076 | version = "0.53.0" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 1079 | 1080 | [[package]] 1081 | name = "windows_x86_64_gnullvm" 1082 | version = "0.52.6" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1085 | 1086 | [[package]] 1087 | name = "windows_x86_64_gnullvm" 1088 | version = "0.53.0" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 1091 | 1092 | [[package]] 1093 | name = "windows_x86_64_msvc" 1094 | version = "0.52.6" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1097 | 1098 | [[package]] 1099 | name = "windows_x86_64_msvc" 1100 | version = "0.53.0" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1103 | 1104 | [[package]] 1105 | name = "wl_drm" 1106 | version = "0.1.0" 1107 | dependencies = [ 1108 | "wayland-client", 1109 | "wayland-scanner", 1110 | "wayland-server", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "xcb" 1115 | version = "1.5.0" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be" 1118 | dependencies = [ 1119 | "bitflags 1.3.2", 1120 | "libc", 1121 | "quick-xml 0.30.0", 1122 | ] 1123 | 1124 | [[package]] 1125 | name = "xcb-util-cursor" 1126 | version = "0.3.3" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "adc4566d84acf11570d684ae89c6da06ab0452aa647a80a7b2f4bc741733e4ac" 1129 | dependencies = [ 1130 | "xcb", 1131 | "xcb-util-cursor-sys", 1132 | ] 1133 | 1134 | [[package]] 1135 | name = "xcb-util-cursor-sys" 1136 | version = "0.1.4" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "e2675ff3564723a6c85e22cedcc05ae84da546470aa1646931d5efdfba3ba601" 1139 | dependencies = [ 1140 | "bindgen", 1141 | "pkg-config", 1142 | "xcb", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "xcursor" 1147 | version = "0.3.8" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" 1150 | 1151 | [[package]] 1152 | name = "xkeysym" 1153 | version = "0.2.1" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" 1156 | 1157 | [[package]] 1158 | name = "xwayland-satellite" 1159 | version = "0.6.0" 1160 | dependencies = [ 1161 | "anyhow", 1162 | "bitflags 2.9.1", 1163 | "env_logger 0.11.8", 1164 | "libc", 1165 | "log", 1166 | "macros", 1167 | "pretty_env_logger", 1168 | "rustix", 1169 | "sd-notify", 1170 | "slotmap", 1171 | "smithay-client-toolkit", 1172 | "testwl", 1173 | "vergen-gitcl", 1174 | "wayland-client", 1175 | "wayland-protocols", 1176 | "wayland-scanner", 1177 | "wayland-server", 1178 | "wl_drm", 1179 | "xcb", 1180 | "xcb-util-cursor", 1181 | ] 1182 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["macros"] 3 | 4 | [workspace.dependencies] 5 | wayland-client = "0.31.2" 6 | wayland-protocols = "0.32.0" 7 | wayland-scanner = "0.31.1" 8 | wayland-server = "0.31.1" 9 | rustix = "0.38.31" 10 | 11 | [workspace.lints.clippy] 12 | all = "deny" 13 | 14 | [package] 15 | name = "xwayland-satellite" 16 | version = "0.6.0" 17 | edition = "2021" 18 | 19 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 20 | 21 | [lib] 22 | crate-type = ["lib"] 23 | 24 | [dependencies] 25 | bitflags = "2.5.0" 26 | rustix = { workspace = true, features = ["event"] } 27 | wayland-client.workspace = true 28 | wayland-protocols = { workspace = true, features = ["client", "server", "staging", "unstable"] } 29 | wayland-scanner.workspace = true 30 | wayland-server.workspace = true 31 | xcb = { version = "1.3.0", features = ["composite", "randr", "res"] } 32 | wl_drm = { path = "wl_drm" } 33 | libc = "0.2.153" 34 | log = "0.4.21" 35 | env_logger = "0.11.3" 36 | pretty_env_logger = "0.5.0" 37 | slotmap = "1.0.7" 38 | xcb-util-cursor = "0.3.2" 39 | smithay-client-toolkit = { version = "0.19.1", default-features = false } 40 | 41 | sd-notify = { version = "0.4.2", optional = true } 42 | macros = { version = "0.1.0", path = "macros" } 43 | 44 | [features] 45 | default = [] 46 | systemd = ["dep:sd-notify"] 47 | 48 | [dev-dependencies] 49 | rustix = { workspace = true, features = ["fs"] } 50 | testwl = { path = "testwl" } 51 | 52 | [build-dependencies] 53 | anyhow = "1.0.98" 54 | vergen-gitcl = "1.0.8" 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xwayland-satellite 2 | xwayland-satellite grants rootless Xwayland integration to any Wayland compositor implementing xdg_wm_base and viewporter. 3 | This is particularly useful for compositors that (understandably) do not want to go through implementing support for rootless Xwayland themselves. 4 | 5 | Found a bug? [Open a bug report.](https://github.com/Supreeeme/xwayland-satellite/issues/new?template=bug_report.yaml) 6 | 7 | Need help troubleshooting, or have some other general question? [Ask on GitHub Discussions.](https://github.com/Supreeeme/xwayland-satellite/discussions) 8 | 9 | ## Dependencies 10 | - Xwayland >=23.1 11 | - xcb 12 | - xcb-util-cursor 13 | - clang (building only) 14 | 15 | ## Usage 16 | Run `xwayland-satellite`. You can specify an X display to use (i.e. `:12`). Be sure to set the same `DISPLAY` environment variable for any X11 clients. 17 | Because xwayland-satellite is a Wayland client (in addition to being a Wayland compositor), it will need to launch after your compositor launches, but obviously before any X11 applications. 18 | 19 | ## Building 20 | ``` 21 | # dev build 22 | cargo build 23 | # release build 24 | cargo build --release 25 | 26 | # run - will also build if not already built 27 | cargo run # --release 28 | ``` 29 | 30 | ## Systemd support 31 | xwayland-satellite can be built with systemd support - simply add `-F systemd` to your build command - i.e. `cargo build --release -F systemd`. 32 | 33 | With systemd support, satellite will send a state change notification when Xwayland has been initialized, allowing for having services dependent on satellite's startup. 34 | 35 | An example service file is located in `resources/xwayland-satellite.service` - be sure to replace the `ExecStart` line with the proper location before using it. 36 | It can be placed in a systemd user unit directory (i.e. `$XDG_CONFIG_HOME/systemd/user` or `/etc/systemd/user`), 37 | and be launched and enabled with `systemctl --user enable --now xwayland-satellite`. 38 | It will be started when the `graphical-session.target` is reached, 39 | which is likely after your compositor is started if it supports systemd. 40 | 41 | ## Scaling/HiDPI 42 | For most GTK and Qt apps, xwayland-satellite should automatically scale them properly. Note that for mixed DPI monitor setups, satellite will choose 43 | the smallest monitor's DPI, meaning apps may have small text on other monitors. 44 | 45 | Other miscellaneous apps (such as Wine apps) may have small text on HiDPI displays. It is application dependent on getting apps to scale properly with satellite, 46 | so you will have to figure out what app specific config needs to be set. See [the Arch Wiki on HiDPI](https://wiki.archlinux.org/title/HiDPI) for a good place start. 47 | 48 | Satellite acts as an Xsettings manager for setting scaling related settings, but will get out of the way of other Xsettings managers. 49 | To manually set these settings, try [xsettingsd](https://codeberg.org/derat/xsettingsd) or another Xsettings manager. 50 | 51 | ## Wayland protocols used 52 | The host compositor **must** implement the following protocols/interfaces for satellite to function: 53 | - Core interfaces (wl_output, wl_surface, wl_compositor, etc) 54 | - xdg_shell (xdg_wm_base, xdg_surface, xdg_popup, xdg_toplevel) 55 | - wp_viewporter - used for scaling 56 | 57 | Additionally, satellite can *optionally* take advantage of the following protocols: 58 | - Linux dmabuf 59 | - XDG activation 60 | - XDG foreign 61 | - Pointer constraints 62 | - Tablet input 63 | - Fractional scale 64 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use vergen_gitcl::{Emitter, GitclBuilder}; 2 | 3 | fn main() -> Result<(), anyhow::Error> { 4 | let builder = GitclBuilder::default().describe(true, true, None).build()?; 5 | Emitter::default().add_instructions(&builder)?.emit() 6 | } 7 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1735563628, 24 | "narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-24.05", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs", 41 | "rust-overlay": "rust-overlay" 42 | } 43 | }, 44 | "rust-overlay": { 45 | "inputs": { 46 | "nixpkgs": [ 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1739240901, 52 | "narHash": "sha256-YDtl/9w71m5WcZvbEroYoWrjECDhzJZLZ8E68S3BYok=", 53 | "owner": "oxalica", 54 | "repo": "rust-overlay", 55 | "rev": "03473e2af8a4b490f4d2cdb2e4d3b75f82c8197c", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "oxalica", 60 | "repo": "rust-overlay", 61 | "type": "github" 62 | } 63 | }, 64 | "systems": { 65 | "locked": { 66 | "lastModified": 1681028828, 67 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 68 | "owner": "nix-systems", 69 | "repo": "default", 70 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "nix-systems", 75 | "repo": "default", 76 | "type": "github" 77 | } 78 | } 79 | }, 80 | "root": "root", 81 | "version": 7 82 | } 83 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | rust-overlay = { 6 | url = "github:oxalica/rust-overlay"; 7 | inputs.nixpkgs.follows = "nixpkgs"; 8 | }; 9 | }; 10 | 11 | outputs = { self, nixpkgs, rust-overlay, flake-utils }: 12 | let systems = [ "x86_64-linux" "aarch64-linux" ]; 13 | in flake-utils.lib.eachSystem systems (system: 14 | let 15 | overlays = [ (import rust-overlay) ]; 16 | pkgs = import nixpkgs { inherit system overlays; }; 17 | 18 | cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); 19 | cargoPackageVersion = cargoToml.package.version; 20 | 21 | commitHash = self.shortRev or self.dirtyShortRev or "unknown"; 22 | 23 | version = "${cargoPackageVersion}-${commitHash}"; 24 | 25 | buildXwaylandSatellite = 26 | { lib 27 | , rustPlatform 28 | , pkg-config 29 | , makeBinaryWrapper 30 | , libxcb 31 | , xcb-util-cursor 32 | , xwayland 33 | , withSystemd ? true 34 | }: 35 | 36 | rustPlatform.buildRustPackage rec { 37 | pname = "xwayland-satellite"; 38 | inherit version; 39 | 40 | src = self; 41 | 42 | cargoLock = { 43 | lockFile = "${src}/Cargo.lock"; 44 | allowBuiltinFetchGit = true; 45 | }; 46 | 47 | nativeBuildInputs = [ 48 | rustPlatform.bindgenHook 49 | pkg-config 50 | makeBinaryWrapper 51 | ]; 52 | 53 | buildInputs = [ 54 | libxcb 55 | xcb-util-cursor 56 | ]; 57 | 58 | buildNoDefaultFeatures = true; 59 | buildFeatures = lib.optionals withSystemd [ "systemd" ]; 60 | 61 | postPatch = '' 62 | substituteInPlace resources/xwayland-satellite.service \ 63 | --replace-fail '/usr/local/bin' "$out/bin" 64 | ''; 65 | 66 | postInstall = lib.optionalString withSystemd '' 67 | install -Dm0644 resources/xwayland-satellite.service -t $out/lib/systemd/user 68 | ''; 69 | 70 | postFixup = '' 71 | wrapProgram $out/bin/xwayland-satellite \ 72 | --prefix PATH : "${lib.makeBinPath [ xwayland ]}" 73 | ''; 74 | 75 | doCheck = false; 76 | 77 | meta = with lib; { 78 | description = "Xwayland outside your Wayland"; 79 | homepage = "https://github.com/Supreeeme/xwayland-satellite"; 80 | license = licenses.mpl20; 81 | platforms = platforms.linux; 82 | }; 83 | }; 84 | 85 | xwayland-satellite = pkgs.callPackage buildXwaylandSatellite { }; 86 | in 87 | { 88 | devShell = (pkgs.mkShell.override { stdenv = pkgs.clangStdenv; }) { 89 | buildInputs = with pkgs; [ 90 | rustPlatform.bindgenHook 91 | rust-bin.stable.latest.default 92 | pkg-config 93 | 94 | xcb-util-cursor 95 | xorg.libxcb 96 | xwayland 97 | ]; 98 | }; 99 | 100 | packages = { 101 | xwayland-satellite = xwayland-satellite; 102 | default = xwayland-satellite; 103 | }; 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "macros" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lints] 7 | workspace = true 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | quote = "1.0.37" 14 | syn = "2.0.79" 15 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | use quote::{format_ident, quote}; 4 | use syn::{ 5 | braced, bracketed, parse::Parse, parse_macro_input, parse_quote, punctuated::Punctuated, Token, 6 | }; 7 | 8 | enum FieldOrClosure { 9 | Field(syn::Ident), 10 | Closure(syn::Ident, syn::Expr), 11 | } 12 | 13 | impl Parse for FieldOrClosure { 14 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 15 | input.parse().map(Self::Field).or_else(|_| { 16 | input.parse().map(|mut closure: syn::ExprClosure| { 17 | assert_eq!(closure.inputs.len(), 1); 18 | let syn::Pat::Ident(arg) = closure.inputs.pop().unwrap().into_value() else { 19 | panic!("expected ident for closure argument"); 20 | }; 21 | 22 | Self::Closure(arg.ident, *closure.body) 23 | }) 24 | }) 25 | } 26 | } 27 | 28 | struct EventVariant { 29 | name: syn::Ident, 30 | fields: Option>, 31 | } 32 | 33 | impl Parse for EventVariant { 34 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 35 | let name = input.parse()?; 36 | let fields = if input.peek(syn::token::Brace) { 37 | let f; 38 | braced!(f in input); 39 | let f = Punctuated::parse_terminated(&f)?; 40 | Some(f) 41 | } else { 42 | None 43 | }; 44 | 45 | Ok(Self { name, fields }) 46 | } 47 | } 48 | 49 | struct Input { 50 | object: syn::Expr, 51 | event_object: syn::Ident, 52 | event_type: syn::Type, 53 | events: Punctuated, 54 | } 55 | 56 | impl Parse for Input { 57 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 58 | let object = input.parse()?; 59 | input.parse::()?; 60 | let event_object = input.parse()?; 61 | input.parse::()?; 62 | let event_type = input.parse()?; 63 | input.parse::]>()?; 64 | let events; 65 | bracketed!(events in input); 66 | let events = Punctuated::parse_terminated(&events)?; 67 | Ok(Self { 68 | object, 69 | event_object, 70 | event_type, 71 | events, 72 | }) 73 | } 74 | } 75 | 76 | #[proc_macro] 77 | pub fn simple_event_shunt(tokens: TokenStream) -> TokenStream { 78 | let Input { 79 | object, 80 | event_object, 81 | event_type, 82 | events, 83 | } = parse_macro_input!(tokens as Input); 84 | 85 | let match_arms = events.into_iter().map(|e| { 86 | let mut field_names = Punctuated::<_, Token![,]>::new(); 87 | let mut fn_args = Punctuated::::new(); 88 | if let Some(fields) = e.fields { 89 | for field in fields { 90 | match field { 91 | FieldOrClosure::Field(name) => { 92 | fn_args.push(parse_quote! { #name }); 93 | field_names.push(name); 94 | } 95 | FieldOrClosure::Closure(name, expr) => { 96 | field_names.push(name); 97 | fn_args.push(expr); 98 | } 99 | } 100 | } 101 | } 102 | 103 | let name = e.name; 104 | let fn_name = String::from_utf8( 105 | name.to_string() 106 | .bytes() 107 | .enumerate() 108 | .flat_map(|(idx, c)| { 109 | if idx != 0 && c.is_ascii_uppercase() { 110 | vec![b'_', c.to_ascii_lowercase()] 111 | } else { 112 | vec![c.to_ascii_lowercase()] 113 | } 114 | }) 115 | .collect::>(), 116 | ) 117 | .unwrap(); 118 | let keyword_pfx = if fn_name == "type" { "_" } else { "" }; 119 | let fn_name = format_ident!("{keyword_pfx}{fn_name}"); 120 | 121 | quote! { 122 | #name { #field_names } => { #object.#fn_name(#fn_args); } 123 | } 124 | }); 125 | quote! {{ 126 | use #event_type::*; 127 | match #event_object { 128 | #(#match_arms)* 129 | _ => log::warn!(concat!("unhandled ", stringify!(#event_type), ": {:?}"), #event_object) 130 | } 131 | }} 132 | .into() 133 | } 134 | -------------------------------------------------------------------------------- /resources/xwayland-satellite.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Xwayland outside your Wayland 3 | BindsTo=graphical-session.target 4 | PartOf=graphical-session.target 5 | After=graphical-session.target 6 | Requisite=graphical-session.target 7 | 8 | [Service] 9 | Type=notify 10 | NotifyAccess=all 11 | ExecStart=/usr/local/bin/xwayland-satellite 12 | StandardOutput=journal 13 | 14 | [Install] 15 | WantedBy=graphical-session.target 16 | -------------------------------------------------------------------------------- /src/clientside/data_device.rs: -------------------------------------------------------------------------------- 1 | use crate::clientside::Globals; 2 | use smithay_client_toolkit::{ 3 | data_device_manager::{ 4 | data_device::DataDeviceHandler, data_offer::DataOfferHandler, 5 | data_source::DataSourceHandler, 6 | }, 7 | delegate_data_device, 8 | }; 9 | delegate_data_device!(Globals); 10 | 11 | impl DataDeviceHandler for Globals { 12 | fn selection( 13 | &mut self, 14 | _: &wayland_client::Connection, 15 | _: &wayland_client::QueueHandle, 16 | data_device: &wayland_client::protocol::wl_data_device::WlDataDevice, 17 | ) { 18 | self.selection = Some(data_device.clone()); 19 | } 20 | 21 | fn drop_performed( 22 | &mut self, 23 | _: &wayland_client::Connection, 24 | _: &wayland_client::QueueHandle, 25 | _: &wayland_client::protocol::wl_data_device::WlDataDevice, 26 | ) { 27 | } 28 | 29 | fn motion( 30 | &mut self, 31 | _: &wayland_client::Connection, 32 | _: &wayland_client::QueueHandle, 33 | _: &wayland_client::protocol::wl_data_device::WlDataDevice, 34 | _: f64, 35 | _: f64, 36 | ) { 37 | } 38 | 39 | fn leave( 40 | &mut self, 41 | _: &wayland_client::Connection, 42 | _: &wayland_client::QueueHandle, 43 | _: &wayland_client::protocol::wl_data_device::WlDataDevice, 44 | ) { 45 | } 46 | 47 | fn enter( 48 | &mut self, 49 | _: &wayland_client::Connection, 50 | _: &wayland_client::QueueHandle, 51 | _: &wayland_client::protocol::wl_data_device::WlDataDevice, 52 | _: f64, 53 | _: f64, 54 | _: &wayland_client::protocol::wl_surface::WlSurface, 55 | ) { 56 | } 57 | } 58 | 59 | impl DataSourceHandler for Globals { 60 | fn send_request( 61 | &mut self, 62 | _: &wayland_client::Connection, 63 | _: &wayland_client::QueueHandle, 64 | _: &wayland_client::protocol::wl_data_source::WlDataSource, 65 | mime: String, 66 | fd: smithay_client_toolkit::data_device_manager::WritePipe, 67 | ) { 68 | self.selection_requests.push((mime, fd)); 69 | } 70 | 71 | fn cancelled( 72 | &mut self, 73 | _: &wayland_client::Connection, 74 | _: &wayland_client::QueueHandle, 75 | _: &wayland_client::protocol::wl_data_source::WlDataSource, 76 | ) { 77 | self.cancelled = true; 78 | } 79 | 80 | fn action( 81 | &mut self, 82 | _: &wayland_client::Connection, 83 | _: &wayland_client::QueueHandle, 84 | _: &wayland_client::protocol::wl_data_source::WlDataSource, 85 | _: wayland_client::protocol::wl_data_device_manager::DndAction, 86 | ) { 87 | } 88 | 89 | fn dnd_finished( 90 | &mut self, 91 | _: &wayland_client::Connection, 92 | _: &wayland_client::QueueHandle, 93 | _: &wayland_client::protocol::wl_data_source::WlDataSource, 94 | ) { 95 | } 96 | 97 | fn dnd_dropped( 98 | &mut self, 99 | _: &wayland_client::Connection, 100 | _: &wayland_client::QueueHandle, 101 | _: &wayland_client::protocol::wl_data_source::WlDataSource, 102 | ) { 103 | } 104 | 105 | fn accept_mime( 106 | &mut self, 107 | _: &wayland_client::Connection, 108 | _: &wayland_client::QueueHandle, 109 | _: &wayland_client::protocol::wl_data_source::WlDataSource, 110 | _: Option, 111 | ) { 112 | } 113 | } 114 | 115 | impl DataOfferHandler for Globals { 116 | fn selected_action( 117 | &mut self, 118 | _: &wayland_client::Connection, 119 | _: &wayland_client::QueueHandle, 120 | _: &mut smithay_client_toolkit::data_device_manager::data_offer::DragOffer, 121 | _: wayland_client::protocol::wl_data_device_manager::DndAction, 122 | ) { 123 | } 124 | 125 | fn source_actions( 126 | &mut self, 127 | _: &wayland_client::Connection, 128 | _: &wayland_client::QueueHandle, 129 | _: &mut smithay_client_toolkit::data_device_manager::data_offer::DragOffer, 130 | _: wayland_client::protocol::wl_data_device_manager::DndAction, 131 | ) { 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/clientside/mod.rs: -------------------------------------------------------------------------------- 1 | mod data_device; 2 | pub mod xdg_activation; 3 | 4 | use crate::server::{ObjectEvent, ObjectKey}; 5 | use std::os::unix::net::UnixStream; 6 | use std::sync::{mpsc, Mutex, OnceLock}; 7 | use wayland_client::protocol::{ 8 | wl_buffer::WlBuffer, wl_callback::WlCallback, wl_compositor::WlCompositor, 9 | wl_keyboard::WlKeyboard, wl_output::WlOutput, wl_pointer::WlPointer, wl_region::WlRegion, 10 | wl_registry::WlRegistry, wl_seat::WlSeat, wl_shm::WlShm, wl_shm_pool::WlShmPool, 11 | wl_surface::WlSurface, wl_touch::WlTouch, 12 | }; 13 | use wayland_client::{ 14 | delegate_noop, event_created_child, 15 | globals::{registry_queue_init, Global, GlobalList, GlobalListContents}, 16 | Connection, Dispatch, EventQueue, Proxy, QueueHandle, 17 | }; 18 | use wayland_protocols::wp::relative_pointer::zv1::client::{ 19 | zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, 20 | zwp_relative_pointer_v1::ZwpRelativePointerV1, 21 | }; 22 | use wayland_protocols::xdg::decoration::zv1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; 23 | use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1; 24 | use wayland_protocols::{ 25 | wp::{ 26 | fractional_scale::v1::client::{ 27 | wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1, 28 | wp_fractional_scale_v1::WpFractionalScaleV1, 29 | }, 30 | linux_dmabuf::zv1::client::{ 31 | self as dmabuf, 32 | zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1 as DmabufFeedback, 33 | zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, 34 | }, 35 | pointer_constraints::zv1::client::{ 36 | zwp_confined_pointer_v1::ZwpConfinedPointerV1, 37 | zwp_locked_pointer_v1::ZwpLockedPointerV1, 38 | zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, 39 | }, 40 | tablet::zv2::client::{ 41 | zwp_tablet_manager_v2::ZwpTabletManagerV2, 42 | zwp_tablet_pad_group_v2::{ZwpTabletPadGroupV2, EVT_RING_OPCODE, EVT_STRIP_OPCODE}, 43 | zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2, 44 | zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2, 45 | zwp_tablet_pad_v2::{ZwpTabletPadV2, EVT_GROUP_OPCODE}, 46 | zwp_tablet_seat_v2::{ 47 | ZwpTabletSeatV2, EVT_PAD_ADDED_OPCODE, EVT_TABLET_ADDED_OPCODE, 48 | EVT_TOOL_ADDED_OPCODE, 49 | }, 50 | zwp_tablet_tool_v2::ZwpTabletToolV2, 51 | zwp_tablet_v2::ZwpTabletV2, 52 | }, 53 | viewporter::client::{wp_viewport::WpViewport, wp_viewporter::WpViewporter}, 54 | }, 55 | xdg::{ 56 | activation::v1::client::xdg_activation_v1::XdgActivationV1, 57 | shell::client::{ 58 | xdg_popup::XdgPopup, xdg_positioner::XdgPositioner, xdg_surface::XdgSurface, 59 | xdg_toplevel::XdgToplevel, xdg_wm_base::XdgWmBase, 60 | }, 61 | xdg_output::zv1::client::{ 62 | zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1::ZxdgOutputV1 as XdgOutput, 63 | }, 64 | }, 65 | }; 66 | use wayland_server::protocol as server; 67 | use wl_drm::client::wl_drm::WlDrm; 68 | 69 | #[derive(Default)] 70 | pub struct Globals { 71 | events: Vec<(ObjectKey, ObjectEvent)>, 72 | queued_events: Vec>, 73 | pub new_globals: Vec, 74 | pub selection: Option, 75 | pub selection_requests: Vec<( 76 | String, 77 | smithay_client_toolkit::data_device_manager::WritePipe, 78 | )>, 79 | pub cancelled: bool, 80 | pub pending_activations: Vec<(xcb::x::Window, String)>, 81 | } 82 | 83 | pub type ClientQueueHandle = QueueHandle; 84 | 85 | pub struct ClientState { 86 | _connection: Connection, 87 | pub queue: EventQueue, 88 | pub qh: ClientQueueHandle, 89 | pub globals: Globals, 90 | pub global_list: GlobalList, 91 | } 92 | 93 | impl ClientState { 94 | pub fn new(server_connection: Option) -> Self { 95 | let connection = if let Some(stream) = server_connection { 96 | Connection::from_socket(stream) 97 | } else { 98 | Connection::connect_to_env() 99 | } 100 | .unwrap(); 101 | let (global_list, queue) = registry_queue_init::(&connection).unwrap(); 102 | let globals = Globals::default(); 103 | let qh = queue.handle(); 104 | 105 | Self { 106 | _connection: connection, 107 | queue, 108 | qh, 109 | globals, 110 | global_list, 111 | } 112 | } 113 | 114 | pub fn read_events(&mut self) -> Vec<(ObjectKey, ObjectEvent)> { 115 | let mut events = std::mem::take(&mut self.globals.events); 116 | self.globals.queued_events.retain(|rx| { 117 | match rx.try_recv() { 118 | Ok(event) => { 119 | events.push(event); 120 | } 121 | Err(std::sync::mpsc::TryRecvError::Empty) => return true, 122 | 123 | Err(_) => unreachable!(), 124 | } 125 | 126 | events.extend(rx.try_iter()); 127 | false 128 | }); 129 | events 130 | } 131 | } 132 | 133 | pub type Event = ::Event; 134 | 135 | delegate_noop!(Globals: WlCompositor); 136 | delegate_noop!(Globals: WlRegion); 137 | delegate_noop!(Globals: ignore WlShm); 138 | delegate_noop!(Globals: ignore ZwpLinuxDmabufV1); 139 | delegate_noop!(Globals: ZwpRelativePointerManagerV1); 140 | delegate_noop!(Globals: ignore dmabuf::zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1); 141 | delegate_noop!(Globals: XdgPositioner); 142 | delegate_noop!(Globals: WlShmPool); 143 | delegate_noop!(Globals: WpViewporter); 144 | delegate_noop!(Globals: WpViewport); 145 | delegate_noop!(Globals: ZxdgOutputManagerV1); 146 | delegate_noop!(Globals: ZwpPointerConstraintsV1); 147 | delegate_noop!(Globals: ZwpTabletManagerV2); 148 | delegate_noop!(Globals: XdgActivationV1); 149 | delegate_noop!(Globals: ZxdgDecorationManagerV1); 150 | delegate_noop!(Globals: WpFractionalScaleManagerV1); 151 | delegate_noop!(Globals: ignore ZxdgToplevelDecorationV1); 152 | 153 | impl Dispatch for Globals { 154 | fn event( 155 | state: &mut Self, 156 | _: &WlRegistry, 157 | event: ::Event, 158 | _: &GlobalListContents, 159 | _: &wayland_client::Connection, 160 | _: &wayland_client::QueueHandle, 161 | ) { 162 | if let Event::::Global { 163 | name, 164 | interface, 165 | version, 166 | } = event 167 | { 168 | state.new_globals.push(Global { 169 | name, 170 | interface, 171 | version, 172 | }); 173 | }; 174 | } 175 | } 176 | 177 | impl Dispatch for Globals { 178 | fn event( 179 | _: &mut Self, 180 | base: &XdgWmBase, 181 | event: ::Event, 182 | _: &(), 183 | _: &wayland_client::Connection, 184 | _: &wayland_client::QueueHandle, 185 | ) { 186 | if let Event::::Ping { serial } = event { 187 | base.pong(serial); 188 | } 189 | } 190 | } 191 | 192 | impl Dispatch for Globals { 193 | fn event( 194 | _: &mut Self, 195 | _: &WlCallback, 196 | event: ::Event, 197 | s_callback: &server::wl_callback::WlCallback, 198 | _: &Connection, 199 | _: &QueueHandle, 200 | ) { 201 | if let Event::::Done { callback_data } = event { 202 | s_callback.done(callback_data); 203 | } 204 | } 205 | } 206 | 207 | macro_rules! push_events { 208 | ($type:ident) => { 209 | impl Dispatch<$type, ObjectKey> for Globals { 210 | fn event( 211 | state: &mut Self, 212 | _: &$type, 213 | event: <$type as Proxy>::Event, 214 | key: &ObjectKey, 215 | _: &Connection, 216 | _: &QueueHandle, 217 | ) { 218 | state.events.push((*key, event.into())); 219 | } 220 | } 221 | }; 222 | } 223 | 224 | push_events!(WlSurface); 225 | push_events!(WlBuffer); 226 | push_events!(XdgSurface); 227 | push_events!(XdgToplevel); 228 | push_events!(XdgPopup); 229 | push_events!(WlSeat); 230 | push_events!(WlPointer); 231 | push_events!(WlOutput); 232 | push_events!(WlKeyboard); 233 | push_events!(ZwpRelativePointerV1); 234 | push_events!(WlDrm); 235 | push_events!(DmabufFeedback); 236 | push_events!(XdgOutput); 237 | push_events!(WlTouch); 238 | push_events!(ZwpConfinedPointerV1); 239 | push_events!(ZwpLockedPointerV1); 240 | push_events!(WpFractionalScaleV1); 241 | 242 | pub(crate) struct LateInitObjectKey { 243 | key: OnceLock, 244 | queued_events: Mutex>, 245 | sender: Mutex>>, 246 | } 247 | 248 | impl LateInitObjectKey

249 | where 250 | P::Event: Into, 251 | { 252 | pub fn init(&self, key: ObjectKey) { 253 | self.key.set(key).expect("Object key should not be set"); 254 | if let Some(sender) = self.sender.lock().unwrap().take() { 255 | for event in self.queued_events.lock().unwrap().drain(..) { 256 | sender.send((key, event.into())).unwrap(); 257 | } 258 | } 259 | } 260 | 261 | fn new() -> Self { 262 | Self { 263 | key: OnceLock::new(), 264 | queued_events: Mutex::default(), 265 | sender: Mutex::default(), 266 | } 267 | } 268 | 269 | fn push_or_queue_event(&self, state: &mut Globals, event: P::Event) { 270 | if let Some(key) = self.key.get().copied() { 271 | state.events.push((key, event.into())); 272 | } else { 273 | let mut sender = self.sender.lock().unwrap(); 274 | if sender.is_none() { 275 | let (send, recv) = mpsc::channel(); 276 | *sender = Some(send); 277 | state.queued_events.push(recv); 278 | } 279 | self.queued_events.lock().unwrap().push(event); 280 | } 281 | } 282 | } 283 | 284 | impl std::ops::Deref for LateInitObjectKey

{ 285 | type Target = ObjectKey; 286 | 287 | #[track_caller] 288 | fn deref(&self) -> &Self::Target { 289 | self.key.get().expect("object key has not been initialized") 290 | } 291 | } 292 | 293 | impl Dispatch for Globals { 294 | fn event( 295 | state: &mut Self, 296 | _: &ZwpTabletSeatV2, 297 | event: ::Event, 298 | key: &ObjectKey, 299 | _: &Connection, 300 | _: &QueueHandle, 301 | ) { 302 | state.events.push((*key, event.into())); 303 | } 304 | 305 | event_created_child!(Globals, ZwpTabletSeatV2, [ 306 | EVT_TABLET_ADDED_OPCODE => (ZwpTabletV2, LateInitObjectKey::new()), 307 | EVT_PAD_ADDED_OPCODE => (ZwpTabletPadV2, LateInitObjectKey::new()), 308 | EVT_TOOL_ADDED_OPCODE => (ZwpTabletToolV2, LateInitObjectKey::new()) 309 | ]); 310 | } 311 | 312 | macro_rules! push_or_queue_events { 313 | ($type:ty) => { 314 | impl Dispatch<$type, LateInitObjectKey<$type>> for Globals { 315 | fn event( 316 | state: &mut Self, 317 | _: &$type, 318 | event: <$type as Proxy>::Event, 319 | key: &LateInitObjectKey<$type>, 320 | _: &Connection, 321 | _: &QueueHandle, 322 | ) { 323 | key.push_or_queue_event(state, event); 324 | } 325 | } 326 | }; 327 | } 328 | 329 | push_or_queue_events!(ZwpTabletV2); 330 | push_or_queue_events!(ZwpTabletToolV2); 331 | push_or_queue_events!(ZwpTabletPadRingV2); 332 | push_or_queue_events!(ZwpTabletPadStripV2); 333 | 334 | impl Dispatch> for Globals { 335 | fn event( 336 | state: &mut Self, 337 | _: &ZwpTabletPadV2, 338 | event: ::Event, 339 | key: &LateInitObjectKey, 340 | _: &Connection, 341 | _: &QueueHandle, 342 | ) { 343 | key.push_or_queue_event(state, event); 344 | } 345 | 346 | event_created_child!(Globals, ZwpTabletPadV2, [ 347 | EVT_GROUP_OPCODE => (ZwpTabletPadGroupV2, LateInitObjectKey::new()) 348 | ]); 349 | } 350 | 351 | impl Dispatch> for Globals { 352 | fn event( 353 | state: &mut Self, 354 | _: &ZwpTabletPadGroupV2, 355 | event: ::Event, 356 | key: &LateInitObjectKey, 357 | _: &Connection, 358 | _: &QueueHandle, 359 | ) { 360 | key.push_or_queue_event(state, event); 361 | } 362 | 363 | event_created_child!(Globals, ZwpTabletPadGroupV2, [ 364 | EVT_RING_OPCODE => (ZwpTabletPadRingV2, LateInitObjectKey::new()), 365 | EVT_STRIP_OPCODE => (ZwpTabletPadStripV2, LateInitObjectKey::new()) 366 | ]); 367 | } 368 | -------------------------------------------------------------------------------- /src/clientside/xdg_activation.rs: -------------------------------------------------------------------------------- 1 | use smithay_client_toolkit::{ 2 | activation::{ActivationHandler, RequestData, RequestDataExt}, 3 | delegate_activation, 4 | }; 5 | use xcb::x; 6 | 7 | use crate::clientside::Globals; 8 | 9 | delegate_activation!(Globals, ActivationData); 10 | 11 | pub struct ActivationData { 12 | window: x::Window, 13 | data: RequestData, 14 | } 15 | 16 | impl ActivationData { 17 | pub fn new(window: x::Window, data: RequestData) -> Self { 18 | Self { window, data } 19 | } 20 | } 21 | 22 | impl RequestDataExt for ActivationData { 23 | fn app_id(&self) -> Option<&str> { 24 | self.data.app_id() 25 | } 26 | 27 | fn seat_and_serial(&self) -> Option<(&wayland_client::protocol::wl_seat::WlSeat, u32)> { 28 | self.data.seat_and_serial() 29 | } 30 | 31 | fn surface(&self) -> Option<&wayland_client::protocol::wl_surface::WlSurface> { 32 | self.data.surface() 33 | } 34 | } 35 | 36 | impl ActivationHandler for Globals { 37 | type RequestData = ActivationData; 38 | 39 | fn new_token(&mut self, token: String, data: &Self::RequestData) { 40 | self.pending_activations.push((data.window, token)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod clientside; 2 | mod server; 3 | pub mod xstate; 4 | 5 | use crate::server::{PendingSurfaceState, ServerState}; 6 | use crate::xstate::{RealConnection, XState}; 7 | use log::{error, info}; 8 | use rustix::event::{poll, PollFd, PollFlags}; 9 | use smithay_client_toolkit::data_device_manager::WritePipe; 10 | use std::io::{BufRead, BufReader, Read, Write}; 11 | use std::os::fd::{AsFd, AsRawFd, BorrowedFd}; 12 | use std::os::unix::net::UnixStream; 13 | use std::process::{Command, ExitStatus, Stdio}; 14 | use wayland_server::{Display, ListeningSocket}; 15 | use xcb::x; 16 | 17 | pub trait XConnection: Sized + 'static { 18 | type X11Selection: X11Selection; 19 | 20 | fn root_window(&self) -> x::Window; 21 | fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState); 22 | fn set_fullscreen(&mut self, window: x::Window, fullscreen: bool); 23 | fn focus_window(&mut self, window: x::Window, output_name: Option); 24 | fn close_window(&mut self, window: x::Window); 25 | fn unmap_window(&mut self, window: x::Window); 26 | fn raise_to_top(&mut self, window: x::Window); 27 | } 28 | 29 | pub trait X11Selection { 30 | fn mime_types(&self) -> Vec<&str>; 31 | fn write_to(&self, mime: &str, pipe: WritePipe); 32 | } 33 | 34 | type RealServerState = ServerState; 35 | 36 | pub trait RunData { 37 | fn display(&self) -> Option<&str>; 38 | fn server(&self) -> Option { 39 | None 40 | } 41 | fn created_server(&self) {} 42 | fn connected_server(&self) {} 43 | fn quit_rx(&self) -> Option { 44 | None 45 | } 46 | fn xwayland_ready(&self, _display: String, _pid: u32) {} 47 | } 48 | 49 | pub fn main(data: impl RunData) -> Option<()> { 50 | let mut version = env!("VERGEN_GIT_DESCRIBE"); 51 | if version == "VERGEN_IDEMPOTENT_OUTPUT" { 52 | version = env!("CARGO_PKG_VERSION"); 53 | } 54 | info!("Starting xwayland-satellite version {version}"); 55 | 56 | let socket = ListeningSocket::bind_auto("xwls", 1..=128).unwrap(); 57 | let mut display = Display::::new().unwrap(); 58 | let dh = display.handle(); 59 | data.created_server(); 60 | 61 | let mut server_state = RealServerState::new(dh, data.server()); 62 | 63 | let (xsock_wl, xsock_xwl) = UnixStream::pair().unwrap(); 64 | // Prevent creation of new Xwayland command from closing fd 65 | rustix::io::fcntl_setfd(&xsock_xwl, rustix::io::FdFlags::empty()).unwrap(); 66 | 67 | let (ready_tx, ready_rx) = UnixStream::pair().unwrap(); 68 | rustix::io::fcntl_setfd(&ready_tx, rustix::io::FdFlags::empty()).unwrap(); 69 | let mut xwayland = Command::new("Xwayland"); 70 | if let Some(display) = data.display() { 71 | xwayland.arg(display); 72 | } 73 | let mut xwayland = xwayland 74 | .args([ 75 | "-rootless", 76 | "-force-xrandr-emulation", 77 | "-wm", 78 | &xsock_xwl.as_raw_fd().to_string(), 79 | "-displayfd", 80 | &ready_tx.as_raw_fd().to_string(), 81 | ]) 82 | .env("WAYLAND_DISPLAY", socket.socket_name().unwrap()) 83 | .stderr(Stdio::piped()) 84 | .spawn() 85 | .unwrap(); 86 | 87 | let xwl_pid = xwayland.id(); 88 | 89 | let (mut finish_tx, mut finish_rx) = UnixStream::pair().unwrap(); 90 | let stderr = xwayland.stderr.take().unwrap(); 91 | std::thread::spawn(move || { 92 | let reader = BufReader::new(stderr); 93 | for line in reader.lines() { 94 | let line = line.unwrap(); 95 | info!(target: "xwayland_process", "{line}"); 96 | } 97 | let status = Box::new(xwayland.wait().unwrap()); 98 | let status = Box::into_raw(status) as usize; 99 | finish_tx.write_all(&status.to_ne_bytes()).unwrap(); 100 | }); 101 | 102 | let mut ready_fds = [ 103 | PollFd::new(&socket, PollFlags::IN), 104 | PollFd::new(&finish_rx, PollFlags::IN), 105 | ]; 106 | 107 | fn xwayland_exit_code(rx: &mut UnixStream) -> Box { 108 | let mut data = [0; (usize::BITS / 8) as usize]; 109 | rx.read_exact(&mut data).unwrap(); 110 | let data = usize::from_ne_bytes(data); 111 | unsafe { Box::from_raw(data as *mut _) } 112 | } 113 | 114 | let connection = match poll(&mut ready_fds, -1) { 115 | Ok(_) => { 116 | if !ready_fds[1].revents().is_empty() { 117 | let status = xwayland_exit_code(&mut finish_rx); 118 | error!("Xwayland exited early with {status}"); 119 | return None; 120 | } 121 | 122 | data.connected_server(); 123 | socket.accept().unwrap().unwrap() 124 | } 125 | Err(e) => { 126 | panic!("first poll failed: {e:?}") 127 | } 128 | }; 129 | 130 | server_state.connect(connection); 131 | server_state.run(); 132 | 133 | let mut xstate: Option = None; 134 | 135 | // Remove the lifetimes on our fds to avoid borrowing issues, since we know they will exist for 136 | // the rest of our program anyway 137 | let server_fd = unsafe { BorrowedFd::borrow_raw(server_state.clientside_fd().as_raw_fd()) }; 138 | let display_fd = unsafe { BorrowedFd::borrow_raw(display.backend().poll_fd().as_raw_fd()) }; 139 | 140 | // `finish_rx` only writes the status code of `Xwayland` exiting, so it is reasonable to use as 141 | // the UnixStream of choice when not running the integration tests. 142 | let mut quit_rx = data.quit_rx().unwrap_or(finish_rx); 143 | 144 | let mut fds = [ 145 | PollFd::from_borrowed_fd(server_fd, PollFlags::IN), 146 | PollFd::new(&xsock_wl, PollFlags::IN), 147 | PollFd::from_borrowed_fd(display_fd, PollFlags::IN), 148 | PollFd::new(&ready_rx, PollFlags::IN), 149 | PollFd::new(&quit_rx, PollFlags::IN), 150 | ]; 151 | 152 | let mut ready = false; 153 | loop { 154 | match poll(&mut fds, -1) { 155 | Ok(_) => { 156 | if !fds[3].revents().is_empty() { 157 | ready = true; 158 | } 159 | if !fds[4].revents().is_empty() { 160 | let status = xwayland_exit_code(&mut quit_rx); 161 | if *status != ExitStatus::default() { 162 | error!("Xwayland exited early with {status}"); 163 | } 164 | return None; 165 | } 166 | } 167 | Err(other) => panic!("Poll failed: {other:?}"), 168 | } 169 | 170 | if xstate.is_none() && ready { 171 | let xstate = xstate.insert(XState::new(xsock_wl.as_fd())); 172 | let mut reader = BufReader::new(&ready_rx); 173 | let mut display = String::new(); 174 | reader.read_line(&mut display).unwrap(); 175 | display.pop(); 176 | display.insert(0, ':'); 177 | info!("Connected to Xwayland on {display}"); 178 | data.xwayland_ready(display, xwl_pid); 179 | xstate.server_state_setup(&mut server_state); 180 | 181 | #[cfg(feature = "systemd")] 182 | { 183 | match sd_notify::notify(true, &[sd_notify::NotifyState::Ready]) { 184 | Ok(()) => info!("Successfully notified systemd of ready state."), 185 | Err(e) => log::warn!("Systemd notify failed: {e:?}"), 186 | } 187 | } 188 | 189 | #[cfg(not(feature = "systemd"))] 190 | info!("Systemd support disabled."); 191 | } 192 | 193 | if let Some(xstate) = &mut xstate { 194 | xstate.handle_events(&mut server_state); 195 | } 196 | 197 | display.dispatch_clients(&mut server_state).unwrap(); 198 | server_state.run(); 199 | display.flush_clients().unwrap(); 200 | 201 | if let Some(xstate) = &mut xstate { 202 | if let Some(sel) = server_state.new_selection() { 203 | xstate.set_clipboard(sel); 204 | } 205 | 206 | if let Some(scale) = server_state.new_global_scale() { 207 | xstate.update_global_scale(scale); 208 | } 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | pretty_env_logger::formatted_timed_builder() 3 | .filter_level(log::LevelFilter::Info) 4 | .parse_default_env() 5 | .init(); 6 | xwayland_satellite::main(RealData(get_display())); 7 | } 8 | 9 | #[repr(transparent)] 10 | struct RealData(Option); 11 | impl xwayland_satellite::RunData for RealData { 12 | fn display(&self) -> Option<&str> { 13 | self.0.as_deref() 14 | } 15 | } 16 | 17 | fn get_display() -> Option { 18 | let mut args: Vec<_> = std::env::args().collect(); 19 | if args.len() > 2 { 20 | panic!("Unexpected arguments: {:?}", &args[2..]); 21 | } 22 | 23 | (args.len() == 2).then(|| args.swap_remove(1)) 24 | } 25 | -------------------------------------------------------------------------------- /src/xstate/mod.rs: -------------------------------------------------------------------------------- 1 | mod settings; 2 | use settings::Settings; 3 | mod selection; 4 | use selection::{Selection, SelectionData}; 5 | use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1; 6 | 7 | use crate::XConnection; 8 | use bitflags::bitflags; 9 | use log::{debug, trace, warn}; 10 | use std::collections::HashMap; 11 | use std::ffi::CString; 12 | use std::os::fd::{AsRawFd, BorrowedFd}; 13 | use std::rc::Rc; 14 | use xcb::{x, Xid, XidNew}; 15 | use xcb_util_cursor::{Cursor, CursorContext}; 16 | 17 | // Sometimes we'll get events on windows that have already been destroyed 18 | #[derive(Debug)] 19 | enum MaybeBadWindow { 20 | BadWindow, 21 | Other(xcb::Error), 22 | } 23 | impl From for MaybeBadWindow { 24 | fn from(value: xcb::Error) -> Self { 25 | match value { 26 | xcb::Error::Protocol(xcb::ProtocolError::X( 27 | x::Error::Window(_) | x::Error::Drawable(_), 28 | _, 29 | )) => Self::BadWindow, 30 | other => Self::Other(other), 31 | } 32 | } 33 | } 34 | impl From for MaybeBadWindow { 35 | fn from(value: xcb::ProtocolError) -> Self { 36 | match value { 37 | xcb::ProtocolError::X(x::Error::Window(_) | x::Error::Drawable(_), _) => { 38 | Self::BadWindow 39 | } 40 | other => Self::Other(xcb::Error::Protocol(other)), 41 | } 42 | } 43 | } 44 | 45 | type XResult = Result; 46 | macro_rules! unwrap_or_skip_bad_window { 47 | ($err:expr) => { 48 | match $err { 49 | Ok(v) => v, 50 | Err(e) => { 51 | let err = MaybeBadWindow::from(e); 52 | match err { 53 | MaybeBadWindow::BadWindow => return, 54 | MaybeBadWindow::Other(other) => panic!("X11 protocol error: {other:?}"), 55 | } 56 | } 57 | } 58 | }; 59 | } 60 | 61 | /// Essentially a trait alias. 62 | trait PropertyResolver { 63 | type Output; 64 | fn resolve(self, reply: x::GetPropertyReply) -> Self::Output; 65 | } 66 | impl PropertyResolver for T 67 | where 68 | T: FnOnce(x::GetPropertyReply) -> Output, 69 | { 70 | type Output = Output; 71 | fn resolve(self, reply: x::GetPropertyReply) -> Self::Output { 72 | (self)(reply) 73 | } 74 | } 75 | 76 | struct PropertyCookieWrapper<'a, F: PropertyResolver> { 77 | connection: &'a xcb::Connection, 78 | cookie: x::GetPropertyCookie, 79 | resolver: F, 80 | } 81 | 82 | impl PropertyCookieWrapper<'_, F> { 83 | /// Get the result from our property cookie. 84 | fn resolve(self) -> XResult> { 85 | let reply = self.connection.wait_for_reply(self.cookie)?; 86 | if reply.r#type() == x::ATOM_NONE { 87 | Ok(None) 88 | } else { 89 | Ok(Some(self.resolver.resolve(reply))) 90 | } 91 | } 92 | } 93 | 94 | #[derive(Debug)] 95 | pub enum WmName { 96 | WmName(String), 97 | NetWmName(String), 98 | } 99 | 100 | impl WmName { 101 | pub fn name(&self) -> &str { 102 | match self { 103 | Self::WmName(n) => n, 104 | Self::NetWmName(n) => n, 105 | } 106 | } 107 | } 108 | 109 | pub struct XState { 110 | connection: Rc, 111 | atoms: Atoms, 112 | window_atoms: WindowTypes, 113 | root: x::Window, 114 | wm_window: x::Window, 115 | selection_data: SelectionData, 116 | settings: Settings, 117 | } 118 | 119 | impl XState { 120 | pub fn new(fd: BorrowedFd) -> Self { 121 | let connection = Rc::new( 122 | xcb::Connection::connect_to_fd_with_extensions( 123 | fd.as_raw_fd(), 124 | None, 125 | &[ 126 | xcb::Extension::Composite, 127 | xcb::Extension::RandR, 128 | xcb::Extension::XFixes, 129 | xcb::Extension::Res, 130 | ], 131 | &[], 132 | ) 133 | .unwrap(), 134 | ); 135 | let setup = connection.get_setup(); 136 | let screen = setup.roots().next().unwrap(); 137 | let root = screen.root(); 138 | 139 | connection 140 | .send_and_check_request(&x::ChangeWindowAttributes { 141 | window: root, 142 | value_list: &[x::Cw::EventMask( 143 | x::EventMask::SUBSTRUCTURE_REDIRECT // To have Xwayland send us WL_SURFACE_ID 144 | | x::EventMask::SUBSTRUCTURE_NOTIFY // To get notified whenever new windows are created 145 | | x::EventMask::RESIZE_REDIRECT, 146 | )], 147 | }) 148 | .unwrap(); 149 | 150 | let atoms = Atoms::intern_all(&connection).unwrap(); 151 | trace!("atoms: {atoms:#?}"); 152 | 153 | // This makes Xwayland spit out damage tracking 154 | connection 155 | .send_and_check_request(&xcb::composite::RedirectSubwindows { 156 | window: screen.root(), 157 | update: xcb::composite::Redirect::Manual, 158 | }) 159 | .unwrap(); 160 | 161 | // Track RandR output changes 162 | connection 163 | .send_and_check_request(&xcb::randr::SelectInput { 164 | window: root, 165 | enable: xcb::randr::NotifyMask::RESOURCE_CHANGE, 166 | }) 167 | .unwrap(); 168 | 169 | // negotiate xfixes version 170 | let reply = connection 171 | .wait_for_reply(connection.send_request(&xcb::xfixes::QueryVersion { 172 | client_major_version: 1, 173 | client_minor_version: 0, 174 | })) 175 | .unwrap(); 176 | log::info!( 177 | "xfixes version: {}.{}", 178 | reply.major_version(), 179 | reply.minor_version() 180 | ); 181 | use xcb::xfixes::SelectionEventMask; 182 | connection 183 | .send_and_check_request(&xcb::xfixes::SelectSelectionInput { 184 | window: root, 185 | selection: atoms.clipboard, 186 | event_mask: SelectionEventMask::SET_SELECTION_OWNER 187 | | SelectionEventMask::SELECTION_WINDOW_DESTROY 188 | | SelectionEventMask::SELECTION_CLIENT_CLOSE, 189 | }) 190 | .unwrap(); 191 | connection 192 | .send_and_check_request(&xcb::xfixes::SelectSelectionInput { 193 | window: root, 194 | selection: atoms.xsettings, 195 | event_mask: SelectionEventMask::SELECTION_WINDOW_DESTROY 196 | | SelectionEventMask::SELECTION_CLIENT_CLOSE, 197 | }) 198 | .unwrap(); 199 | { 200 | // Setup default cursor theme 201 | let ctx = CursorContext::new(&connection, screen).unwrap(); 202 | let left_ptr = ctx.load_cursor(Cursor::LeftPtr); 203 | connection 204 | .send_and_check_request(&x::ChangeWindowAttributes { 205 | window: root, 206 | value_list: &[x::Cw::Cursor(left_ptr)], 207 | }) 208 | .unwrap(); 209 | } 210 | 211 | let wm_window = connection.generate_id(); 212 | let selection_data = SelectionData::new(&connection, root); 213 | let window_atoms = WindowTypes::intern_all(&connection).unwrap(); 214 | let settings = Settings::new(&connection, &atoms, root); 215 | 216 | let mut r = Self { 217 | connection, 218 | wm_window, 219 | root, 220 | atoms, 221 | window_atoms, 222 | selection_data, 223 | settings, 224 | }; 225 | r.create_ewmh_window(); 226 | r.set_xsettings_owner(); 227 | r 228 | } 229 | 230 | pub fn server_state_setup(&self, server_state: &mut super::RealServerState) { 231 | let mut c = RealConnection::new(self.connection.clone(), self.atoms.clone()); 232 | c.update_outputs(self.root); 233 | server_state.set_x_connection(c); 234 | } 235 | 236 | fn set_root_property(&self, property: x::Atom, r#type: x::Atom, data: &[P]) { 237 | self.connection 238 | .send_and_check_request(&x::ChangeProperty { 239 | mode: x::PropMode::Replace, 240 | window: self.root, 241 | property, 242 | r#type, 243 | data, 244 | }) 245 | .unwrap(); 246 | } 247 | 248 | fn create_ewmh_window(&mut self) { 249 | self.connection 250 | .send_and_check_request(&x::CreateWindow { 251 | depth: 0, 252 | wid: self.wm_window, 253 | parent: self.root, 254 | x: 0, 255 | y: 0, 256 | width: 1, 257 | height: 1, 258 | border_width: 0, 259 | class: x::WindowClass::InputOnly, 260 | visual: x::COPY_FROM_PARENT, 261 | value_list: &[], 262 | }) 263 | .unwrap(); 264 | 265 | self.set_root_property(self.atoms.wm_check, x::ATOM_WINDOW, &[self.wm_window]); 266 | self.set_root_property(self.atoms.active_win, x::ATOM_WINDOW, &[x::Window::none()]); 267 | self.set_root_property( 268 | self.atoms.supported, 269 | x::ATOM_ATOM, 270 | &[ 271 | self.atoms.active_win, 272 | self.atoms.motif_wm_hints, 273 | self.atoms.net_wm_state, 274 | self.atoms.wm_fullscreen, 275 | ], 276 | ); 277 | 278 | self.connection 279 | .send_and_check_request(&x::ChangeProperty { 280 | mode: x::PropMode::Replace, 281 | window: self.wm_window, 282 | property: self.atoms.wm_check, 283 | r#type: x::ATOM_WINDOW, 284 | data: &[self.wm_window], 285 | }) 286 | .unwrap(); 287 | 288 | self.connection 289 | .send_and_check_request(&x::ChangeProperty { 290 | mode: x::PropMode::Replace, 291 | window: self.wm_window, 292 | property: self.atoms.net_wm_name, 293 | r#type: x::ATOM_STRING, 294 | data: b"xwayland-satellite", 295 | }) 296 | .unwrap(); 297 | } 298 | 299 | pub fn handle_events(&mut self, server_state: &mut super::RealServerState) { 300 | macro_rules! unwrap_or_skip_bad_window_cont { 301 | ($err:expr) => { 302 | match $err { 303 | Ok(v) => v, 304 | Err(e) => { 305 | let err = MaybeBadWindow::from(e); 306 | match err { 307 | MaybeBadWindow::BadWindow => continue, 308 | MaybeBadWindow::Other(other) => panic!("X11 protocol error: {other:?}"), 309 | } 310 | } 311 | } 312 | }; 313 | } 314 | 315 | let mut ignored_windows = Vec::new(); 316 | while let Some(event) = self.connection.poll_for_event().unwrap() { 317 | trace!("x11 event: {event:?}"); 318 | 319 | if self.handle_selection_event(&event, server_state) { 320 | continue; 321 | } 322 | 323 | match event { 324 | xcb::Event::X(x::Event::CreateNotify(e)) => { 325 | debug!("new window: {:?}", e); 326 | server_state.new_window( 327 | e.window(), 328 | e.override_redirect(), 329 | (&e).into(), 330 | self.get_pid(e.window()), 331 | ); 332 | } 333 | xcb::Event::X(x::Event::ReparentNotify(e)) => { 334 | debug!("reparent event: {e:?}"); 335 | if e.parent() == self.root { 336 | let geometry = self.connection.send_request(&x::GetGeometry { 337 | drawable: x::Drawable::Window(e.window()), 338 | }); 339 | let attrs = self 340 | .connection 341 | .send_request(&x::GetWindowAttributes { window: e.window() }); 342 | let geometry = unwrap_or_skip_bad_window_cont!(self 343 | .connection 344 | .wait_for_reply(geometry)); 345 | let attrs = 346 | unwrap_or_skip_bad_window_cont!(self.connection.wait_for_reply(attrs)); 347 | 348 | server_state.new_window( 349 | e.window(), 350 | attrs.override_redirect(), 351 | WindowDims { 352 | x: geometry.x(), 353 | y: geometry.y(), 354 | width: geometry.width(), 355 | height: geometry.height(), 356 | }, 357 | self.get_pid(e.window()), 358 | ); 359 | } else { 360 | debug!("destroying window since its parent is no longer root!"); 361 | server_state.destroy_window(e.window()); 362 | ignored_windows.push(e.window()); 363 | } 364 | } 365 | xcb::Event::X(x::Event::MapRequest(e)) => { 366 | debug!("requested to map {:?}", e.window()); 367 | unwrap_or_skip_bad_window_cont!(self 368 | .connection 369 | .send_and_check_request(&x::MapWindow { window: e.window() })); 370 | } 371 | xcb::Event::X(x::Event::MapNotify(e)) => { 372 | unwrap_or_skip_bad_window_cont!(self.connection.send_and_check_request( 373 | &x::ChangeWindowAttributes { 374 | window: e.window(), 375 | value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], 376 | } 377 | )); 378 | unwrap_or_skip_bad_window_cont!( 379 | self.handle_window_properties(server_state, e.window()) 380 | ); 381 | server_state.map_window(e.window()); 382 | } 383 | xcb::Event::X(x::Event::ConfigureNotify(e)) => { 384 | server_state.reconfigure_window(e); 385 | } 386 | xcb::Event::X(x::Event::UnmapNotify(e)) => { 387 | trace!("unmap event: {:?}", e.event()); 388 | server_state.unmap_window(e.window()); 389 | let active_win = self 390 | .connection 391 | .wait_for_reply(self.get_property_cookie( 392 | self.root, 393 | self.atoms.active_win, 394 | x::ATOM_WINDOW, 395 | 1, 396 | )) 397 | .unwrap(); 398 | 399 | let active_win: &[x::Window] = active_win.value(); 400 | if active_win[0] == e.window() { 401 | // The connection on the server state stores state. 402 | server_state 403 | .connection 404 | .as_mut() 405 | .unwrap() 406 | .focus_window(x::Window::none(), None); 407 | } 408 | 409 | unwrap_or_skip_bad_window_cont!(self.connection.send_and_check_request( 410 | &x::ChangeWindowAttributes { 411 | window: e.window(), 412 | value_list: &[x::Cw::EventMask(x::EventMask::empty())], 413 | } 414 | )); 415 | } 416 | xcb::Event::X(x::Event::DestroyNotify(e)) => { 417 | debug!("destroying window {:?}", e.window()); 418 | server_state.destroy_window(e.window()); 419 | } 420 | xcb::Event::X(x::Event::PropertyNotify(e)) => { 421 | if ignored_windows.contains(&e.window()) { 422 | continue; 423 | } 424 | self.handle_property_change(e, server_state); 425 | } 426 | xcb::Event::X(x::Event::ConfigureRequest(e)) => { 427 | debug!("{:?} request: {:?}", e.window(), e.value_mask()); 428 | 429 | let mut list = Vec::new(); 430 | let mask = e.value_mask(); 431 | 432 | if server_state.can_change_position(e.window()) { 433 | if mask.contains(x::ConfigWindowMask::X) { 434 | list.push(x::ConfigWindow::X(e.x().into())); 435 | } 436 | if mask.contains(x::ConfigWindowMask::Y) { 437 | list.push(x::ConfigWindow::Y(e.y().into())); 438 | } 439 | } 440 | if mask.contains(x::ConfigWindowMask::WIDTH) { 441 | list.push(x::ConfigWindow::Width(e.width().into())); 442 | } 443 | if mask.contains(x::ConfigWindowMask::HEIGHT) { 444 | list.push(x::ConfigWindow::Height(e.height().into())); 445 | } 446 | 447 | unwrap_or_skip_bad_window_cont!(self.connection.send_and_check_request( 448 | &x::ConfigureWindow { 449 | window: e.window(), 450 | value_list: &list, 451 | } 452 | )); 453 | } 454 | xcb::Event::X(x::Event::ClientMessage(e)) => match e.r#type() { 455 | x if x == self.atoms.wl_surface_id => { 456 | panic!(concat!( 457 | "Xserver should be using WL_SURFACE_SERIAL, not WL_SURFACE_ID\n", 458 | "Your Xwayland is likely too old, it should be version 23.1 or greater." 459 | )); 460 | } 461 | x if x == self.atoms.wl_surface_serial => { 462 | let x::ClientMessageData::Data32(data) = e.data() else { 463 | unreachable!(); 464 | }; 465 | server_state.set_window_serial(e.window(), [data[0], data[1]]); 466 | } 467 | x if x == self.atoms.net_wm_state => { 468 | let x::ClientMessageData::Data32(data) = e.data() else { 469 | unreachable!(); 470 | }; 471 | let Ok(action) = SetState::try_from(data[0]) else { 472 | warn!("unknown action for _NET_WM_STATE: {}", data[0]); 473 | continue; 474 | }; 475 | let prop1 = unsafe { x::Atom::new(data[1]) }; 476 | let prop2 = unsafe { x::Atom::new(data[2]) }; 477 | 478 | trace!("_NET_WM_STATE ({action:?}) props: {prop1:?} {prop2:?}"); 479 | 480 | for prop in [prop1, prop2] { 481 | match prop { 482 | x if x == self.atoms.wm_fullscreen => { 483 | server_state.set_fullscreen(e.window(), action); 484 | } 485 | _ => {} 486 | } 487 | } 488 | } 489 | x if x == self.atoms.active_win => { 490 | server_state.activate_window(e.window()); 491 | } 492 | t => warn!("unrecognized message: {t:?}"), 493 | }, 494 | xcb::Event::X(x::Event::MappingNotify(_)) => {} 495 | xcb::Event::RandR(xcb::randr::Event::Notify(e)) 496 | if matches!(e.u(), xcb::randr::NotifyData::Rc(_)) => 497 | { 498 | server_state 499 | .connection 500 | .as_mut() 501 | .unwrap() 502 | .update_outputs(self.root); 503 | } 504 | other => { 505 | warn!("unhandled event: {other:?}"); 506 | } 507 | } 508 | 509 | server_state.run(); 510 | } 511 | } 512 | 513 | fn handle_window_properties( 514 | &self, 515 | server_state: &mut super::RealServerState, 516 | window: x::Window, 517 | ) -> XResult<()> { 518 | let name = self.get_net_wm_name(window); 519 | let class = self.get_wm_class(window); 520 | let size_hints = self.get_wm_size_hints(window); 521 | let motif_wm_hints = self.get_motif_wm_hints(window); 522 | let mut title = name.resolve()?; 523 | if title.is_none() { 524 | title = self.get_wm_name(window).resolve()?; 525 | } 526 | 527 | if let Some(name) = title { 528 | server_state.set_win_title(window, name); 529 | } 530 | if let Some(class) = class.resolve()? { 531 | server_state.set_win_class(window, class); 532 | } 533 | if let Some(hints) = size_hints.resolve()? { 534 | server_state.set_size_hints(window, hints); 535 | } 536 | 537 | let motif_hints = motif_wm_hints.resolve()?; 538 | if let Some(decorations) = motif_hints.as_ref().and_then(|m| m.decorations) { 539 | server_state.set_win_decorations(window, decorations); 540 | } 541 | 542 | let transient_for = self 543 | .property_cookie_wrapper( 544 | window, 545 | self.atoms.wm_transient_for, 546 | x::ATOM_WINDOW, 547 | 1, 548 | |reply: x::GetPropertyReply| reply.value::().first().copied(), 549 | ) 550 | .resolve()? 551 | .flatten(); 552 | 553 | let is_popup = self.guess_is_popup(window, motif_hints, transient_for.is_some())?; 554 | server_state.set_popup(window, is_popup); 555 | if let Some(parent) = transient_for.and_then(|t| (!is_popup).then_some(t)) { 556 | server_state.set_transient_for(window, parent); 557 | } 558 | 559 | Ok(()) 560 | } 561 | 562 | fn property_cookie_wrapper( 563 | &self, 564 | window: x::Window, 565 | property: x::Atom, 566 | ty: x::Atom, 567 | len: u32, 568 | resolver: F, 569 | ) -> PropertyCookieWrapper { 570 | PropertyCookieWrapper { 571 | connection: &self.connection, 572 | cookie: self.get_property_cookie(window, property, ty, len), 573 | resolver, 574 | } 575 | } 576 | 577 | fn guess_is_popup( 578 | &self, 579 | window: x::Window, 580 | motif_hints: Option, 581 | has_transient_for: bool, 582 | ) -> XResult { 583 | if let Some(hints) = motif_hints { 584 | // If the motif hints indicate the user shouldn't be able to do anything 585 | // to the window at all, it stands to reason it's probably a popup. 586 | if hints.functions.is_some_and(|f| f.is_empty()) { 587 | return Ok(true); 588 | } 589 | } 590 | 591 | let attrs = self 592 | .connection 593 | .send_request(&x::GetWindowAttributes { window }); 594 | 595 | let atoms_vec = |reply: x::GetPropertyReply| reply.value::().to_vec(); 596 | let window_types = 597 | self.property_cookie_wrapper(window, self.window_atoms.ty, x::ATOM_ATOM, 10, atoms_vec); 598 | let window_state = self.property_cookie_wrapper( 599 | window, 600 | self.atoms.net_wm_state, 601 | x::ATOM_ATOM, 602 | 10, 603 | atoms_vec, 604 | ); 605 | 606 | let override_redirect = self.connection.wait_for_reply(attrs)?.override_redirect(); 607 | let mut is_popup = override_redirect; 608 | 609 | let window_types = window_types.resolve()?.unwrap_or_else(|| { 610 | if !override_redirect && has_transient_for { 611 | vec![self.window_atoms.dialog] 612 | } else { 613 | vec![self.window_atoms.normal] 614 | } 615 | }); 616 | 617 | if log::log_enabled!(log::Level::Debug) { 618 | let win_types = window_types 619 | .iter() 620 | .copied() 621 | .map(|t| get_atom_name(&self.connection, t)) 622 | .collect::>(); 623 | 624 | debug!("{window:?} window_types: {win_types:?}"); 625 | } 626 | debug!("{window:?} override_redirect: {override_redirect:?}"); 627 | 628 | let mut known_window_type = false; 629 | for ty in window_types { 630 | match ty { 631 | x if x == self.window_atoms.normal || x == self.window_atoms.dialog => { 632 | is_popup = override_redirect; 633 | } 634 | x if x == self.window_atoms.menu || x == self.window_atoms.tooltip => { 635 | is_popup = true; 636 | } 637 | _ => { 638 | continue; 639 | } 640 | } 641 | 642 | known_window_type = true; 643 | break; 644 | } 645 | 646 | if !known_window_type { 647 | if let Some(states) = window_state.resolve()? { 648 | is_popup = states.contains(&self.atoms.skip_taskbar); 649 | } 650 | } 651 | 652 | Ok(is_popup) 653 | } 654 | 655 | fn get_property_cookie( 656 | &self, 657 | window: x::Window, 658 | property: x::Atom, 659 | ty: x::Atom, 660 | long_length: u32, 661 | ) -> x::GetPropertyCookie { 662 | self.connection.send_request(&x::GetProperty { 663 | delete: false, 664 | window, 665 | property, 666 | r#type: ty, 667 | long_offset: 0, 668 | long_length, 669 | }) 670 | } 671 | 672 | fn get_wm_class( 673 | &self, 674 | window: x::Window, 675 | ) -> PropertyCookieWrapper> { 676 | let cookie = self.get_property_cookie(window, x::ATOM_WM_CLASS, x::ATOM_STRING, 256); 677 | let resolver = move |reply: x::GetPropertyReply| { 678 | let data: &[u8] = reply.value(); 679 | trace!("wm class data: {data:?}"); 680 | // wm class (normally) is instance + class - ignore instance 681 | let class_start = if let Some(p) = data.iter().copied().position(|b| b == 0u8) { 682 | p + 1 683 | } else { 684 | 0 685 | }; 686 | let mut data = data[class_start..].to_vec(); 687 | if data.last().copied() != Some(0) { 688 | data.push(0); 689 | } 690 | let class = CString::from_vec_with_nul(data).unwrap(); 691 | trace!("{:?} class: {class:?}", window); 692 | class.to_string_lossy().to_string() 693 | }; 694 | PropertyCookieWrapper { 695 | connection: &self.connection, 696 | cookie, 697 | resolver, 698 | } 699 | } 700 | 701 | fn get_wm_name( 702 | &self, 703 | window: x::Window, 704 | ) -> PropertyCookieWrapper> { 705 | let cookie = self.get_property_cookie(window, x::ATOM_WM_NAME, x::ATOM_STRING, 256); 706 | let resolver = |reply: x::GetPropertyReply| { 707 | let data: &[u8] = reply.value(); 708 | // strip trailing zeros or wayland-rs will lose its mind 709 | // https://github.com/Smithay/wayland-rs/issues/748 710 | let data = data.split(|byte| *byte == 0).next().unwrap(); 711 | let name = String::from_utf8_lossy(data).to_string(); 712 | WmName::WmName(name) 713 | }; 714 | 715 | PropertyCookieWrapper { 716 | connection: &self.connection, 717 | cookie, 718 | resolver, 719 | } 720 | } 721 | 722 | fn get_net_wm_name( 723 | &self, 724 | window: x::Window, 725 | ) -> PropertyCookieWrapper> { 726 | let cookie = 727 | self.get_property_cookie(window, self.atoms.net_wm_name, self.atoms.utf8_string, 256); 728 | let resolver = |reply: x::GetPropertyReply| { 729 | let data: &[u8] = reply.value(); 730 | let data = data.split(|byte| *byte == 0).next().unwrap(); 731 | let name = String::from_utf8_lossy(data).to_string(); 732 | WmName::NetWmName(name) 733 | }; 734 | 735 | PropertyCookieWrapper { 736 | connection: &self.connection, 737 | cookie, 738 | resolver, 739 | } 740 | } 741 | 742 | fn get_wm_hints( 743 | &self, 744 | window: x::Window, 745 | ) -> PropertyCookieWrapper> { 746 | let cookie = self.get_property_cookie(window, x::ATOM_WM_HINTS, x::ATOM_WM_HINTS, 9); 747 | let resolver = |reply: x::GetPropertyReply| { 748 | let data: &[u32] = reply.value(); 749 | let hints = WmHints::from(data); 750 | trace!("wm hints: {hints:?}"); 751 | hints 752 | }; 753 | PropertyCookieWrapper { 754 | connection: &self.connection, 755 | cookie, 756 | resolver, 757 | } 758 | } 759 | 760 | fn get_wm_size_hints( 761 | &self, 762 | window: x::Window, 763 | ) -> PropertyCookieWrapper> { 764 | let cookie = 765 | self.get_property_cookie(window, x::ATOM_WM_NORMAL_HINTS, x::ATOM_WM_SIZE_HINTS, 9); 766 | let resolver = |reply: x::GetPropertyReply| { 767 | let data: &[u32] = reply.value(); 768 | WmNormalHints::from(data) 769 | }; 770 | 771 | PropertyCookieWrapper { 772 | connection: &self.connection, 773 | cookie, 774 | resolver, 775 | } 776 | } 777 | 778 | fn get_motif_wm_hints( 779 | &self, 780 | window: x::Window, 781 | ) -> PropertyCookieWrapper> { 782 | let cookie = self.get_property_cookie( 783 | window, 784 | self.atoms.motif_wm_hints, 785 | self.atoms.motif_wm_hints, 786 | 5, 787 | ); 788 | let resolver = |reply: x::GetPropertyReply| { 789 | let data: &[u32] = reply.value(); 790 | motif::Hints::from(data) 791 | }; 792 | 793 | PropertyCookieWrapper { 794 | connection: &self.connection, 795 | cookie, 796 | resolver, 797 | } 798 | } 799 | 800 | fn get_pid(&self, window: x::Window) -> Option { 801 | let Some(pid) = self 802 | .connection 803 | .wait_for_reply(self.connection.send_request(&xcb::res::QueryClientIds { 804 | specs: &[xcb::res::ClientIdSpec { 805 | client: window.resource_id(), 806 | mask: xcb::res::ClientIdMask::LOCAL_CLIENT_PID, 807 | }], 808 | })) 809 | .ok() 810 | .and_then(|reply| Some(*reply.ids().next()?.value().first()?)) 811 | else { 812 | warn!("Failed to get pid of window: {window:?}"); 813 | return None; 814 | }; 815 | Some(pid) 816 | } 817 | 818 | fn handle_property_change( 819 | &mut self, 820 | event: x::PropertyNotifyEvent, 821 | server_state: &mut super::RealServerState, 822 | ) { 823 | if event.state() != x::Property::NewValue { 824 | debug!( 825 | "ignoring non newvalue for property {:?}", 826 | get_atom_name(&self.connection, event.atom()) 827 | ); 828 | return; 829 | } 830 | 831 | let window = event.window(); 832 | 833 | match event.atom() { 834 | x if x == x::ATOM_WM_HINTS => { 835 | let hints = 836 | unwrap_or_skip_bad_window!(self.get_wm_hints(window).resolve()).unwrap(); 837 | server_state.set_win_hints(window, hints); 838 | } 839 | x if x == x::ATOM_WM_NORMAL_HINTS => { 840 | let hints = 841 | unwrap_or_skip_bad_window!(self.get_wm_size_hints(window).resolve()).unwrap(); 842 | server_state.set_size_hints(window, hints); 843 | } 844 | x if x == x::ATOM_WM_NAME => { 845 | let name = unwrap_or_skip_bad_window!(self.get_wm_name(window).resolve()).unwrap(); 846 | server_state.set_win_title(window, name); 847 | } 848 | x if x == self.atoms.net_wm_name => { 849 | let name = 850 | unwrap_or_skip_bad_window!(self.get_net_wm_name(window).resolve()).unwrap(); 851 | server_state.set_win_title(window, name); 852 | } 853 | x if x == x::ATOM_WM_CLASS => { 854 | let class = 855 | unwrap_or_skip_bad_window!(self.get_wm_class(window).resolve()).unwrap(); 856 | server_state.set_win_class(window, class); 857 | } 858 | x if x == self.atoms.motif_wm_hints => { 859 | let motif_hints = 860 | unwrap_or_skip_bad_window!(self.get_motif_wm_hints(window).resolve()).unwrap(); 861 | if let Some(decorations) = motif_hints.decorations { 862 | server_state.set_win_decorations(window, decorations); 863 | } 864 | } 865 | _ => { 866 | if !self.handle_selection_property_change(&event) 867 | && log::log_enabled!(log::Level::Debug) 868 | { 869 | debug!( 870 | "changed property {:?} for {:?}", 871 | get_atom_name(&self.connection, event.atom()), 872 | window 873 | ); 874 | } 875 | } 876 | } 877 | } 878 | } 879 | 880 | xcb::atoms_struct! { 881 | #[derive(Clone, Debug)] 882 | struct Atoms { 883 | wl_surface_id => b"WL_SURFACE_ID" only_if_exists = false, 884 | wl_surface_serial => b"WL_SURFACE_SERIAL" only_if_exists = false, 885 | wm_protocols => b"WM_PROTOCOLS" only_if_exists = false, 886 | wm_delete_window => b"WM_DELETE_WINDOW" only_if_exists = false, 887 | wm_transient_for => b"WM_TRANSIENT_FOR" only_if_exists = false, 888 | wm_state => b"WM_STATE" only_if_exists = false, 889 | wm_check => b"_NET_SUPPORTING_WM_CHECK" only_if_exists = false, 890 | net_wm_name => b"_NET_WM_NAME" only_if_exists = false, 891 | wm_pid => b"_NET_WM_PID" only_if_exists = false, 892 | net_wm_state => b"_NET_WM_STATE" only_if_exists = false, 893 | wm_fullscreen => b"_NET_WM_STATE_FULLSCREEN" only_if_exists = false, 894 | skip_taskbar => b"_NET_WM_STATE_SKIP_TASKBAR" only_if_exists = false, 895 | active_win => b"_NET_ACTIVE_WINDOW" only_if_exists = false, 896 | client_list => b"_NET_CLIENT_LIST" only_if_exists = false, 897 | supported => b"_NET_SUPPORTED" only_if_exists = false, 898 | motif_wm_hints => b"_MOTIF_WM_HINTS" only_if_exists = false, 899 | utf8_string => b"UTF8_STRING" only_if_exists = false, 900 | clipboard => b"CLIPBOARD" only_if_exists = false, 901 | targets => b"TARGETS" only_if_exists = false, 902 | save_targets => b"SAVE_TARGETS" only_if_exists = false, 903 | multiple => b"MULTIPLE" only_if_exists = false, 904 | timestamp => b"TIMESTAMP" only_if_exists = false, 905 | selection_reply => b"_selection_reply" only_if_exists = false, 906 | incr => b"INCR" only_if_exists = false, 907 | xsettings => b"_XSETTINGS_S0" only_if_exists = false, 908 | xsettings_settings => b"_XSETTINGS_SETTINGS" only_if_exists = false, 909 | } 910 | } 911 | 912 | xcb::atoms_struct! { 913 | struct WindowTypes { 914 | ty => b"_NET_WM_WINDOW_TYPE" only_if_exists = false, 915 | normal => b"_NET_WM_WINDOW_TYPE_NORMAL" only_if_exists = false, 916 | dialog => b"_NET_WM_WINDOW_TYPE_DIALOG" only_if_exists = false, 917 | splash => b"_NET_WM_WINDOW_TYPE_SPLASH" only_if_exists = false, 918 | menu => b"_NET_WM_WINDOW_TYPE_MENU" only_if_exists = false, 919 | utility => b"_NET_WM_WINDOW_TYPE_UTILITY" only_if_exists = false, 920 | tooltip => b"_NET_WM_WINDOW_TYPE_TOOLTIP" only_if_exists = false, 921 | } 922 | } 923 | 924 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] 925 | pub struct WindowDims { 926 | pub x: i16, 927 | pub y: i16, 928 | pub width: u16, 929 | pub height: u16, 930 | } 931 | 932 | bitflags! { 933 | /// From ICCCM spec. 934 | /// https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.2.3 935 | pub struct WmSizeHintsFlags: u32 { 936 | const ProgramMinSize = 16; 937 | const ProgramMaxSize = 32; 938 | } 939 | } 940 | 941 | bitflags! { 942 | /// https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.2.4 943 | pub struct WmHintsFlags: u32 { 944 | const WindowGroup = 64; 945 | } 946 | } 947 | 948 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 949 | pub struct WinSize { 950 | pub width: i32, 951 | pub height: i32, 952 | } 953 | 954 | #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] 955 | pub struct WmNormalHints { 956 | pub min_size: Option, 957 | pub max_size: Option, 958 | } 959 | 960 | impl From<&[u32]> for WmNormalHints { 961 | fn from(value: &[u32]) -> Self { 962 | let mut ret = Self::default(); 963 | let flags = WmSizeHintsFlags::from_bits_truncate(value[0]); 964 | 965 | if flags.contains(WmSizeHintsFlags::ProgramMinSize) { 966 | ret.min_size = Some(WinSize { 967 | width: value[5] as _, 968 | height: value[6] as _, 969 | }); 970 | } 971 | 972 | if flags.contains(WmSizeHintsFlags::ProgramMaxSize) { 973 | ret.max_size = Some(WinSize { 974 | width: value[7] as _, 975 | height: value[8] as _, 976 | }); 977 | } 978 | 979 | ret 980 | } 981 | } 982 | 983 | #[derive(Default, Debug, PartialEq, Eq)] 984 | pub struct WmHints { 985 | pub window_group: Option, 986 | } 987 | 988 | impl From<&[u32]> for WmHints { 989 | fn from(value: &[u32]) -> Self { 990 | let mut ret = Self::default(); 991 | let flags = WmHintsFlags::from_bits_truncate(value[0]); 992 | 993 | if flags.contains(WmHintsFlags::WindowGroup) { 994 | let window = unsafe { x::Window::new(value[8]) }; 995 | ret.window_group = Some(window); 996 | } 997 | 998 | ret 999 | } 1000 | } 1001 | 1002 | pub use motif::Decorations; 1003 | mod motif { 1004 | use super::*; 1005 | // Motif WM hints are incredibly poorly documented, I could only find this header: 1006 | // https://www.opengroup.org/infosrv/openmotif/R2.1.30/motif/lib/Xm/MwmUtil.h 1007 | // and these random Perl docs: 1008 | // https://metacpan.org/pod/X11::Protocol::WM#_MOTIF_WM_HINTS 1009 | 1010 | bitflags! { 1011 | struct HintsFlags: u32 { 1012 | const Functions = 1; 1013 | const Decorations = 2; 1014 | } 1015 | } 1016 | 1017 | bitflags! { 1018 | pub(super) struct Functions: u32 { 1019 | const All = 1; 1020 | const Resize = 2; 1021 | const Move = 4; 1022 | const Minimize = 8; 1023 | const Maximize = 16; 1024 | const Close = 32; 1025 | } 1026 | } 1027 | 1028 | #[derive(Default)] 1029 | pub(super) struct Hints { 1030 | pub(super) functions: Option, 1031 | pub(super) decorations: Option, 1032 | } 1033 | 1034 | impl From<&[u32]> for Hints { 1035 | fn from(value: &[u32]) -> Self { 1036 | let mut ret = Self::default(); 1037 | 1038 | let flags = HintsFlags::from_bits_truncate(value[0]); 1039 | 1040 | if flags.contains(HintsFlags::Functions) { 1041 | ret.functions = Some(Functions::from_bits_truncate(value[1])); 1042 | } 1043 | if flags.contains(HintsFlags::Decorations) { 1044 | ret.decorations = value[2].try_into().ok(); 1045 | } 1046 | 1047 | ret 1048 | } 1049 | } 1050 | 1051 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 1052 | pub enum Decorations { 1053 | Client = 0, 1054 | Server = 1, 1055 | } 1056 | 1057 | impl TryFrom for Decorations { 1058 | type Error = (); 1059 | 1060 | fn try_from(value: u32) -> Result { 1061 | match value { 1062 | 0 => Ok(Self::Client), 1063 | 1 => Ok(Self::Server), 1064 | _ => Err(()), 1065 | } 1066 | } 1067 | } 1068 | 1069 | impl From for zxdg_toplevel_decoration_v1::Mode { 1070 | fn from(value: Decorations) -> Self { 1071 | match value { 1072 | Decorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide, 1073 | Decorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide, 1074 | } 1075 | } 1076 | } 1077 | } 1078 | 1079 | #[derive(Debug, Clone, Copy)] 1080 | pub enum SetState { 1081 | Remove, 1082 | Add, 1083 | Toggle, 1084 | } 1085 | 1086 | impl TryFrom for SetState { 1087 | type Error = (); 1088 | fn try_from(value: u32) -> Result { 1089 | match value { 1090 | 0 => Ok(Self::Remove), 1091 | 1 => Ok(Self::Add), 1092 | 2 => Ok(Self::Toggle), 1093 | _ => Err(()), 1094 | } 1095 | } 1096 | } 1097 | 1098 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 1099 | pub enum WmState { 1100 | Withdrawn = 0, 1101 | Normal = 1, 1102 | Iconic = 3, 1103 | } 1104 | 1105 | impl TryFrom for WmState { 1106 | type Error = (); 1107 | fn try_from(value: u32) -> Result { 1108 | match value { 1109 | 0 => Ok(Self::Withdrawn), 1110 | 1 => Ok(Self::Normal), 1111 | 3 => Ok(Self::Iconic), 1112 | _ => Err(()), 1113 | } 1114 | } 1115 | } 1116 | 1117 | pub struct RealConnection { 1118 | atoms: Atoms, 1119 | connection: Rc, 1120 | outputs: HashMap, 1121 | primary_output: xcb::randr::Output, 1122 | } 1123 | 1124 | impl RealConnection { 1125 | fn new(connection: Rc, atoms: Atoms) -> Self { 1126 | Self { 1127 | atoms, 1128 | connection, 1129 | outputs: Default::default(), 1130 | primary_output: Xid::none(), 1131 | } 1132 | } 1133 | 1134 | fn update_outputs(&mut self, root: x::Window) { 1135 | self.outputs.clear(); 1136 | let reply = self 1137 | .connection 1138 | .wait_for_reply( 1139 | self.connection 1140 | .send_request(&xcb::randr::GetScreenResources { window: root }), 1141 | ) 1142 | .expect("Couldn't grab screen resources"); 1143 | 1144 | for output in reply.outputs().iter().copied() { 1145 | let reply = self 1146 | .connection 1147 | .wait_for_reply(self.connection.send_request(&xcb::randr::GetOutputInfo { 1148 | output, 1149 | config_timestamp: reply.config_timestamp(), 1150 | })) 1151 | .expect("Couldn't get output info"); 1152 | 1153 | let name = std::str::from_utf8(reply.name()) 1154 | .unwrap_or_else(|_| panic!("couldn't parse output name: {:?}", reply.name())); 1155 | 1156 | self.outputs.insert(name.to_string(), output); 1157 | } 1158 | 1159 | self.primary_output = self 1160 | .connection 1161 | .wait_for_reply( 1162 | self.connection 1163 | .send_request(&xcb::randr::GetOutputPrimary { window: root }), 1164 | ) 1165 | .expect("Couldn't get primary output") 1166 | .output(); 1167 | 1168 | debug!( 1169 | "new outputs: {:?} | primary: {:?}", 1170 | self.outputs, self.primary_output 1171 | ); 1172 | } 1173 | } 1174 | 1175 | impl XConnection for RealConnection { 1176 | type X11Selection = Selection; 1177 | 1178 | fn root_window(&self) -> x::Window { 1179 | self.connection.get_setup().roots().next().unwrap().root() 1180 | } 1181 | 1182 | fn set_window_dims(&mut self, window: x::Window, dims: crate::server::PendingSurfaceState) { 1183 | trace!("set window dimensions {window:?} {dims:?}"); 1184 | unwrap_or_skip_bad_window!(self.connection.send_and_check_request(&x::ConfigureWindow { 1185 | window, 1186 | value_list: &[ 1187 | x::ConfigWindow::X(dims.x), 1188 | x::ConfigWindow::Y(dims.y), 1189 | x::ConfigWindow::Width(dims.width as _), 1190 | x::ConfigWindow::Height(dims.height as _), 1191 | ] 1192 | })); 1193 | } 1194 | 1195 | fn set_fullscreen(&mut self, window: x::Window, fullscreen: bool) { 1196 | let data = if fullscreen { 1197 | std::slice::from_ref(&self.atoms.wm_fullscreen) 1198 | } else { 1199 | &[] 1200 | }; 1201 | 1202 | if let Err(e) = self 1203 | .connection 1204 | .send_and_check_request(&x::ChangeProperty:: { 1205 | mode: x::PropMode::Replace, 1206 | window, 1207 | property: self.atoms.net_wm_state, 1208 | r#type: x::ATOM_ATOM, 1209 | data, 1210 | }) 1211 | { 1212 | warn!("Failed to set fullscreen state on {window:?} ({e})"); 1213 | } 1214 | } 1215 | 1216 | fn focus_window(&mut self, window: x::Window, output_name: Option) { 1217 | trace!("{window:?} {output_name:?}"); 1218 | if let Err(e) = self.connection.send_and_check_request(&x::SetInputFocus { 1219 | focus: window, 1220 | revert_to: x::InputFocus::None, 1221 | time: x::CURRENT_TIME, 1222 | }) { 1223 | debug!("SetInputFocus failed ({:?}: {:?})", window, e); 1224 | return; 1225 | } 1226 | if let Err(e) = self.connection.send_and_check_request(&x::ChangeProperty { 1227 | mode: x::PropMode::Replace, 1228 | window: self.root_window(), 1229 | property: self.atoms.active_win, 1230 | r#type: x::ATOM_WINDOW, 1231 | data: &[window], 1232 | }) { 1233 | debug!("ChangeProperty failed ({:?}: {:?})", window, e); 1234 | } 1235 | if let Err(e) = self.connection.send_and_check_request(&x::ChangeProperty { 1236 | mode: x::PropMode::Replace, 1237 | window, 1238 | property: self.atoms.wm_state, 1239 | r#type: self.atoms.wm_state, 1240 | data: &[WmState::Normal as u32, 0], 1241 | }) { 1242 | debug!("ChangeProperty failed ({:?}: {:?})", window, e); 1243 | } 1244 | 1245 | if let Some(name) = output_name { 1246 | let Some(output) = self.outputs.get(&name).copied() else { 1247 | warn!("Couldn't find output {name}, primary output will be wrong"); 1248 | return; 1249 | }; 1250 | if output == self.primary_output { 1251 | debug!("primary output is already {name}"); 1252 | return; 1253 | } 1254 | 1255 | if let Err(e) = self 1256 | .connection 1257 | .send_and_check_request(&xcb::randr::SetOutputPrimary { window, output }) 1258 | { 1259 | warn!("Couldn't set output {name} as primary: {e:?}"); 1260 | } else { 1261 | debug!("set {name} as primary output"); 1262 | self.primary_output = output; 1263 | } 1264 | } else { 1265 | let _ = self 1266 | .connection 1267 | .send_and_check_request(&xcb::randr::SetOutputPrimary { 1268 | window, 1269 | output: Xid::none(), 1270 | }); 1271 | self.primary_output = Xid::none(); 1272 | } 1273 | } 1274 | 1275 | fn close_window(&mut self, window: x::Window) { 1276 | let cookie = self.connection.send_request(&x::GetProperty { 1277 | window, 1278 | delete: false, 1279 | property: self.atoms.wm_protocols, 1280 | r#type: x::ATOM_ATOM, 1281 | long_offset: 0, 1282 | long_length: 10, 1283 | }); 1284 | let reply = unwrap_or_skip_bad_window!(self.connection.wait_for_reply(cookie)); 1285 | 1286 | if reply 1287 | .value::() 1288 | .contains(&self.atoms.wm_delete_window) 1289 | { 1290 | let data = [self.atoms.wm_delete_window.resource_id(), 0, 0, 0, 0]; 1291 | let event = &x::ClientMessageEvent::new( 1292 | window, 1293 | self.atoms.wm_protocols, 1294 | x::ClientMessageData::Data32(data), 1295 | ); 1296 | 1297 | unwrap_or_skip_bad_window!(self.connection.send_and_check_request(&x::SendEvent { 1298 | destination: x::SendEventDest::Window(window), 1299 | propagate: false, 1300 | event_mask: x::EventMask::empty(), 1301 | event, 1302 | })); 1303 | } else { 1304 | unwrap_or_skip_bad_window!(self.connection.send_and_check_request(&x::KillClient { 1305 | resource: window.resource_id() 1306 | })) 1307 | } 1308 | } 1309 | 1310 | fn unmap_window(&mut self, window: x::Window) { 1311 | unwrap_or_skip_bad_window!(self 1312 | .connection 1313 | .send_and_check_request(&x::UnmapWindow { window })); 1314 | } 1315 | 1316 | fn raise_to_top(&mut self, window: x::Window) { 1317 | unwrap_or_skip_bad_window!(self.connection.send_and_check_request(&x::ConfigureWindow { 1318 | window, 1319 | value_list: &[x::ConfigWindow::StackMode(x::StackMode::Above)], 1320 | })); 1321 | } 1322 | } 1323 | 1324 | fn get_atom_name(connection: &xcb::Connection, atom: x::Atom) -> String { 1325 | match connection.wait_for_reply(connection.send_request(&x::GetAtomName { atom })) { 1326 | Ok(reply) => reply.name().to_string(), 1327 | Err(err) => format!(" {atom:?}"), 1328 | } 1329 | } 1330 | -------------------------------------------------------------------------------- /src/xstate/selection.rs: -------------------------------------------------------------------------------- 1 | use super::{get_atom_name, XState}; 2 | use crate::server::ForeignSelection; 3 | use crate::{RealServerState, X11Selection}; 4 | use log::{debug, error, warn}; 5 | use smithay_client_toolkit::data_device_manager::WritePipe; 6 | use std::cell::RefCell; 7 | use std::io::Write; 8 | use std::rc::Rc; 9 | use xcb::x; 10 | 11 | #[derive(Debug)] 12 | struct SelectionTargetId { 13 | name: String, 14 | atom: x::Atom, 15 | source: Option, 16 | } 17 | 18 | struct PendingSelectionData { 19 | target: x::Atom, 20 | pipe: WritePipe, 21 | incr: bool, 22 | } 23 | 24 | pub struct Selection { 25 | mimes: Vec, 26 | connection: Rc, 27 | window: x::Window, 28 | pending: RefCell>, 29 | clipboard: x::Atom, 30 | selection_time: u32, 31 | incr: x::Atom, 32 | } 33 | 34 | impl X11Selection for Selection { 35 | fn mime_types(&self) -> Vec<&str> { 36 | self.mimes 37 | .iter() 38 | .map(|target| target.name.as_str()) 39 | .collect() 40 | } 41 | 42 | fn write_to(&self, mime: &str, pipe: WritePipe) { 43 | if let Some(target) = self.mimes.iter().find(|target| target.name == mime) { 44 | // We use the target as the property to write to 45 | if let Err(e) = self 46 | .connection 47 | .send_and_check_request(&x::ConvertSelection { 48 | requestor: self.window, 49 | selection: self.clipboard, 50 | target: target.atom, 51 | property: target.atom, 52 | time: self.selection_time, 53 | }) 54 | { 55 | error!("Failed to request clipboard data (mime type: {mime}, error: {e})"); 56 | return; 57 | } 58 | 59 | self.pending.borrow_mut().push(PendingSelectionData { 60 | target: target.atom, 61 | pipe, 62 | incr: false, 63 | }) 64 | } else { 65 | warn!("Could not find mime type {mime}"); 66 | } 67 | } 68 | } 69 | 70 | impl Selection { 71 | fn handle_notify(&self, target: x::Atom) { 72 | let mut pending = self.pending.borrow_mut(); 73 | let Some(idx) = pending.iter().position(|t| t.target == target) else { 74 | warn!( 75 | "Got selection notify for unknown target {}", 76 | get_atom_name(&self.connection, target), 77 | ); 78 | return; 79 | }; 80 | 81 | let PendingSelectionData { 82 | mut pipe, 83 | incr, 84 | target, 85 | } = pending.swap_remove(idx); 86 | let reply = match get_property_any(&self.connection, self.window, target) { 87 | Ok(reply) => reply, 88 | Err(e) => { 89 | warn!( 90 | "Couldn't get mime type for {}: {e:?}", 91 | get_atom_name(&self.connection, target) 92 | ); 93 | return; 94 | } 95 | }; 96 | 97 | debug!( 98 | "got type {} for mime type {}", 99 | get_atom_name(&self.connection, reply.r#type()), 100 | get_atom_name(&self.connection, target) 101 | ); 102 | 103 | if reply.r#type() == self.incr { 104 | debug!( 105 | "beginning incr for {}", 106 | get_atom_name(&self.connection, target) 107 | ); 108 | pending.push(PendingSelectionData { 109 | target, 110 | pipe, 111 | incr: true, 112 | }); 113 | return; 114 | } 115 | 116 | let data = match reply.format() { 117 | 8 => reply.value::(), 118 | 32 => unsafe { reply.value::().align_to().1 }, 119 | other => { 120 | warn!("Unexpected format {other} in selection reply"); 121 | return; 122 | } 123 | }; 124 | 125 | if !incr || !data.is_empty() { 126 | if let Err(e) = pipe.write_all(data) { 127 | warn!("Failed to write selection data: {e:?}"); 128 | } else if incr { 129 | debug!( 130 | "received some incr data for {}", 131 | get_atom_name(&self.connection, target) 132 | ); 133 | pending.push(PendingSelectionData { 134 | target, 135 | pipe, 136 | incr: true, 137 | }) 138 | } 139 | } else if incr { 140 | // data is empty 141 | debug!( 142 | "completed incr for mime {}", 143 | get_atom_name(&self.connection, target) 144 | ); 145 | } 146 | } 147 | 148 | fn check_for_incr(&self, event: &x::PropertyNotifyEvent) -> bool { 149 | if event.window() != self.window || event.state() != x::Property::NewValue { 150 | return false; 151 | } 152 | 153 | let target = self.pending.borrow().iter().find_map(|pending| { 154 | (pending.target == event.atom() && pending.incr).then_some(pending.target) 155 | }); 156 | if let Some(target) = target { 157 | self.handle_notify(target); 158 | true 159 | } else { 160 | false 161 | } 162 | } 163 | } 164 | 165 | enum CurrentSelection { 166 | X11(Rc), 167 | Wayland { 168 | mimes: Vec, 169 | inner: ForeignSelection, 170 | }, 171 | } 172 | pub(crate) struct SelectionData { 173 | last_selection_timestamp: u32, 174 | target_window: x::Window, 175 | current_selection: Option, 176 | } 177 | 178 | impl SelectionData { 179 | pub fn new(connection: &xcb::Connection, root: x::Window) -> Self { 180 | let target_window = connection.generate_id(); 181 | connection 182 | .send_and_check_request(&x::CreateWindow { 183 | wid: target_window, 184 | width: 1, 185 | height: 1, 186 | depth: 0, 187 | parent: root, 188 | x: 0, 189 | y: 0, 190 | border_width: 0, 191 | class: x::WindowClass::InputOnly, 192 | visual: x::COPY_FROM_PARENT, 193 | // Watch for INCR property changes. 194 | value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], 195 | }) 196 | .expect("Couldn't create window for selections"); 197 | Self { 198 | last_selection_timestamp: x::CURRENT_TIME, 199 | target_window, 200 | current_selection: None, 201 | } 202 | } 203 | } 204 | 205 | impl XState { 206 | fn set_clipboard_owner(&mut self) { 207 | self.connection 208 | .send_and_check_request(&x::SetSelectionOwner { 209 | owner: self.wm_window, 210 | selection: self.atoms.clipboard, 211 | time: self.selection_data.last_selection_timestamp, 212 | }) 213 | .unwrap(); 214 | 215 | let reply = self 216 | .connection 217 | .wait_for_reply(self.connection.send_request(&x::GetSelectionOwner { 218 | selection: self.atoms.clipboard, 219 | })) 220 | .unwrap(); 221 | 222 | if reply.owner() != self.wm_window { 223 | warn!( 224 | "Could not get CLIPBOARD selection (owned by {:?})", 225 | reply.owner() 226 | ); 227 | } 228 | } 229 | 230 | pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection) { 231 | let mut utf8_xwl = false; 232 | let mut utf8_wl = false; 233 | let mut mimes: Vec = selection 234 | .mime_types 235 | .iter() 236 | .map(|mime| { 237 | match mime.as_str() { 238 | "UTF8_STRING" => utf8_xwl = true, 239 | "text/plain;charset=utf-8" => utf8_wl = true, 240 | _ => {} 241 | } 242 | 243 | let atom = self 244 | .connection 245 | .wait_for_reply(self.connection.send_request(&x::InternAtom { 246 | only_if_exists: false, 247 | name: mime.as_bytes(), 248 | })) 249 | .unwrap(); 250 | 251 | SelectionTargetId { 252 | name: mime.clone(), 253 | atom: atom.atom(), 254 | source: None, 255 | } 256 | }) 257 | .collect(); 258 | 259 | if utf8_wl && !utf8_xwl { 260 | let name = "UTF8_STRING".to_string(); 261 | let atom = self 262 | .connection 263 | .wait_for_reply(self.connection.send_request(&x::InternAtom { 264 | only_if_exists: false, 265 | name: name.as_bytes(), 266 | })) 267 | .unwrap() 268 | .atom(); 269 | mimes.push(SelectionTargetId { 270 | name, 271 | atom, 272 | source: Some("text/plain;charset=utf-8".to_string()), 273 | }); 274 | } 275 | 276 | self.selection_data.current_selection = Some(CurrentSelection::Wayland { 277 | mimes, 278 | inner: selection, 279 | }); 280 | self.set_clipboard_owner(); 281 | debug!("Clipboard set from Wayland"); 282 | } 283 | 284 | pub(super) fn handle_selection_event( 285 | &mut self, 286 | event: &xcb::Event, 287 | server_state: &mut RealServerState, 288 | ) -> bool { 289 | match event { 290 | xcb::Event::X(x::Event::SelectionClear(e)) => { 291 | if e.selection() == self.atoms.clipboard { 292 | self.handle_new_selection_owner(e.owner(), e.time()); 293 | } 294 | } 295 | xcb::Event::X(x::Event::SelectionNotify(e)) => { 296 | if e.property() == x::ATOM_NONE { 297 | warn!("selection notify fail?"); 298 | return true; 299 | } 300 | 301 | debug!( 302 | "selection notify requestor: {:?} target: {}", 303 | e.requestor(), 304 | get_atom_name(&self.connection, e.target()) 305 | ); 306 | 307 | if e.requestor() == self.wm_window { 308 | match e.target() { 309 | x if x == self.atoms.targets => { 310 | self.handle_target_list(e.property(), server_state) 311 | } 312 | other => warn!( 313 | "got unexpected selection notify for target {}", 314 | get_atom_name(&self.connection, other) 315 | ), 316 | } 317 | } else if e.requestor() == self.selection_data.target_window { 318 | if let Some(CurrentSelection::X11(selection)) = 319 | &self.selection_data.current_selection 320 | { 321 | selection.handle_notify(e.target()); 322 | } 323 | } else { 324 | warn!( 325 | "Got selection notify from unexpected requestor: {:?}", 326 | e.requestor() 327 | ); 328 | } 329 | } 330 | xcb::Event::X(x::Event::SelectionRequest(e)) => { 331 | let send_notify = |property| { 332 | self.connection 333 | .send_and_check_request(&x::SendEvent { 334 | propagate: false, 335 | destination: x::SendEventDest::Window(e.requestor()), 336 | event_mask: x::EventMask::empty(), 337 | event: &x::SelectionNotifyEvent::new( 338 | e.time(), 339 | e.requestor(), 340 | e.selection(), 341 | e.target(), 342 | property, 343 | ), 344 | }) 345 | .unwrap(); 346 | }; 347 | let refuse = || send_notify(x::ATOM_NONE); 348 | let success = || send_notify(e.property()); 349 | 350 | if log::log_enabled!(log::Level::Debug) { 351 | let target = get_atom_name(&self.connection, e.target()); 352 | debug!("Got selection request for target {target}"); 353 | } 354 | 355 | if e.property() == x::ATOM_NONE { 356 | debug!("refusing - property is set to none"); 357 | refuse(); 358 | return true; 359 | } 360 | 361 | let Some(CurrentSelection::Wayland { mimes, inner }) = 362 | &self.selection_data.current_selection 363 | else { 364 | warn!("Got selection request, but we don't seem to be the selection owner"); 365 | refuse(); 366 | return true; 367 | }; 368 | 369 | match e.target() { 370 | x if x == self.atoms.targets => { 371 | let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.atom).collect(); 372 | 373 | self.connection 374 | .send_and_check_request(&x::ChangeProperty { 375 | mode: x::PropMode::Replace, 376 | window: e.requestor(), 377 | property: e.property(), 378 | r#type: x::ATOM_ATOM, 379 | data: &atoms, 380 | }) 381 | .unwrap(); 382 | 383 | success(); 384 | } 385 | other => { 386 | let Some(target) = mimes.iter().find(|t| t.atom == other) else { 387 | if log::log_enabled!(log::Level::Debug) { 388 | let name = get_atom_name(&self.connection, other); 389 | debug!("refusing selection request because given atom could not be found ({})", name); 390 | } 391 | refuse(); 392 | return true; 393 | }; 394 | 395 | let mime_name = target 396 | .source 397 | .as_ref() 398 | .cloned() 399 | .unwrap_or_else(|| target.name.clone()); 400 | let data = inner.receive(mime_name, server_state); 401 | match self.connection.send_and_check_request(&x::ChangeProperty { 402 | mode: x::PropMode::Replace, 403 | window: e.requestor(), 404 | property: e.property(), 405 | r#type: target.atom, 406 | data: &data, 407 | }) { 408 | Ok(_) => success(), 409 | Err(e) => { 410 | warn!("Failed setting selection property: {e:?}"); 411 | refuse(); 412 | } 413 | } 414 | } 415 | } 416 | } 417 | 418 | xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => match e.selection() { 419 | x if x == self.atoms.clipboard => match e.subtype() { 420 | xcb::xfixes::SelectionEvent::SetSelectionOwner => { 421 | if e.owner() == self.wm_window { 422 | return true; 423 | } 424 | 425 | self.handle_new_selection_owner(e.owner(), e.selection_timestamp()); 426 | } 427 | xcb::xfixes::SelectionEvent::SelectionClientClose 428 | | xcb::xfixes::SelectionEvent::SelectionWindowDestroy => { 429 | debug!("Selection owner destroyed, selection will be unset"); 430 | self.selection_data.current_selection = None; 431 | } 432 | }, 433 | x if x == self.atoms.xsettings => match e.subtype() { 434 | xcb::xfixes::SelectionEvent::SelectionClientClose 435 | | xcb::xfixes::SelectionEvent::SelectionWindowDestroy => { 436 | debug!("Xsettings owner disappeared, reacquiring"); 437 | self.set_xsettings_owner(); 438 | } 439 | _ => {} 440 | }, 441 | _ => {} 442 | }, 443 | _ => return false, 444 | } 445 | 446 | true 447 | } 448 | 449 | fn handle_new_selection_owner(&mut self, owner: x::Window, timestamp: u32) { 450 | debug!("new selection owner: {:?}", owner); 451 | self.selection_data.last_selection_timestamp = timestamp; 452 | // Grab targets 453 | self.connection 454 | .send_and_check_request(&x::ConvertSelection { 455 | requestor: self.wm_window, 456 | selection: self.atoms.clipboard, 457 | target: self.atoms.targets, 458 | property: self.atoms.selection_reply, 459 | time: timestamp, 460 | }) 461 | .unwrap(); 462 | } 463 | 464 | fn handle_target_list(&mut self, dest_property: x::Atom, server_state: &mut RealServerState) { 465 | let reply = self 466 | .connection 467 | .wait_for_reply(self.connection.send_request(&x::GetProperty { 468 | delete: true, 469 | window: self.wm_window, 470 | property: dest_property, 471 | r#type: x::ATOM_ATOM, 472 | long_offset: 0, 473 | long_length: 20, 474 | })) 475 | .unwrap(); 476 | 477 | let targets: &[x::Atom] = reply.value(); 478 | if targets.is_empty() { 479 | warn!("Got empty selection target list, trying again..."); 480 | match self.connection.wait_for_reply(self.connection.send_request( 481 | &x::GetSelectionOwner { 482 | selection: self.atoms.clipboard, 483 | }, 484 | )) { 485 | Ok(reply) => { 486 | if reply.owner() == self.wm_window { 487 | warn!("We are unexpectedly the selection owner? Clipboard may be broken!"); 488 | } else { 489 | self.handle_new_selection_owner( 490 | reply.owner(), 491 | self.selection_data.last_selection_timestamp, 492 | ); 493 | } 494 | } 495 | Err(e) => { 496 | error!("Couldn't grab selection owner: {e:?}. Clipboard is stale!"); 497 | } 498 | } 499 | return; 500 | } 501 | if log::log_enabled!(log::Level::Debug) { 502 | let targets_str: Vec = targets 503 | .iter() 504 | .map(|t| get_atom_name(&self.connection, *t)) 505 | .collect(); 506 | debug!("got targets: {targets_str:?}"); 507 | } 508 | 509 | let mimes = targets 510 | .iter() 511 | .copied() 512 | .filter(|atom| { 513 | ![ 514 | self.atoms.targets, 515 | self.atoms.multiple, 516 | self.atoms.save_targets, 517 | ] 518 | .contains(atom) 519 | }) 520 | .map(|target_atom| SelectionTargetId { 521 | name: get_atom_name(&self.connection, target_atom), 522 | atom: target_atom, 523 | source: None, 524 | }) 525 | .collect(); 526 | 527 | let selection = Rc::new(Selection { 528 | mimes, 529 | connection: self.connection.clone(), 530 | window: self.selection_data.target_window, 531 | pending: RefCell::default(), 532 | clipboard: self.atoms.clipboard, 533 | selection_time: self.selection_data.last_selection_timestamp, 534 | incr: self.atoms.incr, 535 | }); 536 | 537 | server_state.set_copy_paste_source(&selection); 538 | self.selection_data.current_selection = Some(CurrentSelection::X11(selection)); 539 | debug!("Clipboard set from X11"); 540 | } 541 | 542 | pub(super) fn handle_selection_property_change( 543 | &mut self, 544 | event: &x::PropertyNotifyEvent, 545 | ) -> bool { 546 | if let Some(CurrentSelection::X11(selection)) = &self.selection_data.current_selection { 547 | return selection.check_for_incr(event); 548 | } 549 | false 550 | } 551 | } 552 | 553 | fn get_property_any( 554 | connection: &xcb::Connection, 555 | window: x::Window, 556 | property: x::Atom, 557 | ) -> xcb::Result { 558 | connection.wait_for_reply(connection.send_request(&x::GetProperty { 559 | delete: true, 560 | window, 561 | property, 562 | r#type: x::ATOM_ANY, 563 | long_offset: 0, 564 | long_length: u32::MAX, 565 | })) 566 | } 567 | -------------------------------------------------------------------------------- /src/xstate/settings.rs: -------------------------------------------------------------------------------- 1 | use super::XState; 2 | use log::warn; 3 | use std::collections::HashMap; 4 | use xcb::x; 5 | 6 | impl XState { 7 | pub(crate) fn set_xsettings_owner(&self) { 8 | self.connection 9 | .send_and_check_request(&x::SetSelectionOwner { 10 | owner: self.settings.window, 11 | selection: self.atoms.xsettings, 12 | time: x::CURRENT_TIME, 13 | }) 14 | .unwrap(); 15 | let reply = self 16 | .connection 17 | .wait_for_reply(self.connection.send_request(&x::GetSelectionOwner { 18 | selection: self.atoms.xsettings, 19 | })) 20 | .unwrap(); 21 | 22 | if reply.owner() != self.settings.window { 23 | warn!( 24 | "Could not get XSETTINGS selection (owned by {:?})", 25 | reply.owner() 26 | ); 27 | } 28 | } 29 | 30 | pub(crate) fn update_global_scale(&mut self, scale: i32) { 31 | self.settings.set_scale(scale); 32 | self.connection 33 | .send_and_check_request(&x::ChangeProperty { 34 | window: self.settings.window, 35 | mode: x::PropMode::Replace, 36 | property: self.atoms.xsettings_settings, 37 | r#type: self.atoms.xsettings_settings, 38 | data: &self.settings.as_data(), 39 | }) 40 | .unwrap(); 41 | } 42 | } 43 | 44 | /// The DPI consider 1x scale by X11. 45 | const DEFAULT_DPI: i32 = 96; 46 | /// I don't know why, but the DPI related xsettings seem to 47 | /// divide the DPI by 1024. 48 | const DPI_SCALE_FACTOR: i32 = 1024; 49 | 50 | const XFT_DPI: &str = "Xft/DPI"; 51 | const GDK_WINDOW_SCALE: &str = "Gdk/WindowScalingFactor"; 52 | const GDK_UNSCALED_DPI: &str = "Gdk/UnscaledDPI"; 53 | 54 | pub(super) struct Settings { 55 | window: x::Window, 56 | serial: u32, 57 | settings: HashMap<&'static str, IntSetting>, 58 | } 59 | 60 | struct IntSetting { 61 | value: i32, 62 | last_change_serial: u32, 63 | } 64 | 65 | mod setting_type { 66 | pub const INTEGER: u8 = 0; 67 | } 68 | 69 | impl Settings { 70 | pub(super) fn new(connection: &xcb::Connection, atoms: &super::Atoms, root: x::Window) -> Self { 71 | let window = connection.generate_id(); 72 | connection 73 | .send_and_check_request(&x::CreateWindow { 74 | wid: window, 75 | width: 1, 76 | height: 1, 77 | depth: 0, 78 | parent: root, 79 | x: 0, 80 | y: 0, 81 | border_width: 0, 82 | class: x::WindowClass::InputOnly, 83 | visual: x::COPY_FROM_PARENT, 84 | value_list: &[], 85 | }) 86 | .expect("Couldn't create window for settings"); 87 | 88 | let s = Settings { 89 | window, 90 | serial: 0, 91 | settings: HashMap::from([ 92 | ( 93 | XFT_DPI, 94 | IntSetting { 95 | value: DEFAULT_DPI * DPI_SCALE_FACTOR, 96 | last_change_serial: 0, 97 | }, 98 | ), 99 | ( 100 | GDK_WINDOW_SCALE, 101 | IntSetting { 102 | value: 1, 103 | last_change_serial: 0, 104 | }, 105 | ), 106 | ( 107 | GDK_UNSCALED_DPI, 108 | IntSetting { 109 | value: DEFAULT_DPI * DPI_SCALE_FACTOR, 110 | last_change_serial: 0, 111 | }, 112 | ), 113 | ]), 114 | }; 115 | 116 | connection 117 | .send_and_check_request(&x::ChangeProperty { 118 | window, 119 | mode: x::PropMode::Replace, 120 | property: atoms.xsettings_settings, 121 | r#type: atoms.xsettings_settings, 122 | data: &s.as_data(), 123 | }) 124 | .unwrap(); 125 | 126 | s 127 | } 128 | 129 | fn as_data(&self) -> Vec { 130 | // https://specifications.freedesktop.org/xsettings-spec/0.5/#format 131 | 132 | let mut data = vec![ 133 | // GTK seems to use this value for byte order from the X.h header, 134 | // so I assume I can use it too. 135 | x::ImageOrder::LsbFirst as u8, 136 | // unused 137 | 0, 138 | 0, 139 | 0, 140 | ]; 141 | 142 | data.extend_from_slice(&self.serial.to_le_bytes()); 143 | data.extend_from_slice(&(self.settings.len() as u32).to_le_bytes()); 144 | 145 | fn insert_with_padding(data: &[u8], out: &mut Vec) { 146 | out.extend_from_slice(data); 147 | // See https://x.org/releases/X11R7.7/doc/xproto/x11protocol.html#Syntactic_Conventions_b 148 | let num_padding_bytes = (4 - (data.len() % 4)) % 4; 149 | out.extend(std::iter::repeat_n(0, num_padding_bytes)); 150 | } 151 | 152 | for (name, setting) in &self.settings { 153 | data.extend_from_slice(&[setting_type::INTEGER, 0]); 154 | data.extend_from_slice(&(name.len() as u16).to_le_bytes()); 155 | insert_with_padding(name.as_bytes(), &mut data); 156 | data.extend_from_slice(&setting.last_change_serial.to_le_bytes()); 157 | data.extend_from_slice(&setting.value.to_le_bytes()); 158 | } 159 | 160 | data 161 | } 162 | 163 | fn set_scale(&mut self, scale: i32) { 164 | self.serial += 1; 165 | 166 | self.settings.entry(XFT_DPI).insert_entry(IntSetting { 167 | value: scale * DEFAULT_DPI * DPI_SCALE_FACTOR, 168 | last_change_serial: self.serial, 169 | }); 170 | self.settings 171 | .entry(GDK_WINDOW_SCALE) 172 | .insert_entry(IntSetting { 173 | value: scale, 174 | last_change_serial: self.serial, 175 | }); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /testwl/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "2.5.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 10 | 11 | [[package]] 12 | name = "cc" 13 | version = "1.0.90" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "dlib" 25 | version = "0.5.2" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" 28 | dependencies = [ 29 | "libloading", 30 | ] 31 | 32 | [[package]] 33 | name = "downcast-rs" 34 | version = "1.2.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" 37 | 38 | [[package]] 39 | name = "errno" 40 | version = "0.3.8" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 43 | dependencies = [ 44 | "libc", 45 | "windows-sys", 46 | ] 47 | 48 | [[package]] 49 | name = "io-lifetimes" 50 | version = "2.0.3" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" 53 | 54 | [[package]] 55 | name = "libc" 56 | version = "0.2.153" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 59 | 60 | [[package]] 61 | name = "libloading" 62 | version = "0.8.3" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" 65 | dependencies = [ 66 | "cfg-if", 67 | "windows-targets", 68 | ] 69 | 70 | [[package]] 71 | name = "linux-raw-sys" 72 | version = "0.4.13" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 75 | 76 | [[package]] 77 | name = "log" 78 | version = "0.4.21" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 81 | 82 | [[package]] 83 | name = "memchr" 84 | version = "2.7.2" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 87 | 88 | [[package]] 89 | name = "pkg-config" 90 | version = "0.3.30" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 93 | 94 | [[package]] 95 | name = "proc-macro2" 96 | version = "1.0.79" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" 99 | dependencies = [ 100 | "unicode-ident", 101 | ] 102 | 103 | [[package]] 104 | name = "quick-xml" 105 | version = "0.31.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" 108 | dependencies = [ 109 | "memchr", 110 | ] 111 | 112 | [[package]] 113 | name = "quote" 114 | version = "1.0.35" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 117 | dependencies = [ 118 | "proc-macro2", 119 | ] 120 | 121 | [[package]] 122 | name = "rustix" 123 | version = "0.38.32" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" 126 | dependencies = [ 127 | "bitflags", 128 | "errno", 129 | "libc", 130 | "linux-raw-sys", 131 | "windows-sys", 132 | ] 133 | 134 | [[package]] 135 | name = "scoped-tls" 136 | version = "1.0.1" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 139 | 140 | [[package]] 141 | name = "smallvec" 142 | version = "1.13.2" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 145 | 146 | [[package]] 147 | name = "testwl" 148 | version = "0.1.0" 149 | dependencies = [ 150 | "wayland-server", 151 | ] 152 | 153 | [[package]] 154 | name = "unicode-ident" 155 | version = "1.0.12" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 158 | 159 | [[package]] 160 | name = "wayland-backend" 161 | version = "0.3.3" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" 164 | dependencies = [ 165 | "cc", 166 | "downcast-rs", 167 | "rustix", 168 | "scoped-tls", 169 | "smallvec", 170 | "wayland-sys", 171 | ] 172 | 173 | [[package]] 174 | name = "wayland-scanner" 175 | version = "0.31.1" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" 178 | dependencies = [ 179 | "proc-macro2", 180 | "quick-xml", 181 | "quote", 182 | ] 183 | 184 | [[package]] 185 | name = "wayland-server" 186 | version = "0.31.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "00e6e4d5c285bc24ba4ed2d5a4bd4febd5fd904451f465973225c8e99772fdb7" 189 | dependencies = [ 190 | "bitflags", 191 | "downcast-rs", 192 | "io-lifetimes", 193 | "rustix", 194 | "wayland-backend", 195 | "wayland-scanner", 196 | ] 197 | 198 | [[package]] 199 | name = "wayland-sys" 200 | version = "0.31.1" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" 203 | dependencies = [ 204 | "dlib", 205 | "log", 206 | "pkg-config", 207 | ] 208 | 209 | [[package]] 210 | name = "windows-sys" 211 | version = "0.52.0" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 214 | dependencies = [ 215 | "windows-targets", 216 | ] 217 | 218 | [[package]] 219 | name = "windows-targets" 220 | version = "0.52.4" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 223 | dependencies = [ 224 | "windows_aarch64_gnullvm", 225 | "windows_aarch64_msvc", 226 | "windows_i686_gnu", 227 | "windows_i686_msvc", 228 | "windows_x86_64_gnu", 229 | "windows_x86_64_gnullvm", 230 | "windows_x86_64_msvc", 231 | ] 232 | 233 | [[package]] 234 | name = "windows_aarch64_gnullvm" 235 | version = "0.52.4" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 238 | 239 | [[package]] 240 | name = "windows_aarch64_msvc" 241 | version = "0.52.4" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 244 | 245 | [[package]] 246 | name = "windows_i686_gnu" 247 | version = "0.52.4" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 250 | 251 | [[package]] 252 | name = "windows_i686_msvc" 253 | version = "0.52.4" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 256 | 257 | [[package]] 258 | name = "windows_x86_64_gnu" 259 | version = "0.52.4" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 262 | 263 | [[package]] 264 | name = "windows_x86_64_gnullvm" 265 | version = "0.52.4" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 268 | 269 | [[package]] 270 | name = "windows_x86_64_msvc" 271 | version = "0.52.4" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 274 | -------------------------------------------------------------------------------- /testwl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "testwl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | wayland-protocols = { workspace = true, features = ["server", "unstable"] } 8 | wayland-server.workspace = true 9 | wl_drm = { path = "../wl_drm" } 10 | rustix = { workspace = true, features = ["pipe"] } 11 | -------------------------------------------------------------------------------- /wl_drm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wl_drm" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | wayland-client.workspace = true 10 | wayland-scanner.workspace = true 11 | wayland-server.workspace = true 12 | -------------------------------------------------------------------------------- /wl_drm/src/drm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Copyright © 2008-2011 Kristian Høgsberg 6 | Copyright © 2010-2011 Intel Corporation 7 | 8 | Permission to use, copy, modify, distribute, and sell this 9 | software and its documentation for any purpose is hereby granted 10 | without fee, provided that\n the above copyright notice appear in 11 | all copies and that both that copyright notice and this permission 12 | notice appear in supporting documentation, and that the name of 13 | the copyright holders not be used in advertising or publicity 14 | pertaining to distribution of the software without specific, 15 | written prior permission. The copyright holders make no 16 | representations about the suitability of this software for any 17 | purpose. It is provided "as is" without express or implied 18 | warranty. 19 | 20 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 21 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 22 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 24 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 25 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 26 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 27 | THIS SOFTWARE. 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 107 | 108 | 109 | 110 | 111 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | Bitmask of capabilities. 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /wl_drm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types, non_upper_case_globals)] 2 | pub mod client { 3 | use wayland_client::{self, protocol::*}; 4 | pub mod __interfaces { 5 | use wayland_client::backend as wayland_backend; 6 | use wayland_client::protocol::__interfaces::*; 7 | wayland_scanner::generate_interfaces!("src/drm.xml"); 8 | } 9 | use self::__interfaces::*; 10 | wayland_scanner::generate_client_code!("src/drm.xml"); 11 | } 12 | 13 | pub mod server { 14 | use self::__interfaces::*; 15 | pub use super::client::__interfaces; 16 | use wayland_server::{self, protocol::*}; 17 | wayland_scanner::generate_server_code!("src/drm.xml"); 18 | } 19 | --------------------------------------------------------------------------------