1use std::{
2 borrow::Cow,
3 io::Read,
4 path::{Path, PathBuf},
5};
6
7use wl_clipboard_rs::{
8 copy::{self, Error as CopyError, MimeSource, MimeType, Options, Source},
9 paste::{self, get_contents, Error as PasteError, Seat},
10 utils::is_primary_selection_supported,
11};
12
13#[cfg(feature = "image-data")]
14use super::encode_as_png;
15use super::{
16 into_unknown, paths_from_uri_list, paths_to_uri_list, LinuxClipboardKind, WaitConfig,
17 KDE_EXCLUSION_HINT, KDE_EXCLUSION_MIME,
18};
19use crate::common::Error;
20#[cfg(feature = "image-data")]
21use crate::common::ImageData;
22
23#[cfg(feature = "image-data")]
24const MIME_PNG: &str = "image/png";
25
26const MIME_URI: &str = "text/uri-list";
27
28pub(crate) struct Clipboard {}
29
30impl TryInto<copy::ClipboardType> for LinuxClipboardKind {
31 type Error = Error;
32
33 fn try_into(self) -> Result<copy::ClipboardType, Self::Error> {
34 match self {
35 LinuxClipboardKind::Clipboard => Ok(copy::ClipboardType::Regular),
36 LinuxClipboardKind::Primary => Ok(copy::ClipboardType::Primary),
37 LinuxClipboardKind::Secondary => Err(Error::ClipboardNotSupported),
38 }
39 }
40}
41
42impl TryInto<paste::ClipboardType> for LinuxClipboardKind {
43 type Error = Error;
44
45 fn try_into(self) -> Result<paste::ClipboardType, Self::Error> {
46 match self {
47 LinuxClipboardKind::Clipboard => Ok(paste::ClipboardType::Regular),
48 LinuxClipboardKind::Primary => Ok(paste::ClipboardType::Primary),
49 LinuxClipboardKind::Secondary => Err(Error::ClipboardNotSupported),
50 }
51 }
52}
53
54fn add_clipboard_exclusions(exclude_from_history: bool, sources: &mut Vec<MimeSource>) {
55 if exclude_from_history {
56 sources.push(MimeSource {
57 source: Source::Bytes(Box::from(KDE_EXCLUSION_HINT)),
58 mime_type: MimeType::Specific(String::from(KDE_EXCLUSION_MIME)),
59 });
60 }
61}
62
63fn handle_copy_error(e: copy::Error) -> Error {
64 match e {
65 CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported,
66 other => into_unknown(other),
67 }
68}
69
70fn handle_paste_error(e: paste::Error) -> Error {
71 match e {
72 PasteError::PrimarySelectionUnsupported => Error::ClipboardNotSupported,
73 other => into_unknown(other),
74 }
75}
76
77fn handle_clipboard_read<T, F: FnOnce(Vec<u8>) -> Result<T, Error>>(
78 selection: LinuxClipboardKind,
79 mime: paste::MimeType,
80 into_requested_data: F,
81) -> Result<T, Error> {
82 let result = get_contents(selection.try_into()?, Seat::Unspecified, mime);
83 match result {
84 Ok((mut pipe, _)) => {
85 let mut buffer = vec![];
86 pipe.read_to_end(&mut buffer).map_err(into_unknown)?;
87 into_requested_data(buffer)
88 }
89 Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => {
90 Err(Error::ContentNotAvailable)
91 }
92 Err(err) => Err(handle_paste_error(err)),
93 }
94}
95
96impl Clipboard {
97 pub(crate) fn new() -> Result<Self, Error> {
98 match is_primary_selection_supported() {
100 Ok(_) => Ok(Self {}),
103 Err(e) => Err(into_unknown(e)),
104 }
105 }
106
107 pub(crate) fn clear(&mut self, selection: LinuxClipboardKind) -> Result<(), Error> {
108 let selection = selection.try_into()?;
109 copy::clear(selection, copy::Seat::All).map_err(handle_copy_error)
110 }
111
112 pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result<String, Error> {
113 handle_clipboard_read(selection, paste::MimeType::Text, |contents| {
114 String::from_utf8(contents).map_err(|_| Error::ConversionFailure)
115 })
116 }
117
118 pub(crate) fn set_text(
119 &self,
120 text: Cow<'_, str>,
121 selection: LinuxClipboardKind,
122 wait: WaitConfig,
123 exclude_from_history: bool,
124 ) -> Result<(), Error> {
125 let mut opts = Options::new();
126 opts.foreground(matches!(wait, WaitConfig::Forever));
127 opts.clipboard(selection.try_into()?);
128
129 let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
130
131 sources.push(MimeSource {
132 source: Source::Bytes(text.into_owned().into_bytes().into_boxed_slice()),
133 mime_type: MimeType::Text,
134 });
135
136 add_clipboard_exclusions(exclude_from_history, &mut sources);
137
138 opts.copy_multi(sources).map_err(handle_copy_error)
139 }
140
141 pub(crate) fn get_html(&mut self, selection: LinuxClipboardKind) -> Result<String, Error> {
142 handle_clipboard_read(selection, paste::MimeType::Specific("text/html"), |contents| {
143 String::from_utf8(contents).map_err(|_| Error::ConversionFailure)
144 })
145 }
146
147 pub(crate) fn set_html(
148 &self,
149 html: Cow<'_, str>,
150 alt: Option<Cow<'_, str>>,
151 selection: LinuxClipboardKind,
152 wait: WaitConfig,
153 exclude_from_history: bool,
154 ) -> Result<(), Error> {
155 let mut opts = Options::new();
156 opts.foreground(matches!(wait, WaitConfig::Forever));
157 opts.clipboard(selection.try_into()?);
158
159 let mut sources = {
160 let cap = [true, alt.is_some(), exclude_from_history]
161 .map(|v| usize::from(v as u8))
162 .iter()
163 .sum();
164 Vec::with_capacity(cap)
165 };
166
167 if let Some(alt) = alt {
168 sources.push(MimeSource {
169 source: Source::Bytes(alt.into_owned().into_bytes().into_boxed_slice()),
170 mime_type: MimeType::Text,
171 });
172 }
173
174 sources.push(MimeSource {
175 source: Source::Bytes(html.into_owned().into_bytes().into_boxed_slice()),
176 mime_type: MimeType::Specific(String::from("text/html")),
177 });
178
179 add_clipboard_exclusions(exclude_from_history, &mut sources);
180
181 opts.copy_multi(sources).map_err(handle_copy_error)
182 }
183
184 #[cfg(feature = "image-data")]
185 pub(crate) fn get_image(
186 &mut self,
187 selection: LinuxClipboardKind,
188 ) -> Result<ImageData<'static>, Error> {
189 use std::io::Cursor;
190
191 handle_clipboard_read(selection, paste::MimeType::Specific(MIME_PNG), |buffer| {
192 let image = image::io::Reader::new(Cursor::new(buffer))
193 .with_guessed_format()
194 .map_err(|_| Error::ConversionFailure)?
195 .decode()
196 .map_err(|_| Error::ConversionFailure)?;
197 let image = image.into_rgba8();
198
199 Ok(ImageData {
200 width: image.width() as usize,
201 height: image.height() as usize,
202 bytes: image.into_raw().into(),
203 })
204 })
205 }
206
207 #[cfg(feature = "image-data")]
208 pub(crate) fn set_image(
209 &mut self,
210 image: ImageData,
211 selection: LinuxClipboardKind,
212 wait: WaitConfig,
213 exclude_from_history: bool,
214 ) -> Result<(), Error> {
215 let mut opts = Options::new();
216 opts.foreground(matches!(wait, WaitConfig::Forever));
217 opts.clipboard(selection.try_into()?);
218
219 let image = encode_as_png(&image)?;
220
221 let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
222
223 sources.push(MimeSource {
224 source: Source::Bytes(image.into()),
225 mime_type: MimeType::Specific(String::from(MIME_PNG)),
226 });
227
228 add_clipboard_exclusions(exclude_from_history, &mut sources);
229
230 opts.copy_multi(sources).map_err(handle_copy_error)
231 }
232
233 pub(crate) fn get_file_list(
234 &mut self,
235 selection: LinuxClipboardKind,
236 ) -> Result<Vec<PathBuf>, Error> {
237 handle_clipboard_read(selection, paste::MimeType::Specific(MIME_URI), |contents| {
238 Ok(paths_from_uri_list(contents))
239 })
240 }
241
242 pub(crate) fn set_file_list(
243 &self,
244 file_list: &[impl AsRef<Path>],
245 selection: LinuxClipboardKind,
246 wait: WaitConfig,
247 exclude_from_history: bool,
248 ) -> Result<(), Error> {
249 let files = paths_to_uri_list(file_list)?;
250
251 let mut opts = Options::new();
252 opts.foreground(matches!(wait, WaitConfig::Forever));
253 opts.clipboard(selection.try_into()?);
254
255 let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
256 sources.push(MimeSource {
257 source: Source::Bytes(files.into_bytes().into_boxed_slice()),
258 mime_type: MimeType::Specific(String::from(MIME_URI)),
259 });
260
261 add_clipboard_exclusions(exclude_from_history, &mut sources);
262
263 opts.copy_multi(sources).map_err(handle_copy_error)
264 }
265}