wayland: Refactor clipboard implementation (#12405)

apricotbucket28 created

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

Change summary

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 
crates/gpui/src/platform/linux/wayland/client.rs    | 257 ++++++++++++--
crates/gpui/src/platform/linux/wayland/clipboard.rs | 218 ++++++++++++
crates/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(-)

Detailed changes

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",
 ]
 

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::<usize>(cx);
+            let selections = &self.selections.disjoint;
             let buffer_handle = self.buffer.read(cx).read(cx);
 
             let mut text = String::new();

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"

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;

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<wp_cursor_shape_manager_v1::WpCursorShapeManagerV1>,
     pub data_device_manager: Option<wl_data_device_manager::WlDataDeviceManager>,
+    pub primary_selection_manager:
+        Option<zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1>,
     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<wl_keyboard::WlKeyboard>,
     cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
     data_device: Option<wl_data_device::WlDataDevice>,
+    primary_selection: Option<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1>,
     text_input: Option<zwp_text_input_v3::ZwpTextInputV3>,
     pre_edit_text: Option<String>,
     composing: bool,
@@ -204,13 +217,13 @@ pub(crate) struct WaylandClientState {
     keyboard_focused_window: Option<WaylandWindowStatePtr>,
     loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
     cursor_style: Option<CursorStyle>,
+    clipboard: Clipboard,
+    data_offers: Vec<DataOffer<WlDataOffer>>,
+    primary_data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
     cursor: Cursor,
-    clipboard: Option<Clipboard>,
-    primary: Option<Primary>,
+    pending_open_uri: Option<String>,
     event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
     common: LinuxCommon,
-
-    pending_open_uri: Option<String>,
 }
 
 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::<WaylandClientStatePtr>::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<crate::ClipboardItem> {
         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<wl_keyboard::WlKeyboard, ()> 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<wl_keyboard::WlKeyboard, ()> 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<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, ObjectId>
     }
 }
 
-const FILE_LIST_MIME_TYPE: &str = "text/uri-list";
-
 impl Dispatch<wl_data_device::WlDataDevice, ()> for WaylandClientStatePtr {
     fn event(
         this: &mut Self,
@@ -1664,6 +1689,28 @@ impl Dispatch<wl_data_device::WlDataDevice, ()> 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<wl_data_offer::WlDataOffer, ()> 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<wl_data_source::WlDataSource, ()> for WaylandClientStatePtr {
+    fn event(
+        this: &mut Self,
+        data_source: &wl_data_source::WlDataSource,
+        event: wl_data_source::Event,
+        _: &(),
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+        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<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, ()>
+    for WaylandClientStatePtr
+{
+    fn event(
+        this: &mut Self,
+        _: &zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1,
+        event: zwp_primary_selection_device_v1::Event,
+        _: &(),
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+        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<zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()>
+    for WaylandClientStatePtr
+{
+    fn event(
+        this: &mut Self,
+        _data_offer: &zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1,
+        event: zwp_primary_selection_offer_v1::Event,
+        _: &(),
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+        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<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()>
+    for WaylandClientStatePtr
+{
+    fn event(
+        this: &mut Self,
+        selection_source: &zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1,
+        event: zwp_primary_selection_source_v1::Event,
+        _: &(),
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+        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();
             }
             _ => {}
         }

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<String>,
+    primary_contents: Option<String>,
+
+    // External clipboard
+    cached_read: Option<String>,
+    current_offer: Option<DataOffer<WlDataOffer>>,
+    cached_primary_read: Option<String>,
+    current_primary_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
+}
+
+#[derive(Clone, Debug)]
+/// Wrapper for `WlDataOffer` and `ZwpPrimarySelectionOfferV1`, used to help track mime types.
+pub(crate) struct DataOffer<T> {
+    pub inner: T,
+    mime_types: Vec<String>,
+}
+
+impl<T> DataOffer<T> {
+    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<String> {
+        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<DataOffer<WlDataOffer>>) {
+        self.cached_read = None;
+        self.current_offer = data_offer;
+    }
+
+    pub fn set_primary_offer(&mut self, data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>) {
+        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<String> {
+        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<String> {
+        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<u8>) {
+        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();
+    }
+}

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<f32>,
 
     pub(crate) common: LinuxCommon,
-    pub(crate) clipboard: X11ClipboardContext<Clipboard>,
-    pub(crate) primary: X11ClipboardContext<Primary>,
+    pub(crate) clipboard: x11_clipboard::Clipboard,
 }
 
 #[derive(Clone)]
@@ -277,8 +274,7 @@ impl X11Client {
             .reply()
             .unwrap();
 
-        let clipboard = X11ClipboardContext::<Clipboard>::new().unwrap();
-        let primary = X11ClipboardContext::<Primary>::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<crate::ClipboardItem> {
-        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<crate::ClipboardItem> {
-        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) {

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<ClipboardItem> {
-        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,
         })
     }