Compare commits

...

83 commits

Author SHA1 Message Date
ComplexSpaces
223f4efc7a Return a conversion failure error when CGImageCreate fails
Some checks failed
Test / rustfmt (push) Failing after 3s
Test / test (windows-latest) (push) Has been skipped
Test / miri (windows-latest) (push) Has been skipped
Test / test (macos-latest) (push) Has been skipped
Test / clippy (macos-latest, 1.71.0) (push) Has been skipped
Test / clippy (macos-latest, stable) (push) Has been skipped
Test / clippy (ubuntu-latest, 1.71.0) (push) Has been skipped
Test / clippy (ubuntu-latest, stable) (push) Has been skipped
Test / clippy (windows-latest, 1.71.0) (push) Has been skipped
Test / clippy (windows-latest, stable) (push) Has been skipped
Test / semver (push) Has been cancelled
2025-09-12 01:33:01 -06:00
ComplexSpaces
a3750c79a5 Release 3.6.1
Some checks failed
Test / semver (push) Has been cancelled
Test / rustfmt (push) Failing after 3s
Test / clippy (macos-latest, 1.71.0) (push) Has been skipped
Test / clippy (macos-latest, stable) (push) Has been skipped
Test / miri (windows-latest) (push) Has been skipped
Test / clippy (ubuntu-latest, 1.71.0) (push) Has been skipped
Test / clippy (ubuntu-latest, stable) (push) Has been skipped
Test / clippy (windows-latest, 1.71.0) (push) Has been skipped
Test / clippy (windows-latest, stable) (push) Has been skipped
Test / test (macos-latest) (push) Has been skipped
Test / test (windows-latest) (push) Has been skipped
2025-08-23 15:05:17 -05:00
ComplexSpaces
edcce2cd6b Remove CHANGELOG.md in favor of GitHub releases
All existing data here already existed or has been copied to a GitHub release
2025-08-23 15:05:17 -05:00
ComplexSpaces
26a96a6199 Bump windows-sys semver range to support 0.60.x
Some checks failed
Test / rustfmt (push) Failing after 3s
Test / clippy (macos-latest, 1.71.0) (push) Has been skipped
Test / clippy (macos-latest, stable) (push) Has been skipped
Test / clippy (ubuntu-latest, 1.71.0) (push) Has been skipped
Test / clippy (ubuntu-latest, stable) (push) Has been skipped
Test / clippy (windows-latest, 1.71.0) (push) Has been skipped
Test / clippy (windows-latest, stable) (push) Has been skipped
Test / test (macos-latest) (push) Has been skipped
Test / test (windows-latest) (push) Has been skipped
Test / miri (windows-latest) (push) Has been skipped
Test / semver (push) Has been cancelled
2025-08-21 15:29:12 -05:00
ComplexSpaces
7bdd1c1175 Update errno for windows-sys 0.60 flexibility 2025-08-21 15:29:12 -05:00
wcassels
55c0b260c4 read/write_unaligned rather than using manual field offsets
Some checks failed
Test / rustfmt (push) Failing after 12s
Test / clippy (macos-latest, 1.71.0) (push) Has been skipped
Test / clippy (macos-latest, stable) (push) Has been skipped
Test / clippy (ubuntu-latest, 1.71.0) (push) Has been skipped
Test / clippy (ubuntu-latest, stable) (push) Has been skipped
Test / clippy (windows-latest, 1.71.0) (push) Has been skipped
Test / clippy (windows-latest, stable) (push) Has been skipped
Test / test (macos-latest) (push) Has been skipped
Test / test (windows-latest) (push) Has been skipped
Test / miri (windows-latest) (push) Has been skipped
Test / semver (push) Has been cancelled
2025-08-20 23:16:28 -05:00
Will Cassels
ff15a093d6 Return conversionFailure instead of adhoc errors 2025-08-20 23:16:28 -05:00
wcassels
16ef18113f Implement fetching PNG on Windows and prefer over DIB when available 2025-08-20 23:16:28 -05:00
wcassels
a3c64f9a93 Add a couple of end-to-end DIBV5 tests 2025-08-20 23:16:28 -05:00
wcassels
e6008eaa91 Use image for reading DIB and try to make it do the right thing for 32-bit BI_RGBs 2025-08-20 23:16:28 -05:00
Gae24
17ef05ce13
add file_list to Set interface (#181)
Some checks failed
Test / rustfmt (push) Failing after 3s
Test / clippy (macos-latest, 1.71.0) (push) Has been skipped
Test / clippy (macos-latest, stable) (push) Has been skipped
Test / clippy (ubuntu-latest, 1.71.0) (push) Has been skipped
Test / clippy (ubuntu-latest, stable) (push) Has been skipped
Test / clippy (windows-latest, 1.71.0) (push) Has been skipped
Test / clippy (windows-latest, stable) (push) Has been skipped
Test / test (macos-latest) (push) Has been skipped
Test / test (windows-latest) (push) Has been skipped
Test / miri (windows-latest) (push) Has been skipped
Test / semver (push) Has been cancelled
2025-08-13 08:30:34 -06:00
ComplexSpaces
ca2e80c409 Update Clippy lints for Rust 1.89 2025-08-13 09:24:29 -05:00
Gae24
6eed118532 wayland: extract common code in helper function
Some checks failed
Test / rustfmt (push) Failing after 3s
Test / clippy (macos-latest, 1.71.0) (push) Has been skipped
Test / clippy (macos-latest, stable) (push) Has been skipped
Test / clippy (ubuntu-latest, 1.71.0) (push) Has been skipped
Test / clippy (ubuntu-latest, stable) (push) Has been skipped
Test / clippy (windows-latest, 1.71.0) (push) Has been skipped
Test / clippy (windows-latest, stable) (push) Has been skipped
Test / test (macos-latest) (push) Has been skipped
Test / test (windows-latest) (push) Has been skipped
Test / miri (windows-latest) (push) Has been skipped
Test / semver (push) Has been cancelled
2025-08-02 10:38:34 -05:00
Gae24
a31adf444d fix: avoid checking if data is utf8 compliant inside file list getter 2025-08-02 10:38:34 -05:00
Agathe
d48f5ff0c7
src/common.rs: fix typo "an other" -> "another" (#196)
Some checks failed
Test / rustfmt (push) Failing after 2s
Test / clippy (macos-latest, 1.71.0) (push) Has been skipped
Test / clippy (macos-latest, stable) (push) Has been skipped
Test / clippy (ubuntu-latest, 1.71.0) (push) Has been skipped
Test / clippy (ubuntu-latest, stable) (push) Has been skipped
Test / clippy (windows-latest, 1.71.0) (push) Has been skipped
Test / clippy (windows-latest, stable) (push) Has been skipped
Test / test (macos-latest) (push) Has been skipped
Test / test (windows-latest) (push) Has been skipped
Test / miri (windows-latest) (push) Has been skipped
Test / semver (push) Has been cancelled
Found by lintian (spelling-error-in-binary).
2025-07-11 09:37:48 -06:00
ComplexSpaces
90f8f526f4 Fix grammar and typos in README
Some checks failed
Test / rustfmt (push) Failing after 2s
Test / clippy (macos-latest, 1.71.0) (push) Has been skipped
Test / clippy (macos-latest, stable) (push) Has been skipped
Test / clippy (ubuntu-latest, 1.71.0) (push) Has been skipped
Test / clippy (ubuntu-latest, stable) (push) Has been skipped
Test / clippy (windows-latest, 1.71.0) (push) Has been skipped
Test / clippy (windows-latest, stable) (push) Has been skipped
Test / test (macos-latest) (push) Has been skipped
Test / test (windows-latest) (push) Has been skipped
Test / miri (windows-latest) (push) Has been skipped
Test / semver (push) Has been cancelled
2025-06-29 01:52:18 +10:00
ComplexSpaces
4f9bff86dc Release 3.6.0 2025-06-28 09:40:46 +10:00
ComplexSpaces
380d2a691b Remove deprecated authors Cargo manifest field 2025-06-28 09:40:46 +10:00
ComplexSpaces
68ea2074ac Resolve new Clippy lints 2025-06-28 08:49:37 +10:00
ComplexSpaces
8f6bab7d48 Add README section about Linux clipboard ownership 2025-06-28 08:49:37 +10:00
ComplexSpaces
b704da3cea Add debug helper for too-early Linux clipboard dropping 2025-06-28 08:49:37 +10:00
ComplexSpaces
1040043ca4 Reword README sections and elaborate on Linux support 2025-06-28 08:49:37 +10:00
crumblingstatue
5f80bc1ddf
linux/x11: Don't stop worker thread if handling selection request fails (#186)
* linux/x11: Don't stop worker thread if handling selection request fails

Just because one request fails, that doesn't mean we can't handle
subsequent requests.
Stopping the worker thread on failure means that every subsequent
request will fail, which is usually undesirable.
2025-06-24 14:11:45 -05:00
ComplexSpaces
b1e6720c3e Fix getting text on Windows when locale identifiers differ 2025-06-15 22:02:59 -05:00
ComplexSpaces
6b0e47ac8a Reimplement Linux clipboard clearing with correct primitives 2025-06-15 16:59:47 -06:00
ComplexSpaces
825026572a Refactor Wayland error handling to better account for missing Primary clipboard 2025-06-15 16:59:47 -06:00
ComplexSpaces
7ea1cf2caa Clarify ownership handling in selection data writing
While there is no behavior difference, its more
obviously correct if we only claim clipboard ownership
_after_ we've written data and prepared to serve it.
2025-06-15 14:52:43 -06:00
ComplexSpaces
b5e123032c Add exclude_from_history on linux by setting x-kde-passwordManagerHint
Addresses #129

Co-authored-by: MrSmör <66489839+MrSmoer@users.noreply.github.com>
2025-06-15 14:52:43 -06:00
Artúr Kovács
91c33159b0
Release v3.5 (#183)
* Release v3.5

* Update CHANGELOG for 3.5 release

---------

Co-authored-by: ComplexSpaces <complexspacescode@gmail.com>
2025-04-03 13:17:08 +11:00
Gae24
1dcc18b221
Add file_list to Get interface (#179) 2025-03-23 18:08:18 +09:00
ComplexSpaces
bbd06b4d57
Merge pull request #180 from Gae24/error
use `Error::unknown` on Linux and macOS code too
2025-03-10 14:57:12 -05:00
Gae24
bcb2954db5 use Error::unknown on linux and osx too 2025-03-10 09:15:34 +01:00
ComplexSpaces
a9c2d68c18 Update wl-clipboard-rs 2025-02-13 18:47:36 -06:00
ComplexSpaces
e824bc8324 Cleanup Windows pointer casting 2025-02-13 18:26:00 -06:00
ComplexSpaces
543484f77c Check multiple windows-sys versions in CI
This is to prevent accidental breakage due to the new version range
2025-02-13 18:26:00 -06:00
ComplexSpaces
9d6a0b9b42 Update windows-sys version compatibility
Co-authored-by: Exotik850 <kidkool850@gmail.com>
2025-02-13 18:26:00 -06:00
ComplexSpaces
c474298e4b Remove unneeded Result from image_from_pixels 2025-02-13 14:17:07 -06:00
Mads Marquart
108cc38269
Update to objc2 v0.6 (#173)
This includes using the new crate `objc2-core-graphics`, which notably
does not have the `CustomData` helper that `core-graphics`. This is
probably for the better, as it allows us to avoid a double-boxing of the
data.
2025-02-13 12:44:37 -07:00
ComplexSpaces
431702b657
Bump Ubuntu runner version for cargo-semver-checks (#174) 2025-02-12 22:16:21 -07:00
Gae24
4b91bfe93e
Implement Get::html() for all platforms (#163)
* implement get html operation

Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
2025-02-12 22:01:02 -07:00
Hamir Mahal
e458e1a26c style: simplify some statements for readability 2024-12-26 13:28:02 -07:00
ComplexSpaces
782b98c1e3 Cleanup unneeded lifetimes 2024-12-26 12:26:29 -07:00
ComplexSpaces
5350a8fb91 Bump env_logger
This removes `atty` and `winapi` from CI's test
builds.
2024-10-19 10:45:23 -05:00
ComplexSpaces
6b45272702 Release 3.4.1 2024-09-12 09:34:21 -06:00
Oscar Hinton
dd43f44ace
Add support for excluding macos clipboard items from history (#159)
* Adds support for excluding macos clipboard items from history
2024-08-27 12:10:21 -06:00
ComplexSpaces
ee39c47896 Fix new lints for CI 2024-08-24 15:36:59 -06:00
ComplexSpaces
151e679ee5 Release 3.4.0 2024-04-29 01:02:49 -05:00
ComplexSpaces
610e29ba81 Remove direct thiserror dependency 2024-04-28 04:08:29 -05:00
Roman Vlasenko
83740b7ab0
Copy image as PNG file on Windows (#141)
* Add image to Windows dependencies

* Set image data as PNG file on Windows
2024-04-28 01:57:04 -05:00
Mads Marquart
0bff1e07ea Use objc2 and its framework crates
`objc2` is a replacement for `objc`/`objc_id` that contains a bunch of safety improvements, including `msg_send_id!` which automatically upholds memory management rules (`Id::from_ptr`/`Id::from_retained_ptr` is no longer necessary).

Additionally, we use the framework crates `objc2-foundation` and `objc2-app-kit`, which provide for example the `NSPasteboard` type, which has the methods that arboard needs already defined, and with the correct types, ensuring that passing e.g. `Id<NSArray>` and thus accidentally giving away ownership over the array won't happen again.

These crates are automatically generated, ensuring that if you need some obscure API in the future, it's very likely to be there already.
2024-04-27 15:53:32 -05:00
ComplexSpaces
1cca83d7e5 Revert "add timeout to RustConnection::connect to X11 server"
This reverts commit efedfb9e20.
2024-04-27 15:38:04 -05:00
ComplexSpaces
b4646f6c5f Increase version of clipboard-win used by default 2024-04-22 14:11:27 -05:00
Noel
e2846f9288 Fix clippy errors 2024-04-12 15:13:45 -05:00
Noel
2f4b502508 Move WaitConfig to src/platform/linux/mod.rs, use WaitConfig inside struct Set 2024-04-12 15:13:45 -05:00
Noel
6cf324cc44 Added WaitConfig, fix wait_until note in docs 2024-04-12 15:13:45 -05:00
Noel
eabb191df0 add notice for X11 in SetExtLinux#wait_until docs 2024-04-12 15:13:45 -05:00
Noel
bc9fd24915 update docs 2024-04-12 15:13:45 -05:00
Noel
c5c798b3a1 Add SetExtLinux#wait_until(Instant)
This was added to allow to wait until the contents of the clipboard were updated but won't block forever. The `wait_until` method will wait
until the deadline was reached.
2024-04-12 15:13:45 -05:00
rhysd
bb2e898d83 Bump up MSRV to 1.67.1 2024-04-11 20:26:50 -05:00
rhysd
e5d3df7017 Fix CI for Rust 1.61 doesn't use rustc 1.61 compiler 2024-04-11 20:26:50 -05:00
TÖRÖK Attila
f6fc4ea691 Update image to 0.25.1 2024-04-09 16:06:49 -05:00
ComplexSpaces
dc8a4bd4f2 Release 3.3.2 2024-03-02 13:39:56 -06:00
ComplexSpaces
f716441fe6 Bump Ubuntu version used in CI
Ubuntu 20 isn't the latest LTS version anymore
2024-03-02 12:51:51 -06:00
ComplexSpaces
2d77eee554 Add cargo-semver-checks to CI 2024-03-02 12:51:51 -06:00
ComplexSpaces
d1ef0918de Enable unreachable_pub lint as a warning
CI will make these a hard error in the future
2024-03-02 12:51:51 -06:00
ComplexSpaces
3f21b88baa Correctly mark windows::image_data functions as pub(super) and not pub 2024-03-02 12:51:51 -06:00
ComplexSpaces
77e0e078eb Release 3.3.1 2024-02-12 17:48:25 -06:00
Magnus Larsen
409bd98978 Update x11rb to 0.13 and core-graphics to 0.23 2024-02-08 23:55:12 -06:00
Rob Ede
bd91f9c438 Increase error documentation on Clipboard type 2024-01-22 11:05:21 -06:00
Rob Ede
a648570ce9 Update CI actions 2024-01-02 11:02:59 -07:00
Rob Ede
0d6725d97f Spell check docs 2023-12-29 12:06:59 -07:00
Linda_pp
a100f2d77c
Update clipboard-win to v5 and replace winapi with windows-sys (#123)
* Refactor Windows code to be more idiomatic
2023-12-04 14:52:14 -06:00
ComplexSpaces
1b8df75ee2 Bump to Rust 2021 edition 2023-11-21 22:15:39 -06:00
ComplexSpaces
e3f54c3049 Document MSRV of 1.61 2023-11-21 22:15:39 -06:00
rhysd
8c475cfd14 Make winapi crate optional 2023-11-21 22:09:09 -06:00
ComplexSpaces
11e395c6d5 Release 3.3.0 2023-11-20 16:18:25 -06:00
ComplexSpaces
b2b0809632 Add support for explicit clipboard monitor excluding on Windows 2023-11-20 15:51:01 -06:00
ComplexSpaces
f801e67e70 Bump Miri toolchain version and resolve used warnings 2023-10-08 17:27:56 -05:00
ComplexSpaces
9bc6fcd3f8 Return a more clear error message when X11 connections timeout 2023-10-08 17:27:56 -05:00
ComplexSpaces
d755392b56 Unify logging dependencies by using env_logger everywhere
There is no reason to depend on two different dev dependency loggers
2023-10-08 17:27:56 -05:00
dAxpeDDa
48117d9688 Update wl-clipboard-rs to v0.8 2023-10-08 16:06:38 -05:00
dAxpeDDa
1e7d475d6e Update x11rb to v0.12 2023-10-08 16:06:38 -05:00
daxpedda
5a627e9445 Fix CI 2023-10-05 19:49:09 -05:00
16 changed files with 2231 additions and 1372 deletions

View file

@ -8,19 +8,15 @@ on:
jobs:
rustfmt:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
run: cargo fmt --all -- --check
clippy:
needs: rustfmt
@ -28,37 +24,36 @@ jobs:
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
# Latest stable and MSRV. We only run checks with all features enabled
# for the MSRV build to keep CI fast, since other configurations should also work.
rust_version: [stable, "1.71.0"]
steps:
- uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
override: true
toolchain: ${{ matrix.rust_version }}
components: clippy
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Run `cargo clippy` with no features
uses: actions-rs/cargo@v1
with:
command: clippy
args: --verbose --no-default-features -- -D warnings -D clippy::dbg_macro
if: ${{ matrix.rust_version == 'stable' }}
run: cargo clippy --verbose --no-default-features -- -D warnings -D clippy::dbg_macro
- name: Run `cargo clippy` with `image-data` feature
uses: actions-rs/cargo@v1
with:
command: clippy
args: --verbose --no-default-features --features image-data -- -D warnings -D clippy::dbg_macro
if: ${{ matrix.rust_version == 'stable' }}
run: cargo clippy --verbose --no-default-features --features image-data -- -D warnings -D clippy::dbg_macro
- name: Run `cargo clippy` with `wayland-data-control` feature
uses: actions-rs/cargo@v1
with:
command: clippy
args: --verbose --no-default-features --features wayland-data-control -- -D warnings -D clippy::dbg_macro
if: ${{ matrix.rust_version == 'stable' }}
run: cargo clippy --verbose --no-default-features --features wayland-data-control -- -D warnings -D clippy::dbg_macro
- name: Run `cargo clippy` with all features
uses: actions-rs/cargo@v1
with:
command: clippy
args: --verbose --all-features -- -D warnings -D clippy::dbg_macro
run: cargo clippy --verbose --all-features -- -D warnings -D clippy::dbg_macro
- name: Run `cargo clippy` with dependency version checks
if: ${{ matrix.rust_version == 'stable' }}
run: |
cargo update -p windows-sys
cargo clippy --verbose --all-features -- -D warnings -D clippy::dbg_macro
test:
needs: clippy
@ -68,32 +63,19 @@ jobs:
# No Linux test for now as it just fails due to not having a desktop environment.
os: [macos-latest, windows-latest]
steps:
- uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
override: true
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Run tests with no features
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features
run: cargo test --no-default-features
- name: Run tests with `image-data` feature
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features image-data
run: cargo test --no-default-features --features image-data
- name: Run tests with `wayland-data-control` feature
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features wayland-data-control
run: cargo test --no-default-features --features wayland-data-control
- name: Run tests with all features
uses: actions-rs/cargo@v1
with:
command: test
args: --all-features
run: cargo test --all-features
miri:
needs: clippy
@ -105,17 +87,20 @@ jobs:
# Currently, only Windows has soundness tests.
os: [windows-latest]
steps:
- uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly-2022-10-15
override: true
toolchain: nightly-2023-10-08
components: miri
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Check soundness
uses: actions-rs/cargo@v1
with:
command: miri
args: test windows --features image-data
run: cargo miri test windows --features image-data
semver:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check semver
uses: obi1kenobi/cargo-semver-checks-action@v2

View file

@ -1,119 +0,0 @@
# Changelog
## 3.2.1 on 2023-28-11
### Fixed
- Removed all leaks from the macOS clipboard code. Previously, both the `get` and `set` methods leaked data.
- Fixed documentation examples so that they compile on Linux.
- Removed extra whitespace macOS's HTML copying template. This caused unexpected behavior in some apps.
### Changed
- Added a timeout when connecting to the X11 server on UNIX platforms. In situations where the X11 socket is present but unusable, the clipboard
initialization will no longer hang indefinitely.
- Removed macOS-specific dependency on the `once_cell` crate.
## 3.2.0 on 2022-04-11
### Changed
- The Windows clipboard now behaves consistently with the other
platform implementations again.
- Significantly improve cross-platform documentation of `Clipboard`.
- Remove lingering uses of the dbg! macro in the Wayland backend.
## 3.1.1 on 2022-17-10
### Added
- Implemented the ability to set HTML on the clipboard
### Changed
- Updated minimum `clipboard-win` version to `4.4`.
- Updated `wl-clipboard-rs` to the version `0.7`.
## 3.1 on 2022-20-09
### Changed
- Updated `image` to the version `0.24`.
- Lowered Wayland clipboard initialization log level.
## 3.0 on 2022-19-09
### Added
- Support for clearing the clipboard.
- Spport for excluding Windows clipboard data from cliboard history and OneDrive.
- Support waiting for another process to read clipboard data before returning
from a `write` call to a X11 and Wayland or clipboard
### Changed
- Updated `wl-clipboard-rs` to the version `0.6`.
- Updated `x11rb` to the version `0.10`.
- Cleaned up spelling in documentation
- (Breaking) Functions that used to accept `String` now take `Into<Cow<'a>, str>` instead.
This avoids cloning the string more times then necessary on platforms that can.
- (Breaking) `Error` is now marked as `#[non_exhaustive]`.
- (Breaking) Removed all platform specific modules and clipboard structures from the public API.
If you were using these directly, the recommended replacement is using `arboard::Clipboard` and
the new platform-specific extension traits instead.
- (Breaking) On Windows, the clipboard is now opened once per call to `Clipboard::new()` instead of on
each operation. This means that instances of `Clipboard` should be dropped once you're performed the
needed operations to prevent other applications from working with it afterwards.
## v2.1.1 on 2022-18-05
### Changed
- Fix compilation on FreeBSD
- Internal cleanup and documentation fixes
- Remove direct dependency on the `once_cell` crate.
- Fixed crates.io repository link
## v2.1.0 on 2022-09-03
### Changed
- Updated most dependencies
- Removed crate deprecation
- Fixed soundness bug in Windows clipboard
## v2.0.1 on 2021-11-05
### Changed
- On X11, re-assert clipboard ownership every time the data changes.
## v2.0.0 on 2021-08-07
### Changed
- Update dependency on yanked crate versions
- Make the image operations an optional feature
### Added
- Support selecting which linux clipboard is used
## v1.2.1 on 2021-05-04
### Changed
- Fixed a bug that caused the `set_image` function on Windows to distort the
image colors.
## v1.2.0 on 2021-04-06
### Added
- Optional native wayland support through the `wl-clipboard-rs` crate.
## v1.1.0 on 2020-12-29
### Changed
- The `set_image` function on Windows now also provides the image in
`CF_BITMAP` format.
## v1.0.2 on 2020-10-29
### Changed
- Fixed the clipboard contents sometimes not being preserved after the program
exited.

724
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,48 +1,91 @@
[package]
name = "arboard"
version = "3.2.1"
authors = ["Artur Kovacs <kovacs.artur.barnabas@gmail.com>", "Avi Weinstock <aweinstock314@gmail.com>", "Arboard contributors"]
version = "3.6.1"
description = "Image and text handling for the OS clipboard."
repository = "https://github.com/1Password/arboard"
license = "MIT OR Apache-2.0"
readme = "README.md"
keywords = ["clipboard", "image"]
edition = "2018"
edition = "2021"
rust-version = "1.71.0"
[features]
default = ["image-data"]
image-data = ["core-graphics", "image", "winapi/minwindef", "winapi/wingdi", "winapi/winnt"]
image-data = [
"dep:objc2-core-graphics",
"dep:objc2-core-foundation",
"image",
"windows-sys",
"core-graphics",
]
wayland-data-control = ["wl-clipboard-rs"]
# For backwards compat
core-graphics = ["dep:objc2-core-graphics"]
windows-sys = ["windows-sys/Win32_Graphics_Gdi"]
image = ["dep:image"]
wl-clipboard-rs = ["dep:wl-clipboard-rs"]
[dependencies]
thiserror = "1.0"
[dev-dependencies]
simple_logger = "2.1"
env_logger = "0.9.0"
env_logger = "0.10.2"
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = [
"basetsd",
"winuser",
"winbase",
]}
clipboard-win = "4.4.2"
windows-sys = { version = ">=0.52.0, <0.61.0", features = [
"Win32_Foundation",
"Win32_Storage_FileSystem",
"Win32_System_DataExchange",
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_UI_Shell",
] }
clipboard-win = { version = "5.3.1", features = ["std"] }
log = "0.4"
image = { version = "0.25", optional = true, default-features = false, features = [
"png", "bmp"
] }
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2"
objc_id = "0.1"
objc-foundation = "0.1"
core-graphics = { version = "0.22", optional = true }
image = { version = "0.24", optional = true, default-features = false, features = ["tiff"] }
objc2 = "0.6.0"
objc2-foundation = { version = "0.3.0", default-features = false, features = [
"std",
"NSArray",
"NSString",
"NSEnumerator",
"NSGeometry",
"NSValue",
] }
objc2-app-kit = { version = "0.3.0", default-features = false, features = [
"std",
"objc2-core-graphics",
"NSPasteboard",
"NSPasteboardItem",
"NSImage",
] }
objc2-core-foundation = { version = "0.3.0", default-features = false, optional = true, features = [
"std",
"CFCGTypes",
] }
objc2-core-graphics = { version = "0.3.0", default-features = false, optional = true, features = [
"std",
"CGImage",
"CGColorSpace",
"CGDataProvider",
] }
image = { version = "0.25", optional = true, default-features = false, features = [
"tiff",
] }
[target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies]
log = "0.4"
x11rb = { version = "0.10" }
wl-clipboard-rs = { version = "0.7", optional = true }
image = { version = "0.24", optional = true, default-features = false, features = ["png"] }
x11rb = { version = "0.13" }
wl-clipboard-rs = { version = "0.9.0", optional = true }
image = { version = "0.25", optional = true, default-features = false, features = [
"png",
] }
parking_lot = "0.12"
percent-encoding = "2.3.1"
[[example]]
name = "get_image"

107
README.md
View file

@ -2,6 +2,7 @@
[![Latest version](https://img.shields.io/crates/v/arboard?color=mediumvioletred)](https://crates.io/crates/arboard)
[![Documentation](https://docs.rs/arboard/badge.svg)](https://docs.rs/arboard)
![MSRV](https://img.shields.io/badge/rustc-1.71.0+-blue.svg)
## General
@ -9,22 +10,67 @@ This is a cross-platform library for interacting with the clipboard. It allows
to copy and paste both text and image data in a platform independent way on
Linux, Mac, and Windows.
Please note that this is not an official 1Password product. Feature requests will be considered like any other volunteer-based crate.
## GNU/Linux
The GNU/Linux implementation uses the X protocol by default for managing the
clipboard but *fear not* because Wayland works with the X11 protocol just as
well. Furthermore this implementation uses the Clipboard selection (as opposed
to the primary selection) and it sends the data to the clipboard manager when
the application exits so that the data placed onto the clipboard with your
application remains to be available after exiting.
### Backend Support
There's also an optional wayland data control backend through the
`wl-clipboard-rs` crate. This can be enabled using the `wayland-data-control`
feature. When enabled this will be prioritized over the X11 backend, but if the
initialization fails, the implementation falls back to using the X11 protocol
automatically. Note that in my tests the wayland backend did not keep the
clipboard contents after the process exited. (Although neither did the X11
backend on my Wayland setup).
By default, `arboard`'s backend on Linux supports X11 (or XWayland implementations) and uses
that for managing the various Linux clipboard variants. This supports the majority of desktop
environments that exist in the wild today. `arboard` will use the `Clipboard` selection by default,
but the [LinuxClipboardKind](https://docs.rs/arboard/latest/arboard/enum.LinuxClipboardKind.html)
selector lets you operate on the `Primary` or `Secondary` clipboard selections (if supported).
However, Wayland is becoming the majority default as of 2025. Some distributions are
even considering the removal of X by default. To support Wayland correctly, `arboard` users
should enable the `wayland-data-control` feature. If enabled, it will be prioritized over the X clipboard.
Wayland support is not enabled by default because it may be counterintuitive
to some users: it relies on the data-control protocol extension(s), which _are not_
supported by all Wayland compositors. You can check compositor support on `wayland.app`:
- [ext-data-control-v1](https://wayland.app/protocols/ext-data-control-v1)
- [wlr-data-control-unstable-v1](https://wayland.app/protocols/wlr-data-control-unstable-v1)
If you or a user's desktop doesn't support these protocols, `arboard` won't function in a pure
Wayland environment. It is recommended to enable `XWayland` for these cases. If your app runs inside
an isolated sandbox, such as Flatpak or Snap, you'll need to expose the X11 socket to the application
_in addition_ to the Wayland communication interface.
### Clipboard Ownership
Some apps and users may notice that sometimes values copied to a Linux clipboard with this crate vanish
before anyone gets the chance to paste, or just aren't available when you expect them to be. The root behind
these problems is _selection ownership_.
X11 and Wayland put the responsibility for answering paste requests and serving data on the application
which originally copied it onto the clipboard. This usually means the app using `arboard`. Nothing you copy
to the clipboard is sent anywhere to start. It stays inside `arboard` until something else on the system requests it,
which is very different to how the clipboard works on other platforms like macOS or Windows.
Note that `arboard` may attempt to warn you about these conditions when compiled in debug mode, to improve the debugging
experience. Even if you don't see these warnings, you should double check the lifetime of the `Clipboard` in your code.
In some cases, an environment may have a clipboard manager installed. These services monitor the clipboard contents and
do their best to retain a copy when needed to smooth over clipboard ownership changes. A clipboar manager can make contents
available even after a process previously owning it exits.
In order to keep the contents around longer, make sure that you don't `Drop` your `Clipboard` object right away or
terminate the copying process too fast. This is why, at times, adding a call to `sleep()` near the set operation
makes it behave more reliabily: the background thread `arboard` uses for serving clipboard contents has more time to run
and let other apps (including clipboard managers) make requests for the contents. However `sleep` isn't the recommended approach.
If your application is exiting, you must make sure there is a clipboard manager running on the system. If nothing is listening for
the clipboard ownership transfer, or made a copy previously, the data will be lost. Note that this isn't a complete
guarantee as races are possible if your program's main thread is exiting. If you would like to fully synchronize the clipboard "paste"
before exiting, you can use the [wait](https://docs.rs/arboard/latest/arboard/trait.SetExtLinux.html#tymethod.wait) method when setting
contents on the clipboard. This will block the calling thread until another app has requested, and then received, the data.
If your application is longer-running (ie a GUI, TUI, etc), it is highly recommended that you either store the `Clipboard` object in some
long-lived data structure (like app context, etc) or utilize `wait` method mentioned above, and/or threading to make sure another
app can request the clipboard data later.
We welcome suggestions to improve on the above issues in ways that don't degrade other use cases.
## Example
@ -41,12 +87,31 @@ fn main() {
}
```
## Yet another clipboard crate
## Credits
This is a fork of `rust-clipboard`. The reason for forking instead of making a
PR is that `rust-clipboard` is not being maintained any more. Furthermore note
that the API of this crate is considerably different from that of
`rust-clipboard`. There are already a ton of clipboard crates out there which
is a bit unfortunate; I don't know why this is happening but while it is, we
might as well just start naming the clipboard crates after ourselves. This one
is arboard which stands for Artur's clipboard.
This crate is a combined effort by 1Password staff and `@ArturKovacs`, the crate's past
maintainer.
#### License
<sup>
Licensed under either of <a href="LICENSE-APACHE.txt">Apache License, Version
2.0</a> or <a href="LICENSE-MIT.txt">MIT license</a> at your option.
</sup>
<br>
<sub>
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.
</sub>
#### History: Yet another clipboard crate
This crate started out as a fork of `rust-clipboard`. The reason for forking is due to the former
crate not being maintained any longer. At this point, `arboard`'s backends and public APIs have diverged
a lot.
`arboard`'s original maintainer noted that "I don't know why this is happening but while it is, we might
as well just start naming the clipboard crates after ourselves. This one is arboard which stands for Artur's clipboard.".

View file

@ -4,7 +4,6 @@
use arboard::Clipboard;
#[cfg(target_os = "linux")]
use arboard::SetExtLinux;
use simple_logger::SimpleLogger;
use std::{env, error::Error, process};
// An argument that can be passed into the program to signal that it should daemonize itself. This
@ -18,7 +17,7 @@ fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
return Ok(());
}
SimpleLogger::new().init().unwrap();
env_logger::init();
if cfg!(target_os = "linux") {
process::Command::new(env::current_exe()?)

View file

@ -1,12 +1,11 @@
use arboard::Clipboard;
use simple_logger::SimpleLogger;
fn main() {
SimpleLogger::new().init().unwrap();
env_logger::init();
let mut clipboard = Clipboard::new().unwrap();
println!("Clipboard text was: {:?}", clipboard.get_text());
let the_string = "Hello, world!";
clipboard.set_text(the_string).unwrap();
println!("But now the clipboard text should be: \"{}\"", the_string);
println!("But now the clipboard text should be: \"{the_string}\"");
}

View file

@ -1,9 +1,8 @@
use arboard::Clipboard;
use simple_logger::SimpleLogger;
use std::{thread, time::Duration};
fn main() {
SimpleLogger::new().init().unwrap();
env_logger::init();
let mut ctx = Clipboard::new().unwrap();
let html = r#"<h1>Hello, World!</h1>
@ -16,4 +15,7 @@ consectetur adipiscing elit."#;
ctx.set_html(html, Some(alt_text)).unwrap();
thread::sleep(Duration::from_secs(5));
let success = ctx.get().html().unwrap() == html;
println!("Set and Get html operations were successful: {success}");
}

View file

@ -10,20 +10,17 @@ and conditions of the chosen license apply to this file.
#[cfg(feature = "image-data")]
use std::borrow::Cow;
use thiserror::Error;
/// An error that might happen during a clipboard operation.
///
/// Note that both the `Display` and the `Debug` trait is implemented for this type in such a way
/// that they give a short human-readable description of the error; however the documentation
/// gives a more detailed explanation for each error kind.
#[derive(Error)]
#[non_exhaustive]
pub enum Error {
/// The clipboard contents were not available in the requested format.
/// This could either be due to the clipboard being empty or the clipboard contents having
/// an incompatible format to the requested one (eg when calling `get_image` on text)
#[error("The clipboard contents were not available in the requested format or the clipboard is empty.")]
ContentNotAvailable,
/// The selected clipboard is not supported by the current configuration (system and/or environment).
@ -31,10 +28,9 @@ pub enum Error {
/// This can be caused by a few conditions:
/// - Using the Primary clipboard with an older Wayland compositor (that doesn't support version 2)
/// - Using the Secondary clipboard on Wayland
#[error("The selected clipboard is not supported with the current system configuration.")]
ClipboardNotSupported,
/// The native clipboard is not accessible due to being held by an other party.
/// The native clipboard is not accessible due to being held by another party.
///
/// This "other party" could be a different process or it could be within
/// the same program. So for example you may get this error when trying
@ -43,25 +39,33 @@ pub enum Error {
/// Note that it's OK to have multiple `Clipboard` instances. The underlying
/// implementation will make sure that the native clipboard is only
/// opened for transferring data and then closed as soon as possible.
#[error("The native clipboard is not accessible due to being held by an other party.")]
ClipboardOccupied,
/// This can happen in either of the following cases.
///
/// - When returned from `set_image`: the image going to the clipboard cannot be converted to the appropriate format.
/// - When returned from `get_image`: the image coming from the clipboard could not be converted into the `ImageData` struct.
/// - When returned from `get_text`: the text coming from the clipboard is not valid utf-8 or cannot be converted to utf-8.
#[error("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format.")]
/// The image or the text that was about the be transferred to/from the clipboard could not be
/// converted to the appropriate format.
ConversionFailure,
/// Any error that doesn't fit the other error types.
///
/// The `description` field is only meant to help the developer and should not be relied on as a
/// means to identify an error case during runtime.
#[error("Unknown error while interacting with the clipboard: {description}")]
Unknown { description: String },
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::ContentNotAvailable => f.write_str("The clipboard contents were not available in the requested format or the clipboard is empty."),
Error::ClipboardNotSupported => f.write_str("The selected clipboard is not supported with the current system configuration."),
Error::ClipboardOccupied => f.write_str("The native clipboard is not accessible due to being held by another party."),
Error::ConversionFailure => f.write_str("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format."),
Error::Unknown { description } => f.write_fmt(format_args!("Unknown error while interacting with the clipboard: {description}")),
}
}
}
impl std::error::Error for Error {}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use Error::*;
@ -81,7 +85,13 @@ impl std::fmt::Debug for Error {
ConversionFailure,
Unknown { .. }
);
f.write_fmt(format_args!("{} - \"{}\"", name, self))
f.write_fmt(format_args!("{name} - \"{self}\""))
}
}
impl Error {
pub(crate) fn unknown<M: Into<String>>(message: M) -> Self {
Error::Unknown { description: message.into() }
}
}
@ -121,7 +131,7 @@ pub struct ImageData<'a> {
}
#[cfg(feature = "image-data")]
impl<'a> ImageData<'a> {
impl ImageData<'_> {
/// Returns a the bytes field in a way that it's guaranteed to be owned.
/// It moves the bytes if they are already owned and clones them if they are borrowed.
pub fn into_owned_bytes(self) -> Cow<'static, [u8]> {

View file

@ -7,9 +7,13 @@ The project to which this file belongs is licensed under either of
the Apache 2.0 or the MIT license at the licensee's choice. The terms
and conditions of the chosen license apply to this file.
*/
#![warn(unreachable_pub)]
mod common;
use std::borrow::Cow;
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
pub use common::Error;
#[cfg(feature = "image-data")]
@ -26,6 +30,9 @@ pub use platform::{ClearExtLinux, GetExtLinux, LinuxClipboardKind, SetExtLinux};
#[cfg(windows)]
pub use platform::SetExtWindows;
#[cfg(target_os = "macos")]
pub use platform::SetExtApple;
/// The OS independent struct for accessing the clipboard.
///
/// Any number of `Clipboard` instances are allowed to exist at a single point in time. Note however
@ -42,7 +49,7 @@ pub use platform::SetExtWindows;
///
/// `arboard` does its best to abstract over different platforms, but sometimes the platform-specific
/// behavior leaks through unsolvably. These differences, depending on which platforms are being targeted,
/// may affect your app's clipboard architecture (ex, opening and closing a [Clipboard] every time
/// may affect your app's clipboard architecture (ex, opening and closing a [`Clipboard`] every time
/// or keeping one open in some application/global state).
///
/// ## Linux
@ -54,33 +61,52 @@ pub use platform::SetExtWindows;
/// ## Windows
///
/// The clipboard on Windows is a global object, which may only be opened on one thread at once.
/// This means that `arboard` only truly opens the clipboard during each operation to ensure that
/// multiple `Clipboard`'s may exist at once. This also means that attempting operations in parallel
/// has a high likelyhood to return an error instead.
/// This means that `arboard` only truly opens the clipboard during each operation to prevent
/// multiple `Clipboard`s from existing at once.
///
/// This means that attempting operations in parallel has a high likelihood to return an error or
/// deadlock. As such, it is recommended to avoid creating/operating clipboard objects on >1 thread.
#[allow(rustdoc::broken_intra_doc_links)]
pub struct Clipboard {
pub(crate) platform: platform::Clipboard,
}
impl Clipboard {
/// Creates an instance of the clipboard
/// Creates an instance of the clipboard.
///
/// # Errors
///
/// On some platforms or desktop environments, an error can be returned if clipboards are not
/// supported. This may be retried.
pub fn new() -> Result<Self, Error> {
Ok(Clipboard { platform: platform::Clipboard::new()? })
}
/// Fetches utf-8 text from the clipboard and returns it.
/// Fetches UTF-8 text from the clipboard and returns it.
///
/// # Errors
///
/// Returns error if clipboard is empty or contents are not UTF-8 text.
pub fn get_text(&mut self) -> Result<String, Error> {
self.get().text()
}
/// Places the text onto the clipboard. Any valid utf-8 string is accepted.
/// Places the text onto the clipboard. Any valid UTF-8 string is accepted.
///
/// # Errors
///
/// Returns error if `text` failed to be stored on the clipboard.
pub fn set_text<'a, T: Into<Cow<'a, str>>>(&mut self, text: T) -> Result<(), Error> {
self.set().text(text)
}
/// Places the HTML as well as a plain-text alternative onto the clipboard.
///
/// Any valid utf-8 string is accepted.
/// Any valid UTF-8 string is accepted.
///
/// # Errors
///
/// Returns error if both `html` and `alt_text` failed to be stored on the clipboard.
pub fn set_html<'a, T: Into<Cow<'a, str>>>(
&mut self,
html: T,
@ -94,6 +120,11 @@ impl Clipboard {
/// Any image data placed on the clipboard with `set_image` will be possible read back, using
/// this function. However it's of not guaranteed that an image placed on the clipboard by any
/// other application will be of a supported format.
///
/// # Errors
///
/// Returns error if clipboard is empty, contents are not an image, or the contents cannot be
/// converted to an appropriate format and stored in the [`ImageData`] type.
#[cfg(feature = "image-data")]
pub fn get_image(&mut self) -> Result<ImageData<'static>, Error> {
self.get().image()
@ -106,6 +137,11 @@ impl Clipboard {
/// - On macOS: `NSImage` object
/// - On Linux: PNG, under the atom `image/png`
/// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP`
///
/// # Errors
///
/// Returns error if `image` cannot be converted to an appropriate format or if it failed to be
/// stored on the clipboard.
#[cfg(feature = "image-data")]
pub fn set_image(&mut self, image: ImageData) -> Result<(), Error> {
self.set().image(image)
@ -113,6 +149,10 @@ impl Clipboard {
/// Clears any contents that may be present from the platform's default clipboard,
/// regardless of the format of the data.
///
/// # Errors
///
/// Returns error on Windows or Linux if clipboard cannot be cleared.
pub fn clear(&mut self) -> Result<(), Error> {
self.clear_with().default()
}
@ -155,6 +195,16 @@ impl Get<'_> {
pub fn image(self) -> Result<ImageData<'static>, Error> {
self.platform.image()
}
/// Completes the "get" operation by fetching HTML from the clipboard.
pub fn html(self) -> Result<String, Error> {
self.platform.html()
}
/// Completes the "get" operation by fetching a list of file paths from the clipboard.
pub fn file_list(self) -> Result<Vec<PathBuf>, Error> {
self.platform.file_list()
}
}
/// A builder for an operation that sets a value to the clipboard.
@ -196,6 +246,11 @@ impl Set<'_> {
pub fn image(self, image: ImageData) -> Result<(), Error> {
self.platform.image(image)
}
/// Completes the "set" operation by placing a list of file paths onto the clipboard.
pub fn file_list(self, file_list: &[impl AsRef<Path>]) -> Result<(), Error> {
self.platform.file_list(file_list)
}
}
/// A builder for an operation that clears the data from the clipboard.
@ -258,7 +313,7 @@ mod tests {
match ctx.get_text() {
Ok(text) => assert!(text.is_empty()),
Err(Error::ContentNotAvailable) => {}
Err(e) => panic!("unexpected error: {}", e),
Err(e) => panic!("unexpected error: {e}"),
};
// confirm it is OK to clear when already empty.
@ -273,7 +328,7 @@ mod tests {
match ctx.get_text() {
Ok(text) => assert!(text.is_empty()),
Err(Error::ContentNotAvailable) => {}
Err(e) => panic!("unexpected error: {}", e),
Err(e) => panic!("unexpected error: {e}"),
};
}
{
@ -285,6 +340,37 @@ mod tests {
ctx.set_html(html, Some(alt_text)).unwrap();
assert_eq!(ctx.get_text().unwrap(), alt_text);
}
{
let mut ctx = Clipboard::new().unwrap();
let html = "<b>hello</b> <i>world</i>!";
ctx.set().html(html, None).unwrap();
if cfg!(target_os = "macos") {
// Copying HTML on macOS adds wrapper content to work around
// historical platform bugs. We control this wrapper, so we are
// able to check that the full user data still appears and at what
// position in the final copy contents.
let content = ctx.get().html().unwrap();
assert!(content.ends_with(&format!("{html}</body></html>")));
} else {
assert_eq!(ctx.get().html().unwrap(), html);
}
}
{
let mut ctx = Clipboard::new().unwrap();
let this_dir = env!("CARGO_MANIFEST_DIR");
let paths = &[
PathBuf::from(this_dir).join("README.md"),
PathBuf::from(this_dir).join("Cargo.toml"),
];
ctx.set().file_list(paths).unwrap();
assert_eq!(ctx.get().file_list().unwrap().as_slice(), paths);
}
#[cfg(feature = "image-data")]
{
let mut ctx = Clipboard::new().unwrap();
@ -304,7 +390,7 @@ mod tests {
ctx.set_text("clipboard test").unwrap();
assert!(matches!(ctx.get_image(), Err(Error::ContentNotAvailable)));
// Test if we get the same image that we put onto the clibboard
// Test if we get the same image that we put onto the clipboard
ctx.set_image(img_data.clone()).unwrap();
let got = ctx.get_image().unwrap();
assert_eq!(img_data.bytes, got.bytes);

View file

@ -1,19 +1,29 @@
use std::borrow::Cow;
use std::{
borrow::Cow,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
time::Instant,
};
#[cfg(feature = "wayland-data-control")]
use log::{trace, warn};
use percent_encoding::{percent_decode, percent_encode, AsciiSet, CONTROLS};
#[cfg(feature = "image-data")]
use crate::ImageData;
use crate::{common::private, Error};
// Magic strings used in `Set::exclude_from_history()` on linux
const KDE_EXCLUSION_MIME: &str = "x-kde-passwordManagerHint";
const KDE_EXCLUSION_HINT: &[u8] = b"secret";
mod x11;
#[cfg(feature = "wayland-data-control")]
mod wayland;
fn into_unknown<E: std::fmt::Display>(error: E) -> Error {
Error::Unknown { description: format!("{}", error) }
Error::Unknown { description: error.to_string() }
}
#[cfg(feature = "image-data")]
@ -31,13 +41,53 @@ fn encode_as_png(image: &ImageData) -> Result<Vec<u8>, Error> {
image.bytes.as_ref(),
image.width as u32,
image.height as u32,
image::ColorType::Rgba8,
image::ExtendedColorType::Rgba8,
)
.map_err(|_| Error::ConversionFailure)?;
Ok(png_bytes)
}
fn paths_from_uri_list(uri_list: Vec<u8>) -> Vec<PathBuf> {
uri_list
.split(|char| *char == b'\n')
.filter_map(|line| line.strip_prefix(b"file://"))
.filter_map(|s| percent_decode(s).decode_utf8().ok())
.map(|decoded| PathBuf::from(decoded.as_ref()))
.collect()
}
fn paths_to_uri_list(file_list: &[impl AsRef<Path>]) -> Result<String, Error> {
// The characters that require encoding, which includes £ and € but they can't be added to the set.
const ASCII_SET: &AsciiSet = &CONTROLS
.add(b'#')
.add(b';')
.add(b'?')
.add(b'[')
.add(b']')
.add(b' ')
.add(b'\"')
.add(b'%')
.add(b'<')
.add(b'>')
.add(b'\\')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
file_list
.iter()
.filter_map(|path| {
path.as_ref().canonicalize().ok().map(|path| {
format!("file://{}", percent_encode(path.as_os_str().as_bytes(), ASCII_SET))
})
})
.reduce(|uri_list, uri| uri_list + "\n" + &uri)
.ok_or(Error::ConversionFailure)
}
/// Clipboard selection
///
/// Linux has a concept of clipboard "selections" which tend to be used in different contexts. This
@ -122,6 +172,22 @@ impl<'clipboard> Get<'clipboard> {
Clipboard::WlDataControl(clipboard) => clipboard.get_image(self.selection),
}
}
pub(crate) fn html(self) -> Result<String, Error> {
match self.clipboard {
Clipboard::X11(clipboard) => clipboard.get_html(self.selection),
#[cfg(feature = "wayland-data-control")]
Clipboard::WlDataControl(clipboard) => clipboard.get_html(self.selection),
}
}
pub(crate) fn file_list(self) -> Result<Vec<PathBuf>, Error> {
match self.clipboard {
Clipboard::X11(clipboard) => clipboard.get_file_list(self.selection),
#[cfg(feature = "wayland-data-control")]
Clipboard::WlDataControl(clipboard) => clipboard.get_file_list(self.selection),
}
}
}
/// Linux-specific extensions to the [`Get`](super::Get) builder.
@ -140,39 +206,93 @@ impl GetExtLinux for crate::Get<'_> {
}
}
/// Configuration on how long to wait for a new X11 copy event is emitted.
#[derive(Default)]
pub(crate) enum WaitConfig {
/// Waits until the given [`Instant`] has reached.
Until(Instant),
/// Waits forever until a new event is reached.
Forever,
/// It shouldn't wait.
#[default]
None,
}
pub(crate) struct Set<'clipboard> {
clipboard: &'clipboard mut Clipboard,
wait: bool,
wait: WaitConfig,
selection: LinuxClipboardKind,
exclude_from_history: bool,
}
impl<'clipboard> Set<'clipboard> {
pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
Self { clipboard, wait: false, selection: LinuxClipboardKind::Clipboard }
Self {
clipboard,
wait: WaitConfig::default(),
selection: LinuxClipboardKind::Clipboard,
exclude_from_history: false,
}
}
pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> {
match self.clipboard {
Clipboard::X11(clipboard) => clipboard.set_text(text, self.selection, self.wait),
Clipboard::X11(clipboard) => {
clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history)
}
#[cfg(feature = "wayland-data-control")]
Clipboard::WlDataControl(clipboard) => clipboard.set_text(text, self.selection, self.wait),
Clipboard::WlDataControl(clipboard) => {
clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history)
}
}
}
pub(crate) fn html(self, html: Cow<'_, str>, alt: Option<Cow<'_, str>>) -> Result<(), Error> {
match self.clipboard {
Clipboard::X11(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait),
Clipboard::X11(clipboard) => {
clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history)
}
#[cfg(feature = "wayland-data-control")]
Clipboard::WlDataControl(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait),
Clipboard::WlDataControl(clipboard) => {
clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history)
}
}
}
#[cfg(feature = "image-data")]
pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> {
match self.clipboard {
Clipboard::X11(clipboard) => clipboard.set_image(image, self.selection, self.wait),
Clipboard::X11(clipboard) => {
clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history)
}
#[cfg(feature = "wayland-data-control")]
Clipboard::WlDataControl(clipboard) => clipboard.set_image(image, self.selection, self.wait),
Clipboard::WlDataControl(clipboard) => {
clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history)
}
}
}
pub(crate) fn file_list(self, file_list: &[impl AsRef<Path>]) -> Result<(), Error> {
match self.clipboard {
Clipboard::X11(clipboard) => clipboard.set_file_list(
file_list,
self.selection,
self.wait,
self.exclude_from_history,
),
#[cfg(feature = "wayland-data-control")]
Clipboard::WlDataControl(clipboard) => clipboard.set_file_list(
file_list,
self.selection,
self.wait,
self.exclude_from_history,
),
}
}
}
@ -206,6 +326,16 @@ pub trait SetExtLinux: private::Sealed {
/// [daemonize example]: https://github.com/1Password/arboard/blob/master/examples/daemonize.rs
fn wait(self) -> Self;
/// Whether or not to wait for the clipboard's content to be replaced after setting it. This waits until the
/// `deadline` has exceeded.
///
/// This is useful for short-lived programs so it won't block until new contents on the clipboard
/// were added.
///
/// Note: this is a superset of [`wait()`][SetExtLinux::wait] and will overwrite any state
/// that was previously set using it.
fn wait_until(self, deadline: Instant) -> Self;
/// Sets the clipboard the operation will store its data to.
///
/// If wayland support is enabled and available, attempting to use the Secondary clipboard will
@ -227,11 +357,18 @@ pub trait SetExtLinux: private::Sealed {
/// # }
/// ```
fn clipboard(self, selection: LinuxClipboardKind) -> Self;
/// Excludes the data which will be set on the clipboard from being added to
/// the desktop clipboard managers' histories by adding the MIME-Type `x-kde-passwordMangagerHint`
/// to the clipboard's selection data.
///
/// This is the most widely adopted convention on Linux.
fn exclude_from_history(self) -> Self;
}
impl SetExtLinux for crate::Set<'_> {
fn wait(mut self) -> Self {
self.platform.wait = true;
self.platform.wait = WaitConfig::Forever;
self
}
@ -239,6 +376,16 @@ impl SetExtLinux for crate::Set<'_> {
self.platform.selection = selection;
self
}
fn wait_until(mut self, deadline: Instant) -> Self {
self.platform.wait = WaitConfig::Until(deadline);
self
}
fn exclude_from_history(mut self) -> Self {
self.platform.exclude_from_history = true;
self
}
}
pub(crate) struct Clear<'clipboard> {
@ -255,10 +402,11 @@ impl<'clipboard> Clear<'clipboard> {
}
fn clear_inner(self, selection: LinuxClipboardKind) -> Result<(), Error> {
let mut set = Set::new(self.clipboard);
set.selection = selection;
set.text(Cow::Borrowed(""))
match self.clipboard {
Clipboard::X11(clipboard) => clipboard.clear(selection),
#[cfg(feature = "wayland-data-control")]
Clipboard::WlDataControl(clipboard) => clipboard.clear(selection),
}
}
}
@ -269,7 +417,7 @@ pub trait ClearExtLinux: private::Sealed {
/// ### Example
///
/// ```no_run
/// # use arboard::{ Clipboard, LinuxClipboardKind, ClearExtLinux, Error };
/// # use arboard::{Clipboard, LinuxClipboardKind, ClearExtLinux, Error};
/// # fn main() -> Result<(), Error> {
/// let mut clipboard = Clipboard::new()?;
///
@ -290,3 +438,28 @@ impl ClearExtLinux for crate::Clear<'_> {
self.platform.clear_inner(selection)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decoding_uri_list() {
// Test that paths_from_uri_list correctly decodes
// differents percent encoded characters
let file_list = [
"file:///tmp/bar.log",
"file:///tmp/test%5C.txt",
"file:///tmp/foo%3F.png",
"file:///tmp/white%20space.txt",
];
let paths = vec![
PathBuf::from("/tmp/bar.log"),
PathBuf::from("/tmp/test\\.txt"),
PathBuf::from("/tmp/foo?.png"),
PathBuf::from("/tmp/white space.txt"),
];
assert_eq!(paths_from_uri_list(file_list.join("\n").into()), paths);
}
}

View file

@ -1,6 +1,8 @@
use std::borrow::Cow;
use std::convert::TryInto;
use std::io::Read;
use std::{
borrow::Cow,
io::Read,
path::{Path, PathBuf},
};
use wl_clipboard_rs::{
copy::{self, Error as CopyError, MimeSource, MimeType, Options, Source},
@ -10,7 +12,10 @@ use wl_clipboard_rs::{
#[cfg(feature = "image-data")]
use super::encode_as_png;
use super::{into_unknown, LinuxClipboardKind};
use super::{
into_unknown, paths_from_uri_list, paths_to_uri_list, LinuxClipboardKind, WaitConfig,
KDE_EXCLUSION_HINT, KDE_EXCLUSION_MIME,
};
use crate::common::Error;
#[cfg(feature = "image-data")]
use crate::common::ImageData;
@ -18,6 +23,8 @@ use crate::common::ImageData;
#[cfg(feature = "image-data")]
const MIME_PNG: &str = "image/png";
const MIME_URI: &str = "text/uri-list";
pub(crate) struct Clipboard {}
impl TryInto<copy::ClipboardType> for LinuxClipboardKind {
@ -44,52 +51,97 @@ impl TryInto<paste::ClipboardType> for LinuxClipboardKind {
}
}
fn add_clipboard_exclusions(exclude_from_history: bool, sources: &mut Vec<MimeSource>) {
if exclude_from_history {
sources.push(MimeSource {
source: Source::Bytes(Box::from(KDE_EXCLUSION_HINT)),
mime_type: MimeType::Specific(String::from(KDE_EXCLUSION_MIME)),
});
}
}
fn handle_copy_error(e: copy::Error) -> Error {
match e {
CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported,
other => into_unknown(other),
}
}
fn handle_paste_error(e: paste::Error) -> Error {
match e {
PasteError::PrimarySelectionUnsupported => Error::ClipboardNotSupported,
other => into_unknown(other),
}
}
fn handle_clipboard_read<T, F: FnOnce(Vec<u8>) -> Result<T, Error>>(
selection: LinuxClipboardKind,
mime: paste::MimeType,
into_requested_data: F,
) -> Result<T, Error> {
let result = get_contents(selection.try_into()?, Seat::Unspecified, mime);
match result {
Ok((mut pipe, _)) => {
let mut buffer = vec![];
pipe.read_to_end(&mut buffer).map_err(into_unknown)?;
into_requested_data(buffer)
}
Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => {
Err(Error::ContentNotAvailable)
}
Err(err) => Err(handle_paste_error(err)),
}
}
impl Clipboard {
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn new() -> Result<Self, Error> {
// Check if it's possible to communicate with the wayland compositor
if let Err(e) = is_primary_selection_supported() {
return Err(into_unknown(e));
match is_primary_selection_supported() {
// We don't care if the primary clipboard is supported or not, `wl-clipboard-rs` will fail
// if not and we don't want to duplicate more of their logic.
Ok(_) => Ok(Self {}),
Err(e) => Err(into_unknown(e)),
}
Ok(Self {})
}
pub(crate) fn clear(&mut self, selection: LinuxClipboardKind) -> Result<(), Error> {
let selection = selection.try_into()?;
copy::clear(selection, copy::Seat::All).map_err(handle_copy_error)
}
pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result<String, Error> {
use wl_clipboard_rs::paste::MimeType;
let result = get_contents(selection.try_into()?, Seat::Unspecified, MimeType::Text);
match result {
Ok((mut pipe, _)) => {
let mut contents = vec![];
pipe.read_to_end(&mut contents).map_err(into_unknown)?;
String::from_utf8(contents).map_err(|_| Error::ConversionFailure)
}
Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => {
Err(Error::ContentNotAvailable)
}
Err(PasteError::PrimarySelectionUnsupported) => Err(Error::ClipboardNotSupported),
Err(err) => Err(Error::Unknown { description: format!("{}", err) }),
}
handle_clipboard_read(selection, paste::MimeType::Text, |contents| {
String::from_utf8(contents).map_err(|_| Error::ConversionFailure)
})
}
pub(crate) fn set_text(
&self,
text: Cow<'_, str>,
selection: LinuxClipboardKind,
wait: bool,
wait: WaitConfig,
exclude_from_history: bool,
) -> Result<(), Error> {
let mut opts = Options::new();
opts.foreground(wait);
opts.foreground(matches!(wait, WaitConfig::Forever));
opts.clipboard(selection.try_into()?);
let source = Source::Bytes(text.into_owned().into_bytes().into_boxed_slice());
opts.copy(source, MimeType::Text).map_err(|e| match e {
CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported,
other => into_unknown(other),
})?;
Ok(())
let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
sources.push(MimeSource {
source: Source::Bytes(text.into_owned().into_bytes().into_boxed_slice()),
mime_type: MimeType::Text,
});
add_clipboard_exclusions(exclude_from_history, &mut sources);
opts.copy_multi(sources).map_err(handle_copy_error)
}
pub(crate) fn get_html(&mut self, selection: LinuxClipboardKind) -> Result<String, Error> {
handle_clipboard_read(selection, paste::MimeType::Specific("text/html"), |contents| {
String::from_utf8(contents).map_err(|_| Error::ConversionFailure)
})
}
pub(crate) fn set_html(
@ -97,29 +149,36 @@ impl Clipboard {
html: Cow<'_, str>,
alt: Option<Cow<'_, str>>,
selection: LinuxClipboardKind,
wait: bool,
wait: WaitConfig,
exclude_from_history: bool,
) -> Result<(), Error> {
let html_mime = MimeType::Specific(String::from("text/html"));
let mut opts = Options::new();
opts.foreground(wait);
opts.foreground(matches!(wait, WaitConfig::Forever));
opts.clipboard(selection.try_into()?);
let html_source = Source::Bytes(html.into_owned().into_bytes().into_boxed_slice());
match alt {
Some(alt_text) => {
let alt_source =
Source::Bytes(alt_text.into_owned().into_bytes().into_boxed_slice());
opts.copy_multi(vec![
MimeSource { source: alt_source, mime_type: MimeType::Text },
MimeSource { source: html_source, mime_type: html_mime },
])
}
None => opts.copy(html_source, html_mime),
let mut sources = {
let cap = [true, alt.is_some(), exclude_from_history]
.map(|v| usize::from(v as u8))
.iter()
.sum();
Vec::with_capacity(cap)
};
if let Some(alt) = alt {
sources.push(MimeSource {
source: Source::Bytes(alt.into_owned().into_bytes().into_boxed_slice()),
mime_type: MimeType::Text,
});
}
.map_err(|e| match e {
CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported,
other => into_unknown(other),
})?;
Ok(())
sources.push(MimeSource {
source: Source::Bytes(html.into_owned().into_bytes().into_boxed_slice()),
mime_type: MimeType::Specific(String::from("text/html")),
});
add_clipboard_exclusions(exclude_from_history, &mut sources);
opts.copy_multi(sources).map_err(handle_copy_error)
}
#[cfg(feature = "image-data")]
@ -128,34 +187,21 @@ impl Clipboard {
selection: LinuxClipboardKind,
) -> Result<ImageData<'static>, Error> {
use std::io::Cursor;
use wl_clipboard_rs::paste::MimeType;
let result =
get_contents(selection.try_into()?, Seat::Unspecified, MimeType::Specific(MIME_PNG));
match result {
Ok((mut pipe, _mime_type)) => {
let mut buffer = vec![];
pipe.read_to_end(&mut buffer).map_err(into_unknown)?;
let image = image::io::Reader::new(Cursor::new(buffer))
.with_guessed_format()
.map_err(|_| Error::ConversionFailure)?
.decode()
.map_err(|_| Error::ConversionFailure)?;
let image = image.into_rgba8();
handle_clipboard_read(selection, paste::MimeType::Specific(MIME_PNG), |buffer| {
let image = image::io::Reader::new(Cursor::new(buffer))
.with_guessed_format()
.map_err(|_| Error::ConversionFailure)?
.decode()
.map_err(|_| Error::ConversionFailure)?;
let image = image.into_rgba8();
Ok(ImageData {
width: image.width() as usize,
height: image.height() as usize,
bytes: image.into_raw().into(),
})
}
Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => {
Err(Error::ContentNotAvailable)
}
Err(err) => Err(Error::Unknown { description: format!("{}", err) }),
}
Ok(ImageData {
width: image.width() as usize,
height: image.height() as usize,
bytes: image.into_raw().into(),
})
})
}
#[cfg(feature = "image-data")]
@ -163,14 +209,57 @@ impl Clipboard {
&mut self,
image: ImageData,
selection: LinuxClipboardKind,
wait: bool,
wait: WaitConfig,
exclude_from_history: bool,
) -> Result<(), Error> {
let image = encode_as_png(&image)?;
let mut opts = Options::new();
opts.foreground(wait);
opts.foreground(matches!(wait, WaitConfig::Forever));
opts.clipboard(selection.try_into()?);
let source = Source::Bytes(image.into());
opts.copy(source, MimeType::Specific(MIME_PNG.into())).map_err(into_unknown)?;
Ok(())
let image = encode_as_png(&image)?;
let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
sources.push(MimeSource {
source: Source::Bytes(image.into()),
mime_type: MimeType::Specific(String::from(MIME_PNG)),
});
add_clipboard_exclusions(exclude_from_history, &mut sources);
opts.copy_multi(sources).map_err(handle_copy_error)
}
pub(crate) fn get_file_list(
&mut self,
selection: LinuxClipboardKind,
) -> Result<Vec<PathBuf>, Error> {
handle_clipboard_read(selection, paste::MimeType::Specific(MIME_URI), |contents| {
Ok(paths_from_uri_list(contents))
})
}
pub(crate) fn set_file_list(
&self,
file_list: &[impl AsRef<Path>],
selection: LinuxClipboardKind,
wait: WaitConfig,
exclude_from_history: bool,
) -> Result<(), Error> {
let files = paths_to_uri_list(file_list)?;
let mut opts = Options::new();
opts.foreground(matches!(wait, WaitConfig::Forever));
opts.clipboard(selection.try_into()?);
let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
sources.push(MimeSource {
source: Source::Bytes(files.into_bytes().into_boxed_slice()),
mime_type: MimeType::Specific(String::from(MIME_URI)),
});
add_clipboard_exclusions(exclude_from_history, &mut sources);
opts.copy_multi(sources).map_err(handle_copy_error)
}
}

View file

@ -16,14 +16,14 @@ use std::{
borrow::Cow,
cell::RefCell,
collections::{hash_map::Entry, HashMap},
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
Arc,
},
thread::{self, JoinHandle},
thread::JoinHandle,
thread_local,
time::{Duration, Instant},
usize,
};
use log::{error, trace, warn};
@ -45,7 +45,10 @@ use x11rb::{
#[cfg(feature = "image-data")]
use super::encode_as_png;
use super::{into_unknown, LinuxClipboardKind};
use super::{
into_unknown, paths_from_uri_list, paths_to_uri_list, LinuxClipboardKind, WaitConfig,
KDE_EXCLUSION_HINT, KDE_EXCLUSION_MIME,
};
#[cfg(feature = "image-data")]
use crate::ImageData;
use crate::{common::ScopeGuard, Error};
@ -78,8 +81,10 @@ x11rb::atom_manager! {
TEXT_MIME_UNKNOWN: b"text/plain",
HTML: b"text/html",
URI_LIST: b"text/uri-list",
PNG_MIME: b"image/png",
X_KDE_PASSWORDMANAGERHINT: KDE_EXCLUSION_MIME.as_bytes(),
// This is just some random name for the property on our window, into which
// the clipboard owner writes the data we requested.
@ -134,19 +139,11 @@ struct Inner {
impl XContext {
fn new() -> Result<Self> {
// create a new connection to an X11 server
// with a timeout on connecting to the socket in case of hangage
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send(RustConnection::connect(None)).ok(); // disregard error sending on channel as main thread has timed out.
});
let patient_conn = rx.recv_timeout(SHORT_TIMEOUT_DUR).map_err(into_unknown)?;
let (conn, screen_num): (RustConnection, _) = patient_conn.map_err(into_unknown)?;
let screen = conn
.setup()
.roots
.get(screen_num)
.ok_or(Error::Unknown { description: String::from("no screen found") })?;
let (conn, screen_num): (RustConnection, _) =
RustConnection::connect(None).map_err(|_| {
Error::unknown("X11 server connection timed out because it was unreachable")
})?;
let screen = conn.setup().roots.get(screen_num).ok_or(Error::unknown("no screen found"))?;
let win_id = conn.generate_id().map_err(into_unknown)?;
let event_mask =
@ -181,8 +178,9 @@ impl XContext {
#[derive(Default)]
struct Selection {
data: RwLock<Option<Vec<ClipboardData>>>,
/// Mutex around nothing to use with the below condvar.
mutex: Mutex<()>,
/// Mutex around when this selection was last changed by us
/// for both use with the below condvar and logging.
mutex: Mutex<Option<Instant>>,
/// A condvar that is notified when the contents of this clipboard are changed.
///
/// This is associated with `Self::mutex`.
@ -221,48 +219,72 @@ impl Inner {
})
}
/// Performs a "clear" operation on the clipboard, which is implemented by
/// relinquishing the selection to revert its owner to `None`. This gracefully
/// and comformly informs the X server and any clipboard managers that the
/// data was no longer valid and won't be offered from our window anymore.
///
/// See `ask_clipboard_manager_to_request_our_data` for more details on why
/// this is important and specification references.
fn clear(&self, selection: LinuxClipboardKind) -> Result<()> {
let selection = self.atom_of(selection);
self.server
.conn
.set_selection_owner(NONE, selection, Time::CURRENT_TIME)
.map_err(into_unknown)?;
self.server.conn.flush().map_err(into_unknown)
}
fn write(
&self,
data: Vec<ClipboardData>,
selection: LinuxClipboardKind,
wait: bool,
clipboard_selection: LinuxClipboardKind,
wait: WaitConfig,
) -> Result<()> {
if self.serve_stopped.load(Ordering::Relaxed) {
return Err(Error::Unknown {
description: "The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)".into()
});
return Err(Error::unknown("The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)"));
}
let server_win = self.server.win_id;
// Just setting the data, and the `serve_requests` will take care of the rest.
let selection = self.selection_of(clipboard_selection);
let mut data_guard = selection.data.write();
*data_guard = Some(data);
// ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data
// changes.
self.server
.conn
.set_selection_owner(server_win, self.atom_of(selection), Time::CURRENT_TIME)
.set_selection_owner(server_win, self.atom_of(clipboard_selection), Time::CURRENT_TIME)
.map_err(|_| Error::ClipboardOccupied)?;
self.server.conn.flush().map_err(into_unknown)?;
// Just setting the data, and the `serve_requests` will take care of the rest.
let selection = self.selection_of(selection);
let mut data_guard = selection.data.write();
*data_guard = Some(data);
// Lock the mutex to both ensure that no wakers of `data_changed` can wake us between
// dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other
// threads in that position.
let mut guard = selection.mutex.lock();
// Record the time we modify the selection.
*guard = Some(Instant::now());
// Notify any existing waiting threads that we have changed the data in the selection.
// It is important that the mutex is locked to prevent this notification getting lost.
selection.data_changed.notify_all();
if wait {
drop(data_guard);
match wait {
WaitConfig::None => {}
WaitConfig::Forever => {
drop(data_guard);
selection.data_changed.wait(&mut guard);
}
// Wait for the clipboard's content to be changed.
selection.data_changed.wait(&mut guard);
WaitConfig::Until(deadline) => {
drop(data_guard);
selection.data_changed.wait_until(&mut guard, deadline);
}
}
Ok(())
@ -524,9 +546,7 @@ impl Inner {
Ok(ReadSelNotifyResult::IncrStarted)
} else {
// this should never happen, we have sent a request only for supported types
Err(Error::Unknown {
description: String::from("incorrect type received from clipboard"),
})
Err(Error::unknown("incorrect type received from clipboard"))
}
}
@ -582,11 +602,13 @@ impl Inner {
// we are asked for a list of supported conversion targets
if event.target == self.atoms.TARGETS {
trace!("Handling TARGETS, dst property is {}", self.atom_name_dbg(event.property));
let mut targets = Vec::with_capacity(10);
targets.push(self.atoms.TARGETS);
targets.push(self.atoms.SAVE_TARGETS);
let data = self.selection_of(selection).data.read();
if let Some(data_list) = &*data {
let (data_targets, excluded) = if let Some(data_list) = &*data {
// Estimation based on current data types, plus the other UTF-8 ones, plus `SAVE_TARGETS`.
let mut targets = Vec::with_capacity(data_list.len() + 3);
let mut excluded = false;
for data in data_list {
targets.push(data.format);
if data.format == self.atoms.UTF8_STRING {
@ -595,8 +617,32 @@ impl Inner {
targets.push(self.atoms.UTF8_MIME_0);
targets.push(self.atoms.UTF8_MIME_1);
}
if data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT {
excluded = true;
}
}
(targets, excluded)
} else {
// If there's no data, we advertise an empty list of targets.
(Vec::with_capacity(2), false)
};
let mut targets = data_targets;
targets.push(self.atoms.TARGETS);
// NB: `SAVE_TARGETS` in this context is a marker atom which infomrs the clipboard manager
// we support this operation and _may_ use it in the future. To try and keep the manager's
// expectations/assumptions (if any) about when we will invoke this handoff, we go ahead and
// skip advertising support for the save operation entirely when the data was marked as
// sensitive.
//
// Note that even if we don't advertise it, some managers may respond to it anyways so this is
// only half of exclusion handling. See `ask_clipboard_manager_to_request_our_data` for more.
if !excluded {
targets.push(self.atoms.SAVE_TARGETS);
}
self.server
.conn
.change_property32(
@ -669,13 +715,48 @@ impl Inner {
return Ok(());
}
if !self.is_owner(LinuxClipboardKind::Clipboard)? {
// Per the `ClipboardManager` specification, only the `CLIPBOARD` target is
// to be saved from other X clients, so if the caller set the `Primary` (or `Secondary`) clipboard,
// we wouldn't expect any clipboard manager to save that anyway.
let selection = LinuxClipboardKind::Clipboard;
if !self.is_owner(selection)? {
// We are not owning the clipboard, nothing to do.
return Ok(());
}
if self.selection_of(LinuxClipboardKind::Clipboard).data.read().is_none() {
// If we don't have any data, there's nothing to do.
return Ok(());
match &*self.selection_of(selection).data.read() {
Some(data) => {
// If the data we are serving intended to be excluded, then don't bother asking the clipboard
// manager to save it. This is for several reasons:
// 1. Its counter-intuitive because the caller asked for this data to be minimally retained.
// 2. Regardless of if `SAVE_TARGETS` was advertised, we have to assume the manager may be saving history
// in a more proactive way and that would also be entirely dependent on it seeing the exclusion MIME before this.
// 3. Due to varying behavior in clipboard managers (some save prior to `SAVE_TARGETS`), it may just
// generate unnessecary warning logs in our handoff path even when we know a well-behaving manager isn't
// trying to save our sensitive data and that is misleading to users.
if data.iter().any(|data| data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT) {
// This step is the most important. Without it, some clipboard managers may think that our process
// crashed since the X window is destroyed without changing the selection owner first and try to save data.
//
// While this shouldn't need to happen based only on ICCCM 2.3.1 ("Voluntarily Giving Up Selection Ownership"),
// its documentation that destorying the owner window or terminating also reverts the owner to `None` doesn't
// reflect how desktop environment's X servers work in reality.
//
// By removing the owner, the manager doesn't think it needs to pick up our window's data serving once
// its destroyed and cleanly lets the data disappear based off the previously advertised exclusion hint.
if let Err(e) = self.clear(selection) {
warn!("failed to release sensitive data's clipboard ownership: {e}; it may end up persisted!");
// This is still not an error because we werent going to handoff anything to the manager.
}
return Ok(());
}
}
None => {
// If we don't have any data, there's nothing to do.
return Ok(());
}
}
// It's important that we lock the state before sending the request
@ -711,9 +792,7 @@ impl Inner {
return Ok(());
}
Err(Error::Unknown {
description: "The handover was not finished and the condvar didn't time out, yet the condvar wait ended. This should be unreachable.".into()
})
unreachable!("This is a bug! The handover was not finished and the condvar didn't time out, yet the condvar wait ended.")
}
}
@ -722,7 +801,7 @@ fn serve_requests(context: Arc<Inner>) -> Result<(), Box<dyn std::error::Error>>
log::trace!("Finishing clipboard manager handover.");
*handover_state = ManagerHandoverState::Finished;
// Not sure if unlocking the mutext is necessary here but better safe than sorry.
// Not sure if unlocking the mutex is necessary here but better safe than sorry.
drop(handover_state);
clip.handover_cv.notify_all();
@ -771,7 +850,10 @@ fn serve_requests(context: Arc<Inner>) -> Result<(), Box<dyn std::error::Error>>
context.atom_name_dbg(event.target),
);
// Someone is requesting the clipboard content from us.
context.handle_selection_request(event).map_err(into_unknown)?;
if let Err(e) = context.handle_selection_request(event) {
error!("Failed to handle selection request: {e}");
continue;
}
// if we are in the progress of saving to the clipboard manager
// make sure we save that we have finished writing
@ -850,6 +932,19 @@ impl Clipboard {
Ok(Self { inner: ctx })
}
fn add_clipboard_exclusions(&self, exclude_from_history: bool, data: &mut Vec<ClipboardData>) {
if exclude_from_history {
data.push(ClipboardData {
bytes: KDE_EXCLUSION_HINT.to_vec(),
format: self.inner.atoms.X_KDE_PASSWORDMANAGERHINT,
})
}
}
pub(crate) fn clear(&self, selection: LinuxClipboardKind) -> Result<()> {
self.inner.clear(selection)
}
pub(crate) fn get_text(&self, selection: LinuxClipboardKind) -> Result<String> {
let formats = [
self.inner.atoms.UTF8_STRING,
@ -873,23 +968,42 @@ impl Clipboard {
&self,
message: Cow<'_, str>,
selection: LinuxClipboardKind,
wait: bool,
wait: WaitConfig,
exclude_from_history: bool,
) -> Result<()> {
let data = vec![ClipboardData {
let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
data.push(ClipboardData {
bytes: message.into_owned().into_bytes(),
format: self.inner.atoms.UTF8_STRING,
}];
});
self.add_clipboard_exclusions(exclude_from_history, &mut data);
self.inner.write(data, selection, wait)
}
pub(crate) fn get_html(&self, selection: LinuxClipboardKind) -> Result<String> {
let formats = [self.inner.atoms.HTML];
let result = self.inner.read(&formats, selection)?;
String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)
}
pub(crate) fn set_html(
&self,
html: Cow<'_, str>,
alt: Option<Cow<'_, str>>,
selection: LinuxClipboardKind,
wait: bool,
wait: WaitConfig,
exclude_from_history: bool,
) -> Result<()> {
let mut data = vec![];
let mut data = {
let cap = [true, alt.is_some(), exclude_from_history]
.map(|v| usize::from(v as u8))
.iter()
.sum();
Vec::with_capacity(cap)
};
if let Some(alt_text) = alt {
data.push(ClipboardData {
bytes: alt_text.into_owned().into_bytes(),
@ -900,6 +1014,9 @@ impl Clipboard {
bytes: html.into_owned().into_bytes(),
format: self.inner.atoms.HTML,
});
self.add_clipboard_exclusions(exclude_from_history, &mut data);
self.inner.write(data, selection, wait)
}
@ -926,10 +1043,38 @@ impl Clipboard {
&self,
image: ImageData,
selection: LinuxClipboardKind,
wait: bool,
wait: WaitConfig,
exclude_from_history: bool,
) -> Result<()> {
let encoded = encode_as_png(&image)?;
let data = vec![ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME }];
let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
data.push(ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME });
self.add_clipboard_exclusions(exclude_from_history, &mut data);
self.inner.write(data, selection, wait)
}
pub(crate) fn get_file_list(&self, selection: LinuxClipboardKind) -> Result<Vec<PathBuf>> {
let result = self.inner.read(&[self.inner.atoms.URI_LIST], selection)?;
Ok(paths_from_uri_list(result.bytes))
}
pub(crate) fn set_file_list(
&self,
file_list: &[impl AsRef<Path>],
selection: LinuxClipboardKind,
wait: WaitConfig,
exclude_from_history: bool,
) -> Result<()> {
let files = paths_to_uri_list(file_list)?;
let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
data.push(ClipboardData { bytes: files.into_bytes(), format: self.inner.atoms.URI_LIST });
self.add_clipboard_exclusions(exclude_from_history, &mut data);
self.inner.write(data, selection, wait)
}
}
@ -961,7 +1106,10 @@ impl Drop for Clipboard {
return;
}
if let Some(global_cb) = global_cb {
if let Err(e) = global_cb.server_handle.join() {
let GlobalClipboard { inner, server_handle } = global_cb;
drop(inner);
if let Err(e) = server_handle.join() {
// Let's try extracting the error message
let message;
if let Some(msg) = e.downcast_ref::<&'static str>() {
@ -980,6 +1128,49 @@ impl Drop for Clipboard {
error!("The clipboard server thread panicked.");
}
}
// By this point we've dropped the Global's reference to `Inner` and the background
// thread has exited which means it also dropped its reference. Therefore `self.inner` should
// be the last strong count.
//
// Note: The following is all best effort and is only for logging. Nothing is guaranteed to execute
// or log.
#[cfg(debug_assertions)]
if let Some(inner) = Arc::get_mut(&mut self.inner) {
use std::io::IsTerminal;
let mut change_timestamps = Vec::with_capacity(2);
let mut collect_changed = |sel: &mut Mutex<Option<Instant>>| {
if let Some(changed) = sel.get_mut() {
change_timestamps.push(*changed);
}
};
collect_changed(&mut inner.clipboard.mutex);
collect_changed(&mut inner.primary.mutex);
collect_changed(&mut inner.secondary.mutex);
change_timestamps.sort();
if let Some(last) = change_timestamps.last() {
let elapsed = last.elapsed().as_millis();
// This number has no meaning, its just a guess for how long
// might be reasonable to give a clipboard manager a chance to
// save contents based ~roughly on the handoff timeout.
if elapsed > 100 {
return;
}
// If the app isn't running in a terminal don't print, use log instead.
// Printing has a higher chance of being seen though, so its our default.
// Its also close enough to a `debug_assert!` that it shouldn't come across strange.
let msg = format!("Clipboard was dropped very quickly after writing ({elapsed}ms); clipboard managers may not have seen the contents\nConsider keeping `Clipboard` in more persistent state somewhere or keeping the contents alive longer using `SetLinuxExt` and/or threads.");
if std::io::stderr().is_terminal() {
eprintln!("{msg}");
} else {
log::warn!("{msg}");
}
}
}
}
}
}

View file

@ -14,4 +14,4 @@ pub use windows::*;
#[cfg(target_os = "macos")]
mod osx;
#[cfg(target_os = "macos")]
pub(crate) use osx::*;
pub use osx::*;

View file

@ -8,34 +8,25 @@ the Apache 2.0 or the MIT license at the licensee's choice. The terms
and conditions of the chosen license apply to this file.
*/
use crate::common::Error;
#[cfg(feature = "image-data")]
use crate::common::ImageData;
#[cfg(feature = "image-data")]
use core_graphics::{
base::{kCGBitmapByteOrderDefault, kCGImageAlphaLast, kCGRenderingIntentDefault, CGFloat},
color_space::CGColorSpace,
data_provider::{CGDataProvider, CustomData},
image::CGImage,
};
use objc::{
use crate::common::{private, Error};
use objc2::{
msg_send,
rc::autoreleasepool,
runtime::{Class, Object},
sel, sel_impl,
rc::{autoreleasepool, Retained},
runtime::ProtocolObject,
ClassType,
};
use objc2_app_kit::{
NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypeString,
NSPasteboardURLReadingFileURLsOnlyKey,
};
use objc2_foundation::{ns_string, NSArray, NSDictionary, NSNumber, NSString, NSURL};
use std::{
borrow::Cow,
panic::{RefUnwindSafe, UnwindSafe},
path::{Path, PathBuf},
};
use objc_foundation::{INSArray, INSFastEnumeration, INSString, NSArray, NSObject, NSString};
use objc_id::{Id, Owned};
use std::{borrow::Cow, ptr::NonNull};
// Required to bring NSPasteboard into the path of the class-resolver
#[link(name = "AppKit", kind = "framework")]
extern "C" {
static NSPasteboardTypeHTML: *const Object;
static NSPasteboardTypeString: *const Object;
#[cfg(feature = "image-data")]
static NSPasteboardTypeTIFF: *const Object;
}
/// Returns an NSImage object on success.
#[cfg(feature = "image-data")]
@ -43,80 +34,122 @@ fn image_from_pixels(
pixels: Vec<u8>,
width: usize,
height: usize,
) -> Result<Id<NSObject>, Box<dyn std::error::Error>> {
#[repr(C)]
#[derive(Copy, Clone)]
struct NSSize {
width: CGFloat,
height: CGFloat,
) -> Result<Retained<objc2_app_kit::NSImage>, Error> {
use objc2::AllocAnyThread;
use objc2_app_kit::NSImage;
use objc2_core_foundation::CGFloat;
use objc2_core_graphics::{
CGBitmapInfo, CGColorRenderingIntent, CGColorSpaceCreateDeviceRGB,
CGDataProviderCreateWithData, CGImageAlphaInfo, CGImageCreate,
};
use objc2_foundation::NSSize;
use std::{
ffi::c_void,
ptr::{self, NonNull},
};
unsafe extern "C-unwind" fn release(_info: *mut c_void, data: NonNull<c_void>, size: usize) {
let data = data.cast::<u8>();
let slice = NonNull::slice_from_raw_parts(data, size);
// SAFETY: This is the same slice that we got from `Box::into_raw`.
drop(unsafe { Box::from_raw(slice.as_ptr()) })
}
#[derive(Debug)]
struct PixelArray {
data: Vec<u8>,
let provider = {
let pixels = pixels.into_boxed_slice();
let len = pixels.len();
let pixels: *mut [u8] = Box::into_raw(pixels);
// Convert slice pointer to thin pointer.
let data_ptr = pixels.cast::<c_void>();
// SAFETY: The data pointer and length are valid.
// The info pointer can safely be NULL, we don't use it in the `release` callback.
unsafe { CGDataProviderCreateWithData(ptr::null_mut(), data_ptr, len, Some(release)) }
}
.unwrap();
impl CustomData for PixelArray {
unsafe fn ptr(&self) -> *const u8 {
self.data.as_ptr()
}
unsafe fn len(&self) -> usize {
self.data.len()
}
let colorspace = unsafe { CGColorSpaceCreateDeviceRGB() }.unwrap();
// XXX: If this returns an error, try running your application from the command line or
// use `Console.app`. For the later, make sure that before you start streaming log messages
// that Action -> `Include Info Messages` and Action -> `Include Debug Messages` are both
// enabled in the menubar. CoreGraphics will write debugging/error information to these places.
//
// - https://redsweater.com/blog/129/coregraphics-log-jam
// - https://github.com/1Password/arboard/issues/204
let cg_image = unsafe {
CGImageCreate(
width,
height,
8,
32,
4 * width,
Some(&colorspace),
CGBitmapInfo::ByteOrderDefault | CGBitmapInfo(CGImageAlphaInfo::Last.0),
Some(&provider),
ptr::null_mut(),
false,
CGColorRenderingIntent::RenderingIntentDefault,
)
}
.ok_or(Error::ConversionFailure)?;
let colorspace = CGColorSpace::create_device_rgb();
let pixel_data: Box<Box<dyn CustomData>> = Box::new(Box::new(PixelArray { data: pixels }));
let provider = unsafe { CGDataProvider::from_custom_data(pixel_data) };
let cg_image = CGImage::new(
width,
height,
8,
32,
4 * width,
&colorspace,
kCGBitmapByteOrderDefault | kCGImageAlphaLast,
&provider,
false,
kCGRenderingIntentDefault,
);
let size = NSSize { width: width as CGFloat, height: height as CGFloat };
let nsimage_class = objc::class!(NSImage);
// Take ownership of the newly allocated object, which has an existing retain count.
let image: Id<NSObject> = unsafe { Id::from_retained_ptr(msg_send![nsimage_class, alloc]) };
#[allow(clippy::let_unit_value)]
{
// Note: `initWithCGImage` expects a reference (`CGImageRef`), not an actual object.
let _: () = unsafe { msg_send![image, initWithCGImage: &*cg_image size:size] };
}
Ok(image)
Ok(unsafe { NSImage::initWithCGImage_size(NSImage::alloc(), &cg_image, size) })
}
pub(crate) struct Clipboard {
pasteboard: Id<Object>,
pasteboard: Retained<NSPasteboard>,
}
unsafe impl Send for Clipboard {}
unsafe impl Sync for Clipboard {}
impl UnwindSafe for Clipboard {}
impl RefUnwindSafe for Clipboard {}
impl Clipboard {
pub(crate) fn new() -> Result<Clipboard, Error> {
let cls = Class::get("NSPasteboard").expect("NSPasteboard not registered");
let pasteboard: *mut Object = unsafe { msg_send![cls, generalPasteboard] };
// Rust only supports 10.7+, while `generalPasteboard` first appeared
// in 10.0, so this should always be available.
//
// However, in some edge cases, like running under launchd (in some
// modes) as a daemon, the clipboard object may be unavailable, and
// then `generalPasteboard` will return NULL even though it's
// documented not to.
//
// Otherwise we'd just use `NSPasteboard::generalPasteboard()` here.
let pasteboard: Option<Retained<NSPasteboard>> =
unsafe { msg_send![NSPasteboard::class(), generalPasteboard] };
if !pasteboard.is_null() {
// SAFETY: `generalPasteboard` is not null and a valid object pointer.
let pasteboard: Id<Object> = unsafe { Id::from_ptr(pasteboard) };
if let Some(pasteboard) = pasteboard {
Ok(Clipboard { pasteboard })
} else {
// Rust only supports 10.7+, while `generalPasteboard` first appeared in 10.0, so this
// is unreachable in "normal apps". However in some edge cases, like running under
// launchd (in some modes) as a daemon, the clipboard object may be unavailable.
Err(Error::ClipboardNotSupported)
}
}
fn clear(&mut self) {
let _: usize = unsafe { msg_send![self.pasteboard, clearContents] };
unsafe { self.pasteboard.clearContents() };
}
fn string_from_type(&self, type_: &'static NSString) -> Result<String, Error> {
// XXX: There does not appear to be an alternative for obtaining text without the need for
// autorelease behavior.
autoreleasepool(|_| {
// XXX: We explicitly use `pasteboardItems` and not `stringForType` since the latter will concat
// multiple strings, if present, into one and return it instead of reading just the first which is `arboard`'s
// historical behavior.
let contents = unsafe { self.pasteboard.pasteboardItems() }
.ok_or_else(|| Error::unknown("NSPasteboard#pasteboardItems errored"))?;
for item in contents {
if let Some(string) = unsafe { item.stringForType(type_) } {
return Ok(string.to_string());
}
}
Err(Error::ContentNotAvailable)
})
}
// fn get_binary_contents(&mut self) -> Result<Option<ClipboardContent>, Box<dyn std::error::Error>> {
@ -171,68 +204,35 @@ impl Clipboard {
}
pub(crate) struct Get<'clipboard> {
pasteboard: &'clipboard Object,
clipboard: &'clipboard Clipboard,
}
impl<'clipboard> Get<'clipboard> {
pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
Self { pasteboard: &*clipboard.pasteboard }
Self { clipboard }
}
pub(crate) fn text(self) -> Result<String, Error> {
// XXX: There does not appear to be an alternative for obtaining text without the need for
// autorelease behavior.
autoreleasepool(|| {
// XXX: We explicitly use `pasteboardItems` and not `stringForType` since the latter will concat
// multiple strings, if present, into one and return it instead of reading just the first which is `arboard`'s
// historical behavior.
let contents: Option<NonNull<NSArray<NSObject>>> =
unsafe { msg_send![self.pasteboard, pasteboardItems] };
unsafe { self.clipboard.string_from_type(NSPasteboardTypeString) }
}
let contents = contents.map(|c| unsafe { c.as_ref() }).ok_or_else(|| {
Error::Unknown { description: String::from("NSPasteboard#pasteboardItems errored") }
})?;
for item in contents.enumerator() {
let maybe_str: Option<NonNull<NSString>> =
unsafe { msg_send![item, stringForType:NSPasteboardTypeString] };
match maybe_str {
Some(string) => {
let string: Id<NSString, Owned> = unsafe { Id::from_ptr(string.as_ptr()) };
return Ok(string.as_str().to_owned());
}
None => continue,
}
}
Err(Error::ContentNotAvailable)
})
pub(crate) fn html(self) -> Result<String, Error> {
unsafe { self.clipboard.string_from_type(NSPasteboardTypeHTML) }
}
#[cfg(feature = "image-data")]
pub(crate) fn image(self) -> Result<ImageData<'static>, Error> {
use objc_foundation::NSData;
use objc2_app_kit::NSPasteboardTypeTIFF;
use std::io::Cursor;
// XXX: There does not appear to be an alternative for obtaining images without the need for
// autorelease behavior.
let image = autoreleasepool(|| {
let obj: Option<NonNull<NSData>> =
unsafe { msg_send![self.pasteboard, dataForType: NSPasteboardTypeTIFF] };
let image = autoreleasepool(|_| {
let image_data = unsafe { self.clipboard.pasteboard.dataForType(NSPasteboardTypeTIFF) }
.ok_or(Error::ContentNotAvailable)?;
let image_data: Id<NSData> = if let Some(obj) = obj {
unsafe { Id::from_ptr(obj.as_ptr()) }
} else {
return Err(Error::ContentNotAvailable);
};
let data = unsafe {
let len: usize = msg_send![&*image_data, length];
let bytes: *const u8 = msg_send![&*image_data, bytes];
Cursor::new(std::slice::from_raw_parts(bytes, len))
};
// SAFETY: The data is not modified while in use here.
let data = Cursor::new(unsafe { image_data.as_bytes_unchecked() });
let reader = image::io::Reader::with_format(data, image::ImageFormat::Tiff);
reader.decode().map_err(|_| Error::ConversionFailure)
@ -247,29 +247,61 @@ impl<'clipboard> Get<'clipboard> {
bytes: rgba.into_raw().into(),
})
}
pub(crate) fn file_list(self) -> Result<Vec<PathBuf>, Error> {
autoreleasepool(|_| {
let class_array = NSArray::from_slice(&[NSURL::class()]);
let options = NSDictionary::from_slices(
&[unsafe { NSPasteboardURLReadingFileURLsOnlyKey }],
&[NSNumber::new_bool(true).as_ref()],
);
let objects = unsafe {
self.clipboard
.pasteboard
.readObjectsForClasses_options(&class_array, Some(&options))
};
objects
.map(|array| {
array
.iter()
.filter_map(|obj| {
obj.downcast::<NSURL>().ok().and_then(|url| {
unsafe { url.path() }.map(|p| PathBuf::from(p.to_string()))
})
})
.collect::<Vec<_>>()
})
.filter(|file_list| !file_list.is_empty())
.ok_or(Error::ContentNotAvailable)
})
}
}
pub(crate) struct Set<'clipboard> {
clipboard: &'clipboard mut Clipboard,
exclude_from_history: bool,
}
impl<'clipboard> Set<'clipboard> {
pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
Self { clipboard }
Self { clipboard, exclude_from_history: false }
}
pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> {
self.clipboard.clear();
let string_array = NSArray::from_vec(vec![NSString::from_str(&data)]);
// Make sure that we pass a pointer to the system and not the array object itself. Otherwise,
// the system won't free it because the API doesn't give it ownership of the data. This results in
// a memory leak because Rust can never run its destructor.
let success = unsafe { msg_send![self.clipboard.pasteboard, writeObjects: &*string_array] };
let string_array = NSArray::from_retained_slice(&[ProtocolObject::from_retained(
NSString::from_str(&data),
)]);
let success = unsafe { self.clipboard.pasteboard.writeObjects(&string_array) };
add_clipboard_exclusions(self.clipboard, self.exclude_from_history);
if success {
Ok(())
} else {
Err(Error::Unknown { description: "NSPasteboard#writeObjects: returned false".into() })
Err(Error::unknown("NSPasteboard#writeObjects: returned false"))
}
}
@ -282,49 +314,80 @@ impl<'clipboard> Set<'clipboard> {
// https://bugzilla.mozilla.org/show_bug.cgi?id=466599
// https://bugs.chromium.org/p/chromium/issues/detail?id=11957
let html = format!(
r#"<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head><body>{}</body></html>"#,
html
r#"<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head><body>{html}</body></html>"#,
);
let html_nss = NSString::from_str(&html);
// Make sure that we pass a pointer to the string and not the object itself.
let mut success: bool = unsafe {
msg_send![self.clipboard.pasteboard, setString: &*html_nss forType:NSPasteboardTypeHTML]
};
let mut success =
unsafe { self.clipboard.pasteboard.setString_forType(&html_nss, NSPasteboardTypeHTML) };
if success {
if let Some(alt_text) = alt {
let alt_nss = NSString::from_str(&alt_text);
// Similar to the primary string, we only want a pointer here too.
success = unsafe {
msg_send![self.clipboard.pasteboard, setString: &*alt_nss forType:NSPasteboardTypeString]
self.clipboard.pasteboard.setString_forType(&alt_nss, NSPasteboardTypeString)
};
}
}
add_clipboard_exclusions(self.clipboard, self.exclude_from_history);
if success {
Ok(())
} else {
Err(Error::Unknown { description: "NSPasteboard#writeObjects: returned false".into() })
Err(Error::unknown("NSPasteboard#writeObjects: returned false"))
}
}
#[cfg(feature = "image-data")]
pub(crate) fn image(self, data: ImageData) -> Result<(), Error> {
let pixels = data.bytes.into();
let image = image_from_pixels(pixels, data.width, data.height)
.map_err(|_| Error::ConversionFailure)?;
let image = image_from_pixels(pixels, data.width, data.height)?;
self.clipboard.clear();
let image_array: Id<NSArray<NSObject, Owned>> = NSArray::from_vec(vec![image]);
// Make sure that we pass a pointer to the system and not the array object itself.
let success = unsafe { msg_send![self.clipboard.pasteboard, writeObjects: &*image_array] };
let image_array = NSArray::from_retained_slice(&[ProtocolObject::from_retained(image)]);
let success = unsafe { self.clipboard.pasteboard.writeObjects(&image_array) };
add_clipboard_exclusions(self.clipboard, self.exclude_from_history);
if success {
Ok(())
} else {
Err(Error::Unknown {
description:
"Failed to write the image to the pasteboard (`writeObjects` returned NO)."
.into(),
Err(Error::unknown(
"Failed to write the image to the pasteboard (`writeObjects` returned NO).",
))
}
}
pub(crate) fn file_list(self, file_list: &[impl AsRef<Path>]) -> Result<(), Error> {
self.clipboard.clear();
let uri_list = file_list
.iter()
.filter_map(|path| {
path.as_ref().canonicalize().ok().and_then(|abs_path| {
abs_path.to_str().map(|str| {
let url = unsafe { NSURL::fileURLWithPath(&NSString::from_str(str)) };
ProtocolObject::from_retained(url)
})
})
})
.collect::<Vec<_>>();
if uri_list.is_empty() {
return Err(Error::ConversionFailure);
}
let objects = NSArray::from_retained_slice(&uri_list);
let success = unsafe { self.clipboard.pasteboard.writeObjects(&objects) };
add_clipboard_exclusions(self.clipboard, self.exclude_from_history);
if success {
Ok(())
} else {
Err(Error::unknown("NSPasteboard#writeObjects: returned false"))
}
}
}
@ -343,3 +406,33 @@ impl<'clipboard> Clear<'clipboard> {
Ok(())
}
}
fn add_clipboard_exclusions(clipboard: &mut Clipboard, exclude_from_history: bool) {
// On Mac there isn't an official standard for excluding data from clipboard, however
// there is an unofficial standard which is to set `org.nspasteboard.ConcealedType`.
//
// See http://nspasteboard.org/ for details about the community standard.
if exclude_from_history {
unsafe {
clipboard
.pasteboard
.setString_forType(ns_string!(""), ns_string!("org.nspasteboard.ConcealedType"));
}
}
}
/// Apple-specific extensions to the [`Set`](crate::Set) builder.
pub trait SetExtApple: private::Sealed {
/// Excludes the data which will be set on the clipboard from being added to
/// third party clipboard history software.
///
/// See http://nspasteboard.org/ for details about the community standard.
fn exclude_from_history(self) -> Self;
}
impl SetExtApple for crate::Set<'_> {
fn exclude_from_history(mut self) -> Self {
self.platform.exclude_from_history = true;
self
}
}

File diff suppressed because it is too large Load diff