Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ wayland-data-control = ["wl-clipboard-rs"]

# For backwards compat
core-graphics = ["dep:objc2-core-graphics"]
windows-sys = ["dep:windows-sys"]
windows-sys = ["windows-sys/Win32_Graphics_Gdi"]
image = ["dep:image"]
wl-clipboard-rs = ["dep:wl-clipboard-rs"]

Expand All @@ -32,12 +32,13 @@ wl-clipboard-rs = ["dep:wl-clipboard-rs"]
env_logger = "0.10.2"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = ">=0.52.0, <0.60.0", optional = true, features = [
windows-sys = { version = ">=0.52.0, <0.60.0", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"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"
Expand Down
23 changes: 22 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ and conditions of the chosen license apply to this file.
#![warn(unreachable_pub)]

mod common;
use std::{borrow::Cow, path::PathBuf};
use std::{
borrow::Cow,
path::{Path, PathBuf},
};

pub use common::Error;
#[cfg(feature = "image-data")]
Expand Down Expand Up @@ -243,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.
Expand Down Expand Up @@ -350,6 +358,19 @@ mod tests {
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();
Expand Down
59 changes: 57 additions & 2 deletions src/platform/linux/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
use std::{borrow::Cow, path::PathBuf, time::Instant};
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;
use percent_encoding::{percent_decode, percent_encode, AsciiSet, CONTROLS};

#[cfg(feature = "image-data")]
use crate::ImageData;
Expand Down Expand Up @@ -52,6 +57,37 @@ fn paths_from_uri_list(uri_list: Vec<u8>) -> Vec<PathBuf> {
.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
Expand Down Expand Up @@ -240,6 +276,25 @@ impl<'clipboard> Set<'clipboard> {
}
}
}

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,
),
}
}
}

/// Linux specific extensions to the [`Set`](super::Set) builder.
Expand Down
38 changes: 34 additions & 4 deletions src/platform/linux/wayland.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use std::{borrow::Cow, io::Read, path::PathBuf};
use std::{
borrow::Cow,
io::Read,
path::{Path, PathBuf},
};

use wl_clipboard_rs::{
copy::{self, Error as CopyError, MimeSource, MimeType, Options, Source},
Expand All @@ -9,8 +13,8 @@ use wl_clipboard_rs::{
#[cfg(feature = "image-data")]
use super::encode_as_png;
use super::{
into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig, KDE_EXCLUSION_HINT,
KDE_EXCLUSION_MIME,
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")]
Expand All @@ -19,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 {
Expand Down Expand Up @@ -228,8 +234,32 @@ impl Clipboard {
&mut self,
selection: LinuxClipboardKind,
) -> Result<Vec<PathBuf>, Error> {
handle_clipboard_read(selection, paste::MimeType::Specific("text/uri-list"), |contents| {
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)
}
}
22 changes: 19 additions & 3 deletions src/platform/linux/x11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::{
borrow::Cow,
cell::RefCell,
collections::{hash_map::Entry, HashMap},
path::PathBuf,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
Expand Down Expand Up @@ -46,8 +46,8 @@ use x11rb::{
#[cfg(feature = "image-data")]
use super::encode_as_png;
use super::{
into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig, KDE_EXCLUSION_HINT,
KDE_EXCLUSION_MIME,
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;
Expand Down Expand Up @@ -1061,6 +1061,22 @@ impl Clipboard {

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)
}
}

impl Drop for Clipboard {
Expand Down
33 changes: 32 additions & 1 deletion src/platform/osx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use objc2_foundation::{ns_string, NSArray, NSDictionary, NSNumber, NSString, NSU
use std::{
borrow::Cow,
panic::{RefUnwindSafe, UnwindSafe},
path::PathBuf,
path::{Path, PathBuf},
};

/// Returns an NSImage object on success.
Expand Down Expand Up @@ -352,6 +352,37 @@ impl<'clipboard> Set<'clipboard> {
))
}
}

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"))
}
}
}

pub(crate) struct Clear<'clipboard> {
Expand Down
Loading
Loading