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
11mod x11;
12
13#[cfg(feature = "wayland-data-control")]
14mod wayland;
15
16fn into_unknown<E: std::fmt::Display>(error: E) -> Error {
17 Error::Unknown { description: error.to_string() }
18}
19
20#[cfg(feature = "image-data")]
21fn encode_as_png(image: &ImageData) -> Result<Vec<u8>, Error> {
22 use image::ImageEncoder as _;
23
24 if image.bytes.is_empty() || image.width == 0 || image.height == 0 {
25 return Err(Error::ConversionFailure);
26 }
27
28 let mut png_bytes = Vec::new();
29 let encoder = image::codecs::png::PngEncoder::new(&mut png_bytes);
30 encoder
31 .write_image(
32 image.bytes.as_ref(),
33 image.width as u32,
34 image.height as u32,
35 image::ExtendedColorType::Rgba8,
36 )
37 .map_err(|_| Error::ConversionFailure)?;
38
39 Ok(png_bytes)
40}
41
42fn paths_from_uri_list(uri_list: String) -> Vec<PathBuf> {
43 uri_list
44 .lines()
45 .filter_map(|s| s.strip_prefix("file://"))
46 .filter_map(|s| percent_decode_str(s).decode_utf8().ok())
47 .map(|decoded| PathBuf::from(decoded.as_ref()))
48 .collect()
49}
50
51#[derive(Copy, Clone, Debug)]
61pub enum LinuxClipboardKind {
62 Clipboard,
65
66 Primary,
72
73 Secondary,
78}
79
80pub(crate) enum Clipboard {
81 X11(x11::Clipboard),
82
83 #[cfg(feature = "wayland-data-control")]
84 WlDataControl(wayland::Clipboard),
85}
86
87impl Clipboard {
88 pub(crate) fn new() -> Result<Self, Error> {
89 #[cfg(feature = "wayland-data-control")]
90 {
91 if std::env::var_os("WAYLAND_DISPLAY").is_some() {
92 match wayland::Clipboard::new() {
94 Ok(clipboard) => {
95 trace!("Successfully initialized the Wayland data control clipboard.");
96 return Ok(Self::WlDataControl(clipboard));
97 }
98 Err(e) => warn!(
99 "Tried to initialize the wayland data control protocol clipboard, but failed. Falling back to the X11 clipboard protocol. The error was: {}",
100 e
101 ),
102 }
103 }
104 }
105 Ok(Self::X11(x11::Clipboard::new()?))
106 }
107}
108
109pub(crate) struct Get<'clipboard> {
110 clipboard: &'clipboard mut Clipboard,
111 selection: LinuxClipboardKind,
112}
113
114impl<'clipboard> Get<'clipboard> {
115 pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
116 Self { clipboard, selection: LinuxClipboardKind::Clipboard }
117 }
118
119 pub(crate) fn text(self) -> Result<String, Error> {
120 match self.clipboard {
121 Clipboard::X11(clipboard) => clipboard.get_text(self.selection),
122 #[cfg(feature = "wayland-data-control")]
123 Clipboard::WlDataControl(clipboard) => clipboard.get_text(self.selection),
124 }
125 }
126
127 #[cfg(feature = "image-data")]
128 pub(crate) fn image(self) -> Result<ImageData<'static>, Error> {
129 match self.clipboard {
130 Clipboard::X11(clipboard) => clipboard.get_image(self.selection),
131 #[cfg(feature = "wayland-data-control")]
132 Clipboard::WlDataControl(clipboard) => clipboard.get_image(self.selection),
133 }
134 }
135
136 pub(crate) fn html(self) -> Result<String, Error> {
137 match self.clipboard {
138 Clipboard::X11(clipboard) => clipboard.get_html(self.selection),
139 #[cfg(feature = "wayland-data-control")]
140 Clipboard::WlDataControl(clipboard) => clipboard.get_html(self.selection),
141 }
142 }
143
144 pub(crate) fn file_list(self) -> Result<Vec<PathBuf>, Error> {
145 match self.clipboard {
146 Clipboard::X11(clipboard) => clipboard.get_file_list(self.selection),
147 #[cfg(feature = "wayland-data-control")]
148 Clipboard::WlDataControl(clipboard) => clipboard.get_file_list(self.selection),
149 }
150 }
151}
152
153pub trait GetExtLinux: private::Sealed {
155 fn clipboard(self, selection: LinuxClipboardKind) -> Self;
160}
161
162impl GetExtLinux for crate::Get<'_> {
163 fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
164 self.platform.selection = selection;
165 self
166 }
167}
168
169#[derive(Default)]
171pub(crate) enum WaitConfig {
172 Until(Instant),
174
175 Forever,
177
178 #[default]
180 None,
181}
182
183pub(crate) struct Set<'clipboard> {
184 clipboard: &'clipboard mut Clipboard,
185 wait: WaitConfig,
186 selection: LinuxClipboardKind,
187}
188
189impl<'clipboard> Set<'clipboard> {
190 pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
191 Self { clipboard, wait: WaitConfig::default(), selection: LinuxClipboardKind::Clipboard }
192 }
193
194 pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> {
195 match self.clipboard {
196 Clipboard::X11(clipboard) => clipboard.set_text(text, self.selection, self.wait),
197
198 #[cfg(feature = "wayland-data-control")]
199 Clipboard::WlDataControl(clipboard) => clipboard.set_text(text, self.selection, self.wait),
200 }
201 }
202
203 pub(crate) fn html(self, html: Cow<'_, str>, alt: Option<Cow<'_, str>>) -> Result<(), Error> {
204 match self.clipboard {
205 Clipboard::X11(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait),
206
207 #[cfg(feature = "wayland-data-control")]
208 Clipboard::WlDataControl(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait),
209 }
210 }
211
212 #[cfg(feature = "image-data")]
213 pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> {
214 match self.clipboard {
215 Clipboard::X11(clipboard) => clipboard.set_image(image, self.selection, self.wait),
216
217 #[cfg(feature = "wayland-data-control")]
218 Clipboard::WlDataControl(clipboard) => clipboard.set_image(image, self.selection, self.wait),
219 }
220 }
221}
222
223pub trait SetExtLinux: private::Sealed {
225 fn wait(self) -> Self;
251
252 fn wait_until(self, deadline: Instant) -> Self;
261
262 fn clipboard(self, selection: LinuxClipboardKind) -> Self;
283}
284
285impl SetExtLinux for crate::Set<'_> {
286 fn wait(mut self) -> Self {
287 self.platform.wait = WaitConfig::Forever;
288 self
289 }
290
291 fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
292 self.platform.selection = selection;
293 self
294 }
295
296 fn wait_until(mut self, deadline: Instant) -> Self {
297 self.platform.wait = WaitConfig::Until(deadline);
298 self
299 }
300}
301
302pub(crate) struct Clear<'clipboard> {
303 clipboard: &'clipboard mut Clipboard,
304}
305
306impl<'clipboard> Clear<'clipboard> {
307 pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
308 Self { clipboard }
309 }
310
311 pub(crate) fn clear(self) -> Result<(), Error> {
312 self.clear_inner(LinuxClipboardKind::Clipboard)
313 }
314
315 fn clear_inner(self, selection: LinuxClipboardKind) -> Result<(), Error> {
316 let mut set = Set::new(self.clipboard);
317 set.selection = selection;
318
319 set.text(Cow::Borrowed(""))
320 }
321}
322
323pub trait ClearExtLinux: private::Sealed {
325 fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error>;
344}
345
346impl ClearExtLinux for crate::Clear<'_> {
347 fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error> {
348 self.platform.clear_inner(selection)
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn test_decoding_uri_list() {
358 let file_list = vec![
361 "file:///tmp/bar.log",
362 "file:///tmp/test%5C.txt",
363 "file:///tmp/foo%3F.png",
364 "file:///tmp/white%20space.txt",
365 ];
366
367 let paths = vec![
368 PathBuf::from("/tmp/bar.log"),
369 PathBuf::from("/tmp/test\\.txt"),
370 PathBuf::from("/tmp/foo?.png"),
371 PathBuf::from("/tmp/white space.txt"),
372 ];
373 assert_eq!(paths_from_uri_list(file_list.join("\n")), paths);
374 }
375}