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