├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── README.tpl ├── doc └── index.html ├── rustfmt.toml ├── src ├── common.rs ├── copy.rs ├── data_control.rs ├── lib.rs ├── paste.rs ├── seat_data.rs ├── tests │ ├── copy.rs │ ├── mod.rs │ ├── paste.rs │ ├── state.rs │ └── utils.rs └── utils.rs └── wl-clipboard-rs-tools ├── Cargo.toml ├── README.md ├── build.rs └── src ├── bin ├── wl-clip.rs ├── wl-copy.rs └── wl-paste.rs ├── lib.rs ├── wl_copy.rs └── wl_paste.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | rust-dependencies: 9 | update-types: 10 | - "minor" 11 | - "patch" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: '0 0 1 * *' # Monthly 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | fail-fast: false 14 | 15 | matrix: 16 | rust: [stable, beta] 17 | features: ['', dlopen] 18 | 19 | name: ${{ matrix.rust }} - ${{ matrix.features }} 20 | runs-on: ubuntu-24.04 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | show-progress: false 26 | 27 | - name: Install Rust 28 | uses: dtolnay/rust-toolchain@master 29 | with: 30 | toolchain: ${{ matrix.rust }} 31 | 32 | - uses: Swatinem/rust-cache@v2 33 | 34 | - name: Build 35 | run: cargo build --all --features=${{ matrix.features }} 36 | 37 | - name: Set up XDG_RUNTIME_DIR 38 | run: | 39 | mkdir .runtime 40 | echo "XDG_RUNTIME_DIR=$PWD/.runtime" >> "$GITHUB_ENV" 41 | 42 | - name: Test 43 | run: cargo test --all --features=${{ matrix.features }} 44 | 45 | - name: Generate documentation 46 | run: cargo doc --features=${{ matrix.features }} 47 | 48 | - name: Copy documentation index 49 | run: cp doc/index.html target/doc/ 50 | 51 | - name: Deploy documentation 52 | if: > 53 | matrix.rust == 'stable' && 54 | matrix.features == '' && 55 | github.event_name == 'push' && 56 | github.ref == 'refs/heads/master' 57 | uses: peaceiris/actions-gh-pages@v4 58 | with: 59 | github_token: ${{ secrets.GITHUB_TOKEN }} 60 | publish_dir: ./target/doc 61 | 62 | clippy: 63 | name: clippy 64 | runs-on: ubuntu-24.04 65 | 66 | steps: 67 | - uses: actions/checkout@v4 68 | with: 69 | show-progress: false 70 | 71 | - name: Install Rust 72 | uses: dtolnay/rust-toolchain@stable 73 | with: 74 | components: clippy 75 | 76 | - uses: Swatinem/rust-cache@v2 77 | 78 | - name: Run clippy 79 | run: cargo clippy --all --all-targets 80 | 81 | rustfmt: 82 | runs-on: ubuntu-24.04 83 | 84 | steps: 85 | - uses: actions/checkout@v4 86 | with: 87 | show-progress: false 88 | 89 | - name: Install Rust 90 | uses: dtolnay/rust-toolchain@nightly 91 | with: 92 | components: rustfmt 93 | 94 | - name: Run rustfmt 95 | run: cargo fmt --all -- --check 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## v0.9.2 (14th Mar 2025) 6 | 7 | - Added support for the `ext-data-control` protocol. It will be used instead of `wlr-data-control` when available. 8 | - Updated dependencies. 9 | 10 | ## v0.9.1 (6th Oct 2024) 11 | 12 | - Added man page and shell completion generation to `wl-clipboard-rs-tools`. 13 | - Updated dependencies. 14 | 15 | ## v0.9.0 (19th June 2024) 16 | 17 | - **Breaking** Removed `utils::copy_data`. It forked into a `/usr/bin/env cat` 18 | for copying. All internal uses of the function have been changed to simply 19 | use `std::io::copy` instead. 20 | - Replaced `nix` with `rustix`, following `wayland-rs`. 21 | - Replaced the deprecated `structopt` with `clap` itself. 22 | - Updated dependencies. 23 | 24 | ## v0.8.1 (7th Mar 2024) 25 | 26 | - Updated dependencies, notably `nix`, which fixes building on LoongArch. 27 | 28 | ## v0.8.0 (3rd Oct 2023) 29 | 30 | - Added `copy::Options::omit_additional_text_mime_types` to disable 31 | wl-clipboard-rs offering several known text MIME types when a text MIME type 32 | is copied. 33 | - Updated `wayland-rs` to 0.31. 34 | - **Breaking** This changed the error types slightly. However, most uses of 35 | wl-clipboard-rs should be completely unaffected. 36 | - Updated other dependencies. 37 | 38 | ## v0.7.0 (23rd Sep 2022) 39 | 40 | - Fixed `paste::get_contents()` leaving behind zombie `cat` processes. 41 | - Changed debug logging from `info!` to `trace!`. 42 | - Bumped `nix` dependency to `0.24` to match that of the wayland-rs crates. 43 | - Replaced `derive_more` with `thiserror`. 44 | 45 | ## v0.6.0 (20th Mar 2022) 46 | 47 | - Fixed `wl-copy` and `wl-clip` hangs when followed by a pipe (e.g. `wl-copy 48 | hello | cat`). 49 | - Removed the deprecated `failure` dependency from both the library and the 50 | tools. The standard `Error` trait is now used. 51 | - Replaced underscores back with dashes in the tool binary names. 52 | - Renamed `wl-clipboard-tools` subcrate to `wl-clipboard-rs-tools`. 53 | 54 | ## v0.5.0 (13th Mar 2022) 55 | 56 | - Split binaries from the main crate `wl-clipboard-rs` into a new sub-crate 57 | `wl-clipboard-tools`. This removes a few dependencies that were only used in 58 | the binaries (like `structopt`). 59 | - This change also unintentionally replaced dashes with underscores in tool 60 | binary names. 61 | - Replaced `tree_magic` (which went unmaintained) with `tree_magic_mini`. 62 | - Changed the `fork` code which runs during the copy operation to exec 63 | `/usr/bin/env cat` instead of just `cat`. This was done to remove 64 | a non-async-signal-safe call in the child process. 65 | - Updated dependencies. 66 | 67 | ## v0.4.1 (1st Sep 2020) 68 | 69 | - Updated `nix` to 0.18 and `wayland-rs` to 0.27. 70 | 71 | ## v0.4.0 (13th Dec 2019) 72 | 73 | - **Breaking** Copying in non-foreground mode no longer forks (which was 74 | **unsafe** in multi-threaded programs). Instead, it spawns a background 75 | thread to serve copy requests. 76 | - Added `copy::prepare_copy()` and `copy::prepare_copy_multi()` (and respective 77 | functions in `copy::Options`) to accommodate workflows which depended on the 78 | forking behavior, such as `wl-copy`. See `wl-copy` for example usage. 79 | - **Breaking** Changed `copy::Source` and `copy::Seat` to own the contained 80 | data rather than borrow it. As a consequence, those types, as well as 81 | `copy::MimeSource` and `copy::Options`, have dropped their lifetime generic 82 | parameter. 83 | 84 | ## v0.3.1 (27th Nov 2019) 85 | 86 | - Reduced the `wl_seat` version requirement from 6 to 2. 87 | - Added `copy::copy_multi()` for offering multiple data sources under multiple 88 | different MIME types. 89 | 90 | ## v0.3.0 (4th Apr 2019) 91 | 92 | - **Breaking** Moved `ClipboardType` into `copy::` and `paste::`. 93 | - **Breaking** Renamed `utils::Error` into `utils::CopyDataError`. 94 | - Added `copy::ClipboardType::Both` for operating both clipboards at once. 95 | - Added `utils::is_primary_selection_supported()`. 96 | - [wl-copy]: added `--regular`, which, when set together with `--primary`, 97 | makes `wl-copy` operate on both clipboards at once. 98 | 99 | ## v0.2.0 (17th Feb 2019) 100 | 101 | - **Breaking** Changed `copy::Options::paste_once` to `serve_requests` which 102 | allows to specify the number of paste requests to serve. 103 | - Marked `copy::Seat` and `copy::Options` as `Copy`. 104 | - Updated `data-control`, it's now merged into `wlr-protocols` so no further 105 | changes without a version bump. 106 | - [wl-copy, wl-paste]: replaced `env_logger` with `stderrlog` which made the 107 | binaries much smaller. 108 | - Implemented `wl-clip`, a Wayland version of `xclip`. 109 | 110 | ## v0.1.0 (12th Feb 2019) 111 | 112 | - Initial release. 113 | -------------------------------------------------------------------------------- /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 = "android-tzdata" 7 | version = "0.1.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 10 | 11 | [[package]] 12 | name = "android_system_properties" 13 | version = "0.1.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 | dependencies = [ 17 | "libc", 18 | ] 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.10" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.6" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 55 | dependencies = [ 56 | "windows-sys", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.7" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 64 | dependencies = [ 65 | "anstyle", 66 | "once_cell", 67 | "windows-sys", 68 | ] 69 | 70 | [[package]] 71 | name = "anyhow" 72 | version = "1.0.97" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 75 | 76 | [[package]] 77 | name = "autocfg" 78 | version = "1.4.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 81 | 82 | [[package]] 83 | name = "bit-set" 84 | version = "0.8.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" 87 | dependencies = [ 88 | "bit-vec", 89 | ] 90 | 91 | [[package]] 92 | name = "bit-vec" 93 | version = "0.8.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" 96 | 97 | [[package]] 98 | name = "bitflags" 99 | version = "2.8.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 102 | 103 | [[package]] 104 | name = "bumpalo" 105 | version = "3.17.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 108 | 109 | [[package]] 110 | name = "byteorder" 111 | version = "1.5.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 114 | 115 | [[package]] 116 | name = "cc" 117 | version = "1.2.15" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" 120 | dependencies = [ 121 | "shlex", 122 | ] 123 | 124 | [[package]] 125 | name = "cfg-if" 126 | version = "1.0.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 129 | 130 | [[package]] 131 | name = "chrono" 132 | version = "0.4.39" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 135 | dependencies = [ 136 | "android-tzdata", 137 | "iana-time-zone", 138 | "js-sys", 139 | "num-traits", 140 | "wasm-bindgen", 141 | "windows-targets", 142 | ] 143 | 144 | [[package]] 145 | name = "clap" 146 | version = "4.5.31" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" 149 | dependencies = [ 150 | "clap_builder", 151 | "clap_derive", 152 | ] 153 | 154 | [[package]] 155 | name = "clap_builder" 156 | version = "4.5.31" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" 159 | dependencies = [ 160 | "anstream", 161 | "anstyle", 162 | "clap_lex", 163 | "strsim", 164 | "terminal_size", 165 | ] 166 | 167 | [[package]] 168 | name = "clap_complete" 169 | version = "4.5.46" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "f5c5508ea23c5366f77e53f5a0070e5a84e51687ec3ef9e0464c86dc8d13ce98" 172 | dependencies = [ 173 | "clap", 174 | ] 175 | 176 | [[package]] 177 | name = "clap_derive" 178 | version = "4.5.28" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" 181 | dependencies = [ 182 | "heck", 183 | "proc-macro2", 184 | "quote", 185 | "syn", 186 | ] 187 | 188 | [[package]] 189 | name = "clap_lex" 190 | version = "0.7.4" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 193 | 194 | [[package]] 195 | name = "clap_mangen" 196 | version = "0.2.26" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" 199 | dependencies = [ 200 | "clap", 201 | "roff", 202 | ] 203 | 204 | [[package]] 205 | name = "colorchoice" 206 | version = "1.0.3" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 209 | 210 | [[package]] 211 | name = "core-foundation-sys" 212 | version = "0.8.7" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 215 | 216 | [[package]] 217 | name = "dlib" 218 | version = "0.5.2" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" 221 | dependencies = [ 222 | "libloading", 223 | ] 224 | 225 | [[package]] 226 | name = "downcast-rs" 227 | version = "1.2.1" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" 230 | 231 | [[package]] 232 | name = "equivalent" 233 | version = "1.0.2" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 236 | 237 | [[package]] 238 | name = "errno" 239 | version = "0.3.10" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 242 | dependencies = [ 243 | "libc", 244 | "windows-sys", 245 | ] 246 | 247 | [[package]] 248 | name = "fastrand" 249 | version = "2.3.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 252 | 253 | [[package]] 254 | name = "fixedbitset" 255 | version = "0.4.2" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" 258 | 259 | [[package]] 260 | name = "fnv" 261 | version = "1.0.7" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 264 | 265 | [[package]] 266 | name = "getrandom" 267 | version = "0.2.15" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 270 | dependencies = [ 271 | "cfg-if", 272 | "libc", 273 | "wasi 0.11.0+wasi-snapshot-preview1", 274 | ] 275 | 276 | [[package]] 277 | name = "getrandom" 278 | version = "0.3.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 281 | dependencies = [ 282 | "cfg-if", 283 | "libc", 284 | "wasi 0.13.3+wasi-0.2.2", 285 | "windows-targets", 286 | ] 287 | 288 | [[package]] 289 | name = "hashbrown" 290 | version = "0.15.2" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 293 | 294 | [[package]] 295 | name = "heck" 296 | version = "0.5.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 299 | 300 | [[package]] 301 | name = "hermit-abi" 302 | version = "0.4.0" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 305 | 306 | [[package]] 307 | name = "iana-time-zone" 308 | version = "0.1.61" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 311 | dependencies = [ 312 | "android_system_properties", 313 | "core-foundation-sys", 314 | "iana-time-zone-haiku", 315 | "js-sys", 316 | "wasm-bindgen", 317 | "windows-core", 318 | ] 319 | 320 | [[package]] 321 | name = "iana-time-zone-haiku" 322 | version = "0.1.2" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 325 | dependencies = [ 326 | "cc", 327 | ] 328 | 329 | [[package]] 330 | name = "indexmap" 331 | version = "2.7.1" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 334 | dependencies = [ 335 | "equivalent", 336 | "hashbrown", 337 | ] 338 | 339 | [[package]] 340 | name = "is-terminal" 341 | version = "0.4.15" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" 344 | dependencies = [ 345 | "hermit-abi", 346 | "libc", 347 | "windows-sys", 348 | ] 349 | 350 | [[package]] 351 | name = "is_terminal_polyfill" 352 | version = "1.70.1" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 355 | 356 | [[package]] 357 | name = "js-sys" 358 | version = "0.3.77" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 361 | dependencies = [ 362 | "once_cell", 363 | "wasm-bindgen", 364 | ] 365 | 366 | [[package]] 367 | name = "lazy_static" 368 | version = "1.5.0" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 371 | 372 | [[package]] 373 | name = "libc" 374 | version = "0.2.170" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" 377 | 378 | [[package]] 379 | name = "libloading" 380 | version = "0.8.6" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" 383 | dependencies = [ 384 | "cfg-if", 385 | "windows-targets", 386 | ] 387 | 388 | [[package]] 389 | name = "linux-raw-sys" 390 | version = "0.4.15" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 393 | 394 | [[package]] 395 | name = "log" 396 | version = "0.4.26" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" 399 | 400 | [[package]] 401 | name = "memchr" 402 | version = "2.7.4" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 405 | 406 | [[package]] 407 | name = "memoffset" 408 | version = "0.9.1" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 411 | dependencies = [ 412 | "autocfg", 413 | ] 414 | 415 | [[package]] 416 | name = "mime" 417 | version = "0.3.17" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 420 | 421 | [[package]] 422 | name = "mime_guess" 423 | version = "2.0.5" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 426 | dependencies = [ 427 | "mime", 428 | "unicase", 429 | ] 430 | 431 | [[package]] 432 | name = "minimal-lexical" 433 | version = "0.2.1" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 436 | 437 | [[package]] 438 | name = "nom" 439 | version = "7.1.3" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 442 | dependencies = [ 443 | "memchr", 444 | "minimal-lexical", 445 | ] 446 | 447 | [[package]] 448 | name = "num-traits" 449 | version = "0.2.19" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 452 | dependencies = [ 453 | "autocfg", 454 | ] 455 | 456 | [[package]] 457 | name = "once_cell" 458 | version = "1.20.3" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 461 | 462 | [[package]] 463 | name = "os_pipe" 464 | version = "1.2.1" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" 467 | dependencies = [ 468 | "libc", 469 | "windows-sys", 470 | ] 471 | 472 | [[package]] 473 | name = "petgraph" 474 | version = "0.6.5" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" 477 | dependencies = [ 478 | "fixedbitset", 479 | "indexmap", 480 | ] 481 | 482 | [[package]] 483 | name = "pkg-config" 484 | version = "0.3.31" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 487 | 488 | [[package]] 489 | name = "ppv-lite86" 490 | version = "0.2.20" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 493 | dependencies = [ 494 | "zerocopy", 495 | ] 496 | 497 | [[package]] 498 | name = "proc-macro2" 499 | version = "1.0.93" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 502 | dependencies = [ 503 | "unicode-ident", 504 | ] 505 | 506 | [[package]] 507 | name = "proptest" 508 | version = "1.6.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" 511 | dependencies = [ 512 | "bit-set", 513 | "bit-vec", 514 | "bitflags", 515 | "lazy_static", 516 | "num-traits", 517 | "rand", 518 | "rand_chacha", 519 | "rand_xorshift", 520 | "regex-syntax", 521 | "rusty-fork", 522 | "tempfile", 523 | "unarray", 524 | ] 525 | 526 | [[package]] 527 | name = "proptest-derive" 528 | version = "0.5.1" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" 531 | dependencies = [ 532 | "proc-macro2", 533 | "quote", 534 | "syn", 535 | ] 536 | 537 | [[package]] 538 | name = "quick-error" 539 | version = "1.2.3" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 542 | 543 | [[package]] 544 | name = "quick-xml" 545 | version = "0.37.2" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" 548 | dependencies = [ 549 | "memchr", 550 | ] 551 | 552 | [[package]] 553 | name = "quote" 554 | version = "1.0.38" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 557 | dependencies = [ 558 | "proc-macro2", 559 | ] 560 | 561 | [[package]] 562 | name = "rand" 563 | version = "0.8.5" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 566 | dependencies = [ 567 | "libc", 568 | "rand_chacha", 569 | "rand_core", 570 | ] 571 | 572 | [[package]] 573 | name = "rand_chacha" 574 | version = "0.3.1" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 577 | dependencies = [ 578 | "ppv-lite86", 579 | "rand_core", 580 | ] 581 | 582 | [[package]] 583 | name = "rand_core" 584 | version = "0.6.4" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 587 | dependencies = [ 588 | "getrandom 0.2.15", 589 | ] 590 | 591 | [[package]] 592 | name = "rand_xorshift" 593 | version = "0.3.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" 596 | dependencies = [ 597 | "rand_core", 598 | ] 599 | 600 | [[package]] 601 | name = "regex-syntax" 602 | version = "0.8.5" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 605 | 606 | [[package]] 607 | name = "roff" 608 | version = "0.2.2" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" 611 | 612 | [[package]] 613 | name = "rustix" 614 | version = "0.38.44" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 617 | dependencies = [ 618 | "bitflags", 619 | "errno", 620 | "libc", 621 | "linux-raw-sys", 622 | "windows-sys", 623 | ] 624 | 625 | [[package]] 626 | name = "rustversion" 627 | version = "1.0.19" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 630 | 631 | [[package]] 632 | name = "rusty-fork" 633 | version = "0.3.0" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" 636 | dependencies = [ 637 | "fnv", 638 | "quick-error", 639 | "tempfile", 640 | "wait-timeout", 641 | ] 642 | 643 | [[package]] 644 | name = "scoped-tls" 645 | version = "1.0.1" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 648 | 649 | [[package]] 650 | name = "shlex" 651 | version = "1.3.0" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 654 | 655 | [[package]] 656 | name = "smallvec" 657 | version = "1.14.0" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 660 | 661 | [[package]] 662 | name = "stderrlog" 663 | version = "0.6.0" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "61c910772f992ab17d32d6760e167d2353f4130ed50e796752689556af07dc6b" 666 | dependencies = [ 667 | "chrono", 668 | "is-terminal", 669 | "log", 670 | "termcolor", 671 | "thread_local", 672 | ] 673 | 674 | [[package]] 675 | name = "strsim" 676 | version = "0.11.1" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 679 | 680 | [[package]] 681 | name = "syn" 682 | version = "2.0.98" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 685 | dependencies = [ 686 | "proc-macro2", 687 | "quote", 688 | "unicode-ident", 689 | ] 690 | 691 | [[package]] 692 | name = "tempfile" 693 | version = "3.17.1" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" 696 | dependencies = [ 697 | "cfg-if", 698 | "fastrand", 699 | "getrandom 0.3.1", 700 | "once_cell", 701 | "rustix", 702 | "windows-sys", 703 | ] 704 | 705 | [[package]] 706 | name = "termcolor" 707 | version = "1.1.3" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 710 | dependencies = [ 711 | "winapi-util", 712 | ] 713 | 714 | [[package]] 715 | name = "terminal_size" 716 | version = "0.4.1" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" 719 | dependencies = [ 720 | "rustix", 721 | "windows-sys", 722 | ] 723 | 724 | [[package]] 725 | name = "thiserror" 726 | version = "2.0.12" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 729 | dependencies = [ 730 | "thiserror-impl", 731 | ] 732 | 733 | [[package]] 734 | name = "thiserror-impl" 735 | version = "2.0.12" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 738 | dependencies = [ 739 | "proc-macro2", 740 | "quote", 741 | "syn", 742 | ] 743 | 744 | [[package]] 745 | name = "thread_local" 746 | version = "1.1.8" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 749 | dependencies = [ 750 | "cfg-if", 751 | "once_cell", 752 | ] 753 | 754 | [[package]] 755 | name = "tree_magic_mini" 756 | version = "3.1.6" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63" 759 | dependencies = [ 760 | "fnv", 761 | "memchr", 762 | "nom", 763 | "once_cell", 764 | "petgraph", 765 | ] 766 | 767 | [[package]] 768 | name = "unarray" 769 | version = "0.1.4" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" 772 | 773 | [[package]] 774 | name = "unicase" 775 | version = "2.8.1" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 778 | 779 | [[package]] 780 | name = "unicode-ident" 781 | version = "1.0.17" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" 784 | 785 | [[package]] 786 | name = "utf8parse" 787 | version = "0.2.2" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 790 | 791 | [[package]] 792 | name = "wait-timeout" 793 | version = "0.2.1" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" 796 | dependencies = [ 797 | "libc", 798 | ] 799 | 800 | [[package]] 801 | name = "wasi" 802 | version = "0.11.0+wasi-snapshot-preview1" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 805 | 806 | [[package]] 807 | name = "wasi" 808 | version = "0.13.3+wasi-0.2.2" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 811 | dependencies = [ 812 | "wit-bindgen-rt", 813 | ] 814 | 815 | [[package]] 816 | name = "wasm-bindgen" 817 | version = "0.2.100" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 820 | dependencies = [ 821 | "cfg-if", 822 | "once_cell", 823 | "rustversion", 824 | "wasm-bindgen-macro", 825 | ] 826 | 827 | [[package]] 828 | name = "wasm-bindgen-backend" 829 | version = "0.2.100" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 832 | dependencies = [ 833 | "bumpalo", 834 | "log", 835 | "proc-macro2", 836 | "quote", 837 | "syn", 838 | "wasm-bindgen-shared", 839 | ] 840 | 841 | [[package]] 842 | name = "wasm-bindgen-macro" 843 | version = "0.2.100" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 846 | dependencies = [ 847 | "quote", 848 | "wasm-bindgen-macro-support", 849 | ] 850 | 851 | [[package]] 852 | name = "wasm-bindgen-macro-support" 853 | version = "0.2.100" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 856 | dependencies = [ 857 | "proc-macro2", 858 | "quote", 859 | "syn", 860 | "wasm-bindgen-backend", 861 | "wasm-bindgen-shared", 862 | ] 863 | 864 | [[package]] 865 | name = "wasm-bindgen-shared" 866 | version = "0.2.100" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 869 | dependencies = [ 870 | "unicode-ident", 871 | ] 872 | 873 | [[package]] 874 | name = "wayland-backend" 875 | version = "0.3.8" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" 878 | dependencies = [ 879 | "cc", 880 | "downcast-rs", 881 | "rustix", 882 | "scoped-tls", 883 | "smallvec", 884 | "wayland-sys", 885 | ] 886 | 887 | [[package]] 888 | name = "wayland-client" 889 | version = "0.31.8" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" 892 | dependencies = [ 893 | "bitflags", 894 | "rustix", 895 | "wayland-backend", 896 | "wayland-scanner", 897 | ] 898 | 899 | [[package]] 900 | name = "wayland-protocols" 901 | version = "0.32.6" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" 904 | dependencies = [ 905 | "bitflags", 906 | "wayland-backend", 907 | "wayland-client", 908 | "wayland-scanner", 909 | "wayland-server", 910 | ] 911 | 912 | [[package]] 913 | name = "wayland-protocols-wlr" 914 | version = "0.3.6" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" 917 | dependencies = [ 918 | "bitflags", 919 | "wayland-backend", 920 | "wayland-client", 921 | "wayland-protocols", 922 | "wayland-scanner", 923 | "wayland-server", 924 | ] 925 | 926 | [[package]] 927 | name = "wayland-scanner" 928 | version = "0.31.6" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" 931 | dependencies = [ 932 | "proc-macro2", 933 | "quick-xml", 934 | "quote", 935 | ] 936 | 937 | [[package]] 938 | name = "wayland-server" 939 | version = "0.31.7" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "97fabd7ed68cff8e7657b8a8a1fbe90cb4a3f0c30d90da4bf179a7a23008a4cb" 942 | dependencies = [ 943 | "bitflags", 944 | "downcast-rs", 945 | "rustix", 946 | "wayland-backend", 947 | "wayland-scanner", 948 | ] 949 | 950 | [[package]] 951 | name = "wayland-sys" 952 | version = "0.31.6" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" 955 | dependencies = [ 956 | "dlib", 957 | "libc", 958 | "log", 959 | "memoffset", 960 | "once_cell", 961 | "pkg-config", 962 | ] 963 | 964 | [[package]] 965 | name = "winapi-util" 966 | version = "0.1.9" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 969 | dependencies = [ 970 | "windows-sys", 971 | ] 972 | 973 | [[package]] 974 | name = "windows-core" 975 | version = "0.52.0" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 978 | dependencies = [ 979 | "windows-targets", 980 | ] 981 | 982 | [[package]] 983 | name = "windows-sys" 984 | version = "0.59.0" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 987 | dependencies = [ 988 | "windows-targets", 989 | ] 990 | 991 | [[package]] 992 | name = "windows-targets" 993 | version = "0.52.6" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 996 | dependencies = [ 997 | "windows_aarch64_gnullvm", 998 | "windows_aarch64_msvc", 999 | "windows_i686_gnu", 1000 | "windows_i686_gnullvm", 1001 | "windows_i686_msvc", 1002 | "windows_x86_64_gnu", 1003 | "windows_x86_64_gnullvm", 1004 | "windows_x86_64_msvc", 1005 | ] 1006 | 1007 | [[package]] 1008 | name = "windows_aarch64_gnullvm" 1009 | version = "0.52.6" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1012 | 1013 | [[package]] 1014 | name = "windows_aarch64_msvc" 1015 | version = "0.52.6" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1018 | 1019 | [[package]] 1020 | name = "windows_i686_gnu" 1021 | version = "0.52.6" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1024 | 1025 | [[package]] 1026 | name = "windows_i686_gnullvm" 1027 | version = "0.52.6" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1030 | 1031 | [[package]] 1032 | name = "windows_i686_msvc" 1033 | version = "0.52.6" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1036 | 1037 | [[package]] 1038 | name = "windows_x86_64_gnu" 1039 | version = "0.52.6" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1042 | 1043 | [[package]] 1044 | name = "windows_x86_64_gnullvm" 1045 | version = "0.52.6" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1048 | 1049 | [[package]] 1050 | name = "windows_x86_64_msvc" 1051 | version = "0.52.6" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1054 | 1055 | [[package]] 1056 | name = "wit-bindgen-rt" 1057 | version = "0.33.0" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 1060 | dependencies = [ 1061 | "bitflags", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "wl-clipboard-rs" 1066 | version = "0.9.2" 1067 | dependencies = [ 1068 | "libc", 1069 | "log", 1070 | "os_pipe", 1071 | "proptest", 1072 | "proptest-derive", 1073 | "rustix", 1074 | "tempfile", 1075 | "thiserror", 1076 | "tree_magic_mini", 1077 | "wayland-backend", 1078 | "wayland-client", 1079 | "wayland-protocols", 1080 | "wayland-protocols-wlr", 1081 | "wayland-server", 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "wl-clipboard-rs-tools" 1086 | version = "0.9.2" 1087 | dependencies = [ 1088 | "anyhow", 1089 | "clap", 1090 | "clap_complete", 1091 | "clap_mangen", 1092 | "libc", 1093 | "log", 1094 | "mime_guess", 1095 | "rustix", 1096 | "stderrlog", 1097 | "wl-clipboard-rs", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "zerocopy" 1102 | version = "0.7.35" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1105 | dependencies = [ 1106 | "byteorder", 1107 | "zerocopy-derive", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "zerocopy-derive" 1112 | version = "0.7.35" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1115 | dependencies = [ 1116 | "proc-macro2", 1117 | "quote", 1118 | "syn", 1119 | ] 1120 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["wl-clipboard-rs-tools"] 3 | 4 | [workspace.package] 5 | version = "0.9.2" # remember to update html_root_url 6 | authors = ["Ivan Molodetskikh "] 7 | edition = "2021" 8 | license = "MIT/Apache-2.0" 9 | repository = "https://github.com/YaLTeR/wl-clipboard-rs" 10 | keywords = ["wayland", "clipboard"] 11 | 12 | [workspace.dependencies] 13 | libc = "0.2.170" 14 | log = "0.4.26" 15 | rustix = "0.38.44" 16 | 17 | [package] 18 | name = "wl-clipboard-rs" 19 | version.workspace = true 20 | authors.workspace = true 21 | description = "Access to the Wayland clipboard for terminal and other window-less applications." 22 | edition.workspace = true 23 | license.workspace = true 24 | 25 | readme = "README.md" 26 | documentation = "https://docs.rs/wl-clipboard-rs" 27 | repository.workspace = true 28 | keywords.workspace = true 29 | categories = ["os"] 30 | 31 | [dependencies] 32 | libc.workspace = true 33 | log.workspace = true 34 | os_pipe = { version = "1.2.1", features = ["io_safety"] } 35 | rustix = { workspace = true, features = ["fs", "event"] } 36 | tempfile = "3.17.1" 37 | thiserror = "2" 38 | tree_magic_mini = "3.1.6" 39 | wayland-backend = "0.3.8" 40 | wayland-client = "0.31.8" 41 | wayland-protocols = { version = "0.32.6", features = ["client", "staging"] } 42 | wayland-protocols-wlr = { version = "0.3.6", features = ["client"] } 43 | 44 | [dev-dependencies] 45 | wayland-server = "0.31.7" 46 | wayland-protocols = { version = "0.32.6", features = ["server", "staging"] } 47 | wayland-protocols-wlr = { version = "0.3.6", features = ["server"] } 48 | proptest = "1.6.0" 49 | proptest-derive = "0.5.1" 50 | 51 | [features] 52 | # Link to libwayland-client.so instead of using the Rust implementation. 53 | native_lib = ["wayland-backend/client_system", "wayland-backend/server_system"] 54 | 55 | dlopen = ["native_lib", "wayland-backend/dlopen", "wayland-backend/dlopen"] 56 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Ivan Molodetskikh 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wl-clipboard-rs 2 | 3 | [![crates.io](https://img.shields.io/crates/v/wl-clipboard-rs.svg)](https://crates.io/crates/wl-clipboard-rs) 4 | [![Build Status](https://github.com/YaLTeR/wl-clipboard-rs/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/YaLTeR/wl-clipboard-rs/actions/workflows/ci.yml?query=branch%3Amaster) 5 | [![Documentation](https://docs.rs/wl-clipboard-rs/badge.svg)](https://docs.rs/wl-clipboard-rs) 6 | 7 | [Documentation (master)](https://yalter.github.io/wl-clipboard-rs/wl_clipboard_rs/) 8 | 9 | A safe Rust crate for working with the Wayland clipboard. 10 | 11 | This crate is intended to be used by terminal applications, clipboard managers and other 12 | utilities which don't spawn Wayland surfaces (windows). If your application has a window, 13 | please use the appropriate Wayland protocols for interacting with the Wayland clipboard 14 | (`wl_data_device` from the core Wayland protocol, the `primary_selection` protocol for the 15 | primary selection), for example via the 16 | [smithay-clipboard](https://crates.io/crates/smithay-clipboard) crate. 17 | 18 | The protocol used for clipboard interaction is `ext-data-control` or `wlr-data-control`. When 19 | using the regular clipboard, the compositor must support any version of either protocol. When 20 | using the "primary" clipboard, the compositor must support any version of `ext-data-control`, 21 | or the second version of the `wlr-data-control` protocol. 22 | 23 | For example applications using these features, see `wl-clipboard-rs-tools/src/bin/wl_copy.rs` 24 | and `wl-clipboard-rs-tools/src/bin/wl_paste.rs` which implement terminal apps similar to 25 | [wl-clipboard](https://github.com/bugaevc/wl-clipboard) or 26 | `wl-clipboard-rs-tools/src/bin/wl_clip.rs` which implements a Wayland version of `xclip`. 27 | 28 | The Rust implementation of the Wayland client is used by default; use the `native_lib` feature 29 | to link to `libwayland-client.so` for communication instead. A `dlopen` feature is also 30 | available for loading `libwayland-client.so` dynamically at runtime rather than linking to it. 31 | 32 | The code of the crate itself (and the code of the example utilities) is 100% safe Rust. This 33 | doesn't include the dependencies. 34 | 35 | ## Examples 36 | 37 | Copying to the regular clipboard: 38 | ```rust 39 | use wl_clipboard_rs::copy::{MimeType, Options, Source}; 40 | 41 | let opts = Options::new(); 42 | opts.copy(Source::Bytes("Hello world!".to_string().into_bytes().into()), MimeType::Autodetect)?; 43 | ``` 44 | 45 | Pasting plain text from the regular clipboard: 46 | ```rust 47 | use std::io::Read; 48 | use wl_clipboard_rs::{paste::{get_contents, ClipboardType, Error, MimeType, Seat}}; 49 | 50 | let result = get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Text); 51 | match result { 52 | Ok((mut pipe, _)) => { 53 | let mut contents = vec![]; 54 | pipe.read_to_end(&mut contents)?; 55 | println!("Pasted: {}", String::from_utf8_lossy(&contents)); 56 | } 57 | 58 | Err(Error::NoSeats) | Err(Error::ClipboardEmpty) | Err(Error::NoMimeType) => { 59 | // The clipboard is empty or doesn't contain text, nothing to worry about. 60 | } 61 | 62 | Err(err) => Err(err)? 63 | } 64 | ``` 65 | 66 | Checking if the "primary" clipboard is supported (note that this might be unnecessary depending 67 | on your crate usage, the regular copying and pasting functions do report if the primary 68 | selection is unsupported when it is requested): 69 | 70 | ```rust 71 | use wl_clipboard_rs::utils::{is_primary_selection_supported, PrimarySelectionCheckError}; 72 | 73 | match is_primary_selection_supported() { 74 | Ok(supported) => { 75 | // We have our definitive result. False means that ext/wlr-data-control is present 76 | // and did not signal the primary selection support, or that only wlr-data-control 77 | // version 1 is present (which does not support primary selection). 78 | }, 79 | Err(PrimarySelectionCheckError::NoSeats) => { 80 | // Impossible to give a definitive result. Primary selection may or may not be 81 | // supported. 82 | 83 | // The required protocol (ext-data-control, or wlr-data-control version 2) is there, 84 | // but there are no seats. Unfortunately, at least one seat is needed to check for the 85 | // primary clipboard support. 86 | }, 87 | Err(PrimarySelectionCheckError::MissingProtocol) => { 88 | // The data-control protocol (required for wl-clipboard-rs operation) is not 89 | // supported by the compositor. 90 | }, 91 | Err(_) => { 92 | // Some communication error occurred. 93 | } 94 | } 95 | ``` 96 | 97 | ## Included terminal utilities 98 | 99 | - `wl-paste`: implements `wl-paste` from 100 | [wl-clipboard](https://github.com/bugaevc/wl-clipboard). 101 | - `wl-copy`: implements `wl-copy` from [wl-clipboard](https://github.com/bugaevc/wl-clipboard). 102 | - `wl-clip`: a Wayland version of `xclip`. 103 | 104 | Stuff that would be neat to add: 105 | - Utility that mimics `xsel` commandline flags. 106 | 107 | License: MIT/Apache-2.0 108 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | # {{crate}} 2 | 3 | [![crates.io](https://img.shields.io/crates/v/wl-clipboard-rs.svg)](https://crates.io/crates/wl-clipboard-rs) 4 | [![Build Status](https://github.com/YaLTeR/wl-clipboard-rs/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/YaLTeR/wl-clipboard-rs/actions/workflows/ci.yml?query=branch%3Amaster) 5 | [![Documentation](https://docs.rs/wl-clipboard-rs/badge.svg)](https://docs.rs/wl-clipboard-rs) 6 | 7 | [Documentation (master)](https://yalter.github.io/wl-clipboard-rs/wl_clipboard_rs/) 8 | 9 | {{readme}} 10 | 11 | Stuff that would be neat to add: 12 | - Utility that mimics `xsel` commandline flags. 13 | 14 | License: {{license}} 15 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Redirection 7 | 8 | 9 | If you are not redirected automatically, follow this link. 10 | 11 | 12 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Module" 2 | group_imports = "StdExternalCrate" 3 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::ffi::OsString; 3 | use std::os::unix::net::UnixStream; 4 | use std::path::PathBuf; 5 | use std::{env, io}; 6 | 7 | use wayland_backend::client::WaylandError; 8 | use wayland_client::globals::{registry_queue_init, GlobalError, GlobalListContents}; 9 | use wayland_client::protocol::wl_registry::WlRegistry; 10 | use wayland_client::protocol::wl_seat::{self, WlSeat}; 11 | use wayland_client::{ConnectError, Connection, Dispatch, EventQueue, Proxy}; 12 | use wayland_protocols::ext::data_control::v1::client::ext_data_control_manager_v1::ExtDataControlManagerV1; 13 | use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; 14 | 15 | use crate::data_control::Manager; 16 | use crate::seat_data::SeatData; 17 | 18 | pub struct State { 19 | pub seats: HashMap, 20 | pub clipboard_manager: Manager, 21 | } 22 | 23 | #[derive(thiserror::Error, Debug)] 24 | pub enum Error { 25 | #[error("Couldn't open the provided Wayland socket")] 26 | SocketOpenError(#[source] io::Error), 27 | 28 | #[error("Couldn't connect to the Wayland compositor")] 29 | WaylandConnection(#[source] ConnectError), 30 | 31 | #[error("Wayland compositor communication error")] 32 | WaylandCommunication(#[source] WaylandError), 33 | 34 | #[error( 35 | "A required Wayland protocol ({name} version {version}) is not supported by the compositor" 36 | )] 37 | MissingProtocol { name: &'static str, version: u32 }, 38 | } 39 | 40 | impl Dispatch for State 41 | where 42 | S: Dispatch + AsMut, 43 | { 44 | fn event( 45 | parent: &mut S, 46 | seat: &WlSeat, 47 | event: ::Event, 48 | _data: &(), 49 | _conn: &wayland_client::Connection, 50 | _qh: &wayland_client::QueueHandle, 51 | ) { 52 | let state = parent.as_mut(); 53 | 54 | if let wl_seat::Event::Name { name } = event { 55 | state.seats.get_mut(seat).unwrap().set_name(name); 56 | } 57 | } 58 | } 59 | 60 | pub fn initialize( 61 | primary: bool, 62 | socket_name: Option, 63 | ) -> Result<(EventQueue, State), Error> 64 | where 65 | S: Dispatch + 'static, 66 | S: Dispatch, 67 | S: Dispatch, 68 | S: Dispatch, 69 | S: AsMut, 70 | { 71 | // Connect to the Wayland compositor. 72 | let conn = match socket_name { 73 | Some(name) => { 74 | let mut socket_path = env::var_os("XDG_RUNTIME_DIR") 75 | .map(Into::::into) 76 | .ok_or(ConnectError::NoCompositor) 77 | .map_err(Error::WaylandConnection)?; 78 | if !socket_path.is_absolute() { 79 | return Err(Error::WaylandConnection(ConnectError::NoCompositor)); 80 | } 81 | socket_path.push(name); 82 | 83 | let stream = UnixStream::connect(socket_path).map_err(Error::SocketOpenError)?; 84 | Connection::from_socket(stream) 85 | } 86 | None => Connection::connect_to_env(), 87 | } 88 | .map_err(Error::WaylandConnection)?; 89 | 90 | // Retrieve the global interfaces. 91 | let (globals, queue) = 92 | registry_queue_init::(&conn).map_err(|err| match err { 93 | GlobalError::Backend(err) => Error::WaylandCommunication(err), 94 | GlobalError::InvalidId(err) => panic!("How's this possible? \ 95 | Is there no wl_registry? \ 96 | {:?}", 97 | err), 98 | })?; 99 | let qh = &queue.handle(); 100 | 101 | // Verify that we got the clipboard manager. 102 | let ext_manager = globals.bind(qh, 1..=1, ()).ok().map(Manager::Ext); 103 | 104 | let wlr_v = if primary { 2 } else { 1 }; 105 | let wlr_manager = || globals.bind(qh, wlr_v..=wlr_v, ()).ok().map(Manager::Zwlr); 106 | 107 | let clipboard_manager = match ext_manager.or_else(wlr_manager) { 108 | Some(manager) => manager, 109 | None => { 110 | return Err(Error::MissingProtocol { 111 | name: "ext-data-control, or wlr-data-control", 112 | version: wlr_v, 113 | }) 114 | } 115 | }; 116 | 117 | let registry = globals.registry(); 118 | let seats = globals.contents().with_list(|globals| { 119 | globals 120 | .iter() 121 | .filter(|global| global.interface == WlSeat::interface().name && global.version >= 2) 122 | .map(|global| { 123 | let seat = registry.bind(global.name, 2, qh, ()); 124 | (seat, SeatData::default()) 125 | }) 126 | .collect() 127 | }); 128 | 129 | let state = State { 130 | seats, 131 | clipboard_manager, 132 | }; 133 | 134 | Ok((queue, state)) 135 | } 136 | -------------------------------------------------------------------------------- /src/copy.rs: -------------------------------------------------------------------------------- 1 | //! Copying and clearing clipboard contents. 2 | 3 | use std::collections::hash_map::Entry; 4 | use std::collections::{HashMap, HashSet}; 5 | use std::ffi::OsString; 6 | use std::fs::{remove_dir, remove_file, File, OpenOptions}; 7 | use std::io::{self, Read, Seek, SeekFrom, Write}; 8 | use std::path::PathBuf; 9 | use std::sync::mpsc::sync_channel; 10 | use std::{iter, thread}; 11 | 12 | use log::trace; 13 | use rustix::fs::{fcntl_setfl, OFlags}; 14 | use wayland_client::globals::GlobalListContents; 15 | use wayland_client::protocol::wl_registry::WlRegistry; 16 | use wayland_client::protocol::wl_seat::WlSeat; 17 | use wayland_client::{ 18 | delegate_dispatch, event_created_child, ConnectError, Dispatch, DispatchError, EventQueue, 19 | }; 20 | 21 | use crate::common::{self, initialize}; 22 | use crate::data_control::{ 23 | self, impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer, impl_dispatch_source, 24 | }; 25 | use crate::seat_data::SeatData; 26 | use crate::utils::is_text; 27 | 28 | /// The clipboard to operate on. 29 | #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)] 30 | #[cfg_attr(test, derive(proptest_derive::Arbitrary))] 31 | pub enum ClipboardType { 32 | /// The regular clipboard. 33 | #[default] 34 | Regular, 35 | /// The "primary" clipboard. 36 | /// 37 | /// Working with the "primary" clipboard requires the compositor to support ext-data-control, 38 | /// or wlr-data-control version 2 or above. 39 | Primary, 40 | /// Operate on both clipboards at once. 41 | /// 42 | /// Useful for atomically setting both clipboards at once. This option requires the "primary" 43 | /// clipboard to be supported. 44 | Both, 45 | } 46 | 47 | /// MIME type to offer the copied data under. 48 | #[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)] 49 | #[cfg_attr(test, derive(proptest_derive::Arbitrary))] 50 | pub enum MimeType { 51 | /// Detect the MIME type automatically from the data. 52 | #[cfg_attr(test, proptest(skip))] 53 | Autodetect, 54 | /// Offer a number of common plain text MIME types. 55 | Text, 56 | /// Offer a specific MIME type. 57 | Specific(String), 58 | } 59 | 60 | /// Source for copying. 61 | #[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)] 62 | #[cfg_attr(test, derive(proptest_derive::Arbitrary))] 63 | pub enum Source { 64 | /// Copy contents of the standard input. 65 | #[cfg_attr(test, proptest(skip))] 66 | StdIn, 67 | /// Copy the given bytes. 68 | Bytes(Box<[u8]>), 69 | } 70 | 71 | /// Source for copying, with a MIME type. 72 | /// 73 | /// Used for [`copy_multi`]. 74 | /// 75 | /// [`copy_multi`]: fn.copy_multi.html 76 | #[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)] 77 | pub struct MimeSource { 78 | pub source: Source, 79 | pub mime_type: MimeType, 80 | } 81 | 82 | /// Seat to operate on. 83 | #[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)] 84 | pub enum Seat { 85 | /// Operate on all existing seats at once. 86 | #[default] 87 | All, 88 | /// Operate on a seat with the given name. 89 | Specific(String), 90 | } 91 | 92 | /// Number of paste requests to serve. 93 | #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)] 94 | pub enum ServeRequests { 95 | /// Serve requests indefinitely. 96 | #[default] 97 | Unlimited, 98 | /// Serve only the given number of requests. 99 | Only(usize), 100 | } 101 | 102 | /// Options and flags that are used to customize the copying. 103 | #[derive(Clone, Eq, PartialEq, Debug, Default, Hash, PartialOrd, Ord)] 104 | pub struct Options { 105 | /// The clipboard to work with. 106 | clipboard: ClipboardType, 107 | 108 | /// The seat to work with. 109 | seat: Seat, 110 | 111 | /// Trim the trailing newline character before copying. 112 | /// 113 | /// This flag is only applied for text MIME types. 114 | trim_newline: bool, 115 | 116 | /// Do not spawn a separate thread for serving copy requests. 117 | /// 118 | /// Setting this flag will result in the call to `copy()` **blocking** until all data sources 119 | /// it creates are destroyed, e.g. until someone else copies something into the clipboard. 120 | foreground: bool, 121 | 122 | /// Number of paste requests to serve. 123 | /// 124 | /// Limiting the number of paste requests to one effectively clears the clipboard after the 125 | /// first paste. It can be used when copying e.g. sensitive data, like passwords. Note however 126 | /// that certain apps may have issues pasting when this option is used, in particular XWayland 127 | /// clients are known to suffer from this. 128 | serve_requests: ServeRequests, 129 | 130 | /// Omit additional text mime types which are offered by default if at least one text mime type is provided. 131 | /// 132 | /// Omits additionally offered `text/plain;charset=utf-8`, `text/plain`, `STRING`, `UTF8_STRING` and 133 | /// `TEXT` mime types which are offered by default if at least one text mime type is provided. 134 | omit_additional_text_mime_types: bool, 135 | } 136 | 137 | /// A copy operation ready to start serving requests. 138 | pub struct PreparedCopy { 139 | queue: EventQueue, 140 | state: State, 141 | sources: Vec, 142 | } 143 | 144 | /// Errors that can occur for copying the source data to a temporary file. 145 | #[derive(thiserror::Error, Debug)] 146 | pub enum SourceCreationError { 147 | #[error("Couldn't create a temporary directory")] 148 | TempDirCreate(#[source] io::Error), 149 | 150 | #[error("Couldn't create a temporary file")] 151 | TempFileCreate(#[source] io::Error), 152 | 153 | #[error("Couldn't copy data to the temporary file")] 154 | DataCopy(#[source] io::Error), 155 | 156 | #[error("Couldn't write to the temporary file")] 157 | TempFileWrite(#[source] io::Error), 158 | 159 | #[error("Couldn't open the temporary file for newline trimming")] 160 | TempFileOpen(#[source] io::Error), 161 | 162 | #[error("Couldn't get the temporary file metadata for newline trimming")] 163 | TempFileMetadata(#[source] io::Error), 164 | 165 | #[error("Couldn't seek the temporary file for newline trimming")] 166 | TempFileSeek(#[source] io::Error), 167 | 168 | #[error("Couldn't read the last byte of the temporary file for newline trimming")] 169 | TempFileRead(#[source] io::Error), 170 | 171 | #[error("Couldn't truncate the temporary file for newline trimming")] 172 | TempFileTruncate(#[source] io::Error), 173 | } 174 | 175 | /// Errors that can occur for copying and clearing the clipboard. 176 | #[derive(thiserror::Error, Debug)] 177 | pub enum Error { 178 | #[error("There are no seats")] 179 | NoSeats, 180 | 181 | #[error("Couldn't open the provided Wayland socket")] 182 | SocketOpenError(#[source] io::Error), 183 | 184 | #[error("Couldn't connect to the Wayland compositor")] 185 | WaylandConnection(#[source] ConnectError), 186 | 187 | #[error("Wayland compositor communication error")] 188 | WaylandCommunication(#[source] DispatchError), 189 | 190 | #[error( 191 | "A required Wayland protocol ({} version {}) is not supported by the compositor", 192 | name, 193 | version 194 | )] 195 | MissingProtocol { name: &'static str, version: u32 }, 196 | 197 | #[error("The compositor does not support primary selection")] 198 | PrimarySelectionUnsupported, 199 | 200 | #[error("The requested seat was not found")] 201 | SeatNotFound, 202 | 203 | #[error("Error copying the source into a temporary file")] 204 | TempCopy(#[source] SourceCreationError), 205 | 206 | #[error("Couldn't remove the temporary file")] 207 | TempFileRemove(#[source] io::Error), 208 | 209 | #[error("Couldn't remove the temporary directory")] 210 | TempDirRemove(#[source] io::Error), 211 | 212 | #[error("Error satisfying a paste request")] 213 | Paste(#[source] DataSourceError), 214 | } 215 | 216 | impl From for Error { 217 | fn from(x: common::Error) -> Self { 218 | use common::Error::*; 219 | 220 | match x { 221 | SocketOpenError(err) => Error::SocketOpenError(err), 222 | WaylandConnection(err) => Error::WaylandConnection(err), 223 | WaylandCommunication(err) => Error::WaylandCommunication(err.into()), 224 | MissingProtocol { name, version } => Error::MissingProtocol { name, version }, 225 | } 226 | } 227 | } 228 | 229 | #[derive(thiserror::Error, Debug)] 230 | pub enum DataSourceError { 231 | #[error("Couldn't open the data file")] 232 | FileOpen(#[source] io::Error), 233 | 234 | #[error("Couldn't copy the data to the target file descriptor")] 235 | Copy(#[source] io::Error), 236 | } 237 | 238 | struct State { 239 | common: common::State, 240 | got_primary_selection: bool, 241 | // This bool can be set to true when serving a request: either if an error occurs, or if the 242 | // number of requests to serve was limited and the last request was served. 243 | should_quit: bool, 244 | data_paths: HashMap, 245 | serve_requests: ServeRequests, 246 | // An error that occurred while serving a request, if any. 247 | error: Option, 248 | } 249 | 250 | delegate_dispatch!(State: [WlSeat: ()] => common::State); 251 | 252 | impl AsMut for State { 253 | fn as_mut(&mut self) -> &mut common::State { 254 | &mut self.common 255 | } 256 | } 257 | 258 | impl Dispatch for State { 259 | fn event( 260 | _state: &mut Self, 261 | _proxy: &WlRegistry, 262 | _event: ::Event, 263 | _data: &GlobalListContents, 264 | _conn: &wayland_client::Connection, 265 | _qhandle: &wayland_client::QueueHandle, 266 | ) { 267 | } 268 | } 269 | 270 | impl_dispatch_manager!(State); 271 | 272 | impl_dispatch_device!(State, WlSeat, |state: &mut Self, event, seat| { 273 | match event { 274 | Event::DataOffer { id } => id.destroy(), 275 | Event::Finished => { 276 | state.common.seats.get_mut(seat).unwrap().set_device(None); 277 | } 278 | Event::PrimarySelection { .. } => { 279 | state.got_primary_selection = true; 280 | } 281 | _ => (), 282 | } 283 | }); 284 | 285 | impl_dispatch_offer!(State); 286 | 287 | impl_dispatch_source!(State, |state: &mut Self, 288 | source: data_control::Source, 289 | event| { 290 | match event { 291 | Event::Send { mime_type, fd } => { 292 | // Check if some other source already handled a paste request and indicated that we should 293 | // quit. 294 | if state.should_quit { 295 | source.destroy(); 296 | return; 297 | } 298 | 299 | // I'm not sure if it's the compositor's responsibility to check that the mime type is 300 | // valid. Let's check here just in case. 301 | if !state.data_paths.contains_key(&mime_type) { 302 | return; 303 | } 304 | 305 | let data_path = &state.data_paths[&mime_type]; 306 | 307 | let file = File::open(data_path).map_err(DataSourceError::FileOpen); 308 | let result = file.and_then(|mut data_file| { 309 | // Clear O_NONBLOCK, otherwise io::copy() will stop halfway. 310 | fcntl_setfl(&fd, OFlags::empty()) 311 | .map_err(io::Error::from) 312 | .map_err(DataSourceError::Copy)?; 313 | 314 | let mut target_file = File::from(fd); 315 | io::copy(&mut data_file, &mut target_file).map_err(DataSourceError::Copy) 316 | }); 317 | 318 | if let Err(err) = result { 319 | state.error = Some(err); 320 | } 321 | 322 | let done = if let ServeRequests::Only(left) = state.serve_requests { 323 | let left = left.checked_sub(1).unwrap(); 324 | state.serve_requests = ServeRequests::Only(left); 325 | left == 0 326 | } else { 327 | false 328 | }; 329 | 330 | if done || state.error.is_some() { 331 | state.should_quit = true; 332 | source.destroy(); 333 | } 334 | } 335 | Event::Cancelled => source.destroy(), 336 | _ => (), 337 | } 338 | }); 339 | 340 | impl Options { 341 | /// Creates a blank new set of options ready for configuration. 342 | #[inline] 343 | pub fn new() -> Self { 344 | Self::default() 345 | } 346 | 347 | /// Sets the clipboard to work with. 348 | #[inline] 349 | pub fn clipboard(&mut self, clipboard: ClipboardType) -> &mut Self { 350 | self.clipboard = clipboard; 351 | self 352 | } 353 | 354 | /// Sets the seat to use for copying. 355 | #[inline] 356 | pub fn seat(&mut self, seat: Seat) -> &mut Self { 357 | self.seat = seat; 358 | self 359 | } 360 | 361 | /// Sets the flag for trimming the trailing newline. 362 | /// 363 | /// This flag is only applied for text MIME types. 364 | #[inline] 365 | pub fn trim_newline(&mut self, trim_newline: bool) -> &mut Self { 366 | self.trim_newline = trim_newline; 367 | self 368 | } 369 | 370 | /// Sets the flag for not spawning a separate thread for serving copy requests. 371 | /// 372 | /// Setting this flag will result in the call to `copy()` **blocking** until all data sources 373 | /// it creates are destroyed, e.g. until someone else copies something into the clipboard. 374 | #[inline] 375 | pub fn foreground(&mut self, foreground: bool) -> &mut Self { 376 | self.foreground = foreground; 377 | self 378 | } 379 | 380 | /// Sets the number of requests to serve. 381 | /// 382 | /// Limiting the number of requests to one effectively clears the clipboard after the first 383 | /// paste. It can be used when copying e.g. sensitive data, like passwords. Note however that 384 | /// certain apps may have issues pasting when this option is used, in particular XWayland 385 | /// clients are known to suffer from this. 386 | #[inline] 387 | pub fn serve_requests(&mut self, serve_requests: ServeRequests) -> &mut Self { 388 | self.serve_requests = serve_requests; 389 | self 390 | } 391 | 392 | /// Sets the flag for omitting additional text mime types which are offered by default if at least one text mime type is provided. 393 | /// 394 | /// Omits additionally offered `text/plain;charset=utf-8`, `text/plain`, `STRING`, `UTF8_STRING` and 395 | /// `TEXT` mime types which are offered by default if at least one text mime type is provided. 396 | #[inline] 397 | pub fn omit_additional_text_mime_types( 398 | &mut self, 399 | omit_additional_text_mime_types: bool, 400 | ) -> &mut Self { 401 | self.omit_additional_text_mime_types = omit_additional_text_mime_types; 402 | self 403 | } 404 | 405 | /// Invokes the copy operation. See `copy()`. 406 | /// 407 | /// # Examples 408 | /// 409 | /// ```no_run 410 | /// # extern crate wl_clipboard_rs; 411 | /// # use wl_clipboard_rs::copy::Error; 412 | /// # fn foo() -> Result<(), Error> { 413 | /// use wl_clipboard_rs::copy::{MimeType, Options, Source}; 414 | /// 415 | /// let opts = Options::new(); 416 | /// opts.copy(Source::Bytes([1, 2, 3][..].into()), MimeType::Autodetect)?; 417 | /// # Ok(()) 418 | /// # } 419 | /// ``` 420 | #[inline] 421 | pub fn copy(self, source: Source, mime_type: MimeType) -> Result<(), Error> { 422 | copy(self, source, mime_type) 423 | } 424 | 425 | /// Invokes the copy_multi operation. See `copy_multi()`. 426 | /// 427 | /// # Examples 428 | /// 429 | /// ```no_run 430 | /// # extern crate wl_clipboard_rs; 431 | /// # use wl_clipboard_rs::copy::Error; 432 | /// # fn foo() -> Result<(), Error> { 433 | /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source}; 434 | /// 435 | /// let opts = Options::new(); 436 | /// opts.copy_multi(vec![MimeSource { source: Source::Bytes([1, 2, 3][..].into()), 437 | /// mime_type: MimeType::Autodetect }, 438 | /// MimeSource { source: Source::Bytes([7, 8, 9][..].into()), 439 | /// mime_type: MimeType::Text }])?; 440 | /// # Ok(()) 441 | /// # } 442 | /// ``` 443 | #[inline] 444 | pub fn copy_multi(self, sources: Vec) -> Result<(), Error> { 445 | copy_multi(self, sources) 446 | } 447 | 448 | /// Invokes the prepare_copy operation. See `prepare_copy()`. 449 | /// 450 | /// # Panics 451 | /// 452 | /// Panics if `foreground` is `false`. 453 | /// 454 | /// # Examples 455 | /// 456 | /// ```no_run 457 | /// # extern crate wl_clipboard_rs; 458 | /// # use wl_clipboard_rs::copy::Error; 459 | /// # fn foo() -> Result<(), Error> { 460 | /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source}; 461 | /// 462 | /// let mut opts = Options::new(); 463 | /// opts.foreground(true); 464 | /// let prepared_copy = opts.prepare_copy(Source::Bytes([1, 2, 3][..].into()), 465 | /// MimeType::Autodetect)?; 466 | /// prepared_copy.serve()?; 467 | /// 468 | /// # Ok(()) 469 | /// # } 470 | /// ``` 471 | #[inline] 472 | pub fn prepare_copy(self, source: Source, mime_type: MimeType) -> Result { 473 | prepare_copy(self, source, mime_type) 474 | } 475 | 476 | /// Invokes the prepare_copy_multi operation. See `prepare_copy_multi()`. 477 | /// 478 | /// # Panics 479 | /// 480 | /// Panics if `foreground` is `false`. 481 | /// 482 | /// # Examples 483 | /// 484 | /// ```no_run 485 | /// # extern crate wl_clipboard_rs; 486 | /// # use wl_clipboard_rs::copy::Error; 487 | /// # fn foo() -> Result<(), Error> { 488 | /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source}; 489 | /// 490 | /// let mut opts = Options::new(); 491 | /// opts.foreground(true); 492 | /// let prepared_copy = 493 | /// opts.prepare_copy_multi(vec![MimeSource { source: Source::Bytes([1, 2, 3][..].into()), 494 | /// mime_type: MimeType::Autodetect }, 495 | /// MimeSource { source: Source::Bytes([7, 8, 9][..].into()), 496 | /// mime_type: MimeType::Text }])?; 497 | /// prepared_copy.serve()?; 498 | /// 499 | /// # Ok(()) 500 | /// # } 501 | /// ``` 502 | #[inline] 503 | pub fn prepare_copy_multi(self, sources: Vec) -> Result { 504 | prepare_copy_multi(self, sources) 505 | } 506 | } 507 | 508 | impl PreparedCopy { 509 | /// Starts serving copy requests. 510 | /// 511 | /// This function **blocks** until all requests are served or the clipboard is taken over by 512 | /// some other application. 513 | pub fn serve(mut self) -> Result<(), Error> { 514 | // Loop until we're done. 515 | while !self.state.should_quit { 516 | self.queue 517 | .blocking_dispatch(&mut self.state) 518 | .map_err(Error::WaylandCommunication)?; 519 | 520 | // Check if all sources have been destroyed. 521 | let all_destroyed = self.sources.iter().all(|x| !x.is_alive()); 522 | if all_destroyed { 523 | self.state.should_quit = true; 524 | } 525 | } 526 | 527 | // Clean up the temp file and directory. 528 | // 529 | // We want to try cleaning up all files and folders, so if any errors occur in process, 530 | // collect them into a vector without interruption, and then return the first one. 531 | let mut results = Vec::new(); 532 | let mut dropped = HashSet::new(); 533 | for data_path in self.state.data_paths.values_mut() { 534 | // data_paths can contain duplicate items, we want to free each only once. 535 | if dropped.contains(data_path) { 536 | continue; 537 | }; 538 | dropped.insert(data_path.clone()); 539 | 540 | match remove_file(&data_path).map_err(Error::TempFileRemove) { 541 | Ok(()) => { 542 | data_path.pop(); 543 | results.push(remove_dir(&data_path).map_err(Error::TempDirRemove)); 544 | } 545 | result @ Err(_) => results.push(result), 546 | } 547 | } 548 | 549 | // Return the error, if any. 550 | let result: Result<(), _> = results.into_iter().collect(); 551 | result?; 552 | 553 | // Check if an error occurred during data transfer. 554 | if let Some(err) = self.state.error.take() { 555 | return Err(Error::Paste(err)); 556 | } 557 | 558 | Ok(()) 559 | } 560 | } 561 | 562 | fn make_source( 563 | source: Source, 564 | mime_type: MimeType, 565 | trim_newline: bool, 566 | ) -> Result<(String, PathBuf), SourceCreationError> { 567 | let temp_dir = tempfile::tempdir().map_err(SourceCreationError::TempDirCreate)?; 568 | let mut temp_filename = temp_dir.into_path(); 569 | temp_filename.push("stdin"); 570 | trace!("Temp filename: {}", temp_filename.to_string_lossy()); 571 | let mut temp_file = 572 | File::create(&temp_filename).map_err(SourceCreationError::TempFileCreate)?; 573 | 574 | if let Source::Bytes(data) = source { 575 | temp_file 576 | .write_all(&data) 577 | .map_err(SourceCreationError::TempFileWrite)?; 578 | } else { 579 | // Copy the standard input into the target file. 580 | io::copy(&mut io::stdin(), &mut temp_file).map_err(SourceCreationError::DataCopy)?; 581 | } 582 | 583 | let mime_type = match mime_type { 584 | MimeType::Autodetect => match tree_magic_mini::from_filepath(&temp_filename) { 585 | Some(magic) => Ok(magic), 586 | None => Err(SourceCreationError::TempFileOpen(std::io::Error::new( 587 | std::io::ErrorKind::Other, 588 | "problem with temp file", 589 | ))), 590 | }? 591 | .to_string(), 592 | MimeType::Text => "text/plain".to_string(), 593 | MimeType::Specific(mime_type) => mime_type, 594 | }; 595 | 596 | trace!("Base MIME type: {}", mime_type); 597 | 598 | // Trim the trailing newline if needed. 599 | if trim_newline && is_text(&mime_type) { 600 | let mut temp_file = OpenOptions::new() 601 | .read(true) 602 | .write(true) 603 | .open(&temp_filename) 604 | .map_err(SourceCreationError::TempFileOpen)?; 605 | let metadata = temp_file 606 | .metadata() 607 | .map_err(SourceCreationError::TempFileMetadata)?; 608 | let length = metadata.len(); 609 | if length > 0 { 610 | temp_file 611 | .seek(SeekFrom::End(-1)) 612 | .map_err(SourceCreationError::TempFileSeek)?; 613 | 614 | let mut buf = [0]; 615 | temp_file 616 | .read_exact(&mut buf) 617 | .map_err(SourceCreationError::TempFileRead)?; 618 | if buf[0] == b'\n' { 619 | temp_file 620 | .set_len(length - 1) 621 | .map_err(SourceCreationError::TempFileTruncate)?; 622 | } 623 | } 624 | } 625 | 626 | Ok((mime_type, temp_filename)) 627 | } 628 | 629 | fn get_devices( 630 | primary: bool, 631 | seat: Seat, 632 | socket_name: Option, 633 | ) -> Result<(EventQueue, State, Vec), Error> { 634 | let (mut queue, mut common) = initialize(primary, socket_name)?; 635 | 636 | // Check if there are no seats. 637 | if common.seats.is_empty() { 638 | return Err(Error::NoSeats); 639 | } 640 | 641 | // Go through the seats and get their data devices. 642 | for (seat, data) in &mut common.seats { 643 | let device = common 644 | .clipboard_manager 645 | .get_data_device(seat, &queue.handle(), seat.clone()); 646 | data.set_device(Some(device)); 647 | } 648 | 649 | let mut state = State { 650 | common, 651 | got_primary_selection: false, 652 | should_quit: false, 653 | data_paths: HashMap::new(), 654 | serve_requests: ServeRequests::default(), 655 | error: None, 656 | }; 657 | 658 | // Retrieve all seat names. 659 | queue 660 | .roundtrip(&mut state) 661 | .map_err(Error::WaylandCommunication)?; 662 | 663 | // Check if the compositor supports primary selection. 664 | if primary && !state.got_primary_selection { 665 | return Err(Error::PrimarySelectionUnsupported); 666 | } 667 | 668 | // Figure out which devices we're interested in. 669 | let devices = state 670 | .common 671 | .seats 672 | .values() 673 | .filter_map(|data| { 674 | let SeatData { name, device, .. } = data; 675 | 676 | let device = device.clone(); 677 | 678 | match seat { 679 | Seat::All => { 680 | // If no seat was specified, handle all of them. 681 | return device; 682 | } 683 | Seat::Specific(ref desired_name) => { 684 | if name.as_deref() == Some(desired_name) { 685 | return device; 686 | } 687 | } 688 | } 689 | 690 | None 691 | }) 692 | .collect::>(); 693 | 694 | // If we didn't find the seat, print an error message and exit. 695 | // 696 | // This also triggers when we found the seat but it had no data device; is this what we want? 697 | if devices.is_empty() { 698 | return Err(Error::SeatNotFound); 699 | } 700 | 701 | Ok((queue, state, devices)) 702 | } 703 | 704 | /// Clears the clipboard for the given seat. 705 | /// 706 | /// If `seat` is `None`, clears clipboards of all existing seats. 707 | /// 708 | /// # Examples 709 | /// 710 | /// ```no_run 711 | /// # extern crate wl_clipboard_rs; 712 | /// # use wl_clipboard_rs::copy::Error; 713 | /// # fn foo() -> Result<(), Error> { 714 | /// use wl_clipboard_rs::{copy::{clear, ClipboardType, Seat}}; 715 | /// 716 | /// clear(ClipboardType::Regular, Seat::All)?; 717 | /// # Ok(()) 718 | /// # } 719 | /// ``` 720 | #[inline] 721 | pub fn clear(clipboard: ClipboardType, seat: Seat) -> Result<(), Error> { 722 | clear_internal(clipboard, seat, None) 723 | } 724 | 725 | pub(crate) fn clear_internal( 726 | clipboard: ClipboardType, 727 | seat: Seat, 728 | socket_name: Option, 729 | ) -> Result<(), Error> { 730 | let primary = clipboard != ClipboardType::Regular; 731 | let (mut queue, mut state, devices) = get_devices(primary, seat, socket_name)?; 732 | 733 | for device in devices { 734 | if clipboard == ClipboardType::Primary || clipboard == ClipboardType::Both { 735 | device.set_primary_selection(None); 736 | } 737 | if clipboard == ClipboardType::Regular || clipboard == ClipboardType::Both { 738 | device.set_selection(None); 739 | } 740 | } 741 | 742 | // We're clearing the clipboard so just do one roundtrip and quit. 743 | queue 744 | .roundtrip(&mut state) 745 | .map_err(Error::WaylandCommunication)?; 746 | 747 | Ok(()) 748 | } 749 | 750 | /// Prepares a data copy to the clipboard. 751 | /// 752 | /// The data is copied from `source` and offered in the `mime_type` MIME type. See `Options` for 753 | /// customizing the behavior of this operation. 754 | /// 755 | /// This function can be used instead of `copy()` when it's desirable to separately prepare the 756 | /// copy operation, handle any errors that this may produce, and then start the serving loop, 757 | /// potentially past a fork (which is how `wl-copy` uses it). It is meant to be used in the 758 | /// foreground mode and does not spawn any threads. 759 | /// 760 | /// # Panics 761 | /// 762 | /// Panics if `foreground` is `false`. 763 | /// 764 | /// # Examples 765 | /// 766 | /// ```no_run 767 | /// # extern crate wl_clipboard_rs; 768 | /// # use wl_clipboard_rs::copy::Error; 769 | /// # fn foo() -> Result<(), Error> { 770 | /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source}; 771 | /// 772 | /// let mut opts = Options::new(); 773 | /// opts.foreground(true); 774 | /// let prepared_copy = opts.prepare_copy(Source::Bytes([1, 2, 3][..].into()), 775 | /// MimeType::Autodetect)?; 776 | /// prepared_copy.serve()?; 777 | /// 778 | /// # Ok(()) 779 | /// # } 780 | /// ``` 781 | #[inline] 782 | pub fn prepare_copy( 783 | options: Options, 784 | source: Source, 785 | mime_type: MimeType, 786 | ) -> Result { 787 | assert!(options.foreground); 788 | 789 | let sources = vec![MimeSource { source, mime_type }]; 790 | 791 | prepare_copy_internal(options, sources, None) 792 | } 793 | 794 | /// Prepares a data copy to the clipboard, offering multiple data sources. 795 | /// 796 | /// The data from each source in `sources` is copied and offered in the corresponding MIME type. 797 | /// See `Options` for customizing the behavior of this operation. 798 | /// 799 | /// If multiple sources specify the same MIME type, the first one is offered. If one of the MIME 800 | /// types is text, all automatically added plain text offers will fall back to the first source 801 | /// with a text MIME type. 802 | /// 803 | /// This function can be used instead of `copy()` when it's desirable to separately prepare the 804 | /// copy operation, handle any errors that this may produce, and then start the serving loop, 805 | /// potentially past a fork (which is how `wl-copy` uses it). It is meant to be used in the 806 | /// foreground mode and does not spawn any threads. 807 | /// 808 | /// # Panics 809 | /// 810 | /// Panics if `foreground` is `false`. 811 | /// 812 | /// # Examples 813 | /// 814 | /// ```no_run 815 | /// # extern crate wl_clipboard_rs; 816 | /// # use wl_clipboard_rs::copy::Error; 817 | /// # fn foo() -> Result<(), Error> { 818 | /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source}; 819 | /// 820 | /// let mut opts = Options::new(); 821 | /// opts.foreground(true); 822 | /// let prepared_copy = 823 | /// opts.prepare_copy_multi(vec![MimeSource { source: Source::Bytes([1, 2, 3][..].into()), 824 | /// mime_type: MimeType::Autodetect }, 825 | /// MimeSource { source: Source::Bytes([7, 8, 9][..].into()), 826 | /// mime_type: MimeType::Text }])?; 827 | /// prepared_copy.serve()?; 828 | /// 829 | /// # Ok(()) 830 | /// # } 831 | /// ``` 832 | #[inline] 833 | pub fn prepare_copy_multi( 834 | options: Options, 835 | sources: Vec, 836 | ) -> Result { 837 | assert!(options.foreground); 838 | 839 | prepare_copy_internal(options, sources, None) 840 | } 841 | 842 | fn prepare_copy_internal( 843 | options: Options, 844 | sources: Vec, 845 | socket_name: Option, 846 | ) -> Result { 847 | let Options { 848 | clipboard, 849 | seat, 850 | trim_newline, 851 | serve_requests, 852 | .. 853 | } = options; 854 | 855 | let primary = clipboard != ClipboardType::Regular; 856 | let (queue, mut state, devices) = get_devices(primary, seat, socket_name)?; 857 | 858 | state.serve_requests = serve_requests; 859 | 860 | // Collect the source data to copy. 861 | state.data_paths = { 862 | let mut data_paths = HashMap::new(); 863 | let mut text_data_path = None; 864 | for MimeSource { source, mime_type } in sources.into_iter() { 865 | let (mime_type, mut data_path) = 866 | make_source(source, mime_type, trim_newline).map_err(Error::TempCopy)?; 867 | 868 | let mime_type_is_text = is_text(&mime_type); 869 | 870 | match data_paths.entry(mime_type) { 871 | Entry::Occupied(_) => { 872 | // This MIME type has already been specified, so ignore it. 873 | remove_file(&*data_path).map_err(Error::TempFileRemove)?; 874 | data_path.pop(); 875 | remove_dir(&*data_path).map_err(Error::TempDirRemove)?; 876 | } 877 | Entry::Vacant(entry) => { 878 | if !options.omit_additional_text_mime_types 879 | && text_data_path.is_none() 880 | && mime_type_is_text 881 | { 882 | text_data_path = Some(data_path.clone()); 883 | } 884 | 885 | entry.insert(data_path); 886 | } 887 | } 888 | } 889 | 890 | // If the MIME type is text, offer it in some other common formats. 891 | if let Some(text_data_path) = text_data_path { 892 | let text_mimes = [ 893 | "text/plain;charset=utf-8", 894 | "text/plain", 895 | "STRING", 896 | "UTF8_STRING", 897 | "TEXT", 898 | ]; 899 | for &mime_type in &text_mimes { 900 | // We don't want to overwrite an explicit mime type, because it might be bound to a 901 | // different data_path 902 | if !data_paths.contains_key(mime_type) { 903 | data_paths.insert(mime_type.to_string(), text_data_path.clone()); 904 | } 905 | } 906 | } 907 | data_paths 908 | }; 909 | 910 | // Create an iterator over (device, primary) for source creation later. 911 | // 912 | // This is needed because for ClipboardType::Both each device needs to appear twice because 913 | // separate data sources need to be made for the regular and the primary clipboards (data 914 | // sources cannot be reused). 915 | let devices_iter = devices.iter().flat_map(|device| { 916 | let first = match clipboard { 917 | ClipboardType::Regular => iter::once((device, false)), 918 | ClipboardType::Primary => iter::once((device, true)), 919 | ClipboardType::Both => iter::once((device, false)), 920 | }; 921 | 922 | let second = if clipboard == ClipboardType::Both { 923 | iter::once(Some((device, true))) 924 | } else { 925 | iter::once(None) 926 | }; 927 | 928 | first.chain(second.flatten()) 929 | }); 930 | 931 | // Create the data sources and set them as selections. 932 | let sources = devices_iter 933 | .map(|(device, primary)| { 934 | let data_source = state 935 | .common 936 | .clipboard_manager 937 | .create_data_source(&queue.handle()); 938 | 939 | for mime_type in state.data_paths.keys() { 940 | data_source.offer(mime_type.clone()); 941 | } 942 | 943 | if primary { 944 | device.set_primary_selection(Some(&data_source)); 945 | } else { 946 | device.set_selection(Some(&data_source)); 947 | } 948 | 949 | // If we need to serve 0 requests, kill the data source right away. 950 | if let ServeRequests::Only(0) = state.serve_requests { 951 | data_source.destroy(); 952 | } 953 | data_source 954 | }) 955 | .collect::>(); 956 | 957 | Ok(PreparedCopy { 958 | queue, 959 | state, 960 | sources, 961 | }) 962 | } 963 | 964 | /// Copies data to the clipboard. 965 | /// 966 | /// The data is copied from `source` and offered in the `mime_type` MIME type. See `Options` for 967 | /// customizing the behavior of this operation. 968 | /// 969 | /// # Examples 970 | /// 971 | /// ```no_run 972 | /// # extern crate wl_clipboard_rs; 973 | /// # use wl_clipboard_rs::copy::Error; 974 | /// # fn foo() -> Result<(), Error> { 975 | /// use wl_clipboard_rs::copy::{copy, MimeType, Options, Source}; 976 | /// 977 | /// let opts = Options::new(); 978 | /// copy(opts, Source::Bytes([1, 2, 3][..].into()), MimeType::Autodetect)?; 979 | /// # Ok(()) 980 | /// # } 981 | /// ``` 982 | #[inline] 983 | pub fn copy(options: Options, source: Source, mime_type: MimeType) -> Result<(), Error> { 984 | let sources = vec![MimeSource { source, mime_type }]; 985 | copy_internal(options, sources, None) 986 | } 987 | 988 | /// Copies data to the clipboard, offering multiple data sources. 989 | /// 990 | /// The data from each source in `sources` is copied and offered in the corresponding MIME type. 991 | /// See `Options` for customizing the behavior of this operation. 992 | /// 993 | /// If multiple sources specify the same MIME type, the first one is offered. If one of the MIME 994 | /// types is text, all automatically added plain text offers will fall back to the first source 995 | /// with a text MIME type. 996 | /// 997 | /// # Examples 998 | /// 999 | /// ```no_run 1000 | /// # extern crate wl_clipboard_rs; 1001 | /// # use wl_clipboard_rs::copy::Error; 1002 | /// # fn foo() -> Result<(), Error> { 1003 | /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source}; 1004 | /// 1005 | /// let opts = Options::new(); 1006 | /// opts.copy_multi(vec![MimeSource { source: Source::Bytes([1, 2, 3][..].into()), 1007 | /// mime_type: MimeType::Autodetect }, 1008 | /// MimeSource { source: Source::Bytes([7, 8, 9][..].into()), 1009 | /// mime_type: MimeType::Text }])?; 1010 | /// # Ok(()) 1011 | /// # } 1012 | /// ``` 1013 | #[inline] 1014 | pub fn copy_multi(options: Options, sources: Vec) -> Result<(), Error> { 1015 | copy_internal(options, sources, None) 1016 | } 1017 | 1018 | pub(crate) fn copy_internal( 1019 | options: Options, 1020 | sources: Vec, 1021 | socket_name: Option, 1022 | ) -> Result<(), Error> { 1023 | if options.foreground { 1024 | prepare_copy_internal(options, sources, socket_name)?.serve() 1025 | } else { 1026 | // The copy must be prepared on the thread because PreparedCopy isn't Send. 1027 | // To receive errors from prepare_copy, use a channel. 1028 | let (tx, rx) = sync_channel(1); 1029 | 1030 | thread::spawn( 1031 | move || match prepare_copy_internal(options, sources, socket_name) { 1032 | Ok(prepared_copy) => { 1033 | // prepare_copy completed successfully, report that. 1034 | drop(tx.send(None)); 1035 | 1036 | // There's nobody listening for errors at this point, just drop it. 1037 | drop(prepared_copy.serve()); 1038 | } 1039 | Err(err) => drop(tx.send(Some(err))), 1040 | }, 1041 | ); 1042 | 1043 | if let Some(err) = rx.recv().unwrap() { 1044 | return Err(err); 1045 | } 1046 | 1047 | Ok(()) 1048 | } 1049 | } 1050 | -------------------------------------------------------------------------------- /src/data_control.rs: -------------------------------------------------------------------------------- 1 | //! Abstraction over ext/wlr-data-control. 2 | 3 | use std::os::fd::BorrowedFd; 4 | 5 | use ext::ext_data_control_device_v1::ExtDataControlDeviceV1; 6 | use ext::ext_data_control_manager_v1::ExtDataControlManagerV1; 7 | use ext::ext_data_control_offer_v1::ExtDataControlOfferV1; 8 | use ext::ext_data_control_source_v1::ExtDataControlSourceV1; 9 | use wayland_client::protocol::wl_seat::WlSeat; 10 | use wayland_client::{Dispatch, Proxy as _, QueueHandle}; 11 | use wayland_protocols::ext::data_control::v1::client as ext; 12 | use wayland_protocols_wlr::data_control::v1::client as zwlr; 13 | use zwlr::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1; 14 | use zwlr::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; 15 | use zwlr::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1; 16 | use zwlr::zwlr_data_control_source_v1::ZwlrDataControlSourceV1; 17 | 18 | #[derive(Clone)] 19 | pub enum Manager { 20 | Zwlr(ZwlrDataControlManagerV1), 21 | Ext(ExtDataControlManagerV1), 22 | } 23 | 24 | #[derive(Clone)] 25 | pub enum Device { 26 | Zwlr(ZwlrDataControlDeviceV1), 27 | Ext(ExtDataControlDeviceV1), 28 | } 29 | 30 | #[derive(Clone)] 31 | pub enum Source { 32 | Zwlr(ZwlrDataControlSourceV1), 33 | Ext(ExtDataControlSourceV1), 34 | } 35 | 36 | #[derive(Clone, PartialEq, Eq, Hash)] 37 | pub enum Offer { 38 | Zwlr(ZwlrDataControlOfferV1), 39 | Ext(ExtDataControlOfferV1), 40 | } 41 | 42 | impl Manager { 43 | pub fn get_data_device(&self, seat: &WlSeat, qh: &QueueHandle, udata: U) -> Device 44 | where 45 | D: Dispatch + 'static, 46 | D: Dispatch + 'static, 47 | U: Send + Sync + 'static, 48 | { 49 | match self { 50 | Manager::Zwlr(manager) => Device::Zwlr(manager.get_data_device(seat, qh, udata)), 51 | Manager::Ext(manager) => Device::Ext(manager.get_data_device(seat, qh, udata)), 52 | } 53 | } 54 | 55 | pub fn create_data_source(&self, qh: &QueueHandle) -> Source 56 | where 57 | D: Dispatch + 'static, 58 | D: Dispatch + 'static, 59 | { 60 | match self { 61 | Manager::Zwlr(manager) => Source::Zwlr(manager.create_data_source(qh, ())), 62 | Manager::Ext(manager) => Source::Ext(manager.create_data_source(qh, ())), 63 | } 64 | } 65 | } 66 | 67 | impl Device { 68 | pub fn destroy(&self) { 69 | match self { 70 | Device::Zwlr(device) => device.destroy(), 71 | Device::Ext(device) => device.destroy(), 72 | } 73 | } 74 | 75 | #[track_caller] 76 | pub fn set_selection(&self, source: Option<&Source>) { 77 | match self { 78 | Device::Zwlr(device) => device.set_selection(source.map(Source::zwlr)), 79 | Device::Ext(device) => device.set_selection(source.map(Source::ext)), 80 | } 81 | } 82 | 83 | #[track_caller] 84 | pub fn set_primary_selection(&self, source: Option<&Source>) { 85 | match self { 86 | Device::Zwlr(device) => device.set_primary_selection(source.map(Source::zwlr)), 87 | Device::Ext(device) => device.set_primary_selection(source.map(Source::ext)), 88 | } 89 | } 90 | } 91 | 92 | impl Source { 93 | pub fn destroy(&self) { 94 | match self { 95 | Source::Zwlr(source) => source.destroy(), 96 | Source::Ext(source) => source.destroy(), 97 | } 98 | } 99 | 100 | pub fn offer(&self, mime_type: String) { 101 | match self { 102 | Source::Zwlr(source) => source.offer(mime_type), 103 | Source::Ext(source) => source.offer(mime_type), 104 | } 105 | } 106 | 107 | pub fn is_alive(&self) -> bool { 108 | match self { 109 | Source::Zwlr(source) => source.is_alive(), 110 | Source::Ext(source) => source.is_alive(), 111 | } 112 | } 113 | 114 | #[track_caller] 115 | pub fn zwlr(&self) -> &ZwlrDataControlSourceV1 { 116 | if let Self::Zwlr(v) = self { 117 | v 118 | } else { 119 | panic!("tried to convert non-Zwlr Source to Zwlr") 120 | } 121 | } 122 | 123 | #[track_caller] 124 | pub fn ext(&self) -> &ExtDataControlSourceV1 { 125 | if let Self::Ext(v) = self { 126 | v 127 | } else { 128 | panic!("tried to convert non-Ext Source to Ext") 129 | } 130 | } 131 | } 132 | 133 | impl Offer { 134 | pub fn destroy(&self) { 135 | match self { 136 | Offer::Zwlr(offer) => offer.destroy(), 137 | Offer::Ext(offer) => offer.destroy(), 138 | } 139 | } 140 | 141 | pub fn receive(&self, mime_type: String, fd: BorrowedFd) { 142 | match self { 143 | Offer::Zwlr(offer) => offer.receive(mime_type, fd), 144 | Offer::Ext(offer) => offer.receive(mime_type, fd), 145 | } 146 | } 147 | } 148 | 149 | impl From for Source { 150 | fn from(v: ZwlrDataControlSourceV1) -> Self { 151 | Self::Zwlr(v) 152 | } 153 | } 154 | 155 | impl From for Source { 156 | fn from(v: ExtDataControlSourceV1) -> Self { 157 | Self::Ext(v) 158 | } 159 | } 160 | 161 | impl From for Offer { 162 | fn from(v: ZwlrDataControlOfferV1) -> Self { 163 | Self::Zwlr(v) 164 | } 165 | } 166 | 167 | impl From for Offer { 168 | fn from(v: ExtDataControlOfferV1) -> Self { 169 | Self::Ext(v) 170 | } 171 | } 172 | 173 | // Some mildly cursed macros to avoid code duplication. 174 | macro_rules! impl_dispatch_manager { 175 | ($handler:ty => [$($iface:ty),*]) => { 176 | $( 177 | impl Dispatch<$iface, ()> for $handler { 178 | fn event( 179 | _state: &mut Self, 180 | _proxy: &$iface, 181 | _event: <$iface as wayland_client::Proxy>::Event, 182 | _data: &(), 183 | _conn: &wayland_client::Connection, 184 | _qhandle: &wayland_client::QueueHandle, 185 | ) { 186 | } 187 | } 188 | )* 189 | }; 190 | 191 | ($handler:ty) => { 192 | impl_dispatch_manager!($handler => [ 193 | wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1, 194 | wayland_protocols::ext::data_control::v1::client::ext_data_control_manager_v1::ExtDataControlManagerV1 195 | ]); 196 | }; 197 | } 198 | pub(crate) use impl_dispatch_manager; 199 | 200 | macro_rules! impl_dispatch_device { 201 | ($handler:ty, $udata:ty, $code:expr => [$(($iface:ty, $opcode:path, $offer:ty)),*]) => { 202 | $( 203 | impl Dispatch<$iface, $udata> for $handler { 204 | fn event( 205 | state: &mut Self, 206 | _proxy: &$iface, 207 | event: <$iface as wayland_client::Proxy>::Event, 208 | data: &$udata, 209 | _conn: &wayland_client::Connection, 210 | _qhandle: &wayland_client::QueueHandle, 211 | ) { 212 | type Event = <$iface as wayland_client::Proxy>::Event; 213 | 214 | ($code)(state, event, data) 215 | } 216 | 217 | event_created_child!($handler, $iface, [ 218 | $opcode => ($offer, ()), 219 | ]); 220 | } 221 | )* 222 | }; 223 | 224 | ($handler:ty, $udata:ty, $code:expr) => { 225 | impl_dispatch_device!($handler, $udata, $code => [ 226 | ( 227 | wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1, 228 | wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::EVT_DATA_OFFER_OPCODE, 229 | wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1 230 | ), 231 | ( 232 | wayland_protocols::ext::data_control::v1::client::ext_data_control_device_v1::ExtDataControlDeviceV1, 233 | wayland_protocols::ext::data_control::v1::client::ext_data_control_device_v1::EVT_DATA_OFFER_OPCODE, 234 | wayland_protocols::ext::data_control::v1::client::ext_data_control_offer_v1::ExtDataControlOfferV1 235 | ) 236 | ]); 237 | }; 238 | } 239 | pub(crate) use impl_dispatch_device; 240 | 241 | macro_rules! impl_dispatch_source { 242 | ($handler:ty, $code:expr => [$($iface:ty),*]) => { 243 | $( 244 | impl Dispatch<$iface, ()> for $handler { 245 | fn event( 246 | state: &mut Self, 247 | proxy: &$iface, 248 | event: <$iface as wayland_client::Proxy>::Event, 249 | _data: &(), 250 | _conn: &wayland_client::Connection, 251 | _qhandle: &wayland_client::QueueHandle, 252 | ) { 253 | type Event = <$iface as wayland_client::Proxy>::Event; 254 | 255 | let source = $crate::data_control::Source::from(proxy.clone()); 256 | ($code)(state, source, event) 257 | } 258 | } 259 | )* 260 | }; 261 | 262 | ($handler:ty, $code:expr) => { 263 | impl_dispatch_source!($handler, $code => [ 264 | wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1, 265 | wayland_protocols::ext::data_control::v1::client::ext_data_control_source_v1::ExtDataControlSourceV1 266 | ]); 267 | }; 268 | } 269 | pub(crate) use impl_dispatch_source; 270 | 271 | macro_rules! impl_dispatch_offer { 272 | ($handler:ty, $code:expr => [$($iface:ty),*]) => { 273 | $( 274 | impl Dispatch<$iface, ()> for $handler { 275 | fn event( 276 | state: &mut Self, 277 | proxy: &$iface, 278 | event: <$iface as wayland_client::Proxy>::Event, 279 | _data: &(), 280 | _conn: &wayland_client::Connection, 281 | _qhandle: &wayland_client::QueueHandle, 282 | ) { 283 | type Event = <$iface as wayland_client::Proxy>::Event; 284 | 285 | let offer = $crate::data_control::Offer::from(proxy.clone()); 286 | ($code)(state, offer, event) 287 | } 288 | } 289 | )* 290 | }; 291 | 292 | ($handler:ty, $code:expr) => { 293 | impl_dispatch_offer!($handler, $code => [ 294 | wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1, 295 | wayland_protocols::ext::data_control::v1::client::ext_data_control_offer_v1::ExtDataControlOfferV1 296 | ]); 297 | }; 298 | 299 | ($handler:ty) => { 300 | impl_dispatch_offer!($handler, |_, _, _: Event| ()); 301 | }; 302 | } 303 | pub(crate) use impl_dispatch_offer; 304 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A safe Rust crate for working with the Wayland clipboard. 2 | //! 3 | //! This crate is intended to be used by terminal applications, clipboard managers and other 4 | //! utilities which don't spawn Wayland surfaces (windows). If your application has a window, 5 | //! please use the appropriate Wayland protocols for interacting with the Wayland clipboard 6 | //! (`wl_data_device` from the core Wayland protocol, the `primary_selection` protocol for the 7 | //! primary selection), for example via the 8 | //! [smithay-clipboard](https://crates.io/crates/smithay-clipboard) crate. 9 | //! 10 | //! The protocol used for clipboard interaction is `ext-data-control` or `wlr-data-control`. When 11 | //! using the regular clipboard, the compositor must support any version of either protocol. When 12 | //! using the "primary" clipboard, the compositor must support any version of `ext-data-control`, 13 | //! or the second version of the `wlr-data-control` protocol. 14 | //! 15 | //! For example applications using these features, see `wl-clipboard-rs-tools/src/bin/wl_copy.rs` 16 | //! and `wl-clipboard-rs-tools/src/bin/wl_paste.rs` which implement terminal apps similar to 17 | //! [wl-clipboard](https://github.com/bugaevc/wl-clipboard) or 18 | //! `wl-clipboard-rs-tools/src/bin/wl_clip.rs` which implements a Wayland version of `xclip`. 19 | //! 20 | //! The Rust implementation of the Wayland client is used by default; use the `native_lib` feature 21 | //! to link to `libwayland-client.so` for communication instead. A `dlopen` feature is also 22 | //! available for loading `libwayland-client.so` dynamically at runtime rather than linking to it. 23 | //! 24 | //! The code of the crate itself (and the code of the example utilities) is 100% safe Rust. This 25 | //! doesn't include the dependencies. 26 | //! 27 | //! # Examples 28 | //! 29 | //! Copying to the regular clipboard: 30 | //! ```no_run 31 | //! # extern crate wl_clipboard_rs; 32 | //! # fn foo() -> Result<(), Box> { 33 | //! use wl_clipboard_rs::copy::{MimeType, Options, Source}; 34 | //! 35 | //! let opts = Options::new(); 36 | //! opts.copy(Source::Bytes("Hello world!".to_string().into_bytes().into()), MimeType::Autodetect)?; 37 | //! # Ok(()) 38 | //! # } 39 | //! ``` 40 | //! 41 | //! Pasting plain text from the regular clipboard: 42 | //! ```no_run 43 | //! # extern crate wl_clipboard_rs; 44 | //! # fn foo() -> Result<(), Box> { 45 | //! use std::io::Read; 46 | //! use wl_clipboard_rs::{paste::{get_contents, ClipboardType, Error, MimeType, Seat}}; 47 | //! 48 | //! let result = get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Text); 49 | //! match result { 50 | //! Ok((mut pipe, _)) => { 51 | //! let mut contents = vec![]; 52 | //! pipe.read_to_end(&mut contents)?; 53 | //! println!("Pasted: {}", String::from_utf8_lossy(&contents)); 54 | //! } 55 | //! 56 | //! Err(Error::NoSeats) | Err(Error::ClipboardEmpty) | Err(Error::NoMimeType) => { 57 | //! // The clipboard is empty or doesn't contain text, nothing to worry about. 58 | //! } 59 | //! 60 | //! Err(err) => Err(err)? 61 | //! } 62 | //! # Ok(()) 63 | //! # } 64 | //! ``` 65 | //! 66 | //! Checking if the "primary" clipboard is supported (note that this might be unnecessary depending 67 | //! on your crate usage, the regular copying and pasting functions do report if the primary 68 | //! selection is unsupported when it is requested): 69 | //! 70 | //! ```no_run 71 | //! # extern crate wl_clipboard_rs; 72 | //! # fn foo() -> Result<(), Box> { 73 | //! use wl_clipboard_rs::utils::{is_primary_selection_supported, PrimarySelectionCheckError}; 74 | //! 75 | //! match is_primary_selection_supported() { 76 | //! Ok(supported) => { 77 | //! // We have our definitive result. False means that ext/wlr-data-control is present 78 | //! // and did not signal the primary selection support, or that only wlr-data-control 79 | //! // version 1 is present (which does not support primary selection). 80 | //! }, 81 | //! Err(PrimarySelectionCheckError::NoSeats) => { 82 | //! // Impossible to give a definitive result. Primary selection may or may not be 83 | //! // supported. 84 | //! 85 | //! // The required protocol (ext-data-control, or wlr-data-control version 2) is there, 86 | //! // but there are no seats. Unfortunately, at least one seat is needed to check for the 87 | //! // primary clipboard support. 88 | //! }, 89 | //! Err(PrimarySelectionCheckError::MissingProtocol) => { 90 | //! // The data-control protocol (required for wl-clipboard-rs operation) is not 91 | //! // supported by the compositor. 92 | //! }, 93 | //! Err(_) => { 94 | //! // Some communication error occurred. 95 | //! } 96 | //! } 97 | //! # Ok(()) 98 | //! # } 99 | //! ``` 100 | //! 101 | //! # Included terminal utilities 102 | //! 103 | //! - `wl-paste`: implements `wl-paste` from 104 | //! [wl-clipboard](https://github.com/bugaevc/wl-clipboard). 105 | //! - `wl-copy`: implements `wl-copy` from [wl-clipboard](https://github.com/bugaevc/wl-clipboard). 106 | //! - `wl-clip`: a Wayland version of `xclip`. 107 | 108 | #![doc(html_root_url = "https://docs.rs/wl-clipboard-rs/0.9.2")] 109 | #![deny(unsafe_code)] 110 | 111 | mod common; 112 | mod data_control; 113 | mod seat_data; 114 | 115 | #[cfg(test)] 116 | #[allow(unsafe_code)] // It's more convenient for testing some stuff. 117 | mod tests; 118 | 119 | pub mod copy; 120 | pub mod paste; 121 | pub mod utils; 122 | -------------------------------------------------------------------------------- /src/paste.rs: -------------------------------------------------------------------------------- 1 | //! Getting the offered MIME types and the clipboard contents. 2 | 3 | use std::collections::{HashMap, HashSet}; 4 | use std::ffi::OsString; 5 | use std::io; 6 | use std::os::fd::AsFd; 7 | 8 | use os_pipe::{pipe, PipeReader}; 9 | use wayland_client::globals::GlobalListContents; 10 | use wayland_client::protocol::wl_registry::WlRegistry; 11 | use wayland_client::protocol::wl_seat::WlSeat; 12 | use wayland_client::{ 13 | delegate_dispatch, event_created_child, ConnectError, Dispatch, DispatchError, EventQueue, 14 | }; 15 | 16 | use crate::common::{self, initialize}; 17 | use crate::data_control::{self, impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer}; 18 | use crate::utils::is_text; 19 | 20 | /// The clipboard to operate on. 21 | #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)] 22 | #[cfg_attr(test, derive(proptest_derive::Arbitrary))] 23 | pub enum ClipboardType { 24 | /// The regular clipboard. 25 | #[default] 26 | Regular, 27 | /// The "primary" clipboard. 28 | /// 29 | /// Working with the "primary" clipboard requires the compositor to support ext-data-control, 30 | /// or wlr-data-control version 2 or above. 31 | Primary, 32 | } 33 | 34 | /// MIME types that can be requested from the clipboard. 35 | #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)] 36 | pub enum MimeType<'a> { 37 | /// Request any available MIME type. 38 | /// 39 | /// If multiple MIME types are offered, the requested MIME type is unspecified and depends on 40 | /// the order they are received from the Wayland compositor. However, plain text formats are 41 | /// prioritized, so if a plain text format is available among others then it will be requested. 42 | Any, 43 | /// Request a plain text MIME type. 44 | /// 45 | /// This will request one of the multiple common plain text MIME types. It will prioritize MIME 46 | /// types known to return UTF-8 text. 47 | Text, 48 | /// Request the given MIME type, and if it's not available fall back to `MimeType::Text`. 49 | /// 50 | /// Example use-case: pasting `text/html` should try `text/html` first, but if it's not 51 | /// available, any other plain text format will do fine too. 52 | TextWithPriority(&'a str), 53 | /// Request a specific MIME type. 54 | Specific(&'a str), 55 | } 56 | 57 | /// Seat to operate on. 58 | #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)] 59 | pub enum Seat<'a> { 60 | /// Operate on one of the existing seats depending on the order returned by the compositor. 61 | /// 62 | /// This is perfectly fine when only a single seat is present, so for most configurations. 63 | #[default] 64 | Unspecified, 65 | /// Operate on a seat with the given name. 66 | Specific(&'a str), 67 | } 68 | 69 | struct State { 70 | common: common::State, 71 | // The value is the set of MIME types in the offer. 72 | // TODO: We never remove offers from here, even if we don't use them or after destroying them. 73 | offers: HashMap>, 74 | got_primary_selection: bool, 75 | } 76 | 77 | delegate_dispatch!(State: [WlSeat: ()] => common::State); 78 | 79 | impl AsMut for State { 80 | fn as_mut(&mut self) -> &mut common::State { 81 | &mut self.common 82 | } 83 | } 84 | 85 | /// Errors that can occur for pasting and listing MIME types. 86 | /// 87 | /// You may want to ignore some of these errors (rather than show an error message), like 88 | /// `NoSeats`, `ClipboardEmpty` or `NoMimeType` as they are essentially equivalent to an empty 89 | /// clipboard. 90 | #[derive(thiserror::Error, Debug)] 91 | pub enum Error { 92 | #[error("There are no seats")] 93 | NoSeats, 94 | 95 | #[error("The clipboard of the requested seat is empty")] 96 | ClipboardEmpty, 97 | 98 | #[error("No suitable type of content copied")] 99 | NoMimeType, 100 | 101 | #[error("Couldn't open the provided Wayland socket")] 102 | SocketOpenError(#[source] io::Error), 103 | 104 | #[error("Couldn't connect to the Wayland compositor")] 105 | WaylandConnection(#[source] ConnectError), 106 | 107 | #[error("Wayland compositor communication error")] 108 | WaylandCommunication(#[source] DispatchError), 109 | 110 | #[error( 111 | "A required Wayland protocol ({} version {}) is not supported by the compositor", 112 | name, 113 | version 114 | )] 115 | MissingProtocol { name: &'static str, version: u32 }, 116 | 117 | #[error("The compositor does not support primary selection")] 118 | PrimarySelectionUnsupported, 119 | 120 | #[error("The requested seat was not found")] 121 | SeatNotFound, 122 | 123 | #[error("Couldn't create a pipe for content transfer")] 124 | PipeCreation(#[source] io::Error), 125 | } 126 | 127 | impl From for Error { 128 | fn from(x: common::Error) -> Self { 129 | use common::Error::*; 130 | 131 | match x { 132 | SocketOpenError(err) => Error::SocketOpenError(err), 133 | WaylandConnection(err) => Error::WaylandConnection(err), 134 | WaylandCommunication(err) => Error::WaylandCommunication(err.into()), 135 | MissingProtocol { name, version } => Error::MissingProtocol { name, version }, 136 | } 137 | } 138 | } 139 | 140 | impl Dispatch for State { 141 | fn event( 142 | _state: &mut Self, 143 | _proxy: &WlRegistry, 144 | _event: ::Event, 145 | _data: &GlobalListContents, 146 | _conn: &wayland_client::Connection, 147 | _qhandle: &wayland_client::QueueHandle, 148 | ) { 149 | } 150 | } 151 | 152 | impl_dispatch_manager!(State); 153 | 154 | impl_dispatch_device!(State, WlSeat, |state: &mut Self, event, seat| { 155 | match event { 156 | Event::DataOffer { id } => { 157 | let offer = data_control::Offer::from(id); 158 | state.offers.insert(offer, HashSet::new()); 159 | } 160 | Event::Selection { id } => { 161 | let offer = id.map(data_control::Offer::from); 162 | let seat = state.common.seats.get_mut(seat).unwrap(); 163 | seat.set_offer(offer); 164 | } 165 | Event::Finished => { 166 | // Destroy the device stored in the seat as it's no longer valid. 167 | let seat = state.common.seats.get_mut(seat).unwrap(); 168 | seat.set_device(None); 169 | } 170 | Event::PrimarySelection { id } => { 171 | let offer = id.map(data_control::Offer::from); 172 | state.got_primary_selection = true; 173 | let seat = state.common.seats.get_mut(seat).unwrap(); 174 | seat.set_primary_offer(offer); 175 | } 176 | _ => (), 177 | } 178 | }); 179 | 180 | impl_dispatch_offer!(State, |state: &mut Self, 181 | offer: data_control::Offer, 182 | event| { 183 | if let Event::Offer { mime_type } = event { 184 | state.offers.get_mut(&offer).unwrap().insert(mime_type); 185 | } 186 | }); 187 | 188 | fn get_offer( 189 | primary: bool, 190 | seat: Seat<'_>, 191 | socket_name: Option, 192 | ) -> Result<(EventQueue, State, data_control::Offer), Error> { 193 | let (mut queue, mut common) = initialize(primary, socket_name)?; 194 | 195 | // Check if there are no seats. 196 | if common.seats.is_empty() { 197 | return Err(Error::NoSeats); 198 | } 199 | 200 | // Go through the seats and get their data devices. 201 | for (seat, data) in &mut common.seats { 202 | let device = common 203 | .clipboard_manager 204 | .get_data_device(seat, &queue.handle(), seat.clone()); 205 | data.set_device(Some(device)); 206 | } 207 | 208 | let mut state = State { 209 | common, 210 | offers: HashMap::new(), 211 | got_primary_selection: false, 212 | }; 213 | 214 | // Retrieve all seat names and offers. 215 | queue 216 | .roundtrip(&mut state) 217 | .map_err(Error::WaylandCommunication)?; 218 | 219 | // Check if the compositor supports primary selection. 220 | if primary && !state.got_primary_selection { 221 | return Err(Error::PrimarySelectionUnsupported); 222 | } 223 | 224 | // Figure out which offer we're interested in. 225 | let data = match seat { 226 | Seat::Unspecified => state.common.seats.values().next(), 227 | Seat::Specific(name) => state 228 | .common 229 | .seats 230 | .values() 231 | .find(|data| data.name.as_deref() == Some(name)), 232 | }; 233 | 234 | let Some(data) = data else { 235 | return Err(Error::SeatNotFound); 236 | }; 237 | 238 | let offer = if primary { 239 | &data.primary_offer 240 | } else { 241 | &data.offer 242 | }; 243 | 244 | // Check if we found anything. 245 | match offer.clone() { 246 | Some(offer) => Ok((queue, state, offer)), 247 | None => Err(Error::ClipboardEmpty), 248 | } 249 | } 250 | 251 | /// Retrieves the offered MIME types. 252 | /// 253 | /// If `seat` is `None`, uses an unspecified seat (it depends on the order returned by the 254 | /// compositor). This is perfectly fine when only a single seat is present, so for most 255 | /// configurations. 256 | /// 257 | /// # Examples 258 | /// 259 | /// ```no_run 260 | /// # extern crate wl_clipboard_rs; 261 | /// # use wl_clipboard_rs::paste::Error; 262 | /// # fn foo() -> Result<(), Error> { 263 | /// use wl_clipboard_rs::{paste::{get_mime_types, ClipboardType, Seat}}; 264 | /// 265 | /// let mime_types = get_mime_types(ClipboardType::Regular, Seat::Unspecified)?; 266 | /// for mime_type in mime_types { 267 | /// println!("{}", mime_type); 268 | /// } 269 | /// # Ok(()) 270 | /// # } 271 | /// ``` 272 | #[inline] 273 | pub fn get_mime_types(clipboard: ClipboardType, seat: Seat<'_>) -> Result, Error> { 274 | get_mime_types_internal(clipboard, seat, None) 275 | } 276 | 277 | // The internal function accepts the socket name, used for tests. 278 | pub(crate) fn get_mime_types_internal( 279 | clipboard: ClipboardType, 280 | seat: Seat<'_>, 281 | socket_name: Option, 282 | ) -> Result, Error> { 283 | let primary = clipboard == ClipboardType::Primary; 284 | let (_, mut state, offer) = get_offer(primary, seat, socket_name)?; 285 | Ok(state.offers.remove(&offer).unwrap()) 286 | } 287 | 288 | /// Retrieves the clipboard contents. 289 | /// 290 | /// This function returns a tuple of the reading end of a pipe containing the clipboard contents 291 | /// and the actual MIME type of the contents. 292 | /// 293 | /// If `seat` is `None`, uses an unspecified seat (it depends on the order returned by the 294 | /// compositor). This is perfectly fine when only a single seat is present, so for most 295 | /// configurations. 296 | /// 297 | /// # Examples 298 | /// 299 | /// ```no_run 300 | /// # extern crate wl_clipboard_rs; 301 | /// # fn foo() -> Result<(), Box> { 302 | /// use std::io::Read; 303 | /// use wl_clipboard_rs::{paste::{get_contents, ClipboardType, Error, MimeType, Seat}}; 304 | /// 305 | /// let result = get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Any); 306 | /// match result { 307 | /// Ok((mut pipe, mime_type)) => { 308 | /// println!("Got data of the {} MIME type", &mime_type); 309 | /// 310 | /// let mut contents = vec![]; 311 | /// pipe.read_to_end(&mut contents)?; 312 | /// println!("Read {} bytes of data", contents.len()); 313 | /// } 314 | /// 315 | /// Err(Error::NoSeats) | Err(Error::ClipboardEmpty) | Err(Error::NoMimeType) => { 316 | /// // The clipboard is empty, nothing to worry about. 317 | /// } 318 | /// 319 | /// Err(err) => Err(err)? 320 | /// } 321 | /// # Ok(()) 322 | /// # } 323 | /// ``` 324 | #[inline] 325 | pub fn get_contents( 326 | clipboard: ClipboardType, 327 | seat: Seat<'_>, 328 | mime_type: MimeType<'_>, 329 | ) -> Result<(PipeReader, String), Error> { 330 | get_contents_internal(clipboard, seat, mime_type, None) 331 | } 332 | 333 | // The internal function accepts the socket name, used for tests. 334 | pub(crate) fn get_contents_internal( 335 | clipboard: ClipboardType, 336 | seat: Seat<'_>, 337 | mime_type: MimeType<'_>, 338 | socket_name: Option, 339 | ) -> Result<(PipeReader, String), Error> { 340 | let primary = clipboard == ClipboardType::Primary; 341 | let (mut queue, mut state, offer) = get_offer(primary, seat, socket_name)?; 342 | 343 | let mut mime_types = state.offers.remove(&offer).unwrap(); 344 | 345 | // Find the desired MIME type. 346 | let mime_type = match mime_type { 347 | MimeType::Any => mime_types 348 | .take("text/plain;charset=utf-8") 349 | .or_else(|| mime_types.take("UTF8_STRING")) 350 | .or_else(|| mime_types.iter().find(|x| is_text(x)).cloned()) 351 | .or_else(|| mime_types.drain().next()), 352 | MimeType::Text => mime_types 353 | .take("text/plain;charset=utf-8") 354 | .or_else(|| mime_types.take("UTF8_STRING")) 355 | .or_else(|| mime_types.drain().find(|x| is_text(x))), 356 | MimeType::TextWithPriority(priority) => mime_types 357 | .take(priority) 358 | .or_else(|| mime_types.take("text/plain;charset=utf-8")) 359 | .or_else(|| mime_types.take("UTF8_STRING")) 360 | .or_else(|| mime_types.drain().find(|x| is_text(x))), 361 | MimeType::Specific(mime_type) => mime_types.take(mime_type), 362 | }; 363 | 364 | // Check if a suitable MIME type is copied. 365 | if mime_type.is_none() { 366 | return Err(Error::NoMimeType); 367 | } 368 | 369 | let mime_type = mime_type.unwrap(); 370 | 371 | // Create a pipe for content transfer. 372 | let (read, write) = pipe().map_err(Error::PipeCreation)?; 373 | 374 | // Start the transfer. 375 | offer.receive(mime_type.clone(), write.as_fd()); 376 | drop(write); 377 | 378 | // A flush() is not enough here, it will result in sometimes pasting empty contents. I suspect this is due to a 379 | // race between the compositor reacting to the receive request, and the compositor reacting to wl-paste 380 | // disconnecting after queue is dropped. The roundtrip solves that race. 381 | queue 382 | .roundtrip(&mut state) 383 | .map_err(Error::WaylandCommunication)?; 384 | 385 | Ok((read, mime_type)) 386 | } 387 | -------------------------------------------------------------------------------- /src/seat_data.rs: -------------------------------------------------------------------------------- 1 | use crate::data_control::{Device, Offer}; 2 | 3 | #[derive(Default)] 4 | pub struct SeatData { 5 | /// The name of this seat, if any. 6 | pub name: Option, 7 | 8 | /// The data device of this seat, if any. 9 | pub device: Option, 10 | 11 | /// The data offer of this seat, if any. 12 | pub offer: Option, 13 | 14 | /// The primary-selection data offer of this seat, if any. 15 | pub primary_offer: Option, 16 | } 17 | 18 | impl SeatData { 19 | /// Sets this seat's name. 20 | pub fn set_name(&mut self, name: String) { 21 | self.name = Some(name) 22 | } 23 | 24 | /// Sets this seat's device. 25 | /// 26 | /// Destroys the old one, if any. 27 | pub fn set_device(&mut self, device: Option) { 28 | let old_device = self.device.take(); 29 | self.device = device; 30 | 31 | if let Some(device) = old_device { 32 | device.destroy(); 33 | } 34 | } 35 | 36 | /// Sets this seat's data offer. 37 | /// 38 | /// Destroys the old one, if any. 39 | pub fn set_offer(&mut self, new_offer: Option) { 40 | let old_offer = self.offer.take(); 41 | self.offer = new_offer; 42 | 43 | if let Some(offer) = old_offer { 44 | offer.destroy(); 45 | } 46 | } 47 | 48 | /// Sets this seat's primary-selection data offer. 49 | /// 50 | /// Destroys the old one, if any. 51 | pub fn set_primary_offer(&mut self, new_offer: Option) { 52 | let old_offer = self.primary_offer.take(); 53 | self.primary_offer = new_offer; 54 | 55 | if let Some(offer) = old_offer { 56 | offer.destroy(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/tests/copy.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io::Read; 3 | use std::sync::mpsc::channel; 4 | use std::sync::{Arc, Mutex}; 5 | 6 | use proptest::prelude::*; 7 | use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; 8 | 9 | use crate::copy::*; 10 | use crate::paste; 11 | use crate::paste::get_contents_internal; 12 | use crate::tests::state::*; 13 | use crate::tests::TestServer; 14 | 15 | #[test] 16 | fn clear_test() { 17 | let server = TestServer::new(); 18 | server 19 | .display 20 | .handle() 21 | .create_global::(2, ()); 22 | 23 | let state = State { 24 | seats: HashMap::from([( 25 | "seat0".into(), 26 | SeatInfo { 27 | offer: Some(OfferInfo::Buffered { 28 | data: HashMap::from([("regular".into(), vec![1, 2, 3])]), 29 | }), 30 | primary_offer: Some(OfferInfo::Buffered { 31 | data: HashMap::from([("primary".into(), vec![1, 2, 3])]), 32 | }), 33 | }, 34 | )]), 35 | ..Default::default() 36 | }; 37 | state.create_seats(&server); 38 | let state = Arc::new(Mutex::new(state)); 39 | 40 | let socket_name = server.socket_name().to_owned(); 41 | server.run_mutex(state.clone()); 42 | 43 | clear_internal(ClipboardType::Regular, Seat::All, Some(socket_name)).unwrap(); 44 | 45 | let state = state.lock().unwrap(); 46 | assert!(state.seats["seat0"].offer.is_none()); 47 | assert!(state.seats["seat0"].primary_offer.is_some()); 48 | } 49 | 50 | #[test] 51 | fn copy_test() { 52 | let server = TestServer::new(); 53 | server 54 | .display 55 | .handle() 56 | .create_global::(2, ()); 57 | 58 | let (tx, rx) = channel(); 59 | 60 | let state = State { 61 | seats: HashMap::from([( 62 | "seat0".into(), 63 | SeatInfo { 64 | ..Default::default() 65 | }, 66 | )]), 67 | selection_updated_sender: Some(tx), 68 | ..Default::default() 69 | }; 70 | state.create_seats(&server); 71 | 72 | let socket_name = server.socket_name().to_owned(); 73 | server.run(state); 74 | 75 | let sources = vec![MimeSource { 76 | source: Source::Bytes([1, 3, 3, 7][..].into()), 77 | mime_type: MimeType::Specific("test".into()), 78 | }]; 79 | copy_internal(Options::new(), sources, Some(socket_name.clone())).unwrap(); 80 | 81 | // Wait for the copy. 82 | let mime_types = rx.recv().unwrap().unwrap(); 83 | assert_eq!(mime_types, ["test"]); 84 | 85 | let (mut read, mime_type) = get_contents_internal( 86 | paste::ClipboardType::Regular, 87 | paste::Seat::Unspecified, 88 | paste::MimeType::Any, 89 | Some(socket_name.clone()), 90 | ) 91 | .unwrap(); 92 | 93 | let mut contents = vec![]; 94 | read.read_to_end(&mut contents).unwrap(); 95 | 96 | assert_eq!(mime_type, "test"); 97 | assert_eq!(contents, [1, 3, 3, 7]); 98 | 99 | clear_internal(ClipboardType::Both, Seat::All, Some(socket_name)).unwrap(); 100 | } 101 | 102 | #[test] 103 | fn copy_multi_test() { 104 | let server = TestServer::new(); 105 | server 106 | .display 107 | .handle() 108 | .create_global::(2, ()); 109 | 110 | let (tx, rx) = channel(); 111 | 112 | let state = State { 113 | seats: HashMap::from([( 114 | "seat0".into(), 115 | SeatInfo { 116 | ..Default::default() 117 | }, 118 | )]), 119 | selection_updated_sender: Some(tx), 120 | ..Default::default() 121 | }; 122 | state.create_seats(&server); 123 | 124 | let socket_name = server.socket_name().to_owned(); 125 | server.run(state); 126 | 127 | let sources = vec![ 128 | MimeSource { 129 | source: Source::Bytes([1, 3, 3, 7][..].into()), 130 | mime_type: MimeType::Specific("test".into()), 131 | }, 132 | MimeSource { 133 | source: Source::Bytes([2, 4, 4][..].into()), 134 | mime_type: MimeType::Specific("test2".into()), 135 | }, 136 | // Ignored because it's the second "test" MIME type. 137 | MimeSource { 138 | source: Source::Bytes([4, 3, 2, 1][..].into()), 139 | mime_type: MimeType::Specific("test".into()), 140 | }, 141 | // The first text source, additional text types should fall back here. 142 | MimeSource { 143 | source: Source::Bytes(b"hello fallback"[..].into()), 144 | mime_type: MimeType::Text, 145 | }, 146 | // A specific override of an additional text type. 147 | MimeSource { 148 | source: Source::Bytes(b"hello TEXT"[..].into()), 149 | mime_type: MimeType::Specific("TEXT".into()), 150 | }, 151 | ]; 152 | copy_internal(Options::new(), sources, Some(socket_name.clone())).unwrap(); 153 | 154 | // Wait for the copy. 155 | let mut mime_types = rx.recv().unwrap().unwrap(); 156 | mime_types.sort_unstable(); 157 | assert_eq!( 158 | mime_types, 159 | [ 160 | "STRING", 161 | "TEXT", 162 | "UTF8_STRING", 163 | "test", 164 | "test2", 165 | "text/plain", 166 | "text/plain;charset=utf-8", 167 | ] 168 | ); 169 | 170 | let expected = [ 171 | ("test", &[1, 3, 3, 7][..]), 172 | ("test2", &[2, 4, 4][..]), 173 | ("STRING", &b"hello fallback"[..]), 174 | ("TEXT", &b"hello TEXT"[..]), 175 | ]; 176 | 177 | for (mime_type, expected_contents) in expected { 178 | let mut read = get_contents_internal( 179 | paste::ClipboardType::Regular, 180 | paste::Seat::Unspecified, 181 | paste::MimeType::Specific(mime_type), 182 | Some(socket_name.clone()), 183 | ) 184 | .unwrap() 185 | .0; 186 | 187 | let mut contents = vec![]; 188 | read.read_to_end(&mut contents).unwrap(); 189 | 190 | assert_eq!(contents, expected_contents); 191 | } 192 | 193 | clear_internal(ClipboardType::Both, Seat::All, Some(socket_name)).unwrap(); 194 | } 195 | 196 | #[test] 197 | fn copy_multi_no_additional_text_mime_types_test() { 198 | let server = TestServer::new(); 199 | server 200 | .display 201 | .handle() 202 | .create_global::(2, ()); 203 | 204 | let (tx, rx) = channel(); 205 | 206 | let state = State { 207 | seats: HashMap::from([( 208 | "seat0".into(), 209 | SeatInfo { 210 | ..Default::default() 211 | }, 212 | )]), 213 | selection_updated_sender: Some(tx), 214 | ..Default::default() 215 | }; 216 | state.create_seats(&server); 217 | 218 | let socket_name = server.socket_name().to_owned(); 219 | server.run(state); 220 | 221 | let mut opts = Options::new(); 222 | opts.omit_additional_text_mime_types(true); 223 | let sources = vec![ 224 | MimeSource { 225 | source: Source::Bytes([1, 3, 3, 7][..].into()), 226 | mime_type: MimeType::Specific("test".into()), 227 | }, 228 | MimeSource { 229 | source: Source::Bytes([2, 4, 4][..].into()), 230 | mime_type: MimeType::Specific("test2".into()), 231 | }, 232 | // Ignored because it's the second "test" MIME type. 233 | MimeSource { 234 | source: Source::Bytes([4, 3, 2, 1][..].into()), 235 | mime_type: MimeType::Specific("test".into()), 236 | }, 237 | // A specific override of an additional text type. 238 | MimeSource { 239 | source: Source::Bytes(b"hello TEXT"[..].into()), 240 | mime_type: MimeType::Specific("TEXT".into()), 241 | }, 242 | ]; 243 | copy_internal(opts, sources, Some(socket_name.clone())).unwrap(); 244 | 245 | // Wait for the copy. 246 | let mut mime_types = rx.recv().unwrap().unwrap(); 247 | mime_types.sort_unstable(); 248 | assert_eq!(mime_types, ["TEXT", "test", "test2"]); 249 | 250 | let expected = [ 251 | ("test", &[1, 3, 3, 7][..]), 252 | ("test2", &[2, 4, 4][..]), 253 | ("TEXT", &b"hello TEXT"[..]), 254 | ]; 255 | 256 | for (mime_type, expected_contents) in expected { 257 | let mut read = get_contents_internal( 258 | paste::ClipboardType::Regular, 259 | paste::Seat::Unspecified, 260 | paste::MimeType::Specific(mime_type), 261 | Some(socket_name.clone()), 262 | ) 263 | .unwrap() 264 | .0; 265 | 266 | let mut contents = vec![]; 267 | read.read_to_end(&mut contents).unwrap(); 268 | 269 | assert_eq!(contents, expected_contents); 270 | } 271 | 272 | clear_internal(ClipboardType::Both, Seat::All, Some(socket_name)).unwrap(); 273 | } 274 | 275 | // The idea here is to exceed the pipe capacity. This fails unless O_NONBLOCK is cleared when 276 | // sending data over the pipe using cat. 277 | #[test] 278 | fn copy_large() { 279 | // Assuming the default pipe capacity is 65536. 280 | let mut bytes_to_copy = vec![]; 281 | for i in 0..65536 * 10 { 282 | bytes_to_copy.push((i % 256) as u8); 283 | } 284 | 285 | let server = TestServer::new(); 286 | server 287 | .display 288 | .handle() 289 | .create_global::(2, ()); 290 | 291 | let (tx, rx) = channel(); 292 | 293 | let state = State { 294 | seats: HashMap::from([( 295 | "seat0".into(), 296 | SeatInfo { 297 | ..Default::default() 298 | }, 299 | )]), 300 | selection_updated_sender: Some(tx), 301 | // Emulate what XWayland does and set O_NONBLOCK. 302 | set_nonblock_on_write_fd: true, 303 | ..Default::default() 304 | }; 305 | state.create_seats(&server); 306 | 307 | let socket_name = server.socket_name().to_owned(); 308 | server.run(state); 309 | 310 | let sources = vec![MimeSource { 311 | source: Source::Bytes(bytes_to_copy.clone().into_boxed_slice()), 312 | mime_type: MimeType::Specific("test".into()), 313 | }]; 314 | copy_internal(Options::new(), sources, Some(socket_name.clone())).unwrap(); 315 | 316 | // Wait for the copy. 317 | let mime_types = rx.recv().unwrap().unwrap(); 318 | assert_eq!(mime_types, ["test"]); 319 | 320 | let (mut read, mime_type) = get_contents_internal( 321 | paste::ClipboardType::Regular, 322 | paste::Seat::Unspecified, 323 | paste::MimeType::Any, 324 | Some(socket_name.clone()), 325 | ) 326 | .unwrap(); 327 | 328 | let mut contents = vec![]; 329 | read.read_to_end(&mut contents).unwrap(); 330 | 331 | assert_eq!(mime_type, "test"); 332 | assert_eq!(contents.len(), bytes_to_copy.len()); 333 | assert_eq!(contents, bytes_to_copy); 334 | 335 | clear_internal(ClipboardType::Both, Seat::All, Some(socket_name)).unwrap(); 336 | } 337 | 338 | proptest! { 339 | #[test] 340 | fn copy_randomized( 341 | mut state: State, 342 | clipboard_type: ClipboardType, 343 | source: Source, 344 | mime_type: MimeType, 345 | seat_index: prop::sample::Index, 346 | clipboard_type_index: prop::sample::Index, 347 | ) { 348 | prop_assume!(!state.seats.is_empty()); 349 | 350 | let server = TestServer::new(); 351 | server 352 | .display 353 | .handle() 354 | .create_global::(2, ()); 355 | 356 | let (tx, rx) = channel(); 357 | state.selection_updated_sender = Some(tx); 358 | 359 | state.create_seats(&server); 360 | 361 | let seat_index = seat_index.index(state.seats.len()); 362 | let seat_name = state.seats.keys().nth(seat_index).unwrap(); 363 | let seat_name = seat_name.to_owned(); 364 | 365 | let paste_clipboard_type = match clipboard_type { 366 | ClipboardType::Regular => paste::ClipboardType::Regular, 367 | ClipboardType::Primary => paste::ClipboardType::Primary, 368 | ClipboardType::Both => *clipboard_type_index 369 | .get(&[paste::ClipboardType::Regular, paste::ClipboardType::Primary]), 370 | }; 371 | 372 | let socket_name = server.socket_name().to_owned(); 373 | server.run(state); 374 | 375 | let expected_contents = match &source { 376 | Source::Bytes(bytes) => bytes.clone(), 377 | Source::StdIn => unreachable!(), 378 | }; 379 | 380 | let sources = vec![MimeSource { 381 | source, 382 | mime_type: mime_type.clone(), 383 | }]; 384 | let mut opts = Options::new(); 385 | opts.clipboard(clipboard_type); 386 | opts.seat(Seat::Specific(seat_name.clone())); 387 | opts.omit_additional_text_mime_types(true); 388 | copy_internal(opts, sources, Some(socket_name.clone())).unwrap(); 389 | 390 | // Wait for the copy. 391 | let mut mime_types = rx.recv().unwrap().unwrap(); 392 | mime_types.sort_unstable(); 393 | match &mime_type { 394 | MimeType::Autodetect => unreachable!(), 395 | MimeType::Text => assert_eq!(mime_types, ["text/plain"]), 396 | MimeType::Specific(mime) => assert_eq!(mime_types, [mime.clone()]), 397 | } 398 | 399 | let paste_mime_type = match mime_type { 400 | MimeType::Autodetect => unreachable!(), 401 | MimeType::Text => "text/plain".into(), 402 | MimeType::Specific(mime) => mime, 403 | }; 404 | let (mut read, mime_type) = get_contents_internal( 405 | paste_clipboard_type, 406 | paste::Seat::Specific(&seat_name), 407 | paste::MimeType::Specific(&paste_mime_type), 408 | Some(socket_name.clone()), 409 | ) 410 | .unwrap(); 411 | 412 | let mut contents = vec![]; 413 | read.read_to_end(&mut contents).unwrap(); 414 | 415 | assert_eq!(mime_type, paste_mime_type); 416 | assert_eq!(contents.into_boxed_slice(), expected_contents); 417 | 418 | clear_internal(clipboard_type, Seat::Specific(seat_name), Some(socket_name)).unwrap(); 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::os::fd::OwnedFd; 3 | use std::sync::atomic::AtomicU8; 4 | use std::sync::atomic::Ordering::SeqCst; 5 | use std::sync::{Arc, Mutex}; 6 | use std::thread; 7 | 8 | use rustix::event::epoll; 9 | use wayland_backend::server::ClientData; 10 | use wayland_server::{Display, ListeningSocket}; 11 | 12 | mod copy; 13 | mod paste; 14 | mod state; 15 | mod utils; 16 | 17 | pub struct TestServer { 18 | pub display: Display, 19 | pub socket: ListeningSocket, 20 | pub epoll: OwnedFd, 21 | } 22 | 23 | struct ClientCounter(AtomicU8); 24 | 25 | impl ClientData for ClientCounter { 26 | fn disconnected( 27 | &self, 28 | _client_id: wayland_backend::server::ClientId, 29 | _reason: wayland_backend::server::DisconnectReason, 30 | ) { 31 | self.0.fetch_sub(1, SeqCst); 32 | } 33 | } 34 | 35 | impl TestServer { 36 | pub fn new() -> Self { 37 | let mut display = Display::new().unwrap(); 38 | let socket = ListeningSocket::bind_auto("wl-clipboard-rs-test", 0..).unwrap(); 39 | 40 | let epoll = epoll::create(epoll::CreateFlags::CLOEXEC).unwrap(); 41 | 42 | epoll::add( 43 | &epoll, 44 | &socket, 45 | epoll::EventData::new_u64(0), 46 | epoll::EventFlags::IN, 47 | ) 48 | .unwrap(); 49 | epoll::add( 50 | &epoll, 51 | display.backend().poll_fd(), 52 | epoll::EventData::new_u64(1), 53 | epoll::EventFlags::IN, 54 | ) 55 | .unwrap(); 56 | 57 | TestServer { 58 | display, 59 | socket, 60 | epoll, 61 | } 62 | } 63 | 64 | pub fn socket_name(&self) -> &OsStr { 65 | self.socket.socket_name().unwrap() 66 | } 67 | 68 | pub fn run(self, mut state: S) { 69 | thread::spawn(move || self.run_internal(&mut state)); 70 | } 71 | 72 | pub fn run_mutex(self, state: Arc>) { 73 | thread::spawn(move || { 74 | let mut state = state.lock().unwrap(); 75 | self.run_internal(&mut *state); 76 | }); 77 | } 78 | 79 | fn run_internal(mut self, state: &mut S) { 80 | let mut waiting_for_first_client = true; 81 | let client_counter = Arc::new(ClientCounter(AtomicU8::new(0))); 82 | 83 | while client_counter.0.load(SeqCst) > 0 || waiting_for_first_client { 84 | // Wait for requests from the client. 85 | let mut events = epoll::EventVec::with_capacity(2); 86 | epoll::wait(&self.epoll, &mut events, -1).unwrap(); 87 | 88 | for event in &events { 89 | match event.data.u64() { 90 | 0 => { 91 | // Try to accept a new client. 92 | if let Some(stream) = self.socket.accept().unwrap() { 93 | waiting_for_first_client = false; 94 | client_counter.0.fetch_add(1, SeqCst); 95 | self.display 96 | .handle() 97 | .insert_client(stream, client_counter.clone()) 98 | .unwrap(); 99 | } 100 | } 101 | 1 => { 102 | // Try to dispatch client messages. 103 | self.display.dispatch_clients(state).unwrap(); 104 | self.display.flush_clients().unwrap(); 105 | } 106 | x => panic!("unexpected epoll event: {x}"), 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | // https://github.com/Smithay/wayland-rs/blob/90a9ad1f8f1fdef72e96d3c48bdb76b53a7722ff/wayland-tests/tests/helpers/mod.rs 114 | #[macro_export] 115 | macro_rules! server_ignore_impl { 116 | ($handler:ty => [$($iface:ty),*]) => { 117 | $( 118 | impl wayland_server::Dispatch<$iface, ()> for $handler { 119 | fn request( 120 | _: &mut Self, 121 | _: &wayland_server::Client, 122 | _: &$iface, 123 | _: <$iface as wayland_server::Resource>::Request, 124 | _: &(), 125 | _: &wayland_server::DisplayHandle, 126 | _: &mut wayland_server::DataInit<'_, Self>, 127 | ) { 128 | } 129 | } 130 | )* 131 | } 132 | } 133 | 134 | #[macro_export] 135 | macro_rules! server_ignore_global_impl { 136 | ($handler:ty => [$($iface:ty),*]) => { 137 | $( 138 | impl wayland_server::GlobalDispatch<$iface, ()> for $handler { 139 | fn bind( 140 | _: &mut Self, 141 | _: &wayland_server::DisplayHandle, 142 | _: &wayland_server::Client, 143 | new_id: wayland_server::New<$iface>, 144 | _: &(), 145 | data_init: &mut wayland_server::DataInit<'_, Self>, 146 | ) { 147 | data_init.init(new_id, ()); 148 | } 149 | } 150 | )* 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/tests/paste.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::io::Read; 3 | 4 | use proptest::prelude::*; 5 | use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; 6 | 7 | use crate::paste::*; 8 | use crate::tests::state::*; 9 | use crate::tests::TestServer; 10 | 11 | #[test] 12 | fn get_mime_types_test() { 13 | let server = TestServer::new(); 14 | server 15 | .display 16 | .handle() 17 | .create_global::(2, ()); 18 | 19 | let state = State { 20 | seats: HashMap::from([( 21 | "seat0".into(), 22 | SeatInfo { 23 | offer: Some(OfferInfo::Buffered { 24 | data: HashMap::from([ 25 | ("first".into(), vec![]), 26 | ("second".into(), vec![]), 27 | ("third".into(), vec![]), 28 | ]), 29 | }), 30 | ..Default::default() 31 | }, 32 | )]), 33 | ..Default::default() 34 | }; 35 | state.create_seats(&server); 36 | 37 | let socket_name = server.socket_name().to_owned(); 38 | server.run(state); 39 | 40 | let mime_types = 41 | get_mime_types_internal(ClipboardType::Regular, Seat::Unspecified, Some(socket_name)) 42 | .unwrap(); 43 | 44 | let expected = HashSet::from(["first", "second", "third"].map(String::from)); 45 | assert_eq!(mime_types, expected); 46 | } 47 | 48 | #[test] 49 | fn get_mime_types_no_data_control() { 50 | let server = TestServer::new(); 51 | 52 | let state = State { 53 | seats: HashMap::from([( 54 | "seat0".into(), 55 | SeatInfo { 56 | ..Default::default() 57 | }, 58 | )]), 59 | ..Default::default() 60 | }; 61 | state.create_seats(&server); 62 | 63 | let socket_name = server.socket_name().to_owned(); 64 | server.run(state); 65 | 66 | let result = 67 | get_mime_types_internal(ClipboardType::Regular, Seat::Unspecified, Some(socket_name)); 68 | assert!(matches!( 69 | result, 70 | Err(Error::MissingProtocol { 71 | name: "ext-data-control, or wlr-data-control", 72 | version: 1 73 | }) 74 | )); 75 | } 76 | 77 | #[test] 78 | fn get_mime_types_no_data_control_2() { 79 | let server = TestServer::new(); 80 | 81 | let state = State { 82 | seats: HashMap::from([( 83 | "seat0".into(), 84 | SeatInfo { 85 | ..Default::default() 86 | }, 87 | )]), 88 | ..Default::default() 89 | }; 90 | state.create_seats(&server); 91 | 92 | let socket_name = server.socket_name().to_owned(); 93 | server.run(state); 94 | 95 | let result = 96 | get_mime_types_internal(ClipboardType::Primary, Seat::Unspecified, Some(socket_name)); 97 | assert!(matches!( 98 | result, 99 | Err(Error::MissingProtocol { 100 | name: "ext-data-control, or wlr-data-control", 101 | version: 2 102 | }) 103 | )); 104 | } 105 | 106 | #[test] 107 | fn get_mime_types_no_seats() { 108 | let server = TestServer::new(); 109 | server 110 | .display 111 | .handle() 112 | .create_global::(2, ()); 113 | 114 | let state = State { 115 | ..Default::default() 116 | }; 117 | state.create_seats(&server); 118 | 119 | let socket_name = server.socket_name().to_owned(); 120 | server.run(state); 121 | 122 | let result = 123 | get_mime_types_internal(ClipboardType::Primary, Seat::Unspecified, Some(socket_name)); 124 | assert!(matches!(result, Err(Error::NoSeats))); 125 | } 126 | 127 | #[test] 128 | fn get_mime_types_empty_clipboard() { 129 | let server = TestServer::new(); 130 | server 131 | .display 132 | .handle() 133 | .create_global::(2, ()); 134 | 135 | let state = State { 136 | seats: HashMap::from([( 137 | "seat0".into(), 138 | SeatInfo { 139 | ..Default::default() 140 | }, 141 | )]), 142 | ..Default::default() 143 | }; 144 | state.create_seats(&server); 145 | 146 | let socket_name = server.socket_name().to_owned(); 147 | server.run(state); 148 | 149 | let result = 150 | get_mime_types_internal(ClipboardType::Primary, Seat::Unspecified, Some(socket_name)); 151 | assert!(matches!(result, Err(Error::ClipboardEmpty))); 152 | } 153 | 154 | #[test] 155 | fn get_mime_types_specific_seat() { 156 | let server = TestServer::new(); 157 | server 158 | .display 159 | .handle() 160 | .create_global::(2, ()); 161 | 162 | let state = State { 163 | seats: HashMap::from([ 164 | ( 165 | "seat0".into(), 166 | SeatInfo { 167 | ..Default::default() 168 | }, 169 | ), 170 | ( 171 | "yay".into(), 172 | SeatInfo { 173 | offer: Some(OfferInfo::Buffered { 174 | data: HashMap::from([ 175 | ("first".into(), vec![]), 176 | ("second".into(), vec![]), 177 | ("third".into(), vec![]), 178 | ]), 179 | }), 180 | ..Default::default() 181 | }, 182 | ), 183 | ]), 184 | ..Default::default() 185 | }; 186 | state.create_seats(&server); 187 | 188 | let socket_name = server.socket_name().to_owned(); 189 | server.run(state); 190 | 191 | let mime_types = get_mime_types_internal( 192 | ClipboardType::Regular, 193 | Seat::Specific("yay"), 194 | Some(socket_name), 195 | ) 196 | .unwrap(); 197 | 198 | let expected = HashSet::from(["first", "second", "third"].map(String::from)); 199 | assert_eq!(mime_types, expected); 200 | } 201 | 202 | #[test] 203 | fn get_mime_types_primary() { 204 | let server = TestServer::new(); 205 | server 206 | .display 207 | .handle() 208 | .create_global::(2, ()); 209 | 210 | let state = State { 211 | seats: HashMap::from([( 212 | "seat0".into(), 213 | SeatInfo { 214 | primary_offer: Some(OfferInfo::Buffered { 215 | data: HashMap::from([ 216 | ("first".into(), vec![]), 217 | ("second".into(), vec![]), 218 | ("third".into(), vec![]), 219 | ]), 220 | }), 221 | ..Default::default() 222 | }, 223 | )]), 224 | ..Default::default() 225 | }; 226 | state.create_seats(&server); 227 | 228 | let socket_name = server.socket_name().to_owned(); 229 | server.run(state); 230 | 231 | let mime_types = 232 | get_mime_types_internal(ClipboardType::Primary, Seat::Unspecified, Some(socket_name)) 233 | .unwrap(); 234 | 235 | let expected = HashSet::from(["first", "second", "third"].map(String::from)); 236 | assert_eq!(mime_types, expected); 237 | } 238 | 239 | #[test] 240 | fn get_contents_test() { 241 | let server = TestServer::new(); 242 | server 243 | .display 244 | .handle() 245 | .create_global::(2, ()); 246 | 247 | let state = State { 248 | seats: HashMap::from([( 249 | "seat0".into(), 250 | SeatInfo { 251 | offer: Some(OfferInfo::Buffered { 252 | data: HashMap::from([("application/octet-stream".into(), vec![1, 3, 3, 7])]), 253 | }), 254 | ..Default::default() 255 | }, 256 | )]), 257 | ..Default::default() 258 | }; 259 | state.create_seats(&server); 260 | 261 | let socket_name = server.socket_name().to_owned(); 262 | server.run(state); 263 | 264 | let (mut read, mime_type) = get_contents_internal( 265 | ClipboardType::Regular, 266 | Seat::Unspecified, 267 | MimeType::Any, 268 | Some(socket_name), 269 | ) 270 | .unwrap(); 271 | 272 | assert_eq!(mime_type, "application/octet-stream"); 273 | 274 | let mut contents = vec![]; 275 | read.read_to_end(&mut contents).unwrap(); 276 | assert_eq!(contents, [1, 3, 3, 7]); 277 | } 278 | 279 | #[test] 280 | fn get_contents_wrong_mime_type() { 281 | let server = TestServer::new(); 282 | server 283 | .display 284 | .handle() 285 | .create_global::(2, ()); 286 | 287 | let state = State { 288 | seats: HashMap::from([( 289 | "seat0".into(), 290 | SeatInfo { 291 | offer: Some(OfferInfo::Buffered { 292 | data: HashMap::from([("application/octet-stream".into(), vec![1, 3, 3, 7])]), 293 | }), 294 | ..Default::default() 295 | }, 296 | )]), 297 | ..Default::default() 298 | }; 299 | state.create_seats(&server); 300 | 301 | let socket_name = server.socket_name().to_owned(); 302 | server.run(state); 303 | 304 | let result = get_contents_internal( 305 | ClipboardType::Regular, 306 | Seat::Unspecified, 307 | MimeType::Specific("wrong"), 308 | Some(socket_name), 309 | ); 310 | assert!(matches!(result, Err(Error::NoMimeType))); 311 | } 312 | 313 | proptest! { 314 | #[test] 315 | fn get_mime_types_randomized( 316 | mut state: State, 317 | clipboard_type: ClipboardType, 318 | seat_index: prop::sample::Index, 319 | ) { 320 | let server = TestServer::new(); 321 | let socket_name = server.socket_name().to_owned(); 322 | server 323 | .display 324 | .handle() 325 | .create_global::(2, ()); 326 | 327 | state.create_seats(&server); 328 | 329 | if state.seats.is_empty() { 330 | server.run(state); 331 | 332 | let result = get_mime_types_internal(clipboard_type, Seat::Unspecified, Some(socket_name)); 333 | prop_assert!(matches!(result, Err(Error::NoSeats))); 334 | } else { 335 | let seat_index = seat_index.index(state.seats.len()); 336 | let (seat_name, seat_info) = state.seats.iter().nth(seat_index).unwrap(); 337 | let seat_name = seat_name.to_owned(); 338 | let seat_info = (*seat_info).clone(); 339 | 340 | server.run(state); 341 | 342 | let result = get_mime_types_internal( 343 | clipboard_type, 344 | Seat::Specific(&seat_name), 345 | Some(socket_name), 346 | ); 347 | 348 | let expected_offer = match clipboard_type { 349 | ClipboardType::Regular => &seat_info.offer, 350 | ClipboardType::Primary => &seat_info.primary_offer, 351 | }; 352 | match expected_offer { 353 | None => prop_assert!(matches!(result, Err(Error::ClipboardEmpty))), 354 | Some(offer) => prop_assert_eq!(result.unwrap(), offer.data().keys().cloned().collect()), 355 | } 356 | } 357 | } 358 | 359 | #[test] 360 | fn get_contents_randomized( 361 | mut state: State, 362 | clipboard_type: ClipboardType, 363 | seat_index: prop::sample::Index, 364 | mime_index: prop::sample::Index, 365 | ) { 366 | let server = TestServer::new(); 367 | let socket_name = server.socket_name().to_owned(); 368 | server 369 | .display 370 | .handle() 371 | .create_global::(2, ()); 372 | 373 | state.create_seats(&server); 374 | 375 | if state.seats.is_empty() { 376 | server.run(state); 377 | 378 | let result = get_mime_types_internal(clipboard_type, Seat::Unspecified, Some(socket_name)); 379 | prop_assert!(matches!(result, Err(Error::NoSeats))); 380 | } else { 381 | let seat_index = seat_index.index(state.seats.len()); 382 | let (seat_name, seat_info) = state.seats.iter().nth(seat_index).unwrap(); 383 | let seat_name = seat_name.to_owned(); 384 | let seat_info = (*seat_info).clone(); 385 | 386 | let expected_offer = match clipboard_type { 387 | ClipboardType::Regular => &seat_info.offer, 388 | ClipboardType::Primary => &seat_info.primary_offer, 389 | }; 390 | 391 | let mime_type = match expected_offer { 392 | Some(offer) if !offer.data().is_empty() => { 393 | let mime_index = mime_index.index(offer.data().len()); 394 | Some(offer.data().keys().nth(mime_index).unwrap()) 395 | } 396 | _ => None, 397 | }; 398 | 399 | server.run(state); 400 | 401 | let result = get_contents_internal( 402 | clipboard_type, 403 | Seat::Specific(&seat_name), 404 | mime_type.map_or(MimeType::Any, |name| MimeType::Specific(name)), 405 | Some(socket_name), 406 | ); 407 | 408 | match expected_offer { 409 | None => prop_assert!(matches!(result, Err(Error::ClipboardEmpty))), 410 | Some(offer) => { 411 | if offer.data().is_empty() { 412 | prop_assert!(matches!(result, Err(Error::NoMimeType))); 413 | } else { 414 | let mime_type = mime_type.unwrap(); 415 | 416 | let (mut read, recv_mime_type) = result.unwrap(); 417 | prop_assert_eq!(&recv_mime_type, mime_type); 418 | 419 | let mut contents = vec![]; 420 | read.read_to_end(&mut contents).unwrap(); 421 | prop_assert_eq!(&contents, &offer.data()[mime_type]); 422 | } 423 | }, 424 | } 425 | 426 | } 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /src/tests/state.rs: -------------------------------------------------------------------------------- 1 | //! Test compositor implementation. 2 | //! 3 | //! This module contains the test compositor ([`State`]), which boils down to a minimal wlr-data-control protocol 4 | //! implementation. The compositor can be initialized with an arbitrary set of seats, each offering arbitrary clipboard 5 | //! contents in their regular and primary selections. Then the compositor handles all wlr-data-control interactions, such 6 | //! as copying and pasting. 7 | 8 | use std::collections::HashMap; 9 | use std::io::Write; 10 | use std::os::fd::AsFd; 11 | use std::sync::atomic::AtomicU8; 12 | use std::sync::atomic::Ordering::SeqCst; 13 | use std::sync::mpsc::Sender; 14 | 15 | use os_pipe::PipeWriter; 16 | use proptest::prelude::*; 17 | use proptest_derive::Arbitrary; 18 | use rustix::fs::{fcntl_setfl, OFlags}; 19 | use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_device_v1::{ 20 | self, ZwlrDataControlDeviceV1, 21 | }; 22 | use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::{ 23 | self, ZwlrDataControlManagerV1, 24 | }; 25 | use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_offer_v1::{ 26 | self, ZwlrDataControlOfferV1, 27 | }; 28 | use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_source_v1::{ 29 | self, ZwlrDataControlSourceV1, 30 | }; 31 | use wayland_server::protocol::wl_seat::WlSeat; 32 | use wayland_server::{Dispatch, GlobalDispatch, Resource}; 33 | 34 | use super::TestServer; 35 | use crate::server_ignore_global_impl; 36 | 37 | #[derive(Debug, Clone, Arbitrary)] 38 | pub enum OfferInfo { 39 | Buffered { 40 | #[proptest( 41 | strategy = "prop::collection::hash_map(any::(), prop::collection::vec(any::(), 0..5), 0..5)" 42 | )] 43 | data: HashMap>, 44 | }, 45 | #[proptest(skip)] 46 | Runtime { source: ZwlrDataControlSourceV1 }, 47 | } 48 | 49 | impl Default for OfferInfo { 50 | fn default() -> Self { 51 | Self::Buffered { 52 | data: HashMap::new(), 53 | } 54 | } 55 | } 56 | 57 | impl OfferInfo { 58 | fn mime_types(&self, state: &State) -> Vec { 59 | match self { 60 | OfferInfo::Buffered { data } => data.keys().cloned().collect(), 61 | OfferInfo::Runtime { source } => state.sources[source].clone(), 62 | } 63 | } 64 | 65 | pub fn data(&self) -> &HashMap> { 66 | match self { 67 | OfferInfo::Buffered { data } => data, 68 | OfferInfo::Runtime { .. } => panic!(), 69 | } 70 | } 71 | } 72 | 73 | #[derive(Debug, Clone, Default, Arbitrary)] 74 | pub struct SeatInfo { 75 | pub offer: Option, 76 | pub primary_offer: Option, 77 | } 78 | 79 | #[derive(Debug, Clone, Default, Arbitrary)] 80 | pub struct State { 81 | #[proptest(strategy = "prop::collection::hash_map(any::(), any::(), 0..5)")] 82 | pub seats: HashMap, 83 | #[proptest(value = "HashMap::new()")] 84 | pub sources: HashMap>, 85 | #[proptest(value = "None")] 86 | pub selection_updated_sender: Option>>>, 87 | pub set_nonblock_on_write_fd: bool, 88 | } 89 | 90 | server_ignore_global_impl!(State => [ZwlrDataControlManagerV1]); 91 | 92 | impl State { 93 | pub fn create_seats(&self, server: &TestServer) { 94 | for name in self.seats.keys() { 95 | server 96 | .display 97 | .handle() 98 | .create_global::(6, name.clone()); 99 | } 100 | } 101 | } 102 | 103 | impl GlobalDispatch for State { 104 | fn bind( 105 | _state: &mut Self, 106 | _handle: &wayland_server::DisplayHandle, 107 | _client: &wayland_server::Client, 108 | resource: wayland_server::New, 109 | name: &String, 110 | data_init: &mut wayland_server::DataInit<'_, Self>, 111 | ) { 112 | let seat = data_init.init(resource, name.clone()); 113 | seat.name((*name).to_owned()); 114 | } 115 | } 116 | 117 | impl Dispatch for State { 118 | fn request( 119 | _state: &mut Self, 120 | _client: &wayland_server::Client, 121 | _seat: &WlSeat, 122 | _request: ::Request, 123 | _name: &String, 124 | _dhandle: &wayland_server::DisplayHandle, 125 | _data_init: &mut wayland_server::DataInit<'_, Self>, 126 | ) { 127 | } 128 | } 129 | 130 | impl Dispatch for State { 131 | fn request( 132 | state: &mut Self, 133 | client: &wayland_server::Client, 134 | manager: &ZwlrDataControlManagerV1, 135 | request: ::Request, 136 | _data: &(), 137 | dhandle: &wayland_server::DisplayHandle, 138 | data_init: &mut wayland_server::DataInit<'_, Self>, 139 | ) { 140 | match request { 141 | zwlr_data_control_manager_v1::Request::GetDataDevice { id, seat } => { 142 | let name: &String = seat.data().unwrap(); 143 | let info = &state.seats[name]; 144 | 145 | let data_device = data_init.init(id, (*name).clone()); 146 | 147 | let create_offer = |offer_info: &OfferInfo, is_primary: bool| { 148 | let offer = client 149 | .create_resource::<_, _, Self>( 150 | dhandle, 151 | manager.version(), 152 | (name.clone(), is_primary), 153 | ) 154 | .unwrap(); 155 | data_device.data_offer(&offer); 156 | 157 | for mime_type in offer_info.mime_types(state) { 158 | offer.offer(mime_type); 159 | } 160 | 161 | offer 162 | }; 163 | 164 | let selection = info 165 | .offer 166 | .as_ref() 167 | .map(|offer_info| create_offer(offer_info, false)); 168 | data_device.selection(selection.as_ref()); 169 | 170 | let primary_selection = info 171 | .primary_offer 172 | .as_ref() 173 | .map(|offer_info| create_offer(offer_info, true)); 174 | data_device.primary_selection(primary_selection.as_ref()); 175 | } 176 | zwlr_data_control_manager_v1::Request::CreateDataSource { id } => { 177 | let source = data_init.init(id, AtomicU8::new(0)); 178 | state.sources.insert(source, vec![]); 179 | } 180 | _ => (), 181 | } 182 | } 183 | } 184 | 185 | impl Dispatch for State { 186 | fn request( 187 | state: &mut Self, 188 | _client: &wayland_server::Client, 189 | _resource: &ZwlrDataControlDeviceV1, 190 | request: ::Request, 191 | name: &String, 192 | _dhandle: &wayland_server::DisplayHandle, 193 | _data_init: &mut wayland_server::DataInit<'_, Self>, 194 | ) { 195 | match request { 196 | zwlr_data_control_device_v1::Request::SetSelection { source } => { 197 | let mime_types = source.as_ref().map(|source| state.sources[source].clone()); 198 | 199 | let info = state.seats.get_mut(name).unwrap(); 200 | 201 | if let Some(source) = &source { 202 | source.data::().unwrap().fetch_add(1, SeqCst); 203 | } 204 | if let Some(OfferInfo::Runtime { source }) = &info.offer { 205 | if source.data::().unwrap().fetch_sub(1, SeqCst) == 1 { 206 | source.cancelled(); 207 | } 208 | } 209 | info.offer = source.map(|source| OfferInfo::Runtime { source }); 210 | 211 | if let Some(sender) = &state.selection_updated_sender { 212 | let _ = sender.send(mime_types); 213 | } 214 | } 215 | zwlr_data_control_device_v1::Request::SetPrimarySelection { source } => { 216 | let mime_types = source.as_ref().map(|source| state.sources[source].clone()); 217 | 218 | let info = state.seats.get_mut(name).unwrap(); 219 | 220 | if let Some(source) = &source { 221 | source.data::().unwrap().fetch_add(1, SeqCst); 222 | } 223 | if let Some(OfferInfo::Runtime { source }) = &info.primary_offer { 224 | if source.data::().unwrap().fetch_sub(1, SeqCst) == 1 { 225 | source.cancelled(); 226 | } 227 | } 228 | info.primary_offer = source.map(|source| OfferInfo::Runtime { source }); 229 | 230 | if let Some(sender) = &state.selection_updated_sender { 231 | let _ = sender.send(mime_types); 232 | } 233 | } 234 | _ => (), 235 | } 236 | } 237 | } 238 | 239 | impl Dispatch for State { 240 | fn request( 241 | state: &mut Self, 242 | _client: &wayland_server::Client, 243 | _resource: &ZwlrDataControlOfferV1, 244 | request: ::Request, 245 | (name, is_primary): &(String, bool), 246 | _dhandle: &wayland_server::DisplayHandle, 247 | _data_init: &mut wayland_server::DataInit<'_, Self>, 248 | ) { 249 | if let zwlr_data_control_offer_v1::Request::Receive { mime_type, fd } = request { 250 | let info = &state.seats[name]; 251 | let offer_info = if *is_primary { 252 | info.primary_offer.as_ref().unwrap() 253 | } else { 254 | info.offer.as_ref().unwrap() 255 | }; 256 | 257 | match offer_info { 258 | OfferInfo::Buffered { data } => { 259 | let mut write = PipeWriter::from(fd); 260 | let _ = write.write_all(&data[mime_type.as_str()]); 261 | } 262 | OfferInfo::Runtime { source } => { 263 | if state.set_nonblock_on_write_fd { 264 | fcntl_setfl(&fd, OFlags::NONBLOCK).unwrap(); 265 | } 266 | 267 | source.send(mime_type, fd.as_fd()) 268 | } 269 | } 270 | } 271 | } 272 | } 273 | 274 | impl Dispatch for State { 275 | fn request( 276 | state: &mut Self, 277 | _client: &wayland_server::Client, 278 | source: &ZwlrDataControlSourceV1, 279 | request: ::Request, 280 | _data: &AtomicU8, 281 | _dhandle: &wayland_server::DisplayHandle, 282 | _data_init: &mut wayland_server::DataInit<'_, Self>, 283 | ) { 284 | if let zwlr_data_control_source_v1::Request::Offer { mime_type } = request { 285 | state.sources.get_mut(source).unwrap().push(mime_type); 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/tests/utils.rs: -------------------------------------------------------------------------------- 1 | use wayland_protocols::ext::data_control::v1::server::ext_data_control_device_v1::ExtDataControlDeviceV1; 2 | use wayland_protocols::ext::data_control::v1::server::ext_data_control_manager_v1::{ 3 | self, ExtDataControlManagerV1, 4 | }; 5 | use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1; 6 | use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::{ 7 | self, ZwlrDataControlManagerV1, 8 | }; 9 | use wayland_server::protocol::wl_seat::WlSeat; 10 | use wayland_server::Dispatch; 11 | 12 | use crate::tests::TestServer; 13 | use crate::utils::*; 14 | use crate::{server_ignore_global_impl, server_ignore_impl}; 15 | 16 | struct State { 17 | advertise_primary_selection: bool, 18 | } 19 | 20 | server_ignore_global_impl!(State => [WlSeat, ZwlrDataControlManagerV1, ExtDataControlManagerV1]); 21 | server_ignore_impl!(State => [WlSeat, ZwlrDataControlDeviceV1, ExtDataControlDeviceV1]); 22 | 23 | impl Dispatch for State { 24 | fn request( 25 | state: &mut Self, 26 | _client: &wayland_server::Client, 27 | _resource: &ZwlrDataControlManagerV1, 28 | request: ::Request, 29 | _data: &(), 30 | _dhandle: &wayland_server::DisplayHandle, 31 | data_init: &mut wayland_server::DataInit<'_, Self>, 32 | ) { 33 | if let zwlr_data_control_manager_v1::Request::GetDataDevice { id, .. } = request { 34 | let data_device = data_init.init(id, ()); 35 | 36 | if state.advertise_primary_selection { 37 | data_device.primary_selection(None); 38 | } 39 | } 40 | } 41 | } 42 | 43 | impl Dispatch for State { 44 | fn request( 45 | state: &mut Self, 46 | _client: &wayland_server::Client, 47 | _resource: &ExtDataControlManagerV1, 48 | request: ::Request, 49 | _data: &(), 50 | _dhandle: &wayland_server::DisplayHandle, 51 | data_init: &mut wayland_server::DataInit<'_, Self>, 52 | ) { 53 | if let ext_data_control_manager_v1::Request::GetDataDevice { id, .. } = request { 54 | let data_device = data_init.init(id, ()); 55 | 56 | if state.advertise_primary_selection { 57 | data_device.primary_selection(None); 58 | } 59 | } 60 | } 61 | } 62 | 63 | #[test] 64 | fn is_primary_selection_supported_test() { 65 | let server = TestServer::new(); 66 | server 67 | .display 68 | .handle() 69 | .create_global::(6, ()); 70 | server 71 | .display 72 | .handle() 73 | .create_global::(2, ()); 74 | 75 | let state = State { 76 | advertise_primary_selection: true, 77 | }; 78 | 79 | let socket_name = server.socket_name().to_owned(); 80 | server.run(state); 81 | 82 | let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); 83 | assert!(result); 84 | } 85 | 86 | #[test] 87 | fn is_primary_selection_supported_primary_selection_unsupported() { 88 | let server = TestServer::new(); 89 | server 90 | .display 91 | .handle() 92 | .create_global::(6, ()); 93 | server 94 | .display 95 | .handle() 96 | .create_global::(2, ()); 97 | 98 | let state = State { 99 | advertise_primary_selection: false, 100 | }; 101 | 102 | let socket_name = server.socket_name().to_owned(); 103 | server.run(state); 104 | 105 | let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); 106 | assert!(!result); 107 | } 108 | 109 | #[test] 110 | fn is_primary_selection_supported_data_control_v1() { 111 | let server = TestServer::new(); 112 | server 113 | .display 114 | .handle() 115 | .create_global::(6, ()); 116 | server 117 | .display 118 | .handle() 119 | .create_global::(1, ()); 120 | 121 | let state = State { 122 | advertise_primary_selection: false, 123 | }; 124 | 125 | let socket_name = server.socket_name().to_owned(); 126 | server.run(state); 127 | 128 | let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); 129 | assert!(!result); 130 | } 131 | 132 | #[test] 133 | fn is_primary_selection_supported_no_seats() { 134 | let server = TestServer::new(); 135 | server 136 | .display 137 | .handle() 138 | .create_global::(2, ()); 139 | 140 | let state = State { 141 | advertise_primary_selection: true, 142 | }; 143 | 144 | let socket_name = server.socket_name().to_owned(); 145 | server.run(state); 146 | 147 | let result = is_primary_selection_supported_internal(Some(socket_name)); 148 | assert!(matches!(result, Err(PrimarySelectionCheckError::NoSeats))); 149 | } 150 | 151 | #[test] 152 | fn supports_v2_seats() { 153 | let server = TestServer::new(); 154 | server 155 | .display 156 | .handle() 157 | .create_global::(2, ()); 158 | server 159 | .display 160 | .handle() 161 | .create_global::(2, ()); 162 | 163 | let state = State { 164 | advertise_primary_selection: true, 165 | }; 166 | 167 | let socket_name = server.socket_name().to_owned(); 168 | server.run(state); 169 | 170 | let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); 171 | assert!(result); 172 | } 173 | 174 | #[test] 175 | fn is_primary_selection_supported_no_data_control() { 176 | let server = TestServer::new(); 177 | server 178 | .display 179 | .handle() 180 | .create_global::(6, ()); 181 | 182 | let state = State { 183 | advertise_primary_selection: false, 184 | }; 185 | 186 | let socket_name = server.socket_name().to_owned(); 187 | server.run(state); 188 | 189 | let result = is_primary_selection_supported_internal(Some(socket_name)); 190 | assert!(matches!( 191 | result, 192 | Err(PrimarySelectionCheckError::MissingProtocol) 193 | )); 194 | } 195 | 196 | #[test] 197 | fn is_primary_selection_supported_ext_data_control() { 198 | let server = TestServer::new(); 199 | server 200 | .display 201 | .handle() 202 | .create_global::(6, ()); 203 | server 204 | .display 205 | .handle() 206 | .create_global::(1, ()); 207 | 208 | let state = State { 209 | advertise_primary_selection: true, 210 | }; 211 | 212 | let socket_name = server.socket_name().to_owned(); 213 | server.run(state); 214 | 215 | let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); 216 | assert!(result); 217 | } 218 | 219 | #[test] 220 | fn is_primary_selection_supported_primary_selection_unsupported_ext_data_control() { 221 | let server = TestServer::new(); 222 | server 223 | .display 224 | .handle() 225 | .create_global::(6, ()); 226 | server 227 | .display 228 | .handle() 229 | .create_global::(1, ()); 230 | 231 | let state = State { 232 | advertise_primary_selection: false, 233 | }; 234 | 235 | let socket_name = server.socket_name().to_owned(); 236 | server.run(state); 237 | 238 | let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); 239 | assert!(!result); 240 | } 241 | 242 | #[test] 243 | fn is_primary_selection_supported_data_control_v1_and_ext_data_control() { 244 | let server = TestServer::new(); 245 | server 246 | .display 247 | .handle() 248 | .create_global::(6, ()); 249 | server 250 | .display 251 | .handle() 252 | .create_global::(1, ()); 253 | server 254 | .display 255 | .handle() 256 | .create_global::(1, ()); 257 | 258 | let state = State { 259 | advertise_primary_selection: true, 260 | }; 261 | 262 | let socket_name = server.socket_name().to_owned(); 263 | server.run(state); 264 | 265 | let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); 266 | assert!(result); 267 | } 268 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Helper functions. 2 | 3 | use std::ffi::OsString; 4 | use std::os::unix::net::UnixStream; 5 | use std::path::PathBuf; 6 | use std::{env, io}; 7 | 8 | use wayland_client::protocol::wl_registry::{self, WlRegistry}; 9 | use wayland_client::protocol::wl_seat::WlSeat; 10 | use wayland_client::{ 11 | event_created_child, ConnectError, Connection, Dispatch, DispatchError, Proxy, 12 | }; 13 | use wayland_protocols::ext::data_control::v1::client::ext_data_control_manager_v1::ExtDataControlManagerV1; 14 | use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; 15 | 16 | use crate::data_control::{ 17 | impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer, Manager, 18 | }; 19 | 20 | /// Checks if the given MIME type represents plain text. 21 | /// 22 | /// # Examples 23 | /// 24 | /// ``` 25 | /// use wl_clipboard_rs::utils::is_text; 26 | /// 27 | /// assert!(is_text("text/plain")); 28 | /// assert!(!is_text("application/octet-stream")); 29 | /// ``` 30 | pub fn is_text(mime_type: &str) -> bool { 31 | match mime_type { 32 | "TEXT" | "STRING" | "UTF8_STRING" => true, 33 | x if x.starts_with("text/") => true, 34 | _ => false, 35 | } 36 | } 37 | 38 | struct PrimarySelectionState { 39 | // Any seat that we get from the compositor. 40 | seat: Option, 41 | clipboard_manager: Option, 42 | saw_zwlr_v1: bool, 43 | got_primary_selection: bool, 44 | } 45 | 46 | impl Dispatch for PrimarySelectionState { 47 | fn event( 48 | state: &mut Self, 49 | registry: &WlRegistry, 50 | event: ::Event, 51 | _data: &(), 52 | _conn: &Connection, 53 | qh: &wayland_client::QueueHandle, 54 | ) { 55 | if let wl_registry::Event::Global { 56 | name, 57 | interface, 58 | version, 59 | } = event 60 | { 61 | if interface == WlSeat::interface().name && version >= 2 && state.seat.is_none() { 62 | let seat = registry.bind(name, 2, qh, ()); 63 | state.seat = Some(seat); 64 | } 65 | 66 | if state.clipboard_manager.is_none() { 67 | if interface == ZwlrDataControlManagerV1::interface().name { 68 | if version == 1 { 69 | state.saw_zwlr_v1 = true; 70 | } else { 71 | let manager = registry.bind(name, 2, qh, ()); 72 | state.clipboard_manager = Some(Manager::Zwlr(manager)); 73 | } 74 | } 75 | 76 | if interface == ExtDataControlManagerV1::interface().name { 77 | let manager = registry.bind(name, 1, qh, ()); 78 | state.clipboard_manager = Some(Manager::Ext(manager)); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | impl Dispatch for PrimarySelectionState { 86 | fn event( 87 | _state: &mut Self, 88 | _proxy: &WlSeat, 89 | _event: ::Event, 90 | _data: &(), 91 | _conn: &Connection, 92 | _qhandle: &wayland_client::QueueHandle, 93 | ) { 94 | } 95 | } 96 | 97 | impl_dispatch_manager!(PrimarySelectionState); 98 | 99 | impl_dispatch_device!(PrimarySelectionState, (), |state: &mut Self, event, _| { 100 | if let Event::PrimarySelection { id: _ } = event { 101 | state.got_primary_selection = true; 102 | } 103 | }); 104 | 105 | impl_dispatch_offer!(PrimarySelectionState); 106 | 107 | /// Errors that can occur when checking whether the primary selection is supported. 108 | #[derive(thiserror::Error, Debug)] 109 | pub enum PrimarySelectionCheckError { 110 | #[error("There are no seats")] 111 | NoSeats, 112 | 113 | #[error("Couldn't open the provided Wayland socket")] 114 | SocketOpenError(#[source] io::Error), 115 | 116 | #[error("Couldn't connect to the Wayland compositor")] 117 | WaylandConnection(#[source] ConnectError), 118 | 119 | #[error("Wayland compositor communication error")] 120 | WaylandCommunication(#[source] DispatchError), 121 | 122 | #[error( 123 | "A required Wayland protocol (ext-data-control, or wlr-data-control version 1) \ 124 | is not supported by the compositor" 125 | )] 126 | MissingProtocol, 127 | } 128 | 129 | /// Checks if the compositor supports the primary selection. 130 | /// 131 | /// # Examples 132 | /// 133 | /// ```no_run 134 | /// # extern crate wl_clipboard_rs; 135 | /// # fn foo() -> Result<(), Box> { 136 | /// use wl_clipboard_rs::utils::{is_primary_selection_supported, PrimarySelectionCheckError}; 137 | /// 138 | /// match is_primary_selection_supported() { 139 | /// Ok(supported) => { 140 | /// // We have our definitive result. False means that ext/wlr-data-control is present 141 | /// // and did not signal the primary selection support, or that only wlr-data-control 142 | /// // version 1 is present (which does not support primary selection). 143 | /// }, 144 | /// Err(PrimarySelectionCheckError::NoSeats) => { 145 | /// // Impossible to give a definitive result. Primary selection may or may not be 146 | /// // supported. 147 | /// 148 | /// // The required protocol (ext-data-control, or wlr-data-control version 2) is there, 149 | /// // but there are no seats. Unfortunately, at least one seat is needed to check for the 150 | /// // primary clipboard support. 151 | /// }, 152 | /// Err(PrimarySelectionCheckError::MissingProtocol) => { 153 | /// // The data-control protocol (required for wl-clipboard-rs operation) is not 154 | /// // supported by the compositor. 155 | /// }, 156 | /// Err(_) => { 157 | /// // Some communication error occurred. 158 | /// } 159 | /// } 160 | /// # Ok(()) 161 | /// # } 162 | /// ``` 163 | #[inline] 164 | pub fn is_primary_selection_supported() -> Result { 165 | is_primary_selection_supported_internal(None) 166 | } 167 | 168 | pub(crate) fn is_primary_selection_supported_internal( 169 | socket_name: Option, 170 | ) -> Result { 171 | // Connect to the Wayland compositor. 172 | let conn = match socket_name { 173 | Some(name) => { 174 | let mut socket_path = env::var_os("XDG_RUNTIME_DIR") 175 | .map(Into::::into) 176 | .ok_or(ConnectError::NoCompositor) 177 | .map_err(PrimarySelectionCheckError::WaylandConnection)?; 178 | if !socket_path.is_absolute() { 179 | return Err(PrimarySelectionCheckError::WaylandConnection( 180 | ConnectError::NoCompositor, 181 | )); 182 | } 183 | socket_path.push(name); 184 | 185 | let stream = UnixStream::connect(socket_path) 186 | .map_err(PrimarySelectionCheckError::SocketOpenError)?; 187 | Connection::from_socket(stream) 188 | } 189 | None => Connection::connect_to_env(), 190 | } 191 | .map_err(PrimarySelectionCheckError::WaylandConnection)?; 192 | let display = conn.display(); 193 | 194 | let mut queue = conn.new_event_queue(); 195 | let qh = queue.handle(); 196 | 197 | let mut state = PrimarySelectionState { 198 | seat: None, 199 | clipboard_manager: None, 200 | saw_zwlr_v1: false, 201 | got_primary_selection: false, 202 | }; 203 | 204 | // Retrieve the global interfaces. 205 | let _registry = display.get_registry(&qh, ()); 206 | queue 207 | .roundtrip(&mut state) 208 | .map_err(PrimarySelectionCheckError::WaylandCommunication)?; 209 | 210 | // If data control is present but is version 1, then return false as version 1 does not support 211 | // primary clipboard. 212 | if state.clipboard_manager.is_none() && state.saw_zwlr_v1 { 213 | return Ok(false); 214 | } 215 | 216 | // Verify that we got the clipboard manager. 217 | let Some(ref clipboard_manager) = state.clipboard_manager else { 218 | return Err(PrimarySelectionCheckError::MissingProtocol); 219 | }; 220 | 221 | // Check if there are no seats. 222 | let Some(ref seat) = state.seat else { 223 | return Err(PrimarySelectionCheckError::NoSeats); 224 | }; 225 | 226 | clipboard_manager.get_data_device(seat, &qh, ()); 227 | 228 | queue 229 | .roundtrip(&mut state) 230 | .map_err(PrimarySelectionCheckError::WaylandCommunication)?; 231 | 232 | Ok(state.got_primary_selection) 233 | } 234 | -------------------------------------------------------------------------------- /wl-clipboard-rs-tools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wl-clipboard-rs-tools" 3 | version.workspace = true 4 | authors.workspace = true 5 | description = "Terminal utilities for accessing the Wayland clipboard." 6 | edition.workspace = true 7 | license.workspace = true 8 | 9 | readme = "README.md" 10 | repository.workspace = true 11 | keywords.workspace = true 12 | categories = ["command-line-utilities"] 13 | 14 | [dependencies] 15 | anyhow = "1.0.97" 16 | clap = { version = "4.5.31", features = ["derive", "wrap_help"] } 17 | libc.workspace = true 18 | log.workspace = true 19 | mime_guess = "2.0.5" 20 | rustix = { workspace = true, features = ["stdio"] } 21 | stderrlog = "0.6.0" 22 | wl-clipboard-rs = { path = "../", version = "0.9.2" } 23 | 24 | [build-dependencies] 25 | clap = { version = "4.5.31", features = ["derive", "wrap_help"] } 26 | clap_complete = "4.5.46" 27 | clap_mangen = "0.2.26" 28 | 29 | [features] 30 | # Link to libwayland-client.so instead of using the Rust implementation. 31 | native_lib = [ 32 | "wl-clipboard-rs/native_lib", 33 | ] 34 | 35 | dlopen = [ 36 | "wl-clipboard-rs/dlopen", 37 | ] 38 | -------------------------------------------------------------------------------- /wl-clipboard-rs-tools/README.md: -------------------------------------------------------------------------------- 1 | # wl-clipboard-rs-tools 2 | 3 | Terminal utilities for accessing the Wayland clipboard, implemented using [`wl-clipboard-rs`](https://crates.io/crates/wl-clipboard-rs). 4 | 5 | ## Included terminal utilities 6 | 7 | - `wl-paste`: implements `wl-paste` from 8 | [wl-clipboard](https://github.com/bugaevc/wl-clipboard). 9 | - `wl-copy`: implements `wl-copy` from [wl-clipboard](https://github.com/bugaevc/wl-clipboard). 10 | - `wl-clip`: a Wayland version of `xclip`. -------------------------------------------------------------------------------- /wl-clipboard-rs-tools/build.rs: -------------------------------------------------------------------------------- 1 | #[path = "src/wl_copy.rs"] 2 | mod wl_copy; 3 | 4 | #[path = "src/wl_paste.rs"] 5 | mod wl_paste; 6 | 7 | use std::error::Error; 8 | use std::fs; 9 | use std::path::PathBuf; 10 | 11 | use clap::{Command, CommandFactory}; 12 | use clap_complete::generate_to; 13 | use clap_complete::Shell::{Bash, Fish, Zsh}; 14 | use clap_mangen::Man; 15 | 16 | fn generate_man_pages(name: &str, cmd: &Command) -> Result<(), Box> { 17 | let man_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../target/man"); 18 | let mut buffer = Vec::default(); 19 | 20 | Man::new(cmd.clone()).render(&mut buffer)?; 21 | fs::create_dir_all(&man_dir)?; 22 | fs::write(man_dir.join(name.to_owned() + ".1"), buffer)?; 23 | 24 | Ok(()) 25 | } 26 | 27 | fn generate_shell_completions(name: &str, mut cmd: Command) -> Result<(), Box> { 28 | let comp_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../target/completions"); 29 | 30 | fs::create_dir_all(&comp_dir)?; 31 | 32 | for shell in [Bash, Fish, Zsh] { 33 | generate_to(shell, &mut cmd, name, &comp_dir)?; 34 | } 35 | 36 | Ok(()) 37 | } 38 | 39 | fn generate(name: &str, mut cmd: Command) { 40 | cmd.set_bin_name(name); 41 | 42 | if let Err(err) = generate_man_pages(name, &cmd) { 43 | println!("cargo::warning=error generating man page for {name}: {err}"); 44 | } 45 | 46 | if let Err(err) = generate_shell_completions(name, cmd) { 47 | println!("cargo::warning=error generating completions for {name}: {err}"); 48 | } 49 | } 50 | 51 | fn main() { 52 | generate("wl-copy", wl_copy::Options::command()); 53 | generate("wl-paste", wl_paste::Options::command()); 54 | } 55 | -------------------------------------------------------------------------------- /wl-clipboard-rs-tools/src/bin/wl-clip.rs: -------------------------------------------------------------------------------- 1 | use std::env::args_os; 2 | use std::ffi::OsString; 3 | use std::fs::{File, OpenOptions}; 4 | use std::io::{stdout, Read, Write}; 5 | use std::process; 6 | 7 | use anyhow::{Context, Error}; 8 | use libc::fork; 9 | use rustix::stdio::{dup2_stdin, dup2_stdout}; 10 | use wl_clipboard_rs::copy::{self, ServeRequests, Source}; 11 | use wl_clipboard_rs::paste::{self, get_contents}; 12 | use wl_clipboard_rs::utils::is_text; 13 | 14 | #[derive(Clone, Copy, Eq, PartialEq)] 15 | enum Verbosity { 16 | Silent, 17 | Quiet, 18 | Verbose, 19 | } 20 | 21 | struct Options { 22 | files: Vec, 23 | out: bool, 24 | loops: usize, 25 | target: Option, 26 | rmlastnl: bool, 27 | verbosity: Verbosity, 28 | primary: bool, 29 | } 30 | 31 | impl Default for Options { 32 | fn default() -> Self { 33 | Self { 34 | files: Vec::new(), 35 | out: false, 36 | loops: 0, 37 | target: None, 38 | rmlastnl: false, 39 | verbosity: Verbosity::Silent, 40 | primary: true, 41 | } 42 | } 43 | } 44 | 45 | impl Options { 46 | // Hand-rolled argument parser to match what xclip does. 47 | fn from_args() -> Result { 48 | let mut opts = Options::default(); 49 | 50 | enum Print { 51 | Help, 52 | Version, 53 | } 54 | 55 | let mut print = None; 56 | 57 | let mut args = args_os().skip(1).peekable(); 58 | while let Some(arg) = args.next() { 59 | match arg.into_string() { 60 | Ok(arg) => { 61 | macro_rules! parse { 62 | ($longest:expr, $shortest:expr => $action:block) => ( 63 | if $longest.starts_with(&arg) && arg.starts_with($shortest) { 64 | $action 65 | } 66 | ); 67 | 68 | ($longest:expr, $shortest:expr, $next:ident => $action:block) => ( 69 | parse!($longest, $shortest => { 70 | if args.peek().is_some() { 71 | let $next = args.next().unwrap(); 72 | $action 73 | } 74 | 75 | // Important: no continue here. 76 | }); 77 | ); 78 | 79 | ($longest:expr, $shortest:expr => $action:stmt) => ( 80 | parse!($longest, $shortest => { 81 | $action 82 | continue; 83 | }) 84 | ); 85 | } 86 | 87 | parse!("-help", "-h" => print = Some(Print::Help)); 88 | parse!("-version", "-vers" => print = Some(Print::Version)); 89 | parse!("-out", "-o" => opts.out = true); 90 | parse!("-in", "-i" => opts.out = false); 91 | parse!("-rmlastnl", "-r" => opts.rmlastnl = true); 92 | parse!("-silent", "-si" => opts.verbosity = Verbosity::Silent); 93 | parse!("-quiet", "-q" => opts.verbosity = Verbosity::Quiet); 94 | parse!("-verbose", "-verb" => opts.verbosity = Verbosity::Verbose); 95 | 96 | parse!("-filter", "-f" => { 97 | // Not sure there's a good way to support this. 98 | anyhow::bail!("Unsupported option: -filter"); 99 | }); 100 | 101 | parse!("-noutf8", "-n" => { 102 | anyhow::bail!("Unsupported option: -noutf8"); 103 | }); 104 | 105 | parse!("-display", "-d" => { 106 | anyhow::bail!("Unsupported option: -display"); 107 | }); 108 | 109 | parse!("-selection", "-se", val => { 110 | match val.to_string_lossy().chars().next().unwrap_or('_') { 111 | 'c' => opts.primary = false, 112 | 'p' => opts.primary = true, 113 | 's' => anyhow::bail!("Unsupported option: -selection secondary"), 114 | 'b' => anyhow::bail!("Unsupported option: -selection buffer-cut"), 115 | _ => {} 116 | } 117 | 118 | continue; 119 | }); 120 | 121 | parse!("-loops", "-l", val => { 122 | if let Some(val) = val.into_string().ok().and_then(|x| x.parse().ok()) { 123 | opts.loops = val; 124 | } 125 | 126 | continue; 127 | }); 128 | 129 | parse!("-target", "-t", val => { 130 | if let Ok(val) = val.into_string() { 131 | opts.target = Some(val); 132 | } else { 133 | anyhow::bail!("Unsupported option: -target "); 134 | } 135 | 136 | continue; 137 | }); 138 | 139 | opts.files.push(arg.into()) 140 | } 141 | 142 | Err(arg) => opts.files.push(arg), 143 | } 144 | } 145 | 146 | // If help or version is requested, print that and exit. 147 | match print { 148 | Some(Print::Help) => { 149 | eprintln!( 150 | "\ 151 | Usage: {} [OPTION] [FILE]... 152 | Access Wayland clipboard for reading or writing, with an xclip interface. 153 | 154 | -i, -in read text into the clipboard from the standard input or files (default) 155 | -o, -out print the contents of the clipboard to standard output 156 | -l, -loops number of paste requests to serve before exiting 157 | -h, -help show this message 158 | -selection clipboard type to access, \"primary\" (default) or \"clipboard\" 159 | -target set the MIME type to request or set 160 | -rmlastnl trim the last newline character 161 | -version show version information 162 | -silent output errors only, run in background (default) 163 | -quiet run in foreground 164 | -verbose run in foreground, show verbose messages 165 | 166 | Unsupported xclip options: 167 | -d, -display 168 | -f, -filter 169 | -selection secondary, buffer-cut 170 | -noutf8", 171 | args_os() 172 | .next() 173 | .and_then(|x| x.into_string().ok()) 174 | .unwrap_or_else(|| "wl-clip".to_string()) 175 | ); 176 | process::exit(0); 177 | } 178 | Some(Print::Version) => { 179 | eprintln!("wl-clip version {}", env!("CARGO_PKG_VERSION")); 180 | eprintln!("{}", env!("CARGO_PKG_AUTHORS")); 181 | process::exit(0); 182 | } 183 | None => {} 184 | } 185 | 186 | Ok(opts) 187 | } 188 | } 189 | 190 | impl From for copy::Options { 191 | fn from(x: Options) -> Self { 192 | let mut opts = copy::Options::new(); 193 | opts.serve_requests(if x.loops == 0 { 194 | ServeRequests::Unlimited 195 | } else { 196 | ServeRequests::Only(x.loops) 197 | }) 198 | .foreground(true) // We fork manually to support background mode. 199 | .clipboard(if x.primary { 200 | copy::ClipboardType::Primary 201 | } else { 202 | copy::ClipboardType::Regular 203 | }) 204 | .trim_newline(x.rmlastnl); 205 | opts 206 | } 207 | } 208 | 209 | fn main() -> Result<(), Error> { 210 | // Parse command-line options. 211 | let mut options = Options::from_args()?; 212 | 213 | stderrlog::new() 214 | .verbosity(if options.verbosity == Verbosity::Verbose { 215 | 2 216 | } else { 217 | 1 218 | }) 219 | .init() 220 | .unwrap(); 221 | 222 | if options.out { 223 | // Paste. 224 | let mime_type = match options.target.as_ref() { 225 | Some(target) => paste::MimeType::Specific(target), 226 | None => paste::MimeType::Text, 227 | }; 228 | 229 | let clipboard_type = if options.primary { 230 | paste::ClipboardType::Primary 231 | } else { 232 | paste::ClipboardType::Regular 233 | }; 234 | 235 | let (mut read, mime_type) = 236 | get_contents(clipboard_type, paste::Seat::Unspecified, mime_type)?; 237 | 238 | // Read the contents. 239 | let mut contents = vec![]; 240 | read.read_to_end(&mut contents) 241 | .context("Couldn't read clipboard contents")?; 242 | 243 | // Remove the last newline character if needed. 244 | let last_character_is_newline = contents.last().map(|&c| c == b'\n').unwrap_or(false); 245 | if options.rmlastnl && is_text(&mime_type) && last_character_is_newline { 246 | contents.pop(); 247 | } 248 | 249 | // Write everything to stdout. 250 | stdout() 251 | .write_all(&contents) 252 | .context("Couldn't write contents to stdout")?; 253 | } else { 254 | // Copy. 255 | let data = if options.files.is_empty() { 256 | None 257 | } else { 258 | let mut data = vec![]; 259 | 260 | for filename in &options.files { 261 | let mut file = File::open(filename) 262 | .context(format!("Couldn't open {}", filename.to_string_lossy()))?; 263 | file.read_to_end(&mut data)?; 264 | } 265 | 266 | Some(data) 267 | }; 268 | 269 | let source = if options.files.is_empty() { 270 | Source::StdIn 271 | } else { 272 | Source::Bytes(data.unwrap().into()) 273 | }; 274 | 275 | let mime_type = if let Some(mime_type) = options.target.take() { 276 | copy::MimeType::Specific(mime_type) 277 | } else { 278 | // xclip uses STRING in this case, but I believe this is better. 279 | // If it breaks anyone, it should be changed to Text or Specific("STRING"). 280 | copy::MimeType::Autodetect 281 | }; 282 | 283 | let foreground = options.verbosity != Verbosity::Silent; 284 | 285 | let prepared_copy = copy::Options::from(options).prepare_copy(source, mime_type)?; 286 | 287 | if foreground { 288 | prepared_copy.serve()?; 289 | } else { 290 | // SAFETY: We don't spawn any threads, so doing things after forking is safe. 291 | // TODO: is there any way to verify that we don't spawn any threads? 292 | match unsafe { fork() } { 293 | -1 => panic!("error forking: {:?}", std::io::Error::last_os_error()), 294 | 0 => { 295 | // Replace STDIN and STDOUT with /dev/null. We won't be using them, and keeping 296 | // them as is hangs a potential pipeline (i.e. wl-copy hello | cat). Also, 297 | // simply closing the file descriptors is a bad idea because then they get 298 | // reused by subsequent temp file opens, which breaks the dup2/close logic 299 | // during data copying. 300 | if let Ok(dev_null) = 301 | OpenOptions::new().read(true).write(true).open("/dev/null") 302 | { 303 | let _ = dup2_stdin(&dev_null); 304 | let _ = dup2_stdout(&dev_null); 305 | } 306 | 307 | drop(prepared_copy.serve()); 308 | } 309 | _ => (), 310 | } 311 | } 312 | } 313 | 314 | Ok(()) 315 | } 316 | -------------------------------------------------------------------------------- /wl-clipboard-rs-tools/src/bin/wl-copy.rs: -------------------------------------------------------------------------------- 1 | use std::fs::OpenOptions; 2 | use std::os::unix::ffi::OsStringExt; 3 | 4 | use clap::Parser; 5 | use libc::fork; 6 | use rustix::stdio::{dup2_stdin, dup2_stdout}; 7 | use wl_clipboard_rs::copy::{self, clear, ClipboardType, MimeType, Seat, ServeRequests, Source}; 8 | use wl_clipboard_rs_tools::wl_copy::Options; 9 | 10 | fn from_options(x: Options) -> wl_clipboard_rs::copy::Options { 11 | let mut opts = copy::Options::new(); 12 | opts.serve_requests(if x.paste_once { 13 | ServeRequests::Only(1) 14 | } else { 15 | ServeRequests::Unlimited 16 | }) 17 | .foreground(true) // We fork manually to support background mode. 18 | .clipboard(if x.primary { 19 | if x.regular { 20 | ClipboardType::Both 21 | } else { 22 | ClipboardType::Primary 23 | } 24 | } else { 25 | ClipboardType::Regular 26 | }) 27 | .trim_newline(x.trim_newline) 28 | .seat(x.seat.map(Seat::Specific).unwrap_or_default()); 29 | opts 30 | } 31 | 32 | fn main() -> Result<(), anyhow::Error> { 33 | // Parse command-line options. 34 | let mut options = Options::parse(); 35 | 36 | stderrlog::new() 37 | .verbosity(usize::from(options.verbose) + 1) 38 | .init() 39 | .unwrap(); 40 | 41 | if options.clear { 42 | let clipboard = if options.primary { 43 | ClipboardType::Primary 44 | } else { 45 | ClipboardType::Regular 46 | }; 47 | clear( 48 | clipboard, 49 | options.seat.map(Seat::Specific).unwrap_or_default(), 50 | )?; 51 | return Ok(()); 52 | } 53 | 54 | // Is there a way to do this without checking twice? 55 | let source_data = if options.text.is_empty() { 56 | None 57 | } else { 58 | // Copy the arguments into the target file. 59 | let mut iter = options.text.drain(..); 60 | let mut data = iter.next().unwrap(); 61 | 62 | for arg in iter { 63 | data.push(" "); 64 | data.push(arg); 65 | } 66 | 67 | Some(data) 68 | }; 69 | 70 | let source = if let Some(source_data) = source_data { 71 | Source::Bytes(source_data.into_vec().into()) 72 | } else { 73 | Source::StdIn 74 | }; 75 | 76 | let mime_type = if let Some(mime_type) = options.mime_type.take() { 77 | MimeType::Specific(mime_type) 78 | } else { 79 | MimeType::Autodetect 80 | }; 81 | 82 | let foreground = options.foreground; 83 | let prepared_copy = from_options(options).prepare_copy(source, mime_type)?; 84 | 85 | if foreground { 86 | prepared_copy.serve()?; 87 | } else { 88 | // SAFETY: We don't spawn any threads, so doing things after forking is safe. 89 | // TODO: is there any way to verify that we don't spawn any threads? 90 | match unsafe { fork() } { 91 | -1 => panic!("error forking: {:?}", std::io::Error::last_os_error()), 92 | 0 => { 93 | // Replace STDIN and STDOUT with /dev/null. We won't be using them, and keeping 94 | // them as is hangs a potential pipeline (i.e. wl-copy hello | cat). Also, simply 95 | // closing the file descriptors is a bad idea because then they get reused by 96 | // subsequent temp file opens, which breaks the dup2/close logic during data 97 | // copying. 98 | if let Ok(dev_null) = OpenOptions::new().read(true).write(true).open("/dev/null") { 99 | let _ = dup2_stdin(&dev_null); 100 | let _ = dup2_stdout(&dev_null); 101 | } 102 | 103 | drop(prepared_copy.serve()); 104 | } 105 | _ => (), 106 | } 107 | } 108 | 109 | Ok(()) 110 | } 111 | -------------------------------------------------------------------------------- /wl-clipboard-rs-tools/src/bin/wl-paste.rs: -------------------------------------------------------------------------------- 1 | #![deny(unsafe_code)] 2 | 3 | use std::fs::read_link; 4 | use std::io::{stdout, Read, Write}; 5 | 6 | use anyhow::Context; 7 | use clap::Parser; 8 | use libc::STDOUT_FILENO; 9 | use log::trace; 10 | use mime_guess::Mime; 11 | use wl_clipboard_rs::paste::*; 12 | use wl_clipboard_rs::utils::is_text; 13 | use wl_clipboard_rs_tools::wl_paste::Options; 14 | 15 | fn infer_mime_type() -> Option { 16 | if let Ok(stdout_path) = read_link(format!("/dev/fd/{}", STDOUT_FILENO)) { 17 | mime_guess::from_path(stdout_path).first() 18 | } else { 19 | None 20 | } 21 | } 22 | 23 | fn main() -> Result<(), anyhow::Error> { 24 | // Parse command-line options. 25 | let options = Options::parse(); 26 | let primary = if options.primary { 27 | ClipboardType::Primary 28 | } else { 29 | ClipboardType::Regular 30 | }; 31 | let seat = options 32 | .seat 33 | .as_ref() 34 | .map(|x| Seat::Specific(x)) 35 | .unwrap_or_default(); 36 | 37 | stderrlog::new() 38 | .verbosity(usize::from(options.verbose) + 1) 39 | .init() 40 | .unwrap(); 41 | 42 | // If listing types is requested, do just that. 43 | if options.list_types { 44 | let mime_types = get_mime_types(primary, seat)?; 45 | 46 | for mime_type in mime_types.iter() { 47 | println!("{}", mime_type); 48 | } 49 | 50 | return Ok(()); 51 | } 52 | 53 | // Otherwise, get the clipboard contents. 54 | 55 | // No MIME type specified—try inferring one from the output file extension (if any). 56 | let inferred = if options.mime_type.is_none() { 57 | infer_mime_type() 58 | } else { 59 | None 60 | }; 61 | 62 | // Do some smart MIME type selection. 63 | let mime_type = match options.mime_type { 64 | Some(ref mime_type) if mime_type == "text" => MimeType::Text, 65 | Some(ref mime_type) => MimeType::Specific(mime_type), 66 | None => { 67 | let inferred: Option<&str> = inferred.as_ref().map(Mime::as_ref); 68 | trace!("Inferred MIME type: {:?}", inferred); 69 | match inferred { 70 | None | Some("application/octet-stream") => MimeType::Any, 71 | // If the inferred MIME type is text, make sure we'll fall back to requesting 72 | // other plain text types if this particular one is unavailable. 73 | Some(t) if is_text(t) => MimeType::TextWithPriority(t), 74 | Some(t) => MimeType::Specific(t), 75 | } 76 | } 77 | }; 78 | 79 | let (mut read, mime_type) = get_contents(primary, seat, mime_type)?; 80 | 81 | // Read the contents. 82 | let mut contents = vec![]; 83 | read.read_to_end(&mut contents) 84 | .context("Couldn't read clipboard contents")?; 85 | 86 | // Append a newline if needed. 87 | let last_character_is_newline = contents.last().map(|&c| c == b'\n').unwrap_or(false); 88 | if !options.no_newline && is_text(&mime_type) && !last_character_is_newline { 89 | contents.push(b'\n'); 90 | } 91 | 92 | // Write everything to stdout. 93 | stdout() 94 | .write_all(&contents) 95 | .context("Couldn't write contents to stdout")?; 96 | 97 | Ok(()) 98 | } 99 | -------------------------------------------------------------------------------- /wl-clipboard-rs-tools/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod wl_copy; 2 | pub mod wl_paste; 3 | -------------------------------------------------------------------------------- /wl-clipboard-rs-tools/src/wl_copy.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | 3 | use clap::Parser; 4 | 5 | #[derive(Parser)] 6 | #[command( 7 | name = "wl-copy", 8 | version, 9 | about = "Copy clipboard contents on Wayland." 10 | )] 11 | pub struct Options { 12 | /// Serve only a single paste request and then exit 13 | /// 14 | /// This option effectively clears the clipboard after the first paste. It can be used when 15 | /// copying e.g. sensitive data, like passwords. Note however that certain apps may have issues 16 | /// pasting when this option is used, in particular XWayland clients are known to suffer from 17 | /// this. 18 | #[arg(long, short = 'o', conflicts_with = "clear")] 19 | pub paste_once: bool, 20 | 21 | /// Stay in the foreground instead of forking 22 | #[arg(long, short, conflicts_with = "clear")] 23 | pub foreground: bool, 24 | 25 | /// Clear the clipboard instead of copying 26 | #[arg(long, short)] 27 | pub clear: bool, 28 | 29 | /// Use the "primary" clipboard 30 | /// 31 | /// Copying to the "primary" clipboard requires the compositor to support the data-control 32 | /// protocol of version 2 or above. 33 | #[arg(long, short)] 34 | pub primary: bool, 35 | 36 | /// Use the regular clipboard 37 | /// 38 | /// Set this flag together with --primary to operate on both clipboards at once. Has no effect 39 | /// otherwise (since the regular clipboard is the default clipboard). 40 | #[arg(long, short)] 41 | pub regular: bool, 42 | 43 | /// Trim the trailing newline character before copying 44 | /// 45 | /// This flag is only applied for text MIME types. 46 | #[arg(long, short = 'n', conflicts_with = "clear")] 47 | pub trim_newline: bool, 48 | 49 | /// Pick the seat to work with 50 | /// 51 | /// By default wl-copy operates on all seats at once. 52 | #[arg(long, short)] 53 | pub seat: Option, 54 | 55 | /// Override the inferred MIME type for the content 56 | #[arg( 57 | name = "MIME/TYPE", 58 | long = "type", 59 | short = 't', 60 | conflicts_with = "clear" 61 | )] 62 | pub mime_type: Option, 63 | 64 | /// Text to copy 65 | /// 66 | /// If not specified, wl-copy will use data from the standard input. 67 | #[arg(name = "TEXT TO COPY", conflicts_with = "clear")] 68 | pub text: Vec, 69 | 70 | /// Enable verbose logging 71 | #[arg(long, short, action = clap::ArgAction::Count)] 72 | pub verbose: u8, 73 | } 74 | -------------------------------------------------------------------------------- /wl-clipboard-rs-tools/src/wl_paste.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser)] 4 | #[command( 5 | name = "wl-paste", 6 | version, 7 | about = "Paste clipboard contents on Wayland." 8 | )] 9 | pub struct Options { 10 | /// List the offered MIME types instead of pasting 11 | #[arg(long, short)] 12 | pub list_types: bool, 13 | 14 | /// Use the "primary" clipboard 15 | /// 16 | /// Pasting to the "primary" clipboard requires the compositor to support the data-control 17 | /// protocol of version 2 or above. 18 | #[arg(long, short)] 19 | pub primary: bool, 20 | 21 | /// Do not append a newline character 22 | /// 23 | /// By default the newline character is appended automatically when pasting text MIME types. 24 | #[arg(long, short, conflicts_with = "list_types")] 25 | pub no_newline: bool, 26 | 27 | /// Pick the seat to work with 28 | /// 29 | /// By default the seat used is unspecified (it depends on the order returned by the 30 | /// compositor). This is perfectly fine when only a single seat is present, so for most 31 | /// configurations. 32 | #[arg(long, short)] 33 | pub seat: Option, 34 | 35 | /// Request the given MIME type instead of inferring the MIME type 36 | /// 37 | /// As a special case, specifying "text" will look for a number of plain text types, 38 | /// prioritizing ones that are known to give UTF-8 text. 39 | #[arg( 40 | name = "MIME/TYPE", 41 | long = "type", 42 | short = 't', 43 | conflicts_with = "list_types" 44 | )] 45 | pub mime_type: Option, 46 | 47 | /// Enable verbose logging 48 | #[arg(long, short, action = clap::ArgAction::Count)] 49 | pub verbose: u8, 50 | } 51 | --------------------------------------------------------------------------------