1use std::{
2 borrow::Cow,
3 os::unix::ffi::OsStrExt,
4 path::{Path, PathBuf},
5 time::Instant,
6};
7
8#[cfg(feature = "wayland-data-control")]
9use log::{trace, warn};
10use percent_encoding::{percent_decode, percent_encode, AsciiSet, CONTROLS};
11
12#[cfg(feature = "image-data")]
13use crate::ImageData;
14use crate::{common::private, Error};
15
16const KDE_EXCLUSION_MIME: &str = "x-kde-passwordManagerHint";
18const KDE_EXCLUSION_HINT: &[u8] = b"secret";
19
20mod x11;
21
22#[cfg(feature = "wayland-data-control")]
23mod wayland;
24
25fn into_unknown<E: std::fmt::Display>(error: E) -> Error {
26 Error::Unknown { description: error.to_string() }
27}
28
29#[cfg(feature = "image-data")]
30fn encode_as_png(image: &ImageData) -> Result<Vec<u8>, Error> {
31 use image::ImageEncoder as _;
32
33 if image.bytes.is_empty() || image.width == 0 || image.height == 0 {
34 return Err(Error::ConversionFailure);
35 }
36
37 let mut png_bytes = Vec::new();
38 let encoder = image::codecs::png::PngEncoder::new(&mut png_bytes);
39 encoder
40 .write_image(
41 image.bytes.as_ref(),
42 image.width as u32,
43 image.height as u32,
44 image::ExtendedColorType::Rgba8,
45 )
46 .map_err(|_| Error::ConversionFailure)?;
47
48 Ok(png_bytes)
49}
50
51fn paths_from_uri_list(uri_list: Vec<u8>) -> Vec<PathBuf> {
52 uri_list
53 .split(|char| *char == b'\n')
54 .map(|line| line.strip_suffix(b"\r").unwrap_or(line))
55 .filter_map(|line| line.strip_prefix(b"file://"))
56 .filter_map(|s| percent_decode(s).decode_utf8().ok())
57 .map(|decoded| PathBuf::from(decoded.as_ref()))
58 .collect()
59}
60
61fn paths_to_uri_list(file_list: &[impl AsRef<Path>]) -> Result<String, Error> {
62 const ASCII_SET: &AsciiSet = &CONTROLS
64 .add(b'#')
65 .add(b';')
66 .add(b'?')
67 .add(b'[')
68 .add(b']')
69 .add(b' ')
70 .add(b'\"')
71 .add(b'%')
72 .add(b'<')
73 .add(b'>')
74 .add(b'\\')
75 .add(b'^')
76 .add(b'`')
77 .add(b'{')
78 .add(b'|')
79 .add(b'}');
80
81 file_list
82 .iter()
83 .filter_map(|path| {
84 path.as_ref().canonicalize().ok().map(|path| {
85 format!("file://{}", percent_encode(path.as_os_str().as_bytes(), ASCII_SET))
86 })
87 })
88 .reduce(|uri_list, uri| uri_list + "\r\n" + &uri)
92 .ok_or(Error::ConversionFailure)
93}
94
95#[derive(Copy, Clone, Debug)]
105pub enum LinuxClipboardKind {
106 Clipboard,
109
110 Primary,
116
117 Secondary,
122}
123
124pub(crate) enum Clipboard {
125 X11(x11::Clipboard),
126
127 #[cfg(feature = "wayland-data-control")]
128 WlDataControl(wayland::Clipboard),
129}
130
131impl Clipboard {
132 pub(crate) fn new() -> Result<Self, Error> {
133 #[cfg(feature = "wayland-data-control")]
134 {
135 if std::env::var_os("WAYLAND_DISPLAY").is_some() {
136 match wayland::Clipboard::new() {
138 Ok(clipboard) => {
139 trace!("Successfully initialized the Wayland data control clipboard.");
140 return Ok(Self::WlDataControl(clipboard));
141 }
142 Err(e) => warn!(
143 "Tried to initialize the wayland data control protocol clipboard, but failed. Falling back to the X11 clipboard protocol. The error was: {}",
144 e
145 ),
146 }
147 }
148 }
149 Ok(Self::X11(x11::Clipboard::new()?))
150 }
151}
152
153pub(crate) struct Get<'clipboard> {
154 clipboard: &'clipboard mut Clipboard,
155 selection: LinuxClipboardKind,
156}
157
158impl<'clipboard> Get<'clipboard> {
159 pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
160 Self { clipboard, selection: LinuxClipboardKind::Clipboard }
161 }
162
163 pub(crate) fn text(self) -> Result<String, Error> {
164 match self.clipboard {
165 Clipboard::X11(clipboard) => clipboard.get_text(self.selection),
166 #[cfg(feature = "wayland-data-control")]
167 Clipboard::WlDataControl(clipboard) => clipboard.get_text(self.selection),
168 }
169 }
170
171 #[cfg(feature = "image-data")]
172 pub(crate) fn image(self) -> Result<ImageData<'static>, Error> {
173 match self.clipboard {
174 Clipboard::X11(clipboard) => clipboard.get_image(self.selection),
175 #[cfg(feature = "wayland-data-control")]
176 Clipboard::WlDataControl(clipboard) => clipboard.get_image(self.selection),
177 }
178 }
179
180 pub(crate) fn html(self) -> Result<String, Error> {
181 match self.clipboard {
182 Clipboard::X11(clipboard) => clipboard.get_html(self.selection),
183 #[cfg(feature = "wayland-data-control")]
184 Clipboard::WlDataControl(clipboard) => clipboard.get_html(self.selection),
185 }
186 }
187
188 pub(crate) fn file_list(self) -> Result<Vec<PathBuf>, Error> {
189 match self.clipboard {
190 Clipboard::X11(clipboard) => clipboard.get_file_list(self.selection),
191 #[cfg(feature = "wayland-data-control")]
192 Clipboard::WlDataControl(clipboard) => clipboard.get_file_list(self.selection),
193 }
194 }
195}
196
197pub trait GetExtLinux: private::Sealed {
199 fn clipboard(self, selection: LinuxClipboardKind) -> Self;
204}
205
206impl GetExtLinux for crate::Get<'_> {
207 fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
208 self.platform.selection = selection;
209 self
210 }
211}
212
213#[derive(Default)]
215pub(crate) enum WaitConfig {
216 Until(Instant),
218
219 Once,
221
222 Forever,
224
225 #[default]
227 None,
228}
229
230pub(crate) struct Set<'clipboard> {
231 clipboard: &'clipboard mut Clipboard,
232 wait: WaitConfig,
233 selection: LinuxClipboardKind,
234 exclude_from_history: bool,
235}
236
237impl<'clipboard> Set<'clipboard> {
238 pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
239 Self {
240 clipboard,
241 wait: WaitConfig::default(),
242 selection: LinuxClipboardKind::Clipboard,
243 exclude_from_history: false,
244 }
245 }
246
247 pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> {
248 match self.clipboard {
249 Clipboard::X11(clipboard) => {
250 clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history)
251 }
252
253 #[cfg(feature = "wayland-data-control")]
254 Clipboard::WlDataControl(clipboard) => {
255 clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history)
256 }
257 }
258 }
259
260 pub(crate) fn html(self, html: Cow<'_, str>, alt: Option<Cow<'_, str>>) -> Result<(), Error> {
261 match self.clipboard {
262 Clipboard::X11(clipboard) => {
263 clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history)
264 }
265
266 #[cfg(feature = "wayland-data-control")]
267 Clipboard::WlDataControl(clipboard) => {
268 clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history)
269 }
270 }
271 }
272
273 #[cfg(feature = "image-data")]
274 pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> {
275 match self.clipboard {
276 Clipboard::X11(clipboard) => {
277 clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history)
278 }
279
280 #[cfg(feature = "wayland-data-control")]
281 Clipboard::WlDataControl(clipboard) => {
282 clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history)
283 }
284 }
285 }
286
287 pub(crate) fn file_list(self, file_list: &[impl AsRef<Path>]) -> Result<(), Error> {
288 match self.clipboard {
289 Clipboard::X11(clipboard) => clipboard.set_file_list(
290 file_list,
291 self.selection,
292 self.wait,
293 self.exclude_from_history,
294 ),
295
296 #[cfg(feature = "wayland-data-control")]
297 Clipboard::WlDataControl(clipboard) => clipboard.set_file_list(
298 file_list,
299 self.selection,
300 self.wait,
301 self.exclude_from_history,
302 ),
303 }
304 }
305}
306
307pub trait SetExtLinux: private::Sealed {
309 fn wait(self) -> Self;
335
336 fn wait_until(self, deadline: Instant) -> Self;
345
346 fn wait_once(self) -> Self;
352
353 fn clipboard(self, selection: LinuxClipboardKind) -> Self;
374
375 fn exclude_from_history(self) -> Self;
381}
382
383impl SetExtLinux for crate::Set<'_> {
384 fn wait(mut self) -> Self {
385 self.platform.wait = WaitConfig::Forever;
386 self
387 }
388
389 fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
390 self.platform.selection = selection;
391 self
392 }
393
394 fn wait_until(mut self, deadline: Instant) -> Self {
395 self.platform.wait = WaitConfig::Until(deadline);
396 self
397 }
398
399 fn wait_once(mut self) -> Self {
400 self.platform.wait = WaitConfig::Once;
401 self
402 }
403
404 fn exclude_from_history(mut self) -> Self {
405 self.platform.exclude_from_history = true;
406 self
407 }
408}
409
410pub(crate) struct Clear<'clipboard> {
411 clipboard: &'clipboard mut Clipboard,
412}
413
414impl<'clipboard> Clear<'clipboard> {
415 pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
416 Self { clipboard }
417 }
418
419 pub(crate) fn clear(self) -> Result<(), Error> {
420 self.clear_inner(LinuxClipboardKind::Clipboard)
421 }
422
423 fn clear_inner(self, selection: LinuxClipboardKind) -> Result<(), Error> {
424 match self.clipboard {
425 Clipboard::X11(clipboard) => clipboard.clear(selection),
426 #[cfg(feature = "wayland-data-control")]
427 Clipboard::WlDataControl(clipboard) => clipboard.clear(selection),
428 }
429 }
430}
431
432pub trait ClearExtLinux: private::Sealed {
434 fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error>;
453}
454
455impl ClearExtLinux for crate::Clear<'_> {
456 fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error> {
457 self.platform.clear_inner(selection)
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_decoding_uri_list() {
467 let file_list = [
470 "file:///tmp/bar.log",
471 "file:///tmp/test%5C.txt",
472 "file:///tmp/foo%3F.png",
473 "file:///tmp/white%20space.txt",
474 ];
475
476 let paths = vec![
477 PathBuf::from("/tmp/bar.log"),
478 PathBuf::from("/tmp/test\\.txt"),
479 PathBuf::from("/tmp/foo?.png"),
480 PathBuf::from("/tmp/white space.txt"),
481 ];
482 assert_eq!(paths_from_uri_list(file_list.join("\r\n").into()), paths);
483 }
484}