From 5571e00073036563fe56188a1918ed3e6716e5fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 7 Mar 2021 03:25:51 +0100 Subject: [PATCH] Rename `Clipboard::new` to `Clipboard::connect` --- examples/read.rs | 2 +- examples/write.rs | 3 +- src/lib.rs | 4 +- src/platform/android.rs | 2 +- src/platform/ios.rs | 2 +- src/platform/linux.rs | 6 +- src/platform/macos.rs | 2 +- src/platform/windows.rs | 2 +- wayland/src/lib.rs | 2 +- x11/src/clipboard.rs | 533 --------------------------------------- x11/src/lib.rs | 536 +++++++++++++++++++++++++++++++++++++++- 11 files changed, 547 insertions(+), 547 deletions(-) delete mode 100644 x11/src/clipboard.rs diff --git a/examples/read.rs b/examples/read.rs index d0de874..6839347 100644 --- a/examples/read.rs +++ b/examples/read.rs @@ -13,7 +13,7 @@ fn main() { .build(&event_loop) .unwrap(); - let clipboard = Clipboard::new(&window).expect("Create clipboard"); + let clipboard = Clipboard::connect(&window).expect("Connect to clipboard"); event_loop.run(move |event, _, control_flow| match event { Event::MainEventsCleared => { diff --git a/examples/write.rs b/examples/write.rs index f16838a..b72fed6 100644 --- a/examples/write.rs +++ b/examples/write.rs @@ -13,7 +13,8 @@ fn main() { .build(&event_loop) .unwrap(); - let mut clipboard = Clipboard::new(&window).expect("Create clipboard"); + let mut clipboard = + Clipboard::connect(&window).expect("Connect to clipboard"); clipboard .write(String::from("Hello, world!")) diff --git a/src/lib.rs b/src/lib.rs index 842161d..c396478 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,10 +34,10 @@ pub struct Clipboard { } impl Clipboard { - pub fn new( + pub fn connect( window: &W, ) -> Result> { - let raw = platform::new_clipboard(window)?; + let raw = platform::connect(window)?; Ok(Clipboard { raw }) } diff --git a/src/platform/android.rs b/src/platform/android.rs index ec249a4..e0e44ed 100644 --- a/src/platform/android.rs +++ b/src/platform/android.rs @@ -3,7 +3,7 @@ use crate::ClipboardProvider; use raw_window_handle::HasRawWindowHandle; use std::error::Error; -pub fn new_clipboard( +pub fn connect( _window: &W, ) -> Result, Box> { Ok(Box::new(Clipboard::new()?)) diff --git a/src/platform/ios.rs b/src/platform/ios.rs index ddf8cbd..77fb216 100644 --- a/src/platform/ios.rs +++ b/src/platform/ios.rs @@ -3,7 +3,7 @@ use crate::ClipboardProvider; use raw_window_handle::HasRawWindowHandle; use std::error::Error; -pub fn new_clipboard( +pub fn connect( _window: &W, ) -> Result, Box> { Ok(Box::new(Clipboard::new()?)) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index e0b3667..8d93964 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -6,7 +6,7 @@ use std::error::Error; pub use clipboard_wayland as wayland; pub use clipboard_x11 as x11; -pub fn new_clipboard( +pub fn connect( window: &W, ) -> Result, Box> { let clipboard = match window.raw_window_handle() { @@ -14,10 +14,10 @@ pub fn new_clipboard( assert!(!handle.display.is_null()); Box::new(unsafe { - wayland::Clipboard::new(handle.display as *mut _) + wayland::Clipboard::connect(handle.display as *mut _) }) as _ } - _ => Box::new(x11::Clipboard::new()?) as _, + _ => Box::new(x11::Clipboard::connect()?) as _, }; Ok(clipboard) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 2f269ee..47ba0a2 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -3,7 +3,7 @@ use crate::ClipboardProvider; use raw_window_handle::HasRawWindowHandle; use std::error::Error; -pub fn new_clipboard( +pub fn connect( _window: &W, ) -> Result, Box> { Ok(Box::new(clipboard_macos::Clipboard::new()?)) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 82d472f..4c498a4 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -5,7 +5,7 @@ use raw_window_handle::HasRawWindowHandle; use std::error::Error; -pub fn new_clipboard( +pub fn connect( _window: &W, ) -> Result, Box> { Ok(Box::new(Clipboard)) diff --git a/wayland/src/lib.rs b/wayland/src/lib.rs index 9d4b92c..801ce0f 100644 --- a/wayland/src/lib.rs +++ b/wayland/src/lib.rs @@ -21,7 +21,7 @@ pub struct Clipboard { } impl Clipboard { - pub unsafe fn new(display: *mut c_void) -> Clipboard { + pub unsafe fn connect(display: *mut c_void) -> Clipboard { let context = Arc::new(Mutex::new(smithay_clipboard::Clipboard::new( display as *mut _, ))); diff --git a/x11/src/clipboard.rs b/x11/src/clipboard.rs deleted file mode 100644 index 0cea0ae..0000000 --- a/x11/src/clipboard.rs +++ /dev/null @@ -1,533 +0,0 @@ -use crate::error::Error; - -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, - pub clipboard: Atom, - pub property: Atom, - pub targets: Atom, - pub string: Atom, - pub utf8_string: Atom, - pub incr: Atom, -} - -#[inline] -fn get_atom(connection: &Connection, name: &str) -> Result { - x11rb::protocol::xproto::intern_atom(connection, false, name.as_bytes()) - .map_err(Into::into) - .and_then(|cookie| cookie.reply()) - .map(|reply| reply.atom) - .map_err(Into::into) -} - -impl Context { - pub fn new(displayname: Option<&str>) -> Result { - let (connection, screen) = Connection::connect(displayname)?; - let window = connection.generate_id().map_err(|_| { - Error::ConnectionFailed(ConnectError::InvalidScreen) - })?; - - { - let screen = - connection.setup().roots.get(screen as usize).ok_or( - Error::ConnectionFailed(ConnectError::InvalidScreen), - )?; - - let _ = xproto::create_window( - &connection, - x11rb::COPY_DEPTH_FROM_PARENT, - window, - screen.root, - 0, - 0, - 1, - 1, - 0, - xproto::WindowClass::INPUT_OUTPUT, - screen.root_visual, - &xproto::CreateWindowAux::new().event_mask( - xproto::EventMask::STRUCTURE_NOTIFY - | xproto::EventMask::PROPERTY_CHANGE, - ), - )?; - - let _ = connection.flush()?; - } - - let atoms = Atoms { - primary: AtomEnum::PRIMARY.into(), - clipboard: get_atom(&connection, "CLIPBOARD")?, - property: get_atom(&connection, "THIS_CLIPBOARD_OUT")?, - targets: get_atom(&connection, "TARGETS")?, - string: AtomEnum::STRING.into(), - utf8_string: get_atom(&connection, "UTF8_STRING")?, - incr: get_atom(&connection, "INCR")?, - }; - - Ok(Context { - connection, - screen, - window, - atoms, - }) - } -} - -pub struct Worker { - context: Arc, - selections: Arc)>>>, - receiver: mpsc::Receiver, -} - -struct IncrState { - selection: Atom, - requestor: Atom, - property: Atom, - pos: usize, -} - -impl Worker { - pub const INCR_CHUNK_SIZE: usize = 4000; - - pub fn run(self) { - use x11rb::connection::RequestConnection; - - 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::SelectionRequest(event) => { - let selections = match self.selections.read().ok() { - Some(selections) => selections, - None => continue, - }; - - 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 _ = xproto::send_event( - &self.context.connection, - false, - 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, - }, - ) - .expect("Send event"); - - let _ = self.context.connection.flush(); - } - Event::PropertyNotify(event) => { - if event.state != xproto::Property::DELETE { - continue; - } - - 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 _ = 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"); - - state.pos += len; - len == 0 - }; - - if is_end { - state_map.remove(&event.atom); - } - - self.context.connection.flush().expect("Flush connection"); - } - Event::SelectionClear(event) => { - if let Some(property) = incr_map.remove(&event.selection) { - state_map.remove(&property); - } - - if let Ok(mut write_setmap) = self.selections.write() { - write_setmap.remove(&event.selection); - } - } - _ => (), - } - } - } -} diff --git a/x11/src/lib.rs b/x11/src/lib.rs index 8906275..da7788e 100644 --- a/x11/src/lib.rs +++ b/x11/src/lib.rs @@ -1,6 +1,538 @@ #[forbid(unsafe_code)] -mod clipboard; mod error; -pub use clipboard::Clipboard; pub use error::Error; + +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); + +/// A connection to an X11 [`Clipboard`]. +pub struct Clipboard { + reader: Context, + writer: Arc, + selections: Arc)>>>, + worker: mpsc::Sender, +} + +impl Clipboard { + /// Connect to the running X11 server and obtain a [`Clipboard`]. + pub fn connect() -> 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, + }) + } + + /// Read the current [`Clipboard`] value. + 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)?) + } + + /// Write a new value to the [`Clipboard`]. + 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, + pub clipboard: Atom, + pub property: Atom, + pub targets: Atom, + pub string: Atom, + pub utf8_string: Atom, + pub incr: Atom, +} + +#[inline] +fn get_atom(connection: &Connection, name: &str) -> Result { + x11rb::protocol::xproto::intern_atom(connection, false, name.as_bytes()) + .map_err(Into::into) + .and_then(|cookie| cookie.reply()) + .map(|reply| reply.atom) + .map_err(Into::into) +} + +impl Context { + pub fn new(displayname: Option<&str>) -> Result { + let (connection, screen) = Connection::connect(displayname)?; + let window = connection.generate_id().map_err(|_| { + Error::ConnectionFailed(ConnectError::InvalidScreen) + })?; + + { + let screen = + connection.setup().roots.get(screen as usize).ok_or( + Error::ConnectionFailed(ConnectError::InvalidScreen), + )?; + + let _ = xproto::create_window( + &connection, + x11rb::COPY_DEPTH_FROM_PARENT, + window, + screen.root, + 0, + 0, + 1, + 1, + 0, + xproto::WindowClass::INPUT_OUTPUT, + screen.root_visual, + &xproto::CreateWindowAux::new().event_mask( + xproto::EventMask::STRUCTURE_NOTIFY + | xproto::EventMask::PROPERTY_CHANGE, + ), + )?; + + let _ = connection.flush()?; + } + + let atoms = Atoms { + primary: AtomEnum::PRIMARY.into(), + clipboard: get_atom(&connection, "CLIPBOARD")?, + property: get_atom(&connection, "THIS_CLIPBOARD_OUT")?, + targets: get_atom(&connection, "TARGETS")?, + string: AtomEnum::STRING.into(), + utf8_string: get_atom(&connection, "UTF8_STRING")?, + incr: get_atom(&connection, "INCR")?, + }; + + Ok(Context { + connection, + screen, + window, + atoms, + }) + } +} + +pub struct Worker { + context: Arc, + selections: Arc)>>>, + receiver: mpsc::Receiver, +} + +struct IncrState { + selection: Atom, + requestor: Atom, + property: Atom, + pos: usize, +} + +impl Worker { + pub const INCR_CHUNK_SIZE: usize = 4000; + + pub fn run(self) { + use x11rb::connection::RequestConnection; + + 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::SelectionRequest(event) => { + let selections = match self.selections.read().ok() { + Some(selections) => selections, + None => continue, + }; + + 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 _ = xproto::send_event( + &self.context.connection, + false, + 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, + }, + ) + .expect("Send event"); + + let _ = self.context.connection.flush(); + } + Event::PropertyNotify(event) => { + if event.state != xproto::Property::DELETE { + continue; + } + + 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 _ = 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"); + + state.pos += len; + len == 0 + }; + + if is_end { + state_map.remove(&event.atom); + } + + self.context.connection.flush().expect("Flush connection"); + } + Event::SelectionClear(event) => { + if let Some(property) = incr_map.remove(&event.selection) { + state_map.remove(&property); + } + + if let Ok(mut write_setmap) = self.selections.write() { + write_setmap.remove(&event.selection); + } + } + _ => (), + } + } + } +}