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};
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: error.to_string() }
}
#[cfg(feature = "image-data")]
fn encode_as_png(image: &ImageData) -> Result<Vec<u8>, Error> {
use image::ImageEncoder as _;
if image.bytes.is_empty() || image.width == 0 || image.height == 0 {
return Err(Error::ConversionFailure);
}
let mut png_bytes = Vec::new();
let encoder = image::codecs::png::PngEncoder::new(&mut png_bytes);
encoder
.write_image(
image.bytes.as_ref(),
image.width as u32,
image.height as u32,
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> {
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)
}
#[derive(Copy, Clone, Debug)]
pub enum LinuxClipboardKind {
Clipboard,
Primary,
Secondary,
}
pub(crate) enum Clipboard {
X11(x11::Clipboard),
#[cfg(feature = "wayland-data-control")]
WlDataControl(wayland::Clipboard),
}
impl Clipboard {
pub(crate) fn new() -> Result<Self, Error> {
#[cfg(feature = "wayland-data-control")]
{
if std::env::var_os("WAYLAND_DISPLAY").is_some() {
match wayland::Clipboard::new() {
Ok(clipboard) => {
trace!("Successfully initialized the Wayland data control clipboard.");
return Ok(Self::WlDataControl(clipboard));
}
Err(e) => warn!(
"Tried to initialize the wayland data control protocol clipboard, but failed. Falling back to the X11 clipboard protocol. The error was: {}",
e
),
}
}
}
Ok(Self::X11(x11::Clipboard::new()?))
}
}
pub(crate) struct Get<'clipboard> {
clipboard: &'clipboard mut Clipboard,
selection: LinuxClipboardKind,
}
impl<'clipboard> Get<'clipboard> {
pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
Self { clipboard, selection: LinuxClipboardKind::Clipboard }
}
pub(crate) fn text(self) -> Result<String, Error> {
match self.clipboard {
Clipboard::X11(clipboard) => clipboard.get_text(self.selection),
#[cfg(feature = "wayland-data-control")]
Clipboard::WlDataControl(clipboard) => clipboard.get_text(self.selection),
}
}
#[cfg(feature = "image-data")]
pub(crate) fn image(self) -> Result<ImageData<'static>, Error> {
match self.clipboard {
Clipboard::X11(clipboard) => clipboard.get_image(self.selection),
#[cfg(feature = "wayland-data-control")]
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),
}
}
}
pub trait GetExtLinux: private::Sealed {
fn clipboard(self, selection: LinuxClipboardKind) -> Self;
}
impl GetExtLinux for crate::Get<'_> {
fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
self.platform.selection = selection;
self
}
}
#[derive(Default)]
pub(crate) enum WaitConfig {
Until(Instant),
Forever,
#[default]
None,
}
pub(crate) struct Set<'clipboard> {
clipboard: &'clipboard mut Clipboard,
wait: WaitConfig,
selection: LinuxClipboardKind,
exclude_from_history: bool,
}
impl<'clipboard> Set<'clipboard> {
pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
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, self.exclude_from_history)
}
#[cfg(feature = "wayland-data-control")]
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, self.exclude_from_history)
}
#[cfg(feature = "wayland-data-control")]
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, self.exclude_from_history)
}
#[cfg(feature = "wayland-data-control")]
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,
),
}
}
}
pub trait SetExtLinux: private::Sealed {
fn wait(self) -> Self;
fn wait_until(self, deadline: Instant) -> Self;
fn clipboard(self, selection: LinuxClipboardKind) -> Self;
fn exclude_from_history(self) -> Self;
}
impl SetExtLinux for crate::Set<'_> {
fn wait(mut self) -> Self {
self.platform.wait = WaitConfig::Forever;
self
}
fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
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> {
clipboard: &'clipboard mut Clipboard,
}
impl<'clipboard> Clear<'clipboard> {
pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
Self { clipboard }
}
pub(crate) fn clear(self) -> Result<(), Error> {
self.clear_inner(LinuxClipboardKind::Clipboard)
}
fn clear_inner(self, selection: LinuxClipboardKind) -> Result<(), Error> {
match self.clipboard {
Clipboard::X11(clipboard) => clipboard.clear(selection),
#[cfg(feature = "wayland-data-control")]
Clipboard::WlDataControl(clipboard) => clipboard.clear(selection),
}
}
}
pub trait ClearExtLinux: private::Sealed {
fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error>;
}
impl ClearExtLinux for crate::Clear<'_> {
fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error> {
self.platform.clear_inner(selection)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decoding_uri_list() {
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);
}
}