From 837756198f91e8b33238a0d186d9a4685e4d5b08 Mon Sep 17 00:00:00 2001 From: maan2003 <49202620+maan2003@users.noreply.github.com> Date: Tue, 1 Oct 2024 04:55:32 +0530 Subject: [PATCH] linux/wayland: Add support for pasting images (#17671) Release Notes: - You can now paste images into the Assistant Panel to include them as context on Linux wayland --- crates/gpui/src/platform/linux/platform.rs | 14 +- .../gpui/src/platform/linux/wayland/client.rs | 9 +- .../src/platform/linux/wayland/clipboard.rs | 148 ++++++++++++------ 3 files changed, 106 insertions(+), 65 deletions(-) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index a0bd6b1d33d176feaabef386b8380f724c8f30aa..67f1a43cbe322088ec7f27dc0587abed0180dabf 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -603,17 +603,11 @@ pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option Result { +pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result> { let mut file = File::from_raw_fd(fd.as_raw_fd()); - - let mut buffer = String::new(); - file.read_to_string(&mut buffer)?; - - // Normalize the text to unix line endings, otherwise - // copying from eg: firefox inserts a lot of blank - // lines, and that is super annoying. - let result = buffer.replace("\r\n", "\n"); - Ok(result) + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer)?; + Ok(buffer) } impl CursorStyle { diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index ba4971b63af6d267dfd8cf8b9fbc490f0496188f..f7ade828615f4f8188aa0289f00f06bbab273bd8 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1799,10 +1799,11 @@ impl Dispatch for WaylandClientStatePtr { let fd = pipe.read; drop(pipe.write); - let read_task = state - .common - .background_executor - .spawn(async { unsafe { read_fd(fd) } }); + let read_task = state.common.background_executor.spawn(async { + let buffer = unsafe { read_fd(fd)? }; + let text = String::from_utf8(buffer)?; + anyhow::Ok(text) + }); let this = this.clone(); state diff --git a/crates/gpui/src/platform/linux/wayland/clipboard.rs b/crates/gpui/src/platform/linux/wayland/clipboard.rs index 26b5256bddcd43dfd1ced9597216d2d56fad7224..b3ec52d20515f94f5fda900448acdd4f5bea66a7 100644 --- a/crates/gpui/src/platform/linux/wayland/clipboard.rs +++ b/crates/gpui/src/platform/linux/wayland/clipboard.rs @@ -6,10 +6,14 @@ use std::{ use calloop::{LoopHandle, PostAction}; use filedescriptor::Pipe; +use strum::IntoEnumIterator; use wayland_client::{protocol::wl_data_offer::WlDataOffer, Connection}; use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1; -use crate::{platform::linux::platform::read_fd, ClipboardItem, WaylandClientStatePtr}; +use crate::{ + hash, platform::linux::platform::read_fd, ClipboardEntry, ClipboardItem, Image, ImageFormat, + WaylandClientStatePtr, +}; pub(crate) const TEXT_MIME_TYPE: &str = "text/plain;charset=utf-8"; pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list"; @@ -33,14 +37,30 @@ pub(crate) struct Clipboard { current_primary_offer: Option>, } +pub(crate) trait ReceiveData { + fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>); +} + +impl ReceiveData for WlDataOffer { + fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) { + self.receive(mime_type, fd); + } +} + +impl ReceiveData for ZwpPrimarySelectionOfferV1 { + fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) { + self.receive(mime_type, fd); + } +} + #[derive(Clone, Debug)] /// Wrapper for `WlDataOffer` and `ZwpPrimarySelectionOfferV1`, used to help track mime types. -pub(crate) struct DataOffer { +pub(crate) struct DataOffer { pub inner: T, mime_types: Vec, } -impl DataOffer { +impl DataOffer { pub fn new(offer: T) -> Self { Self { inner: offer, @@ -52,17 +72,71 @@ impl DataOffer { self.mime_types.push(mime_type) } - pub fn has_mime_type(&self, mime_type: &str) -> bool { + fn has_mime_type(&self, mime_type: &str) -> bool { self.mime_types.iter().any(|t| t == mime_type) } - pub fn find_text_mime_type(&self) -> Option { - for offered_mime_type in &self.mime_types { - if let Some(offer_text_mime_type) = ALLOWED_TEXT_MIME_TYPES - .into_iter() - .find(|text_mime_type| text_mime_type == offered_mime_type) - { - return Some(offer_text_mime_type.to_owned()); + fn read_bytes(&self, connection: &Connection, mime_type: &str) -> Option> { + let pipe = Pipe::new().unwrap(); + self.inner.receive_data(mime_type.to_string(), unsafe { + BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) + }); + let fd = pipe.read; + drop(pipe.write); + + connection.flush().unwrap(); + + match unsafe { read_fd(fd) } { + Ok(bytes) => Some(bytes), + Err(err) => { + log::error!("error reading clipboard pipe: {err:?}"); + None + } + } + } + + fn read_text(&self, connection: &Connection) -> Option { + let mime_type = self.mime_types.iter().find(|&mime_type| { + ALLOWED_TEXT_MIME_TYPES + .iter() + .any(|&allowed| allowed == mime_type) + })?; + let bytes = self.read_bytes(connection, mime_type)?; + let text_content = match String::from_utf8(bytes) { + Ok(content) => content, + Err(e) => { + log::error!("Failed to convert clipboard content to UTF-8: {}", e); + return None; + } + }; + + // Normalize the text to unix line endings, otherwise + // copying from eg: firefox inserts a lot of blank + // lines, and that is super annoying. + let result = text_content.replace("\r\n", "\n"); + Some(ClipboardItem::new_string(result)) + } + + fn read_image(&self, connection: &Connection) -> Option { + for format in ImageFormat::iter() { + let mime_type = match format { + ImageFormat::Png => "image/png", + ImageFormat::Jpeg => "image/jpeg", + ImageFormat::Webp => "image/webp", + ImageFormat::Gif => "image/gif", + ImageFormat::Svg => "image/svg+xml", + ImageFormat::Bmp => "image/bmp", + ImageFormat::Tiff => "image/tiff", + }; + if !self.has_mime_type(mime_type) { + continue; + } + + if let Some(bytes) = self.read_bytes(connection, mime_type) { + let id = hash(&bytes); + return Some(ClipboardItem { + entries: vec![ClipboardEntry::Image(Image { format, bytes, id })], + }); } } None @@ -128,7 +202,7 @@ impl Clipboard { } pub fn read(&mut self) -> Option { - let offer = self.current_offer.clone()?; + let offer = self.current_offer.as_ref()?; if let Some(cached) = self.cached_read.clone() { return Some(cached); } @@ -137,30 +211,16 @@ impl Clipboard { return self.contents.clone(); } - let mime_type = offer.find_text_mime_type()?; - let pipe = Pipe::new().unwrap(); - offer.inner.receive(mime_type, unsafe { - BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) - }); - let fd = pipe.read; - drop(pipe.write); - - self.connection.flush().unwrap(); + let item = offer + .read_text(&self.connection) + .or_else(|| offer.read_image(&self.connection))?; - match unsafe { read_fd(fd) } { - Ok(v) => { - self.cached_read = Some(ClipboardItem::new_string(v)); - self.cached_read.clone() - } - Err(err) => { - log::error!("error reading clipboard pipe: {err:?}"); - None - } - } + self.cached_read = Some(item.clone()); + Some(item) } pub fn read_primary(&mut self) -> Option { - let offer = self.current_primary_offer.clone()?; + let offer = self.current_primary_offer.as_ref()?; if let Some(cached) = self.cached_primary_read.clone() { return Some(cached); } @@ -169,26 +229,12 @@ impl Clipboard { return self.primary_contents.clone(); } - let mime_type = offer.find_text_mime_type()?; - let pipe = Pipe::new().unwrap(); - offer.inner.receive(mime_type, unsafe { - BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) - }); - let fd = pipe.read; - drop(pipe.write); - - self.connection.flush().unwrap(); + let item = offer + .read_text(&self.connection) + .or_else(|| offer.read_image(&self.connection))?; - match unsafe { read_fd(fd) } { - Ok(v) => { - self.cached_primary_read = Some(ClipboardItem::new_string(v.clone())); - self.cached_primary_read.clone() - } - Err(err) => { - log::error!("error reading clipboard pipe: {err:?}"); - None - } - } + self.cached_primary_read = Some(item.clone()); + Some(item) } fn send_internal(&self, fd: OwnedFd, bytes: Vec) {