diff --git a/examples/basic.rs b/examples/read.rs similarity index 90% rename from examples/basic.rs rename to examples/read.rs index d0de874..6839347 100644 --- a/examples/basic.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 new file mode 100644 index 0000000..b72fed6 --- /dev/null +++ b/examples/write.rs @@ -0,0 +1,31 @@ +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::connect(&window).expect("Connect to 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..c396478 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,21 +34,25 @@ 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 }) } 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..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()?)) @@ -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..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()?)) @@ -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..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) @@ -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..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()?)) @@ -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..4c498a4 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1,11 +1,11 @@ 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; -pub fn new_clipboard( +pub fn connect( _window: &W, ) -> Result, Box> { Ok(Box::new(Clipboard)) @@ -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/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 0c7db51..0000000 --- a/x11/src/clipboard.rs +++ /dev/null @@ -1,274 +0,0 @@ -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; - -const POLL_DURATION: std::time::Duration = Duration::from_micros(50); - -#[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, -} - -/// 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()) - .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, - }) - } -} - -impl Clipboard { - /// Create Clipboard. - pub fn new() -> Result { - let getter = Context::new(None)?; - - Ok(Clipboard { getter }) - } - - 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.getter.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.getter.connection, - false, - self.getter.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.getter.atoms.incr { - if let Some(&size) = reply.value.get(0) { - buff.reserve(size as usize); - } - - let _ = xproto::delete_property( - &self.getter.connection, - self.getter.window, - property, - ); - - let _ = self.getter.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.getter.connection, - false, - self.getter.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.getter.connection, - true, - self.getter.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(()) - } - - /// 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..da7788e 100644 --- a/x11/src/lib.rs +++ b/x11/src/lib.rs @@ -1,22 +1,538 @@ #[forbid(unsafe_code)] -mod clipboard; mod error; -use std::error::Error; +pub use error::Error; -pub struct Clipboard(clipboard::Clipboard); +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 { - pub fn new() -> Result> { - Ok(Clipboard(clipboard::Clipboard::new()?)) + /// 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, + }) } - 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, + /// 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); + } + } + _ => (), + } + } } }