wl-clipboard-rs-0.9.2/.cargo_vcs_info.json0000644000000001360000000000100140450ustar { "git": { "sha1": "e551d8ed97579e7ca0e3423f114eccffdb4e2fa5" }, "path_in_vcs": "" }wl-clipboard-rs-0.9.2/.github/dependabot.yml000064400000000000000000000004631046102023000170300ustar 00000000000000version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" groups: rust-dependencies: update-types: - "minor" - "patch" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" wl-clipboard-rs-0.9.2/.github/workflows/ci.yml000064400000000000000000000040571046102023000173560ustar 00000000000000name: CI on: push: pull_request: workflow_dispatch: schedule: - cron: '0 0 1 * *' # Monthly jobs: build: strategy: fail-fast: false matrix: rust: [stable, beta] features: ['', dlopen] name: ${{ matrix.rust }} - ${{ matrix.features }} runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: show-progress: false - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} - uses: Swatinem/rust-cache@v2 - name: Build run: cargo build --all --features=${{ matrix.features }} - name: Set up XDG_RUNTIME_DIR run: | mkdir .runtime echo "XDG_RUNTIME_DIR=$PWD/.runtime" >> "$GITHUB_ENV" - name: Test run: cargo test --all --features=${{ matrix.features }} - name: Generate documentation run: cargo doc --features=${{ matrix.features }} - name: Copy documentation index run: cp doc/index.html target/doc/ - name: Deploy documentation if: > matrix.rust == 'stable' && matrix.features == '' && github.event_name == 'push' && github.ref == 'refs/heads/master' uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/doc clippy: name: clippy runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: show-progress: false - name: Install Rust uses: dtolnay/rust-toolchain@stable with: components: clippy - uses: Swatinem/rust-cache@v2 - name: Run clippy run: cargo clippy --all --all-targets rustfmt: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: show-progress: false - name: Install Rust uses: dtolnay/rust-toolchain@nightly with: components: rustfmt - name: Run rustfmt run: cargo fmt --all -- --check wl-clipboard-rs-0.9.2/.gitignore000064400000000000000000000000231046102023000146200ustar 00000000000000/target **/*.rs.bk wl-clipboard-rs-0.9.2/CHANGELOG.md000064400000000000000000000103141046102023000144450ustar 00000000000000# Changelog ## Unreleased ## v0.9.2 (14th Mar 2025) - Added support for the `ext-data-control` protocol. It will be used instead of `wlr-data-control` when available. - Updated dependencies. ## v0.9.1 (6th Oct 2024) - Added man page and shell completion generation to `wl-clipboard-rs-tools`. - Updated dependencies. ## v0.9.0 (19th June 2024) - **Breaking** Removed `utils::copy_data`. It forked into a `/usr/bin/env cat` for copying. All internal uses of the function have been changed to simply use `std::io::copy` instead. - Replaced `nix` with `rustix`, following `wayland-rs`. - Replaced the deprecated `structopt` with `clap` itself. - Updated dependencies. ## v0.8.1 (7th Mar 2024) - Updated dependencies, notably `nix`, which fixes building on LoongArch. ## v0.8.0 (3rd Oct 2023) - Added `copy::Options::omit_additional_text_mime_types` to disable wl-clipboard-rs offering several known text MIME types when a text MIME type is copied. - Updated `wayland-rs` to 0.31. - **Breaking** This changed the error types slightly. However, most uses of wl-clipboard-rs should be completely unaffected. - Updated other dependencies. ## v0.7.0 (23rd Sep 2022) - Fixed `paste::get_contents()` leaving behind zombie `cat` processes. - Changed debug logging from `info!` to `trace!`. - Bumped `nix` dependency to `0.24` to match that of the wayland-rs crates. - Replaced `derive_more` with `thiserror`. ## v0.6.0 (20th Mar 2022) - Fixed `wl-copy` and `wl-clip` hangs when followed by a pipe (e.g. `wl-copy hello | cat`). - Removed the deprecated `failure` dependency from both the library and the tools. The standard `Error` trait is now used. - Replaced underscores back with dashes in the tool binary names. - Renamed `wl-clipboard-tools` subcrate to `wl-clipboard-rs-tools`. ## v0.5.0 (13th Mar 2022) - Split binaries from the main crate `wl-clipboard-rs` into a new sub-crate `wl-clipboard-tools`. This removes a few dependencies that were only used in the binaries (like `structopt`). - This change also unintentionally replaced dashes with underscores in tool binary names. - Replaced `tree_magic` (which went unmaintained) with `tree_magic_mini`. - Changed the `fork` code which runs during the copy operation to exec `/usr/bin/env cat` instead of just `cat`. This was done to remove a non-async-signal-safe call in the child process. - Updated dependencies. ## v0.4.1 (1st Sep 2020) - Updated `nix` to 0.18 and `wayland-rs` to 0.27. ## v0.4.0 (13th Dec 2019) - **Breaking** Copying in non-foreground mode no longer forks (which was **unsafe** in multi-threaded programs). Instead, it spawns a background thread to serve copy requests. - Added `copy::prepare_copy()` and `copy::prepare_copy_multi()` (and respective functions in `copy::Options`) to accommodate workflows which depended on the forking behavior, such as `wl-copy`. See `wl-copy` for example usage. - **Breaking** Changed `copy::Source` and `copy::Seat` to own the contained data rather than borrow it. As a consequence, those types, as well as `copy::MimeSource` and `copy::Options`, have dropped their lifetime generic parameter. ## v0.3.1 (27th Nov 2019) - Reduced the `wl_seat` version requirement from 6 to 2. - Added `copy::copy_multi()` for offering multiple data sources under multiple different MIME types. ## v0.3.0 (4th Apr 2019) - **Breaking** Moved `ClipboardType` into `copy::` and `paste::`. - **Breaking** Renamed `utils::Error` into `utils::CopyDataError`. - Added `copy::ClipboardType::Both` for operating both clipboards at once. - Added `utils::is_primary_selection_supported()`. - [wl-copy]: added `--regular`, which, when set together with `--primary`, makes `wl-copy` operate on both clipboards at once. ## v0.2.0 (17th Feb 2019) - **Breaking** Changed `copy::Options::paste_once` to `serve_requests` which allows to specify the number of paste requests to serve. - Marked `copy::Seat` and `copy::Options` as `Copy`. - Updated `data-control`, it's now merged into `wlr-protocols` so no further changes without a version bump. - [wl-copy, wl-paste]: replaced `env_logger` with `stderrlog` which made the binaries much smaller. - Implemented `wl-clip`, a Wayland version of `xclip`. ## v0.1.0 (12th Feb 2019) - Initial release. wl-clipboard-rs-0.9.2/Cargo.lock0000644000000442620000000000100120300ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" version = "1.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "dlib" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ "libloading", ] [[package]] name = "downcast-rs" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if", "libc", "wasi 0.13.3+wasi-0.2.2", "windows-targets", ] [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "indexmap" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "libloading" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets", ] [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "log" version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "os_pipe" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" dependencies = [ "libc", "windows-sys", ] [[package]] name = "petgraph" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap", ] [[package]] name = "pkg-config" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", "bitflags", "lazy_static", "num-traits", "rand", "rand_chacha", "rand_xorshift", "regex-syntax", "rusty-fork", "tempfile", "unarray", ] [[package]] name = "proptest-derive" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" dependencies = [ "memchr", ] [[package]] name = "quote" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.15", ] [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ "rand_core", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "rusty-fork" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", "quick-error", "tempfile", "wait-timeout", ] [[package]] name = "scoped-tls" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "smallvec" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "syn" version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" dependencies = [ "cfg-if", "fastrand", "getrandom 0.3.1", "once_cell", "rustix", "windows-sys", ] [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tree_magic_mini" version = "3.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63" dependencies = [ "fnv", "memchr", "nom", "once_cell", "petgraph", ] [[package]] name = "unarray" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" [[package]] name = "wait-timeout" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wayland-backend" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" dependencies = [ "cc", "downcast-rs", "rustix", "scoped-tls", "smallvec", "wayland-sys", ] [[package]] name = "wayland-client" version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" dependencies = [ "bitflags", "rustix", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-protocols" version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" dependencies = [ "bitflags", "wayland-backend", "wayland-client", "wayland-scanner", "wayland-server", ] [[package]] name = "wayland-protocols-wlr" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" dependencies = [ "bitflags", "wayland-backend", "wayland-client", "wayland-protocols", "wayland-scanner", "wayland-server", ] [[package]] name = "wayland-scanner" version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" dependencies = [ "proc-macro2", "quick-xml", "quote", ] [[package]] name = "wayland-server" version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fabd7ed68cff8e7657b8a8a1fbe90cb4a3f0c30d90da4bf179a7a23008a4cb" dependencies = [ "bitflags", "downcast-rs", "rustix", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-sys" version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" dependencies = [ "dlib", "libc", "log", "memoffset", "once_cell", "pkg-config", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wit-bindgen-rt" version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ "bitflags", ] [[package]] name = "wl-clipboard-rs" version = "0.9.2" dependencies = [ "libc", "log", "os_pipe", "proptest", "proptest-derive", "rustix", "tempfile", "thiserror", "tree_magic_mini", "wayland-backend", "wayland-client", "wayland-protocols", "wayland-protocols-wlr", "wayland-server", ] [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn", ] wl-clipboard-rs-0.9.2/Cargo.toml0000644000000042730000000000100120510ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "wl-clipboard-rs" version = "0.9.2" authors = ["Ivan Molodetskikh "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Access to the Wayland clipboard for terminal and other window-less applications." documentation = "https://docs.rs/wl-clipboard-rs" readme = "README.md" keywords = [ "wayland", "clipboard", ] categories = ["os"] license = "MIT/Apache-2.0" repository = "https://github.com/YaLTeR/wl-clipboard-rs" [features] dlopen = [ "native_lib", "wayland-backend/dlopen", "wayland-backend/dlopen", ] native_lib = [ "wayland-backend/client_system", "wayland-backend/server_system", ] [lib] name = "wl_clipboard_rs" path = "src/lib.rs" [dependencies.libc] version = "0.2.170" [dependencies.log] version = "0.4.26" [dependencies.os_pipe] version = "1.2.1" features = ["io_safety"] [dependencies.rustix] version = "0.38.44" features = [ "fs", "event", ] [dependencies.tempfile] version = "3.17.1" [dependencies.thiserror] version = "2" [dependencies.tree_magic_mini] version = "3.1.6" [dependencies.wayland-backend] version = "0.3.8" [dependencies.wayland-client] version = "0.31.8" [dependencies.wayland-protocols] version = "0.32.6" features = [ "client", "staging", ] [dependencies.wayland-protocols-wlr] version = "0.3.6" features = ["client"] [dev-dependencies.proptest] version = "1.6.0" [dev-dependencies.proptest-derive] version = "0.5.1" [dev-dependencies.wayland-protocols] version = "0.32.6" features = [ "server", "staging", ] [dev-dependencies.wayland-protocols-wlr] version = "0.3.6" features = ["server"] [dev-dependencies.wayland-server] version = "0.31.7" wl-clipboard-rs-0.9.2/Cargo.toml.orig000064400000000000000000000032211046102023000155220ustar 00000000000000[workspace] members = ["wl-clipboard-rs-tools"] [workspace.package] version = "0.9.2" # remember to update html_root_url authors = ["Ivan Molodetskikh "] edition = "2021" license = "MIT/Apache-2.0" repository = "https://github.com/YaLTeR/wl-clipboard-rs" keywords = ["wayland", "clipboard"] [workspace.dependencies] libc = "0.2.170" log = "0.4.26" rustix = "0.38.44" [package] name = "wl-clipboard-rs" version.workspace = true authors.workspace = true description = "Access to the Wayland clipboard for terminal and other window-less applications." edition.workspace = true license.workspace = true readme = "README.md" documentation = "https://docs.rs/wl-clipboard-rs" repository.workspace = true keywords.workspace = true categories = ["os"] [dependencies] libc.workspace = true log.workspace = true os_pipe = { version = "1.2.1", features = ["io_safety"] } rustix = { workspace = true, features = ["fs", "event"] } tempfile = "3.17.1" thiserror = "2" tree_magic_mini = "3.1.6" wayland-backend = "0.3.8" wayland-client = "0.31.8" wayland-protocols = { version = "0.32.6", features = ["client", "staging"] } wayland-protocols-wlr = { version = "0.3.6", features = ["client"] } [dev-dependencies] wayland-server = "0.31.7" wayland-protocols = { version = "0.32.6", features = ["server", "staging"] } wayland-protocols-wlr = { version = "0.3.6", features = ["server"] } proptest = "1.6.0" proptest-derive = "0.5.1" [features] # Link to libwayland-client.so instead of using the Rust implementation. native_lib = ["wayland-backend/client_system", "wayland-backend/server_system"] dlopen = ["native_lib", "wayland-backend/dlopen", "wayland-backend/dlopen"] wl-clipboard-rs-0.9.2/LICENSE-APACHE000064400000000000000000000251371046102023000145710ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. wl-clipboard-rs-0.9.2/LICENSE-MIT000064400000000000000000000020451046102023000142720ustar 00000000000000Copyright (c) 2019 Ivan Molodetskikh Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. wl-clipboard-rs-0.9.2/README.md000064400000000000000000000111371046102023000141170ustar 00000000000000# wl-clipboard-rs [![crates.io](https://img.shields.io/crates/v/wl-clipboard-rs.svg)](https://crates.io/crates/wl-clipboard-rs) [![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) [![Documentation](https://docs.rs/wl-clipboard-rs/badge.svg)](https://docs.rs/wl-clipboard-rs) [Documentation (master)](https://yalter.github.io/wl-clipboard-rs/wl_clipboard_rs/) A safe Rust crate for working with the Wayland clipboard. This crate is intended to be used by terminal applications, clipboard managers and other utilities which don't spawn Wayland surfaces (windows). If your application has a window, please use the appropriate Wayland protocols for interacting with the Wayland clipboard (`wl_data_device` from the core Wayland protocol, the `primary_selection` protocol for the primary selection), for example via the [smithay-clipboard](https://crates.io/crates/smithay-clipboard) crate. The protocol used for clipboard interaction is `ext-data-control` or `wlr-data-control`. When using the regular clipboard, the compositor must support any version of either protocol. When using the "primary" clipboard, the compositor must support any version of `ext-data-control`, or the second version of the `wlr-data-control` protocol. For example applications using these features, see `wl-clipboard-rs-tools/src/bin/wl_copy.rs` and `wl-clipboard-rs-tools/src/bin/wl_paste.rs` which implement terminal apps similar to [wl-clipboard](https://github.com/bugaevc/wl-clipboard) or `wl-clipboard-rs-tools/src/bin/wl_clip.rs` which implements a Wayland version of `xclip`. The Rust implementation of the Wayland client is used by default; use the `native_lib` feature to link to `libwayland-client.so` for communication instead. A `dlopen` feature is also available for loading `libwayland-client.so` dynamically at runtime rather than linking to it. The code of the crate itself (and the code of the example utilities) is 100% safe Rust. This doesn't include the dependencies. ## Examples Copying to the regular clipboard: ```rust use wl_clipboard_rs::copy::{MimeType, Options, Source}; let opts = Options::new(); opts.copy(Source::Bytes("Hello world!".to_string().into_bytes().into()), MimeType::Autodetect)?; ``` Pasting plain text from the regular clipboard: ```rust use std::io::Read; use wl_clipboard_rs::{paste::{get_contents, ClipboardType, Error, MimeType, Seat}}; let result = get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Text); match result { Ok((mut pipe, _)) => { let mut contents = vec![]; pipe.read_to_end(&mut contents)?; println!("Pasted: {}", String::from_utf8_lossy(&contents)); } Err(Error::NoSeats) | Err(Error::ClipboardEmpty) | Err(Error::NoMimeType) => { // The clipboard is empty or doesn't contain text, nothing to worry about. } Err(err) => Err(err)? } ``` Checking if the "primary" clipboard is supported (note that this might be unnecessary depending on your crate usage, the regular copying and pasting functions do report if the primary selection is unsupported when it is requested): ```rust use wl_clipboard_rs::utils::{is_primary_selection_supported, PrimarySelectionCheckError}; match is_primary_selection_supported() { Ok(supported) => { // We have our definitive result. False means that ext/wlr-data-control is present // and did not signal the primary selection support, or that only wlr-data-control // version 1 is present (which does not support primary selection). }, Err(PrimarySelectionCheckError::NoSeats) => { // Impossible to give a definitive result. Primary selection may or may not be // supported. // The required protocol (ext-data-control, or wlr-data-control version 2) is there, // but there are no seats. Unfortunately, at least one seat is needed to check for the // primary clipboard support. }, Err(PrimarySelectionCheckError::MissingProtocol) => { // The data-control protocol (required for wl-clipboard-rs operation) is not // supported by the compositor. }, Err(_) => { // Some communication error occurred. } } ``` ## Included terminal utilities - `wl-paste`: implements `wl-paste` from [wl-clipboard](https://github.com/bugaevc/wl-clipboard). - `wl-copy`: implements `wl-copy` from [wl-clipboard](https://github.com/bugaevc/wl-clipboard). - `wl-clip`: a Wayland version of `xclip`. Stuff that would be neat to add: - Utility that mimics `xsel` commandline flags. License: MIT/Apache-2.0 wl-clipboard-rs-0.9.2/README.tpl000064400000000000000000000011531046102023000143130ustar 00000000000000# {{crate}} [![crates.io](https://img.shields.io/crates/v/wl-clipboard-rs.svg)](https://crates.io/crates/wl-clipboard-rs) [![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) [![Documentation](https://docs.rs/wl-clipboard-rs/badge.svg)](https://docs.rs/wl-clipboard-rs) [Documentation (master)](https://yalter.github.io/wl-clipboard-rs/wl_clipboard_rs/) {{readme}} Stuff that would be neat to add: - Utility that mimics `xsel` commandline flags. License: {{license}} wl-clipboard-rs-0.9.2/doc/index.html000064400000000000000000000005351046102023000154020ustar 00000000000000 Page Redirection If you are not redirected automatically, follow this link. wl-clipboard-rs-0.9.2/rustfmt.toml000064400000000000000000000001021046102023000152270ustar 00000000000000imports_granularity = "Module" group_imports = "StdExternalCrate" wl-clipboard-rs-0.9.2/src/common.rs000064400000000000000000000111561046102023000152660ustar 00000000000000use std::collections::HashMap; use std::ffi::OsString; use std::os::unix::net::UnixStream; use std::path::PathBuf; use std::{env, io}; use wayland_backend::client::WaylandError; use wayland_client::globals::{registry_queue_init, GlobalError, GlobalListContents}; use wayland_client::protocol::wl_registry::WlRegistry; use wayland_client::protocol::wl_seat::{self, WlSeat}; use wayland_client::{ConnectError, Connection, Dispatch, EventQueue, Proxy}; use wayland_protocols::ext::data_control::v1::client::ext_data_control_manager_v1::ExtDataControlManagerV1; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; use crate::data_control::Manager; use crate::seat_data::SeatData; pub struct State { pub seats: HashMap, pub clipboard_manager: Manager, } #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Couldn't open the provided Wayland socket")] SocketOpenError(#[source] io::Error), #[error("Couldn't connect to the Wayland compositor")] WaylandConnection(#[source] ConnectError), #[error("Wayland compositor communication error")] WaylandCommunication(#[source] WaylandError), #[error( "A required Wayland protocol ({name} version {version}) is not supported by the compositor" )] MissingProtocol { name: &'static str, version: u32 }, } impl Dispatch for State where S: Dispatch + AsMut, { fn event( parent: &mut S, seat: &WlSeat, event: ::Event, _data: &(), _conn: &wayland_client::Connection, _qh: &wayland_client::QueueHandle, ) { let state = parent.as_mut(); if let wl_seat::Event::Name { name } = event { state.seats.get_mut(seat).unwrap().set_name(name); } } } pub fn initialize( primary: bool, socket_name: Option, ) -> Result<(EventQueue, State), Error> where S: Dispatch + 'static, S: Dispatch, S: Dispatch, S: Dispatch, S: AsMut, { // Connect to the Wayland compositor. let conn = match socket_name { Some(name) => { let mut socket_path = env::var_os("XDG_RUNTIME_DIR") .map(Into::::into) .ok_or(ConnectError::NoCompositor) .map_err(Error::WaylandConnection)?; if !socket_path.is_absolute() { return Err(Error::WaylandConnection(ConnectError::NoCompositor)); } socket_path.push(name); let stream = UnixStream::connect(socket_path).map_err(Error::SocketOpenError)?; Connection::from_socket(stream) } None => Connection::connect_to_env(), } .map_err(Error::WaylandConnection)?; // Retrieve the global interfaces. let (globals, queue) = registry_queue_init::(&conn).map_err(|err| match err { GlobalError::Backend(err) => Error::WaylandCommunication(err), GlobalError::InvalidId(err) => panic!("How's this possible? \ Is there no wl_registry? \ {:?}", err), })?; let qh = &queue.handle(); // Verify that we got the clipboard manager. let ext_manager = globals.bind(qh, 1..=1, ()).ok().map(Manager::Ext); let wlr_v = if primary { 2 } else { 1 }; let wlr_manager = || globals.bind(qh, wlr_v..=wlr_v, ()).ok().map(Manager::Zwlr); let clipboard_manager = match ext_manager.or_else(wlr_manager) { Some(manager) => manager, None => { return Err(Error::MissingProtocol { name: "ext-data-control, or wlr-data-control", version: wlr_v, }) } }; let registry = globals.registry(); let seats = globals.contents().with_list(|globals| { globals .iter() .filter(|global| global.interface == WlSeat::interface().name && global.version >= 2) .map(|global| { let seat = registry.bind(global.name, 2, qh, ()); (seat, SeatData::default()) }) .collect() }); let state = State { seats, clipboard_manager, }; Ok((queue, state)) } wl-clipboard-rs-0.9.2/src/copy.rs000064400000000000000000001037661046102023000147610ustar 00000000000000//! Copying and clearing clipboard contents. use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::ffi::OsString; use std::fs::{remove_dir, remove_file, File, OpenOptions}; use std::io::{self, Read, Seek, SeekFrom, Write}; use std::path::PathBuf; use std::sync::mpsc::sync_channel; use std::{iter, thread}; use log::trace; use rustix::fs::{fcntl_setfl, OFlags}; use wayland_client::globals::GlobalListContents; use wayland_client::protocol::wl_registry::WlRegistry; use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::{ delegate_dispatch, event_created_child, ConnectError, Dispatch, DispatchError, EventQueue, }; use crate::common::{self, initialize}; use crate::data_control::{ self, impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer, impl_dispatch_source, }; use crate::seat_data::SeatData; use crate::utils::is_text; /// The clipboard to operate on. #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)] #[cfg_attr(test, derive(proptest_derive::Arbitrary))] pub enum ClipboardType { /// The regular clipboard. #[default] Regular, /// The "primary" clipboard. /// /// Working with the "primary" clipboard requires the compositor to support ext-data-control, /// or wlr-data-control version 2 or above. Primary, /// Operate on both clipboards at once. /// /// Useful for atomically setting both clipboards at once. This option requires the "primary" /// clipboard to be supported. Both, } /// MIME type to offer the copied data under. #[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)] #[cfg_attr(test, derive(proptest_derive::Arbitrary))] pub enum MimeType { /// Detect the MIME type automatically from the data. #[cfg_attr(test, proptest(skip))] Autodetect, /// Offer a number of common plain text MIME types. Text, /// Offer a specific MIME type. Specific(String), } /// Source for copying. #[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)] #[cfg_attr(test, derive(proptest_derive::Arbitrary))] pub enum Source { /// Copy contents of the standard input. #[cfg_attr(test, proptest(skip))] StdIn, /// Copy the given bytes. Bytes(Box<[u8]>), } /// Source for copying, with a MIME type. /// /// Used for [`copy_multi`]. /// /// [`copy_multi`]: fn.copy_multi.html #[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)] pub struct MimeSource { pub source: Source, pub mime_type: MimeType, } /// Seat to operate on. #[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)] pub enum Seat { /// Operate on all existing seats at once. #[default] All, /// Operate on a seat with the given name. Specific(String), } /// Number of paste requests to serve. #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)] pub enum ServeRequests { /// Serve requests indefinitely. #[default] Unlimited, /// Serve only the given number of requests. Only(usize), } /// Options and flags that are used to customize the copying. #[derive(Clone, Eq, PartialEq, Debug, Default, Hash, PartialOrd, Ord)] pub struct Options { /// The clipboard to work with. clipboard: ClipboardType, /// The seat to work with. seat: Seat, /// Trim the trailing newline character before copying. /// /// This flag is only applied for text MIME types. trim_newline: bool, /// Do not spawn a separate thread for serving copy requests. /// /// Setting this flag will result in the call to `copy()` **blocking** until all data sources /// it creates are destroyed, e.g. until someone else copies something into the clipboard. foreground: bool, /// Number of paste requests to serve. /// /// Limiting the number of paste requests to one effectively clears the clipboard after the /// first paste. It can be used when copying e.g. sensitive data, like passwords. Note however /// that certain apps may have issues pasting when this option is used, in particular XWayland /// clients are known to suffer from this. serve_requests: ServeRequests, /// Omit additional text mime types which are offered by default if at least one text mime type is provided. /// /// Omits additionally offered `text/plain;charset=utf-8`, `text/plain`, `STRING`, `UTF8_STRING` and /// `TEXT` mime types which are offered by default if at least one text mime type is provided. omit_additional_text_mime_types: bool, } /// A copy operation ready to start serving requests. pub struct PreparedCopy { queue: EventQueue, state: State, sources: Vec, } /// Errors that can occur for copying the source data to a temporary file. #[derive(thiserror::Error, Debug)] pub enum SourceCreationError { #[error("Couldn't create a temporary directory")] TempDirCreate(#[source] io::Error), #[error("Couldn't create a temporary file")] TempFileCreate(#[source] io::Error), #[error("Couldn't copy data to the temporary file")] DataCopy(#[source] io::Error), #[error("Couldn't write to the temporary file")] TempFileWrite(#[source] io::Error), #[error("Couldn't open the temporary file for newline trimming")] TempFileOpen(#[source] io::Error), #[error("Couldn't get the temporary file metadata for newline trimming")] TempFileMetadata(#[source] io::Error), #[error("Couldn't seek the temporary file for newline trimming")] TempFileSeek(#[source] io::Error), #[error("Couldn't read the last byte of the temporary file for newline trimming")] TempFileRead(#[source] io::Error), #[error("Couldn't truncate the temporary file for newline trimming")] TempFileTruncate(#[source] io::Error), } /// Errors that can occur for copying and clearing the clipboard. #[derive(thiserror::Error, Debug)] pub enum Error { #[error("There are no seats")] NoSeats, #[error("Couldn't open the provided Wayland socket")] SocketOpenError(#[source] io::Error), #[error("Couldn't connect to the Wayland compositor")] WaylandConnection(#[source] ConnectError), #[error("Wayland compositor communication error")] WaylandCommunication(#[source] DispatchError), #[error( "A required Wayland protocol ({} version {}) is not supported by the compositor", name, version )] MissingProtocol { name: &'static str, version: u32 }, #[error("The compositor does not support primary selection")] PrimarySelectionUnsupported, #[error("The requested seat was not found")] SeatNotFound, #[error("Error copying the source into a temporary file")] TempCopy(#[source] SourceCreationError), #[error("Couldn't remove the temporary file")] TempFileRemove(#[source] io::Error), #[error("Couldn't remove the temporary directory")] TempDirRemove(#[source] io::Error), #[error("Error satisfying a paste request")] Paste(#[source] DataSourceError), } impl From for Error { fn from(x: common::Error) -> Self { use common::Error::*; match x { SocketOpenError(err) => Error::SocketOpenError(err), WaylandConnection(err) => Error::WaylandConnection(err), WaylandCommunication(err) => Error::WaylandCommunication(err.into()), MissingProtocol { name, version } => Error::MissingProtocol { name, version }, } } } #[derive(thiserror::Error, Debug)] pub enum DataSourceError { #[error("Couldn't open the data file")] FileOpen(#[source] io::Error), #[error("Couldn't copy the data to the target file descriptor")] Copy(#[source] io::Error), } struct State { common: common::State, got_primary_selection: bool, // This bool can be set to true when serving a request: either if an error occurs, or if the // number of requests to serve was limited and the last request was served. should_quit: bool, data_paths: HashMap, serve_requests: ServeRequests, // An error that occurred while serving a request, if any. error: Option, } delegate_dispatch!(State: [WlSeat: ()] => common::State); impl AsMut for State { fn as_mut(&mut self) -> &mut common::State { &mut self.common } } impl Dispatch for State { fn event( _state: &mut Self, _proxy: &WlRegistry, _event: ::Event, _data: &GlobalListContents, _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle, ) { } } impl_dispatch_manager!(State); impl_dispatch_device!(State, WlSeat, |state: &mut Self, event, seat| { match event { Event::DataOffer { id } => id.destroy(), Event::Finished => { state.common.seats.get_mut(seat).unwrap().set_device(None); } Event::PrimarySelection { .. } => { state.got_primary_selection = true; } _ => (), } }); impl_dispatch_offer!(State); impl_dispatch_source!(State, |state: &mut Self, source: data_control::Source, event| { match event { Event::Send { mime_type, fd } => { // Check if some other source already handled a paste request and indicated that we should // quit. if state.should_quit { source.destroy(); return; } // I'm not sure if it's the compositor's responsibility to check that the mime type is // valid. Let's check here just in case. if !state.data_paths.contains_key(&mime_type) { return; } let data_path = &state.data_paths[&mime_type]; let file = File::open(data_path).map_err(DataSourceError::FileOpen); let result = file.and_then(|mut data_file| { // Clear O_NONBLOCK, otherwise io::copy() will stop halfway. fcntl_setfl(&fd, OFlags::empty()) .map_err(io::Error::from) .map_err(DataSourceError::Copy)?; let mut target_file = File::from(fd); io::copy(&mut data_file, &mut target_file).map_err(DataSourceError::Copy) }); if let Err(err) = result { state.error = Some(err); } let done = if let ServeRequests::Only(left) = state.serve_requests { let left = left.checked_sub(1).unwrap(); state.serve_requests = ServeRequests::Only(left); left == 0 } else { false }; if done || state.error.is_some() { state.should_quit = true; source.destroy(); } } Event::Cancelled => source.destroy(), _ => (), } }); impl Options { /// Creates a blank new set of options ready for configuration. #[inline] pub fn new() -> Self { Self::default() } /// Sets the clipboard to work with. #[inline] pub fn clipboard(&mut self, clipboard: ClipboardType) -> &mut Self { self.clipboard = clipboard; self } /// Sets the seat to use for copying. #[inline] pub fn seat(&mut self, seat: Seat) -> &mut Self { self.seat = seat; self } /// Sets the flag for trimming the trailing newline. /// /// This flag is only applied for text MIME types. #[inline] pub fn trim_newline(&mut self, trim_newline: bool) -> &mut Self { self.trim_newline = trim_newline; self } /// Sets the flag for not spawning a separate thread for serving copy requests. /// /// Setting this flag will result in the call to `copy()` **blocking** until all data sources /// it creates are destroyed, e.g. until someone else copies something into the clipboard. #[inline] pub fn foreground(&mut self, foreground: bool) -> &mut Self { self.foreground = foreground; self } /// Sets the number of requests to serve. /// /// Limiting the number of requests to one effectively clears the clipboard after the first /// paste. It can be used when copying e.g. sensitive data, like passwords. Note however that /// certain apps may have issues pasting when this option is used, in particular XWayland /// clients are known to suffer from this. #[inline] pub fn serve_requests(&mut self, serve_requests: ServeRequests) -> &mut Self { self.serve_requests = serve_requests; self } /// Sets the flag for omitting additional text mime types which are offered by default if at least one text mime type is provided. /// /// Omits additionally offered `text/plain;charset=utf-8`, `text/plain`, `STRING`, `UTF8_STRING` and /// `TEXT` mime types which are offered by default if at least one text mime type is provided. #[inline] pub fn omit_additional_text_mime_types( &mut self, omit_additional_text_mime_types: bool, ) -> &mut Self { self.omit_additional_text_mime_types = omit_additional_text_mime_types; self } /// Invokes the copy operation. See `copy()`. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # use wl_clipboard_rs::copy::Error; /// # fn foo() -> Result<(), Error> { /// use wl_clipboard_rs::copy::{MimeType, Options, Source}; /// /// let opts = Options::new(); /// opts.copy(Source::Bytes([1, 2, 3][..].into()), MimeType::Autodetect)?; /// # Ok(()) /// # } /// ``` #[inline] pub fn copy(self, source: Source, mime_type: MimeType) -> Result<(), Error> { copy(self, source, mime_type) } /// Invokes the copy_multi operation. See `copy_multi()`. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # use wl_clipboard_rs::copy::Error; /// # fn foo() -> Result<(), Error> { /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source}; /// /// let opts = Options::new(); /// opts.copy_multi(vec![MimeSource { source: Source::Bytes([1, 2, 3][..].into()), /// mime_type: MimeType::Autodetect }, /// MimeSource { source: Source::Bytes([7, 8, 9][..].into()), /// mime_type: MimeType::Text }])?; /// # Ok(()) /// # } /// ``` #[inline] pub fn copy_multi(self, sources: Vec) -> Result<(), Error> { copy_multi(self, sources) } /// Invokes the prepare_copy operation. See `prepare_copy()`. /// /// # Panics /// /// Panics if `foreground` is `false`. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # use wl_clipboard_rs::copy::Error; /// # fn foo() -> Result<(), Error> { /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source}; /// /// let mut opts = Options::new(); /// opts.foreground(true); /// let prepared_copy = opts.prepare_copy(Source::Bytes([1, 2, 3][..].into()), /// MimeType::Autodetect)?; /// prepared_copy.serve()?; /// /// # Ok(()) /// # } /// ``` #[inline] pub fn prepare_copy(self, source: Source, mime_type: MimeType) -> Result { prepare_copy(self, source, mime_type) } /// Invokes the prepare_copy_multi operation. See `prepare_copy_multi()`. /// /// # Panics /// /// Panics if `foreground` is `false`. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # use wl_clipboard_rs::copy::Error; /// # fn foo() -> Result<(), Error> { /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source}; /// /// let mut opts = Options::new(); /// opts.foreground(true); /// let prepared_copy = /// opts.prepare_copy_multi(vec![MimeSource { source: Source::Bytes([1, 2, 3][..].into()), /// mime_type: MimeType::Autodetect }, /// MimeSource { source: Source::Bytes([7, 8, 9][..].into()), /// mime_type: MimeType::Text }])?; /// prepared_copy.serve()?; /// /// # Ok(()) /// # } /// ``` #[inline] pub fn prepare_copy_multi(self, sources: Vec) -> Result { prepare_copy_multi(self, sources) } } impl PreparedCopy { /// Starts serving copy requests. /// /// This function **blocks** until all requests are served or the clipboard is taken over by /// some other application. pub fn serve(mut self) -> Result<(), Error> { // Loop until we're done. while !self.state.should_quit { self.queue .blocking_dispatch(&mut self.state) .map_err(Error::WaylandCommunication)?; // Check if all sources have been destroyed. let all_destroyed = self.sources.iter().all(|x| !x.is_alive()); if all_destroyed { self.state.should_quit = true; } } // Clean up the temp file and directory. // // We want to try cleaning up all files and folders, so if any errors occur in process, // collect them into a vector without interruption, and then return the first one. let mut results = Vec::new(); let mut dropped = HashSet::new(); for data_path in self.state.data_paths.values_mut() { // data_paths can contain duplicate items, we want to free each only once. if dropped.contains(data_path) { continue; }; dropped.insert(data_path.clone()); match remove_file(&data_path).map_err(Error::TempFileRemove) { Ok(()) => { data_path.pop(); results.push(remove_dir(&data_path).map_err(Error::TempDirRemove)); } result @ Err(_) => results.push(result), } } // Return the error, if any. let result: Result<(), _> = results.into_iter().collect(); result?; // Check if an error occurred during data transfer. if let Some(err) = self.state.error.take() { return Err(Error::Paste(err)); } Ok(()) } } fn make_source( source: Source, mime_type: MimeType, trim_newline: bool, ) -> Result<(String, PathBuf), SourceCreationError> { let temp_dir = tempfile::tempdir().map_err(SourceCreationError::TempDirCreate)?; let mut temp_filename = temp_dir.into_path(); temp_filename.push("stdin"); trace!("Temp filename: {}", temp_filename.to_string_lossy()); let mut temp_file = File::create(&temp_filename).map_err(SourceCreationError::TempFileCreate)?; if let Source::Bytes(data) = source { temp_file .write_all(&data) .map_err(SourceCreationError::TempFileWrite)?; } else { // Copy the standard input into the target file. io::copy(&mut io::stdin(), &mut temp_file).map_err(SourceCreationError::DataCopy)?; } let mime_type = match mime_type { MimeType::Autodetect => match tree_magic_mini::from_filepath(&temp_filename) { Some(magic) => Ok(magic), None => Err(SourceCreationError::TempFileOpen(std::io::Error::new( std::io::ErrorKind::Other, "problem with temp file", ))), }? .to_string(), MimeType::Text => "text/plain".to_string(), MimeType::Specific(mime_type) => mime_type, }; trace!("Base MIME type: {}", mime_type); // Trim the trailing newline if needed. if trim_newline && is_text(&mime_type) { let mut temp_file = OpenOptions::new() .read(true) .write(true) .open(&temp_filename) .map_err(SourceCreationError::TempFileOpen)?; let metadata = temp_file .metadata() .map_err(SourceCreationError::TempFileMetadata)?; let length = metadata.len(); if length > 0 { temp_file .seek(SeekFrom::End(-1)) .map_err(SourceCreationError::TempFileSeek)?; let mut buf = [0]; temp_file .read_exact(&mut buf) .map_err(SourceCreationError::TempFileRead)?; if buf[0] == b'\n' { temp_file .set_len(length - 1) .map_err(SourceCreationError::TempFileTruncate)?; } } } Ok((mime_type, temp_filename)) } fn get_devices( primary: bool, seat: Seat, socket_name: Option, ) -> Result<(EventQueue, State, Vec), Error> { let (mut queue, mut common) = initialize(primary, socket_name)?; // Check if there are no seats. if common.seats.is_empty() { return Err(Error::NoSeats); } // Go through the seats and get their data devices. for (seat, data) in &mut common.seats { let device = common .clipboard_manager .get_data_device(seat, &queue.handle(), seat.clone()); data.set_device(Some(device)); } let mut state = State { common, got_primary_selection: false, should_quit: false, data_paths: HashMap::new(), serve_requests: ServeRequests::default(), error: None, }; // Retrieve all seat names. queue .roundtrip(&mut state) .map_err(Error::WaylandCommunication)?; // Check if the compositor supports primary selection. if primary && !state.got_primary_selection { return Err(Error::PrimarySelectionUnsupported); } // Figure out which devices we're interested in. let devices = state .common .seats .values() .filter_map(|data| { let SeatData { name, device, .. } = data; let device = device.clone(); match seat { Seat::All => { // If no seat was specified, handle all of them. return device; } Seat::Specific(ref desired_name) => { if name.as_deref() == Some(desired_name) { return device; } } } None }) .collect::>(); // If we didn't find the seat, print an error message and exit. // // This also triggers when we found the seat but it had no data device; is this what we want? if devices.is_empty() { return Err(Error::SeatNotFound); } Ok((queue, state, devices)) } /// Clears the clipboard for the given seat. /// /// If `seat` is `None`, clears clipboards of all existing seats. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # use wl_clipboard_rs::copy::Error; /// # fn foo() -> Result<(), Error> { /// use wl_clipboard_rs::{copy::{clear, ClipboardType, Seat}}; /// /// clear(ClipboardType::Regular, Seat::All)?; /// # Ok(()) /// # } /// ``` #[inline] pub fn clear(clipboard: ClipboardType, seat: Seat) -> Result<(), Error> { clear_internal(clipboard, seat, None) } pub(crate) fn clear_internal( clipboard: ClipboardType, seat: Seat, socket_name: Option, ) -> Result<(), Error> { let primary = clipboard != ClipboardType::Regular; let (mut queue, mut state, devices) = get_devices(primary, seat, socket_name)?; for device in devices { if clipboard == ClipboardType::Primary || clipboard == ClipboardType::Both { device.set_primary_selection(None); } if clipboard == ClipboardType::Regular || clipboard == ClipboardType::Both { device.set_selection(None); } } // We're clearing the clipboard so just do one roundtrip and quit. queue .roundtrip(&mut state) .map_err(Error::WaylandCommunication)?; Ok(()) } /// Prepares a data copy to the clipboard. /// /// The data is copied from `source` and offered in the `mime_type` MIME type. See `Options` for /// customizing the behavior of this operation. /// /// This function can be used instead of `copy()` when it's desirable to separately prepare the /// copy operation, handle any errors that this may produce, and then start the serving loop, /// potentially past a fork (which is how `wl-copy` uses it). It is meant to be used in the /// foreground mode and does not spawn any threads. /// /// # Panics /// /// Panics if `foreground` is `false`. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # use wl_clipboard_rs::copy::Error; /// # fn foo() -> Result<(), Error> { /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source}; /// /// let mut opts = Options::new(); /// opts.foreground(true); /// let prepared_copy = opts.prepare_copy(Source::Bytes([1, 2, 3][..].into()), /// MimeType::Autodetect)?; /// prepared_copy.serve()?; /// /// # Ok(()) /// # } /// ``` #[inline] pub fn prepare_copy( options: Options, source: Source, mime_type: MimeType, ) -> Result { assert!(options.foreground); let sources = vec![MimeSource { source, mime_type }]; prepare_copy_internal(options, sources, None) } /// Prepares a data copy to the clipboard, offering multiple data sources. /// /// The data from each source in `sources` is copied and offered in the corresponding MIME type. /// See `Options` for customizing the behavior of this operation. /// /// If multiple sources specify the same MIME type, the first one is offered. If one of the MIME /// types is text, all automatically added plain text offers will fall back to the first source /// with a text MIME type. /// /// This function can be used instead of `copy()` when it's desirable to separately prepare the /// copy operation, handle any errors that this may produce, and then start the serving loop, /// potentially past a fork (which is how `wl-copy` uses it). It is meant to be used in the /// foreground mode and does not spawn any threads. /// /// # Panics /// /// Panics if `foreground` is `false`. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # use wl_clipboard_rs::copy::Error; /// # fn foo() -> Result<(), Error> { /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source}; /// /// let mut opts = Options::new(); /// opts.foreground(true); /// let prepared_copy = /// opts.prepare_copy_multi(vec![MimeSource { source: Source::Bytes([1, 2, 3][..].into()), /// mime_type: MimeType::Autodetect }, /// MimeSource { source: Source::Bytes([7, 8, 9][..].into()), /// mime_type: MimeType::Text }])?; /// prepared_copy.serve()?; /// /// # Ok(()) /// # } /// ``` #[inline] pub fn prepare_copy_multi( options: Options, sources: Vec, ) -> Result { assert!(options.foreground); prepare_copy_internal(options, sources, None) } fn prepare_copy_internal( options: Options, sources: Vec, socket_name: Option, ) -> Result { let Options { clipboard, seat, trim_newline, serve_requests, .. } = options; let primary = clipboard != ClipboardType::Regular; let (queue, mut state, devices) = get_devices(primary, seat, socket_name)?; state.serve_requests = serve_requests; // Collect the source data to copy. state.data_paths = { let mut data_paths = HashMap::new(); let mut text_data_path = None; for MimeSource { source, mime_type } in sources.into_iter() { let (mime_type, mut data_path) = make_source(source, mime_type, trim_newline).map_err(Error::TempCopy)?; let mime_type_is_text = is_text(&mime_type); match data_paths.entry(mime_type) { Entry::Occupied(_) => { // This MIME type has already been specified, so ignore it. remove_file(&*data_path).map_err(Error::TempFileRemove)?; data_path.pop(); remove_dir(&*data_path).map_err(Error::TempDirRemove)?; } Entry::Vacant(entry) => { if !options.omit_additional_text_mime_types && text_data_path.is_none() && mime_type_is_text { text_data_path = Some(data_path.clone()); } entry.insert(data_path); } } } // If the MIME type is text, offer it in some other common formats. if let Some(text_data_path) = text_data_path { let text_mimes = [ "text/plain;charset=utf-8", "text/plain", "STRING", "UTF8_STRING", "TEXT", ]; for &mime_type in &text_mimes { // We don't want to overwrite an explicit mime type, because it might be bound to a // different data_path if !data_paths.contains_key(mime_type) { data_paths.insert(mime_type.to_string(), text_data_path.clone()); } } } data_paths }; // Create an iterator over (device, primary) for source creation later. // // This is needed because for ClipboardType::Both each device needs to appear twice because // separate data sources need to be made for the regular and the primary clipboards (data // sources cannot be reused). let devices_iter = devices.iter().flat_map(|device| { let first = match clipboard { ClipboardType::Regular => iter::once((device, false)), ClipboardType::Primary => iter::once((device, true)), ClipboardType::Both => iter::once((device, false)), }; let second = if clipboard == ClipboardType::Both { iter::once(Some((device, true))) } else { iter::once(None) }; first.chain(second.flatten()) }); // Create the data sources and set them as selections. let sources = devices_iter .map(|(device, primary)| { let data_source = state .common .clipboard_manager .create_data_source(&queue.handle()); for mime_type in state.data_paths.keys() { data_source.offer(mime_type.clone()); } if primary { device.set_primary_selection(Some(&data_source)); } else { device.set_selection(Some(&data_source)); } // If we need to serve 0 requests, kill the data source right away. if let ServeRequests::Only(0) = state.serve_requests { data_source.destroy(); } data_source }) .collect::>(); Ok(PreparedCopy { queue, state, sources, }) } /// Copies data to the clipboard. /// /// The data is copied from `source` and offered in the `mime_type` MIME type. See `Options` for /// customizing the behavior of this operation. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # use wl_clipboard_rs::copy::Error; /// # fn foo() -> Result<(), Error> { /// use wl_clipboard_rs::copy::{copy, MimeType, Options, Source}; /// /// let opts = Options::new(); /// copy(opts, Source::Bytes([1, 2, 3][..].into()), MimeType::Autodetect)?; /// # Ok(()) /// # } /// ``` #[inline] pub fn copy(options: Options, source: Source, mime_type: MimeType) -> Result<(), Error> { let sources = vec![MimeSource { source, mime_type }]; copy_internal(options, sources, None) } /// Copies data to the clipboard, offering multiple data sources. /// /// The data from each source in `sources` is copied and offered in the corresponding MIME type. /// See `Options` for customizing the behavior of this operation. /// /// If multiple sources specify the same MIME type, the first one is offered. If one of the MIME /// types is text, all automatically added plain text offers will fall back to the first source /// with a text MIME type. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # use wl_clipboard_rs::copy::Error; /// # fn foo() -> Result<(), Error> { /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source}; /// /// let opts = Options::new(); /// opts.copy_multi(vec![MimeSource { source: Source::Bytes([1, 2, 3][..].into()), /// mime_type: MimeType::Autodetect }, /// MimeSource { source: Source::Bytes([7, 8, 9][..].into()), /// mime_type: MimeType::Text }])?; /// # Ok(()) /// # } /// ``` #[inline] pub fn copy_multi(options: Options, sources: Vec) -> Result<(), Error> { copy_internal(options, sources, None) } pub(crate) fn copy_internal( options: Options, sources: Vec, socket_name: Option, ) -> Result<(), Error> { if options.foreground { prepare_copy_internal(options, sources, socket_name)?.serve() } else { // The copy must be prepared on the thread because PreparedCopy isn't Send. // To receive errors from prepare_copy, use a channel. let (tx, rx) = sync_channel(1); thread::spawn( move || match prepare_copy_internal(options, sources, socket_name) { Ok(prepared_copy) => { // prepare_copy completed successfully, report that. drop(tx.send(None)); // There's nobody listening for errors at this point, just drop it. drop(prepared_copy.serve()); } Err(err) => drop(tx.send(Some(err))), }, ); if let Some(err) = rx.recv().unwrap() { return Err(err); } Ok(()) } } wl-clipboard-rs-0.9.2/src/data_control.rs000064400000000000000000000233301046102023000164440ustar 00000000000000//! Abstraction over ext/wlr-data-control. use std::os::fd::BorrowedFd; use ext::ext_data_control_device_v1::ExtDataControlDeviceV1; use ext::ext_data_control_manager_v1::ExtDataControlManagerV1; use ext::ext_data_control_offer_v1::ExtDataControlOfferV1; use ext::ext_data_control_source_v1::ExtDataControlSourceV1; use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::{Dispatch, Proxy as _, QueueHandle}; use wayland_protocols::ext::data_control::v1::client as ext; use wayland_protocols_wlr::data_control::v1::client as zwlr; use zwlr::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1; use zwlr::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; use zwlr::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1; use zwlr::zwlr_data_control_source_v1::ZwlrDataControlSourceV1; #[derive(Clone)] pub enum Manager { Zwlr(ZwlrDataControlManagerV1), Ext(ExtDataControlManagerV1), } #[derive(Clone)] pub enum Device { Zwlr(ZwlrDataControlDeviceV1), Ext(ExtDataControlDeviceV1), } #[derive(Clone)] pub enum Source { Zwlr(ZwlrDataControlSourceV1), Ext(ExtDataControlSourceV1), } #[derive(Clone, PartialEq, Eq, Hash)] pub enum Offer { Zwlr(ZwlrDataControlOfferV1), Ext(ExtDataControlOfferV1), } impl Manager { pub fn get_data_device(&self, seat: &WlSeat, qh: &QueueHandle, udata: U) -> Device where D: Dispatch + 'static, D: Dispatch + 'static, U: Send + Sync + 'static, { match self { Manager::Zwlr(manager) => Device::Zwlr(manager.get_data_device(seat, qh, udata)), Manager::Ext(manager) => Device::Ext(manager.get_data_device(seat, qh, udata)), } } pub fn create_data_source(&self, qh: &QueueHandle) -> Source where D: Dispatch + 'static, D: Dispatch + 'static, { match self { Manager::Zwlr(manager) => Source::Zwlr(manager.create_data_source(qh, ())), Manager::Ext(manager) => Source::Ext(manager.create_data_source(qh, ())), } } } impl Device { pub fn destroy(&self) { match self { Device::Zwlr(device) => device.destroy(), Device::Ext(device) => device.destroy(), } } #[track_caller] pub fn set_selection(&self, source: Option<&Source>) { match self { Device::Zwlr(device) => device.set_selection(source.map(Source::zwlr)), Device::Ext(device) => device.set_selection(source.map(Source::ext)), } } #[track_caller] pub fn set_primary_selection(&self, source: Option<&Source>) { match self { Device::Zwlr(device) => device.set_primary_selection(source.map(Source::zwlr)), Device::Ext(device) => device.set_primary_selection(source.map(Source::ext)), } } } impl Source { pub fn destroy(&self) { match self { Source::Zwlr(source) => source.destroy(), Source::Ext(source) => source.destroy(), } } pub fn offer(&self, mime_type: String) { match self { Source::Zwlr(source) => source.offer(mime_type), Source::Ext(source) => source.offer(mime_type), } } pub fn is_alive(&self) -> bool { match self { Source::Zwlr(source) => source.is_alive(), Source::Ext(source) => source.is_alive(), } } #[track_caller] pub fn zwlr(&self) -> &ZwlrDataControlSourceV1 { if let Self::Zwlr(v) = self { v } else { panic!("tried to convert non-Zwlr Source to Zwlr") } } #[track_caller] pub fn ext(&self) -> &ExtDataControlSourceV1 { if let Self::Ext(v) = self { v } else { panic!("tried to convert non-Ext Source to Ext") } } } impl Offer { pub fn destroy(&self) { match self { Offer::Zwlr(offer) => offer.destroy(), Offer::Ext(offer) => offer.destroy(), } } pub fn receive(&self, mime_type: String, fd: BorrowedFd) { match self { Offer::Zwlr(offer) => offer.receive(mime_type, fd), Offer::Ext(offer) => offer.receive(mime_type, fd), } } } impl From for Source { fn from(v: ZwlrDataControlSourceV1) -> Self { Self::Zwlr(v) } } impl From for Source { fn from(v: ExtDataControlSourceV1) -> Self { Self::Ext(v) } } impl From for Offer { fn from(v: ZwlrDataControlOfferV1) -> Self { Self::Zwlr(v) } } impl From for Offer { fn from(v: ExtDataControlOfferV1) -> Self { Self::Ext(v) } } // Some mildly cursed macros to avoid code duplication. macro_rules! impl_dispatch_manager { ($handler:ty => [$($iface:ty),*]) => { $( impl Dispatch<$iface, ()> for $handler { fn event( _state: &mut Self, _proxy: &$iface, _event: <$iface as wayland_client::Proxy>::Event, _data: &(), _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle, ) { } } )* }; ($handler:ty) => { impl_dispatch_manager!($handler => [ wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1, wayland_protocols::ext::data_control::v1::client::ext_data_control_manager_v1::ExtDataControlManagerV1 ]); }; } pub(crate) use impl_dispatch_manager; macro_rules! impl_dispatch_device { ($handler:ty, $udata:ty, $code:expr => [$(($iface:ty, $opcode:path, $offer:ty)),*]) => { $( impl Dispatch<$iface, $udata> for $handler { fn event( state: &mut Self, _proxy: &$iface, event: <$iface as wayland_client::Proxy>::Event, data: &$udata, _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle, ) { type Event = <$iface as wayland_client::Proxy>::Event; ($code)(state, event, data) } event_created_child!($handler, $iface, [ $opcode => ($offer, ()), ]); } )* }; ($handler:ty, $udata:ty, $code:expr) => { impl_dispatch_device!($handler, $udata, $code => [ ( wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1, wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::EVT_DATA_OFFER_OPCODE, wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1 ), ( wayland_protocols::ext::data_control::v1::client::ext_data_control_device_v1::ExtDataControlDeviceV1, wayland_protocols::ext::data_control::v1::client::ext_data_control_device_v1::EVT_DATA_OFFER_OPCODE, wayland_protocols::ext::data_control::v1::client::ext_data_control_offer_v1::ExtDataControlOfferV1 ) ]); }; } pub(crate) use impl_dispatch_device; macro_rules! impl_dispatch_source { ($handler:ty, $code:expr => [$($iface:ty),*]) => { $( impl Dispatch<$iface, ()> for $handler { fn event( state: &mut Self, proxy: &$iface, event: <$iface as wayland_client::Proxy>::Event, _data: &(), _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle, ) { type Event = <$iface as wayland_client::Proxy>::Event; let source = $crate::data_control::Source::from(proxy.clone()); ($code)(state, source, event) } } )* }; ($handler:ty, $code:expr) => { impl_dispatch_source!($handler, $code => [ wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1, wayland_protocols::ext::data_control::v1::client::ext_data_control_source_v1::ExtDataControlSourceV1 ]); }; } pub(crate) use impl_dispatch_source; macro_rules! impl_dispatch_offer { ($handler:ty, $code:expr => [$($iface:ty),*]) => { $( impl Dispatch<$iface, ()> for $handler { fn event( state: &mut Self, proxy: &$iface, event: <$iface as wayland_client::Proxy>::Event, _data: &(), _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle, ) { type Event = <$iface as wayland_client::Proxy>::Event; let offer = $crate::data_control::Offer::from(proxy.clone()); ($code)(state, offer, event) } } )* }; ($handler:ty, $code:expr) => { impl_dispatch_offer!($handler, $code => [ wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1, wayland_protocols::ext::data_control::v1::client::ext_data_control_offer_v1::ExtDataControlOfferV1 ]); }; ($handler:ty) => { impl_dispatch_offer!($handler, |_, _, _: Event| ()); }; } pub(crate) use impl_dispatch_offer; wl-clipboard-rs-0.9.2/src/lib.rs000064400000000000000000000117161046102023000145460ustar 00000000000000//! A safe Rust crate for working with the Wayland clipboard. //! //! This crate is intended to be used by terminal applications, clipboard managers and other //! utilities which don't spawn Wayland surfaces (windows). If your application has a window, //! please use the appropriate Wayland protocols for interacting with the Wayland clipboard //! (`wl_data_device` from the core Wayland protocol, the `primary_selection` protocol for the //! primary selection), for example via the //! [smithay-clipboard](https://crates.io/crates/smithay-clipboard) crate. //! //! The protocol used for clipboard interaction is `ext-data-control` or `wlr-data-control`. When //! using the regular clipboard, the compositor must support any version of either protocol. When //! using the "primary" clipboard, the compositor must support any version of `ext-data-control`, //! or the second version of the `wlr-data-control` protocol. //! //! For example applications using these features, see `wl-clipboard-rs-tools/src/bin/wl_copy.rs` //! and `wl-clipboard-rs-tools/src/bin/wl_paste.rs` which implement terminal apps similar to //! [wl-clipboard](https://github.com/bugaevc/wl-clipboard) or //! `wl-clipboard-rs-tools/src/bin/wl_clip.rs` which implements a Wayland version of `xclip`. //! //! The Rust implementation of the Wayland client is used by default; use the `native_lib` feature //! to link to `libwayland-client.so` for communication instead. A `dlopen` feature is also //! available for loading `libwayland-client.so` dynamically at runtime rather than linking to it. //! //! The code of the crate itself (and the code of the example utilities) is 100% safe Rust. This //! doesn't include the dependencies. //! //! # Examples //! //! Copying to the regular clipboard: //! ```no_run //! # extern crate wl_clipboard_rs; //! # fn foo() -> Result<(), Box> { //! use wl_clipboard_rs::copy::{MimeType, Options, Source}; //! //! let opts = Options::new(); //! opts.copy(Source::Bytes("Hello world!".to_string().into_bytes().into()), MimeType::Autodetect)?; //! # Ok(()) //! # } //! ``` //! //! Pasting plain text from the regular clipboard: //! ```no_run //! # extern crate wl_clipboard_rs; //! # fn foo() -> Result<(), Box> { //! use std::io::Read; //! use wl_clipboard_rs::{paste::{get_contents, ClipboardType, Error, MimeType, Seat}}; //! //! let result = get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Text); //! match result { //! Ok((mut pipe, _)) => { //! let mut contents = vec![]; //! pipe.read_to_end(&mut contents)?; //! println!("Pasted: {}", String::from_utf8_lossy(&contents)); //! } //! //! Err(Error::NoSeats) | Err(Error::ClipboardEmpty) | Err(Error::NoMimeType) => { //! // The clipboard is empty or doesn't contain text, nothing to worry about. //! } //! //! Err(err) => Err(err)? //! } //! # Ok(()) //! # } //! ``` //! //! Checking if the "primary" clipboard is supported (note that this might be unnecessary depending //! on your crate usage, the regular copying and pasting functions do report if the primary //! selection is unsupported when it is requested): //! //! ```no_run //! # extern crate wl_clipboard_rs; //! # fn foo() -> Result<(), Box> { //! use wl_clipboard_rs::utils::{is_primary_selection_supported, PrimarySelectionCheckError}; //! //! match is_primary_selection_supported() { //! Ok(supported) => { //! // We have our definitive result. False means that ext/wlr-data-control is present //! // and did not signal the primary selection support, or that only wlr-data-control //! // version 1 is present (which does not support primary selection). //! }, //! Err(PrimarySelectionCheckError::NoSeats) => { //! // Impossible to give a definitive result. Primary selection may or may not be //! // supported. //! //! // The required protocol (ext-data-control, or wlr-data-control version 2) is there, //! // but there are no seats. Unfortunately, at least one seat is needed to check for the //! // primary clipboard support. //! }, //! Err(PrimarySelectionCheckError::MissingProtocol) => { //! // The data-control protocol (required for wl-clipboard-rs operation) is not //! // supported by the compositor. //! }, //! Err(_) => { //! // Some communication error occurred. //! } //! } //! # Ok(()) //! # } //! ``` //! //! # Included terminal utilities //! //! - `wl-paste`: implements `wl-paste` from //! [wl-clipboard](https://github.com/bugaevc/wl-clipboard). //! - `wl-copy`: implements `wl-copy` from [wl-clipboard](https://github.com/bugaevc/wl-clipboard). //! - `wl-clip`: a Wayland version of `xclip`. #![doc(html_root_url = "https://docs.rs/wl-clipboard-rs/0.9.2")] #![deny(unsafe_code)] mod common; mod data_control; mod seat_data; #[cfg(test)] #[allow(unsafe_code)] // It's more convenient for testing some stuff. mod tests; pub mod copy; pub mod paste; pub mod utils; wl-clipboard-rs-0.9.2/src/paste.rs000064400000000000000000000311731046102023000151130ustar 00000000000000//! Getting the offered MIME types and the clipboard contents. use std::collections::{HashMap, HashSet}; use std::ffi::OsString; use std::io; use std::os::fd::AsFd; use os_pipe::{pipe, PipeReader}; use wayland_client::globals::GlobalListContents; use wayland_client::protocol::wl_registry::WlRegistry; use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::{ delegate_dispatch, event_created_child, ConnectError, Dispatch, DispatchError, EventQueue, }; use crate::common::{self, initialize}; use crate::data_control::{self, impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer}; use crate::utils::is_text; /// The clipboard to operate on. #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)] #[cfg_attr(test, derive(proptest_derive::Arbitrary))] pub enum ClipboardType { /// The regular clipboard. #[default] Regular, /// The "primary" clipboard. /// /// Working with the "primary" clipboard requires the compositor to support ext-data-control, /// or wlr-data-control version 2 or above. Primary, } /// MIME types that can be requested from the clipboard. #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)] pub enum MimeType<'a> { /// Request any available MIME type. /// /// If multiple MIME types are offered, the requested MIME type is unspecified and depends on /// the order they are received from the Wayland compositor. However, plain text formats are /// prioritized, so if a plain text format is available among others then it will be requested. Any, /// Request a plain text MIME type. /// /// This will request one of the multiple common plain text MIME types. It will prioritize MIME /// types known to return UTF-8 text. Text, /// Request the given MIME type, and if it's not available fall back to `MimeType::Text`. /// /// Example use-case: pasting `text/html` should try `text/html` first, but if it's not /// available, any other plain text format will do fine too. TextWithPriority(&'a str), /// Request a specific MIME type. Specific(&'a str), } /// Seat to operate on. #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)] pub enum Seat<'a> { /// Operate on one of the existing seats depending on the order returned by the compositor. /// /// This is perfectly fine when only a single seat is present, so for most configurations. #[default] Unspecified, /// Operate on a seat with the given name. Specific(&'a str), } struct State { common: common::State, // The value is the set of MIME types in the offer. // TODO: We never remove offers from here, even if we don't use them or after destroying them. offers: HashMap>, got_primary_selection: bool, } delegate_dispatch!(State: [WlSeat: ()] => common::State); impl AsMut for State { fn as_mut(&mut self) -> &mut common::State { &mut self.common } } /// Errors that can occur for pasting and listing MIME types. /// /// You may want to ignore some of these errors (rather than show an error message), like /// `NoSeats`, `ClipboardEmpty` or `NoMimeType` as they are essentially equivalent to an empty /// clipboard. #[derive(thiserror::Error, Debug)] pub enum Error { #[error("There are no seats")] NoSeats, #[error("The clipboard of the requested seat is empty")] ClipboardEmpty, #[error("No suitable type of content copied")] NoMimeType, #[error("Couldn't open the provided Wayland socket")] SocketOpenError(#[source] io::Error), #[error("Couldn't connect to the Wayland compositor")] WaylandConnection(#[source] ConnectError), #[error("Wayland compositor communication error")] WaylandCommunication(#[source] DispatchError), #[error( "A required Wayland protocol ({} version {}) is not supported by the compositor", name, version )] MissingProtocol { name: &'static str, version: u32 }, #[error("The compositor does not support primary selection")] PrimarySelectionUnsupported, #[error("The requested seat was not found")] SeatNotFound, #[error("Couldn't create a pipe for content transfer")] PipeCreation(#[source] io::Error), } impl From for Error { fn from(x: common::Error) -> Self { use common::Error::*; match x { SocketOpenError(err) => Error::SocketOpenError(err), WaylandConnection(err) => Error::WaylandConnection(err), WaylandCommunication(err) => Error::WaylandCommunication(err.into()), MissingProtocol { name, version } => Error::MissingProtocol { name, version }, } } } impl Dispatch for State { fn event( _state: &mut Self, _proxy: &WlRegistry, _event: ::Event, _data: &GlobalListContents, _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle, ) { } } impl_dispatch_manager!(State); impl_dispatch_device!(State, WlSeat, |state: &mut Self, event, seat| { match event { Event::DataOffer { id } => { let offer = data_control::Offer::from(id); state.offers.insert(offer, HashSet::new()); } Event::Selection { id } => { let offer = id.map(data_control::Offer::from); let seat = state.common.seats.get_mut(seat).unwrap(); seat.set_offer(offer); } Event::Finished => { // Destroy the device stored in the seat as it's no longer valid. let seat = state.common.seats.get_mut(seat).unwrap(); seat.set_device(None); } Event::PrimarySelection { id } => { let offer = id.map(data_control::Offer::from); state.got_primary_selection = true; let seat = state.common.seats.get_mut(seat).unwrap(); seat.set_primary_offer(offer); } _ => (), } }); impl_dispatch_offer!(State, |state: &mut Self, offer: data_control::Offer, event| { if let Event::Offer { mime_type } = event { state.offers.get_mut(&offer).unwrap().insert(mime_type); } }); fn get_offer( primary: bool, seat: Seat<'_>, socket_name: Option, ) -> Result<(EventQueue, State, data_control::Offer), Error> { let (mut queue, mut common) = initialize(primary, socket_name)?; // Check if there are no seats. if common.seats.is_empty() { return Err(Error::NoSeats); } // Go through the seats and get their data devices. for (seat, data) in &mut common.seats { let device = common .clipboard_manager .get_data_device(seat, &queue.handle(), seat.clone()); data.set_device(Some(device)); } let mut state = State { common, offers: HashMap::new(), got_primary_selection: false, }; // Retrieve all seat names and offers. queue .roundtrip(&mut state) .map_err(Error::WaylandCommunication)?; // Check if the compositor supports primary selection. if primary && !state.got_primary_selection { return Err(Error::PrimarySelectionUnsupported); } // Figure out which offer we're interested in. let data = match seat { Seat::Unspecified => state.common.seats.values().next(), Seat::Specific(name) => state .common .seats .values() .find(|data| data.name.as_deref() == Some(name)), }; let Some(data) = data else { return Err(Error::SeatNotFound); }; let offer = if primary { &data.primary_offer } else { &data.offer }; // Check if we found anything. match offer.clone() { Some(offer) => Ok((queue, state, offer)), None => Err(Error::ClipboardEmpty), } } /// Retrieves the offered MIME types. /// /// If `seat` is `None`, uses an unspecified seat (it depends on the order returned by the /// compositor). This is perfectly fine when only a single seat is present, so for most /// configurations. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # use wl_clipboard_rs::paste::Error; /// # fn foo() -> Result<(), Error> { /// use wl_clipboard_rs::{paste::{get_mime_types, ClipboardType, Seat}}; /// /// let mime_types = get_mime_types(ClipboardType::Regular, Seat::Unspecified)?; /// for mime_type in mime_types { /// println!("{}", mime_type); /// } /// # Ok(()) /// # } /// ``` #[inline] pub fn get_mime_types(clipboard: ClipboardType, seat: Seat<'_>) -> Result, Error> { get_mime_types_internal(clipboard, seat, None) } // The internal function accepts the socket name, used for tests. pub(crate) fn get_mime_types_internal( clipboard: ClipboardType, seat: Seat<'_>, socket_name: Option, ) -> Result, Error> { let primary = clipboard == ClipboardType::Primary; let (_, mut state, offer) = get_offer(primary, seat, socket_name)?; Ok(state.offers.remove(&offer).unwrap()) } /// Retrieves the clipboard contents. /// /// This function returns a tuple of the reading end of a pipe containing the clipboard contents /// and the actual MIME type of the contents. /// /// If `seat` is `None`, uses an unspecified seat (it depends on the order returned by the /// compositor). This is perfectly fine when only a single seat is present, so for most /// configurations. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # fn foo() -> Result<(), Box> { /// use std::io::Read; /// use wl_clipboard_rs::{paste::{get_contents, ClipboardType, Error, MimeType, Seat}}; /// /// let result = get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Any); /// match result { /// Ok((mut pipe, mime_type)) => { /// println!("Got data of the {} MIME type", &mime_type); /// /// let mut contents = vec![]; /// pipe.read_to_end(&mut contents)?; /// println!("Read {} bytes of data", contents.len()); /// } /// /// Err(Error::NoSeats) | Err(Error::ClipboardEmpty) | Err(Error::NoMimeType) => { /// // The clipboard is empty, nothing to worry about. /// } /// /// Err(err) => Err(err)? /// } /// # Ok(()) /// # } /// ``` #[inline] pub fn get_contents( clipboard: ClipboardType, seat: Seat<'_>, mime_type: MimeType<'_>, ) -> Result<(PipeReader, String), Error> { get_contents_internal(clipboard, seat, mime_type, None) } // The internal function accepts the socket name, used for tests. pub(crate) fn get_contents_internal( clipboard: ClipboardType, seat: Seat<'_>, mime_type: MimeType<'_>, socket_name: Option, ) -> Result<(PipeReader, String), Error> { let primary = clipboard == ClipboardType::Primary; let (mut queue, mut state, offer) = get_offer(primary, seat, socket_name)?; let mut mime_types = state.offers.remove(&offer).unwrap(); // Find the desired MIME type. let mime_type = match mime_type { MimeType::Any => mime_types .take("text/plain;charset=utf-8") .or_else(|| mime_types.take("UTF8_STRING")) .or_else(|| mime_types.iter().find(|x| is_text(x)).cloned()) .or_else(|| mime_types.drain().next()), MimeType::Text => mime_types .take("text/plain;charset=utf-8") .or_else(|| mime_types.take("UTF8_STRING")) .or_else(|| mime_types.drain().find(|x| is_text(x))), MimeType::TextWithPriority(priority) => mime_types .take(priority) .or_else(|| mime_types.take("text/plain;charset=utf-8")) .or_else(|| mime_types.take("UTF8_STRING")) .or_else(|| mime_types.drain().find(|x| is_text(x))), MimeType::Specific(mime_type) => mime_types.take(mime_type), }; // Check if a suitable MIME type is copied. if mime_type.is_none() { return Err(Error::NoMimeType); } let mime_type = mime_type.unwrap(); // Create a pipe for content transfer. let (read, write) = pipe().map_err(Error::PipeCreation)?; // Start the transfer. offer.receive(mime_type.clone(), write.as_fd()); drop(write); // A flush() is not enough here, it will result in sometimes pasting empty contents. I suspect this is due to a // race between the compositor reacting to the receive request, and the compositor reacting to wl-paste // disconnecting after queue is dropped. The roundtrip solves that race. queue .roundtrip(&mut state) .map_err(Error::WaylandCommunication)?; Ok((read, mime_type)) } wl-clipboard-rs-0.9.2/src/seat_data.rs000064400000000000000000000027401046102023000157220ustar 00000000000000use crate::data_control::{Device, Offer}; #[derive(Default)] pub struct SeatData { /// The name of this seat, if any. pub name: Option, /// The data device of this seat, if any. pub device: Option, /// The data offer of this seat, if any. pub offer: Option, /// The primary-selection data offer of this seat, if any. pub primary_offer: Option, } impl SeatData { /// Sets this seat's name. pub fn set_name(&mut self, name: String) { self.name = Some(name) } /// Sets this seat's device. /// /// Destroys the old one, if any. pub fn set_device(&mut self, device: Option) { let old_device = self.device.take(); self.device = device; if let Some(device) = old_device { device.destroy(); } } /// Sets this seat's data offer. /// /// Destroys the old one, if any. pub fn set_offer(&mut self, new_offer: Option) { let old_offer = self.offer.take(); self.offer = new_offer; if let Some(offer) = old_offer { offer.destroy(); } } /// Sets this seat's primary-selection data offer. /// /// Destroys the old one, if any. pub fn set_primary_offer(&mut self, new_offer: Option) { let old_offer = self.primary_offer.take(); self.primary_offer = new_offer; if let Some(offer) = old_offer { offer.destroy(); } } } wl-clipboard-rs-0.9.2/src/tests/copy.rs000064400000000000000000000305221046102023000161100ustar 00000000000000use std::collections::HashMap; use std::io::Read; use std::sync::mpsc::channel; use std::sync::{Arc, Mutex}; use proptest::prelude::*; use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; use crate::copy::*; use crate::paste; use crate::paste::get_contents_internal; use crate::tests::state::*; use crate::tests::TestServer; #[test] fn clear_test() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let state = State { seats: HashMap::from([( "seat0".into(), SeatInfo { offer: Some(OfferInfo::Buffered { data: HashMap::from([("regular".into(), vec![1, 2, 3])]), }), primary_offer: Some(OfferInfo::Buffered { data: HashMap::from([("primary".into(), vec![1, 2, 3])]), }), }, )]), ..Default::default() }; state.create_seats(&server); let state = Arc::new(Mutex::new(state)); let socket_name = server.socket_name().to_owned(); server.run_mutex(state.clone()); clear_internal(ClipboardType::Regular, Seat::All, Some(socket_name)).unwrap(); let state = state.lock().unwrap(); assert!(state.seats["seat0"].offer.is_none()); assert!(state.seats["seat0"].primary_offer.is_some()); } #[test] fn copy_test() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let (tx, rx) = channel(); let state = State { seats: HashMap::from([( "seat0".into(), SeatInfo { ..Default::default() }, )]), selection_updated_sender: Some(tx), ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let sources = vec![MimeSource { source: Source::Bytes([1, 3, 3, 7][..].into()), mime_type: MimeType::Specific("test".into()), }]; copy_internal(Options::new(), sources, Some(socket_name.clone())).unwrap(); // Wait for the copy. let mime_types = rx.recv().unwrap().unwrap(); assert_eq!(mime_types, ["test"]); let (mut read, mime_type) = get_contents_internal( paste::ClipboardType::Regular, paste::Seat::Unspecified, paste::MimeType::Any, Some(socket_name.clone()), ) .unwrap(); let mut contents = vec![]; read.read_to_end(&mut contents).unwrap(); assert_eq!(mime_type, "test"); assert_eq!(contents, [1, 3, 3, 7]); clear_internal(ClipboardType::Both, Seat::All, Some(socket_name)).unwrap(); } #[test] fn copy_multi_test() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let (tx, rx) = channel(); let state = State { seats: HashMap::from([( "seat0".into(), SeatInfo { ..Default::default() }, )]), selection_updated_sender: Some(tx), ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let sources = vec![ MimeSource { source: Source::Bytes([1, 3, 3, 7][..].into()), mime_type: MimeType::Specific("test".into()), }, MimeSource { source: Source::Bytes([2, 4, 4][..].into()), mime_type: MimeType::Specific("test2".into()), }, // Ignored because it's the second "test" MIME type. MimeSource { source: Source::Bytes([4, 3, 2, 1][..].into()), mime_type: MimeType::Specific("test".into()), }, // The first text source, additional text types should fall back here. MimeSource { source: Source::Bytes(b"hello fallback"[..].into()), mime_type: MimeType::Text, }, // A specific override of an additional text type. MimeSource { source: Source::Bytes(b"hello TEXT"[..].into()), mime_type: MimeType::Specific("TEXT".into()), }, ]; copy_internal(Options::new(), sources, Some(socket_name.clone())).unwrap(); // Wait for the copy. let mut mime_types = rx.recv().unwrap().unwrap(); mime_types.sort_unstable(); assert_eq!( mime_types, [ "STRING", "TEXT", "UTF8_STRING", "test", "test2", "text/plain", "text/plain;charset=utf-8", ] ); let expected = [ ("test", &[1, 3, 3, 7][..]), ("test2", &[2, 4, 4][..]), ("STRING", &b"hello fallback"[..]), ("TEXT", &b"hello TEXT"[..]), ]; for (mime_type, expected_contents) in expected { let mut read = get_contents_internal( paste::ClipboardType::Regular, paste::Seat::Unspecified, paste::MimeType::Specific(mime_type), Some(socket_name.clone()), ) .unwrap() .0; let mut contents = vec![]; read.read_to_end(&mut contents).unwrap(); assert_eq!(contents, expected_contents); } clear_internal(ClipboardType::Both, Seat::All, Some(socket_name)).unwrap(); } #[test] fn copy_multi_no_additional_text_mime_types_test() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let (tx, rx) = channel(); let state = State { seats: HashMap::from([( "seat0".into(), SeatInfo { ..Default::default() }, )]), selection_updated_sender: Some(tx), ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let mut opts = Options::new(); opts.omit_additional_text_mime_types(true); let sources = vec![ MimeSource { source: Source::Bytes([1, 3, 3, 7][..].into()), mime_type: MimeType::Specific("test".into()), }, MimeSource { source: Source::Bytes([2, 4, 4][..].into()), mime_type: MimeType::Specific("test2".into()), }, // Ignored because it's the second "test" MIME type. MimeSource { source: Source::Bytes([4, 3, 2, 1][..].into()), mime_type: MimeType::Specific("test".into()), }, // A specific override of an additional text type. MimeSource { source: Source::Bytes(b"hello TEXT"[..].into()), mime_type: MimeType::Specific("TEXT".into()), }, ]; copy_internal(opts, sources, Some(socket_name.clone())).unwrap(); // Wait for the copy. let mut mime_types = rx.recv().unwrap().unwrap(); mime_types.sort_unstable(); assert_eq!(mime_types, ["TEXT", "test", "test2"]); let expected = [ ("test", &[1, 3, 3, 7][..]), ("test2", &[2, 4, 4][..]), ("TEXT", &b"hello TEXT"[..]), ]; for (mime_type, expected_contents) in expected { let mut read = get_contents_internal( paste::ClipboardType::Regular, paste::Seat::Unspecified, paste::MimeType::Specific(mime_type), Some(socket_name.clone()), ) .unwrap() .0; let mut contents = vec![]; read.read_to_end(&mut contents).unwrap(); assert_eq!(contents, expected_contents); } clear_internal(ClipboardType::Both, Seat::All, Some(socket_name)).unwrap(); } // The idea here is to exceed the pipe capacity. This fails unless O_NONBLOCK is cleared when // sending data over the pipe using cat. #[test] fn copy_large() { // Assuming the default pipe capacity is 65536. let mut bytes_to_copy = vec![]; for i in 0..65536 * 10 { bytes_to_copy.push((i % 256) as u8); } let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let (tx, rx) = channel(); let state = State { seats: HashMap::from([( "seat0".into(), SeatInfo { ..Default::default() }, )]), selection_updated_sender: Some(tx), // Emulate what XWayland does and set O_NONBLOCK. set_nonblock_on_write_fd: true, ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let sources = vec![MimeSource { source: Source::Bytes(bytes_to_copy.clone().into_boxed_slice()), mime_type: MimeType::Specific("test".into()), }]; copy_internal(Options::new(), sources, Some(socket_name.clone())).unwrap(); // Wait for the copy. let mime_types = rx.recv().unwrap().unwrap(); assert_eq!(mime_types, ["test"]); let (mut read, mime_type) = get_contents_internal( paste::ClipboardType::Regular, paste::Seat::Unspecified, paste::MimeType::Any, Some(socket_name.clone()), ) .unwrap(); let mut contents = vec![]; read.read_to_end(&mut contents).unwrap(); assert_eq!(mime_type, "test"); assert_eq!(contents.len(), bytes_to_copy.len()); assert_eq!(contents, bytes_to_copy); clear_internal(ClipboardType::Both, Seat::All, Some(socket_name)).unwrap(); } proptest! { #[test] fn copy_randomized( mut state: State, clipboard_type: ClipboardType, source: Source, mime_type: MimeType, seat_index: prop::sample::Index, clipboard_type_index: prop::sample::Index, ) { prop_assume!(!state.seats.is_empty()); let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let (tx, rx) = channel(); state.selection_updated_sender = Some(tx); state.create_seats(&server); let seat_index = seat_index.index(state.seats.len()); let seat_name = state.seats.keys().nth(seat_index).unwrap(); let seat_name = seat_name.to_owned(); let paste_clipboard_type = match clipboard_type { ClipboardType::Regular => paste::ClipboardType::Regular, ClipboardType::Primary => paste::ClipboardType::Primary, ClipboardType::Both => *clipboard_type_index .get(&[paste::ClipboardType::Regular, paste::ClipboardType::Primary]), }; let socket_name = server.socket_name().to_owned(); server.run(state); let expected_contents = match &source { Source::Bytes(bytes) => bytes.clone(), Source::StdIn => unreachable!(), }; let sources = vec![MimeSource { source, mime_type: mime_type.clone(), }]; let mut opts = Options::new(); opts.clipboard(clipboard_type); opts.seat(Seat::Specific(seat_name.clone())); opts.omit_additional_text_mime_types(true); copy_internal(opts, sources, Some(socket_name.clone())).unwrap(); // Wait for the copy. let mut mime_types = rx.recv().unwrap().unwrap(); mime_types.sort_unstable(); match &mime_type { MimeType::Autodetect => unreachable!(), MimeType::Text => assert_eq!(mime_types, ["text/plain"]), MimeType::Specific(mime) => assert_eq!(mime_types, [mime.clone()]), } let paste_mime_type = match mime_type { MimeType::Autodetect => unreachable!(), MimeType::Text => "text/plain".into(), MimeType::Specific(mime) => mime, }; let (mut read, mime_type) = get_contents_internal( paste_clipboard_type, paste::Seat::Specific(&seat_name), paste::MimeType::Specific(&paste_mime_type), Some(socket_name.clone()), ) .unwrap(); let mut contents = vec![]; read.read_to_end(&mut contents).unwrap(); assert_eq!(mime_type, paste_mime_type); assert_eq!(contents.into_boxed_slice(), expected_contents); clear_internal(clipboard_type, Seat::Specific(seat_name), Some(socket_name)).unwrap(); } } wl-clipboard-rs-0.9.2/src/tests/mod.rs000064400000000000000000000107771046102023000157270ustar 00000000000000use std::ffi::OsStr; use std::os::fd::OwnedFd; use std::sync::atomic::AtomicU8; use std::sync::atomic::Ordering::SeqCst; use std::sync::{Arc, Mutex}; use std::thread; use rustix::event::epoll; use wayland_backend::server::ClientData; use wayland_server::{Display, ListeningSocket}; mod copy; mod paste; mod state; mod utils; pub struct TestServer { pub display: Display, pub socket: ListeningSocket, pub epoll: OwnedFd, } struct ClientCounter(AtomicU8); impl ClientData for ClientCounter { fn disconnected( &self, _client_id: wayland_backend::server::ClientId, _reason: wayland_backend::server::DisconnectReason, ) { self.0.fetch_sub(1, SeqCst); } } impl TestServer { pub fn new() -> Self { let mut display = Display::new().unwrap(); let socket = ListeningSocket::bind_auto("wl-clipboard-rs-test", 0..).unwrap(); let epoll = epoll::create(epoll::CreateFlags::CLOEXEC).unwrap(); epoll::add( &epoll, &socket, epoll::EventData::new_u64(0), epoll::EventFlags::IN, ) .unwrap(); epoll::add( &epoll, display.backend().poll_fd(), epoll::EventData::new_u64(1), epoll::EventFlags::IN, ) .unwrap(); TestServer { display, socket, epoll, } } pub fn socket_name(&self) -> &OsStr { self.socket.socket_name().unwrap() } pub fn run(self, mut state: S) { thread::spawn(move || self.run_internal(&mut state)); } pub fn run_mutex(self, state: Arc>) { thread::spawn(move || { let mut state = state.lock().unwrap(); self.run_internal(&mut *state); }); } fn run_internal(mut self, state: &mut S) { let mut waiting_for_first_client = true; let client_counter = Arc::new(ClientCounter(AtomicU8::new(0))); while client_counter.0.load(SeqCst) > 0 || waiting_for_first_client { // Wait for requests from the client. let mut events = epoll::EventVec::with_capacity(2); epoll::wait(&self.epoll, &mut events, -1).unwrap(); for event in &events { match event.data.u64() { 0 => { // Try to accept a new client. if let Some(stream) = self.socket.accept().unwrap() { waiting_for_first_client = false; client_counter.0.fetch_add(1, SeqCst); self.display .handle() .insert_client(stream, client_counter.clone()) .unwrap(); } } 1 => { // Try to dispatch client messages. self.display.dispatch_clients(state).unwrap(); self.display.flush_clients().unwrap(); } x => panic!("unexpected epoll event: {x}"), } } } } } // https://github.com/Smithay/wayland-rs/blob/90a9ad1f8f1fdef72e96d3c48bdb76b53a7722ff/wayland-tests/tests/helpers/mod.rs #[macro_export] macro_rules! server_ignore_impl { ($handler:ty => [$($iface:ty),*]) => { $( impl wayland_server::Dispatch<$iface, ()> for $handler { fn request( _: &mut Self, _: &wayland_server::Client, _: &$iface, _: <$iface as wayland_server::Resource>::Request, _: &(), _: &wayland_server::DisplayHandle, _: &mut wayland_server::DataInit<'_, Self>, ) { } } )* } } #[macro_export] macro_rules! server_ignore_global_impl { ($handler:ty => [$($iface:ty),*]) => { $( impl wayland_server::GlobalDispatch<$iface, ()> for $handler { fn bind( _: &mut Self, _: &wayland_server::DisplayHandle, _: &wayland_server::Client, new_id: wayland_server::New<$iface>, _: &(), data_init: &mut wayland_server::DataInit<'_, Self>, ) { data_init.init(new_id, ()); } } )* } } wl-clipboard-rs-0.9.2/src/tests/paste.rs000064400000000000000000000302511046102023000162510ustar 00000000000000use std::collections::{HashMap, HashSet}; use std::io::Read; use proptest::prelude::*; use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; use crate::paste::*; use crate::tests::state::*; use crate::tests::TestServer; #[test] fn get_mime_types_test() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let state = State { seats: HashMap::from([( "seat0".into(), SeatInfo { offer: Some(OfferInfo::Buffered { data: HashMap::from([ ("first".into(), vec![]), ("second".into(), vec![]), ("third".into(), vec![]), ]), }), ..Default::default() }, )]), ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let mime_types = get_mime_types_internal(ClipboardType::Regular, Seat::Unspecified, Some(socket_name)) .unwrap(); let expected = HashSet::from(["first", "second", "third"].map(String::from)); assert_eq!(mime_types, expected); } #[test] fn get_mime_types_no_data_control() { let server = TestServer::new(); let state = State { seats: HashMap::from([( "seat0".into(), SeatInfo { ..Default::default() }, )]), ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let result = get_mime_types_internal(ClipboardType::Regular, Seat::Unspecified, Some(socket_name)); assert!(matches!( result, Err(Error::MissingProtocol { name: "ext-data-control, or wlr-data-control", version: 1 }) )); } #[test] fn get_mime_types_no_data_control_2() { let server = TestServer::new(); let state = State { seats: HashMap::from([( "seat0".into(), SeatInfo { ..Default::default() }, )]), ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let result = get_mime_types_internal(ClipboardType::Primary, Seat::Unspecified, Some(socket_name)); assert!(matches!( result, Err(Error::MissingProtocol { name: "ext-data-control, or wlr-data-control", version: 2 }) )); } #[test] fn get_mime_types_no_seats() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let state = State { ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let result = get_mime_types_internal(ClipboardType::Primary, Seat::Unspecified, Some(socket_name)); assert!(matches!(result, Err(Error::NoSeats))); } #[test] fn get_mime_types_empty_clipboard() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let state = State { seats: HashMap::from([( "seat0".into(), SeatInfo { ..Default::default() }, )]), ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let result = get_mime_types_internal(ClipboardType::Primary, Seat::Unspecified, Some(socket_name)); assert!(matches!(result, Err(Error::ClipboardEmpty))); } #[test] fn get_mime_types_specific_seat() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let state = State { seats: HashMap::from([ ( "seat0".into(), SeatInfo { ..Default::default() }, ), ( "yay".into(), SeatInfo { offer: Some(OfferInfo::Buffered { data: HashMap::from([ ("first".into(), vec![]), ("second".into(), vec![]), ("third".into(), vec![]), ]), }), ..Default::default() }, ), ]), ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let mime_types = get_mime_types_internal( ClipboardType::Regular, Seat::Specific("yay"), Some(socket_name), ) .unwrap(); let expected = HashSet::from(["first", "second", "third"].map(String::from)); assert_eq!(mime_types, expected); } #[test] fn get_mime_types_primary() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let state = State { seats: HashMap::from([( "seat0".into(), SeatInfo { primary_offer: Some(OfferInfo::Buffered { data: HashMap::from([ ("first".into(), vec![]), ("second".into(), vec![]), ("third".into(), vec![]), ]), }), ..Default::default() }, )]), ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let mime_types = get_mime_types_internal(ClipboardType::Primary, Seat::Unspecified, Some(socket_name)) .unwrap(); let expected = HashSet::from(["first", "second", "third"].map(String::from)); assert_eq!(mime_types, expected); } #[test] fn get_contents_test() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let state = State { seats: HashMap::from([( "seat0".into(), SeatInfo { offer: Some(OfferInfo::Buffered { data: HashMap::from([("application/octet-stream".into(), vec![1, 3, 3, 7])]), }), ..Default::default() }, )]), ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let (mut read, mime_type) = get_contents_internal( ClipboardType::Regular, Seat::Unspecified, MimeType::Any, Some(socket_name), ) .unwrap(); assert_eq!(mime_type, "application/octet-stream"); let mut contents = vec![]; read.read_to_end(&mut contents).unwrap(); assert_eq!(contents, [1, 3, 3, 7]); } #[test] fn get_contents_wrong_mime_type() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let state = State { seats: HashMap::from([( "seat0".into(), SeatInfo { offer: Some(OfferInfo::Buffered { data: HashMap::from([("application/octet-stream".into(), vec![1, 3, 3, 7])]), }), ..Default::default() }, )]), ..Default::default() }; state.create_seats(&server); let socket_name = server.socket_name().to_owned(); server.run(state); let result = get_contents_internal( ClipboardType::Regular, Seat::Unspecified, MimeType::Specific("wrong"), Some(socket_name), ); assert!(matches!(result, Err(Error::NoMimeType))); } proptest! { #[test] fn get_mime_types_randomized( mut state: State, clipboard_type: ClipboardType, seat_index: prop::sample::Index, ) { let server = TestServer::new(); let socket_name = server.socket_name().to_owned(); server .display .handle() .create_global::(2, ()); state.create_seats(&server); if state.seats.is_empty() { server.run(state); let result = get_mime_types_internal(clipboard_type, Seat::Unspecified, Some(socket_name)); prop_assert!(matches!(result, Err(Error::NoSeats))); } else { let seat_index = seat_index.index(state.seats.len()); let (seat_name, seat_info) = state.seats.iter().nth(seat_index).unwrap(); let seat_name = seat_name.to_owned(); let seat_info = (*seat_info).clone(); server.run(state); let result = get_mime_types_internal( clipboard_type, Seat::Specific(&seat_name), Some(socket_name), ); let expected_offer = match clipboard_type { ClipboardType::Regular => &seat_info.offer, ClipboardType::Primary => &seat_info.primary_offer, }; match expected_offer { None => prop_assert!(matches!(result, Err(Error::ClipboardEmpty))), Some(offer) => prop_assert_eq!(result.unwrap(), offer.data().keys().cloned().collect()), } } } #[test] fn get_contents_randomized( mut state: State, clipboard_type: ClipboardType, seat_index: prop::sample::Index, mime_index: prop::sample::Index, ) { let server = TestServer::new(); let socket_name = server.socket_name().to_owned(); server .display .handle() .create_global::(2, ()); state.create_seats(&server); if state.seats.is_empty() { server.run(state); let result = get_mime_types_internal(clipboard_type, Seat::Unspecified, Some(socket_name)); prop_assert!(matches!(result, Err(Error::NoSeats))); } else { let seat_index = seat_index.index(state.seats.len()); let (seat_name, seat_info) = state.seats.iter().nth(seat_index).unwrap(); let seat_name = seat_name.to_owned(); let seat_info = (*seat_info).clone(); let expected_offer = match clipboard_type { ClipboardType::Regular => &seat_info.offer, ClipboardType::Primary => &seat_info.primary_offer, }; let mime_type = match expected_offer { Some(offer) if !offer.data().is_empty() => { let mime_index = mime_index.index(offer.data().len()); Some(offer.data().keys().nth(mime_index).unwrap()) } _ => None, }; server.run(state); let result = get_contents_internal( clipboard_type, Seat::Specific(&seat_name), mime_type.map_or(MimeType::Any, |name| MimeType::Specific(name)), Some(socket_name), ); match expected_offer { None => prop_assert!(matches!(result, Err(Error::ClipboardEmpty))), Some(offer) => { if offer.data().is_empty() { prop_assert!(matches!(result, Err(Error::NoMimeType))); } else { let mime_type = mime_type.unwrap(); let (mut read, recv_mime_type) = result.unwrap(); prop_assert_eq!(&recv_mime_type, mime_type); let mut contents = vec![]; read.read_to_end(&mut contents).unwrap(); prop_assert_eq!(&contents, &offer.data()[mime_type]); } }, } } } } wl-clipboard-rs-0.9.2/src/tests/state.rs000064400000000000000000000236411046102023000162620ustar 00000000000000//! Test compositor implementation. //! //! This module contains the test compositor ([`State`]), which boils down to a minimal wlr-data-control protocol //! implementation. The compositor can be initialized with an arbitrary set of seats, each offering arbitrary clipboard //! contents in their regular and primary selections. Then the compositor handles all wlr-data-control interactions, such //! as copying and pasting. use std::collections::HashMap; use std::io::Write; use std::os::fd::AsFd; use std::sync::atomic::AtomicU8; use std::sync::atomic::Ordering::SeqCst; use std::sync::mpsc::Sender; use os_pipe::PipeWriter; use proptest::prelude::*; use proptest_derive::Arbitrary; use rustix::fs::{fcntl_setfl, OFlags}; use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_device_v1::{ self, ZwlrDataControlDeviceV1, }; use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::{ self, ZwlrDataControlManagerV1, }; use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_offer_v1::{ self, ZwlrDataControlOfferV1, }; use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_source_v1::{ self, ZwlrDataControlSourceV1, }; use wayland_server::protocol::wl_seat::WlSeat; use wayland_server::{Dispatch, GlobalDispatch, Resource}; use super::TestServer; use crate::server_ignore_global_impl; #[derive(Debug, Clone, Arbitrary)] pub enum OfferInfo { Buffered { #[proptest( strategy = "prop::collection::hash_map(any::(), prop::collection::vec(any::(), 0..5), 0..5)" )] data: HashMap>, }, #[proptest(skip)] Runtime { source: ZwlrDataControlSourceV1 }, } impl Default for OfferInfo { fn default() -> Self { Self::Buffered { data: HashMap::new(), } } } impl OfferInfo { fn mime_types(&self, state: &State) -> Vec { match self { OfferInfo::Buffered { data } => data.keys().cloned().collect(), OfferInfo::Runtime { source } => state.sources[source].clone(), } } pub fn data(&self) -> &HashMap> { match self { OfferInfo::Buffered { data } => data, OfferInfo::Runtime { .. } => panic!(), } } } #[derive(Debug, Clone, Default, Arbitrary)] pub struct SeatInfo { pub offer: Option, pub primary_offer: Option, } #[derive(Debug, Clone, Default, Arbitrary)] pub struct State { #[proptest(strategy = "prop::collection::hash_map(any::(), any::(), 0..5)")] pub seats: HashMap, #[proptest(value = "HashMap::new()")] pub sources: HashMap>, #[proptest(value = "None")] pub selection_updated_sender: Option>>>, pub set_nonblock_on_write_fd: bool, } server_ignore_global_impl!(State => [ZwlrDataControlManagerV1]); impl State { pub fn create_seats(&self, server: &TestServer) { for name in self.seats.keys() { server .display .handle() .create_global::(6, name.clone()); } } } impl GlobalDispatch for State { fn bind( _state: &mut Self, _handle: &wayland_server::DisplayHandle, _client: &wayland_server::Client, resource: wayland_server::New, name: &String, data_init: &mut wayland_server::DataInit<'_, Self>, ) { let seat = data_init.init(resource, name.clone()); seat.name((*name).to_owned()); } } impl Dispatch for State { fn request( _state: &mut Self, _client: &wayland_server::Client, _seat: &WlSeat, _request: ::Request, _name: &String, _dhandle: &wayland_server::DisplayHandle, _data_init: &mut wayland_server::DataInit<'_, Self>, ) { } } impl Dispatch for State { fn request( state: &mut Self, client: &wayland_server::Client, manager: &ZwlrDataControlManagerV1, request: ::Request, _data: &(), dhandle: &wayland_server::DisplayHandle, data_init: &mut wayland_server::DataInit<'_, Self>, ) { match request { zwlr_data_control_manager_v1::Request::GetDataDevice { id, seat } => { let name: &String = seat.data().unwrap(); let info = &state.seats[name]; let data_device = data_init.init(id, (*name).clone()); let create_offer = |offer_info: &OfferInfo, is_primary: bool| { let offer = client .create_resource::<_, _, Self>( dhandle, manager.version(), (name.clone(), is_primary), ) .unwrap(); data_device.data_offer(&offer); for mime_type in offer_info.mime_types(state) { offer.offer(mime_type); } offer }; let selection = info .offer .as_ref() .map(|offer_info| create_offer(offer_info, false)); data_device.selection(selection.as_ref()); let primary_selection = info .primary_offer .as_ref() .map(|offer_info| create_offer(offer_info, true)); data_device.primary_selection(primary_selection.as_ref()); } zwlr_data_control_manager_v1::Request::CreateDataSource { id } => { let source = data_init.init(id, AtomicU8::new(0)); state.sources.insert(source, vec![]); } _ => (), } } } impl Dispatch for State { fn request( state: &mut Self, _client: &wayland_server::Client, _resource: &ZwlrDataControlDeviceV1, request: ::Request, name: &String, _dhandle: &wayland_server::DisplayHandle, _data_init: &mut wayland_server::DataInit<'_, Self>, ) { match request { zwlr_data_control_device_v1::Request::SetSelection { source } => { let mime_types = source.as_ref().map(|source| state.sources[source].clone()); let info = state.seats.get_mut(name).unwrap(); if let Some(source) = &source { source.data::().unwrap().fetch_add(1, SeqCst); } if let Some(OfferInfo::Runtime { source }) = &info.offer { if source.data::().unwrap().fetch_sub(1, SeqCst) == 1 { source.cancelled(); } } info.offer = source.map(|source| OfferInfo::Runtime { source }); if let Some(sender) = &state.selection_updated_sender { let _ = sender.send(mime_types); } } zwlr_data_control_device_v1::Request::SetPrimarySelection { source } => { let mime_types = source.as_ref().map(|source| state.sources[source].clone()); let info = state.seats.get_mut(name).unwrap(); if let Some(source) = &source { source.data::().unwrap().fetch_add(1, SeqCst); } if let Some(OfferInfo::Runtime { source }) = &info.primary_offer { if source.data::().unwrap().fetch_sub(1, SeqCst) == 1 { source.cancelled(); } } info.primary_offer = source.map(|source| OfferInfo::Runtime { source }); if let Some(sender) = &state.selection_updated_sender { let _ = sender.send(mime_types); } } _ => (), } } } impl Dispatch for State { fn request( state: &mut Self, _client: &wayland_server::Client, _resource: &ZwlrDataControlOfferV1, request: ::Request, (name, is_primary): &(String, bool), _dhandle: &wayland_server::DisplayHandle, _data_init: &mut wayland_server::DataInit<'_, Self>, ) { if let zwlr_data_control_offer_v1::Request::Receive { mime_type, fd } = request { let info = &state.seats[name]; let offer_info = if *is_primary { info.primary_offer.as_ref().unwrap() } else { info.offer.as_ref().unwrap() }; match offer_info { OfferInfo::Buffered { data } => { let mut write = PipeWriter::from(fd); let _ = write.write_all(&data[mime_type.as_str()]); } OfferInfo::Runtime { source } => { if state.set_nonblock_on_write_fd { fcntl_setfl(&fd, OFlags::NONBLOCK).unwrap(); } source.send(mime_type, fd.as_fd()) } } } } } impl Dispatch for State { fn request( state: &mut Self, _client: &wayland_server::Client, source: &ZwlrDataControlSourceV1, request: ::Request, _data: &AtomicU8, _dhandle: &wayland_server::DisplayHandle, _data_init: &mut wayland_server::DataInit<'_, Self>, ) { if let zwlr_data_control_source_v1::Request::Offer { mime_type } = request { state.sources.get_mut(source).unwrap().push(mime_type); } } } wl-clipboard-rs-0.9.2/src/tests/utils.rs000064400000000000000000000165171046102023000163060ustar 00000000000000use wayland_protocols::ext::data_control::v1::server::ext_data_control_device_v1::ExtDataControlDeviceV1; use wayland_protocols::ext::data_control::v1::server::ext_data_control_manager_v1::{ self, ExtDataControlManagerV1, }; use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1; use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::{ self, ZwlrDataControlManagerV1, }; use wayland_server::protocol::wl_seat::WlSeat; use wayland_server::Dispatch; use crate::tests::TestServer; use crate::utils::*; use crate::{server_ignore_global_impl, server_ignore_impl}; struct State { advertise_primary_selection: bool, } server_ignore_global_impl!(State => [WlSeat, ZwlrDataControlManagerV1, ExtDataControlManagerV1]); server_ignore_impl!(State => [WlSeat, ZwlrDataControlDeviceV1, ExtDataControlDeviceV1]); impl Dispatch for State { fn request( state: &mut Self, _client: &wayland_server::Client, _resource: &ZwlrDataControlManagerV1, request: ::Request, _data: &(), _dhandle: &wayland_server::DisplayHandle, data_init: &mut wayland_server::DataInit<'_, Self>, ) { if let zwlr_data_control_manager_v1::Request::GetDataDevice { id, .. } = request { let data_device = data_init.init(id, ()); if state.advertise_primary_selection { data_device.primary_selection(None); } } } } impl Dispatch for State { fn request( state: &mut Self, _client: &wayland_server::Client, _resource: &ExtDataControlManagerV1, request: ::Request, _data: &(), _dhandle: &wayland_server::DisplayHandle, data_init: &mut wayland_server::DataInit<'_, Self>, ) { if let ext_data_control_manager_v1::Request::GetDataDevice { id, .. } = request { let data_device = data_init.init(id, ()); if state.advertise_primary_selection { data_device.primary_selection(None); } } } } #[test] fn is_primary_selection_supported_test() { let server = TestServer::new(); server .display .handle() .create_global::(6, ()); server .display .handle() .create_global::(2, ()); let state = State { advertise_primary_selection: true, }; let socket_name = server.socket_name().to_owned(); server.run(state); let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); assert!(result); } #[test] fn is_primary_selection_supported_primary_selection_unsupported() { let server = TestServer::new(); server .display .handle() .create_global::(6, ()); server .display .handle() .create_global::(2, ()); let state = State { advertise_primary_selection: false, }; let socket_name = server.socket_name().to_owned(); server.run(state); let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); assert!(!result); } #[test] fn is_primary_selection_supported_data_control_v1() { let server = TestServer::new(); server .display .handle() .create_global::(6, ()); server .display .handle() .create_global::(1, ()); let state = State { advertise_primary_selection: false, }; let socket_name = server.socket_name().to_owned(); server.run(state); let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); assert!(!result); } #[test] fn is_primary_selection_supported_no_seats() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); let state = State { advertise_primary_selection: true, }; let socket_name = server.socket_name().to_owned(); server.run(state); let result = is_primary_selection_supported_internal(Some(socket_name)); assert!(matches!(result, Err(PrimarySelectionCheckError::NoSeats))); } #[test] fn supports_v2_seats() { let server = TestServer::new(); server .display .handle() .create_global::(2, ()); server .display .handle() .create_global::(2, ()); let state = State { advertise_primary_selection: true, }; let socket_name = server.socket_name().to_owned(); server.run(state); let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); assert!(result); } #[test] fn is_primary_selection_supported_no_data_control() { let server = TestServer::new(); server .display .handle() .create_global::(6, ()); let state = State { advertise_primary_selection: false, }; let socket_name = server.socket_name().to_owned(); server.run(state); let result = is_primary_selection_supported_internal(Some(socket_name)); assert!(matches!( result, Err(PrimarySelectionCheckError::MissingProtocol) )); } #[test] fn is_primary_selection_supported_ext_data_control() { let server = TestServer::new(); server .display .handle() .create_global::(6, ()); server .display .handle() .create_global::(1, ()); let state = State { advertise_primary_selection: true, }; let socket_name = server.socket_name().to_owned(); server.run(state); let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); assert!(result); } #[test] fn is_primary_selection_supported_primary_selection_unsupported_ext_data_control() { let server = TestServer::new(); server .display .handle() .create_global::(6, ()); server .display .handle() .create_global::(1, ()); let state = State { advertise_primary_selection: false, }; let socket_name = server.socket_name().to_owned(); server.run(state); let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); assert!(!result); } #[test] fn is_primary_selection_supported_data_control_v1_and_ext_data_control() { let server = TestServer::new(); server .display .handle() .create_global::(6, ()); server .display .handle() .create_global::(1, ()); server .display .handle() .create_global::(1, ()); let state = State { advertise_primary_selection: true, }; let socket_name = server.socket_name().to_owned(); server.run(state); let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap(); assert!(result); } wl-clipboard-rs-0.9.2/src/utils.rs000064400000000000000000000170131046102023000151340ustar 00000000000000//! Helper functions. use std::ffi::OsString; use std::os::unix::net::UnixStream; use std::path::PathBuf; use std::{env, io}; use wayland_client::protocol::wl_registry::{self, WlRegistry}; use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::{ event_created_child, ConnectError, Connection, Dispatch, DispatchError, Proxy, }; use wayland_protocols::ext::data_control::v1::client::ext_data_control_manager_v1::ExtDataControlManagerV1; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; use crate::data_control::{ impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer, Manager, }; /// Checks if the given MIME type represents plain text. /// /// # Examples /// /// ``` /// use wl_clipboard_rs::utils::is_text; /// /// assert!(is_text("text/plain")); /// assert!(!is_text("application/octet-stream")); /// ``` pub fn is_text(mime_type: &str) -> bool { match mime_type { "TEXT" | "STRING" | "UTF8_STRING" => true, x if x.starts_with("text/") => true, _ => false, } } struct PrimarySelectionState { // Any seat that we get from the compositor. seat: Option, clipboard_manager: Option, saw_zwlr_v1: bool, got_primary_selection: bool, } impl Dispatch for PrimarySelectionState { fn event( state: &mut Self, registry: &WlRegistry, event: ::Event, _data: &(), _conn: &Connection, qh: &wayland_client::QueueHandle, ) { if let wl_registry::Event::Global { name, interface, version, } = event { if interface == WlSeat::interface().name && version >= 2 && state.seat.is_none() { let seat = registry.bind(name, 2, qh, ()); state.seat = Some(seat); } if state.clipboard_manager.is_none() { if interface == ZwlrDataControlManagerV1::interface().name { if version == 1 { state.saw_zwlr_v1 = true; } else { let manager = registry.bind(name, 2, qh, ()); state.clipboard_manager = Some(Manager::Zwlr(manager)); } } if interface == ExtDataControlManagerV1::interface().name { let manager = registry.bind(name, 1, qh, ()); state.clipboard_manager = Some(Manager::Ext(manager)); } } } } } impl Dispatch for PrimarySelectionState { fn event( _state: &mut Self, _proxy: &WlSeat, _event: ::Event, _data: &(), _conn: &Connection, _qhandle: &wayland_client::QueueHandle, ) { } } impl_dispatch_manager!(PrimarySelectionState); impl_dispatch_device!(PrimarySelectionState, (), |state: &mut Self, event, _| { if let Event::PrimarySelection { id: _ } = event { state.got_primary_selection = true; } }); impl_dispatch_offer!(PrimarySelectionState); /// Errors that can occur when checking whether the primary selection is supported. #[derive(thiserror::Error, Debug)] pub enum PrimarySelectionCheckError { #[error("There are no seats")] NoSeats, #[error("Couldn't open the provided Wayland socket")] SocketOpenError(#[source] io::Error), #[error("Couldn't connect to the Wayland compositor")] WaylandConnection(#[source] ConnectError), #[error("Wayland compositor communication error")] WaylandCommunication(#[source] DispatchError), #[error( "A required Wayland protocol (ext-data-control, or wlr-data-control version 1) \ is not supported by the compositor" )] MissingProtocol, } /// Checks if the compositor supports the primary selection. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # fn foo() -> Result<(), Box> { /// use wl_clipboard_rs::utils::{is_primary_selection_supported, PrimarySelectionCheckError}; /// /// match is_primary_selection_supported() { /// Ok(supported) => { /// // We have our definitive result. False means that ext/wlr-data-control is present /// // and did not signal the primary selection support, or that only wlr-data-control /// // version 1 is present (which does not support primary selection). /// }, /// Err(PrimarySelectionCheckError::NoSeats) => { /// // Impossible to give a definitive result. Primary selection may or may not be /// // supported. /// /// // The required protocol (ext-data-control, or wlr-data-control version 2) is there, /// // but there are no seats. Unfortunately, at least one seat is needed to check for the /// // primary clipboard support. /// }, /// Err(PrimarySelectionCheckError::MissingProtocol) => { /// // The data-control protocol (required for wl-clipboard-rs operation) is not /// // supported by the compositor. /// }, /// Err(_) => { /// // Some communication error occurred. /// } /// } /// # Ok(()) /// # } /// ``` #[inline] pub fn is_primary_selection_supported() -> Result { is_primary_selection_supported_internal(None) } pub(crate) fn is_primary_selection_supported_internal( socket_name: Option, ) -> Result { // Connect to the Wayland compositor. let conn = match socket_name { Some(name) => { let mut socket_path = env::var_os("XDG_RUNTIME_DIR") .map(Into::::into) .ok_or(ConnectError::NoCompositor) .map_err(PrimarySelectionCheckError::WaylandConnection)?; if !socket_path.is_absolute() { return Err(PrimarySelectionCheckError::WaylandConnection( ConnectError::NoCompositor, )); } socket_path.push(name); let stream = UnixStream::connect(socket_path) .map_err(PrimarySelectionCheckError::SocketOpenError)?; Connection::from_socket(stream) } None => Connection::connect_to_env(), } .map_err(PrimarySelectionCheckError::WaylandConnection)?; let display = conn.display(); let mut queue = conn.new_event_queue(); let qh = queue.handle(); let mut state = PrimarySelectionState { seat: None, clipboard_manager: None, saw_zwlr_v1: false, got_primary_selection: false, }; // Retrieve the global interfaces. let _registry = display.get_registry(&qh, ()); queue .roundtrip(&mut state) .map_err(PrimarySelectionCheckError::WaylandCommunication)?; // If data control is present but is version 1, then return false as version 1 does not support // primary clipboard. if state.clipboard_manager.is_none() && state.saw_zwlr_v1 { return Ok(false); } // Verify that we got the clipboard manager. let Some(ref clipboard_manager) = state.clipboard_manager else { return Err(PrimarySelectionCheckError::MissingProtocol); }; // Check if there are no seats. let Some(ref seat) = state.seat else { return Err(PrimarySelectionCheckError::NoSeats); }; clipboard_manager.get_data_device(seat, &qh, ()); queue .roundtrip(&mut state) .map_err(PrimarySelectionCheckError::WaylandCommunication)?; Ok(state.got_primary_selection) }