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 wayland_client::{protocol::wl_data_offer::WlDataOffer, Connection};
10use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1;
11
12use crate::{platform::linux::platform::read_fd, ClipboardItem, WaylandClientStatePtr};
13
14pub(crate) const TEXT_MIME_TYPE: &str = "text/plain;charset=utf-8";
15pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list";
16
17/// Text mime types that we'll accept from other programs.
18pub(crate) const ALLOWED_TEXT_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"];
19
20pub(crate) struct Clipboard {
21 connection: Connection,
22 loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
23 self_mime: String,
24
25 // Internal clipboard
26 contents: Option<ClipboardItem>,
27 primary_contents: Option<ClipboardItem>,
28
29 // External clipboard
30 cached_read: Option<ClipboardItem>,
31 current_offer: Option<DataOffer<WlDataOffer>>,
32 cached_primary_read: Option<ClipboardItem>,
33 current_primary_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
34}
35
36#[derive(Clone, Debug)]
37/// Wrapper for `WlDataOffer` and `ZwpPrimarySelectionOfferV1`, used to help track mime types.
38pub(crate) struct DataOffer<T> {
39 pub inner: T,
40 mime_types: Vec<String>,
41}
42
43impl<T> DataOffer<T> {
44 pub fn new(offer: T) -> Self {
45 Self {
46 inner: offer,
47 mime_types: Vec::new(),
48 }
49 }
50
51 pub fn add_mime_type(&mut self, mime_type: String) {
52 self.mime_types.push(mime_type)
53 }
54
55 pub fn has_mime_type(&self, mime_type: &str) -> bool {
56 self.mime_types.iter().any(|t| t == mime_type)
57 }
58
59 pub fn find_text_mime_type(&self) -> Option<String> {
60 for offered_mime_type in &self.mime_types {
61 if let Some(offer_text_mime_type) = ALLOWED_TEXT_MIME_TYPES
62 .into_iter()
63 .find(|text_mime_type| text_mime_type == offered_mime_type)
64 {
65 return Some(offer_text_mime_type.to_owned());
66 }
67 }
68 None
69 }
70}
71
72impl Clipboard {
73 pub fn new(
74 connection: Connection,
75 loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
76 ) -> Self {
77 Self {
78 connection,
79 loop_handle,
80 self_mime: format!("pid/{}", std::process::id()),
81
82 contents: None,
83 primary_contents: None,
84
85 cached_read: None,
86 current_offer: None,
87 cached_primary_read: None,
88 current_primary_offer: None,
89 }
90 }
91
92 pub fn set(&mut self, item: ClipboardItem) {
93 self.contents = Some(item);
94 }
95
96 pub fn set_primary(&mut self, item: ClipboardItem) {
97 self.primary_contents = Some(item);
98 }
99
100 pub fn set_offer(&mut self, data_offer: Option<DataOffer<WlDataOffer>>) {
101 self.cached_read = None;
102 self.current_offer = data_offer;
103 }
104
105 pub fn set_primary_offer(&mut self, data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>) {
106 self.cached_primary_read = None;
107 self.current_primary_offer = data_offer;
108 }
109
110 pub fn self_mime(&self) -> String {
111 self.self_mime.clone()
112 }
113
114 pub fn send(&self, _mime_type: String, fd: OwnedFd) {
115 if let Some(text) = self.contents.as_ref().and_then(|contents| contents.text()) {
116 self.send_internal(fd, text.as_bytes().to_owned());
117 }
118 }
119
120 pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) {
121 if let Some(text) = self
122 .primary_contents
123 .as_ref()
124 .and_then(|contents| contents.text())
125 {
126 self.send_internal(fd, text.as_bytes().to_owned());
127 }
128 }
129
130 pub fn read(&mut self) -> Option<ClipboardItem> {
131 let offer = self.current_offer.clone()?;
132 if let Some(cached) = self.cached_read.clone() {
133 return Some(cached);
134 }
135
136 if offer.has_mime_type(&self.self_mime) {
137 return self.contents.clone();
138 }
139
140 let mime_type = offer.find_text_mime_type()?;
141 let pipe = Pipe::new().unwrap();
142 offer.inner.receive(mime_type, unsafe {
143 BorrowedFd::borrow_raw(pipe.write.as_raw_fd())
144 });
145 let fd = pipe.read;
146 drop(pipe.write);
147
148 self.connection.flush().unwrap();
149
150 match unsafe { read_fd(fd) } {
151 Ok(v) => {
152 self.cached_read = Some(ClipboardItem::new_string(v));
153 self.cached_read.clone()
154 }
155 Err(err) => {
156 log::error!("error reading clipboard pipe: {err:?}");
157 None
158 }
159 }
160 }
161
162 pub fn read_primary(&mut self) -> Option<ClipboardItem> {
163 let offer = self.current_primary_offer.clone()?;
164 if let Some(cached) = self.cached_primary_read.clone() {
165 return Some(cached);
166 }
167
168 if offer.has_mime_type(&self.self_mime) {
169 return self.primary_contents.clone();
170 }
171
172 let mime_type = offer.find_text_mime_type()?;
173 let pipe = Pipe::new().unwrap();
174 offer.inner.receive(mime_type, unsafe {
175 BorrowedFd::borrow_raw(pipe.write.as_raw_fd())
176 });
177 let fd = pipe.read;
178 drop(pipe.write);
179
180 self.connection.flush().unwrap();
181
182 match unsafe { read_fd(fd) } {
183 Ok(v) => {
184 self.cached_primary_read = Some(ClipboardItem::new_string(v.clone()));
185 self.cached_primary_read.clone()
186 }
187 Err(err) => {
188 log::error!("error reading clipboard pipe: {err:?}");
189 None
190 }
191 }
192 }
193
194 fn send_internal(&self, fd: OwnedFd, bytes: Vec<u8>) {
195 let mut written = 0;
196 self.loop_handle
197 .insert_source(
198 calloop::generic::Generic::new(
199 File::from(fd),
200 calloop::Interest::WRITE,
201 calloop::Mode::Level,
202 ),
203 move |_, file, _| {
204 let mut file = unsafe { file.get_mut() };
205 loop {
206 match file.write(&bytes[written..]) {
207 Ok(n) if written + n == bytes.len() => {
208 written += n;
209 break Ok(PostAction::Remove);
210 }
211 Ok(n) => written += n,
212 Err(err) if err.kind() == ErrorKind::WouldBlock => {
213 break Ok(PostAction::Continue)
214 }
215 Err(_) => break Ok(PostAction::Remove),
216 }
217 }
218 },
219 )
220 .unwrap();
221 }
222}