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 .filter_map(|line| line.strip_prefix(b"file://"))
55 .filter_map(|s| percent_decode(s).decode_utf8().ok())
56 .map(|decoded| PathBuf::from(decoded.as_ref()))
57 .collect()
58}
59
60fn paths_to_uri_list(file_list: &[impl AsRef<Path>]) -> Result<String, Error> {
61 const ASCII_SET: &AsciiSet = &CONTROLS
63 .add(b'#')
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
80 file_list
81 .iter()
82 .filter_map(|path| {
83 path.as_ref().canonicalize().ok().map(|path| {
84 format!("file://{}", percent_encode(path.as_os_str().as_bytes(), ASCII_SET))
85 })
86 })
87 .reduce(|uri_list, uri| uri_list + "\n" + &uri)
88 .ok_or(Error::ConversionFailure)
89}
90
91#[derive(Copy, Clone, Debug)]
101pub enum LinuxClipboardKind {
102 Clipboard,
105
106 Primary,
112
113 Secondary,
118}
119
120pub(crate) enum Clipboard {
121 X11(x11::Clipboard),
122
123 #[cfg(feature = "wayland-data-control")]
124 WlDataControl(wayland::Clipboard),
125}
126
127impl Clipboard {
128 pub(crate) fn new() -> Result<Self, Error> {
129 #[cfg(feature = "wayland-data-control")]
130 {
131 if std::env::var_os("WAYLAND_DISPLAY").is_some() {
132 match wayland::Clipboard::new() {
134 Ok(clipboard) => {
135 trace!("Successfully initialized the Wayland data control clipboard.");
136 return Ok(Self::WlDataControl(clipboard));
137 }
138 Err(e) => warn!(
139 "Tried to initialize the wayland data control protocol clipboard, but failed. Falling back to the X11 clipboard protocol. The error was: {}",
140 e
141 ),
142 }
143 }
144 }
145 Ok(Self::X11(x11::Clipboard::new()?))
146 }
147}
148
149pub(crate) struct Get<'clipboard> {
150 clipboard: &'clipboard mut Clipboard,
151 selection: LinuxClipboardKind,
152}
153
154impl<'clipboard> Get<'clipboard> {
155 pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
156 Self { clipboard, selection: LinuxClipboardKind::Clipboard }
157 }
158
159 pub(crate) fn text(self) -> Result<String, Error> {
160 match self.clipboard {
161 Clipboard::X11(clipboard) => clipboard.get_text(self.selection),
162 #[cfg(feature = "wayland-data-control")]
163 Clipboard::WlDataControl(clipboard) => clipboard.get_text(self.selection),
164 }
165 }
166
167 #[cfg(feature = "image-data")]
168 pub(crate) fn image(self) -> Result<ImageData<'static>, Error> {
169 match self.clipboard {
170 Clipboard::X11(clipboard) => clipboard.get_image(self.selection),
171 #[cfg(feature = "wayland-data-control")]
172 Clipboard::WlDataControl(clipboard) => clipboard.get_image(self.selection),
173 }
174 }
175
176 pub(crate) fn html(self) -> Result<String, Error> {
177 match self.clipboard {
178 Clipboard::X11(clipboard) => clipboard.get_html(self.selection),
179 #[cfg(feature = "wayland-data-control")]
180 Clipboard::WlDataControl(clipboard) => clipboard.get_html(self.selection),
181 }
182 }
183
184 pub(crate) fn file_list(self) -> Result<Vec<PathBuf>, Error> {
185 match self.clipboard {
186 Clipboard::X11(clipboard) => clipboard.get_file_list(self.selection),
187 #[cfg(feature = "wayland-data-control")]
188 Clipboard::WlDataControl(clipboard) => clipboard.get_file_list(self.selection),
189 }
190 }
191}
192
193pub trait GetExtLinux: private::Sealed {
195 fn clipboard(self, selection: LinuxClipboardKind) -> Self;
200}
201
202impl GetExtLinux for crate::Get<'_> {
203 fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
204 self.platform.selection = selection;
205 self
206 }
207}
208
209#[derive(Default)]
211pub(crate) enum WaitConfig {
212 Until(Instant),
214
215 Forever,
217
218 #[default]
220 None,
221}
222
223pub(crate) struct Set<'clipboard> {
224 clipboard: &'clipboard mut Clipboard,
225 wait: WaitConfig,
226 selection: LinuxClipboardKind,
227 exclude_from_history: bool,
228}
229
230impl<'clipboard> Set<'clipboard> {
231 pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
232 Self {
233 clipboard,
234 wait: WaitConfig::default(),
235 selection: LinuxClipboardKind::Clipboard,
236 exclude_from_history: false,
237 }
238 }
239
240 pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> {
241 match self.clipboard {
242 Clipboard::X11(clipboard) => {
243 clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history)
244 }
245
246 #[cfg(feature = "wayland-data-control")]
247 Clipboard::WlDataControl(clipboard) => {
248 clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history)
249 }
250 }
251 }
252
253 pub(crate) fn html(self, html: Cow<'_, str>, alt: Option<Cow<'_, str>>) -> Result<(), Error> {
254 match self.clipboard {
255 Clipboard::X11(clipboard) => {
256 clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history)
257 }
258
259 #[cfg(feature = "wayland-data-control")]
260 Clipboard::WlDataControl(clipboard) => {
261 clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history)
262 }
263 }
264 }
265
266 #[cfg(feature = "image-data")]
267 pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> {
268 match self.clipboard {
269 Clipboard::X11(clipboard) => {
270 clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history)
271 }
272
273 #[cfg(feature = "wayland-data-control")]
274 Clipboard::WlDataControl(clipboard) => {
275 clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history)
276 }
277 }
278 }
279
280 pub(crate) fn file_list(self, file_list: &[impl AsRef<Path>]) -> Result<(), Error> {
281 match self.clipboard {
282 Clipboard::X11(clipboard) => clipboard.set_file_list(
283 file_list,
284 self.selection,
285 self.wait,
286 self.exclude_from_history,
287 ),
288
289 #[cfg(feature = "wayland-data-control")]
290 Clipboard::WlDataControl(clipboard) => clipboard.set_file_list(
291 file_list,
292 self.selection,
293 self.wait,
294 self.exclude_from_history,
295 ),
296 }
297 }
298}
299
300pub trait SetExtLinux: private::Sealed {
302 fn wait(self) -> Self;
328
329 fn wait_until(self, deadline: Instant) -> Self;
338
339 fn clipboard(self, selection: LinuxClipboardKind) -> Self;
360
361 fn exclude_from_history(self) -> Self;
367}
368
369impl SetExtLinux for crate::Set<'_> {
370 fn wait(mut self) -> Self {
371 self.platform.wait = WaitConfig::Forever;
372 self
373 }
374
375 fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
376 self.platform.selection = selection;
377 self
378 }
379
380 fn wait_until(mut self, deadline: Instant) -> Self {
381 self.platform.wait = WaitConfig::Until(deadline);
382 self
383 }
384
385 fn exclude_from_history(mut self) -> Self {
386 self.platform.exclude_from_history = true;
387 self
388 }
389}
390
391pub(crate) struct Clear<'clipboard> {
392 clipboard: &'clipboard mut Clipboard,
393}
394
395impl<'clipboard> Clear<'clipboard> {
396 pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
397 Self { clipboard }
398 }
399
400 pub(crate) fn clear(self) -> Result<(), Error> {
401 self.clear_inner(LinuxClipboardKind::Clipboard)
402 }
403
404 fn clear_inner(self, selection: LinuxClipboardKind) -> Result<(), Error> {
405 match self.clipboard {
406 Clipboard::X11(clipboard) => clipboard.clear(selection),
407 #[cfg(feature = "wayland-data-control")]
408 Clipboard::WlDataControl(clipboard) => clipboard.clear(selection),
409 }
410 }
411}
412
413pub trait ClearExtLinux: private::Sealed {
415 fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error>;
434}
435
436impl ClearExtLinux for crate::Clear<'_> {
437 fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error> {
438 self.platform.clear_inner(selection)
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn test_decoding_uri_list() {
448 let file_list = [
451 "file:///tmp/bar.log",
452 "file:///tmp/test%5C.txt",
453 "file:///tmp/foo%3F.png",
454 "file:///tmp/white%20space.txt",
455 ];
456
457 let paths = vec![
458 PathBuf::from("/tmp/bar.log"),
459 PathBuf::from("/tmp/test\\.txt"),
460 PathBuf::from("/tmp/foo?.png"),
461 PathBuf::from("/tmp/white space.txt"),
462 ];
463 assert_eq!(paths_from_uri_list(file_list.join("\n").into()), paths);
464 }
465}