From f6fa6600bc0293707457f27f5849c3ce73bd985f Mon Sep 17 00:00:00 2001 From: apricotbucket28 <71973804+apricotbucket28@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:04:19 -0300 Subject: [PATCH] wayland: Refactor clipboard implementation (#12405) Fixes https://github.com/zed-industries/zed/issues/12054 Replaces the `copypasta`/`smithay-clipboard` implementation with a new, custom one TODO list: - [x] Cleanup code - [x] Remove `smithay-clipboard` - [x] Add more mime types to the supported list Release Notes: - Fixed drag and drop on Gnome - Fixed clipboard paste on Hyprland --- Cargo.lock | 106 +------- crates/editor/src/editor.rs | 2 +- crates/gpui/Cargo.toml | 7 +- crates/gpui/src/platform/linux/platform.rs | 1 - crates/gpui/src/platform/linux/wayland.rs | 1 + .../gpui/src/platform/linux/wayland/client.rs | 257 +++++++++++++++--- .../src/platform/linux/wayland/clipboard.rs | 218 +++++++++++++++ .../gpui/src/platform/linux/wayland/serial.rs | 1 + crates/gpui/src/platform/linux/x11/client.rs | 63 +++-- crates/gpui/src/platform/windows/platform.rs | 10 +- 10 files changed, 491 insertions(+), 175 deletions(-) create mode 100644 crates/gpui/src/platform/linux/wayland/clipboard.rs diff --git a/Cargo.lock b/Cargo.lock index d63e86cab7..bcb4885f66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1846,9 +1846,9 @@ dependencies = [ [[package]] name = "calloop" -version = "0.12.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ "bitflags 2.4.2", "log", @@ -1860,9 +1860,9 @@ dependencies = [ [[package]] name = "calloop-wayland-source" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", "rustix 0.38.32", @@ -2686,20 +2686,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "copypasta" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb85422867ca93da58b7f95fb5c0c10f6183ed6e1ef8841568968a896d3a858" -dependencies = [ - "clipboard-win", - "objc", - "objc-foundation", - "objc_id", - "smithay-clipboard", - "x11-clipboard", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -4792,9 +4778,9 @@ dependencies = [ "calloop", "calloop-wayland-source", "cbindgen", + "clipboard-win", "cocoa", "collections", - "copypasta", "core-foundation", "core-graphics", "core-text", @@ -4855,6 +4841,7 @@ dependencies = [ "wayland-protocols-plasma", "windows 0.57.0", "windows-core 0.57.0", + "x11-clipboard", "x11rb", "xim", "xkbcommon", @@ -6940,17 +6927,6 @@ dependencies = [ "objc_exception", ] -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - [[package]] name = "objc_exception" version = "0.1.2" @@ -6960,15 +6936,6 @@ dependencies = [ "cc", ] -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - [[package]] name = "object" version = "0.32.1" @@ -9685,42 +9652,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" -[[package]] -name = "smithay-client-toolkit" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" -dependencies = [ - "bitflags 2.4.2", - "calloop", - "calloop-wayland-source", - "cursor-icon", - "libc", - "log", - "memmap2 0.9.4", - "rustix 0.38.32", - "thiserror", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-clipboard" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" -dependencies = [ - "libc", - "smithay-client-toolkit", - "wayland-backend", -] - [[package]] name = "smol" version = "1.3.0" @@ -12383,17 +12314,6 @@ dependencies = [ "wayland-scanner", ] -[[package]] -name = "wayland-csd-frame" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" -dependencies = [ - "bitflags 2.4.2", - "cursor-icon", - "wayland-backend", -] - [[package]] name = "wayland-cursor" version = "0.31.1" @@ -12430,19 +12350,6 @@ dependencies = [ "wayland-scanner", ] -[[package]] -name = "wayland-protocols-wlr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" -dependencies = [ - "bitflags 2.4.2", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - [[package]] name = "wayland-scanner" version = "0.31.1" @@ -12462,7 +12369,6 @@ checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" dependencies = [ "dlib", "log", - "once_cell", "pkg-config", ] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 24e4ee020b..62f4caaa95 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2210,7 +2210,7 @@ impl Editor { // Copy selections to primary selection buffer #[cfg(target_os = "linux")] if local { - let selections = self.selections.all::(cx); + let selections = &self.selections.disjoint; let buffer_handle = self.buffer.read(cx).read(cx); let mut text = String::new(); diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 5561ba94bc..1092852359 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -104,13 +104,12 @@ blade-macros.workspace = true blade-util.workspace = true bytemuck = "1" cosmic-text = { git = "https://github.com/pop-os/cosmic-text", rev = "542b20c" } -copypasta = "0.10.1" [target.'cfg(target_os = "linux")'.dependencies] as-raw-xcb-connection = "1" ashpd.workspace = true -calloop = "0.12.4" -calloop-wayland-source = "0.2.0" +calloop = "0.13.0" +calloop-wayland-source = "0.3.0" wayland-backend = { version = "0.3.3", features = ["client_system"] } wayland-client = { version = "0.31.2" } wayland-cursor = "0.31.1" @@ -136,10 +135,12 @@ xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca "x11rb-xcb", "x11rb-client", ] } +x11-clipboard = "0.9.2" [target.'cfg(windows)'.dependencies] windows.workspace = true windows-core = "0.57" +clipboard-win = "3.1.1" [target.'cfg(windows)'.build-dependencies] embed-resource = "2.4" diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 513eafe0da..b28ad697ed 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -22,7 +22,6 @@ use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest}; use async_task::Runnable; use calloop::channel::Channel; use calloop::{EventLoop, LoopHandle, LoopSignal}; -use copypasta::ClipboardProvider; use filedescriptor::FileDescriptor; use flume::{Receiver, Sender}; use futures::channel::oneshot; diff --git a/crates/gpui/src/platform/linux/wayland.rs b/crates/gpui/src/platform/linux/wayland.rs index 61eb0bf498..e8594426fa 100644 --- a/crates/gpui/src/platform/linux/wayland.rs +++ b/crates/gpui/src/platform/linux/wayland.rs @@ -1,4 +1,5 @@ mod client; +mod clipboard; mod cursor; mod display; mod serial; diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 6729876f5c..004b8dbb90 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -10,8 +10,6 @@ use calloop::timer::{TimeoutAction, Timer}; use calloop::{EventLoop, LoopHandle}; use calloop_wayland_source::WaylandSource; use collections::HashMap; -use copypasta::wayland_clipboard::{create_clipboards_from_external, Clipboard, Primary}; -use copypasta::ClipboardProvider; use filedescriptor::Pipe; use smallvec::SmallVec; @@ -22,9 +20,10 @@ use wayland_client::event_created_child; use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContents}; use wayland_client::protocol::wl_callback::{self, WlCallback}; use wayland_client::protocol::wl_data_device_manager::DndAction; +use wayland_client::protocol::wl_data_offer::WlDataOffer; use wayland_client::protocol::wl_pointer::AxisSource; use wayland_client::protocol::{ - wl_data_device, wl_data_device_manager, wl_data_offer, wl_output, wl_region, + wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region, }; use wayland_client::{ delegate_noop, @@ -40,6 +39,13 @@ use wayland_protocols::wp::cursor_shape::v1::client::{ use wayland_protocols::wp::fractional_scale::v1::client::{ wp_fractional_scale_manager_v1, wp_fractional_scale_v1, }; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{ + self, ZwpPrimarySelectionOfferV1, +}; +use wayland_protocols::wp::primary_selection::zv1::client::{ + zwp_primary_selection_device_manager_v1, zwp_primary_selection_device_v1, + zwp_primary_selection_source_v1, +}; use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ ContentHint, ContentPurpose, }; @@ -60,6 +66,9 @@ use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL}; use super::display::WaylandDisplay; use super::window::{ImeInput, WaylandWindowStatePtr}; use crate::platform::linux::is_within_click_distance; +use crate::platform::linux::wayland::clipboard::{ + Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPE, +}; use crate::platform::linux::wayland::cursor::Cursor; use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker}; use crate::platform::linux::wayland::window::WaylandWindow; @@ -88,6 +97,8 @@ pub struct Globals { pub compositor: wl_compositor::WlCompositor, pub cursor_shape_manager: Option, pub data_device_manager: Option, + pub primary_selection_manager: + Option, pub wm_base: xdg_wm_base::XdgWmBase, pub shm: wl_shm::WlShm, pub seat: wl_seat::WlSeat, @@ -125,6 +136,7 @@ impl Globals { (), ) .ok(), + primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(), shm: globals.bind(&qh, 1..=1, ()).unwrap(), seat, wm_base: globals.bind(&qh, 1..=1, ()).unwrap(), @@ -177,6 +189,7 @@ pub(crate) struct WaylandClientState { wl_keyboard: Option, cursor_shape_device: Option, data_device: Option, + primary_selection: Option, text_input: Option, pre_edit_text: Option, composing: bool, @@ -204,13 +217,13 @@ pub(crate) struct WaylandClientState { keyboard_focused_window: Option, loop_handle: LoopHandle<'static, WaylandClientStatePtr>, cursor_style: Option, + clipboard: Clipboard, + data_offers: Vec>, + primary_data_offer: Option>, cursor: Cursor, - clipboard: Option, - primary: Option, + pending_open_uri: Option, event_loop: Option>, common: LinuxCommon, - - pending_open_uri: Option, } pub struct DragState { @@ -311,9 +324,6 @@ impl Drop for WaylandClient { let mut state = self.0.borrow_mut(); state.windows.clear(); - // Drop the clipboard to prevent a seg fault after we've closed all Wayland connections. - state.primary = None; - state.clipboard = None; if let Some(wl_pointer) = &state.wl_pointer { wl_pointer.release(); } @@ -395,8 +405,6 @@ impl WaylandClient { } }); - let display = conn.backend().display_ptr() as *mut std::ffi::c_void; - let event_loop = EventLoop::::try_new().unwrap(); let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); @@ -428,7 +436,10 @@ impl WaylandClient { .as_ref() .map(|data_device_manager| data_device_manager.get_data_device(&seat, &qh, ())); - let (primary, clipboard) = unsafe { create_clipboards_from_external(display) }; + let primary_selection = globals + .primary_selection_manager + .as_ref() + .map(|primary_selection_manager| primary_selection_manager.get_device(&seat, &qh, ())); let mut cursor = Cursor::new(&conn, &globals, 24); @@ -470,6 +481,7 @@ impl WaylandClient { wl_keyboard: None, cursor_shape_device: None, data_device, + primary_selection, text_input: None, pre_edit_text: None, composing: false, @@ -515,12 +527,12 @@ impl WaylandClient { loop_handle: handle.clone(), enter_token: None, cursor_style: None, + clipboard: Clipboard::new(conn.clone(), handle.clone()), + data_offers: Vec::new(), + primary_data_offer: None, cursor, - clipboard: Some(clipboard), - primary: Some(primary), - event_loop: Some(event_loop), - pending_open_uri: None, + event_loop: Some(event_loop), })); WaylandSource::new(conn, event_queue) @@ -651,33 +663,46 @@ impl LinuxClient for WaylandClient { } fn write_to_primary(&self, item: crate::ClipboardItem) { - self.0 - .borrow_mut() - .primary - .as_mut() - .unwrap() - .set_contents(item.text) - .ok(); + let mut state = self.0.borrow_mut(); + let (Some(primary_selection_manager), Some(primary_selection)) = ( + state.globals.primary_selection_manager.clone(), + state.primary_selection.clone(), + ) else { + return; + }; + if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() { + let serial = state.serial_tracker.get(SerialKind::KeyEnter); + let data_source = primary_selection_manager.create_source(&state.globals.qh, ()); + data_source.offer(state.clipboard.self_mime()); + data_source.offer(TEXT_MIME_TYPE.to_string()); + primary_selection.set_selection(Some(&data_source), serial); + state.clipboard.set_primary(item.text); + } } fn write_to_clipboard(&self, item: crate::ClipboardItem) { - self.0 - .borrow_mut() - .clipboard - .as_mut() - .unwrap() - .set_contents(item.text) - .ok(); + let mut state = self.0.borrow_mut(); + let (Some(data_device_manager), Some(data_device)) = ( + state.globals.data_device_manager.clone(), + state.data_device.clone(), + ) else { + return; + }; + if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() { + let serial = state.serial_tracker.get(SerialKind::KeyEnter); + let data_source = data_device_manager.create_data_source(&state.globals.qh, ()); + data_source.offer(state.clipboard.self_mime()); + data_source.offer(TEXT_MIME_TYPE.to_string()); + data_device.set_selection(Some(&data_source), serial); + state.clipboard.set(item.text); + } } fn read_from_primary(&self) -> Option { self.0 .borrow_mut() - .primary - .as_mut() - .unwrap() - .get_contents() - .ok() + .clipboard + .read_primary() .map(|s| crate::ClipboardItem { text: s, metadata: None, @@ -688,10 +713,7 @@ impl LinuxClient for WaylandClient { self.0 .borrow_mut() .clipboard - .as_mut() - .unwrap() - .get_contents() - .ok() + .read() .map(|s| crate::ClipboardItem { text: s, metadata: None, @@ -771,6 +793,7 @@ delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor); delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1); delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1); delegate_noop!(WaylandClientStatePtr: ignore wl_data_device_manager::WlDataDeviceManager); +delegate_noop!(WaylandClientStatePtr: ignore zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1); delegate_noop!(WaylandClientStatePtr: ignore wl_shm::WlShm); delegate_noop!(WaylandClientStatePtr: ignore wl_shm_pool::WlShmPool); delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer); @@ -1061,7 +1084,10 @@ impl Dispatch for WaylandClientStatePtr { xkb::compose::STATE_NO_FLAGS, )); } - wl_keyboard::Event::Enter { surface, .. } => { + wl_keyboard::Event::Enter { + serial, surface, .. + } => { + state.serial_tracker.update(SerialKind::KeyEnter, serial); state.keyboard_focused_window = get_window(&mut state, &surface.id()); state.enter_token = Some(()); @@ -1074,6 +1100,7 @@ impl Dispatch for WaylandClientStatePtr { let keyboard_focused_window = get_window(&mut state, &surface.id()); state.keyboard_focused_window = None; state.enter_token.take(); + state.clipboard.set_offer(None); if let Some(window) = keyboard_focused_window { if let Some(ref mut compose) = state.compose_state { @@ -1649,8 +1676,6 @@ impl Dispatch } } -const FILE_LIST_MIME_TYPE: &str = "text/uri-list"; - impl Dispatch for WaylandClientStatePtr { fn event( this: &mut Self, @@ -1664,6 +1689,28 @@ impl Dispatch for WaylandClientStatePtr { let mut state = client.borrow_mut(); match event { + // Clipboard + wl_data_device::Event::DataOffer { id: data_offer } => { + state.data_offers.push(DataOffer::new(data_offer)); + if state.data_offers.len() > 2 { + // At most we store a clipboard offer and a drag and drop offer. + state.data_offers.remove(0).inner.destroy(); + } + } + wl_data_device::Event::Selection { id: data_offer } => { + if let Some(offer) = data_offer { + let offer = state + .data_offers + .iter() + .find(|wrapper| wrapper.inner.id() == offer.id()); + let offer = offer.cloned(); + state.clipboard.set_offer(offer); + } else { + state.clipboard.set_offer(None); + } + } + + // Drag and drop wl_data_device::Event::Enter { serial, surface, @@ -1800,10 +1847,134 @@ impl Dispatch for WaylandClientStatePtr { match event { wl_data_offer::Event::Offer { mime_type } => { + // Drag and drop if mime_type == FILE_LIST_MIME_TYPE { let serial = state.serial_tracker.get(SerialKind::DataDevice); + let mime_type = mime_type.clone(); data_offer.accept(serial, Some(mime_type)); } + + // Clipboard + if let Some(offer) = state + .data_offers + .iter_mut() + .find(|wrapper| wrapper.inner.id() == data_offer.id()) + { + offer.add_mime_type(mime_type); + } + } + _ => {} + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + data_source: &wl_data_source::WlDataSource, + event: wl_data_source::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + wl_data_source::Event::Send { mime_type, fd } => { + state.clipboard.send(mime_type, fd); + } + wl_data_source::Event::Cancelled => { + data_source.destroy(); + } + _ => {} + } + } +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + _: &zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, + event: zwp_primary_selection_device_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + zwp_primary_selection_device_v1::Event::DataOffer { offer } => { + let old_offer = state.primary_data_offer.replace(DataOffer::new(offer)); + if let Some(old_offer) = old_offer { + old_offer.inner.destroy(); + } + } + zwp_primary_selection_device_v1::Event::Selection { id: data_offer } => { + if data_offer.is_some() { + let offer = state.primary_data_offer.clone(); + state.clipboard.set_primary_offer(offer); + } else { + state.clipboard.set_primary_offer(None); + } + } + _ => {} + } + } + + event_created_child!(WaylandClientStatePtr, zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, [ + zwp_primary_selection_device_v1::EVT_DATA_OFFER_OPCODE => (zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()), + ]); +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + _data_offer: &zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, + event: zwp_primary_selection_offer_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + zwp_primary_selection_offer_v1::Event::Offer { mime_type } => { + if let Some(offer) = state.primary_data_offer.as_mut() { + offer.add_mime_type(mime_type); + } + } + _ => {} + } + } +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + selection_source: &zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, + event: zwp_primary_selection_source_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + zwp_primary_selection_source_v1::Event::Send { mime_type, fd } => { + state.clipboard.send_primary(mime_type, fd); + } + zwp_primary_selection_source_v1::Event::Cancelled => { + selection_source.destroy(); } _ => {} } diff --git a/crates/gpui/src/platform/linux/wayland/clipboard.rs b/crates/gpui/src/platform/linux/wayland/clipboard.rs new file mode 100644 index 0000000000..ec1ced07fe --- /dev/null +++ b/crates/gpui/src/platform/linux/wayland/clipboard.rs @@ -0,0 +1,218 @@ +use std::{ + fs::File, + io::{ErrorKind, Write}, + os::fd::{AsRawFd, BorrowedFd, OwnedFd}, +}; + +use calloop::{LoopHandle, PostAction}; +use filedescriptor::Pipe; +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, WaylandClientStatePtr}; + +pub(crate) const TEXT_MIME_TYPE: &str = "text/plain;charset=utf-8"; +pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list"; + +/// Text mime types that we'll accept from other programs. +pub(crate) const ALLOWED_TEXT_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"]; + +pub(crate) struct Clipboard { + connection: Connection, + loop_handle: LoopHandle<'static, WaylandClientStatePtr>, + self_mime: String, + + // Internal clipboard + contents: Option, + primary_contents: Option, + + // External clipboard + cached_read: Option, + current_offer: Option>, + cached_primary_read: Option, + current_primary_offer: Option>, +} + +#[derive(Clone, Debug)] +/// Wrapper for `WlDataOffer` and `ZwpPrimarySelectionOfferV1`, used to help track mime types. +pub(crate) struct DataOffer { + pub inner: T, + mime_types: Vec, +} + +impl DataOffer { + pub fn new(offer: T) -> Self { + Self { + inner: offer, + mime_types: Vec::new(), + } + } + + pub fn add_mime_type(&mut self, mime_type: String) { + self.mime_types.push(mime_type) + } + + pub 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()); + } + } + None + } +} + +impl Clipboard { + pub fn new( + connection: Connection, + loop_handle: LoopHandle<'static, WaylandClientStatePtr>, + ) -> Self { + Self { + connection, + loop_handle, + self_mime: format!("pid/{}", std::process::id()), + + contents: None, + primary_contents: None, + + cached_read: None, + current_offer: None, + cached_primary_read: None, + current_primary_offer: None, + } + } + + pub fn set(&mut self, text: String) { + self.contents = Some(text); + } + + pub fn set_primary(&mut self, text: String) { + self.primary_contents = Some(text); + } + + pub fn set_offer(&mut self, data_offer: Option>) { + self.cached_read = None; + self.current_offer = data_offer; + } + + pub fn set_primary_offer(&mut self, data_offer: Option>) { + self.cached_primary_read = None; + self.current_primary_offer = data_offer; + } + + pub fn self_mime(&self) -> String { + self.self_mime.clone() + } + + pub fn send(&self, _mime_type: String, fd: OwnedFd) { + if let Some(contents) = &self.contents { + self.send_internal(fd, contents.as_bytes().to_owned()); + } + } + + pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) { + if let Some(primary_contents) = &self.primary_contents { + self.send_internal(fd, primary_contents.as_bytes().to_owned()); + } + } + + pub fn read(&mut self) -> Option { + let offer = self.current_offer.clone()?; + if let Some(cached) = self.cached_read.clone() { + return Some(cached); + } + + if offer.has_mime_type(&self.self_mime) { + 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(); + + match unsafe { read_fd(fd) } { + Ok(v) => { + self.cached_read = Some(v.clone()); + Some(v) + } + Err(err) => { + log::error!("error reading clipboard pipe: {err:?}"); + None + } + } + } + + pub fn read_primary(&mut self) -> Option { + let offer = self.current_primary_offer.clone()?; + if let Some(cached) = self.cached_primary_read.clone() { + return Some(cached); + } + + if offer.has_mime_type(&self.self_mime) { + 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(); + + match unsafe { read_fd(fd) } { + Ok(v) => { + self.cached_primary_read = Some(v.clone()); + Some(v) + } + Err(err) => { + log::error!("error reading clipboard pipe: {err:?}"); + None + } + } + } + + fn send_internal(&self, fd: OwnedFd, bytes: Vec) { + let mut written = 0; + self.loop_handle + .insert_source( + calloop::generic::Generic::new( + File::from(fd), + calloop::Interest::WRITE, + calloop::Mode::Level, + ), + move |_, file, _| { + let mut file = unsafe { file.get_mut() }; + loop { + match file.write(&bytes[written..]) { + Ok(n) if written + n == bytes.len() => { + written += n; + break Ok(PostAction::Remove); + } + Ok(n) => written += n, + Err(err) if err.kind() == ErrorKind::WouldBlock => { + break Ok(PostAction::Continue) + } + Err(_) => break Ok(PostAction::Remove), + } + } + }, + ) + .unwrap(); + } +} diff --git a/crates/gpui/src/platform/linux/wayland/serial.rs b/crates/gpui/src/platform/linux/wayland/serial.rs index eadc7a9ca9..5ac73dcd22 100644 --- a/crates/gpui/src/platform/linux/wayland/serial.rs +++ b/crates/gpui/src/platform/linux/wayland/serial.rs @@ -6,6 +6,7 @@ pub(crate) enum SerialKind { InputMethod, MouseEnter, MousePress, + KeyEnter, KeyPress, } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 05e20b5d8a..f2ad5b9a86 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -8,10 +8,8 @@ use calloop::generic::{FdWrapper, Generic}; use calloop::{EventLoop, LoopHandle, RegistrationToken}; use collections::HashMap; -use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext}; -use copypasta::ClipboardProvider; - use util::ResultExt; + use x11rb::connection::{Connection, RequestConnection}; use x11rb::cursor; use x11rb::errors::ConnectionError; @@ -129,8 +127,7 @@ pub struct X11ClientState { pub(crate) scroll_y: Option, pub(crate) common: LinuxCommon, - pub(crate) clipboard: X11ClipboardContext, - pub(crate) primary: X11ClipboardContext, + pub(crate) clipboard: x11_clipboard::Clipboard, } #[derive(Clone)] @@ -277,8 +274,7 @@ impl X11Client { .reply() .unwrap(); - let clipboard = X11ClipboardContext::::new().unwrap(); - let primary = X11ClipboardContext::::new().unwrap(); + let clipboard = x11_clipboard::Clipboard::new().unwrap(); let xcb_connection = Rc::new(xcb_connection); @@ -399,7 +395,6 @@ impl X11Client { scroll_y: None, clipboard, - primary, }))) } @@ -1072,35 +1067,61 @@ impl LinuxClient for X11Client { } fn write_to_primary(&self, item: crate::ClipboardItem) { - self.0.borrow_mut().primary.set_contents(item.text).ok(); + let state = self.0.borrow_mut(); + state + .clipboard + .store( + state.clipboard.setter.atoms.primary, + state.clipboard.setter.atoms.utf8_string, + item.text().as_bytes(), + ) + .ok(); } fn write_to_clipboard(&self, item: crate::ClipboardItem) { - self.0.borrow_mut().clipboard.set_contents(item.text).ok(); + let state = self.0.borrow_mut(); + state + .clipboard + .store( + state.clipboard.setter.atoms.clipboard, + state.clipboard.setter.atoms.utf8_string, + item.text().as_bytes(), + ) + .ok(); } fn read_from_primary(&self) -> Option { - self.0 - .borrow_mut() - .primary - .get_contents() - .ok() + let state = self.0.borrow_mut(); + 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 { - text, + text: String::from_utf8(text).unwrap(), metadata: None, }) + .ok() } fn read_from_clipboard(&self) -> Option { - self.0 - .borrow_mut() + let state = self.0.borrow_mut(); + state .clipboard - .get_contents() - .ok() + .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 { - text, + text: String::from_utf8(text).unwrap(), metadata: None, }) + .ok() } fn run(&self) { diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 57b85c5ca5..4edd807b8b 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -12,7 +12,7 @@ use std::{ use ::util::ResultExt; use anyhow::{anyhow, Context, Result}; -use copypasta::{ClipboardContext, ClipboardProvider}; +use clipboard_win::{get_clipboard_string, set_clipboard_string}; use futures::channel::oneshot::{self, Receiver}; use itertools::Itertools; use parking_lot::RwLock; @@ -502,16 +502,14 @@ impl Platform for WindowsPlatform { fn write_to_clipboard(&self, item: ClipboardItem) { if item.text.len() > 0 { - let mut ctx = ClipboardContext::new().unwrap(); - ctx.set_contents(item.text().to_owned()).unwrap(); + set_clipboard_string(item.text()).unwrap(); } } fn read_from_clipboard(&self) -> Option { - let mut ctx = ClipboardContext::new().unwrap(); - let content = ctx.get_contents().ok()?; + let text = get_clipboard_string().ok()?; Some(ClipboardItem { - text: content, + text, metadata: None, }) }