diff --git a/crates/gpui/src/platform/linux/x11.rs b/crates/gpui/src/platform/linux/x11.rs index 6df8e9a3d6..5c7a0c2ac8 100644 --- a/crates/gpui/src/platform/linux/x11.rs +++ b/crates/gpui/src/platform/linux/x11.rs @@ -1,4 +1,5 @@ mod client; +mod clipboard; mod display; mod event; mod window; diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index b0fbf4276d..3c2e24b0c9 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1,4 +1,3 @@ -use crate::platform::scap_screen_capture::scap_screen_sources; use core::str; use std::{ cell::RefCell, @@ -41,8 +40,9 @@ use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSIO use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask, STATE_LAYOUT_EFFECTIVE}; use super::{ - ButtonOrScroll, ScrollDirection, button_or_scroll_from_event_detail, get_valuator_axis_index, - modifiers_from_state, pressed_button_from_mask, + ButtonOrScroll, ScrollDirection, button_or_scroll_from_event_detail, + clipboard::{self, Clipboard}, + get_valuator_axis_index, modifiers_from_state, pressed_button_from_mask, }; use super::{X11Display, X11WindowStatePtr, XcbAtoms}; use super::{XimCallbackEvent, XimHandler}; @@ -56,6 +56,7 @@ use crate::platform::{ reveal_path_internal, xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, }, + scap_screen_capture::scap_screen_sources, }; use crate::{ AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke, @@ -201,7 +202,7 @@ pub struct X11ClientState { pointer_device_states: BTreeMap, pub(crate) common: LinuxCommon, - pub(crate) clipboard: x11_clipboard::Clipboard, + pub(crate) clipboard: Clipboard, pub(crate) clipboard_item: Option, pub(crate) xdnd_state: Xdnd, } @@ -388,7 +389,7 @@ impl X11Client { .reply() .unwrap(); - let clipboard = x11_clipboard::Clipboard::new().unwrap(); + let clipboard = Clipboard::new().unwrap(); let xcb_connection = Rc::new(xcb_connection); @@ -1504,39 +1505,36 @@ impl LinuxClient for X11Client { let state = self.0.borrow_mut(); state .clipboard - .store( - state.clipboard.setter.atoms.primary, - state.clipboard.setter.atoms.utf8_string, - item.text().unwrap_or_default().as_bytes(), + .set_text( + std::borrow::Cow::Owned(item.text().unwrap_or_default()), + clipboard::ClipboardKind::Primary, + clipboard::WaitConfig::None, ) - .ok(); + .context("Failed to write to clipboard (primary)") + .log_with_level(log::Level::Debug); } fn write_to_clipboard(&self, item: crate::ClipboardItem) { let mut state = self.0.borrow_mut(); state .clipboard - .store( - state.clipboard.setter.atoms.clipboard, - state.clipboard.setter.atoms.utf8_string, - item.text().unwrap_or_default().as_bytes(), + .set_text( + std::borrow::Cow::Owned(item.text().unwrap_or_default()), + clipboard::ClipboardKind::Clipboard, + clipboard::WaitConfig::None, ) - .ok(); + .context("Failed to write to clipboard (clipboard)") + .log_with_level(log::Level::Debug); state.clipboard_item.replace(item); } fn read_from_primary(&self) -> Option { let state = self.0.borrow_mut(); - state + return state .clipboard - .load( - state.clipboard.getter.atoms.primary, - state.clipboard.getter.atoms.utf8_string, - state.clipboard.getter.atoms.property, - Duration::from_secs(3), - ) - .map(|text| crate::ClipboardItem::new_string(String::from_utf8(text).unwrap())) - .ok() + .get_any(clipboard::ClipboardKind::Primary) + .context("Failed to read from clipboard (primary)") + .log_with_level(log::Level::Debug); } fn read_from_clipboard(&self) -> Option { @@ -1545,26 +1543,15 @@ impl LinuxClient for X11Client { // which has metadata attached. if state .clipboard - .setter - .connection - .get_selection_owner(state.clipboard.setter.atoms.clipboard) - .ok() - .and_then(|r| r.reply().ok()) - .map(|reply| reply.owner == state.clipboard.setter.window) - .unwrap_or(false) + .is_owner(clipboard::ClipboardKind::Clipboard) { return state.clipboard_item.clone(); } - state + return state .clipboard - .load( - state.clipboard.getter.atoms.clipboard, - state.clipboard.getter.atoms.utf8_string, - state.clipboard.getter.atoms.property, - Duration::from_secs(3), - ) - .map(|text| crate::ClipboardItem::new_string(String::from_utf8(text).unwrap())) - .ok() + .get_any(clipboard::ClipboardKind::Clipboard) + .context("Failed to read from clipboard (clipboard)") + .log_with_level(log::Level::Debug); } fn run(&self) { diff --git a/crates/gpui/src/platform/linux/x11/clipboard.rs b/crates/gpui/src/platform/linux/x11/clipboard.rs index 497794bb11..5d42eadaaf 100644 --- a/crates/gpui/src/platform/linux/x11/clipboard.rs +++ b/crates/gpui/src/platform/linux/x11/clipboard.rs @@ -200,7 +200,7 @@ struct ClipboardData { } enum ReadSelNotifyResult { - GotData(Vec), + GotData(ClipboardData), IncrStarted, EventNotRecognized, } @@ -297,30 +297,83 @@ impl Inner { } let reader = XContext::new()?; - log::trace!("Trying to get the clipboard data."); + let highest_precedence_format = + match self.read_single(&reader, selection, self.atoms.TARGETS) { + Err(err) => { + log::trace!("Clipboard TARGETS query failed with {err:?}"); + None + } + Ok(ClipboardData { bytes, format }) => { + if format == self.atoms.ATOM { + let available_formats = Self::parse_formats(&bytes); + formats + .iter() + .find(|format| available_formats.contains(format)) + } else { + log::trace!( + "Unexpected clipboard TARGETS format {}", + self.atom_name(format) + ); + None + } + } + }; + + if let Some(&format) = highest_precedence_format { + let data = self.read_single(&reader, selection, format)?; + if !formats.contains(&data.format) { + // This shouldn't happen since the format is from the TARGETS list. + log::trace!( + "Conversion to {} responded with {} which is not supported", + self.atom_name(format), + self.atom_name(data.format), + ); + return Err(Error::ConversionFailure); + } + return Ok(data); + } + + log::trace!("Falling back on attempting to convert clipboard to each format."); for format in formats { match self.read_single(&reader, selection, *format) { - Ok(bytes) => { - return Ok(ClipboardData { - bytes, - format: *format, - }); + Ok(data) => { + if formats.contains(&data.format) { + return Ok(data); + } else { + log::trace!( + "Conversion to {} responded with {} which is not supported", + self.atom_name(*format), + self.atom_name(data.format), + ); + continue; + } } Err(Error::ContentNotAvailable) => { continue; } - Err(e) => return Err(e), + Err(e) => { + log::trace!("Conversion to {} failed: {}", self.atom_name(*format), e); + return Err(e); + } } } + log::trace!("All conversions to supported formats failed."); Err(Error::ContentNotAvailable) } + fn parse_formats(bytes: &[u8]) -> Vec { + bytes + .chunks_exact(4) + .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect() + } + fn read_single( &self, reader: &XContext, selection: ClipboardKind, target_format: Atom, - ) -> Result> { + ) -> Result { // Delete the property so that we can detect (using property notify) // when the selection owner receives our request. reader @@ -392,10 +445,16 @@ impl Inner { event, )?; if result { - return Ok(incr_data); + return Ok(ClipboardData { + bytes: incr_data, + format: target_format, + }); } } - _ => log::trace!("An unexpected event arrived while reading the clipboard."), + _ => log::trace!( + "An unexpected event arrived while reading the clipboard: {:?}", + event + ), } } log::info!("Time-out hit while reading the clipboard."); @@ -440,7 +499,7 @@ impl Inner { Ok(current == self.server.win_id) } - fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result { + fn query_atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result { String::from_utf8( self.server .conn @@ -453,14 +512,14 @@ impl Inner { .map_err(into_unknown) } - fn atom_name_dbg(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str { + fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str { ATOM_NAME_CACHE.with(|cache| { let mut cache = cache.borrow_mut(); match cache.entry(atom) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { let s = self - .atom_name(atom) + .query_atom_name(atom) .map(|s| Box::leak(s.into_boxed_str()) as &str) .unwrap_or("FAILED-TO-GET-THE-ATOM-NAME"); entry.insert(s); @@ -496,6 +555,12 @@ impl Inner { log::warn!("Received a SelectionNotify while already expecting INCR segments."); return Ok(ReadSelNotifyResult::EventNotRecognized); } + // Accept any property type. The property type will typically match the format type except + // when it is `TARGETS` in which case it is `ATOM`. `ANY` is provided to handle the case + // where the clipboard is not convertible to the requested format. In this case + // `reply.type_` will have format information, but `bytes` will only be non-empty if `ANY` + // is provided. + let property_type = AtomEnum::ANY; // request the selection let mut reply = reader .conn @@ -503,7 +568,7 @@ impl Inner { true, event.requestor, event.property, - event.target, + property_type, 0, u32::MAX / 4, ) @@ -511,12 +576,8 @@ impl Inner { .reply() .map_err(into_unknown)?; - //log::trace!("Property.type: {:?}", self.atom_name(reply.type_)); - // we found something - if reply.type_ == target_format { - Ok(ReadSelNotifyResult::GotData(reply.value)) - } else if reply.type_ == self.atoms.INCR { + if reply.type_ == self.atoms.INCR { // Note that we call the get_property again because we are // indicating that we are ready to receive the data by deleting the // property, however deleting only works if the type matches the @@ -545,8 +606,10 @@ impl Inner { } Ok(ReadSelNotifyResult::IncrStarted) } else { - // this should never happen, we have sent a request only for supported types - Err(Error::unknown("incorrect type received from clipboard")) + Ok(ReadSelNotifyResult::GotData(ClipboardData { + bytes: reply.value, + format: reply.type_, + })) } } @@ -574,7 +637,11 @@ impl Inner { true, event.window, event.atom, - target_format, + if target_format == self.atoms.TARGETS { + self.atoms.ATOM + } else { + target_format + }, 0, u32::MAX / 4, ) @@ -612,7 +679,7 @@ impl Inner { if event.target == self.atoms.TARGETS { log::trace!( "Handling TARGETS, dst property is {}", - self.atom_name_dbg(event.property) + self.atom_name(event.property) ); let mut targets = Vec::with_capacity(10); targets.push(self.atoms.TARGETS); @@ -812,8 +879,8 @@ fn serve_requests(context: Arc) -> Result<(), Box> Event::SelectionRequest(event) => { log::trace!( "SelectionRequest - selection is: {}, target is {}", - context.atom_name_dbg(event.selection), - context.atom_name_dbg(event.target), + context.atom_name(event.selection), + context.atom_name(event.target), ); // Someone is requesting the clipboard content from us. context @@ -987,6 +1054,11 @@ impl Clipboard { let result = self.inner.read(&format_atoms, selection)?; + log::trace!( + "read clipboard as format {:?}", + self.inner.atom_name(result.format) + ); + for (format_atom, image_format) in image_format_atoms.into_iter().zip(image_formats) { if result.format == format_atom { let bytes = result.bytes;