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, ServeRequests, 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 options(wait: WaitConfig, selection: LinuxClipboardKind) -> Result<Options, Error> {
64 let mut opts = Options::new();
65 opts.foreground(matches!(wait, WaitConfig::Forever));
66 if matches!(wait, WaitConfig::Once) {
67 opts.serve_requests(ServeRequests::Only(1));
68 }
69 opts.clipboard(selection.try_into()?);
70 Ok(opts)
71}
72
73fn handle_copy_error(e: copy::Error) -> Error {
74 match e {
75 CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported,
76 other => into_unknown(other),
77 }
78}
79
80fn handle_paste_error(e: paste::Error) -> Error {
81 match e {
82 PasteError::PrimarySelectionUnsupported => Error::ClipboardNotSupported,
83 other => into_unknown(other),
84 }
85}
86
87fn handle_clipboard_read<T, F: FnOnce(Vec<u8>) -> Result<T, Error>>(
88 selection: LinuxClipboardKind,
89 mime: paste::MimeType,
90 into_requested_data: F,
91) -> Result<T, Error> {
92 let result = get_contents(selection.try_into()?, Seat::Unspecified, mime);
93 match result {
94 Ok((mut pipe, _)) => {
95 let mut buffer = vec![];
96 pipe.read_to_end(&mut buffer).map_err(into_unknown)?;
97 into_requested_data(buffer)
98 }
99 Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => {
100 Err(Error::ContentNotAvailable)
101 }
102 Err(err) => Err(handle_paste_error(err)),
103 }
104}
105
106impl Clipboard {
107 pub(crate) fn new() -> Result<Self, Error> {
108 match is_primary_selection_supported() {
110 Ok(_) => Ok(Self {}),
113 Err(e) => Err(into_unknown(e)),
114 }
115 }
116
117 pub(crate) fn clear(&mut self, selection: LinuxClipboardKind) -> Result<(), Error> {
118 let selection = selection.try_into()?;
119 copy::clear(selection, copy::Seat::All).map_err(handle_copy_error)
120 }
121
122 pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result<String, Error> {
123 handle_clipboard_read(selection, paste::MimeType::Text, |contents| {
124 String::from_utf8(contents).map_err(|_| Error::ConversionFailure)
125 })
126 }
127
128 pub(crate) fn set_text(
129 &self,
130 text: Cow<'_, str>,
131 selection: LinuxClipboardKind,
132 wait: WaitConfig,
133 exclude_from_history: bool,
134 ) -> Result<(), Error> {
135 let opts = options(wait, selection)?;
136 let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
137
138 sources.push(MimeSource {
139 source: Source::Bytes(text.into_owned().into_bytes().into_boxed_slice()),
140 mime_type: MimeType::Text,
141 });
142
143 add_clipboard_exclusions(exclude_from_history, &mut sources);
144
145 opts.copy_multi(sources).map_err(handle_copy_error)
146 }
147
148 pub(crate) fn get_html(&mut self, selection: LinuxClipboardKind) -> Result<String, Error> {
149 handle_clipboard_read(selection, paste::MimeType::Specific("text/html"), |contents| {
150 String::from_utf8(contents).map_err(|_| Error::ConversionFailure)
151 })
152 }
153
154 pub(crate) fn set_html(
155 &self,
156 html: Cow<'_, str>,
157 alt: Option<Cow<'_, str>>,
158 selection: LinuxClipboardKind,
159 wait: WaitConfig,
160 exclude_from_history: bool,
161 ) -> Result<(), Error> {
162 let opts = options(wait, selection)?;
163 let mut sources = {
164 let cap = [true, alt.is_some(), exclude_from_history]
165 .map(|v| usize::from(v as u8))
166 .iter()
167 .sum();
168 Vec::with_capacity(cap)
169 };
170
171 if let Some(alt) = alt {
172 sources.push(MimeSource {
173 source: Source::Bytes(alt.into_owned().into_bytes().into_boxed_slice()),
174 mime_type: MimeType::Text,
175 });
176 }
177
178 sources.push(MimeSource {
179 source: Source::Bytes(html.into_owned().into_bytes().into_boxed_slice()),
180 mime_type: MimeType::Specific(String::from("text/html")),
181 });
182
183 add_clipboard_exclusions(exclude_from_history, &mut sources);
184
185 opts.copy_multi(sources).map_err(handle_copy_error)
186 }
187
188 #[cfg(feature = "image-data")]
189 pub(crate) fn get_image(
190 &mut self,
191 selection: LinuxClipboardKind,
192 ) -> Result<ImageData<'static>, Error> {
193 use std::io::Cursor;
194
195 handle_clipboard_read(selection, paste::MimeType::Specific(MIME_PNG), |buffer| {
196 let image = image::io::Reader::new(Cursor::new(buffer))
197 .with_guessed_format()
198 .map_err(|_| Error::ConversionFailure)?
199 .decode()
200 .map_err(|_| Error::ConversionFailure)?;
201 let image = image.into_rgba8();
202
203 Ok(ImageData {
204 width: image.width() as usize,
205 height: image.height() as usize,
206 bytes: image.into_raw().into(),
207 })
208 })
209 }
210
211 #[cfg(feature = "image-data")]
212 pub(crate) fn set_image(
213 &mut self,
214 image: ImageData,
215 selection: LinuxClipboardKind,
216 wait: WaitConfig,
217 exclude_from_history: bool,
218 ) -> Result<(), Error> {
219 let opts = options(wait, selection)?;
220 let image = encode_as_png(&image)?;
221
222 let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
223
224 sources.push(MimeSource {
225 source: Source::Bytes(image.into()),
226 mime_type: MimeType::Specific(String::from(MIME_PNG)),
227 });
228
229 add_clipboard_exclusions(exclude_from_history, &mut sources);
230
231 opts.copy_multi(sources).map_err(handle_copy_error)
232 }
233
234 pub(crate) fn get_file_list(
235 &mut self,
236 selection: LinuxClipboardKind,
237 ) -> Result<Vec<PathBuf>, Error> {
238 handle_clipboard_read(selection, paste::MimeType::Specific(MIME_URI), |contents| {
239 Ok(paths_from_uri_list(contents))
240 })
241 }
242
243 pub(crate) fn set_file_list(
244 &self,
245 file_list: &[impl AsRef<Path>],
246 selection: LinuxClipboardKind,
247 wait: WaitConfig,
248 exclude_from_history: bool,
249 ) -> Result<(), Error> {
250 let opts = options(wait, selection)?;
251 let files = paths_to_uri_list(file_list)?;
252 let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
253 sources.push(MimeSource {
254 source: Source::Bytes(files.into_bytes().into_boxed_slice()),
255 mime_type: MimeType::Specific(String::from(MIME_URI)),
256 });
257
258 add_clipboard_exclusions(exclude_from_history, &mut sources);
259
260 opts.copy_multi(sources).map_err(handle_copy_error)
261 }
262}