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