1use std::{borrow::Cow, path::PathBuf, time::Instant};
2
3#[cfg(feature = "wayland-data-control")]
4use log::{trace, warn};
5use percent_encoding::percent_decode_str;
6
7#[cfg(feature = "image-data")]
8use crate::ImageData;
9use crate::{common::private, Error};
10
11const KDE_EXCLUSION_MIME: &str = "x-kde-passwordManagerHint";
13const KDE_EXCLUSION_HINT: &[u8] = b"secret";
14
15mod x11;
16
17#[cfg(feature = "wayland-data-control")]
18mod wayland;
19
20fn into_unknown<E: std::fmt::Display>(error: E) -> Error {
21 Error::Unknown { description: error.to_string() }
22}
23
24#[cfg(feature = "image-data")]
25fn encode_as_png(image: &ImageData) -> Result<Vec<u8>, Error> {
26 use image::ImageEncoder as _;
27
28 if image.bytes.is_empty() || image.width == 0 || image.height == 0 {
29 return Err(Error::ConversionFailure);
30 }
31
32 let mut png_bytes = Vec::new();
33 let encoder = image::codecs::png::PngEncoder::new(&mut png_bytes);
34 encoder
35 .write_image(
36 image.bytes.as_ref(),
37 image.width as u32,
38 image.height as u32,
39 image::ExtendedColorType::Rgba8,
40 )
41 .map_err(|_| Error::ConversionFailure)?;
42
43 Ok(png_bytes)
44}
45
46fn paths_from_uri_list(uri_list: String) -> Vec<PathBuf> {
47 uri_list
48 .lines()
49 .filter_map(|s| s.strip_prefix("file://"))
50 .filter_map(|s| percent_decode_str(s).decode_utf8().ok())
51 .map(|decoded| PathBuf::from(decoded.as_ref()))
52 .collect()
53}
54
55#[derive(Copy, Clone, Debug)]
65pub enum LinuxClipboardKind {
66 Clipboard,
69
70 Primary,
76
77 Secondary,
82}
83
84pub(crate) enum Clipboard {
85 X11(x11::Clipboard),
86
87 #[cfg(feature = "wayland-data-control")]
88 WlDataControl(wayland::Clipboard),
89}
90
91impl Clipboard {
92 pub(crate) fn new() -> Result<Self, Error> {
93 #[cfg(feature = "wayland-data-control")]
94 {
95 if std::env::var_os("WAYLAND_DISPLAY").is_some() {
96 match wayland::Clipboard::new() {
98 Ok(clipboard) => {
99 trace!("Successfully initialized the Wayland data control clipboard.");
100 return Ok(Self::WlDataControl(clipboard));
101 }
102 Err(e) => warn!(
103 "Tried to initialize the wayland data control protocol clipboard, but failed. Falling back to the X11 clipboard protocol. The error was: {}",
104 e
105 ),
106 }
107 }
108 }
109 Ok(Self::X11(x11::Clipboard::new()?))
110 }
111}
112
113pub(crate) struct Get<'clipboard> {
114 clipboard: &'clipboard mut Clipboard,
115 selection: LinuxClipboardKind,
116}
117
118impl<'clipboard> Get<'clipboard> {
119 pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
120 Self { clipboard, selection: LinuxClipboardKind::Clipboard }
121 }
122
123 pub(crate) fn text(self) -> Result<String, Error> {
124 match self.clipboard {
125 Clipboard::X11(clipboard) => clipboard.get_text(self.selection),
126 #[cfg(feature = "wayland-data-control")]
127 Clipboard::WlDataControl(clipboard) => clipboard.get_text(self.selection),
128 }
129 }
130
131 #[cfg(feature = "image-data")]
132 pub(crate) fn image(self) -> Result<ImageData<'static>, Error> {
133 match self.clipboard {
134 Clipboard::X11(clipboard) => clipboard.get_image(self.selection),
135 #[cfg(feature = "wayland-data-control")]
136 Clipboard::WlDataControl(clipboard) => clipboard.get_image(self.selection),
137 }
138 }
139
140 pub(crate) fn html(self) -> Result<String, Error> {
141 match self.clipboard {
142 Clipboard::X11(clipboard) => clipboard.get_html(self.selection),
143 #[cfg(feature = "wayland-data-control")]
144 Clipboard::WlDataControl(clipboard) => clipboard.get_html(self.selection),
145 }
146 }
147
148 pub(crate) fn file_list(self) -> Result<Vec<PathBuf>, Error> {
149 match self.clipboard {
150 Clipboard::X11(clipboard) => clipboard.get_file_list(self.selection),
151 #[cfg(feature = "wayland-data-control")]
152 Clipboard::WlDataControl(clipboard) => clipboard.get_file_list(self.selection),
153 }
154 }
155}
156
157pub trait GetExtLinux: private::Sealed {
159 fn clipboard(self, selection: LinuxClipboardKind) -> Self;
164}
165
166impl GetExtLinux for crate::Get<'_> {
167 fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
168 self.platform.selection = selection;
169 self
170 }
171}
172
173#[derive(Default)]
175pub(crate) enum WaitConfig {
176 Until(Instant),
178
179 Forever,
181
182 #[default]
184 None,
185}
186
187pub(crate) struct Set<'clipboard> {
188 clipboard: &'clipboard mut Clipboard,
189 wait: WaitConfig,
190 selection: LinuxClipboardKind,
191 exclude_from_history: bool,
192}
193
194impl<'clipboard> Set<'clipboard> {
195 pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
196 Self {
197 clipboard,
198 wait: WaitConfig::default(),
199 selection: LinuxClipboardKind::Clipboard,
200 exclude_from_history: false,
201 }
202 }
203
204 pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> {
205 match self.clipboard {
206 Clipboard::X11(clipboard) => {
207 clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history)
208 }
209
210 #[cfg(feature = "wayland-data-control")]
211 Clipboard::WlDataControl(clipboard) => {
212 clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history)
213 }
214 }
215 }
216
217 pub(crate) fn html(self, html: Cow<'_, str>, alt: Option<Cow<'_, str>>) -> Result<(), Error> {
218 match self.clipboard {
219 Clipboard::X11(clipboard) => {
220 clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history)
221 }
222
223 #[cfg(feature = "wayland-data-control")]
224 Clipboard::WlDataControl(clipboard) => {
225 clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history)
226 }
227 }
228 }
229
230 #[cfg(feature = "image-data")]
231 pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> {
232 match self.clipboard {
233 Clipboard::X11(clipboard) => {
234 clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history)
235 }
236
237 #[cfg(feature = "wayland-data-control")]
238 Clipboard::WlDataControl(clipboard) => {
239 clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history)
240 }
241 }
242 }
243}
244
245pub trait SetExtLinux: private::Sealed {
247 fn wait(self) -> Self;
273
274 fn wait_until(self, deadline: Instant) -> Self;
283
284 fn clipboard(self, selection: LinuxClipboardKind) -> Self;
305
306 fn exclude_from_history(self) -> Self;
312}
313
314impl SetExtLinux for crate::Set<'_> {
315 fn wait(mut self) -> Self {
316 self.platform.wait = WaitConfig::Forever;
317 self
318 }
319
320 fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
321 self.platform.selection = selection;
322 self
323 }
324
325 fn wait_until(mut self, deadline: Instant) -> Self {
326 self.platform.wait = WaitConfig::Until(deadline);
327 self
328 }
329
330 fn exclude_from_history(mut self) -> Self {
331 self.platform.exclude_from_history = true;
332 self
333 }
334}
335
336pub(crate) struct Clear<'clipboard> {
337 clipboard: &'clipboard mut Clipboard,
338}
339
340impl<'clipboard> Clear<'clipboard> {
341 pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
342 Self { clipboard }
343 }
344
345 pub(crate) fn clear(self) -> Result<(), Error> {
346 self.clear_inner(LinuxClipboardKind::Clipboard)
347 }
348
349 fn clear_inner(self, selection: LinuxClipboardKind) -> Result<(), Error> {
350 match self.clipboard {
351 Clipboard::X11(clipboard) => clipboard.clear(selection),
352 #[cfg(feature = "wayland-data-control")]
353 Clipboard::WlDataControl(clipboard) => clipboard.clear(selection),
354 }
355 }
356}
357
358pub trait ClearExtLinux: private::Sealed {
360 fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error>;
379}
380
381impl ClearExtLinux for crate::Clear<'_> {
382 fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error> {
383 self.platform.clear_inner(selection)
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_decoding_uri_list() {
393 let file_list = [
396 "file:///tmp/bar.log",
397 "file:///tmp/test%5C.txt",
398 "file:///tmp/foo%3F.png",
399 "file:///tmp/white%20space.txt",
400 ];
401
402 let paths = vec![
403 PathBuf::from("/tmp/bar.log"),
404 PathBuf::from("/tmp/test\\.txt"),
405 PathBuf::from("/tmp/foo?.png"),
406 PathBuf::from("/tmp/white space.txt"),
407 ];
408 assert_eq!(paths_from_uri_list(file_list.join("\n")), paths);
409 }
410}