├── .gitignore ├── .gitjournal.toml ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── monitor_primary_selection.rs ├── paste.rs └── wait_copy_event.rs ├── src ├── error.rs ├── lib.rs └── run.rs └── tests ├── simple-test.rs └── window-leak.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | /.idea 5 | -------------------------------------------------------------------------------- /.gitjournal.toml: -------------------------------------------------------------------------------- 1 | categories = ["Added", "Changed", "Fixed", "Improved", "Removed"] 2 | category_delimiters = ["[", "]"] 3 | colored_output = true 4 | enable_debug = true 5 | enable_footers = false 6 | excluded_commit_tags = [] 7 | show_commit_hash = false 8 | show_prefix = false 9 | sort_by = "date" 10 | template_prefix = "" 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | cache: cargo 5 | os: 6 | - linux 7 | sudo: required 8 | services: 9 | - xvfb 10 | before_script: 11 | - sudo apt-get update -qq 12 | - sudo apt-get install -y libxcb-shape0-dev libxcb-xfixes0-dev 13 | script: 14 | - cargo test 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "x11-clipboard" 3 | version = "0.9.3" 4 | authors = ["quininer kel "] 5 | description = "x11 clipboard support for Rust." 6 | repository = "https://github.com/quininer/x11-clipboard" 7 | documentation = "https://docs.rs/x11-clipboard/" 8 | keywords = [ "x11", "xcb", "clipboard" ] 9 | license = "MIT" 10 | 11 | [dependencies] 12 | libc = { version = "0.2.152" } 13 | x11rb = { version = "0.13.0", features = ["xfixes"]} 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2017 quininer@live.com 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # x11-clipboard 2 | [![travis-ci](https://travis-ci.org/quininer/x11-clipboard.svg?branch=master)](https://travis-ci.org/quininer/x11-clipboard) 3 | [![crates](https://img.shields.io/crates/v/x11-clipboard.svg)](https://crates.io/crates/x11-clipboard) 4 | [![license](https://img.shields.io/github/license/quininer/x11-clipboard.svg)](https://github.com/quininer/x11-clipboard/blob/master/LICENSE) 5 | [![docs.rs](https://docs.rs/x11-clipboard/badge.svg)](https://docs.rs/x11-clipboard/) 6 | 7 | x11 clipboard support for Rust. 8 | 9 | ## requirements 10 | 11 | * xcb 12 | 13 | ## reference 14 | 15 | * [2. Peer-to-Peer Communication by Means of Selections](https://tronche.com/gui/x/icccm/sec-2.html#s-2) 16 | -------------------------------------------------------------------------------- /examples/monitor_primary_selection.rs: -------------------------------------------------------------------------------- 1 | extern crate x11_clipboard; 2 | 3 | use x11_clipboard::Clipboard; 4 | 5 | 6 | fn main() { 7 | let clipboard = Clipboard::new().unwrap(); 8 | let mut last = String::new(); 9 | 10 | println!("Waiting for selection..."); 11 | 12 | loop { 13 | if let Ok(curr) = clipboard.load_wait( 14 | clipboard.getter.atoms.primary, 15 | clipboard.getter.atoms.utf8_string, 16 | clipboard.getter.atoms.property 17 | ) { 18 | let curr = String::from_utf8_lossy(&curr); 19 | let curr = curr 20 | .trim_matches('\u{0}') 21 | .trim(); 22 | if !curr.is_empty() && last != curr { 23 | last = curr.to_owned(); 24 | println!("Contents of primary selection: {}", last); 25 | println!("Waiting for selection..."); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/paste.rs: -------------------------------------------------------------------------------- 1 | extern crate x11_clipboard; 2 | 3 | use std::time::Duration; 4 | use x11_clipboard::Clipboard; 5 | 6 | 7 | fn main() { 8 | let clipboard = Clipboard::new().unwrap(); 9 | let val = 10 | clipboard.load( 11 | clipboard.setter.atoms.clipboard, 12 | clipboard.setter.atoms.utf8_string, 13 | clipboard.setter.atoms.property, 14 | Duration::from_secs(3) 15 | ) 16 | .unwrap(); 17 | let val = String::from_utf8(val).unwrap(); 18 | 19 | print!("{}", val); 20 | } 21 | -------------------------------------------------------------------------------- /examples/wait_copy_event.rs: -------------------------------------------------------------------------------- 1 | extern crate x11_clipboard; 2 | 3 | use x11_clipboard::Clipboard; 4 | 5 | fn main() { 6 | let clipboard = Clipboard::new().unwrap(); 7 | 8 | loop { 9 | let val = clipboard 10 | .load_wait( 11 | clipboard.setter.atoms.clipboard, 12 | clipboard.setter.atoms.string, 13 | clipboard.setter.atoms.property, 14 | ) 15 | .unwrap(); 16 | 17 | let val = String::from_utf8(val).unwrap(); 18 | 19 | println!("{}", val); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::sync::mpsc::SendError; 3 | use std::error::Error as StdError; 4 | use x11rb::errors::{ConnectError, ConnectionError, ReplyError, ReplyOrIdError}; 5 | use x11rb::protocol::xproto::Atom; 6 | 7 | #[must_use] 8 | #[derive(Debug)] 9 | #[non_exhaustive] 10 | pub enum Error { 11 | Set(SendError), 12 | XcbConnect(ConnectError), 13 | XcbConnection(ConnectionError), 14 | XcbReplyOrId(ReplyOrIdError), 15 | XcbReply(ReplyError), 16 | Lock, 17 | Timeout, 18 | Owner, 19 | UnexpectedType(Atom), 20 | // Could change name on next major, since this uses pipes now. 21 | EventFdCreate, 22 | } 23 | 24 | impl fmt::Display for Error { 25 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 26 | use self::Error::*; 27 | match self { 28 | Set(e) => write!(f, "XCB - couldn't set atom: {:?}", e), 29 | XcbConnect(e) => write!(f, "XCB - couldn't establish conection: {:?}", e), 30 | XcbConnection(e) => write!(f, "XCB connection error: {:?}", e), 31 | XcbReplyOrId(e) => write!(f, "XCB reply error: {:?}", e), 32 | XcbReply(e) => write!(f, "XCB reply error: {:?}", e), 33 | Lock => write!(f, "XCB: Lock is poisoned"), 34 | Timeout => write!(f, "Selection timed out"), 35 | Owner => write!(f, "Failed to set new owner of XCB selection"), 36 | UnexpectedType(target) => write!(f, "Unexpected Reply type: {:?}", target), 37 | EventFdCreate => write!(f, "Failed to create eventfd"), 38 | } 39 | } 40 | } 41 | 42 | impl StdError for Error { 43 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 44 | use self::Error::*; 45 | match self { 46 | Set(e) => Some(e), 47 | XcbConnection(e) => Some(e), 48 | XcbReply(e) => Some(e), 49 | XcbReplyOrId(e) => Some(e), 50 | XcbConnect(e) => Some(e), 51 | Lock | Timeout | Owner | UnexpectedType(_) | EventFdCreate => None, 52 | } 53 | } 54 | } 55 | 56 | macro_rules! define_from { 57 | ( $item:ident from $err:ty ) => { 58 | impl From<$err> for Error { 59 | fn from(err: $err) -> Error { 60 | Error::$item(err) 61 | } 62 | } 63 | } 64 | } 65 | 66 | define_from!(Set from SendError); 67 | define_from!(XcbConnect from ConnectError); 68 | define_from!(XcbConnection from ConnectionError); 69 | define_from!(XcbReply from ReplyError); 70 | define_from!(XcbReplyOrId from ReplyOrIdError); 71 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate x11rb; 2 | extern crate libc; 3 | 4 | pub mod error; 5 | mod run; 6 | 7 | pub use x11rb::protocol::xproto::{Atom, Window}; 8 | pub use x11rb::rust_connection::RustConnection; 9 | 10 | use std::thread; 11 | use std::time::{ Duration, Instant }; 12 | use std::sync::{ Arc, RwLock }; 13 | use std::sync::mpsc::{ Sender, channel }; 14 | use std::collections::HashMap; 15 | use std::os::fd::OwnedFd; 16 | use x11rb::connection::{Connection, RequestConnection}; 17 | use x11rb::{COPY_DEPTH_FROM_PARENT, CURRENT_TIME}; 18 | use x11rb::errors::ConnectError; 19 | use x11rb::protocol::{Event, xfixes}; 20 | use x11rb::protocol::xproto::{AtomEnum, ConnectionExt, CreateWindowAux, EventMask, Property, WindowClass}; 21 | use error::Error; 22 | use run::{create_pipe_drop_fd, PipeDropFds}; 23 | 24 | pub const INCR_CHUNK_SIZE: usize = 4000; 25 | const POLL_DURATION: u64 = 50; 26 | type SetMap = Arc)>>>; 27 | 28 | #[derive(Clone, Debug)] 29 | pub struct Atoms { 30 | pub primary: Atom, 31 | pub clipboard: Atom, 32 | pub property: Atom, 33 | pub targets: Atom, 34 | pub string: Atom, 35 | pub utf8_string: Atom, 36 | pub incr: Atom, 37 | } 38 | 39 | impl Atoms { 40 | fn intern_all(conn: &RustConnection) -> Result { 41 | let clipboard = conn.intern_atom( 42 | false, 43 | b"CLIPBOARD", 44 | )?; 45 | let property = conn.intern_atom( 46 | false, 47 | b"THIS_CLIPBOARD_OUT", 48 | )?; 49 | let targets = conn.intern_atom( 50 | false, 51 | b"TARGETS", 52 | )?; 53 | let utf8_string = conn.intern_atom( 54 | false, 55 | b"UTF8_STRING", 56 | )?; 57 | let incr = conn.intern_atom( 58 | false, 59 | b"INCR", 60 | )?; 61 | Ok(Atoms { 62 | primary: Atom::from(AtomEnum::PRIMARY), 63 | clipboard: clipboard.reply()?.atom, 64 | property: property.reply()?.atom, 65 | targets: targets.reply()?.atom, 66 | string: Atom::from(AtomEnum::STRING), 67 | utf8_string: utf8_string.reply()?.atom, 68 | incr: incr.reply()?.atom, 69 | }) 70 | } 71 | } 72 | 73 | /// X11 Clipboard 74 | pub struct Clipboard { 75 | pub getter: Context, 76 | pub setter: Arc, 77 | setmap: SetMap, 78 | send: Sender, 79 | // Relying on the Drop in OwnedFd to close the fd 80 | _drop_fd: OwnedFd, 81 | } 82 | 83 | pub struct Context { 84 | pub connection: RustConnection, 85 | pub screen: usize, 86 | pub window: Window, 87 | pub atoms: Atoms 88 | } 89 | 90 | #[inline] 91 | fn get_atom(connection: &RustConnection, name: &str) -> Result { 92 | let intern_atom = connection.intern_atom( 93 | false, 94 | name.as_bytes() 95 | )?; 96 | let reply = intern_atom.reply() 97 | .map_err(Error::XcbReply)?; 98 | Ok(reply.atom) 99 | } 100 | 101 | impl Context { 102 | pub fn new(displayname: Option<&str>) -> Result { 103 | let (connection, screen) = RustConnection::connect(displayname)?; 104 | let window = connection.generate_id()?; 105 | 106 | { 107 | let screen = connection.setup().roots.get(screen) 108 | .ok_or(Error::XcbConnect(ConnectError::InvalidScreen))?; 109 | connection.create_window( 110 | COPY_DEPTH_FROM_PARENT, 111 | window, 112 | screen.root, 113 | 0, 114 | 0, 115 | 1, 116 | 1, 117 | 0, 118 | WindowClass::INPUT_OUTPUT, 119 | screen.root_visual, 120 | &CreateWindowAux::new() 121 | .event_mask(EventMask::STRUCTURE_NOTIFY | EventMask::PROPERTY_CHANGE) 122 | )? 123 | .check()?; 124 | } 125 | 126 | let atoms = Atoms::intern_all(&connection)?; 127 | 128 | Ok(Context { connection, screen, window, atoms }) 129 | } 130 | 131 | pub fn get_atom(&self, name: &str) -> Result { 132 | get_atom(&self.connection, name) 133 | } 134 | } 135 | 136 | 137 | impl Clipboard { 138 | /// Create Clipboard. 139 | pub fn new() -> Result { 140 | let getter = Context::new(None)?; 141 | let setter = Arc::new(Context::new(None)?); 142 | let setter2 = Arc::clone(&setter); 143 | let setmap = Arc::new(RwLock::new(HashMap::new())); 144 | let setmap2 = Arc::clone(&setmap); 145 | 146 | let PipeDropFds { 147 | read_pipe, write_pipe 148 | } = create_pipe_drop_fd()?; 149 | let (sender, receiver) = channel(); 150 | let max_length = setter.connection.maximum_request_bytes(); 151 | thread::spawn(move || run::run(setter2, setmap2, max_length, receiver, read_pipe)); 152 | 153 | Ok(Clipboard { getter, setter, setmap, send: sender, _drop_fd: write_pipe }) 154 | } 155 | 156 | fn process_event(&self, buff: &mut Vec, selection: Atom, target: Atom, property: Atom, timeout: T, use_xfixes: bool, sequence_number: u64) 157 | -> Result<(), Error> 158 | where T: Into> 159 | { 160 | let mut is_incr = false; 161 | let timeout = timeout.into(); 162 | let start_time = 163 | if timeout.is_some() { Some(Instant::now()) } 164 | else { None }; 165 | 166 | loop { 167 | if timeout.into_iter() 168 | .zip(start_time) 169 | .next() 170 | .map(|(timeout, time)| (Instant::now() - time) >= timeout) 171 | .unwrap_or(false) 172 | { 173 | return Err(Error::Timeout); 174 | } 175 | 176 | let (event, seq) = match use_xfixes { 177 | true => self.getter.connection.wait_for_event_with_sequence()?, 178 | false => { 179 | match self.getter.connection.poll_for_event_with_sequence()? { 180 | Some(event) => event, 181 | None => { 182 | thread::park_timeout(Duration::from_millis(POLL_DURATION)); 183 | continue 184 | } 185 | } 186 | } 187 | }; 188 | 189 | if seq < sequence_number { 190 | continue; 191 | } 192 | 193 | match event { 194 | Event::XfixesSelectionNotify(event) if use_xfixes => { 195 | self.getter.connection.convert_selection( 196 | self.getter.window, 197 | selection, 198 | target, 199 | property, 200 | event.timestamp, 201 | )?.check()?; 202 | } 203 | Event::SelectionNotify(event) => { 204 | if event.selection != selection { continue }; 205 | 206 | // Note that setting the property argument to None indicates that the 207 | // conversion requested could not be made. 208 | if event.property == Atom::from(AtomEnum::NONE) { 209 | break; 210 | } 211 | 212 | let reply = self.getter.connection.get_property( 213 | false, 214 | self.getter.window, 215 | event.property, 216 | AtomEnum::NONE, 217 | buff.len() as u32, 218 | u32::MAX 219 | )?.reply()?; 220 | 221 | if reply.type_ == self.getter.atoms.incr { 222 | if let Some(mut value) = reply.value32() { 223 | if let Some(size) = value.next() { 224 | buff.reserve(size as usize); 225 | } 226 | } 227 | self.getter.connection.delete_property( 228 | self.getter.window, 229 | property 230 | )?.check()?; 231 | is_incr = true; 232 | continue 233 | } else if reply.type_ != target { 234 | return Err(Error::UnexpectedType(reply.type_)); 235 | } 236 | 237 | buff.extend_from_slice(&reply.value); 238 | break 239 | } 240 | 241 | Event::PropertyNotify(event) if is_incr => { 242 | if event.state != Property::NEW_VALUE { continue }; 243 | 244 | 245 | let cookie = self.getter.connection.get_property( 246 | false, 247 | self.getter.window, 248 | property, 249 | AtomEnum::NONE, 250 | 0, 251 | 0 252 | )?; 253 | 254 | let length = cookie.reply()?.bytes_after; 255 | 256 | let cookie = self.getter.connection.get_property( 257 | true, 258 | self.getter.window, 259 | property, 260 | AtomEnum::NONE, 261 | 0, length 262 | )?; 263 | let reply = cookie.reply()?; 264 | if reply.type_ != target { continue }; 265 | 266 | let value = reply.value; 267 | 268 | if !value.is_empty() { 269 | buff.extend_from_slice(&value); 270 | } else { 271 | break 272 | } 273 | }, 274 | _ => () 275 | } 276 | } 277 | Ok(()) 278 | } 279 | 280 | /// load value. 281 | pub fn load(&self, selection: Atom, target: Atom, property: Atom, timeout: T) 282 | -> Result, Error> 283 | where T: Into> 284 | { 285 | let mut buff = Vec::new(); 286 | let timeout = timeout.into(); 287 | 288 | let cookie = self.getter.connection.convert_selection( 289 | self.getter.window, 290 | selection, 291 | target, 292 | property, 293 | CURRENT_TIME, 294 | // FIXME ^ 295 | // Clients should not use CurrentTime for the time argument of a ConvertSelection request. 296 | // Instead, they should use the timestamp of the event that caused the request to be made. 297 | )?; 298 | 299 | let sequence_number = cookie.sequence_number(); 300 | cookie.check()?; 301 | 302 | self.process_event(&mut buff, selection, target, property, timeout, false, sequence_number)?; 303 | 304 | self.getter.connection.delete_property( 305 | self.getter.window, 306 | property 307 | )?.check()?; 308 | 309 | Ok(buff) 310 | } 311 | 312 | /// wait for a new value and load it 313 | pub fn load_wait(&self, selection: Atom, target: Atom, property: Atom) 314 | -> Result, Error> 315 | { 316 | let mut buff = Vec::new(); 317 | 318 | let screen = &self.getter.connection.setup().roots.get(self.getter.screen) 319 | .ok_or(Error::XcbConnect(ConnectError::InvalidScreen))?; 320 | 321 | xfixes::query_version( 322 | &self.getter.connection, 323 | 5, 324 | 0, 325 | )?; 326 | // Clear selection sources... 327 | xfixes::select_selection_input( 328 | &self.getter.connection, 329 | screen.root, 330 | self.getter.atoms.primary, 331 | xfixes::SelectionEventMask::default() 332 | )?; 333 | xfixes::select_selection_input( 334 | &self.getter.connection, 335 | screen.root, 336 | self.getter.atoms.clipboard, 337 | xfixes::SelectionEventMask::default() 338 | )?; 339 | // ...and set the one requested now 340 | let cookie = xfixes::select_selection_input( 341 | &self.getter.connection, 342 | screen.root, 343 | selection, 344 | xfixes::SelectionEventMask::SET_SELECTION_OWNER | 345 | xfixes::SelectionEventMask::SELECTION_CLIENT_CLOSE | 346 | xfixes::SelectionEventMask::SELECTION_WINDOW_DESTROY 347 | )?; 348 | 349 | let sequence_number = cookie.sequence_number(); 350 | cookie.check()?; 351 | 352 | self.process_event(&mut buff, selection, target, property, None, true, sequence_number)?; 353 | 354 | self.getter.connection.delete_property(self.getter.window, property)?.check()?; 355 | 356 | Ok(buff) 357 | } 358 | 359 | /// store value. 360 | pub fn store>>(&self, selection: Atom, target: Atom, value: T) 361 | -> Result<(), Error> 362 | { 363 | self.send.send(selection)?; 364 | self.setmap 365 | .write() 366 | .map_err(|_| Error::Lock)? 367 | .insert(selection, (target, value.into())); 368 | 369 | self.setter.connection.set_selection_owner( 370 | self.setter.window, 371 | selection, 372 | CURRENT_TIME 373 | )?.check()?; 374 | 375 | if self.setter.connection.get_selection_owner( 376 | selection 377 | )?.reply() 378 | .map(|reply| reply.owner == self.setter.window) 379 | .unwrap_or(false) { 380 | Ok(()) 381 | } else { 382 | Err(Error::Owner) 383 | } 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/run.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::sync::Arc; 3 | use std::sync::mpsc::{ Receiver, TryRecvError }; 4 | use std::collections::HashMap; 5 | use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd}; 6 | use ::{AtomEnum, EventMask}; 7 | use x11rb::connection::Connection; 8 | use x11rb::protocol::Event; 9 | use x11rb::protocol::xproto::{Atom, ChangeWindowAttributesAux, ConnectionExt, Property, PropMode, SELECTION_NOTIFY_EVENT, SelectionNotifyEvent, Window}; 10 | use ::{ INCR_CHUNK_SIZE, Context, SetMap }; 11 | use error::Error; 12 | 13 | macro_rules! try_continue { 14 | ( $expr:expr ) => { 15 | match $expr { 16 | Some(val) => val, 17 | None => continue 18 | } 19 | }; 20 | } 21 | 22 | struct IncrState { 23 | selection: Atom, 24 | requestor: Window, 25 | property: Atom, 26 | pos: usize 27 | } 28 | 29 | pub(crate) struct PipeDropFds { 30 | pub(crate) read_pipe: OwnedFd, 31 | pub(crate) write_pipe: OwnedFd, 32 | } 33 | 34 | pub(crate) fn create_pipe_drop_fd() -> Result{ 35 | let pipe_drop_fds = unsafe { 36 | // Docs Linux: https://man7.org/linux/man-pages/man2/pipe.2.html 37 | // Posix: https://pubs.opengroup.org/onlinepubs/9699919799/ 38 | // Safety: See above docs, api expects a 2-long array of file descriptors, and flags 39 | let mut pipes: [libc::c_int; 2] = [0, 0]; 40 | let pipe_create_res = libc::pipe2(pipes.as_mut_ptr(), libc::O_CLOEXEC); 41 | if pipe_create_res < 0 { 42 | // Don't want to have to read from errno_location, just skip propagating errno. 43 | return Err(Error::EventFdCreate); 44 | } 45 | // Safety: Trusting the OS to give correct FDs 46 | let read_pipe = OwnedFd::from_raw_fd(pipes[0]); 47 | let write_pipe = OwnedFd::from_raw_fd(pipes[1]); 48 | PipeDropFds { 49 | read_pipe, 50 | write_pipe, 51 | } 52 | }; 53 | Ok(pipe_drop_fds) 54 | } 55 | 56 | pub(crate) fn run(context: Arc, setmap: SetMap, max_length: usize, receiver: Receiver, read_pipe: OwnedFd) { 57 | let mut incr_map = HashMap::::new(); 58 | let mut state_map = HashMap::::new(); 59 | 60 | let stream_fd = context.connection.stream().as_fd(); 61 | let borrowed_fd = read_pipe.as_fd(); 62 | // Poll stream for new Read-ready events, check if the other side of the pipe has been dropped 63 | let mut pollfds: [libc::pollfd; 2] = [libc::pollfd { 64 | fd: stream_fd.as_raw_fd(), 65 | events: libc::POLLIN, 66 | revents: 0, 67 | }, libc::pollfd { 68 | fd: borrowed_fd.as_raw_fd(), 69 | // If the other end is dropped, this pipe will get a HUP on poll 70 | events: libc::POLLHUP, 71 | revents: 0, 72 | }]; 73 | let len = pollfds.len(); 74 | loop { 75 | unsafe { 76 | // Docs Linux: https://man7.org/linux/man-pages/man2/poll.2.html 77 | // Posix: https://pubs.opengroup.org/onlinepubs/9699919799/ 78 | // Safety: Passing in a mutable pointer that lives for the duration of the call, the length is 79 | // set to the length of that pointer. 80 | // Any negative value (-1 for example) means infinite timeout. 81 | let poll_res = libc::poll(&mut pollfds as *mut libc::pollfd, len as libc::nfds_t, -1); 82 | if poll_res < 0 { 83 | // Error polling, can't continue 84 | return; 85 | } 86 | } 87 | if pollfds[1].revents & libc::POLLHUP != 0 { 88 | // kill-signal on pollfd 89 | return; 90 | } 91 | loop { 92 | let evt = if let Ok(evt) = context.connection.poll_for_event() { 93 | evt 94 | } else { 95 | // Connection died, exit 96 | return; 97 | }; 98 | let event = if let Some(evt) = evt { 99 | evt 100 | } else { 101 | // No event on POLLIN happens, fd being readable doesn't mean there's a complete event ready to read. 102 | // Poll again. 103 | break; 104 | }; 105 | loop { 106 | match receiver.try_recv() { 107 | Ok(selection) => if let Some(property) = incr_map.remove(&selection) { 108 | state_map.remove(&property); 109 | }, 110 | Err(TryRecvError::Empty) => break, 111 | Err(TryRecvError::Disconnected) => if state_map.is_empty() { 112 | return 113 | } 114 | } 115 | } 116 | 117 | match event { 118 | Event::SelectionRequest(event) => { 119 | let read_map = try_continue!(setmap.read().ok()); 120 | let &(target, ref value) = try_continue!(read_map.get(&event.selection)); 121 | 122 | if event.target == context.atoms.targets { 123 | let _ = x11rb::wrapper::ConnectionExt::change_property32( 124 | &context.connection, 125 | PropMode::REPLACE, 126 | event.requestor, 127 | event.property, 128 | Atom::from(AtomEnum::ATOM), 129 | &[context.atoms.targets, target] 130 | ); 131 | } else if value.len() < max_length - 24 { 132 | let _ = x11rb::wrapper::ConnectionExt::change_property8( 133 | &context.connection, 134 | PropMode::REPLACE, 135 | event.requestor, 136 | event.property, 137 | target, 138 | value 139 | ); 140 | } else { 141 | let _ = context.connection.change_window_attributes( 142 | event.requestor, 143 | &ChangeWindowAttributesAux::new() 144 | .event_mask(EventMask::PROPERTY_CHANGE) 145 | ); 146 | let _ = x11rb::wrapper::ConnectionExt::change_property32( 147 | &context.connection, 148 | PropMode::REPLACE, 149 | event.requestor, 150 | event.property, 151 | context.atoms.incr, 152 | &[0u32; 0], 153 | ); 154 | incr_map.insert(event.selection, event.property); 155 | state_map.insert( 156 | event.property, 157 | IncrState { 158 | selection: event.selection, 159 | requestor: event.requestor, 160 | property: event.property, 161 | pos: 0 162 | } 163 | ); 164 | } 165 | let _ = context.connection.send_event( 166 | false, 167 | event.requestor, 168 | EventMask::default(), 169 | SelectionNotifyEvent { 170 | response_type: SELECTION_NOTIFY_EVENT, 171 | sequence: 0, 172 | time: event.time, 173 | requestor: event.requestor, 174 | selection: event.selection, 175 | target: event.target, 176 | property: event.property 177 | } 178 | ); 179 | let _ = context.connection.flush(); 180 | }, 181 | Event::PropertyNotify(event) => { 182 | if event.state != Property::DELETE { continue }; 183 | 184 | let is_end = { 185 | let state = try_continue!(state_map.get_mut(&event.atom)); 186 | let read_setmap = try_continue!(setmap.read().ok()); 187 | let &(target, ref value) = try_continue!(read_setmap.get(&state.selection)); 188 | 189 | let len = cmp::min(INCR_CHUNK_SIZE, value.len() - state.pos); 190 | let _ = x11rb::wrapper::ConnectionExt::change_property8( 191 | &context.connection, 192 | PropMode::REPLACE, 193 | state.requestor, 194 | state.property, 195 | target, 196 | &value[state.pos..][..len] 197 | ); 198 | state.pos += len; 199 | len == 0 200 | }; 201 | 202 | if is_end { 203 | state_map.remove(&event.atom); 204 | } 205 | let _ = context.connection.flush(); 206 | }, 207 | Event::SelectionClear(event) => { 208 | if let Some(property) = incr_map.remove(&event.selection) { 209 | state_map.remove(&property); 210 | } 211 | if let Ok(mut write_setmap) = setmap.write() { 212 | write_setmap.remove(&event.selection); 213 | } 214 | } 215 | _ => () 216 | } 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /tests/simple-test.rs: -------------------------------------------------------------------------------- 1 | extern crate x11_clipboard; 2 | 3 | use std::time::{ Instant, Duration }; 4 | use x11_clipboard::Clipboard; 5 | 6 | 7 | #[test] 8 | fn it_work() { 9 | let data = format!("{:?}", Instant::now()); 10 | let clipboard = Clipboard::new().unwrap(); 11 | 12 | let atom_clipboard = clipboard.setter.atoms.clipboard; 13 | let atom_utf8string = clipboard.setter.atoms.utf8_string; 14 | let atom_property = clipboard.setter.atoms.property; 15 | 16 | clipboard.store(atom_clipboard, atom_utf8string, data.as_bytes()).unwrap(); 17 | 18 | let output = clipboard.load(atom_clipboard, atom_utf8string, atom_property, None).unwrap(); 19 | assert_eq!(output, data.as_bytes()); 20 | 21 | let data = format!("{:?}", Instant::now()); 22 | clipboard.store(atom_clipboard, atom_utf8string, data.as_bytes()).unwrap(); 23 | 24 | let output = clipboard.load(atom_clipboard, atom_utf8string, atom_property, None).unwrap(); 25 | assert_eq!(output, data.as_bytes()); 26 | 27 | let output = clipboard.load(atom_clipboard, atom_utf8string, atom_property, None).unwrap(); 28 | assert_eq!(output, data.as_bytes()); 29 | 30 | let dur = Duration::from_secs(3); 31 | let output = clipboard.load(atom_clipboard, atom_utf8string, atom_property, dur).unwrap(); 32 | assert_eq!(output, data.as_bytes()); 33 | } 34 | -------------------------------------------------------------------------------- /tests/window-leak.rs: -------------------------------------------------------------------------------- 1 | extern crate x11_clipboard; 2 | 3 | use x11_clipboard::error::Error; 4 | use x11_clipboard::Clipboard; 5 | 6 | pub fn paste_to_clipboard(content: &str) -> Result<(), Error> { 7 | let clipboard = Clipboard::new()?; 8 | 9 | clipboard.store( 10 | clipboard.setter.atoms.primary, 11 | clipboard.setter.atoms.utf8_string, 12 | content, 13 | )?; 14 | 15 | clipboard.store( 16 | clipboard.setter.atoms.clipboard, 17 | clipboard.setter.atoms.utf8_string, 18 | content, 19 | )?; 20 | 21 | Ok(()) 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use super::*; 27 | 28 | #[test] 29 | fn should_work_but_does_not() -> Result<(), Error> { 30 | for i in 0..1000 { 31 | paste_to_clipboard(&format!("I have told you {} times!", i))?; 32 | } 33 | 34 | Ok(()) 35 | } 36 | } 37 | --------------------------------------------------------------------------------