diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de22d16..1eb2be0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,9 +37,16 @@ jobs: - uses: hecrj/setup-rust-action@v1 with: rust-version: ${{ matrix.rust_version }} + components: clippy - - name: Check documentation - run: cargo doc --no-deps --document-private-items + - name: Install system dependencies + run: sudo apt-get install libxkbcommon-dev libwayland-dev - name: Run tests run: cargo test --verbose + + - name: Clippy + run: cargo clippy -- -D warnings + + - name: Check documentation + run: cargo doc --no-deps --document-private-items diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ba8023..15bd276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Update SCTK to 0.18 +- Fix active polling of the clipboard each 50ms +- Fix freeze when copying data larger than the pipe buffer size + ## 0.6.6 -- 2022-06-20 - Update SCTK to 0.16 diff --git a/Cargo.toml b/Cargo.toml index ace0e5e..be8dd60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "smithay-clipboard" version = "0.6.6" -authors = ["Kirill Chibisov ", "Victor Berger ", "Lucas Timmins "] -edition = "2018" +authors = ["Kirill Chibisov ", "Victor Berger "] +edition = "2021" description = "Provides access to the wayland clipboard for client applications." repository = "https://github.com/smithay/smithay-clipboard" documentation = "https://smithay.github.io/smithay-clipboard" @@ -11,12 +11,13 @@ keywords = ["clipboard", "wayland"] rust-version = "1.65.0" [dependencies] -sctk = { package = "smithay-client-toolkit", version = "0.16", default-features = false } -wayland-client = { version = "0.29", features = ["use_system_lib"] } +libc = "0.2.149" +sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop"] } +wayland-backend = { version = "0.3.0", default_features = false, features = ["client_system"] } [dev-dependencies] -sctk = { package = "smithay-client-toolkit", version = "0.16", default-features = false, features = ["calloop"] } +sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop", "xkbcommon"] } [features] default = ["dlopen"] -dlopen = ["sctk/dlopen", "wayland-client/dlopen"] +dlopen = ["wayland-backend/dlopen" ] diff --git a/examples/clipboard.rs b/examples/clipboard.rs index aa2b75c..15fc026 100644 --- a/examples/clipboard.rs +++ b/examples/clipboard.rs @@ -1,269 +1,405 @@ -use std::io::{BufWriter, Seek, SeekFrom, Write}; +// The example just demonstrates how to integrate the smithay-clipboard into the +// application. For more details on what is going on, consult the +// `smithay-client-toolkit` examples. -use sctk::seat; -use sctk::seat::keyboard::{self, Event as KeyboardEvent, KeyState, RepeatKind}; -use sctk::shm::MemPool; -use sctk::window::{Event as WindowEvent, FallbackFrame}; +use std::convert::TryInto; -use sctk::reexports::client::protocol::wl_shm; -use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::compositor::{CompositorHandler, CompositorState}; +use sctk::output::{OutputHandler, OutputState}; +use sctk::reexports::calloop::{EventLoop, LoopHandle}; +use sctk::reexports::calloop_wayland_source::WaylandSource; +use sctk::reexports::client::globals::registry_queue_init; +use sctk::reexports::client::protocol::{wl_keyboard, wl_output, wl_seat, wl_shm, wl_surface}; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; +use sctk::registry::{ProvidesRegistryState, RegistryState}; +use sctk::seat::keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers}; +use sctk::seat::{Capability, SeatHandler, SeatState}; +use sctk::shell::xdg::window::{Window, WindowConfigure, WindowDecorations, WindowHandler}; +use sctk::shell::xdg::XdgShell; +use sctk::shell::WaylandSurface; +use sctk::shm::slot::{Buffer, SlotPool}; +use sctk::shm::{Shm, ShmHandler}; +use sctk::{ + delegate_compositor, delegate_keyboard, delegate_output, delegate_registry, delegate_seat, + delegate_shm, delegate_xdg_shell, delegate_xdg_window, registry_handlers, +}; +use smithay_clipboard::{self, Clipboard}; -use smithay_clipboard::Clipboard; - -sctk::default_environment!(ClipboardExample, desktop); - -/// 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, -} - -impl DispatchData { - fn new(clipboard: Clipboard) -> Self { - Self { pending_frame_event: None, clipboard } - } -} +const MIN_DIM_SIZE: usize = 256; fn main() { - // Setup default desktop environment - let (env, display, queue) = sctk::new_default_environment!(ClipboardExample, desktop) - .expect("unable to connect to a Wayland compositor."); + let connection = Connection::connect_to_env().unwrap(); + let (globals, event_queue) = registry_queue_init(&connection).unwrap(); + let queue_handle = event_queue.handle(); + let mut event_loop: EventLoop = + EventLoop::try_new().expect("Failed to initialize the event loop!"); + let loop_handle = event_loop.handle(); + WaylandSource::new(connection.clone(), event_queue).insert(loop_handle).unwrap(); - // Create event loop - let mut event_loop = sctk::reexports::calloop::EventLoop::::try_new().unwrap(); + let compositor = + CompositorState::bind(&globals, &queue_handle).expect("wl_compositor not available"); + let xdg_shell = XdgShell::bind(&globals, &queue_handle).expect("xdg shell is not available"); - // Initial window dimentions - let mut dimentions = (320u32, 240u32); + let shm = Shm::bind(&globals, &queue_handle).expect("wl shm is not available."); + let surface = compositor.create_surface(&queue_handle); + let window = xdg_shell.create_window(surface, WindowDecorations::RequestServer, &queue_handle); - // Create surface - let surface = env.create_surface().detach(); + window.set_title(String::from("smithay-clipboard example. Press C/c/P/p to copy/paste")); + window.set_min_size(Some((MIN_DIM_SIZE as u32, MIN_DIM_SIZE as u32))); + window.commit(); - // Create window - let mut window = env - .create_window::( - surface, - None, - dimentions, - move |event, mut dispatch_data| { - // Get our dispath data - let dispatch_data = dispatch_data.get::().unwrap(); + let clipboard = unsafe { Clipboard::new(connection.display().id().as_ptr() as *mut _) }; - // 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 pool = SlotPool::new(MIN_DIM_SIZE * MIN_DIM_SIZE * 4, &shm).expect("Failed to create pool"); - if should_replace_event { - dispatch_data.pending_frame_event = Some(event); - } - }, - ) - .expect("failed to create a window."); + let mut simple_window = SimpleWindow { + registry_state: RegistryState::new(&globals), + seat_state: SeatState::new(&globals, &queue_handle), + output_state: OutputState::new(&globals, &queue_handle), + shm, + clipboard, - // 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::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, - }; - - 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) => { - seats.push((seat.detach(), Some(keyboard))); - } - 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)); - } - } - - // 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) => { - *mapped_keyboard = Some(keyboard); - } - Err(err) => { - eprintln!("Failed to map keyboard on seat {} : {:?}", seat_data.name, err); - } - } - } - } else if let Some(keyboard) = mapped_keyboard.take() { - if keyboard.as_ref().version() >= 3 { - keyboard.release(); - } - } - }); - - 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 = unsafe { 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, + exit: false, + first_configure: true, + pool, + width: 256, + height: 256, + buffer: None, + window, + keyboard: None, + keyboard_focus: false, + loop_handle: event_loop.handle(), }; - 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); + // We don't draw immediately, the configure will notify us when to first draw. + loop { + event_loop.dispatch(None, &mut simple_window).unwrap(); + + if simple_window.exit { + break; } - // Paste. - "p" => { - let contents = dispatch_data - .clipboard - .load() - .unwrap_or_else(|_| String::from("Failed to load 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"); +struct SimpleWindow { + registry_state: RegistryState, + seat_state: SeatState, + output_state: OutputState, + shm: Shm, + clipboard: Clipboard, - { - 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()?; + exit: bool, + first_configure: bool, + pool: SlotPool, + width: u32, + height: u32, + buffer: Option, + window: Window, + keyboard: Option, + keyboard_focus: bool, + loop_handle: LoopHandle<'static, SimpleWindow>, +} + +impl CompositorHandler for SimpleWindow { + fn scale_factor_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _new_factor: i32, + ) { + // Not needed for this example. } - let new_buffer = pool.buffer( - 0, - 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(); + fn transform_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _new_transform: wl_output::Transform, + ) { + // Not needed for this example. + } - Ok(()) + fn frame( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _time: u32, + ) { + self.draw(conn, qh); + } +} + +impl OutputHandler for SimpleWindow { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } + + fn update_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } + + fn output_destroyed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } +} + +impl WindowHandler for SimpleWindow { + fn request_close(&mut self, _: &Connection, _: &QueueHandle, _: &Window) { + self.exit = true; + } + + fn configure( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _window: &Window, + configure: WindowConfigure, + _serial: u32, + ) { + println!("Window configured to: {:?}", configure); + + self.buffer = None; + self.width = configure.new_size.0.map(|v| v.get()).unwrap_or(256); + self.height = configure.new_size.1.map(|v| v.get()).unwrap_or(256); + + // Initiate the first draw. + if self.first_configure { + self.first_configure = false; + self.draw(conn, qh); + } + } +} + +impl SeatHandler for SimpleWindow { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn new_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} + + fn new_capability( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + seat: wl_seat::WlSeat, + capability: Capability, + ) { + if capability == Capability::Keyboard && self.keyboard.is_none() { + println!("Set keyboard capability"); + let keyboard = self + .seat_state + .get_keyboard_with_repeat( + qh, + &seat, + None, + self.loop_handle.clone(), + Box::new(|_state, _wl_kbd, event| { + println!("Repeat: {:?} ", event); + }), + ) + .expect("Failed to create keyboard"); + + self.keyboard = Some(keyboard); + } + } + + fn remove_capability( + &mut self, + _conn: &Connection, + _: &QueueHandle, + _: wl_seat::WlSeat, + capability: Capability, + ) { + if capability == Capability::Keyboard && self.keyboard.is_some() { + println!("Unset keyboard capability"); + self.keyboard.take().unwrap().release(); + } + } + + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} +} + +impl KeyboardHandler for SimpleWindow { + fn enter( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + surface: &wl_surface::WlSurface, + _: u32, + _: &[u32], + keysyms: &[Keysym], + ) { + if self.window.wl_surface() == surface { + println!("Keyboard focus on window with pressed syms: {keysyms:?}"); + self.keyboard_focus = true; + } + } + + fn leave( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + surface: &wl_surface::WlSurface, + _: u32, + ) { + if self.window.wl_surface() == surface { + println!("Release keyboard focus on window"); + self.keyboard_focus = false; + } + } + + fn press_key( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + _: u32, + event: KeyEvent, + ) { + match event.utf8.as_deref() { + // Paste primary. + Some("P") => match self.clipboard.load_primary() { + Ok(contents) => println!("Paste from primary clipboard: {contents}"), + Err(err) => eprintln!("Error loading from primary clipboard: {err}"), + }, + // Paste clipboard. + Some("p") => match self.clipboard.load() { + Ok(contents) => println!("Paste from clipboard: {contents}"), + Err(err) => eprintln!("Error loading from clipboard: {err}"), + }, + // Copy primary. + Some("C") => { + let to_store = "Copy primary"; + self.clipboard.store_primary(to_store); + println!("Copied string into primary clipboard: {}", to_store); + }, + // Copy clipboard. + Some("c") => { + let to_store = "Copy"; + self.clipboard.store(to_store); + println!("Copied string into clipboard: {}", to_store); + }, + _ => (), + } + } + + fn release_key( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + _: u32, + _event: KeyEvent, + ) { + } + + fn update_modifiers( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + _serial: u32, + _modifiers: Modifiers, + ) { + } +} + +impl ShmHandler for SimpleWindow { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm + } +} + +impl SimpleWindow { + pub fn draw(&mut self, _conn: &Connection, qh: &QueueHandle) { + let width = self.width; + let height = self.height; + let stride = self.width as i32 * 4; + + let buffer = self.buffer.get_or_insert_with(|| { + self.pool + .create_buffer(width as i32, height as i32, stride, wl_shm::Format::Argb8888) + .expect("create buffer") + .0 + }); + + let canvas = match self.pool.canvas(buffer) { + Some(canvas) => canvas, + None => { + // This should be rare, but if the compositor has not released the previous + // buffer, we need double-buffering. + let (second_buffer, canvas) = self + .pool + .create_buffer( + self.width as i32, + self.height as i32, + stride, + wl_shm::Format::Argb8888, + ) + .expect("create buffer"); + *buffer = second_buffer; + canvas + }, + }; + + // Draw to the window: + canvas.chunks_exact_mut(4).enumerate().for_each(|(_, chunk)| { + // ARGB color. + let color = 0xFF181818u32; + + let array: &mut [u8; 4] = chunk.try_into().unwrap(); + *array = color.to_le_bytes(); + }); + + // Damage the entire window + self.window.wl_surface().damage_buffer(0, 0, self.width as i32, self.height as i32); + + // Request our next frame + self.window.wl_surface().frame(qh, self.window.wl_surface().clone()); + + // Attach and commit to present. + buffer.attach_to(self.window.wl_surface()).expect("buffer attach"); + self.window.commit(); + } +} + +delegate_compositor!(SimpleWindow); +delegate_output!(SimpleWindow); +delegate_shm!(SimpleWindow); + +delegate_seat!(SimpleWindow); +delegate_keyboard!(SimpleWindow); + +delegate_xdg_shell!(SimpleWindow); +delegate_xdg_window!(SimpleWindow); + +delegate_registry!(SimpleWindow); + +impl ProvidesRegistryState for SimpleWindow { + registry_handlers![OutputState, SeatState,]; + + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } } diff --git a/rustfmt.toml b/rustfmt.toml index 9e91dd0..8057fee 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,16 @@ -use_small_heuristics = "Max" +format_code_in_doc_comments = true +match_block_trailing_comma = true +condense_wildcard_suffixes = true use_field_init_shorthand = true +normalize_doc_attributes = true +overflow_delimited_expr = true +imports_granularity = "Module" +use_small_heuristics = "Max" +normalize_comments = true +reorder_impl_items = true +use_try_shorthand = true newline_style = "Unix" -edition = "2018" +format_strings = true +wrap_comments = true +comment_width = 80 +edition = "2021" diff --git a/src/env.rs b/src/env.rs deleted file mode 100644 index b7dc89c..0000000 --- a/src/env.rs +++ /dev/null @@ -1,79 +0,0 @@ -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}; -use sctk::MissingGlobal; - -/// 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<(), MissingGlobal> { - 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<(), MissingGlobal> - where - F: FnMut(WlSeat, DndEvent, DispatchData) + 'static, - { - self.data_device_manager.set_callback(callback) - } - - fn with_device( - &self, - seat: &WlSeat, - f: F, - ) -> Result<(), MissingGlobal> { - 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 d0fd09d..1884eb8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,19 @@ //! Smithay Clipboard //! -//! Provides access to the Wayland clipboard for gui applications. The user should have surface -//! around. +//! Provides access to the Wayland clipboard for gui applications. The user +//! should have surface around. #![deny(clippy::all, clippy::if_not_else, clippy::enum_glob_use)] use std::ffi::c_void; use std::io::Result; -use std::sync::mpsc::{self, Receiver, Sender}; +use std::sync::mpsc::{self, Receiver}; -use sctk::reexports::client::Display; +use sctk::reexports::calloop::channel::{self, Sender}; +use sctk::reexports::client::backend::Backend; +use sctk::reexports::client::Connection; -mod env; mod mime; +mod state; mod worker; /// Access to a Wayland clipboard. @@ -22,24 +24,24 @@ pub struct Clipboard { } impl Clipboard { - /// Creates new clipboard which will be running on its own thread with its own event queue to - /// handle clipboard requests. + /// Creates new clipboard which will be running on its own thread with its + /// own event queue to handle clipboard requests. /// /// # Safety /// /// `display` must be a valid `*mut wl_display` pointer, and it must remain /// valid for as long as `Clipboard` object is alive. pub unsafe fn new(display: *mut c_void) -> Self { - let display = Display::from_external_display(display as *mut _); + let backend = unsafe { Backend::from_foreign_display(display.cast()) }; + let connection = Connection::from_backend(backend); // Create channel to send data to clipboard thread. - let (request_sender, clipboard_request_receiver) = mpsc::channel(); + let (request_sender, rx_chan) = channel::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); + let clipboard_thread = worker::spawn(name, connection, rx_chan, clipboard_reply_sender); Self { request_receiver, request_sender, clipboard_thread } } diff --git a/src/mime.rs b/src/mime.rs index f786ab6..f3fc9a7 100644 --- a/src/mime.rs +++ b/src/mime.rs @@ -1,5 +1,5 @@ /// List of allowed mimes. -static ALLOWED_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"]; +pub static ALLOWED_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"]; /// Mime type supported by clipboard. #[derive(Clone, Copy, Eq, PartialEq, Debug)] @@ -18,8 +18,8 @@ pub enum MimeType { 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`. + /// `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] { diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..adf5fcb --- /dev/null +++ b/src/state.rs @@ -0,0 +1,588 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::io::{Error, ErrorKind, Read, Result, Write}; +use std::mem; +use std::os::unix::io::{AsRawFd, RawFd}; +use std::rc::Rc; +use std::sync::mpsc::Sender; + +use sctk::data_device_manager::data_device::{DataDevice, DataDeviceHandler}; +use sctk::data_device_manager::data_offer::{DataOfferError, DataOfferHandler, DragOffer}; +use sctk::data_device_manager::data_source::{CopyPasteSource, DataSourceHandler}; +use sctk::data_device_manager::{DataDeviceManagerState, WritePipe}; +use sctk::primary_selection::device::{PrimarySelectionDevice, PrimarySelectionDeviceHandler}; +use sctk::primary_selection::selection::{PrimarySelectionSource, PrimarySelectionSourceHandler}; +use sctk::primary_selection::PrimarySelectionManagerState; +use sctk::registry::{ProvidesRegistryState, RegistryState}; +use sctk::seat::pointer::{PointerData, PointerEvent, PointerEventKind, PointerHandler}; +use sctk::seat::{Capability, SeatHandler, SeatState}; +use sctk::{ + delegate_data_device, delegate_pointer, delegate_primary_selection, delegate_registry, + delegate_seat, registry_handlers, +}; + +use sctk::reexports::calloop::{LoopHandle, PostAction}; +use sctk::reexports::client::globals::GlobalList; +use sctk::reexports::client::protocol::wl_data_device::WlDataDevice; +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; +use sctk::reexports::client::protocol::wl_data_source::WlDataSource; +use sctk::reexports::client::protocol::wl_keyboard::WlKeyboard; +use sctk::reexports::client::protocol::wl_pointer::WlPointer; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::{Connection, Dispatch, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::primary_selection::zv1::client::{ + zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, + zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, +}; +use wayland_backend::client::ObjectId; + +use crate::mime::{normalize_to_lf, MimeType, ALLOWED_MIME_TYPES}; + +pub struct State { + pub primary_selection_manager_state: Option, + pub data_device_manager_state: Option, + pub reply_tx: Sender>, + pub exit: bool, + + registry_state: RegistryState, + seat_state: SeatState, + + seats: HashMap, + /// The latest seat which got an event. + latest_seat: Option, + + loop_handle: LoopHandle<'static, Self>, + queue_handle: QueueHandle, + + primary_sources: Vec, + primary_selection_content: Rc<[u8]>, + + data_sources: Vec, + data_selection_content: Rc<[u8]>, +} + +impl State { + #[must_use] + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + loop_handle: LoopHandle<'static, Self>, + reply_tx: Sender>, + ) -> Option { + let mut seats = HashMap::new(); + + let data_device_manager_state = DataDeviceManagerState::bind(globals, queue_handle).ok(); + let primary_selection_manager_state = + PrimarySelectionManagerState::bind(globals, queue_handle).ok(); + + // When both globals are not available nothing could be done. + if data_device_manager_state.is_none() && primary_selection_manager_state.is_none() { + return None; + } + + let seat_state = SeatState::new(globals, queue_handle); + for seat in seat_state.seats() { + seats.insert(seat.id(), Default::default()); + } + + Some(Self { + registry_state: RegistryState::new(globals), + primary_selection_content: Rc::from([]), + data_selection_content: Rc::from([]), + queue_handle: queue_handle.clone(), + primary_selection_manager_state, + primary_sources: Vec::new(), + data_device_manager_state, + data_sources: Vec::new(), + latest_seat: None, + loop_handle, + exit: false, + seat_state, + reply_tx, + seats, + }) + } + + /// Store selection for the given target. + /// + /// Selection source is only created when `Some(())` is returned. + pub fn store_selection(&mut self, ty: SelectionTarget, contents: String) -> Option<()> { + let latest = self.latest_seat.as_ref()?; + let seat = self.seats.get_mut(latest)?; + + if !seat.has_focus { + return None; + } + + let contents = Rc::from(contents.into_bytes()); + + match ty { + SelectionTarget::Clipboard => { + let mgr = self.data_device_manager_state.as_ref()?; + self.data_selection_content = contents; + let source = + mgr.create_copy_paste_source(&self.queue_handle, ALLOWED_MIME_TYPES.iter()); + source.set_selection(seat.data_device.as_ref().unwrap(), seat.latest_serial); + self.data_sources.push(source); + }, + SelectionTarget::Primary => { + let mgr = self.primary_selection_manager_state.as_ref()?; + self.primary_selection_content = contents; + let source = + mgr.create_selection_source(&self.queue_handle, ALLOWED_MIME_TYPES.iter()); + source.set_selection(seat.primary_device.as_ref().unwrap(), seat.latest_serial); + self.primary_sources.push(source); + }, + } + + Some(()) + } + + /// Load selection for the given target. + pub fn load_selection(&mut self, ty: SelectionTarget) -> Result<()> { + let latest = self + .latest_seat + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::Other, "no events received on any seat"))?; + let seat = self + .seats + .get_mut(latest) + .ok_or_else(|| Error::new(ErrorKind::Other, "active seat lost"))?; + + if !seat.has_focus { + return Err(Error::new(ErrorKind::Other, "client doesn't have focus")); + } + + let (read_pipe, mime_type) = match ty { + SelectionTarget::Clipboard => { + let selection = seat + .data_device + .as_ref() + .and_then(|data| data.data().selection_offer()) + .ok_or_else(|| Error::new(ErrorKind::Other, "selection is empty"))?; + + let mime_type = + selection.with_mime_types(MimeType::find_allowed).ok_or_else(|| { + Error::new(ErrorKind::NotFound, "supported mime-type is not found") + })?; + + ( + selection.receive(mime_type.to_string()).map_err(|err| match err { + DataOfferError::InvalidReceive => { + Error::new(ErrorKind::Other, "offer is not ready yet") + }, + DataOfferError::Io(err) => err, + })?, + mime_type, + ) + }, + SelectionTarget::Primary => { + let selection = seat + .primary_device + .as_ref() + .and_then(|data| data.data().selection_offer()) + .ok_or_else(|| Error::new(ErrorKind::Other, "selection is empty"))?; + + let mime_type = + selection.with_mime_types(MimeType::find_allowed).ok_or_else(|| { + Error::new(ErrorKind::NotFound, "supported mime-type is not found") + })?; + + (selection.receive(mime_type.to_string())?, mime_type) + }, + }; + + // Mark FD as non-blocking so we won't block ourselves. + unsafe { + set_non_blocking(read_pipe.as_raw_fd())?; + } + + let mut reader_buffer = [0; 4096]; + let mut content = Vec::new(); + let _ = self.loop_handle.insert_source(read_pipe, move |_, file, state| { + let file = unsafe { file.get_mut() }; + loop { + match file.read(&mut reader_buffer) { + Ok(0) => { + let utf8 = String::from_utf8_lossy(&content); + let content = match utf8 { + Cow::Borrowed(_) => { + // Don't clone the read data. + let mut to_send = Vec::new(); + mem::swap(&mut content, &mut to_send); + String::from_utf8(to_send).unwrap() + }, + Cow::Owned(content) => content, + }; + + // Post-process the content according to mime type. + let content = match mime_type { + MimeType::TextPlainUtf8 => normalize_to_lf(content), + MimeType::Utf8String => content, + }; + + let _ = state.reply_tx.send(Ok(content)); + break PostAction::Remove; + }, + Ok(n) => content.extend_from_slice(&reader_buffer[..n]), + Err(err) if err.kind() == ErrorKind::WouldBlock => break PostAction::Continue, + Err(err) => { + let _ = state.reply_tx.send(Err(err)); + break PostAction::Remove; + }, + }; + } + }); + + Ok(()) + } + + fn send_request(&mut self, ty: SelectionTarget, write_pipe: WritePipe, mime: String) { + // We can only send strings, so don't do anything with the mime-type. + if MimeType::find_allowed(&[mime]).is_none() { + return; + } + + // Mark FD as non-blocking so we won't block ourselves. + unsafe { + if set_non_blocking(write_pipe.as_raw_fd()).is_err() { + return; + } + } + + // Don't access the content on the state directly, since it could change during + // the send. + let contents = match ty { + SelectionTarget::Clipboard => self.data_selection_content.clone(), + SelectionTarget::Primary => self.primary_selection_content.clone(), + }; + + let mut written = 0; + let _ = self.loop_handle.insert_source(write_pipe, move |_, file, _| { + let file = unsafe { file.get_mut() }; + loop { + match file.write(&contents[written..]) { + Ok(n) if written + n == contents.len() => { + written += n; + break PostAction::Remove; + }, + Ok(n) => written += n, + Err(err) if err.kind() == ErrorKind::WouldBlock => break PostAction::Continue, + Err(_) => break PostAction::Remove, + } + } + }); + } +} + +impl SeatHandler for State { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn new_seat(&mut self, _: &Connection, _: &QueueHandle, seat: WlSeat) { + self.seats.insert(seat.id(), Default::default()); + } + + fn new_capability( + &mut self, + _: &Connection, + qh: &QueueHandle, + seat: WlSeat, + capability: Capability, + ) { + let seat_state = self.seats.get_mut(&seat.id()).unwrap(); + + match capability { + Capability::Keyboard => { + seat_state.keyboard = Some(seat.get_keyboard(qh, seat.id())); + + // Selection sources are tied to the keyboard, so add/remove decives + // when we gain/loss capability. + + if seat_state.data_device.is_none() && self.data_device_manager_state.is_some() { + seat_state.data_device = self + .data_device_manager_state + .as_ref() + .map(|mgr| mgr.get_data_device(qh, &seat)); + } + + if seat_state.primary_device.is_none() + && self.primary_selection_manager_state.is_some() + { + seat_state.primary_device = self + .primary_selection_manager_state + .as_ref() + .map(|mgr| mgr.get_selection_device(qh, &seat)); + } + }, + Capability::Pointer => { + seat_state.pointer = self.seat_state.get_pointer(qh, &seat).ok(); + }, + _ => (), + } + } + + fn remove_capability( + &mut self, + _: &Connection, + _: &QueueHandle, + seat: WlSeat, + capability: Capability, + ) { + let seat_state = self.seats.get_mut(&seat.id()).unwrap(); + match capability { + Capability::Keyboard => { + seat_state.data_device = None; + seat_state.primary_device = None; + + if let Some(keyboard) = seat_state.keyboard.take() { + if keyboard.version() >= 3 { + keyboard.release() + } + } + }, + Capability::Pointer => { + if let Some(pointer) = seat_state.pointer.take() { + if pointer.version() >= 3 { + pointer.release() + } + } + }, + _ => (), + } + } + + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, seat: WlSeat) { + self.seats.remove(&seat.id()); + } +} + +impl PointerHandler for State { + fn pointer_frame( + &mut self, + _: &Connection, + _: &QueueHandle, + pointer: &WlPointer, + events: &[PointerEvent], + ) { + let seat = pointer.data::().unwrap().seat(); + let seat_id = seat.id(); + let seat_state = match self.seats.get_mut(&seat_id) { + Some(seat_state) => seat_state, + None => return, + }; + + let mut updated_serial = false; + for event in events { + match event.kind { + PointerEventKind::Press { serial, .. } + | PointerEventKind::Release { serial, .. } => { + updated_serial = true; + seat_state.latest_serial = serial; + }, + _ => (), + } + } + + // Only update the seat we're using when the serial got updated. + if updated_serial { + self.latest_seat = Some(seat_id); + } + } +} + +impl DataDeviceHandler for State { + fn enter(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} + + fn leave(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} + + fn motion(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} + + fn drop_performed(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} + + // The selection is finished and ready to be used. + fn selection(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} +} + +impl DataSourceHandler for State { + fn send_request( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlDataSource, + mime: String, + write_pipe: WritePipe, + ) { + self.send_request(SelectionTarget::Clipboard, write_pipe, mime) + } + + fn cancelled(&mut self, _: &Connection, _: &QueueHandle, deleted: &WlDataSource) { + self.data_sources.retain(|source| source.inner() != deleted) + } + + fn accept_mime( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlDataSource, + _: Option, + ) { + } + + fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource) {} + + fn action(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource, _: DndAction) {} + + fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource) {} +} + +impl DataOfferHandler for State { + fn source_actions( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &mut DragOffer, + _: DndAction, + ) { + } + + fn selected_action( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &mut DragOffer, + _: DndAction, + ) { + } +} + +impl ProvidesRegistryState for State { + registry_handlers![SeatState]; + + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } +} + +impl PrimarySelectionDeviceHandler for State { + fn selection( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &ZwpPrimarySelectionDeviceV1, + ) { + } +} + +impl PrimarySelectionSourceHandler for State { + fn send_request( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &ZwpPrimarySelectionSourceV1, + mime: String, + write_pipe: WritePipe, + ) { + self.send_request(SelectionTarget::Primary, write_pipe, mime); + } + + fn cancelled( + &mut self, + _: &Connection, + _: &QueueHandle, + deleted: &ZwpPrimarySelectionSourceV1, + ) { + self.primary_sources.retain(|source| source.inner() != deleted) + } +} + +impl Dispatch for State { + fn event( + state: &mut State, + _: &WlKeyboard, + event: ::Event, + data: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + use sctk::reexports::client::protocol::wl_keyboard::Event as WlKeyboardEvent; + let seat_state = match state.seats.get_mut(data) { + Some(seat_state) => seat_state, + None => return, + }; + match event { + WlKeyboardEvent::Key { serial, .. } | WlKeyboardEvent::Modifiers { serial, .. } => { + seat_state.latest_serial = serial; + state.latest_seat = Some(data.clone()); + }, + // NOTE both selections rely on keyboard focus. + WlKeyboardEvent::Enter { serial, .. } => { + seat_state.latest_serial = serial; + seat_state.has_focus = true; + }, + WlKeyboardEvent::Leave { .. } => { + seat_state.latest_serial = 0; + seat_state.has_focus = false; + }, + _ => (), + } + } +} + +delegate_seat!(State); +delegate_pointer!(State); +delegate_data_device!(State); +delegate_primary_selection!(State); +delegate_registry!(State); + +#[derive(Debug, Clone, Copy)] +pub enum SelectionTarget { + /// The target is clipboard selection. + Clipboard, + /// The target is primary selection. + Primary, +} + +#[derive(Debug, Default)] +struct ClipboardSeatState { + keyboard: Option, + pointer: Option, + data_device: Option, + primary_device: Option, + has_focus: bool, + + /// The latest serial used to set the selection content. + latest_serial: u32, +} + +impl Drop for ClipboardSeatState { + fn drop(&mut self) { + if let Some(keyboard) = self.keyboard.take() { + if keyboard.version() >= 3 { + keyboard.release(); + } + } + + if let Some(pointer) = self.pointer.take() { + if pointer.version() >= 3 { + pointer.release(); + } + } + } +} + +unsafe fn set_non_blocking(raw_fd: RawFd) -> std::io::Result<()> { + let flags = libc::fcntl(raw_fd, libc::F_GETFL); + + if flags < 0 { + return Err(std::io::Error::last_os_error()); + } + + let result = libc::fcntl(raw_fd, libc::F_SETFL, flags | libc::O_NONBLOCK); + if result < 0 { + return Err(std::io::Error::last_os_error()); + } + + Ok(()) +} diff --git a/src/worker.rs b/src/worker.rs new file mode 100644 index 0000000..4b0349f --- /dev/null +++ b/src/worker.rs @@ -0,0 +1,104 @@ +use std::io::{Error, ErrorKind, Result}; +use std::sync::mpsc::Sender; + +use sctk::reexports::calloop::channel::Channel; +use sctk::reexports::calloop::{channel, EventLoop}; +use sctk::reexports::calloop_wayland_source::WaylandSource; +use sctk::reexports::client::globals::registry_queue_init; +use sctk::reexports::client::Connection; + +use crate::state::{SelectionTarget, State}; + +/// Spawn a clipboard worker, which dispatches its own `EventQueue` and handles +/// clipboard requests. +pub fn spawn( + name: String, + display: Connection, + rx_chan: Channel, + worker_replier: Sender>, +) -> Option> { + std::thread::Builder::new() + .name(name) + .spawn(move || { + worker_impl(display, rx_chan, 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( + connection: Connection, + rx_chan: Channel, + reply_tx: Sender>, +) { + let (globals, event_queue) = match registry_queue_init(&connection) { + Ok(data) => data, + Err(_) => return, + }; + + let mut event_loop = EventLoop::::try_new().unwrap(); + let loop_handle = event_loop.handle(); + + let mut state = match State::new(&globals, &event_queue.handle(), loop_handle.clone(), reply_tx) + { + Some(state) => state, + None => return, + }; + + loop_handle + .insert_source(rx_chan, |event, _, state| { + if let channel::Event::Msg(event) = event { + match event { + Command::StorePrimary(contents) => { + state.store_selection(SelectionTarget::Primary, contents); + }, + Command::Store(contents) => { + state.store_selection(SelectionTarget::Clipboard, contents); + }, + Command::Load if state.data_device_manager_state.is_some() => { + if let Err(err) = state.load_selection(SelectionTarget::Clipboard) { + let _ = state.reply_tx.send(Err(err)); + } + }, + Command::LoadPrimary if state.data_device_manager_state.is_some() => { + if let Err(err) = state.load_selection(SelectionTarget::Primary) { + let _ = state.reply_tx.send(Err(err)); + } + }, + Command::Load | Command::LoadPrimary => { + let _ = state.reply_tx.send(Err(Error::new( + ErrorKind::Other, + "requested selection is not supported", + ))); + }, + Command::Exit => state.exit = true, + } + } + }) + .unwrap(); + + WaylandSource::new(connection, event_queue).insert(loop_handle).unwrap(); + + loop { + event_loop.dispatch(None, &mut state).unwrap(); + + if state.exit { + break; + } + } +} diff --git a/src/worker/dispatch_data.rs b/src/worker/dispatch_data.rs deleted file mode 100644 index b1d8fce..0000000 --- a/src/worker/dispatch_data.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::collections::VecDeque; -use std::slice::IterMut; - -use sctk::reexports::client::protocol::wl_seat::WlSeat; - -use super::seat_data::SeatData; - -/// Data to track latest seat and serial for clipboard requests. -pub struct ClipboardDispatchData { - /// Seats that our application encountered. The first seat is the latest one we've encountered. - observed_seats: VecDeque<(WlSeat, u32)>, - - /// All the seats that were advertised. - seats: Vec, -} - -impl ClipboardDispatchData { - /// Builds new `ClipboardDispatchData` with all fields equal to `None`. - pub fn new(seats: Vec) -> Self { - Self { observed_seats: Default::default(), seats } - } - - /// Returns the requested seat's data or adds a new one. - pub fn get_seat_data_or_add(&mut self, seat: WlSeat) -> &mut SeatData { - let pos = self.seats.iter().position(|st| st.seat == seat); - let index = pos.unwrap_or_else(|| { - self.seats.push(SeatData::new(seat, None, None)); - self.seats.len() - 1 - }); - - &mut self.seats[index] - } - - pub fn seats(&mut self) -> IterMut<'_, SeatData> { - self.seats.iter_mut() - } - - /// Set the last observed seat. - pub fn set_last_observed_seat(&mut self, seat: WlSeat, serial: u32) { - // Assure each seat exists only once. - self.remove_observed_seat(&seat); - - // Add the seat to front, making it the latest observed one. - self.observed_seats.push_front((seat, serial)); - } - - /// Remove the given seat from the observed seats. - pub fn remove_observed_seat(&mut self, seat: &WlSeat) { - if let Some(pos) = self.observed_seats.iter().position(|st| &st.0 == seat) { - self.observed_seats.remove(pos); - } - } - - /// Return the last observed seat and the serial. - pub fn last_observed_seat(&self) -> Option<&(WlSeat, u32)> { - self.observed_seats.front() - } -} diff --git a/src/worker/handlers.rs b/src/worker/handlers.rs deleted file mode 100644 index bbdc308..0000000 --- a/src/worker/handlers.rs +++ /dev/null @@ -1,138 +0,0 @@ -#![macro_use] - -use std::fs::File; -use std::io::Result; -use std::os::unix::io::FromRawFd; -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 (); - } - }; - - // If we fail here, it means that we likely won't be able - // to read clipboard anyway, so return and reply to prevent block/crash. - if $queue.sync_roundtrip(&mut (), |_, _, _| unreachable!()).is_err() { - handlers::reply_error(&$tx, "failed to access clipboard."); - return; - }; - - let mut contents = String::new(); - let result = reader.read_to_string(&mut contents).map(|_| { - if mime_type == MimeType::Utf8String { - mime::normalize_to_lf(contents) - } else { - contents - } - }); - - let _ = $tx.send(result); - }); - - // 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(), MimeType::Utf8String.to_string()], - move |event, _| { - if let $event_ty::Send { mut pipe, .. } = event { - // If we fail to write here, it means that other side closed the pipe, thus - // we can't do anything about it. - let _ = write!(pipe, "{}", $contents); - } - }, - ); - - 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) { - let _ = tx.send(Err(std::io::Error::new(std::io::ErrorKind::Other, description))); -} - -/// 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_observed_seat(seat, serial); - } - PointerEvent::Button { serial, .. } => { - dispatch_data.set_last_observed_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_observed_seat(seat, serial); - } - KeyboardEvent::Key { serial, .. } => { - dispatch_data.set_last_observed_seat(seat, serial); - } - KeyboardEvent::Leave { .. } => { - dispatch_data.remove_observed_seat(&seat); - } - KeyboardEvent::Keymap { fd, .. } => { - // Prevent fd leaking. - let _ = unsafe { File::from_raw_fd(fd) }; - } - _ => {} - } -} diff --git a/src/worker/mod.rs b/src/worker/mod.rs deleted file mode 100644 index 3590a61..0000000 --- a/src/worker/mod.rs +++ /dev/null @@ -1,321 +0,0 @@ -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 = match Environment::new(&display_proxy, &mut queue, SmithayClipboard::new()) { - Ok(env) => env, - // We shouldn't crash the application if we've failed to create environment. - Err(_) => return, - }; - - // 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, mut dispatch_data| { - let dispatch_data = match dispatch_data.get::() { - Some(dispatch_data) => dispatch_data, - None => return, - }; - - let seat_resources = dispatch_data.get_seat_data_or_add(seat.detach()); - - 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(seats); - - // 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(); - - if queue.sync_roundtrip(&mut dispatch_data, |_, _, _| unimplemented!()).is_err() - && (request == Command::LoadPrimary || request == Command::Load) - { - handlers::reply_error(&reply_tx, "primary clipboard is not available."); - break; - } - - // Get latest observed seat and serial. - let (seat, serial) = match dispatch_data.last_observed_seat() { - Some(data) => data, - None => { - handlers::reply_error(&reply_tx, "no focus on a seat."); - continue; - } - }; - 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 = match queue.dispatch_pending(&mut dispatch_data, |_, _, _| {}) { - Ok(pending_events) => pending_events, - Err(_) => break, - }; - - // 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_observed_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(); - } - } - - // While everything inside this block is safe, the logic is generally unsafe, since we must - // drop every proxy on the current `queue`, since dropping it in multithreaded context - // could result in use-after-free in libwayland-client. - // - // For more see https://gitlab.freedesktop.org/wayland/wayland/-/issues/13. - #[allow(unused_unsafe)] - unsafe { - for seat in dispatch_data.seats() { - if let Some(pointer) = seat.pointer.take() { - if pointer.as_ref().version() >= 3 { - pointer.release(); - } - } - if let Some(keyboard) = seat.keyboard.take() { - if keyboard.as_ref().version() >= 3 { - keyboard.release(); - } - } - } - std::mem::drop(listener); - - let _ = queue.sync_roundtrip(&mut dispatch_data, |_, _, _| unimplemented!()); - let _ = queue.display().flush(); - } -} diff --git a/src/worker/seat_data.rs b/src/worker/seat_data.rs deleted file mode 100644 index c6374f7..0000000 --- a/src/worker/seat_data.rs +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 28ec2ed..0000000 --- a/src/worker/sleep_amount_tracker.rs +++ /dev/null @@ -1,55 +0,0 @@ -/// 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 - } -}