clipboard.rs

  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}