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
 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}