linux: clipboard (#8822)

Rom Grk created

Linux clipboard implementation with `copypasta`.


Release Notes:

- Added linux clipboard support

Change summary

Cargo.lock                                       | 158 ++++++++++++++++++
crates/gpui/Cargo.toml                           |   1 
crates/gpui/src/platform/linux/client.rs         |   5 
crates/gpui/src/platform/linux/platform.rs       |  19 +
crates/gpui/src/platform/linux/wayland/client.rs |  25 ++
crates/gpui/src/platform/linux/x11/client.rs     |  17 +
6 files changed, 216 insertions(+), 9 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2141,6 +2141,16 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "clipboard-win"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342"
+dependencies = [
+ "lazy-bytes-cast",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "clock"
 version = "0.1.0"
@@ -2490,6 +2500,20 @@ dependencies = [
  "zed_actions",
 ]
 
+[[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"
@@ -4147,6 +4171,16 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "gethostname"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
+dependencies = [
+ "libc",
+ "windows-targets 0.48.5",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.1.16"
@@ -4325,6 +4359,7 @@ dependencies = [
  "cbindgen",
  "cocoa",
  "collections",
+ "copypasta",
  "core-foundation",
  "core-graphics",
  "core-text",
@@ -5333,6 +5368,12 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "lazy-bytes-cast"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b"
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -5733,6 +5774,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "memmap2"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "memoffset"
 version = "0.7.1"
@@ -6376,6 +6426,17 @@ 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"
@@ -6385,6 +6446,15 @@ 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"
@@ -8915,6 +8985,42 @@ 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.30",
+ "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"
@@ -11730,6 +11836,17 @@ 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"
@@ -11753,6 +11870,19 @@ 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"
@@ -11772,6 +11902,7 @@ checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af"
 dependencies = [
  "dlib",
  "log",
+ "once_cell",
  "pkg-config",
 ]
 
@@ -12417,6 +12548,33 @@ dependencies = [
  "tap",
 ]
 
+[[package]]
+name = "x11-clipboard"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286"
+dependencies = [
+ "libc",
+ "x11rb",
+]
+
+[[package]]
+name = "x11rb"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a"
+dependencies = [
+ "gethostname",
+ "rustix 0.38.30",
+ "x11rb-protocol",
+]
+
+[[package]]
+name = "x11rb-protocol"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34"
+
 [[package]]
 name = "xattr"
 version = "0.2.3"

crates/gpui/Cargo.toml 🔗

@@ -113,6 +113,7 @@ xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
 as-raw-xcb-connection = "1"
 calloop = "0.12.4"
 calloop-wayland-source = "0.2.0"
+copypasta = "0.10.1"
 oo7 = "0.3.0"
 
 [target.'cfg(windows)'.dependencies]

crates/gpui/src/platform/linux/client.rs 🔗

@@ -1,5 +1,8 @@
+use std::cell::RefCell;
 use std::rc::Rc;
 
+use copypasta::ClipboardProvider;
+
 use crate::platform::PlatformWindow;
 use crate::{AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, WindowOptions};
 
@@ -12,4 +15,6 @@ pub trait Client {
         options: WindowOptions,
     ) -> Box<dyn PlatformWindow>;
     fn set_cursor_style(&self, style: CursorStyle);
+    fn get_clipboard(&self) -> Rc<RefCell<dyn ClipboardProvider>>;
+    fn get_primary(&self) -> Rc<RefCell<dyn ClipboardProvider>>;
 }

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -354,12 +354,21 @@ impl Platform for LinuxPlatform {
         false
     }
 
-    // todo(linux)
-    fn write_to_clipboard(&self, item: ClipboardItem) {}
+    fn write_to_clipboard(&self, item: ClipboardItem) {
+        let clipboard = self.client.get_clipboard();
+        clipboard.borrow_mut().set_contents(item.text);
+    }
 
-    // todo(linux)
     fn read_from_clipboard(&self) -> Option<ClipboardItem> {
-        None
+        let clipboard = self.client.get_clipboard();
+        let contents = clipboard.borrow_mut().get_contents();
+        match contents {
+            Ok(text) => Some(ClipboardItem {
+                metadata: None,
+                text,
+            }),
+            _ => None,
+        }
     }
 
     //todo!(linux)
@@ -382,6 +391,8 @@ impl Platform for LinuxPlatform {
         })
     }
 
+    //todo!(linux): add trait methods for accessing the primary selection
+
     //todo!(linux)
     fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
         let url = url.to_string();

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -6,6 +6,8 @@ use std::time::Duration;
 use calloop::timer::{TimeoutAction, Timer};
 use calloop::LoopHandle;
 use calloop_wayland_source::WaylandSource;
+use copypasta::wayland_clipboard::{create_clipboards_from_external, Clipboard, Primary};
+use copypasta::ClipboardProvider;
 use wayland_backend::client::ObjectId;
 use wayland_backend::protocol::WEnum;
 use wayland_client::globals::{registry_queue_init, GlobalListContents};
@@ -74,6 +76,8 @@ pub(crate) struct CursorState {
 pub(crate) struct WaylandClientState {
     client_state_inner: Rc<RefCell<WaylandClientStateInner>>,
     cursor_state: Rc<RefCell<CursorState>>,
+    clipboard: Rc<RefCell<Clipboard>>,
+    primary: Rc<RefCell<Primary>>,
 }
 
 pub(crate) struct KeyRepeat {
@@ -111,6 +115,9 @@ impl WaylandClient {
             }
         });
 
+        let display = conn.backend().display_ptr() as *mut std::ffi::c_void;
+        let (primary, clipboard) = unsafe { create_clipboards_from_external(display) };
+
         let mut state_inner = Rc::new(RefCell::new(WaylandClientStateInner {
             compositor: globals.bind(&qh, 1..=1, ()).unwrap(),
             wm_base: globals.bind(&qh, 1..=1, ()).unwrap(),
@@ -152,20 +159,20 @@ impl WaylandClient {
         let mut state = WaylandClientState {
             client_state_inner: Rc::clone(&state_inner),
             cursor_state: Rc::clone(&cursor_state),
+            clipboard: Rc::new(RefCell::new(clipboard)),
+            primary: Rc::new(RefCell::new(primary)),
         };
+        let mut state_loop = state.clone();
         linux_platform_inner
             .loop_handle
             .insert_source(source, move |_, queue, _| {
-                queue.dispatch_pending(&mut state)
+                queue.dispatch_pending(&mut state_loop)
             })
             .unwrap();
 
         Self {
             platform_inner: linux_platform_inner,
-            state: WaylandClientState {
-                client_state_inner: state_inner,
-                cursor_state,
-            },
+            state,
             qh: Arc::new(qh),
         }
     }
@@ -265,6 +272,14 @@ impl Client for WaylandClient {
         let mut cursor_state = self.state.cursor_state.borrow_mut();
         cursor_state.cursor_icon_name = cursor_icon_name;
     }
+
+    fn get_clipboard(&self) -> Rc<RefCell<dyn ClipboardProvider>> {
+        self.state.clipboard.clone()
+    }
+
+    fn get_primary(&self) -> Rc<RefCell<dyn ClipboardProvider>> {
+        self.state.primary.clone()
+    }
 }
 
 impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientState {

crates/gpui/src/platform/linux/x11/client.rs 🔗

@@ -6,6 +6,8 @@ use xcb::{x, Xid as _};
 use xkbcommon::xkb;
 
 use collections::HashMap;
+use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext};
+use copypasta::ClipboardProvider;
 
 use crate::platform::linux::client::Client;
 use crate::platform::{LinuxPlatformInner, PlatformWindow};
@@ -28,6 +30,8 @@ struct WindowRef {
 struct X11ClientState {
     windows: HashMap<x::Window, WindowRef>,
     xkb: xkbcommon::xkb::State,
+    clipboard: Rc<RefCell<X11ClipboardContext<Clipboard>>>,
+    primary: Rc<RefCell<X11ClipboardContext<Primary>>>,
 }
 
 pub(crate) struct X11Client {
@@ -70,6 +74,9 @@ impl X11Client {
             xkb::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
         };
 
+        let clipboard = X11ClipboardContext::<Clipboard>::new().unwrap();
+        let primary = X11ClipboardContext::<Primary>::new().unwrap();
+
         let client: Rc<X11Client> = Rc::new(Self {
             platform_inner: inner.clone(),
             xcb_connection: Rc::clone(&xcb_connection),
@@ -78,6 +85,8 @@ impl X11Client {
             state: RefCell::new(X11ClientState {
                 windows: HashMap::default(),
                 xkb: xkb_state,
+                clipboard: Rc::new(RefCell::new(clipboard)),
+                primary: Rc::new(RefCell::new(primary)),
             }),
         });
 
@@ -354,6 +363,14 @@ impl Client for X11Client {
 
     //todo!(linux)
     fn set_cursor_style(&self, _style: CursorStyle) {}
+
+    fn get_clipboard(&self) -> Rc<RefCell<dyn ClipboardProvider>> {
+        self.state.borrow().clipboard.clone()
+    }
+
+    fn get_primary(&self) -> Rc<RefCell<dyn ClipboardProvider>> {
+        self.state.borrow().primary.clone()
+    }
 }
 
 // Adatpted from: