From a97dfae8f61762d7e17781a1c912d76c1a0a59d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 6 Mar 2021 04:34:02 +0100 Subject: [PATCH] Implement write support for `clipboard_x11` --- examples/{basic.rs => read.rs} | 0 examples/write.rs | 30 ++ src/lib.rs | 8 +- src/platform/android.rs | 4 + src/platform/ios.rs | 4 + src/platform/linux.rs | 10 +- src/platform/macos.rs | 4 + src/platform/windows.rs | 6 +- x11/src/clipboard.rs | 585 ++++++++++++++++++++++++--------- x11/src/error.rs | 10 + x11/src/lib.rs | 20 +- 11 files changed, 496 insertions(+), 185 deletions(-) rename examples/{basic.rs => read.rs} (100%) create mode 100644 examples/write.rs diff --git a/examples/basic.rs b/examples/read.rs similarity index 100% rename from examples/basic.rs rename to examples/read.rs diff --git a/examples/write.rs b/examples/write.rs new file mode 100644 index 0000000..f16838a --- /dev/null +++ b/examples/write.rs @@ -0,0 +1,30 @@ +use window_clipboard::Clipboard; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +fn main() { + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + let mut clipboard = Clipboard::new(&window).expect("Create clipboard"); + + clipboard + .write(String::from("Hello, world!")) + .expect("Write to clipboard"); + + event_loop.run(move |event, _, control_flow| match event { + Event::MainEventsCleared => {} + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + } if window_id == window.id() => *control_flow = ControlFlow::Exit, + _ => *control_flow = ControlFlow::Wait, + }); +} diff --git a/src/lib.rs b/src/lib.rs index 22d49b2..842161d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,12 +43,16 @@ impl Clipboard { } pub fn read(&self) -> Result> { - // TODO: Think about use of `RefCell` - // Maybe we should make `read` mutable (?) self.raw.read() } + + pub fn write(&mut self, contents: String) -> Result<(), Box> { + self.raw.write(contents) + } } pub trait ClipboardProvider { fn read(&self) -> Result>; + + fn write(&mut self, contents: String) -> Result<(), Box>; } diff --git a/src/platform/android.rs b/src/platform/android.rs index 35326da..ec249a4 100644 --- a/src/platform/android.rs +++ b/src/platform/android.rs @@ -35,4 +35,8 @@ impl ClipboardProvider for Clipboard { fn read(&self) -> Result> { Err(Box::new(AndroidClipboardError::Unimplemented)) } + + fn write(&mut self, contents: String) -> Result<(), Box> { + Err(Box::new(AndroidClipboardError::Unimplemented)) + } } diff --git a/src/platform/ios.rs b/src/platform/ios.rs index 1727634..ddf8cbd 100644 --- a/src/platform/ios.rs +++ b/src/platform/ios.rs @@ -35,4 +35,8 @@ impl ClipboardProvider for Clipboard { fn read(&self) -> Result> { Err(Box::new(iOSClipboardError::Unimplemented)) } + + fn write(&mut self, contents: String) -> Result<(), Box> { + Err(Box::new(AndroidClipboardError::Unimplemented)) + } } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 5cb7042..e0b3667 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -27,10 +27,18 @@ impl ClipboardProvider for wayland::Clipboard { fn read(&self) -> Result> { self.read() } + + fn write(&mut self, contents: String) -> Result<(), Box> { + self.write(contents) + } } impl ClipboardProvider for x11::Clipboard { fn read(&self) -> Result> { - self.read() + self.read().map_err(Box::from) + } + + fn write(&mut self, contents: String) -> Result<(), Box> { + self.write(contents).map_err(Box::from) } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index d178ddb..2f269ee 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -13,4 +13,8 @@ impl ClipboardProvider for clipboard_macos::Clipboard { fn read(&self) -> Result> { self.read() } + + fn write(&mut self, contents: String) -> Result<(), Box> { + self.write(contents) + } } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 5802d48..82d472f 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1,6 +1,6 @@ use crate::ClipboardProvider; -use clipboard_win::get_clipboard_string; +use clipboard_win::{get_clipboard_string, set_clipboard_string}; use raw_window_handle::HasRawWindowHandle; use std::error::Error; @@ -17,4 +17,8 @@ impl ClipboardProvider for Clipboard { fn read(&self) -> Result> { Ok(get_clipboard_string()?) } + + fn write(&mut self, contents: String) -> Result<(), Box> { + Ok(set_clipboard_string(&contents)?) + } } diff --git a/x11/src/clipboard.rs b/x11/src/clipboard.rs index 0c7db51..0cea0ae 100644 --- a/x11/src/clipboard.rs +++ b/x11/src/clipboard.rs @@ -1,15 +1,267 @@ use crate::error::Error; -use std::thread; -use std::time::{Duration, Instant}; use x11rb::connection::Connection as _; use x11rb::errors::ConnectError; use x11rb::protocol::xproto::{self, Atom, AtomEnum, Window}; use x11rb::protocol::Event; use x11rb::rust_connection::RustConnection as Connection; +use std::collections::HashMap; +use std::sync::mpsc; +use std::sync::{Arc, RwLock}; +use std::thread; +use std::time::{Duration, Instant}; + const POLL_DURATION: std::time::Duration = Duration::from_micros(50); +/// X11 Clipboard +pub struct Clipboard { + reader: Context, + writer: Arc, + selections: Arc)>>>, + worker: mpsc::Sender, +} + +impl Clipboard { + /// Create Clipboard. + pub fn new() -> Result { + let reader = Context::new(None)?; + let writer = Arc::new(Context::new(None)?); + let selections = Arc::new(RwLock::new(HashMap::new())); + let (sender, receiver) = mpsc::channel(); + + let worker = Worker { + context: Arc::clone(&writer), + selections: Arc::clone(&selections), + receiver, + }; + + thread::spawn(move || worker.run()); + + Ok(Clipboard { + reader, + writer, + selections, + worker: sender, + }) + } + + pub fn read(&self) -> Result { + Ok(String::from_utf8(self.load( + self.reader.atoms.clipboard, + self.reader.atoms.utf8_string, + self.reader.atoms.property, + std::time::Duration::from_secs(3), + )?) + .map_err(Error::InvalidUtf8)?) + } + + pub fn write(&mut self, contents: String) -> Result<(), Error> { + let selection = self.writer.atoms.clipboard; + let target = self.writer.atoms.utf8_string; + + let _ = self.worker.send(selection)?; + + self.selections + .write() + .map_err(|_| Error::SelectionLocked)? + .insert(selection, (target, contents.into())); + + let _ = xproto::set_selection_owner( + &self.writer.connection, + self.writer.window, + selection, + x11rb::CURRENT_TIME, + )?; + + let _ = self.writer.connection.flush()?; + + let reply = + xproto::get_selection_owner(&self.writer.connection, selection) + .map_err(Into::into) + .and_then(|cookie| cookie.reply())?; + + if reply.owner == self.writer.window { + Ok(()) + } else { + Err(Error::InvalidOwner) + } + } + + /// load value. + fn load( + &self, + selection: Atom, + target: Atom, + property: Atom, + timeout: impl Into>, + ) -> Result, Error> { + let mut buff = Vec::new(); + let timeout = timeout.into(); + + let _ = xproto::convert_selection( + &self.reader.connection, + self.reader.window, + selection, + target, + property, + x11rb::CURRENT_TIME, // FIXME ^ + // Clients should not use CurrentTime for the time argument of a ConvertSelection request. + // Instead, they should use the timestamp of the event that caused the request to be made. + )?; + let _ = self.reader.connection.flush()?; + + self.process_event(&mut buff, selection, target, property, timeout)?; + + let _ = xproto::delete_property( + &self.reader.connection, + self.reader.window, + property, + )?; + let _ = self.reader.connection.flush()?; + + Ok(buff) + } + + fn process_event( + &self, + buff: &mut Vec, + selection: Atom, + target: Atom, + property: Atom, + timeout: T, + ) -> Result<(), Error> + where + T: Into>, + { + let mut is_incr = false; + let timeout = timeout.into(); + let start_time = if timeout.is_some() { + Some(Instant::now()) + } else { + None + }; + + loop { + if timeout + .into_iter() + .zip(start_time) + .next() + .map(|(timeout, time)| (Instant::now() - time) >= timeout) + .unwrap_or(false) + { + return Err(Error::Timeout); + } + + let event = match self.reader.connection.poll_for_event()? { + Some(event) => event, + None => { + thread::park_timeout(POLL_DURATION); + continue; + } + }; + + match event { + Event::SelectionNotify(event) => { + if event.selection != selection { + continue; + }; + + // Note that setting the property argument to None indicates that the + // conversion requested could not be made. + if event.property == AtomEnum::NONE.into() { + break; + } + + let reply = xproto::get_property( + &self.reader.connection, + false, + self.reader.window, + event.property, + Atom::from(AtomEnum::ANY), + buff.len() as u32, + ::std::u32::MAX, // FIXME reasonable buffer size + ) + .map_err(Into::into) + .and_then(|cookie| cookie.reply())?; + + if reply.type_ == self.reader.atoms.incr { + if let Some(&size) = reply.value.get(0) { + buff.reserve(size as usize); + } + + let _ = xproto::delete_property( + &self.reader.connection, + self.reader.window, + property, + ); + + let _ = self.reader.connection.flush(); + is_incr = true; + + continue; + } else if reply.type_ != target { + return Err(Error::UnexpectedType(reply.type_)); + } + + buff.extend_from_slice(&reply.value); + break; + } + Event::PropertyNotify(event) if is_incr => { + if event.state != xproto::Property::NEW_VALUE { + continue; + }; + + let length = xproto::get_property( + &self.reader.connection, + false, + self.reader.window, + property, + Atom::from(AtomEnum::ANY), + 0, + 0, + ) + .map_err(Into::into) + .and_then(|cookie| cookie.reply())? + .bytes_after; + + let reply = xproto::get_property( + &self.reader.connection, + true, + self.reader.window, + property, + Atom::from(AtomEnum::ANY), + 0, + length, + ) + .map_err(Into::into) + .and_then(|cookie| cookie.reply())?; + + if reply.type_ != target { + continue; + }; + + if reply.value_len != 0 { + buff.extend_from_slice(&reply.value); + } else { + break; + } + } + _ => {} + } + } + + Ok(()) + } +} + +pub struct Context { + pub connection: Connection, + pub screen: usize, + pub window: Window, + pub atoms: Atoms, +} + #[derive(Clone, Debug)] pub struct Atoms { pub primary: Atom, @@ -21,18 +273,6 @@ pub struct Atoms { pub incr: Atom, } -/// X11 Clipboard -pub struct Clipboard { - pub getter: Context, -} - -pub struct Context { - pub connection: Connection, - pub screen: usize, - pub window: Window, - pub atoms: Atoms, -} - #[inline] fn get_atom(connection: &Connection, name: &str) -> Result { x11rb::protocol::xproto::intern_atom(connection, false, name.as_bytes()) @@ -95,180 +335,199 @@ impl Context { } } -impl Clipboard { - /// Create Clipboard. - pub fn new() -> Result { - let getter = Context::new(None)?; +pub struct Worker { + context: Arc, + selections: Arc)>>>, + receiver: mpsc::Receiver, +} - Ok(Clipboard { getter }) - } +struct IncrState { + selection: Atom, + requestor: Atom, + property: Atom, + pos: usize, +} - fn process_event( - &self, - buff: &mut Vec, - selection: Atom, - target: Atom, - property: Atom, - timeout: T, - ) -> Result<(), Error> - where - T: Into>, - { - let mut is_incr = false; - let timeout = timeout.into(); - let start_time = if timeout.is_some() { - Some(Instant::now()) - } else { - None - }; +impl Worker { + pub const INCR_CHUNK_SIZE: usize = 4000; - loop { - if timeout - .into_iter() - .zip(start_time) - .next() - .map(|(timeout, time)| (Instant::now() - time) >= timeout) - .unwrap_or(false) - { - return Err(Error::Timeout); - } + pub fn run(self) { + use x11rb::connection::RequestConnection; - let event = match self.getter.connection.poll_for_event()? { - Some(event) => event, - None => { - thread::park_timeout(POLL_DURATION); - continue; + let mut incr_map = HashMap::new(); + let mut state_map = HashMap::new(); + + let max_length = self.context.connection.maximum_request_bytes() * 4; + + while let Ok(event) = self.context.connection.wait_for_event() { + while let Ok(selection) = self.receiver.try_recv() { + if let Some(property) = incr_map.remove(&selection) { + state_map.remove(&property); } - }; + } match event { - Event::SelectionNotify(event) => { - if event.selection != selection { - continue; + Event::SelectionRequest(event) => { + let selections = match self.selections.read().ok() { + Some(selections) => selections, + None => continue, }; - // Note that setting the property argument to None indicates that the - // conversion requested could not be made. - if event.property == AtomEnum::NONE.into() { - break; + let &(target, ref value) = + match selections.get(&event.selection) { + Some(key_value) => key_value, + None => continue, + }; + + if event.target == self.context.atoms.targets { + let data: Vec = { + let atom_target_bytes = + self.context.atoms.targets.to_le_bytes(); + + let target_bytes = target.to_le_bytes(); + + atom_target_bytes + .iter() + .chain(target_bytes.iter()) + .copied() + .collect() + }; + + xproto::change_property( + &self.context.connection, + xproto::PropMode::REPLACE, + event.requestor, + event.property, + xproto::AtomEnum::ATOM, + 32, + 2, + &data, + ) + .expect("Change property"); + } else if value.len() < max_length - 24 { + let _ = xproto::change_property( + &self.context.connection, + xproto::PropMode::REPLACE, + event.requestor, + event.property, + target, + 8, + value.len() as u32, + value, + ) + .expect("Change property"); + } else { + let _ = xproto::change_window_attributes( + &self.context.connection, + event.requestor, + &xproto::ChangeWindowAttributesAux::new() + .event_mask(xproto::EventMask::PROPERTY_CHANGE), + ) + .expect("Change window attributes"); + + xproto::change_property( + &self.context.connection, + xproto::PropMode::REPLACE, + event.requestor, + event.property, + self.context.atoms.incr, + 32, + 0, + &[], + ) + .expect("Change property"); + + incr_map.insert(event.selection, event.property); + state_map.insert( + event.property, + IncrState { + selection: event.selection, + requestor: event.requestor, + property: event.property, + pos: 0, + }, + ); } - let reply = xproto::get_property( - &self.getter.connection, + let _ = xproto::send_event( + &self.context.connection, false, - self.getter.window, - event.property, - Atom::from(AtomEnum::ANY), - buff.len() as u32, - ::std::u32::MAX, // FIXME reasonable buffer size + event.requestor, + 0u32, + xproto::SelectionNotifyEvent { + response_type: 31, + sequence: event.sequence, + time: event.time, + requestor: event.requestor, + selection: event.selection, + target: event.target, + property: event.property, + }, ) - .map_err(Into::into) - .and_then(|cookie| cookie.reply())?; + .expect("Send event"); - if reply.type_ == self.getter.atoms.incr { - if let Some(&size) = reply.value.get(0) { - buff.reserve(size as usize); - } + let _ = self.context.connection.flush(); + } + Event::PropertyNotify(event) => { + if event.state != xproto::Property::DELETE { + continue; + } - let _ = xproto::delete_property( - &self.getter.connection, - self.getter.window, - property, + let is_end = { + let state = match state_map.get_mut(&event.atom) { + Some(state) => state, + None => continue, + }; + + let selections = match self.selections.read().ok() { + Some(selections) => selections, + None => continue, + }; + + let &(target, ref value) = + match selections.get(&state.selection) { + Some(key_value) => key_value, + None => continue, + }; + + let len = std::cmp::min( + Self::INCR_CHUNK_SIZE, + value.len() - state.pos, ); - let _ = self.getter.connection.flush(); - is_incr = true; + let _ = xproto::change_property( + &self.context.connection, + xproto::PropMode::REPLACE, + state.requestor, + state.property, + target, + 8, + len as u32, + &value[state.pos..][..len], + ) + .expect("Change property"); - continue; - } else if reply.type_ != target { - return Err(Error::UnexpectedType(reply.type_)); - } - - buff.extend_from_slice(&reply.value); - break; - } - Event::PropertyNotify(event) if is_incr => { - if event.state != xproto::Property::NEW_VALUE { - continue; + state.pos += len; + len == 0 }; - let length = xproto::get_property( - &self.getter.connection, - false, - self.getter.window, - property, - Atom::from(AtomEnum::ANY), - 0, - 0, - ) - .map_err(Into::into) - .and_then(|cookie| cookie.reply())? - .bytes_after; + if is_end { + state_map.remove(&event.atom); + } - let reply = xproto::get_property( - &self.getter.connection, - true, - self.getter.window, - property, - Atom::from(AtomEnum::ANY), - 0, - length, - ) - .map_err(Into::into) - .and_then(|cookie| cookie.reply())?; + self.context.connection.flush().expect("Flush connection"); + } + Event::SelectionClear(event) => { + if let Some(property) = incr_map.remove(&event.selection) { + state_map.remove(&property); + } - if reply.type_ != target { - continue; - }; - - if reply.value_len != 0 { - buff.extend_from_slice(&reply.value); - } else { - break; + if let Ok(mut write_setmap) = self.selections.write() { + write_setmap.remove(&event.selection); } } - _ => {} + _ => (), } } - - Ok(()) - } - - /// load value. - pub fn load( - &self, - selection: Atom, - target: Atom, - property: Atom, - timeout: T, - ) -> Result, Error> - where - T: Into>, - { - let mut buff = Vec::new(); - let timeout = timeout.into(); - - let _ = xproto::convert_selection( - &self.getter.connection, - self.getter.window, - selection, - target, - property, - x11rb::CURRENT_TIME, // FIXME ^ - // Clients should not use CurrentTime for the time argument of a ConvertSelection request. - // Instead, they should use the timestamp of the event that caused the request to be made. - )?; - let _ = self.getter.connection.flush(); - - self.process_event(&mut buff, selection, target, property, timeout)?; - - let _ = xproto::delete_property( - &self.getter.connection, - self.getter.window, - property, - )?; - let _ = self.getter.connection.flush()?; - - Ok(buff) } } diff --git a/x11/src/error.rs b/x11/src/error.rs index d9a0160..cf04d2f 100644 --- a/x11/src/error.rs +++ b/x11/src/error.rs @@ -1,6 +1,8 @@ use x11rb::errors::{ConnectError, ConnectionError, ReplyError}; use x11rb::protocol::xproto::Atom; +use std::sync::mpsc; + #[must_use] #[derive(Debug, thiserror::Error)] pub enum Error { @@ -14,4 +16,12 @@ pub enum Error { Timeout, #[error("unexpected type: {0}")] UnexpectedType(Atom), + #[error("invalid utf8 string: {0}")] + InvalidUtf8(std::string::FromUtf8Error), + #[error("deadlock")] + SelectionLocked, + #[error("invalid selection owner")] + InvalidOwner, + #[error("worker communication error")] + SendError(#[from] mpsc::SendError), } diff --git a/x11/src/lib.rs b/x11/src/lib.rs index aa1e7e0..8906275 100644 --- a/x11/src/lib.rs +++ b/x11/src/lib.rs @@ -2,21 +2,5 @@ mod clipboard; mod error; -use std::error::Error; - -pub struct Clipboard(clipboard::Clipboard); - -impl Clipboard { - pub fn new() -> Result> { - Ok(Clipboard(clipboard::Clipboard::new()?)) - } - - pub fn read(&self) -> Result> { - Ok(String::from_utf8(self.0.load( - self.0.getter.atoms.clipboard, - self.0.getter.atoms.utf8_string, - self.0.getter.atoms.property, - std::time::Duration::from_secs(3), - )?)?) - } -} +pub use clipboard::Clipboard; +pub use error::Error;