From bb652c775b8226e55a22437476bcf45bffb5ed6c Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Sun, 10 May 2020 16:40:03 +0300 Subject: [PATCH] Update smithay-clipboard to SCTK 0.9.1 (#16) This commit entirely reworks the internal structure of the entire crate, as well as some of its APIs. This crate only accepts a C pointer to a Wayland display object, since the target audience of this crate are libraries without a wayland-client types. Also since seat information is not presented in such clients most of the time, the clipboard entirely relies on its seat tracking. --- CHANGELOG.md | 5 + CONTRIBUTING.md | 2 +- Cargo.toml | 8 +- README.md | 8 +- examples/clipboard.rs | 468 +++++++++------- src/env.rs | 80 +++ src/lib.rs | 100 +++- src/mime.rs | 49 ++ src/threaded.rs | 829 ----------------------------- src/worker/dispatch_data.rs | 47 ++ src/worker/handlers.rs | 131 +++++ src/worker/mod.rs | 288 ++++++++++ src/worker/seat_data.rs | 20 + src/worker/sleep_amount_tracker.rs | 60 +++ 14 files changed, 1040 insertions(+), 1055 deletions(-) create mode 100644 src/env.rs create mode 100644 src/mime.rs delete mode 100644 src/threaded.rs create mode 100644 src/worker/dispatch_data.rs create mode 100644 src/worker/handlers.rs create mode 100644 src/worker/mod.rs create mode 100644 src/worker/seat_data.rs create mode 100644 src/worker/sleep_amount_tracker.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 537c88c..11dd286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Minimal rust version was bumped to 1.40.0 +- Add support for `UTF8_STRING` mime type +- Clipboard now works only with extern display +- Clipboard now works only with last observed seats, instead of optionally accepting seat names + ## 0.4.0 -- 2020-03-09 - Fix crash when receiving non-utf8 data diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e83c1ee..ac85a02 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,4 +6,4 @@ Smithay Clipboard is open to contributions from anyone. There is a Matrix room dedicated to the Smithay project: [#smithay:matrix.org](https://matrix.to/#/#smithay:matrix.org). If you don't want to use matrix, this room is -also bridged to gitter: https://gitter.im/smithay/Lobby. +also bridged to gitter: https://gitter.im/smithay/Lobby and freenode: `#smithay`. diff --git a/Cargo.toml b/Cargo.toml index 7db9c1e..e5ff7ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "smithay-clipboard" version = "0.4.0" -authors = ["Lucas Timmins ", "Victor Berger "] +authors = ["Kirill Chibisov ", "Lucas Timmins ", "Victor Berger "] edition = "2018" description = "Provides access to the wayland clipboard for client applications." repository = "https://github.com/smithay/smithay-clipboard" @@ -10,8 +10,8 @@ license = "MIT" keywords = ["clipboard", "wayland"] [dependencies] -sctk = { package = "smithay-client-toolkit", version = "0.6.1" } -nix = "0.14.1" +sctk = { package = "smithay-client-toolkit", version = "0.9.1", default-features = false } +wayland-client = { version = "0.26.3", features = ["dlopen"] } [dev-dependencies] -andrew = "0.2" +sctk = { package = "smithay-client-toolkit", version = "0.9.1"} diff --git a/README.md b/README.md index 1dbf11d..b9ce053 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,13 @@ # Smithay Clipboard -This crate provides access to the wayland clipboard. +This crate provides access to the Wayland clipboard for applications +already using some sort of GUI toolkit or a windowing library, like +[winit](https://github.com/rust-windowing/winit), since you should +have some surface around to receive keyboard/pointer events. + +If you want to access clipboard from the CLI or to write clipboard manager, +this is not what you're looking for. ## Documentation diff --git a/examples/clipboard.rs b/examples/clipboard.rs index d1a513c..23f0156 100644 --- a/examples/clipboard.rs +++ b/examples/clipboard.rs @@ -1,234 +1,290 @@ -use std::io::{Read, Seek, SeekFrom, Write}; -use std::sync::{atomic, Arc, Mutex}; +use std::io::{BufWriter, Seek, SeekFrom, Write}; -use sctk::keyboard::{map_keyboard_auto, Event as KbEvent, KeyState}; -use sctk::utils::{DoubleMemPool, MemPool}; -use sctk::window::{ConceptFrame, Event as WEvent, Window}; -use sctk::Environment; +use sctk::seat; +use sctk::seat::keyboard::{self, Event as KeyboardEvent, KeyState, RepeatKind}; +use sctk::shm::MemPool; +use sctk::window::{ConceptFrame, Event as WindowEvent}; -use sctk::reexports::client::protocol::{wl_shm, wl_surface}; -use sctk::reexports::client::{Display, NewProxy}; +use sctk::reexports::calloop::Source as EventLoopSource; +use sctk::reexports::client::protocol::wl_keyboard::WlKeyboard; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_shm; +use sctk::reexports::client::protocol::wl_surface::WlSurface; -use andrew::shapes::rectangle; -use andrew::text; -use andrew::text::fontconfig; +use smithay_clipboard::Clipboard; -fn main() { - let (display, mut event_queue) = - Display::connect_to_env().expect("Failed to connect to the wayland server."); - let env = Environment::from_display(&*display, &mut event_queue).unwrap(); +sctk::default_environment!(ClipboardExample, desktop); - let mut clipboard = smithay_clipboard::WaylandClipboard::new(&display); - let cb_contents = Arc::new(Mutex::new(String::new())); +/// Our dispatch data for simple clipboard access and processing frame events. +struct DispatchData { + /// Pending event from SCTK to update window. + pub pending_frame_event: Option, + /// Clipboard handler. + pub clipboard: Clipboard, +} - let seat = env - .manager - .instantiate_range(2, 6, NewProxy::implement_dummy) - .unwrap(); - - let need_redraw = Arc::new(atomic::AtomicBool::new(false)); - let need_redraw_clone = need_redraw.clone(); - let cb_contents_clone = cb_contents.clone(); - map_keyboard_auto(&seat, move |event: KbEvent, _| { - if let KbEvent::Key { - state: KeyState::Pressed, - utf8: Some(text), - .. - } = event - { - if text == " " { - *cb_contents_clone.lock().unwrap() = dbg!(clipboard.load(None).unwrap()); - need_redraw_clone.store(true, atomic::Ordering::Relaxed) - } else if text == "s" { - clipboard.store( - None, - "This is an example text thats been copied to the wayland clipboard :)" - .to_string(), - ); - } +impl DispatchData { + fn new(clipboard: Clipboard) -> Self { + Self { + pending_frame_event: None, + clipboard, } - }) - .unwrap(); - - let mut dimensions = (320u32, 240u32); - let surface = env - .compositor - .create_surface(NewProxy::implement_dummy) - .unwrap(); - - let next_action = Arc::new(Mutex::new(None::)); - - let waction = next_action.clone(); - let mut window = Window::::init_from_env(&env, surface, dimensions, move |evt| { - let mut next_action = waction.lock().unwrap(); - // Keep last event in priority order : Close > Configure > Refresh - let replace = match (&evt, &*next_action) { - (_, &None) - | (_, &Some(WEvent::Refresh)) - | (&WEvent::Configure { .. }, &Some(WEvent::Configure { .. })) - | (&WEvent::Close, _) => true, - _ => false, - }; - if replace { - *next_action = Some(evt); - } - }) - .expect("Failed to create a window !"); - - window.new_seat(&seat); - window.set_title("Clipboard".to_string()); - - let mut pools = DoubleMemPool::new(&env.shm, || {}).expect("Failed to create a memory pool !"); - - let mut font_data = Vec::new(); - std::fs::File::open( - &fontconfig::FontConfig::new() - .unwrap() - .get_regular_family_fonts("sans") - .unwrap()[0], - ) - .unwrap() - .read_to_end(&mut font_data) - .unwrap(); - - if !env.shell.needs_configure() { - // initial draw to bootstrap on wl_shell - if let Some(pool) = pools.pool() { - redraw( - pool, - window.surface(), - dimensions, - &font_data, - "".to_string(), - ); - } - window.refresh(); - } - - loop { - match next_action.lock().unwrap().take() { - Some(WEvent::Close) => break, - Some(WEvent::Refresh) => { - window.refresh(); - window.surface().commit(); - } - Some(WEvent::Configure { new_size, .. }) => { - if let Some((w, h)) = new_size { - window.resize(w, h); - dimensions = (w, h) - } - window.refresh(); - if let Some(pool) = pools.pool() { - redraw( - pool, - window.surface(), - dimensions, - &font_data, - cb_contents.lock().unwrap().clone(), - ); - } - } - None => {} - } - - if need_redraw.swap(false, atomic::Ordering::Relaxed) { - if let Some(pool) = pools.pool() { - redraw( - pool, - window.surface(), - dimensions, - &font_data, - cb_contents.lock().unwrap().clone(), - ); - } - window - .surface() - .damage_buffer(0, 0, dimensions.0 as i32, dimensions.1 as i32); - window.surface().commit(); - } - - event_queue.dispatch().unwrap(); } } -fn redraw( - pool: &mut MemPool, - surface: &wl_surface::WlSurface, - dimensions: (u32, u32), - font_data: &[u8], - cb_contents: String, -) { - let (buf_x, buf_y) = (dimensions.0 as usize, dimensions.1 as usize); +fn main() { + // Setup default desktop environment + let (env, display, queue) = sctk::init_default_environment!(ClipboardExample, desktop) + .expect("unable to connect to a Wayland compositor."); - pool.resize(4 * buf_x * buf_y) - .expect("Failed to resize the memory pool."); + // Create event loop + let mut event_loop = sctk::reexports::calloop::EventLoop::::new().unwrap(); - let mut buf = vec![0; 4 * buf_x * buf_y]; - let mut canvas = - andrew::Canvas::new(&mut buf, buf_x, buf_y, 4 * buf_x, andrew::Endian::native()); + // Initial window dimentions + let mut dimentions = (320u32, 240u32); - let bg = rectangle::Rectangle::new((0, 0), (buf_x, buf_y), None, Some([255, 170, 20, 45])); - canvas.draw(&bg); + // Create surface + let surface = env.create_surface(); - let text_box = rectangle::Rectangle::new( - (buf_x / 30, buf_y / 35), - (buf_x - 2 * (buf_x / 30), (buf_x as f32 / 14.) as usize), - Some((3, [255, 255, 255, 255], rectangle::Sides::ALL, Some(4))), - None, - ); - canvas.draw(&text_box); + // Create window + let mut window = env + .create_window::(surface, dimentions, move |event, mut dispatch_data| { + // Get our dispath data + let dispatch_data = dispatch_data.get::().unwrap(); - let helper_text = text::Text::new( - (buf_x / 25, buf_y / 30), - [255, 255, 255, 255], - font_data, - buf_x as f32 / 40., - 2.0, - "Press space to draw clipboard contents", - ); - canvas.draw(&helper_text); + // Keep last event in priority order : Close > Configure > Refresh + let should_replace_event = match (&event, &dispatch_data.pending_frame_event) { + (_, &None) + | (_, &Some(WindowEvent::Refresh)) + | (&WindowEvent::Configure { .. }, &Some(WindowEvent::Configure { .. })) + | (&WindowEvent::Close, _) => true, + _ => false, + }; - let helper_text = text::Text::new( - (buf_x / 25, buf_y / 15), - [255, 255, 255, 255], - font_data, - buf_x as f32 / 40., - 2.0, - "Press 's' to store example text to clipboard", - ); - canvas.draw(&helper_text); + if should_replace_event { + dispatch_data.pending_frame_event = Some(event); + } + }) + .expect("failed to create a window."); - for i in (0..cb_contents.len()).step_by(36) { - let content = if cb_contents.len() < i + 36 { - cb_contents[i..].to_string() - } else { - cb_contents[i..i + 36].to_string() + // Set title and app id + window.set_title(String::from( + "smithay-clipboard example. Press C/P to copy/paste", + )); + window.set_app_id(String::from("smithay-clipboard-example")); + + // Create memory pool + let mut pools = env + .create_double_pool(|_| {}) + .expect("failed to create a memory pool."); + + // Structure to track seats + let mut seats = Vec::<(WlSeat, Option<(WlKeyboard, EventLoopSource<_>)>)>::new(); + + // Process existing seats + for seat in env.get_all_seats() { + let seat_data = match seat::with_seat_data(&seat, |seat_data| seat_data.clone()) { + Some(seat_data) => seat_data, + _ => continue, }; - let text = text::Text::new( - ( - buf_x / 10, - buf_y / 8 + (i as f32 * buf_y as f32 / 1000.) as usize, - ), - [255, 255, 255, 255], - font_data, - buf_x as f32 / 40., - 2.0, - content, - ); - canvas.draw(&text); + + if seat_data.has_keyboard && !seat_data.defunct { + // Suply event_loop's handle to handle key repeat + let event_loop_handle = event_loop.handle(); + + // Map keyboard for exising seats + let keyboard_mapping_result = keyboard::map_keyboard_repeat( + event_loop_handle, + &seat, + None, + RepeatKind::System, + move |event, _, mut dispatch_data| { + let dispatch_data = dispatch_data.get::().unwrap(); + process_keyboard_event(event, dispatch_data); + }, + ); + + // Insert repeat rate handling source + match keyboard_mapping_result { + Ok((keyboard, repeat_source)) => { + seats.push((seat.detach(), Some((keyboard, repeat_source)))); + } + Err(err) => { + eprintln!( + "Failed to map keyboard on seat {:?} : {:?}", + seat_data.name, err + ); + seats.push((seat.detach(), None)); + } + } + } else { + // Handle seats without keyboard, since they can gain keyboard later + seats.push((seat.detach(), None)); + } } - pool.seek(SeekFrom::Start(0)).unwrap(); - pool.write_all(canvas.buffer).unwrap(); - pool.flush().unwrap(); + // Implement event listener for seats to handle capability change, etc + let event_loop_handle = event_loop.handle(); + let _listener = env.listen_for_seats(move |seat, seat_data, _| { + // find the seat in the vec of seats or insert it + let idx = seats.iter().position(|(st, _)| st == &seat.detach()); + let idx = idx.unwrap_or_else(|| { + seats.push((seat.detach(), None)); + seats.len() - 1 + }); + + let (_, mapped_keyboard) = &mut seats[idx]; + + if seat_data.has_keyboard && !seat_data.defunct { + // Map keyboard if it's not mapped already + if mapped_keyboard.is_none() { + let keyboard_mapping_result = keyboard::map_keyboard_repeat( + event_loop_handle.clone(), + &seat, + None, + RepeatKind::System, + move |event, _, mut dispatch_data| { + let dispatch_data = dispatch_data.get::().unwrap(); + process_keyboard_event(event, dispatch_data); + }, + ); + + // Insert repeat rate source + match keyboard_mapping_result { + Ok((keyboard, repeat_source)) => { + *mapped_keyboard = Some((keyboard, repeat_source)); + } + Err(err) => { + eprintln!( + "Failed to map keyboard on seat {} : {:?}", + seat_data.name, err + ); + } + } + } + } else if let Some((keyboard, repeat_source)) = mapped_keyboard.take() { + if keyboard.as_ref().version() >= 3 { + keyboard.release(); + } + event_loop_handle.remove(repeat_source); + } + }); + + if !env.get_shell().unwrap().needs_configure() { + if let Some(pool) = pools.pool() { + draw(pool, window.surface().clone(), dimentions).expect("failed to draw.") + } + // Refresh our frame + window.refresh(); + } + + sctk::WaylandSource::new(queue) + .quick_insert(event_loop.handle()) + .unwrap(); + + let clipboard = Clipboard::new(display.get_display_ptr() as *mut _); + let mut dispatch_data = DispatchData::new(clipboard); + + loop { + if let Some(frame_event) = dispatch_data.pending_frame_event.take() { + match frame_event { + WindowEvent::Close => break, + WindowEvent::Refresh => { + window.refresh(); + window.surface().commit(); + } + WindowEvent::Configure { new_size, .. } => { + if let Some((w, h)) = new_size { + window.resize(w, h); + dimentions = (w, h) + } + window.refresh(); + if let Some(pool) = pools.pool() { + draw(pool, window.surface().clone(), dimentions).expect("failed to draw.") + } + } + } + } + + display.flush().unwrap(); + + event_loop.dispatch(None, &mut dispatch_data).unwrap(); + } +} + +fn process_keyboard_event(event: KeyboardEvent, dispatch_data: &mut DispatchData) { + let text = match event { + KeyboardEvent::Key { + state, + utf8: Some(text), + .. + } if state == KeyState::Pressed => text, + KeyboardEvent::Repeat { + utf8: Some(text), .. + } => text, + _ => return, + }; + + match text.as_str() { + // Paste primary. + "P" => { + let contents = dispatch_data + .clipboard + .load_primary() + .unwrap_or_else(|_| String::from("Failed to load primary selection")); + println!("Paste from primary clipboard: {}", contents); + } + // Paste. + "p" => { + let contents = dispatch_data + .clipboard + .load() + .unwrap_or_else(|_| String::from("Failed to load primary selection")); + println!("Paste: {}", contents); + } + // Copy primary. + "C" => { + let text = String::from("Copy primary"); + dispatch_data.clipboard.store_primary(text.clone()); + println!("Copied string into primary selection buffer: {}", text); + } + // Copy. + "c" => { + let text = String::from("Copy"); + dispatch_data.clipboard.store(text.clone()); + println!("Copied string: {}", text); + } + _ => (), + } +} + +fn draw( + pool: &mut MemPool, + surface: WlSurface, + dimensions: (u32, u32), +) -> Result<(), std::io::Error> { + pool.resize((4 * dimensions.0 * dimensions.1) as usize) + .expect("failed to resize memory pool"); + + { + pool.seek(SeekFrom::Start(0))?; + let mut writer = BufWriter::new(&mut *pool); + for _ in 0..dimensions.0 * dimensions.1 { + // ARGB color written in LE, so it's #FF1C1C1C + writer.write_all(&[0x1c, 0x1c, 0x1c, 0xff])?; + } + writer.flush()?; + } let new_buffer = pool.buffer( 0, - buf_x as i32, - buf_y as i32, - 4 * buf_x as i32, + dimensions.0 as i32, + dimensions.1 as i32, + 4 * dimensions.0 as i32, wl_shm::Format::Argb8888, ); surface.attach(Some(&new_buffer), 0, 0); surface.commit(); + + Ok(()) } diff --git a/src/env.rs b/src/env.rs new file mode 100644 index 0000000..8c49765 --- /dev/null +++ b/src/env.rs @@ -0,0 +1,80 @@ +use sctk::data_device::{DataDevice, DataDeviceHandler, DataDeviceHandling, DndEvent}; +use sctk::primary_selection::{ + PrimarySelectionDevice, PrimarySelectionDeviceManager, PrimarySelectionHandler, + PrimarySelectionHandling, +}; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::{Attached, DispatchData}; +use sctk::seat::{SeatData, SeatHandler, SeatHandling, SeatListener}; + +/// Environemt setup for smithay-clipboard. +pub struct SmithayClipboard { + seats: SeatHandler, + primary_selection_manager: PrimarySelectionHandler, + data_device_manager: DataDeviceHandler, +} + +impl SmithayClipboard { + /// Create new environment. + pub fn new() -> Self { + let mut seats = SeatHandler::new(); + let data_device_manager = DataDeviceHandler::init(&mut seats); + let primary_selection_manager = PrimarySelectionHandler::init(&mut seats); + Self { + seats, + primary_selection_manager, + data_device_manager, + } + } +} + +// Seat handling for data device manager and primary selection. +impl SeatHandling for SmithayClipboard { + fn listen, &SeatData, DispatchData) + 'static>( + &mut self, + f: F, + ) -> SeatListener { + self.seats.listen(f) + } +} + +impl PrimarySelectionHandling for SmithayClipboard { + fn with_primary_selection( + &self, + seat: &WlSeat, + f: F, + ) -> Result<(), ()> { + self.primary_selection_manager + .with_primary_selection(seat, f) + } + + fn get_primary_selection_manager(&self) -> Option { + self.primary_selection_manager + .get_primary_selection_manager() + } +} + +impl DataDeviceHandling for SmithayClipboard { + fn set_callback(&mut self, callback: F) -> Result<(), ()> + where + F: FnMut(WlSeat, DndEvent, DispatchData) + 'static, + { + self.data_device_manager.set_callback(callback) + } + + fn with_device(&self, seat: &WlSeat, f: F) -> Result<(), ()> { + self.data_device_manager.with_device(seat, f) + } +} + +// Setup globals. +sctk::environment!(SmithayClipboard, + singles = [ + sctk::reexports::protocols::unstable::primary_selection::v1::client::zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1 => primary_selection_manager, + sctk::reexports::protocols::misc::gtk_primary_selection::client::gtk_primary_selection_device_manager::GtkPrimarySelectionDeviceManager => primary_selection_manager, + sctk::reexports::client::protocol::wl_data_device_manager::WlDataDeviceManager => data_device_manager, + ], +multis = [ + WlSeat => seats, +] +); diff --git a/src/lib.rs b/src/lib.rs index a2fefd1..6f572b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,90 @@ //! Smithay Clipboard //! -//! Provides access to the wayland clipboard with only requirement being a WlDisplay -//! object -//! -//! ```norun -//! let (display, _) = -//! Display::connect_to_env().expect("Failed to connect to the wayland server."); -//! let mut clipboard = smithay_clipboard::WaylandClipboard::new(&display); -//! clipboard.store(None, "Test data"); -//! println!("{}", clipboard.load(None)); -//! ``` +//! Provides access to the Wayland clipboard for gui applications. The user should have surface +//! around. -#![warn(missing_docs)] +use std::ffi::c_void; +use std::io::Result; +use std::sync::mpsc::{self, Receiver, Sender}; -mod threaded; -pub use crate::threaded::ThreadedClipboard; -pub use crate::threaded::ThreadedClipboard as WaylandClipboard; +use sctk::reexports::client::Display; + +mod env; +mod mime; +mod worker; + +/// Access to a Wayland clipboard. +pub struct Clipboard { + request_sender: Sender, + request_receiver: Receiver>, + clipboard_thread: Option>, +} + +impl Clipboard { + /// Creates new clipboard which will be running on its own thread with its own event queue to + /// handle clipboard requests. + pub fn new(display: *mut c_void) -> Self { + let display = unsafe { Display::from_external_display(display as *mut _) }; + + // Create channel to send data to clipboard thread. + let (request_sender, clipboard_request_receiver) = mpsc::channel(); + // Create channel to get data from the clipboard thread. + let (clipboard_reply_sender, request_receiver) = mpsc::channel(); + + let name = String::from("smithay-clipboard"); + let clipboard_thread = worker::spawn( + name, + display, + clipboard_request_receiver, + clipboard_reply_sender, + ); + + Self { + request_receiver, + request_sender, + clipboard_thread, + } + } + + /// Load clipboard data. + /// + /// Loads content from a clipboard on a last observed seat. + pub fn load(&self) -> Result { + let _ = self.request_sender.send(worker::Command::Load); + self.request_receiver.recv().unwrap() + } + + /// Store to a clipboard + /// + /// Stores to a clipboard on a last observed seat. + pub fn store>(&self, text: T) { + let request = worker::Command::Store(text.into()); + let _ = self.request_sender.send(request); + } + + /// Load primary clipboard data. + /// + /// Loads content from a primary clipboard on a last observed seat. + pub fn load_primary(&self) -> Result { + let _ = self.request_sender.send(worker::Command::LoadPrimary); + self.request_receiver.recv().unwrap() + } + + /// Store to a primary clipboard. + /// + /// Stores to a primary clipboard on a last observed seat. + pub fn store_primary>(&self, text: T) { + let request = worker::Command::StorePrimary(text.into()); + let _ = self.request_sender.send(request); + } +} + +impl Drop for Clipboard { + fn drop(&mut self) { + // Shutdown smithay-clipboard. + self.request_sender.send(worker::Command::Exit).unwrap(); + if let Some(clipboard_thread) = self.clipboard_thread.take() { + let _ = clipboard_thread.join(); + } + } +} diff --git a/src/mime.rs b/src/mime.rs new file mode 100644 index 0000000..cf36d75 --- /dev/null +++ b/src/mime.rs @@ -0,0 +1,49 @@ +/// List of allowed mimes. +static ALLOWED_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"]; + +/// Mime type supported by clipboard. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum MimeType { + /// text/plain;charset=utf-8 mime type. + /// + /// The primary mime type used by most clients + TextPlainUtf8 = 0, + /// UTF8_STRING mime type. + /// + /// Some X11 clients are using only this mime type, so we + /// should have it as a fallback just in case. + Utf8String = 1, +} + +impl MimeType { + /// Find first allowed mime type among the `offered_mime_types`. + /// + /// `find_allowed()` searches for mime type clipboard supports, if we have a match, + /// returns `Some(MimeType)`, otherwise `None`. + pub fn find_allowed(offered_mime_types: &[String]) -> Option { + for offered_mime_type in offered_mime_types.iter() { + if offered_mime_type == ALLOWED_MIME_TYPES[Self::TextPlainUtf8 as usize] { + return Some(Self::TextPlainUtf8); + } else if offered_mime_type == ALLOWED_MIME_TYPES[Self::Utf8String as usize] { + return Some(Self::Utf8String); + } + } + + None + } +} + +impl ToString for MimeType { + fn to_string(&self) -> String { + String::from(ALLOWED_MIME_TYPES[*self as usize]) + } +} + +/// Normalize \r and \r\n into \n. +/// +/// Gtk does this for text/plain;charset=utf-8, so following them here, otherwise there is +/// a chance of getting extra new lines on load, since they're converting \r and \n into +/// \r\n on every store. +pub fn normilize_to_lf(text: String) -> String { + text.replace("\r\n", "\n").replace("\r", "\n") +} diff --git a/src/threaded.rs b/src/threaded.rs deleted file mode 100644 index fb63e73..0000000 --- a/src/threaded.rs +++ /dev/null @@ -1,829 +0,0 @@ -use std::collections::HashMap; -use std::io::{Read, Result, Write}; -use std::ops::Deref; -use std::os::unix::io::FromRawFd; -use std::sync::mpsc; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration; - -use nix::fcntl::OFlag; -use nix::unistd::{close, pipe2}; - -use sctk::data_device::{DataDevice, DataSource, DataSourceEvent}; -use sctk::keyboard::{map_keyboard_auto, Event as KbEvent}; -use sctk::reexports::client::protocol::{ - wl_data_device_manager, - wl_display::WlDisplay, - wl_pointer::Event as PtrEvent, - wl_registry, - wl_seat::{self, Capability}, -}; - -use sctk::reexports::client::{Display, EventQueue, GlobalEvent, GlobalManager, NewProxy}; -use sctk::reexports::protocols::misc::gtk_primary_selection::client::{ - gtk_primary_selection_device::Event as GtkPrimarySelectionDeviceEvent, - gtk_primary_selection_device::GtkPrimarySelectionDevice, - gtk_primary_selection_device_manager::GtkPrimarySelectionDeviceManager, - gtk_primary_selection_offer::GtkPrimarySelectionOffer, gtk_primary_selection_source, -}; -use sctk::reexports::protocols::unstable::primary_selection::v1::client::{ - zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1 as PrimarySelectionDeviceMgr, - zwp_primary_selection_device_v1::{ - Event as ZwpPrimarySelectionDeviceEvent, - ZwpPrimarySelectionDeviceV1 as PrimarySelectionDevice, - }, - zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1 as PrimarySelectionOffer, - zwp_primary_selection_source_v1, -}; -use sctk::wayland_client::sys::client::wl_display; - -/// Used to store registered seats and their last event serial -type SeatMap = HashMap< - String, - ( - Arc>>, - u32, - Arc>>, - Arc>>, - Arc>>, - Arc>>, - ), ->; - -/// Object representing the Wayland clipboard -pub struct ThreadedClipboard { - request_send: mpsc::Sender, - load_recv: mpsc::Receiver>, -} - -// Kill thread when clipboard object is dropped -impl Drop for ThreadedClipboard { - fn drop(&mut self) { - self.request_send.send(ThreadRequest::Kill).unwrap() - } -} - -impl ThreadedClipboard { - /// Creates a new wayland clipboard object - /// - /// Spawns a new thread to dispatch messages to the wayland server every - /// 50ms to ensure the server can read stored data - pub fn new(display: &Display) -> Self { - let (request_send, request_recv) = mpsc::channel(); - let (load_send, load_recv) = mpsc::channel(); - let display = display.clone(); - - // Spawn a thread to handle the clipboard as regular dispatching of the wayland thread is needed - std::thread::spawn(move || { - let mut event_queue = display.create_event_queue(); - let display = (*display) - .as_ref() - .make_wrapper(&event_queue.get_token()) - .unwrap(); - clipboard_thread(&display, &mut event_queue, request_recv, load_send); - }); - - ThreadedClipboard { - request_send, - load_recv, - } - } - - /// Creates a new wayland clipboard object from a mutable `wl_display` ptr - /// - /// Spawns a new thread to dispatch messages to the wayland server every - /// 50ms to ensure the server can read stored data - pub unsafe fn new_from_external(display_ptr: *mut wl_display) -> Self { - let (request_send, request_recv) = mpsc::channel(); - let (load_send, load_recv) = mpsc::channel(); - let display = display_ptr.as_mut().unwrap(); - - // Spawn a thread to handle the clipboard as regular dispatching of the wayland thread is needed - std::thread::spawn(move || { - let (display, mut event_queue) = Display::from_external_display(display); - clipboard_thread(&display, &mut event_queue, request_recv, load_send); - }); - - ThreadedClipboard { - request_send, - load_recv, - } - } - - /// Returns text from the wayland clipboard - /// - /// If provided with a seat name that seat must be in - /// focus to work. Otherwise if no seat name is provided - /// the name of the seat to last generate a key or pointer event - /// is used - pub fn load(&mut self, seat_name: Option) -> Result { - self.request_send - .send(ThreadRequest::Load(seat_name)) - .unwrap(); - self.load_recv.recv().unwrap() - } - - /// Stores text in the wayland clipboard - /// - /// If provided with a seat name that seat must be in - /// focus to work. Otherwise if no seat name is provided - /// the name of the seat to last generate a key or pointer event - /// is used - pub fn store(&mut self, seat_name: Option, text: T) - where - T: Into, - { - self.request_send - .send(ThreadRequest::Store(seat_name, text.into())) - .unwrap() - } - - /// Returns text from the primary selection of the wayland clipboard - /// - /// If provided with a seat name that seat must be in - /// focus to work. Otherwise if no seat name is provided - /// the name of the seat to last generate a key or pointer event - /// is used - pub fn load_primary(&mut self, seat_name: Option) -> Result { - self.request_send - .send(ThreadRequest::LoadPrimary(seat_name)) - .unwrap(); - self.load_recv.recv().unwrap() - } - - /// Stores text in the primary selection of the wayland clipboard - /// - /// If provided with a seat name that seat must be in - /// focus to work. Otherwise if no seat name is provided - /// the name of the seat to last generate a key or pointer event - /// is used - pub fn store_primary(&mut self, seat_name: Option, text: String) { - self.request_send - .send(ThreadRequest::StorePrimary(seat_name, text)) - .unwrap() - } -} - -/// Requests sent to the clipboard thread -enum ThreadRequest { - /// Store text in a specific seats clipboard - Store(Option, String), - /// Load text from a specific seats clipboard - Load(Option), - /// Store text in a specific seats primary clipboard - StorePrimary(Option, String), - /// Load text in a specific seats primary clipboard - LoadPrimary(Option), - /// Kill the thread - Kill, -} - -/// Handles the setup and running of the clipboard thread -fn clipboard_thread( - display: &WlDisplay, - event_queue: &mut EventQueue, - request_recv: mpsc::Receiver, - load_send: mpsc::Sender>, -) { - // Create a seat map to register seats - let seat_map = Arc::new(Mutex::new(SeatMap::new())); - - // Store unimplemented seats so we can implement them when the data device manager is implemented - let data_device_manager = Arc::new(Mutex::new(None)); - let mut unimplemented_seats = Vec::new(); - - let primary_selection_device_manager = Arc::new(Mutex::new(None)); - let gtk_primary_selection_device_manager = Arc::new(Mutex::new(None)); - - // Store the name of the seat that last sends an event for use as the default seat - let last_seat_name = Arc::new(Mutex::new(String::new())); - - let data_device_manager_clone = data_device_manager.clone(); - let primary_selection_device_manager_clone = primary_selection_device_manager.clone(); - let gtk_primary_selection_device_manager_clone = gtk_primary_selection_device_manager.clone(); - let seat_map_clone = seat_map.clone(); - let last_seat_name_clone = last_seat_name.clone(); - - // Register wl_seat objects and wl_data_device_manager - GlobalManager::new_with_cb(&display, move |event, reg| { - if let GlobalEvent::New { - id, - ref interface, - version, - } = event - { - if "wl_seat" == interface.as_str() && version >= 2 { - if let Some(ref data_device_manager) = - data_device_manager_clone.lock().unwrap().deref() - { - // Implement the seat - implement_seat( - id, - std::cmp::min(version, 6), // only seat up to version 6 is supported - seat_map_clone.clone(), - last_seat_name_clone.clone(), - data_device_manager, - ®, - primary_selection_device_manager_clone.clone(), - gtk_primary_selection_device_manager_clone.clone(), - ); - } else { - // Store the seat for implementation once wl_data_device_manager is registered - unimplemented_seats.push((id, version)); - } - } else if "wl_data_device_manager" == interface.as_str() { - // Register the wl_data_device_manager - *data_device_manager_clone.lock().unwrap() = Some( - reg.bind::( - version, - id, - NewProxy::implement_dummy, - ) - .unwrap(), - ); - // Implement the unimplemented seats - for (id, version) in &unimplemented_seats { - implement_seat( - *id, - std::cmp::min(*version, 6), // only seat up to version 6 is supported - seat_map_clone.clone(), - last_seat_name_clone.clone(), - data_device_manager_clone.lock().unwrap().as_ref().unwrap(), - ®, - primary_selection_device_manager_clone.clone(), - gtk_primary_selection_device_manager_clone.clone(), - ); - } - } else if "zwp_primary_selection_device_manager_v1" == interface.as_str() { - // Register the zwp_primary_selection_device_manager - *primary_selection_device_manager_clone.lock().unwrap() = Some( - reg.bind::( - version, - id, - NewProxy::implement_dummy, - ) - .unwrap(), - ); - } else if "gtk_primary_selection_device_manager" == interface.as_str() { - *gtk_primary_selection_device_manager_clone.lock().unwrap() = Some( - reg.bind::( - version, - id, - NewProxy::implement_dummy, - ) - .unwrap(), - ); - } - } - }); - event_queue.sync_roundtrip().unwrap(); - - // We should provide lower sleep amounts in a moments of spaming our clipboard - let mut sleep_amount = 50; - // Provide our clipboard a warm start, so 16 initial cycles will be at 1ms and other will go - // like 1 2 4 8 16 32 50 50 and so on - let mut warm_start_amount = 0; - - // Thread loop to handle requests and dispatch the event queue - loop { - if let Ok(request) = request_recv.try_recv() { - // Lower sleep amount to zero, so the next recv will be instant - sleep_amount = 0; - - match request { - // Load text from clipboard - ThreadRequest::Load(seat_name) => { - event_queue.sync_roundtrip().unwrap(); - let seat_map = seat_map.lock().unwrap().clone(); - - // Get the clipboard contents of the requested seat from the seat map - let contents = seat_map - .get(&seat_name.unwrap_or_else(|| last_seat_name.lock().unwrap().clone())) - .map_or(Ok(String::new()), |seat| { - let mut reader = None; - if let Some(device) = seat.0.lock().unwrap().as_ref() { - device.with_selection(|offer| { - if let Some(offer) = offer { - offer.with_mime_types(|types| { - if types - .contains(&"text/plain;charset=utf-8".to_string()) - { - reader = Some( - offer - .receive("text/plain;charset=utf-8".into()) - .unwrap(), - ); - } - }); - } - }); - - event_queue.sync_roundtrip().unwrap(); - } - reader.map_or(Ok(String::new()), |mut reader| { - let mut contents = String::new(); - if let Err(err) = reader.read_to_string(&mut contents) { - Err(err) - } else { - Ok(contents) - } - }) - }); - // Normalization should happen only on `text/plain;charset=utf-8`, in case we - // add other mime types consult gtk for normalization. - let contents = contents.and_then(|contents| Ok(normilize_to_lf(contents))); - load_send.send(contents).unwrap(); - } - // Store text in the clipboard - ThreadRequest::Store(seat_name, contents) => { - event_queue.sync_roundtrip().unwrap(); - let seat_map = seat_map.lock().unwrap().clone(); - - // Get the requested seat from the seat map - if let Some((device, enter_serial, _, _, _, _)) = seat_map - .get(&seat_name.unwrap_or_else(|| last_seat_name.lock().unwrap().clone())) - { - if let Some(device) = device.lock().unwrap().as_ref() { - let data_source = DataSource::new( - data_device_manager.lock().unwrap().as_ref().unwrap(), - &["text/plain;charset=utf-8"], - move |source_event| { - if let DataSourceEvent::Send { mut pipe, .. } = source_event { - write!(pipe, "{}", contents).unwrap(); - } - }, - ); - - device.set_selection(&Some(data_source), *enter_serial); - - event_queue.sync_roundtrip().unwrap(); - } - } - } - // Load text from primary clipboard - ThreadRequest::LoadPrimary(seat_name) => { - event_queue.sync_roundtrip().unwrap(); - let seat_map = seat_map.lock().unwrap().clone(); - - // Get the primary clipboard contents of the requested seat from the seat map - let contents = if primary_selection_device_manager.lock().unwrap().is_some() { - seat_map - .get( - &seat_name - .unwrap_or_else(|| last_seat_name.lock().unwrap().clone()), - ) - .map_or(Ok(String::new()), |seat| { - seat.3.lock().unwrap().as_ref().map_or( - Ok(String::new()), - |primary_offer| { - let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC).unwrap(); - let mut file = - unsafe { std::fs::File::from_raw_fd(readfd) }; - primary_offer.receive( - "text/plain;charset=utf-8".to_string(), - writefd, - ); - close(writefd).unwrap(); - let mut contents = String::new(); - event_queue.sync_roundtrip().unwrap(); - if let Err(err) = file.read_to_string(&mut contents) { - Err(err) - } else { - Ok(contents) - } - }, - ) - }) - } else if gtk_primary_selection_device_manager - .lock() - .unwrap() - .is_some() - { - seat_map - .get( - &seat_name - .unwrap_or_else(|| last_seat_name.lock().unwrap().clone()), - ) - .map_or(Ok(String::new()), |seat| { - seat.5.lock().unwrap().as_ref().map_or( - Ok(String::new()), - |primary_offer| { - let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC).unwrap(); - let mut file = - unsafe { std::fs::File::from_raw_fd(readfd) }; - primary_offer.receive( - "text/plain;charset=utf-8".to_string(), - writefd, - ); - close(writefd).unwrap(); - let mut contents = String::new(); - event_queue.sync_roundtrip().unwrap(); - if let Err(err) = file.read_to_string(&mut contents) { - Err(err) - } else { - Ok(contents) - } - }, - ) - }) - } else { - Ok(String::new()) - }; - // Normalization should happen only on `text/plain;charset=utf-8`, in case we - // add other mime types consult gtk for normalization. - let contents = contents.and_then(|contents| Ok(normilize_to_lf(contents))); - load_send.send(contents).unwrap(); - } - // Store text in the primary clipboard - ThreadRequest::StorePrimary(seat_name, contents) => { - event_queue.sync_roundtrip().unwrap(); - let seat_map = seat_map.lock().unwrap().clone(); - - // Get the requested seat from the seat map - if let Some((_, enter_serial, primary_device, _, gtk_primary_device, _)) = - seat_map.get( - &seat_name.unwrap_or_else(|| last_seat_name.lock().unwrap().clone()), - ) - { - if let Some(manager) = &*primary_selection_device_manager.lock().unwrap() { - if let Some(primary_device) = &*primary_device.lock().unwrap() { - let source = manager.create_source(|proxy| { - proxy.implement_closure( - move |event, _| { - if let zwp_primary_selection_source_v1::Event::Send { - mime_type, - fd, - } = event - { - if mime_type == "text/plain;charset=utf-8" { - let mut file = - unsafe { std::fs::File::from_raw_fd(fd) }; - file.write_fmt(format_args!("{}", contents)) - .unwrap(); - } - } - }, - (), - ) - }); - if let Ok(source) = &source { - source.offer("text/plain;charset=utf-8".to_string()); - } - primary_device.set_selection(source.ok().as_ref(), *enter_serial); - } - } else if let Some(manager) = - &*gtk_primary_selection_device_manager.lock().unwrap() - { - if let Some(gtk_primary_device) = &*gtk_primary_device.lock().unwrap() { - let source = manager.create_source(|proxy| { - proxy.implement_closure( - move |event, _| { - if let gtk_primary_selection_source::Event::Send { - mime_type, - fd, - } = event - { - if mime_type == "text/plain;charset=utf-8" { - let mut file = - unsafe { std::fs::File::from_raw_fd(fd) }; - file.write_fmt(format_args!("{}", contents)) - .unwrap(); - } - } - }, - (), - ) - }); - if let Ok(source) = &source { - source.offer("text/plain;charset=utf-8".to_string()); - } - gtk_primary_device - .set_selection(source.ok().as_ref(), *enter_serial); - } - } - } - } - ThreadRequest::Kill => break, - } - } - // Dispatch the event queue and block for `sleep_amount` - let pending_events = event_queue.dispatch_pending().unwrap(); - let num_seats = seat_map.lock().unwrap().len(); - - // If some app is trying to spam us when there no seats, it's likely that someone is - // trying to paste from us - if num_seats == 0 && pending_events != 0 { - sleep_amount = 0; - } else if sleep_amount > 0 { - thread::sleep(Duration::from_millis(sleep_amount)); - - if warm_start_amount < 16 { - warm_start_amount += 1; - if warm_start_amount == 16 { - sleep_amount = 1; - } - } else if sleep_amount < 50 { - // The aim of this different sleep times is to provide a good performance under - // high load and not waste system resources too much when idle - sleep_amount = std::cmp::min(2 * sleep_amount, 50); - } - } else if sleep_amount == 0 { - // Reset sleep amount from zero back to one, so sleep sequence could reach 50 - sleep_amount = 1; - // Reset warm start to accelerate the initial clipboard requests - warm_start_amount = 0; - } - } -} - -/// Implement seats that we register -fn implement_seat( - id: u32, - version: u32, - seat_map: Arc>, - last_seat_name: Arc>, - data_device_manager: &wl_data_device_manager::WlDataDeviceManager, - reg: &wl_registry::WlRegistry, - primary_device_manager: Arc>>, - gtk_primary_device_manager: Arc>>, -) { - let device = Arc::new(Mutex::new(None)); - let device_clone = device.clone(); - let seat_name = Arc::new(Mutex::new(String::new())); - let seat_name_clone = seat_name.clone(); - let seat_map_clone = seat_map.clone(); - - let primary_device = Arc::new(Mutex::new(None)); - let primary_offer = Arc::new(Mutex::new(None)); - - let primary_device_clone = primary_device.clone(); - let primary_offer_clone = primary_offer.clone(); - - let gtk_primary_device = Arc::new(Mutex::new(None)); - let gtk_primary_offer = Arc::new(Mutex::new(None)); - - let gtk_primary_device_clone = gtk_primary_device.clone(); - let gtk_primary_offer_clone = gtk_primary_offer.clone(); - - let mut pointer = None; - let mut keyboard = None; - - // Register the seat - let seat = reg - .bind::(version, id, move |proxy| { - proxy.implement_closure( - move |event, seat| match event { - wl_seat::Event::Name { name } => *seat_name_clone.lock().unwrap() = name, - wl_seat::Event::Capabilities { capabilities } => { - if capabilities.contains(Capability::Pointer) { - if pointer.is_none() { - let device_clone = device_clone.clone(); - - let primary_device_clone = primary_device_clone.clone(); - let primary_offer_clone = primary_offer_clone.clone(); - - let gtk_primary_device_clone = gtk_primary_device_clone.clone(); - let gtk_primary_offer_clone = gtk_primary_offer_clone.clone(); - - let last_seat_name_clone = last_seat_name.clone(); - let seat_map_clone = seat_map_clone.clone(); - let seat_name_clone = seat_name_clone.clone(); - pointer = Some( - seat.get_pointer(move |pointer| { - pointer.implement_closure( - move |evt, _| { - // Set this seat as the last to send an event - *last_seat_name_clone.lock().unwrap() = - seat_name_clone.lock().unwrap().clone(); - - // Get serials from recieved events from the seat - // pointer - match evt { - PtrEvent::Enter { serial, .. } => { - if let Some(seat) = - seat_map_clone.lock().unwrap().get_mut( - &seat_name_clone - .lock() - .unwrap() - .clone(), - ) - { - // Update serial if "seat" is already - // presented - seat.1 = serial; - return; - } - - seat_map_clone.lock().unwrap().insert( - seat_name_clone.lock().unwrap().clone(), - ( - device_clone.clone(), - serial, - primary_device_clone.clone(), - primary_offer_clone.clone(), - gtk_primary_device_clone.clone(), - gtk_primary_offer_clone.clone(), - ), - ); - } - PtrEvent::Button { serial, .. } => { - if let Some(seat) = - seat_map_clone.lock().unwrap().get_mut( - &seat_name_clone - .lock() - .unwrap() - .clone(), - ) - { - // Update serial if seat is already - // presented - seat.1 = serial; - return; - } - - // This is for consistency with - // `PtrEvent::Enter` - seat_map_clone.lock().unwrap().insert( - seat_name_clone.lock().unwrap().clone(), - ( - device_clone.clone(), - serial, - primary_device_clone.clone(), - primary_offer_clone.clone(), - gtk_primary_device_clone.clone(), - gtk_primary_offer_clone.clone(), - ), - ); - } - _ => {} - } - }, - (), - ) - }) - .unwrap(), - ); - } - } else if let Some(pointer) = pointer.take() { - // Release old pointer - if pointer.as_ref().version() >= 3 { - pointer.release(); - } - } - - if capabilities.contains(Capability::Keyboard) { - if keyboard.is_none() { - let device_clone = device_clone.clone(); - - let primary_device_clone = primary_device_clone.clone(); - let primary_offer_clone = primary_offer_clone.clone(); - - let gtk_primary_device_clone = gtk_primary_device_clone.clone(); - let gtk_primary_offer_clone = gtk_primary_offer_clone.clone(); - - let last_seat_name_clone = last_seat_name.clone(); - let seat_map_clone = seat_map_clone.clone(); - let seat_name_clone = seat_name_clone.clone(); - keyboard = Some( - map_keyboard_auto(&seat, move |event, _| { - // Set this seat as the last to send an event - *last_seat_name_clone.lock().unwrap() = - seat_name_clone.lock().unwrap().clone(); - - // Get serials from recieved events from the seat keyboard - match event { - KbEvent::Enter { serial, .. } => { - seat_map_clone.lock().unwrap().insert( - seat_name_clone.lock().unwrap().clone(), - ( - device_clone.clone(), - serial, - primary_device_clone.clone(), - primary_offer_clone.clone(), - gtk_primary_device_clone.clone(), - gtk_primary_offer_clone.clone(), - ), - ); - } - KbEvent::Key { serial, .. } => { - seat_map_clone.lock().unwrap().insert( - seat_name_clone.lock().unwrap().clone(), - ( - device_clone.clone(), - serial, - primary_device_clone.clone(), - primary_offer_clone.clone(), - gtk_primary_device_clone.clone(), - gtk_primary_offer_clone.clone(), - ), - ); - } - KbEvent::Leave { .. } => { - seat_map_clone - .lock() - .unwrap() - .remove(&*seat_name_clone.lock().unwrap()); - } - _ => {} - } - }) - .unwrap(), - ); - } - } else if let Some(keyboard) = keyboard.take() { - // Release old keyboard - if keyboard.as_ref().version() >= 3 { - keyboard.release(); - } - } - } - _ => (), - }, - (), - ) - }) - .unwrap(); - - // Create a device for the seat - *device.lock().unwrap() = Some(DataDevice::init_for_seat( - data_device_manager, - &seat, - |_| {}, - )); - - if let Some(manager) = &*primary_device_manager.lock().unwrap() { - *primary_device.lock().unwrap() = manager - .get_device(&seat, |proxy| { - proxy.implement_closure( - move |event, _| { - if let ZwpPrimarySelectionDeviceEvent::DataOffer { offer } = event { - *primary_offer.lock().unwrap() = Some(offer.implement_dummy()); - - let map_contents = seat_map - .lock() - .unwrap() - .get(&seat_name.lock().unwrap().clone()) - .cloned(); - if let Some(map_contents) = map_contents { - seat_map.lock().unwrap().insert( - seat_name.lock().unwrap().clone(), - ( - map_contents.0.clone(), - map_contents.1, - map_contents.2, - primary_offer.clone(), - Arc::new(Mutex::new(None)), - Arc::new(Mutex::new(None)), - ), - ); - } - } - }, - (), - ) - }) - .ok(); - } else if let Some(manager) = &*gtk_primary_device_manager.lock().unwrap() { - *gtk_primary_device.lock().unwrap() = manager - .get_device(&seat, |proxy| { - proxy.implement_closure( - move |event, _| { - if let GtkPrimarySelectionDeviceEvent::DataOffer { offer } = event { - *gtk_primary_offer.lock().unwrap() = Some(offer.implement_dummy()); - - let map_contents = seat_map - .lock() - .unwrap() - .get(&seat_name.lock().unwrap().clone()) - .cloned(); - if let Some(map_contents) = map_contents { - seat_map.lock().unwrap().insert( - seat_name.lock().unwrap().clone(), - ( - map_contents.0.clone(), - map_contents.1, - Arc::new(Mutex::new(None)), - Arc::new(Mutex::new(None)), - map_contents.4, - gtk_primary_offer.clone(), - ), - ); - } - } - }, - (), - ) - }) - .ok(); - } -} - -// Normalize \r and \r\n into \n. -// -// Gtk does this for text/plain;charset=utf-8, so following them here, otherwise there is -// a chance of getting extra new lines on load, since they're converting \r and \n into -// \r\n on every store. -fn normilize_to_lf(text: String) -> String { - text.replace("\r\n", "\n").replace("\r", "\n") -} diff --git a/src/worker/dispatch_data.rs b/src/worker/dispatch_data.rs new file mode 100644 index 0000000..7411ef3 --- /dev/null +++ b/src/worker/dispatch_data.rs @@ -0,0 +1,47 @@ +use sctk::reexports::client::protocol::wl_seat::WlSeat; + +/// Data to track latest seat and serial for clipboard requests. +#[derive(Default)] +pub struct ClipboardDispatchData { + observed_seats: Vec<(WlSeat, u32)>, + last_pos: usize, +} + +impl ClipboardDispatchData { + /// Builds new `ClipboardDispatchData` with all fields equal to `None`. + pub fn new() -> Self { + Self::default() + } + + /// Set the last observed seat. + pub fn set_last_seat(&mut self, seat: WlSeat, serial: u32) { + let pos = self.observed_seats.iter().position(|st| st.0 == seat); + match pos { + Some(pos) => { + // Update serial and set the last data we've seen. + self.observed_seats[pos].1 = serial; + self.last_pos = pos; + } + None => { + // Add new seat and mark it as last. + self.last_pos = self.observed_seats.len(); + self.observed_seats.push((seat, serial)); + } + } + } + + /// Remove the given seat from the observer seats. + pub fn remove_seat(&mut self, seat: WlSeat) { + let pos = self.observed_seats.iter().position(|st| st.0 == seat); + + if let Some(pos) = pos { + // Remove the seat data. + self.observed_seats.remove(pos); + } + } + + /// Return the last observed seat and the serial. + pub fn last_seat(&self) -> Option<&(WlSeat, u32)> { + self.observed_seats.get(self.last_pos) + } +} diff --git a/src/worker/handlers.rs b/src/worker/handlers.rs new file mode 100644 index 0000000..5e96b2c --- /dev/null +++ b/src/worker/handlers.rs @@ -0,0 +1,131 @@ +#![macro_use] + +use std::io::Result; +use std::sync::mpsc::Sender; + +use sctk::reexports::client::protocol::wl_keyboard::Event as KeyboardEvent; +use sctk::reexports::client::protocol::wl_pointer::Event as PointerEvent; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::DispatchData; + +use super::dispatch_data::ClipboardDispatchData; + +/// Macro to handle load for selection and primary clipboards. +macro_rules! handle_load { + ($env:ident, $sel_ty:ident, $seat:ident, $queue:ident, $tx:ident ) => { + let result = $env.$sel_ty(&$seat, |device| { + let (mut reader, mime_type) = match device.with_selection(|offer| { + // Check that we have an offer. + let offer = match offer { + Some(offer) => offer, + None => return None, + }; + + // Check that we can work with advertised mime type and pick the one + // that suits us more. + let mime_type = match offer.with_mime_types(MimeType::find_allowed) { + Some(mime_type) => mime_type, + None => return None, + }; + + // Request given the mime type. + let reader = offer.receive(mime_type.to_string()).ok()?; + Some((reader, mime_type)) + }) { + Some((reader, mime_type)) => (reader, mime_type), + None => { + handlers::reply_error(&$tx, "offer receive failed."); + return (); + } + }; + + $queue + .sync_roundtrip(&mut (), |_, _, _| unreachable!()) + .unwrap(); + + let mut contents = String::new(); + let result = reader.read_to_string(&mut contents).map(|_| { + if mime_type == MimeType::Utf8String { + mime::normilize_to_lf(contents) + } else { + contents + } + }); + + $tx.send(result).unwrap(); + }); + + // Send back that we've failed to load data from the clipboard. + if result.is_err() { + handlers::reply_error(&$tx, "failed to access clipboard."); + } + }; +} + +/// Macro to handle store for selection and primary clipboards. +macro_rules! handle_store { + ($env:ident, + $sel_source:ident, $sel_device:ident, $event_ty:ident, + $seat:ident, $serial:ident, $queue:ident, $contents:ident) => { + let data_source = $env.$sel_source( + vec![MimeType::TextPlainUtf8.to_string()], + move |event, _| { + if let $event_ty::Send { mut pipe, .. } = event { + write!(pipe, "{}", $contents).unwrap(); + } + }, + ); + + let _ = $env.$sel_device(&$seat, |device| { + device.set_selection(&Some(data_source), $serial); + + let _ = $queue.sync_roundtrip(&mut (), |_, _, _| unreachable!()); + }); + }; +} + +/// Reply an error to a clipboard master. +pub fn reply_error(tx: &Sender>, description: &str) { + tx.send(Err(std::io::Error::new( + std::io::ErrorKind::Other, + description, + ))) + .unwrap(); +} + +/// Update seat and serial on pointer events. +pub fn pointer_handler(seat: WlSeat, event: PointerEvent, mut dispatch_data: DispatchData) { + let dispatch_data = match dispatch_data.get::() { + Some(dispatch_data) => dispatch_data, + None => return, + }; + match event { + PointerEvent::Enter { serial, .. } => { + dispatch_data.set_last_seat(seat, serial); + } + PointerEvent::Button { serial, .. } => { + dispatch_data.set_last_seat(seat, serial); + } + _ => {} + } +} + +/// Update seat and serial on keyboard events. +pub fn keyboard_handler(seat: WlSeat, event: KeyboardEvent, mut dispatch_data: DispatchData) { + let dispatch_data = match dispatch_data.get::() { + Some(dispatch_data) => dispatch_data, + None => return, + }; + match event { + KeyboardEvent::Enter { serial, .. } => { + dispatch_data.set_last_seat(seat, serial); + } + KeyboardEvent::Key { serial, .. } => { + dispatch_data.set_last_seat(seat, serial); + } + KeyboardEvent::Leave { .. } => { + dispatch_data.remove_seat(seat); + } + _ => {} + } +} diff --git a/src/worker/mod.rs b/src/worker/mod.rs new file mode 100644 index 0000000..5c91050 --- /dev/null +++ b/src/worker/mod.rs @@ -0,0 +1,288 @@ +use std::io::prelude::*; +use std::io::Result; +use std::sync::mpsc::{Receiver, Sender}; +use std::time::Duration; + +use sctk::reexports::client::protocol::wl_data_device_manager::WlDataDeviceManager; +use sctk::reexports::client::Display; + +use sctk::data_device::DataSourceEvent; +use sctk::primary_selection::PrimarySelectionSourceEvent; + +use sctk::environment::Environment; +use sctk::seat; + +use crate::env::SmithayClipboard; +use crate::mime::{self, MimeType}; + +mod dispatch_data; +mod handlers; +mod seat_data; +mod sleep_amount_tracker; + +use dispatch_data::ClipboardDispatchData; +use seat_data::SeatData; +use sleep_amount_tracker::SleepAmountTracker; + +/// Max time clipboard thread can sleep. +const MAX_TIME_TO_SLEEP: u8 = 50; + +/// Max warm wakeups. +const MAX_WARM_WAKEUPS: u8 = 16; + +/// Spawn a clipboard worker, which dispatches it's own `EventQueue` each 50ms and handles +/// clipboard requests. +pub fn spawn( + name: String, + display: Display, + worker_receiver: Receiver, + worker_replier: Sender>, +) -> Option> { + std::thread::Builder::new() + .name(name) + .spawn(move || { + worker_impl(display, worker_receiver, worker_replier); + }) + .ok() +} + +/// Clipboard worker thread command. +#[derive(Eq, PartialEq)] +pub enum Command { + /// Store data to a clipboard. + Store(String), + /// Store data to a primary selection. + StorePrimary(String), + /// Load data from a clipboard. + Load, + /// Load primary selection. + LoadPrimary, + /// Shutdown the worker. + Exit, +} + +/// Handle clipboard requests. +fn worker_impl(display: Display, request_rx: Receiver, reply_tx: Sender>) { + let mut queue = display.create_event_queue(); + let display_proxy = display.attach(queue.token()); + + let env = Environment::init(&display_proxy, SmithayClipboard::new()); + let req = queue.sync_roundtrip(&mut (), |_, _, _| unreachable!()); + let _ = req + .and_then(|_| queue.sync_roundtrip(&mut (), |_, _, _| unreachable!())) + .unwrap(); + + // Get data device manager. + let data_device_manager = env.get_global::(); + + // Get primary selection device manager. + let primary_selection_manager = env.get_primary_selection_manager(); + + // Both clipboards are not available, spin the loop and reply to a clipboard master. + if data_device_manager.is_none() && primary_selection_manager.is_none() { + loop { + if let Ok(event) = request_rx.recv() { + match event { + Command::Exit => { + return; + } + _ => { + // Reply with error + handlers::reply_error(&reply_tx, "Clipboard are missing."); + } + } + } + } + } + + // Track seats. + let mut seats = Vec::::new(); + + for seat in env.get_all_seats() { + let seat_data = match seat::clone_seat_data(&seat) { + Some(seat_data) => { + // Handle defunct seats early on. + if seat_data.defunct { + seats.push(SeatData::new(seat.detach(), None, None)); + continue; + } + + seat_data + } + _ => continue, + }; + + let keyboard = if seat_data.has_keyboard { + let keyboard = seat.get_keyboard(); + let seat_clone = seat.clone(); + + keyboard.quick_assign(move |_keyboard, event, dispatch_data| { + handlers::keyboard_handler(seat_clone.detach(), event, dispatch_data); + }); + + Some(keyboard.detach()) + } else { + None + }; + + let pointer = if seat_data.has_pointer { + let pointer = seat.get_pointer(); + let seat_clone = seat.clone(); + + pointer.quick_assign(move |_pointer, event, dispatch_data| { + handlers::pointer_handler(seat_clone.detach(), event, dispatch_data); + }); + + Some(pointer.detach()) + } else { + None + }; + + // Track the seat. + seats.push(SeatData::new(seat.detach(), keyboard, pointer)); + } + + // Listen for seats. + let _listener = env.listen_for_seats(move |seat, seat_data, _| { + let detached_seat = seat.clone().detach(); + let pos = seats.iter().position(|st| st.seat == detached_seat); + let index = pos.unwrap_or_else(|| { + seats.push(SeatData::new(detached_seat, None, None)); + seats.len() - 1 + }); + + let seat_resources = &mut seats[index]; + + if seat_data.has_keyboard && !seat_data.defunct { + if seat_resources.keyboard.is_none() { + let keyboard = seat.get_keyboard(); + let seat_clone = seat.clone(); + + keyboard.quick_assign(move |_keyboard, event, dispatch_data| { + handlers::keyboard_handler(seat_clone.detach(), event, dispatch_data); + }); + + seat_resources.keyboard = Some(keyboard.detach()); + } + } else { + // Clean up. + if let Some(keyboard) = seat_resources.keyboard.take() { + if keyboard.as_ref().version() >= 3 { + keyboard.release(); + } + } + } + + if seat_data.has_pointer && !seat_data.defunct { + if seat_resources.pointer.is_none() { + let pointer = seat.get_pointer(); + + pointer.quick_assign(move |_pointer, event, dispatch_data| { + handlers::pointer_handler(seat.detach(), event, dispatch_data); + }); + + seat_resources.pointer = Some(pointer.detach()); + } + } else if let Some(pointer) = seat_resources.pointer.take() { + // Clean up. + if pointer.as_ref().version() >= 3 { + pointer.release(); + } + } + }); + + // Flush the display. + let _ = queue.display().flush(); + + let mut dispatch_data = ClipboardDispatchData::new(); + + // Setup sleep amount tracker. + let mut sa_tracker = SleepAmountTracker::new(MAX_TIME_TO_SLEEP, MAX_WARM_WAKEUPS); + + loop { + // Try to get event from the user. + if let Ok(request) = request_rx.try_recv() { + // Break early on to handle shutdown gracefully, otherwise we can crash on + // `sync_roundtrip`, if client closed connection to a server before releasing the + // clipboard. + if request == Command::Exit { + break; + } + // Reset the time we're sleeping. + sa_tracker.reset_sleep(); + + queue + .sync_roundtrip(&mut dispatch_data, |_, _, _| unimplemented!()) + .unwrap(); + + // Get latest observed seat and serial. + let (seat, serial) = dispatch_data.last_seat().unwrap(); + let serial = *serial; + + // Handle requests. + match request { + Command::Load => { + if data_device_manager.is_some() { + handle_load!(env, with_data_device, seat, queue, reply_tx); + } else { + handlers::reply_error(&reply_tx, "Clipboard is not available"); + } + } + Command::Store(contents) => { + if data_device_manager.is_some() { + handle_store!( + env, + new_data_source, + with_data_device, + DataSourceEvent, + seat, + serial, + queue, + contents + ); + } + } + Command::LoadPrimary => { + if primary_selection_manager.is_some() { + handle_load!(env, with_primary_selection, seat, queue, reply_tx); + } else { + handlers::reply_error(&reply_tx, "Primary clipboard is not available"); + } + } + Command::StorePrimary(contents) => { + if primary_selection_manager.is_some() { + handle_store!( + env, + new_primary_selection_source, + with_primary_selection, + PrimarySelectionSourceEvent, + seat, + serial, + queue, + contents + ); + } + } + _ => unreachable!(), + } + } + + let pending_events = queue + .dispatch_pending(&mut dispatch_data, |_, _, _| {}) + .unwrap(); + + // If some application is trying to spam us when there're no seats, it's likely that + // someone is trying to paste from us. + if dispatch_data.last_seat().is_none() && pending_events != 0 { + sa_tracker.reset_sleep(); + } else { + // Time for thread to sleep. + let tts = sa_tracker.sleep_amount(); + if tts > 0 { + std::thread::sleep(Duration::from_millis(tts as _)); + } + + sa_tracker.increase_sleep(); + } + } +} diff --git a/src/worker/seat_data.rs b/src/worker/seat_data.rs new file mode 100644 index 0000000..9ab58fa --- /dev/null +++ b/src/worker/seat_data.rs @@ -0,0 +1,20 @@ +use sctk::reexports::client::protocol::wl_keyboard::WlKeyboard; +use sctk::reexports::client::protocol::wl_pointer::WlPointer; +use sctk::reexports::client::protocol::wl_seat::WlSeat; + +/// Data to track seat capability changes and handle release of the objects. +pub struct SeatData { + pub seat: WlSeat, + pub keyboard: Option, + pub pointer: Option, +} + +impl SeatData { + pub fn new(seat: WlSeat, keyboard: Option, pointer: Option) -> Self { + SeatData { + seat, + keyboard, + pointer, + } + } +} diff --git a/src/worker/sleep_amount_tracker.rs b/src/worker/sleep_amount_tracker.rs new file mode 100644 index 0000000..9e254ac --- /dev/null +++ b/src/worker/sleep_amount_tracker.rs @@ -0,0 +1,60 @@ +/// Handles dynamic thread sleep time. +pub struct SleepAmountTracker { + /// The maximum amount of time for thread to sleep. + max_time_to_sleep: u8, + /// Current time to block the thread. + time_to_sleep: u8, + /// The current warm wakeup number. + warm_wakeup: u8, + /// The maximum amount of clipboard wakeups in a row with low sleep amount. + max_warm_wakeups: u8, +} + +impl SleepAmountTracker { + /// Build new tracker for sleep amount. + /// + /// `max_time_to_sleep` - maximum sleep value for a thread. + /// `` + pub fn new(max_time_to_sleep: u8, max_warm_wakeups: u8) -> Self { + Self { + max_time_to_sleep, + max_warm_wakeups, + warm_wakeup: 0, + time_to_sleep: 0, + } + } + + /// Reset the current sleep amount to 0ms. + #[inline] + pub fn reset_sleep(&mut self) { + self.time_to_sleep = 0; + } + + /// Adjust the sleep amount. + #[inline] + pub fn increase_sleep(&mut self) { + if self.time_to_sleep == 0 { + // Reset `time_to_sleep` to one, so we can reach `max_time_to_sleep`. + self.time_to_sleep = 1; + // Reset `warm_wakeup` count. + self.warm_wakeup = 0; + + return; + } + + if self.warm_wakeup < self.max_warm_wakeups { + // Handled warm wake up. + self.warm_wakeup += 1; + } else if self.time_to_sleep < self.max_warm_wakeups { + // The aim of this different sleep times is to provide a good performance under + // high the load and not waste system resources too much when idle. + self.time_to_sleep = std::cmp::min(2 * self.time_to_sleep, self.max_time_to_sleep); + } + } + + /// Get the current time to sleep in ms. + #[inline] + pub fn sleep_amount(&self) -> u8 { + self.time_to_sleep + } +}