1use std::{
2 fs::File,
3 io::{ErrorKind, Write},
4 os::fd::{AsRawFd, BorrowedFd, OwnedFd},
5};
6
7use calloop::{LoopHandle, PostAction};
8use filedescriptor::Pipe;
9use strum::IntoEnumIterator;
10use wayland_client::{Connection, protocol::wl_data_offer::WlDataOffer};
11use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1;
12
13use crate::{
14 ClipboardEntry, ClipboardItem, Image, ImageFormat, WaylandClientStatePtr, hash,
15 platform::linux::platform::read_fd,
16};
17
18/// Text mime types that we'll offer to other programs.
19pub(crate) const TEXT_MIME_TYPES: [&str; 3] =
20 ["text/plain;charset=utf-8", "UTF8_STRING", "text/plain"];
21pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list";
22
23/// Text mime types that we'll accept from other programs.
24pub(crate) const ALLOWED_TEXT_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"];
25
26pub(crate) struct Clipboard {
27 connection: Connection,
28 loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
29 self_mime: String,
30
31 // Internal clipboard
32 contents: Option<ClipboardItem>,
33 primary_contents: Option<ClipboardItem>,
34
35 // External clipboard
36 cached_read: Option<ClipboardItem>,
37 current_offer: Option<DataOffer<WlDataOffer>>,
38 cached_primary_read: Option<ClipboardItem>,
39 current_primary_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
40}
41
42pub(crate) trait ReceiveData {
43 fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>);
44}
45
46impl ReceiveData for WlDataOffer {
47 fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) {
48 self.receive(mime_type, fd);
49 }
50}
51
52impl ReceiveData for ZwpPrimarySelectionOfferV1 {
53 fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) {
54 self.receive(mime_type, fd);
55 }
56}
57
58#[derive(Clone, Debug)]
59/// Wrapper for `WlDataOffer` and `ZwpPrimarySelectionOfferV1`, used to help track mime types.
60pub(crate) struct DataOffer<T: ReceiveData> {
61 pub inner: T,
62 mime_types: Vec<String>,
63}
64
65impl<T: ReceiveData> DataOffer<T> {
66 pub fn new(offer: T) -> Self {
67 Self {
68 inner: offer,
69 mime_types: Vec::new(),
70 }
71 }
72
73 pub fn add_mime_type(&mut self, mime_type: String) {
74 self.mime_types.push(mime_type)
75 }
76
77 fn has_mime_type(&self, mime_type: &str) -> bool {
78 self.mime_types.iter().any(|t| t == mime_type)
79 }
80
81 fn read_bytes(&self, connection: &Connection, mime_type: &str) -> Option<Vec<u8>> {
82 let pipe = Pipe::new().unwrap();
83 self.inner.receive_data(mime_type.to_string(), unsafe {
84 BorrowedFd::borrow_raw(pipe.write.as_raw_fd())
85 });
86 let fd = pipe.read;
87 drop(pipe.write);
88
89 connection.flush().unwrap();
90
91 match unsafe { read_fd(fd) } {
92 Ok(bytes) => Some(bytes),
93 Err(err) => {
94 log::error!("error reading clipboard pipe: {err:?}");
95 None
96 }
97 }
98 }
99
100 fn read_text(&self, connection: &Connection) -> Option<ClipboardItem> {
101 let mime_type = self.mime_types.iter().find(|&mime_type| {
102 ALLOWED_TEXT_MIME_TYPES
103 .iter()
104 .any(|&allowed| allowed == mime_type)
105 })?;
106 let bytes = self.read_bytes(connection, mime_type)?;
107 let text_content = match String::from_utf8(bytes) {
108 Ok(content) => content,
109 Err(e) => {
110 log::error!("Failed to convert clipboard content to UTF-8: {}", e);
111 return None;
112 }
113 };
114
115 // Normalize the text to unix line endings, otherwise
116 // copying from eg: firefox inserts a lot of blank
117 // lines, and that is super annoying.
118 let result = text_content.replace("\r\n", "\n");
119 Some(ClipboardItem::new_string(result))
120 }
121
122 fn read_image(&self, connection: &Connection) -> Option<ClipboardItem> {
123 for format in ImageFormat::iter() {
124 let mime_type = format.mime_type();
125 if !self.has_mime_type(mime_type) {
126 continue;
127 }
128
129 if let Some(bytes) = self.read_bytes(connection, mime_type) {
130 let id = hash(&bytes);
131 return Some(ClipboardItem {
132 entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
133 });
134 }
135 }
136 None
137 }
138}
139
140impl Clipboard {
141 pub fn new(
142 connection: Connection,
143 loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
144 ) -> Self {
145 Self {
146 connection,
147 loop_handle,
148 self_mime: format!("pid/{}", std::process::id()),
149
150 contents: None,
151 primary_contents: None,
152
153 cached_read: None,
154 current_offer: None,
155 cached_primary_read: None,
156 current_primary_offer: None,
157 }
158 }
159
160 pub fn set(&mut self, item: ClipboardItem) {
161 self.contents = Some(item);
162 }
163
164 pub fn set_primary(&mut self, item: ClipboardItem) {
165 self.primary_contents = Some(item);
166 }
167
168 pub fn set_offer(&mut self, data_offer: Option<DataOffer<WlDataOffer>>) {
169 self.cached_read = None;
170 self.current_offer = data_offer;
171 }
172
173 pub fn set_primary_offer(&mut self, data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>) {
174 self.cached_primary_read = None;
175 self.current_primary_offer = data_offer;
176 }
177
178 pub fn self_mime(&self) -> String {
179 self.self_mime.clone()
180 }
181
182 pub fn send(&self, _mime_type: String, fd: OwnedFd) {
183 if let Some(text) = self.contents.as_ref().and_then(|contents| contents.text()) {
184 self.send_internal(fd, text.as_bytes().to_owned());
185 }
186 }
187
188 pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) {
189 if let Some(text) = self
190 .primary_contents
191 .as_ref()
192 .and_then(|contents| contents.text())
193 {
194 self.send_internal(fd, text.as_bytes().to_owned());
195 }
196 }
197
198 pub fn read(&mut self) -> Option<ClipboardItem> {
199 let offer = self.current_offer.as_ref()?;
200 if let Some(cached) = self.cached_read.clone() {
201 return Some(cached);
202 }
203
204 if offer.has_mime_type(&self.self_mime) {
205 return self.contents.clone();
206 }
207
208 let item = offer
209 .read_text(&self.connection)
210 .or_else(|| offer.read_image(&self.connection))?;
211
212 self.cached_read = Some(item.clone());
213 Some(item)
214 }
215
216 pub fn read_primary(&mut self) -> Option<ClipboardItem> {
217 let offer = self.current_primary_offer.as_ref()?;
218 if let Some(cached) = self.cached_primary_read.clone() {
219 return Some(cached);
220 }
221
222 if offer.has_mime_type(&self.self_mime) {
223 return self.primary_contents.clone();
224 }
225
226 let item = offer
227 .read_text(&self.connection)
228 .or_else(|| offer.read_image(&self.connection))?;
229
230 self.cached_primary_read = Some(item.clone());
231 Some(item)
232 }
233
234 fn send_internal(&self, fd: OwnedFd, bytes: Vec<u8>) {
235 let mut written = 0;
236 self.loop_handle
237 .insert_source(
238 calloop::generic::Generic::new(
239 File::from(fd),
240 calloop::Interest::WRITE,
241 calloop::Mode::Level,
242 ),
243 move |_, file, _| {
244 let mut file = unsafe { file.get_mut() };
245 loop {
246 match file.write(&bytes[written..]) {
247 Ok(n) if written + n == bytes.len() => {
248 written += n;
249 break Ok(PostAction::Remove);
250 }
251 Ok(n) => written += n,
252 Err(err) if err.kind() == ErrorKind::WouldBlock => {
253 break Ok(PostAction::Continue);
254 }
255 Err(_) => break Ok(PostAction::Remove),
256 }
257 }
258 },
259 )
260 .unwrap();
261 }
262}