1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
/*
SPDX-License-Identifier: Apache-2.0 OR MIT
Copyright 2022 The Arboard contributors
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;
pub use common::Error;
#[cfg(feature = "image-data")]
pub use common::ImageData;
mod platform;
#[cfg(all(
unix,
not(any(target_os = "macos", target_os = "android", target_os = "emscripten")),
))]
pub use platform::{ClearExtLinux, GetExtLinux, LinuxClipboardKind, SetExtLinux};
#[cfg(windows)]
pub use platform::SetExtWindows;
/// 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
/// that all `Clipboard`s must be 'dropped' before the program exits. In most scenarios this happens
/// automatically but there are frameworks (for example, `winit`) that take over the execution
/// and where the objects don't get dropped when the application exits. In these cases you have to
/// make sure the object is dropped by taking ownership of it in a confined scope when detecting
/// that your application is about to quit.
///
/// It is also valid to have these multiple `Clipboards` on separate threads at once but note that
/// executing multiple clipboard operations in parallel might fail with a `ClipboardOccupied` error.
///
/// # Platform-specific behavior
///
/// `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
/// or keeping one open in some application/global state).
///
/// ## Linux
///
/// Using either Wayland and X11, the clipboard and its content is "hosted" inside of the application
/// that last put data onto it. This means that when the last `Clipboard` instance is dropped, the contents
/// may become unavailable to other apps. See [SetExtLinux] for more details.
///
/// ## 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 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.
///
/// # 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.
///
/// # 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.
///
/// # 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.
///
/// # 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,
alt_text: Option<T>,
) -> Result<(), Error> {
self.set().html(html, alt_text)
}
/// Fetches image data from the clipboard, and returns the decoded pixels.
///
/// 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()
}
/// Places an image to the clipboard.
///
/// The chosen output format, depending on the platform is the following:
///
/// - 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)
}
/// 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()
}
/// Begins a "clear" option to remove data from the clipboard.
pub fn clear_with(&mut self) -> Clear<'_> {
Clear { platform: platform::Clear::new(&mut self.platform) }
}
/// Begins a "get" operation to retrieve data from the clipboard.
pub fn get(&mut self) -> Get<'_> {
Get { platform: platform::Get::new(&mut self.platform) }
}
/// Begins a "set" operation to set the clipboard's contents.
pub fn set(&mut self) -> Set<'_> {
Set { platform: platform::Set::new(&mut self.platform) }
}
}
/// A builder for an operation that gets a value from the clipboard.
#[must_use]
pub struct Get<'clipboard> {
pub(crate) platform: platform::Get<'clipboard>,
}
impl Get<'_> {
/// Completes the "get" operation by fetching UTF-8 text from the clipboard.
pub fn text(self) -> Result<String, Error> {
self.platform.text()
}
/// Completes the "get" operation by fetching image data from the clipboard and returning the
/// decoded pixels.
///
/// 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.
#[cfg(feature = "image-data")]
pub fn image(self) -> Result<ImageData<'static>, Error> {
self.platform.image()
}
}
/// A builder for an operation that sets a value to the clipboard.
#[must_use]
pub struct Set<'clipboard> {
pub(crate) platform: platform::Set<'clipboard>,
}
impl Set<'_> {
/// Completes the "set" operation by placing text onto the clipboard. Any valid UTF-8 string
/// is accepted.
pub fn text<'a, T: Into<Cow<'a, str>>>(self, text: T) -> Result<(), Error> {
let text = text.into();
self.platform.text(text)
}
/// Completes the "set" operation by placing HTML as well as a plain-text alternative onto the
/// clipboard.
///
/// Any valid UTF-8 string is accepted.
pub fn html<'a, T: Into<Cow<'a, str>>>(
self,
html: T,
alt_text: Option<T>,
) -> Result<(), Error> {
let html = html.into();
let alt_text = alt_text.map(|e| e.into());
self.platform.html(html, alt_text)
}
/// Completes the "set" operation by placing an image onto the clipboard.
///
/// The chosen output format, depending on the platform is the following:
///
/// - On macOS: `NSImage` object
/// - On Linux: PNG, under the atom `image/png`
/// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP`
#[cfg(feature = "image-data")]
pub fn image(self, image: ImageData) -> Result<(), Error> {
self.platform.image(image)
}
}
/// A builder for an operation that clears the data from the clipboard.
#[must_use]
pub struct Clear<'clipboard> {
pub(crate) platform: platform::Clear<'clipboard>,
}
impl Clear<'_> {
/// Completes the "clear" operation by deleting any existing clipboard data,
/// regardless of the format.
pub fn default(self) -> Result<(), Error> {
self.platform.clear()
}
}
/// All tests grouped in one because the windows clipboard cannot be open on
/// multiple threads at once.
#[cfg(test)]
mod tests {
use super::*;
use std::{sync::Arc, thread, time::Duration};
#[test]
fn all_tests() {
let _ = env_logger::builder().is_test(true).try_init();
{
let mut ctx = Clipboard::new().unwrap();
let text = "some string";
ctx.set_text(text).unwrap();
assert_eq!(ctx.get_text().unwrap(), text);
// We also need to check that the content persists after the drop; this is
// especially important on X11
drop(ctx);
// Give any external mechanism a generous amount of time to take over
// responsibility for the clipboard, in case that happens asynchronously
// (it appears that this is the case on X11 plus Mutter 3.34+, see #4)
thread::sleep(Duration::from_millis(300));
let mut ctx = Clipboard::new().unwrap();
assert_eq!(ctx.get_text().unwrap(), text);
}
{
let mut ctx = Clipboard::new().unwrap();
let text = "Some utf8: 🤓 ∑φ(n)<ε 🐔";
ctx.set_text(text).unwrap();
assert_eq!(ctx.get_text().unwrap(), text);
}
{
let mut ctx = Clipboard::new().unwrap();
let text = "hello world";
ctx.set_text(text).unwrap();
assert_eq!(ctx.get_text().unwrap(), text);
ctx.clear().unwrap();
match ctx.get_text() {
Ok(text) => assert!(text.is_empty()),
Err(Error::ContentNotAvailable) => {}
Err(e) => panic!("unexpected error: {}", e),
};
// confirm it is OK to clear when already empty.
ctx.clear().unwrap();
}
{
let mut ctx = Clipboard::new().unwrap();
let html = "<b>hello</b> <i>world</i>!";
ctx.set_html(html, None).unwrap();
match ctx.get_text() {
Ok(text) => assert!(text.is_empty()),
Err(Error::ContentNotAvailable) => {}
Err(e) => panic!("unexpected error: {}", e),
};
}
{
let mut ctx = Clipboard::new().unwrap();
let html = "<b>hello</b> <i>world</i>!";
let alt_text = "hello world!";
ctx.set_html(html, Some(alt_text)).unwrap();
assert_eq!(ctx.get_text().unwrap(), alt_text);
}
#[cfg(feature = "image-data")]
{
let mut ctx = Clipboard::new().unwrap();
#[rustfmt::skip]
let bytes = [
255, 100, 100, 255,
100, 255, 100, 100,
100, 100, 255, 100,
0, 0, 0, 255,
];
let img_data = ImageData { width: 2, height: 2, bytes: bytes.as_ref().into() };
// Make sure that setting one format overwrites the other.
ctx.set_image(img_data.clone()).unwrap();
assert!(matches!(ctx.get_text(), Err(Error::ContentNotAvailable)));
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 clipboard
ctx.set_image(img_data.clone()).unwrap();
let got = ctx.get_image().unwrap();
assert_eq!(img_data.bytes, got.bytes);
#[rustfmt::skip]
let big_bytes = vec![
255, 100, 100, 255,
100, 255, 100, 100,
100, 100, 255, 100,
0, 1, 2, 255,
0, 1, 2, 255,
0, 1, 2, 255,
];
let bytes_cloned = big_bytes.clone();
let big_img_data = ImageData { width: 3, height: 2, bytes: big_bytes.into() };
ctx.set_image(big_img_data).unwrap();
let got = ctx.get_image().unwrap();
assert_eq!(bytes_cloned.as_slice(), got.bytes.as_ref());
}
#[cfg(all(
unix,
not(any(target_os = "macos", target_os = "android", target_os = "emscripten")),
))]
{
use crate::{LinuxClipboardKind, SetExtLinux};
use std::sync::atomic::{self, AtomicBool};
let mut ctx = Clipboard::new().unwrap();
const TEXT1: &str = "I'm a little teapot,";
const TEXT2: &str = "short and stout,";
const TEXT3: &str = "here is my handle";
ctx.set().clipboard(LinuxClipboardKind::Clipboard).text(TEXT1.to_string()).unwrap();
ctx.set().clipboard(LinuxClipboardKind::Primary).text(TEXT2.to_string()).unwrap();
// The secondary clipboard is not available under wayland
if !cfg!(feature = "wayland-data-control")
|| std::env::var_os("WAYLAND_DISPLAY").is_none()
{
ctx.set().clipboard(LinuxClipboardKind::Secondary).text(TEXT3.to_string()).unwrap();
}
assert_eq!(TEXT1, &ctx.get().clipboard(LinuxClipboardKind::Clipboard).text().unwrap());
assert_eq!(TEXT2, &ctx.get().clipboard(LinuxClipboardKind::Primary).text().unwrap());
// The secondary clipboard is not available under wayland
if !cfg!(feature = "wayland-data-control")
|| std::env::var_os("WAYLAND_DISPLAY").is_none()
{
assert_eq!(
TEXT3,
&ctx.get().clipboard(LinuxClipboardKind::Secondary).text().unwrap()
);
}
let was_replaced = Arc::new(AtomicBool::new(false));
let setter = thread::spawn({
let was_replaced = was_replaced.clone();
move || {
thread::sleep(Duration::from_millis(100));
let mut ctx = Clipboard::new().unwrap();
ctx.set_text("replacement text".to_owned()).unwrap();
was_replaced.store(true, atomic::Ordering::Release);
}
});
ctx.set().wait().text("initial text".to_owned()).unwrap();
assert!(was_replaced.load(atomic::Ordering::Acquire));
setter.join().unwrap();
}
}
// The cross-platform abstraction should allow any number of clipboards
// to be open at once without issue, as documented under [Clipboard].
#[test]
fn multiple_clipboards_at_once() {
const THREAD_COUNT: usize = 100;
let mut handles = Vec::with_capacity(THREAD_COUNT);
let barrier = Arc::new(std::sync::Barrier::new(THREAD_COUNT));
for _ in 0..THREAD_COUNT {
let barrier = barrier.clone();
handles.push(thread::spawn(move || {
// As long as the clipboard isn't used multiple times at once, multiple instances
// are perfectly fine.
let _ctx = Clipboard::new().unwrap();
thread::sleep(Duration::from_millis(10));
barrier.wait();
}));
}
for thread_handle in handles {
thread_handle.join().unwrap();
}
}
#[test]
fn clipboard_trait_consistently() {
fn assert_send_sync<T: Send + Sync + 'static>() {}
assert_send_sync::<Clipboard>();
assert!(std::mem::needs_drop::<Clipboard>());
}
}