Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:Factory:Rebuild
wl-clipboard-rs
wl-clipboard-rs-0.8.1.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File wl-clipboard-rs-0.8.1.obscpio of Package wl-clipboard-rs
07070100000000000041ED00000000000000000000000265E9594700000000000000000000000000000000000000000000001E00000000wl-clipboard-rs-0.8.1/.github07070100000001000041ED00000000000000000000000265E9594700000000000000000000000000000000000000000000002800000000wl-clipboard-rs-0.8.1/.github/workflows07070100000002000081A400000000000000000000000165E95947000008E9000000000000000000000000000000000000002F00000000wl-clipboard-rs-0.8.1/.github/workflows/ci.ymlname: 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-22.04 steps: - uses: actions/checkout@v4 with: show-progress: false - name: Install Rust run: | rustup set auto-self-update check-only rustup toolchain install ${{ matrix.rust }} --profile minimal - 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@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/doc clippy: name: clippy runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 with: show-progress: false - name: Install Rust run: | rustup set auto-self-update check-only rustup toolchain install stable --profile minimal --component clippy - uses: Swatinem/rust-cache@v2 - name: Run clippy run: cargo clippy --all --all-targets rustfmt: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 with: show-progress: false - name: Install Rust run: | rustup set auto-self-update check-only rustup toolchain install nightly --profile minimal --component rustfmt rustup override set nightly - name: Run rustfmt run: cargo fmt --all -- --check 07070100000003000081A400000000000000000000000165E9594700000013000000000000000000000000000000000000002100000000wl-clipboard-rs-0.8.1/.gitignore/target **/*.rs.bk 07070100000004000081A400000000000000000000000165E9594700000E43000000000000000000000000000000000000002300000000wl-clipboard-rs-0.8.1/CHANGELOG.md# Changelog ## Unreleased ## 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. 07070100000005000081A400000000000000000000000165E9594700007125000000000000000000000000000000000000002100000000wl-clipboard-rs-0.8.1/Cargo.lock# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ "winapi", ] [[package]] name = "anyhow" version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", "winapi", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "bumpalo" version = "3.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" [[package]] name = "bytecount" version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" [[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "chrono" version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-targets", ] [[package]] name = "clap" version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", "bitflags 1.3.2", "strsim", "term_size", "textwrap", "unicode-width", "vec_map", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "derive-new" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", "syn 2.0.52", ] [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys", ] [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[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.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" dependencies = [ "unicode-segmentation", ] [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "indexmap" version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "io-lifetimes" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" [[package]] name = "is-terminal" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ "hermit-abi 0.3.9", "libc", "windows-sys", ] [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", "windows-targets", ] [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" dependencies = [ "mime", "unicase", ] [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nix" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.4.2", "cfg-if", "cfg_aliases", "libc", ] [[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.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", "libm", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "os_pipe" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" dependencies = [ "libc", "windows-sys", ] [[package]] name = "petgraph" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", "indexmap", ] [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", "syn 1.0.109", "version_check", ] [[package]] name = "proc-macro-error-attr" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", "version_check", ] [[package]] name = "proc-macro2" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", "bit-vec", "bitflags 2.4.2", "lazy_static", "num-traits", "rand", "rand_chacha", "rand_xorshift", "regex-syntax", "rusty-fork", "tempfile", "unarray", ] [[package]] name = "proptest-derive" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf16337405ca084e9c78985114633b6827711d22b9e6ef6c6c0d665eb3f0b6e" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[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.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ "memchr", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 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", ] [[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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rustix" version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "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 = "smallvec" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "stderrlog" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c910772f992ab17d32d6760e167d2353f4130ed50e796752689556af07dc6b" dependencies = [ "chrono", "is-terminal", "log", "termcolor", "thread_local", ] [[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" dependencies = [ "clap", "lazy_static", "structopt-derive", ] [[package]] name = "structopt-derive" version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ "heck", "proc-macro-error", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys", ] [[package]] name = "term_size" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" dependencies = [ "libc", "winapi", ] [[package]] name = "termcolor" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ "term_size", "unicode-width", ] [[package]] name = "thiserror" version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", "syn 2.0.52", ] [[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "tree_magic_mini" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91adfd0607cacf6e4babdb870e9bec4037c1c4b151cfd279ccefc5e0c7feaa6d" dependencies = [ "bytecount", "fnv", "lazy_static", "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 = "unicase" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-segmentation" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 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 = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 2.0.52", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wayland-backend" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" dependencies = [ "cc", "downcast-rs", "rustix", "scoped-tls", "smallvec", "wayland-sys", ] [[package]] name = "wayland-client" version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" dependencies = [ "bitflags 2.4.2", "rustix", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-protocols" version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ "bitflags 2.4.2", "wayland-backend", "wayland-client", "wayland-scanner", "wayland-server", ] [[package]] name = "wayland-protocols-wlr" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ "bitflags 2.4.2", "wayland-backend", "wayland-client", "wayland-protocols", "wayland-scanner", "wayland-server", ] [[package]] name = "wayland-scanner" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" dependencies = [ "proc-macro2", "quick-xml", "quote", ] [[package]] name = "wayland-server" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00e6e4d5c285bc24ba4ed2d5a4bd4febd5fd904451f465973225c8e99772fdb7" dependencies = [ "bitflags 2.4.2", "downcast-rs", "io-lifetimes", "rustix", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-sys" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" dependencies = [ "dlib", "libc", "log", "memoffset", "once_cell", "pkg-config", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "wl-clipboard-rs" version = "0.8.1" dependencies = [ "derive-new", "libc", "log", "nix", "os_pipe", "proptest", "proptest-derive", "tempfile", "thiserror", "tree_magic_mini", "wayland-backend", "wayland-client", "wayland-protocols", "wayland-protocols-wlr", "wayland-server", ] [[package]] name = "wl-clipboard-rs-tools" version = "0.8.1" dependencies = [ "anyhow", "libc", "log", "mime_guess", "nix", "stderrlog", "structopt", "wl-clipboard-rs", ] 07070100000006000081A400000000000000000000000165E95947000005AC000000000000000000000000000000000000002100000000wl-clipboard-rs-0.8.1/Cargo.toml[package] name = "wl-clipboard-rs" version = "0.8.1" # remember to update html_root_url authors = ["Ivan Molodetskikh <yalterz@gmail.com>"] description = "Access to the Wayland clipboard for terminal and other window-less applications." edition = "2021" license = "MIT/Apache-2.0" readme = "README.md" documentation = "https://docs.rs/wl-clipboard-rs" repository = "https://github.com/YaLTeR/wl-clipboard-rs" keywords = ["wayland", "clipboard"] categories = ["command-line-utilities"] [workspace] members = [".", "wl-clipboard-rs-tools"] [dependencies] derive-new = "0.6.0" libc = "0.2.153" log = "0.4.21" nix = { version = "0.28.0", features = ["fs", "process", "event"] } os_pipe = { version = "1.1.5", features = ["io_safety"] } tempfile = "3.10.1" thiserror = "1" tree_magic_mini = "3.0.3" wayland-backend = "0.3.3" wayland-client = "0.31.2" wayland-protocols = { version = "0.31.2", features = ["client"] } wayland-protocols-wlr = { version = "0.2.0", features = ["client"] } [dev-dependencies] wayland-server = "0.31.1" wayland-protocols = { version = "0.31.2", features = ["server"] } wayland-protocols-wlr = { version = "0.2.0", features = ["server"] } proptest = "1.4.0" proptest-derive = "0.4.0" [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"] 07070100000007000081A400000000000000000000000165E9594700002A5F000000000000000000000000000000000000002500000000wl-clipboard-rs-0.8.1/LICENSE-APACHE 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. 07070100000008000081A400000000000000000000000165E9594700000425000000000000000000000000000000000000002200000000wl-clipboard-rs-0.8.1/LICENSE-MITCopyright (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. 07070100000009000081A400000000000000000000000165E9594700001248000000000000000000000000000000000000002000000000wl-clipboard-rs-0.8.1/README.md# 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 `data-control` from [wlroots](https://github.com/swaywm/wlr-protocols). When using the regular clipboard, the compositor must support the first version of the protocol. When using the "primary" clipboard, the compositor must support the second version of the protocol (or higher). 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 either data-control version 1 // is present (which does not support the primary selection), or that data-control // version 2 is present and it did not signal the primary selection support. }, Err(PrimarySelectionCheckError::NoSeats) => { // Impossible to give a definitive result. Primary selection may or may not be // supported. // The required protocol (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 0707010000000A000081A400000000000000000000000165E959470000026B000000000000000000000000000000000000002100000000wl-clipboard-rs-0.8.1/README.tpl# {{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}} 0707010000000B000041ED00000000000000000000000265E9594700000000000000000000000000000000000000000000001A00000000wl-clipboard-rs-0.8.1/doc0707010000000C000081A400000000000000000000000165E959470000015D000000000000000000000000000000000000002500000000wl-clipboard-rs-0.8.1/doc/index.html<!DOCTYPE HTML> <html lang="en-US"> <head> <meta charset="UTF-8"> <meta http-equiv="refresh" content="0; url=wl_clipboard_rs/index.html"> <title>Page Redirection</title> </head> <body> If you are not redirected automatically, follow this <a href='wl_clipboard_rs/index.html'>link</a>. </body> </html> 0707010000000D000081A400000000000000000000000165E9594700000042000000000000000000000000000000000000002300000000wl-clipboard-rs-0.8.1/rustfmt.tomlimports_granularity = "Module" group_imports = "StdExternalCrate" 0707010000000E000041ED00000000000000000000000265E9594700000000000000000000000000000000000000000000001A00000000wl-clipboard-rs-0.8.1/src0707010000000F000081A400000000000000000000000165E95947000011A7000000000000000000000000000000000000002400000000wl-clipboard-rs-0.8.1/src/common.rsuse 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, BindError, 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_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; use crate::seat_data::SeatData; pub struct State { pub seats: HashMap<WlSeat, SeatData>, pub clipboard_manager: ZwlrDataControlManagerV1, } #[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<S> Dispatch<WlSeat, (), S> for State where S: Dispatch<WlSeat, ()> + AsMut<State>, { fn event( parent: &mut S, seat: &WlSeat, event: <WlSeat as wayland_client::Proxy>::Event, _data: &(), _conn: &wayland_client::Connection, _qh: &wayland_client::QueueHandle<S>, ) { 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<S>( primary: bool, socket_name: Option<OsString>, ) -> Result<(EventQueue<S>, State), Error> where S: Dispatch<WlRegistry, GlobalListContents> + 'static, S: Dispatch<ZwlrDataControlManagerV1, ()>, S: Dispatch<WlSeat, ()>, S: AsMut<State>, { // Connect to the Wayland compositor. let conn = match socket_name { Some(name) => { let mut socket_path = env::var_os("XDG_RUNTIME_DIR") .map(Into::<PathBuf>::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::<S>(&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(); let data_control_version = if primary { 2 } else { 1 }; // Verify that we got the clipboard manager. let clipboard_manager = match globals.bind(qh, data_control_version..=data_control_version, ()) { Ok(manager) => manager, Err(BindError::NotPresent | BindError::UnsupportedVersion) => { return Err(Error::MissingProtocol { name: ZwlrDataControlManagerV1::interface().name, version: data_control_version, }) } }; 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)) } 07070100000010000081A400000000000000000000000165E9594700008EB1000000000000000000000000000000000000002200000000wl-clipboard-rs-0.8.1/src/copy.rs//! 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::os::unix::io::IntoRawFd; use std::path::PathBuf; use std::sync::mpsc::sync_channel; use std::{iter, thread}; use log::trace; 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, Proxy, }; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::{ self, ZwlrDataControlDeviceV1, }; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{ self, ZwlrDataControlSourceV1, }; use crate::common::{self, initialize}; use crate::seat_data::SeatData; use crate::utils::{self, copy_data, 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 the data-control /// protocol of 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: State, sources: Vec<ZwlrDataControlSourceV1>, } /// 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] utils::CopyDataError), #[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<common::Error> 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] utils::CopyDataError), } 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<String, PathBuf>, serve_requests: ServeRequests, // An error that occurred while serving a request, if any. error: Option<DataSourceError>, } delegate_dispatch!(State: [WlSeat: ()] => common::State); impl AsMut<common::State> for State { fn as_mut(&mut self) -> &mut common::State { &mut self.common } } impl Dispatch<WlRegistry, GlobalListContents> for State { fn event( _state: &mut Self, _proxy: &WlRegistry, _event: <WlRegistry as wayland_client::Proxy>::Event, _data: &GlobalListContents, _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle<Self>, ) { } } impl Dispatch<ZwlrDataControlManagerV1, ()> for State { fn event( _state: &mut Self, _proxy: &ZwlrDataControlManagerV1, _event: <ZwlrDataControlManagerV1 as wayland_client::Proxy>::Event, _data: &(), _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle<Self>, ) { } } impl Dispatch<ZwlrDataControlDeviceV1, WlSeat> for State { fn event( state: &mut Self, _device: &ZwlrDataControlDeviceV1, event: <ZwlrDataControlDeviceV1 as Proxy>::Event, seat: &WlSeat, _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle<Self>, ) { match event { zwlr_data_control_device_v1::Event::DataOffer { id } => id.destroy(), zwlr_data_control_device_v1::Event::Finished => { state.common.seats.get_mut(seat).unwrap().set_device(None); } zwlr_data_control_device_v1::Event::PrimarySelection { .. } => { state.got_primary_selection = true; } _ => (), } } event_created_child!(State, ZwlrDataControlDeviceV1, [ zwlr_data_control_device_v1::EVT_DATA_OFFER_OPCODE => (ZwlrDataControlOfferV1, ()), ]); } impl Dispatch<ZwlrDataControlOfferV1, ()> for State { fn event( _state: &mut Self, _offer: &ZwlrDataControlOfferV1, _event: <ZwlrDataControlOfferV1 as wayland_client::Proxy>::Event, _data: &(), _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle<Self>, ) { } } impl Dispatch<ZwlrDataControlSourceV1, ()> for State { fn event( state: &mut Self, source: &ZwlrDataControlSourceV1, event: <ZwlrDataControlSourceV1 as Proxy>::Event, _data: &(), _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle<Self>, ) { match event { zwlr_data_control_source_v1::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(|data_file| { let data_fd = data_file.into_raw_fd(); copy_data(Some(data_fd), fd.into_raw_fd(), true).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(); } } zwlr_data_control_source_v1::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<MimeSource>) -> 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<PreparedCopy, Error> { 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<MimeSource>) -> Result<PreparedCopy, Error> { 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. copy_data(None, temp_file.into_raw_fd(), true).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<OsString>, ) -> Result<(EventQueue<State>, State, Vec<ZwlrDataControlDeviceV1>), 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::<Vec<_>>(); // 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<OsString>, ) -> 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<PreparedCopy, Error> { 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<MimeSource>, ) -> Result<PreparedCopy, Error> { assert!(options.foreground); prepare_copy_internal(options, sources, None) } fn prepare_copy_internal( options: Options, sources: Vec<MimeSource>, socket_name: Option<OsString>, ) -> Result<PreparedCopy, Error> { 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::<Vec<_>>(); 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<MimeSource>) -> Result<(), Error> { copy_internal(options, sources, None) } pub(crate) fn copy_internal( options: Options, sources: Vec<MimeSource>, socket_name: Option<OsString>, ) -> 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(()) } } 07070100000011000081A400000000000000000000000165E95947000013A5000000000000000000000000000000000000002100000000wl-clipboard-rs-0.8.1/src/lib.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 `data-control` from //! [wlroots](https://github.com/swaywm/wlr-protocols). When using the regular clipboard, the //! compositor must support the first version of the protocol. When using the "primary" clipboard, //! the compositor must support the second version of the protocol (or higher). //! //! 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<dyn std::error::Error>> { //! 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<dyn std::error::Error>> { //! 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<dyn std::error::Error>> { //! 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 either data-control version 1 //! // is present (which does not support the primary selection), or that data-control //! // version 2 is present and it did not signal the primary selection support. //! }, //! Err(PrimarySelectionCheckError::NoSeats) => { //! // Impossible to give a definitive result. Primary selection may or may not be //! // supported. //! //! // The required protocol (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.8.1")] #![deny(unsafe_code)] mod common; 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; 07070100000012000081A400000000000000000000000165E9594700003762000000000000000000000000000000000000002300000000wl-clipboard-rs-0.8.1/src/paste.rs//! 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 wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::{ self, ZwlrDataControlDeviceV1, }; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{ self, ZwlrDataControlOfferV1, }; use crate::common::{self, initialize}; 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 the data-control /// protocol of 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<ZwlrDataControlOfferV1, HashSet<String>>, got_primary_selection: bool, } delegate_dispatch!(State: [WlSeat: ()] => common::State); impl AsMut<common::State> 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<common::Error> 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<WlRegistry, GlobalListContents> for State { fn event( _state: &mut Self, _proxy: &WlRegistry, _event: <WlRegistry as wayland_client::Proxy>::Event, _data: &GlobalListContents, _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle<Self>, ) { } } impl Dispatch<ZwlrDataControlManagerV1, ()> for State { fn event( _state: &mut Self, _proxy: &ZwlrDataControlManagerV1, _event: <ZwlrDataControlManagerV1 as wayland_client::Proxy>::Event, _data: &(), _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle<Self>, ) { } } impl Dispatch<ZwlrDataControlDeviceV1, WlSeat> for State { fn event( state: &mut Self, _device: &ZwlrDataControlDeviceV1, event: <ZwlrDataControlDeviceV1 as wayland_client::Proxy>::Event, seat: &WlSeat, _conn: &wayland_client::Connection, _qh: &wayland_client::QueueHandle<Self>, ) { match event { zwlr_data_control_device_v1::Event::DataOffer { id } => { state.offers.insert(id, HashSet::new()); } zwlr_data_control_device_v1::Event::Selection { id } => { state.common.seats.get_mut(seat).unwrap().set_offer(id); } zwlr_data_control_device_v1::Event::Finished => { // Destroy the device stored in the seat as it's no longer valid. state.common.seats.get_mut(seat).unwrap().set_device(None); } zwlr_data_control_device_v1::Event::PrimarySelection { id } => { state.got_primary_selection = true; state .common .seats .get_mut(seat) .unwrap() .set_primary_offer(id); } _ => (), } } event_created_child!(State, ZwlrDataControlDeviceV1, [ zwlr_data_control_device_v1::EVT_DATA_OFFER_OPCODE => (ZwlrDataControlOfferV1, ()), ]); } impl Dispatch<ZwlrDataControlOfferV1, ()> for State { fn event( state: &mut Self, offer: &ZwlrDataControlOfferV1, event: <ZwlrDataControlOfferV1 as wayland_client::Proxy>::Event, _data: &(), _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle<Self>, ) { if let zwlr_data_control_offer_v1::Event::Offer { mime_type } = event { state.offers.get_mut(offer).unwrap().insert(mime_type); } } } fn get_offer( primary: bool, seat: Seat<'_>, socket_name: Option<OsString>, ) -> Result<(EventQueue<State>, State, ZwlrDataControlOfferV1), 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<HashSet<String>, 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<OsString>, ) -> Result<HashSet<String>, 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<dyn std::error::Error>> { /// 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<OsString>, ) -> 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)) } 07070100000013000081A400000000000000000000000165E95947000006F0000000000000000000000000000000000000002700000000wl-clipboard-rs-0.8.1/src/seat_data.rsuse wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1; #[derive(Default)] pub struct SeatData { /// The name of this seat, if any. pub name: Option<String>, /// The data device of this seat, if any. pub device: Option<ZwlrDataControlDeviceV1>, /// The data offer of this seat, if any. pub offer: Option<ZwlrDataControlOfferV1>, /// The primary-selection data offer of this seat, if any. pub primary_offer: Option<ZwlrDataControlOfferV1>, } 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<ZwlrDataControlDeviceV1>) { 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<ZwlrDataControlOfferV1>) { 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<ZwlrDataControlOfferV1>) { let old_offer = self.primary_offer.take(); self.primary_offer = new_offer; if let Some(offer) = old_offer { offer.destroy(); } } } 07070100000014000041ED00000000000000000000000265E9594700000000000000000000000000000000000000000000002000000000wl-clipboard-rs-0.8.1/src/tests07070100000015000081A400000000000000000000000165E959470000314D000000000000000000000000000000000000002800000000wl-clipboard-rs-0.8.1/src/tests/copy.rsuse 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::<State, ZwlrDataControlManagerV1, ()>(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::<State, ZwlrDataControlManagerV1, ()>(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::<State, ZwlrDataControlManagerV1, ()>(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::<State, ZwlrDataControlManagerV1, ()>(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..70000 { bytes_to_copy.push((i % 256) as u8); } let server = TestServer::new(); server .display .handle() .create_global::<State, ZwlrDataControlManagerV1, ()>(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::<State, ZwlrDataControlManagerV1, ()>(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(); } } 07070100000016000081A400000000000000000000000165E9594700001181000000000000000000000000000000000000002700000000wl-clipboard-rs-0.8.1/src/tests/mod.rsuse std::ffi::OsStr; use std::sync::atomic::AtomicU8; use std::sync::atomic::Ordering::SeqCst; use std::sync::{Arc, Mutex}; use std::thread; use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags, EpollTimeout}; use wayland_backend::server::ClientData; use wayland_server::{Display, ListeningSocket}; mod copy; mod paste; mod state; mod utils; pub struct TestServer<S: 'static> { pub display: Display<S>, pub socket: ListeningSocket, pub epoll: Epoll, } 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<S: Send + 'static> TestServer<S> { pub fn new() -> Self { let mut display = Display::new().unwrap(); let socket = ListeningSocket::bind_auto("wl-clipboard-rs-test", 0..).unwrap(); let epoll = Epoll::new(EpollCreateFlags::EPOLL_CLOEXEC).unwrap(); epoll .add(&socket, EpollEvent::new(EpollFlags::EPOLLIN, 0)) .unwrap(); epoll .add( display.backend().poll_fd(), EpollEvent::new(EpollFlags::EPOLLIN, 1), ) .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<Mutex<S>>) { 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 = [EpollEvent::empty(); 2]; let nevents = self.epoll.wait(&mut events, EpollTimeout::NONE).unwrap(); let ready_socket = events.iter().take(nevents).any(|event| event.data() == 0); let ready_clients = events.iter().take(nevents).any(|event| event.data() == 1); if ready_socket { // 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(); } } if ready_clients { // Try to dispatch client messages. self.display.dispatch_clients(state).unwrap(); self.display.flush_clients().unwrap(); } } } } // 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, ()); } } )* } } 07070100000017000081A400000000000000000000000165E9594700003097000000000000000000000000000000000000002900000000wl-clipboard-rs-0.8.1/src/tests/paste.rsuse 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::<State, ZwlrDataControlManagerV1, ()>(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: "zwlr_data_control_manager_v1", 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: "zwlr_data_control_manager_v1", version: 2 }) )); } #[test] fn get_mime_types_no_seats() { let server = TestServer::new(); server .display .handle() .create_global::<State, ZwlrDataControlManagerV1, ()>(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::<State, ZwlrDataControlManagerV1, ()>(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::<State, ZwlrDataControlManagerV1, ()>(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::<State, ZwlrDataControlManagerV1, ()>(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::<State, ZwlrDataControlManagerV1, ()>(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::<State, ZwlrDataControlManagerV1, ()>(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::<State, ZwlrDataControlManagerV1, ()>(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::<State, ZwlrDataControlManagerV1, ()>(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]); } }, } } } } 07070100000018000081A400000000000000000000000165E95947000027C8000000000000000000000000000000000000002900000000wl-clipboard-rs-0.8.1/src/tests/state.rs//! 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, AsRawFd}; use std::sync::atomic::AtomicU8; use std::sync::atomic::Ordering::SeqCst; use std::sync::mpsc::Sender; use nix::fcntl::{fcntl, FcntlArg, OFlag}; use os_pipe::PipeWriter; use proptest::prelude::*; use proptest_derive::Arbitrary; 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::<String>(), prop::collection::vec(any::<u8>(), 0..5), 0..5)" )] data: HashMap<String, Vec<u8>>, }, #[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<String> { match self { OfferInfo::Buffered { data } => data.keys().cloned().collect(), OfferInfo::Runtime { source } => state.sources[source].clone(), } } pub fn data(&self) -> &HashMap<String, Vec<u8>> { match self { OfferInfo::Buffered { data } => data, OfferInfo::Runtime { .. } => panic!(), } } } #[derive(Debug, Clone, Default, Arbitrary)] pub struct SeatInfo { pub offer: Option<OfferInfo>, pub primary_offer: Option<OfferInfo>, } #[derive(Debug, Clone, Default, Arbitrary)] pub struct State { #[proptest(strategy = "prop::collection::hash_map(any::<String>(), any::<SeatInfo>(), 0..5)")] pub seats: HashMap<String, SeatInfo>, #[proptest(value = "HashMap::new()")] pub sources: HashMap<ZwlrDataControlSourceV1, Vec<String>>, #[proptest(value = "None")] pub selection_updated_sender: Option<Sender<Option<Vec<String>>>>, pub set_nonblock_on_write_fd: bool, } server_ignore_global_impl!(State => [ZwlrDataControlManagerV1]); impl State { pub fn create_seats(&self, server: &TestServer<Self>) { for name in self.seats.keys() { server .display .handle() .create_global::<Self, WlSeat, _>(6, name.clone()); } } } impl GlobalDispatch<WlSeat, String> for State { fn bind( _state: &mut Self, _handle: &wayland_server::DisplayHandle, _client: &wayland_server::Client, resource: wayland_server::New<WlSeat>, name: &String, data_init: &mut wayland_server::DataInit<'_, Self>, ) { let seat = data_init.init(resource, name.clone()); seat.name((*name).to_owned()); } } impl Dispatch<WlSeat, String> for State { fn request( _state: &mut Self, _client: &wayland_server::Client, _seat: &WlSeat, _request: <WlSeat as wayland_server::Resource>::Request, _name: &String, _dhandle: &wayland_server::DisplayHandle, _data_init: &mut wayland_server::DataInit<'_, Self>, ) { } } impl Dispatch<ZwlrDataControlManagerV1, ()> for State { fn request( state: &mut Self, client: &wayland_server::Client, manager: &ZwlrDataControlManagerV1, request: <ZwlrDataControlManagerV1 as wayland_server::Resource>::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<ZwlrDataControlDeviceV1, String> for State { fn request( state: &mut Self, _client: &wayland_server::Client, _resource: &ZwlrDataControlDeviceV1, request: <ZwlrDataControlDeviceV1 as Resource>::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::<AtomicU8>().unwrap().fetch_add(1, SeqCst); } if let Some(OfferInfo::Runtime { source }) = &info.offer { if source.data::<AtomicU8>().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::<AtomicU8>().unwrap().fetch_add(1, SeqCst); } if let Some(OfferInfo::Runtime { source }) = &info.primary_offer { if source.data::<AtomicU8>().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<ZwlrDataControlOfferV1, (String, bool)> for State { fn request( state: &mut Self, _client: &wayland_server::Client, _resource: &ZwlrDataControlOfferV1, request: <ZwlrDataControlOfferV1 as Resource>::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(fd.as_raw_fd(), FcntlArg::F_SETFL(OFlag::O_NONBLOCK)).unwrap(); } source.send(mime_type, fd.as_fd()) } } } } } impl Dispatch<ZwlrDataControlSourceV1, AtomicU8> for State { fn request( state: &mut Self, _client: &wayland_server::Client, source: &ZwlrDataControlSourceV1, request: <ZwlrDataControlSourceV1 as Resource>::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); } } } 07070100000019000081A400000000000000000000000165E9594700001276000000000000000000000000000000000000002900000000wl-clipboard-rs-0.8.1/src/tests/utils.rsuse 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]); server_ignore_impl!(State => [WlSeat, ZwlrDataControlDeviceV1]); impl Dispatch<ZwlrDataControlManagerV1, ()> for State { fn request( state: &mut Self, _client: &wayland_server::Client, _resource: &ZwlrDataControlManagerV1, request: <ZwlrDataControlManagerV1 as wayland_server::Resource>::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); } } } } #[test] fn is_primary_selection_supported_test() { let server = TestServer::new(); server .display .handle() .create_global::<State, WlSeat, ()>(6, ()); server .display .handle() .create_global::<State, ZwlrDataControlManagerV1, ()>(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::<State, WlSeat, ()>(6, ()); server .display .handle() .create_global::<State, ZwlrDataControlManagerV1, ()>(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::<State, WlSeat, ()>(6, ()); server .display .handle() .create_global::<State, ZwlrDataControlManagerV1, ()>(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::<State, ZwlrDataControlManagerV1, ()>(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::<State, WlSeat, ()>(2, ()); server .display .handle() .create_global::<State, ZwlrDataControlManagerV1, ()>(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::<State, WlSeat, ()>(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 { name: "zwlr_data_control_manager_v1", version: 1 }) )); } 0707010000001A000081A400000000000000000000000165E95947000034C2000000000000000000000000000000000000002300000000wl-clipboard-rs-0.8.1/src/utils.rs//! Helper functions. use std::ffi::{CString, OsString}; use std::os::unix::io::RawFd; use std::os::unix::net::UnixStream; use std::path::PathBuf; use std::process::abort; use std::{env, io}; use libc::{STDIN_FILENO, STDOUT_FILENO}; use nix::fcntl::{fcntl, FcntlArg, OFlag}; use nix::sys::wait::{waitpid, WaitStatus}; use nix::unistd::{close, dup2, execv, fork, ForkResult}; 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_wlr::data_control::v1::client::zwlr_data_control_device_v1::{ self, ZwlrDataControlDeviceV1, }; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1; /// 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, } } /// Errors that can occur in `copy_data()`. #[derive(thiserror::Error, Debug)] pub enum CopyDataError { #[error("Couldn't set the source file descriptor flags")] SetSourceFdFlags(#[source] nix::Error), #[error("Couldn't set the target file descriptor flags")] SetTargetFdFlags(#[source] nix::Error), #[error("Couldn't fork")] Fork(#[source] nix::Error), #[error("Couldn't close the source file descriptor")] CloseSourceFd(#[source] nix::Error), #[error("Couldn't close the target file descriptor")] CloseTargetFd(#[source] nix::Error), #[error("Couldn't wait for the child process")] Wait(#[source] nix::Error), #[error( "Received an unexpected status when waiting for the child process: {:?}", _0 )] WaitUnexpected(WaitStatus), #[error("The child process exited with a non-zero error code: {}", _0)] ChildError(i32), } /// Copies data from one file to another. /// /// This function assumes ownership of the passed file descriptors. That is, it closes them by /// itself. Use `into_raw_fd()`. /// /// If `from_fd` is `None`, the standard input is used as the data source. /// /// If `wait` is `true`, this function returns after all data has been copied, otherwise it may /// return before all data has been copied. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # fn foo() -> Result<(), Box<dyn std::error::Error>> { /// use std::{fs::File, os::unix::io::IntoRawFd}; /// use wl_clipboard_rs::utils::copy_data; /// /// let file = File::create("stdin-contents")?; /// /// // Copy the standard input into the file. /// copy_data(None, file.into_raw_fd(), true)?; /// # Ok(()) /// # } /// ``` #[allow(unsafe_code)] pub fn copy_data(from_fd: Option<RawFd>, to_fd: RawFd, wait: bool) -> Result<(), CopyDataError> { // We use the cat utility for data copying. It's easier (no need to implement any complex // buffering logic), surprisingly safer (a Rust implementation would likely require usage of // `from_raw_fd()` which is unsafe) and ideally faster (cat's been around for a while and is // probably pretty optimized). // Clear O_NONBLOCK because cat doesn't know how to deal with it. if let Some(from_fd) = from_fd { fcntl(from_fd, FcntlArg::F_SETFL(OFlag::empty())) .map_err(CopyDataError::SetSourceFdFlags)?; } fcntl(to_fd, FcntlArg::F_SETFL(OFlag::empty())).map_err(CopyDataError::SetTargetFdFlags)?; // Don't allocate memory in the child process, it's not async-signal-safe. let bin_env = CString::new("/usr/bin/env").unwrap(); let env = CString::new("env").unwrap(); let cat = CString::new("cat").unwrap(); // Fork and exec cat. // SAFETY: Within the child, we are only using the following system calls: dup2, close, execv // As required by the safety of `fork`, these are all [async-signal-safe](https://man7.org/linux/man-pages/man7/signal-safety.7.html). let fork_result = unsafe { fork() }.map_err(CopyDataError::Fork)?; match fork_result { ForkResult::Child => { if let Some(fd) = from_fd { // Redirect the "from" fd to stdin. if dup2(fd, STDIN_FILENO).is_err() { abort(); } } // Redirect stdout to the "to" fd. if dup2(to_fd, STDOUT_FILENO).is_err() { abort(); } // Close the original fds. if let Some(fd) = from_fd { if close(fd).is_err() { abort(); } } if close(to_fd).is_err() { abort(); } // Exec cat. if execv(&bin_env, &[&env, &cat]).is_err() { abort(); } } ForkResult::Parent { child } => { // Close the fds in the parent process. if let Some(fd) = from_fd { close(fd).map_err(CopyDataError::CloseSourceFd)?; } close(to_fd).map_err(CopyDataError::CloseTargetFd)?; if wait { // Wait for the child process to exit. match waitpid(child, None).map_err(CopyDataError::Wait)? { WaitStatus::Exited(_, status) => { if status != 0 { return Err(CopyDataError::ChildError(status)); } } x => return Err(CopyDataError::WaitUnexpected(x)), } } } } Ok(()) } struct PrimarySelectionState { // Any seat that we get from the compositor. seat: Option<WlSeat>, clipboard_manager: Option<ZwlrDataControlManagerV1>, clipboard_manager_was_v1: bool, got_primary_selection: bool, } impl Dispatch<WlRegistry, ()> for PrimarySelectionState { fn event( state: &mut Self, registry: &WlRegistry, event: <WlRegistry as wayland_client::Proxy>::Event, _data: &(), _conn: &Connection, qh: &wayland_client::QueueHandle<Self>, ) { 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 interface == ZwlrDataControlManagerV1::interface().name { assert_eq!(state.clipboard_manager, None); if version == 1 { state.clipboard_manager_was_v1 = true; } else { let manager = registry.bind(name, 2, qh, ()); state.clipboard_manager = Some(manager); } } } } } impl Dispatch<WlSeat, ()> for PrimarySelectionState { fn event( _state: &mut Self, _proxy: &WlSeat, _event: <WlSeat as Proxy>::Event, _data: &(), _conn: &Connection, _qhandle: &wayland_client::QueueHandle<Self>, ) { } } impl Dispatch<ZwlrDataControlManagerV1, ()> for PrimarySelectionState { fn event( _state: &mut Self, _proxy: &ZwlrDataControlManagerV1, _event: <ZwlrDataControlManagerV1 as Proxy>::Event, _data: &(), _conn: &Connection, _qhandle: &wayland_client::QueueHandle<Self>, ) { } } impl Dispatch<ZwlrDataControlDeviceV1, ()> for PrimarySelectionState { fn event( state: &mut Self, _device: &ZwlrDataControlDeviceV1, event: <ZwlrDataControlDeviceV1 as wayland_client::Proxy>::Event, _data: &(), _conn: &wayland_client::Connection, _qh: &wayland_client::QueueHandle<Self>, ) { if let zwlr_data_control_device_v1::Event::PrimarySelection { id: _ } = event { state.got_primary_selection = true; } } event_created_child!(PrimarySelectionState, ZwlrDataControlDeviceV1, [ zwlr_data_control_device_v1::EVT_DATA_OFFER_OPCODE => (ZwlrDataControlOfferV1, ()), ]); } impl Dispatch<ZwlrDataControlOfferV1, ()> for PrimarySelectionState { fn event( _state: &mut Self, _offer: &ZwlrDataControlOfferV1, _event: <ZwlrDataControlOfferV1 as wayland_client::Proxy>::Event, _data: &(), _conn: &wayland_client::Connection, _qhandle: &wayland_client::QueueHandle<Self>, ) { } } /// 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 ({} version {}) is not supported by the compositor", name, version )] MissingProtocol { name: &'static str, version: u32 }, } /// Checks if the compositor supports the primary selection. /// /// # Examples /// /// ```no_run /// # extern crate wl_clipboard_rs; /// # fn foo() -> Result<(), Box<dyn std::error::Error>> { /// 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 either data-control version 1 /// // is present (which does not support the primary selection), or that data-control /// // version 2 is present and it did not signal the primary selection support. /// }, /// Err(PrimarySelectionCheckError::NoSeats) => { /// // Impossible to give a definitive result. Primary selection may or may not be /// // supported. /// /// // The required protocol (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<bool, PrimarySelectionCheckError> { is_primary_selection_supported_internal(None) } pub(crate) fn is_primary_selection_supported_internal( socket_name: Option<OsString>, ) -> Result<bool, PrimarySelectionCheckError> { // Connect to the Wayland compositor. let conn = match socket_name { Some(name) => { let mut socket_path = env::var_os("XDG_RUNTIME_DIR") .map(Into::<PathBuf>::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, clipboard_manager_was_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_was_v1 { return Ok(false); } // Verify that we got the clipboard manager. let Some(ref clipboard_manager) = state.clipboard_manager else { return Err(PrimarySelectionCheckError::MissingProtocol { name: ZwlrDataControlManagerV1::interface().name, version: 1, }); }; // 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) } 0707010000001B000041ED00000000000000000000000265E9594700000000000000000000000000000000000000000000002C00000000wl-clipboard-rs-0.8.1/wl-clipboard-rs-tools0707010000001C000081A400000000000000000000000165E9594700000351000000000000000000000000000000000000003700000000wl-clipboard-rs-0.8.1/wl-clipboard-rs-tools/Cargo.toml[package] name = "wl-clipboard-rs-tools" version = "0.8.1" authors = ["Ivan Molodetskikh <yalterz@gmail.com>"] description = "Terminal utilities for accessing the Wayland clipboard." edition = "2021" license = "MIT/Apache-2.0" readme = "README.md" documentation = "https://docs.rs/wl-clipboard-rs" repository = "https://github.com/YaLTeR/wl-clipboard-rs" keywords = ["wayland", "clipboard"] categories = ["command-line-utilities"] [dependencies] wl-clipboard-rs = { path = "../", version = "0.8.1" } anyhow = "1.0.80" libc = "0.2.153" log = "0.4.21" mime_guess = "2.0.4" nix = "0.28.0" stderrlog = "0.6.0" structopt = { version = "0.3.26", features = ["wrap_help"] } [features] # Link to libwayland-client.so instead of using the Rust implementation. native_lib = [ "wl-clipboard-rs/native_lib", ] dlopen = [ "wl-clipboard-rs/dlopen", ] 0707010000001D000081A400000000000000000000000165E95947000001B1000000000000000000000000000000000000003600000000wl-clipboard-rs-0.8.1/wl-clipboard-rs-tools/README.md# wl-clipboard-rs-tools Terminal utilities for accessing the Wayland clipboard, implemented using [`wl-clipboard-rs`](https://crates.io/crates/wl-clipboard-rs). ## 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`.0707010000001E000041ED00000000000000000000000265E9594700000000000000000000000000000000000000000000003000000000wl-clipboard-rs-0.8.1/wl-clipboard-rs-tools/src0707010000001F000041ED00000000000000000000000265E9594700000000000000000000000000000000000000000000003400000000wl-clipboard-rs-0.8.1/wl-clipboard-rs-tools/src/bin07070100000020000081A400000000000000000000000165E9594700002A28000000000000000000000000000000000000003F00000000wl-clipboard-rs-0.8.1/wl-clipboard-rs-tools/src/bin/wl-clip.rsuse std::env::args_os; use std::ffi::OsString; use std::fs::File; use std::io::{stdout, Read, Write}; use std::process; use anyhow::{Context, Error}; use libc::{STDIN_FILENO, STDOUT_FILENO}; use nix::fcntl::OFlag; use nix::unistd::{close, dup2, fork, ForkResult}; use wl_clipboard_rs::copy::{self, ServeRequests, Source}; use wl_clipboard_rs::paste::{self, get_contents}; use wl_clipboard_rs::utils::is_text; #[derive(Clone, Copy, Eq, PartialEq)] enum Verbosity { Silent, Quiet, Verbose, } struct Options { files: Vec<OsString>, out: bool, loops: usize, target: Option<String>, rmlastnl: bool, verbosity: Verbosity, primary: bool, } impl Default for Options { fn default() -> Self { Self { files: Vec::new(), out: false, loops: 0, target: None, rmlastnl: false, verbosity: Verbosity::Silent, primary: true, } } } impl Options { // Hand-rolled argument parser to match what xclip does. fn from_args() -> Result<Self, Error> { let mut opts = Options::default(); enum Print { Help, Version, } let mut print = None; let mut args = args_os().skip(1).peekable(); while let Some(arg) = args.next() { match arg.into_string() { Ok(arg) => { macro_rules! parse { ($longest:expr, $shortest:expr => $action:block) => ( if $longest.starts_with(&arg) && arg.starts_with($shortest) { $action } ); ($longest:expr, $shortest:expr, $next:ident => $action:block) => ( parse!($longest, $shortest => { if args.peek().is_some() { let $next = args.next().unwrap(); $action } // Important: no continue here. }); ); ($longest:expr, $shortest:expr => $action:stmt) => ( parse!($longest, $shortest => { $action continue; }) ); } parse!("-help", "-h" => print = Some(Print::Help)); parse!("-version", "-vers" => print = Some(Print::Version)); parse!("-out", "-o" => opts.out = true); parse!("-in", "-i" => opts.out = false); parse!("-rmlastnl", "-r" => opts.rmlastnl = true); parse!("-silent", "-si" => opts.verbosity = Verbosity::Silent); parse!("-quiet", "-q" => opts.verbosity = Verbosity::Quiet); parse!("-verbose", "-verb" => opts.verbosity = Verbosity::Verbose); parse!("-filter", "-f" => { // Not sure there's a good way to support this. anyhow::bail!("Unsupported option: -filter"); }); parse!("-noutf8", "-n" => { anyhow::bail!("Unsupported option: -noutf8"); }); parse!("-display", "-d" => { anyhow::bail!("Unsupported option: -display"); }); parse!("-selection", "-se", val => { match val.to_string_lossy().chars().next().unwrap_or('_') { 'c' => opts.primary = false, 'p' => opts.primary = true, 's' => anyhow::bail!("Unsupported option: -selection secondary"), 'b' => anyhow::bail!("Unsupported option: -selection buffer-cut"), _ => {} } continue; }); parse!("-loops", "-l", val => { if let Some(val) = val.into_string().ok().and_then(|x| x.parse().ok()) { opts.loops = val; } continue; }); parse!("-target", "-t", val => { if let Ok(val) = val.into_string() { opts.target = Some(val); } else { anyhow::bail!("Unsupported option: -target <invalid UTF-8>"); } continue; }); opts.files.push(arg.into()) } Err(arg) => opts.files.push(arg), } } // If help or version is requested, print that and exit. match print { Some(Print::Help) => { eprintln!( "\ Usage: {} [OPTION] [FILE]... Access Wayland clipboard for reading or writing, with an xclip interface. -i, -in read text into the clipboard from the standard input or files (default) -o, -out print the contents of the clipboard to standard output -l, -loops number of paste requests to serve before exiting -h, -help show this message -selection clipboard type to access, \"primary\" (default) or \"clipboard\" -target set the MIME type to request or set -rmlastnl trim the last newline character -version show version information -silent output errors only, run in background (default) -quiet run in foreground -verbose run in foreground, show verbose messages Unsupported xclip options: -d, -display -f, -filter -selection secondary, buffer-cut -noutf8", args_os() .next() .and_then(|x| x.into_string().ok()) .unwrap_or_else(|| "wl-clip".to_string()) ); process::exit(0); } Some(Print::Version) => { eprintln!("wl-clip version {}", env!("CARGO_PKG_VERSION")); eprintln!("{}", env!("CARGO_PKG_AUTHORS")); process::exit(0); } None => {} } Ok(opts) } } impl From<Options> for copy::Options { fn from(x: Options) -> Self { let mut opts = copy::Options::new(); opts.serve_requests(if x.loops == 0 { ServeRequests::Unlimited } else { ServeRequests::Only(x.loops) }) .foreground(true) // We fork manually to support background mode. .clipboard(if x.primary { copy::ClipboardType::Primary } else { copy::ClipboardType::Regular }) .trim_newline(x.rmlastnl); opts } } fn main() -> Result<(), Error> { // Parse command-line options. let mut options = Options::from_args()?; stderrlog::new() .verbosity(if options.verbosity == Verbosity::Verbose { 2 } else { 1 }) .init() .unwrap(); if options.out { // Paste. let mime_type = match options.target.as_ref() { Some(target) => paste::MimeType::Specific(target), None => paste::MimeType::Text, }; let clipboard_type = if options.primary { paste::ClipboardType::Primary } else { paste::ClipboardType::Regular }; let (mut read, mime_type) = get_contents(clipboard_type, paste::Seat::Unspecified, mime_type)?; // Read the contents. let mut contents = vec![]; read.read_to_end(&mut contents) .context("Couldn't read clipboard contents")?; // Remove the last newline character if needed. let last_character_is_newline = contents.last().map(|&c| c == b'\n').unwrap_or(false); if options.rmlastnl && is_text(&mime_type) && last_character_is_newline { contents.pop(); } // Write everything to stdout. stdout() .write_all(&contents) .context("Couldn't write contents to stdout")?; } else { // Copy. let data = if options.files.is_empty() { None } else { let mut data = vec![]; for filename in &options.files { let mut file = File::open(filename) .context(format!("Couldn't open {}", filename.to_string_lossy()))?; file.read_to_end(&mut data)?; } Some(data) }; let source = if options.files.is_empty() { Source::StdIn } else { Source::Bytes(data.unwrap().into()) }; let mime_type = if let Some(mime_type) = options.target.take() { copy::MimeType::Specific(mime_type) } else { // xclip uses STRING in this case, but I believe this is better. // If it breaks anyone, it should be changed to Text or Specific("STRING"). copy::MimeType::Autodetect }; let foreground = options.verbosity != Verbosity::Silent; let prepared_copy = copy::Options::from(options).prepare_copy(source, mime_type)?; if foreground { prepared_copy.serve()?; } else { // SAFETY: We don't spawn any threads, so doing things after forking is safe. // TODO: is there any way to verify that we don't spawn any threads? if let ForkResult::Child = unsafe { fork() }.unwrap() { // Replace STDIN and STDOUT with /dev/null. We won't be using them, and keeping them as // is hangs a potential pipeline (i.e. wl-copy hello | cat). Also, simply closing the // file descriptors is a bad idea because then they get reused by subsequent temp file // opens, which breaks the dup2/close logic during data copying. if let Ok(dev_null) = nix::fcntl::open("/dev/null", OFlag::O_RDWR, nix::sys::stat::Mode::empty()) { let _ = dup2(dev_null, STDIN_FILENO); let _ = dup2(dev_null, STDOUT_FILENO); let _ = close(dev_null); } drop(prepared_copy.serve()); } } } Ok(()) } 07070100000021000081A400000000000000000000000165E9594700001679000000000000000000000000000000000000003F00000000wl-clipboard-rs-0.8.1/wl-clipboard-rs-tools/src/bin/wl-copy.rsuse std::ffi::OsString; use std::os::unix::ffi::OsStringExt; use libc::{STDIN_FILENO, STDOUT_FILENO}; use nix::fcntl::OFlag; use nix::unistd::{close, dup2, fork, ForkResult}; use structopt::clap::AppSettings; use structopt::StructOpt; use wl_clipboard_rs::copy::{self, clear, ClipboardType, MimeType, Seat, ServeRequests, Source}; #[derive(StructOpt)] #[structopt(name = "wl-copy", about = "Copy clipboard contents on Wayland.", rename_all = "kebab-case", setting = AppSettings::ColoredHelp)] struct Options { /// Serve only a single paste request and then exit /// /// This option 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. #[structopt(long, short = "o", conflicts_with = "clear")] paste_once: bool, /// Stay in the foreground instead of forking #[structopt(long, short, conflicts_with = "clear")] foreground: bool, /// Clear the clipboard instead of copying #[structopt(long, short)] clear: bool, /// Use the "primary" clipboard /// /// Copying to the "primary" clipboard requires the compositor to support the data-control /// protocol of version 2 or above. #[structopt(long, short)] primary: bool, /// Use the regular clipboard /// /// Set this flag together with --primary to operate on both clipboards at once. Has no effect /// otherwise (since the regular clipboard is the default clipboard). #[structopt(long, short)] regular: bool, /// Trim the trailing newline character before copying /// /// This flag is only applied for text MIME types. #[structopt(long, short = "n", conflicts_with = "clear")] trim_newline: bool, /// Pick the seat to work with /// /// By default wl-copy operates on all seats at once. #[structopt(long, short)] seat: Option<String>, /// Override the inferred MIME type for the content #[structopt( name = "mime-type", long = "type", short = "t", conflicts_with = "clear" )] mime_type: Option<String>, /// Text to copy /// /// If not specified, wl-copy will use data from the standard input. #[structopt(name = "text to copy", conflicts_with = "clear", parse(from_os_str))] text: Vec<OsString>, /// Enable verbose logging #[structopt(long, short, parse(from_occurrences))] verbose: usize, } impl From<Options> for copy::Options { fn from(x: Options) -> Self { let mut opts = copy::Options::new(); opts.serve_requests(if x.paste_once { ServeRequests::Only(1) } else { ServeRequests::Unlimited }) .foreground(true) // We fork manually to support background mode. .clipboard(if x.primary { if x.regular { ClipboardType::Both } else { ClipboardType::Primary } } else { ClipboardType::Regular }) .trim_newline(x.trim_newline) .seat(x.seat.map(Seat::Specific).unwrap_or_default()); opts } } fn main() -> Result<(), anyhow::Error> { // Parse command-line options. let mut options = Options::from_args(); stderrlog::new() .verbosity(options.verbose.saturating_add(1)) .init() .unwrap(); if options.clear { let clipboard = if options.primary { ClipboardType::Primary } else { ClipboardType::Regular }; clear( clipboard, options.seat.map(Seat::Specific).unwrap_or_default(), )?; return Ok(()); } // Is there a way to do this without checking twice? let source_data = if options.text.is_empty() { None } else { // Copy the arguments into the target file. let mut iter = options.text.drain(..); let mut data = iter.next().unwrap(); for arg in iter { data.push(" "); data.push(arg); } Some(data) }; let source = if let Some(source_data) = source_data { Source::Bytes(source_data.into_vec().into()) } else { Source::StdIn }; let mime_type = if let Some(mime_type) = options.mime_type.take() { MimeType::Specific(mime_type) } else { MimeType::Autodetect }; let foreground = options.foreground; let prepared_copy = copy::Options::from(options).prepare_copy(source, mime_type)?; if foreground { prepared_copy.serve()?; } else { // SAFETY: We don't spawn any threads, so doing things after forking is safe. // TODO: is there any way to verify that we don't spawn any threads? if let ForkResult::Child = unsafe { fork() }.unwrap() { // Replace STDIN and STDOUT with /dev/null. We won't be using them, and keeping them as // is hangs a potential pipeline (i.e. wl-copy hello | cat). Also, simply closing the // file descriptors is a bad idea because then they get reused by subsequent temp file // opens, which breaks the dup2/close logic during data copying. if let Ok(dev_null) = nix::fcntl::open("/dev/null", OFlag::O_RDWR, nix::sys::stat::Mode::empty()) { let _ = dup2(dev_null, STDIN_FILENO); let _ = dup2(dev_null, STDOUT_FILENO); let _ = close(dev_null); } drop(prepared_copy.serve()); } } Ok(()) } 07070100000022000081A400000000000000000000000165E9594700001194000000000000000000000000000000000000004000000000wl-clipboard-rs-0.8.1/wl-clipboard-rs-tools/src/bin/wl-paste.rs#![deny(unsafe_code)] use std::fs::read_link; use std::io::{stdout, Read, Write}; use anyhow::Context; use libc::STDOUT_FILENO; use log::trace; use mime_guess::Mime; use structopt::clap::AppSettings; use structopt::StructOpt; use wl_clipboard_rs::paste::*; use wl_clipboard_rs::utils::is_text; #[derive(StructOpt)] #[structopt(name = "wl-paste", about = "Paste clipboard contents on Wayland.", rename_all = "kebab-case", setting = AppSettings::ColoredHelp)] struct Options { /// List the offered MIME types instead of pasting #[structopt(long, short)] list_types: bool, /// Use the "primary" clipboard /// /// Pasting to the "primary" clipboard requires the compositor to support the data-control /// protocol of version 2 or above. #[structopt(long, short)] primary: bool, /// Do not append a newline character /// /// By default the newline character is appended automatically when pasting text MIME types. #[structopt(long, short, conflicts_with = "list-types")] no_newline: bool, /// Pick the seat to work with /// /// By default the seat used is unspecified (it depends on the order returned by the /// compositor). This is perfectly fine when only a single seat is present, so for most /// configurations. #[structopt(long, short)] seat: Option<String>, /// Request the given MIME type instead of inferring the MIME type /// /// As a special case, specifying "text" will look for a number of plain text types, /// prioritizing ones that are known to give UTF-8 text. #[structopt( name = "mime-type", long = "type", short = "t", conflicts_with = "list-types" )] mime_type: Option<String>, /// Enable verbose logging #[structopt(long, short, parse(from_occurrences))] verbose: usize, } fn infer_mime_type() -> Option<Mime> { if let Ok(stdout_path) = read_link(format!("/dev/fd/{}", STDOUT_FILENO)) { mime_guess::from_path(stdout_path).first() } else { None } } fn main() -> Result<(), anyhow::Error> { // Parse command-line options. let options = Options::from_args(); let primary = if options.primary { ClipboardType::Primary } else { ClipboardType::Regular }; let seat = options .seat .as_ref() .map(|x| Seat::Specific(x)) .unwrap_or_default(); stderrlog::new() .verbosity(options.verbose.saturating_add(1)) .init() .unwrap(); // If listing types is requested, do just that. if options.list_types { let mime_types = get_mime_types(primary, seat)?; for mime_type in mime_types.iter() { println!("{}", mime_type); } return Ok(()); } // Otherwise, get the clipboard contents. // No MIME type specified—try inferring one from the output file extension (if any). let inferred = if options.mime_type.is_none() { infer_mime_type() } else { None }; // Do some smart MIME type selection. let mime_type = match options.mime_type { Some(ref mime_type) if mime_type == "text" => MimeType::Text, Some(ref mime_type) => MimeType::Specific(mime_type), None => { let inferred: Option<&str> = inferred.as_ref().map(Mime::as_ref); trace!("Inferred MIME type: {:?}", inferred); match inferred { None | Some("application/octet-stream") => MimeType::Any, // If the inferred MIME type is text, make sure we'll fall back to requesting // other plain text types if this particular one is unavailable. Some(t) if is_text(t) => MimeType::TextWithPriority(t), Some(t) => MimeType::Specific(t), } } }; let (mut read, mime_type) = get_contents(primary, seat, mime_type)?; // Read the contents. let mut contents = vec![]; read.read_to_end(&mut contents) .context("Couldn't read clipboard contents")?; // Append a newline if needed. let last_character_is_newline = contents.last().map(|&c| c == b'\n').unwrap_or(false); if !options.no_newline && is_text(&mime_type) && !last_character_is_newline { contents.push(b'\n'); } // Write everything to stdout. stdout() .write_all(&contents) .context("Couldn't write contents to stdout")?; Ok(()) } 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!395 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor