x11-clipboard-0.5.1/.gitignore010064400017500001750000000000361350515100400142650ustar0000000000000000target/ **/*.rs.bk Cargo.lock x11-clipboard-0.5.1/.gitjournal.toml010064400017500001750000000004211350515100400154240ustar0000000000000000categories = ["Added", "Changed", "Fixed", "Improved", "Removed"] category_delimiters = ["[", "]"] colored_output = true enable_debug = true enable_footers = false excluded_commit_tags = [] show_commit_hash = false show_prefix = false sort_by = "date" template_prefix = "" x11-clipboard-0.5.1/.travis.yml010064400017500001750000000003541356676257400144440ustar0000000000000000language: rust rust: - stable cache: cargo os: - linux sudo: required services: - xvfb before_script: - sudo apt-get update -qq - sudo apt-get install -y libxcb-shape0-dev libxcb-xfixes0-dev script: - cargo test x11-clipboard-0.5.1/Cargo.toml.orig010064400017500001750000000007021357076161200152020ustar0000000000000000[package] name = "x11-clipboard" version = "0.5.1" authors = ["quininer kel "] description = "x11 clipboard support for Rust." repository = "https://github.com/quininer/x11-clipboard" documentation = "https://docs.rs/x11-clipboard/" keywords = [ "x11", "xcb", "clipboard" ] license = "MIT" [badges] travis-ci = { repository = "quininer/x11-clipboard" } [dependencies] xcb = { version = "0.9", features = [ "thread", "xfixes" ] } x11-clipboard-0.5.1/Cargo.toml0000644000000016741357076202500115200ustar00# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] name = "x11-clipboard" version = "0.5.1" authors = ["quininer kel "] description = "x11 clipboard support for Rust." documentation = "https://docs.rs/x11-clipboard/" keywords = ["x11", "xcb", "clipboard"] license = "MIT" repository = "https://github.com/quininer/x11-clipboard" [dependencies.xcb] version = "0.9" features = ["thread", "xfixes"] [badges.travis-ci] repository = "quininer/x11-clipboard" x11-clipboard-0.5.1/LICENSE010064400017500001750000000020611350515100400133020ustar0000000000000000MIT License Copyright (c) 2017 quininer@live.com 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. x11-clipboard-0.5.1/README.md010064400017500001750000000012111350515100400135500ustar0000000000000000# x11-clipboard [![travis-ci](https://travis-ci.org/quininer/x11-clipboard.svg?branch=master)](https://travis-ci.org/quininer/x11-clipboard) [![crates](https://img.shields.io/crates/v/x11-clipboard.svg)](https://crates.io/crates/x11-clipboard) [![license](https://img.shields.io/github/license/quininer/x11-clipboard.svg)](https://github.com/quininer/x11-clipboard/blob/master/LICENSE) [![docs.rs](https://docs.rs/x11-clipboard/badge.svg)](https://docs.rs/x11-clipboard/) x11 clipboard support for Rust. ## requirements * xcb ## reference * [2. Peer-to-Peer Communication by Means of Selections](https://tronche.com/gui/x/icccm/sec-2.html#s-2) x11-clipboard-0.5.1/examples/monitor_primary_selection.rs010064400017500001750000000014341356676257400220160ustar0000000000000000extern crate x11_clipboard; use x11_clipboard::Clipboard; fn main() { let clipboard = Clipboard::new().unwrap(); let mut last = String::new(); println!("Waiting for selection..."); loop { if let Ok(curr) = clipboard.load_wait( clipboard.getter.atoms.primary, clipboard.getter.atoms.utf8_string, clipboard.getter.atoms.property ) { let curr = String::from_utf8_lossy(&curr); let curr = curr .trim_matches('\u{0}') .trim(); if !curr.is_empty() && last != curr { last = curr.to_owned(); println!("Contents of primary selection: {}", last); println!("Waiting for selection..."); } } } } x11-clipboard-0.5.1/examples/paste.rs010064400017500001750000000007131350515500000155770ustar0000000000000000extern crate x11_clipboard; use std::time::Duration; use x11_clipboard::Clipboard; fn main() { let clipboard = Clipboard::new().unwrap(); let val = clipboard.load( clipboard.setter.atoms.clipboard, clipboard.setter.atoms.utf8_string, clipboard.setter.atoms.property, Duration::from_secs(3) ) .unwrap(); let val = String::from_utf8(val).unwrap(); print!("{}", val); } x11-clipboard-0.5.1/src/error.rs010064400017500001750000000033131357076177100146100ustar0000000000000000use xcb::Atom; use xcb::base::{ ConnError, GenericError }; use std::fmt; use std::sync::mpsc::SendError; use std::error::Error as StdError; #[must_use] #[derive(Debug)] pub enum Error { Set(SendError), XcbConn(ConnError), XcbGeneric(GenericError), Lock, Timeout, Owner, UnexpectedType(Atom), #[doc(hidden)] __Unknown } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::Error::*; match self { Set(e) => write!(f, "XCB - couldn't set atom: {:?}", e), XcbConn(e) => write!(f, "XCB connection error: {:?}", e), XcbGeneric(e) => write!(f, "XCB generic error: {:?}", e), Lock => write!(f, "XCB: Lock is poisoned"), Timeout => write!(f, "Selection timed out"), Owner => write!(f, "Failed to set new owner of XCB selection"), UnexpectedType(target) => write!(f, "Unexpected Reply type: {}", target), __Unknown => unreachable!() } } } impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { use self::Error::*; match self { Set(e) => Some(e), XcbConn(e) => Some(e), XcbGeneric(e) => Some(e), Lock | Timeout | Owner | UnexpectedType(_) => None, __Unknown => unreachable!() } } } macro_rules! define_from { ( $item:ident from $err:ty ) => { impl From<$err> for Error { fn from(err: $err) -> Error { Error::$item(err) } } } } define_from!(Set from SendError); define_from!(XcbConn from ConnError); define_from!(XcbGeneric from GenericError); x11-clipboard-0.5.1/src/lib.rs010064400017500001750000000257571357076022300142340ustar0000000000000000pub extern crate xcb; pub mod error; mod run; use std::thread; use std::time::{ Duration, Instant }; use std::sync::{ Arc, RwLock }; use std::sync::mpsc::{ Sender, channel }; use std::collections::HashMap; use xcb::{ Connection, Window, Atom }; use xcb::base::ConnError; use error::Error; pub const INCR_CHUNK_SIZE: usize = 4000; const POLL_DURATION: u64 = 50; type SetMap = Arc)>>>; #[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 setter: Arc, setmap: SetMap, send: Sender } pub struct Context { pub connection: Connection, pub screen: i32, pub window: Window, pub atoms: Atoms } #[inline] fn get_atom(connection: &Connection, name: &str) -> Result { xcb::intern_atom(connection, false, name) .get_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(); { let screen = connection.get_setup().roots().nth(screen as usize) .ok_or(Error::XcbConn(ConnError::ClosedInvalidScreen))?; xcb::create_window( &connection, xcb::COPY_FROM_PARENT as u8, window, screen.root(), 0, 0, 1, 1, 0, xcb::WINDOW_CLASS_INPUT_OUTPUT as u16, screen.root_visual(), &[( xcb::CW_EVENT_MASK, xcb::EVENT_MASK_STRUCTURE_NOTIFY | xcb::EVENT_MASK_PROPERTY_CHANGE )] ); connection.flush(); } macro_rules! intern_atom { ( $name:expr ) => { get_atom(&connection, $name)? } } let atoms = Atoms { primary: xcb::ATOM_PRIMARY, clipboard: intern_atom!("CLIPBOARD"), property: intern_atom!("THIS_CLIPBOARD_OUT"), targets: intern_atom!("TARGETS"), string: xcb::ATOM_STRING, utf8_string: intern_atom!("UTF8_STRING"), incr: intern_atom!("INCR") }; Ok(Context { connection, screen, window, atoms }) } pub fn get_atom(&self, name: &str) -> Result { get_atom(&self.connection, name) } } impl Clipboard { /// Create Clipboard. pub fn new() -> Result { let getter = Context::new(None)?; let setter = Arc::new(Context::new(None)?); let setter2 = Arc::clone(&setter); let setmap = Arc::new(RwLock::new(HashMap::new())); let setmap2 = Arc::clone(&setmap); let (sender, receiver) = channel(); let max_length = setter.connection.get_maximum_request_length() as usize * 4; thread::spawn(move || run::run(&setter2, &setmap2, max_length, &receiver)); Ok(Clipboard { getter, setter, setmap, send: sender }) } fn process_event(&self, buff: &mut Vec, selection: Atom, target: Atom, property: Atom, timeout: T, use_xfixes: bool, xfixes_event_base: u8) -> 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 use_xfixes { true => { match self.getter.connection.wait_for_event() { Some(event) => event, None => { continue } } }, false => { match self.getter.connection.poll_for_event() { Some(event) => event, None => { thread::park_timeout(Duration::from_millis(POLL_DURATION)); continue } } } }; let r = event.response_type(); if use_xfixes && r == (xfixes_event_base + xcb::xfixes::SELECTION_NOTIFY) { let event = unsafe { xcb::cast_event::(&event) }; xcb::convert_selection(&self.getter.connection, self.getter.window, selection, target, property, event.timestamp()); self.getter.connection.flush(); continue; } match r & !0x80 { xcb::SELECTION_NOTIFY => { let event = unsafe { xcb::cast_event::(&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() == xcb::ATOM_NONE { break; } let reply = xcb::get_property( &self.getter.connection, false, self.getter.window, event.property(), xcb::ATOM_ANY, buff.len() as u32, ::std::u32::MAX // FIXME reasonable buffer size ) .get_reply()?; if reply.type_() == self.getter.atoms.incr { if let Some(&size) = reply.value::().get(0) { buff.reserve(size as usize); } xcb::delete_property(&self.getter.connection, self.getter.window, property); 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 }, xcb::PROPERTY_NOTIFY if is_incr => { let event = unsafe { xcb::cast_event::(&event) }; if event.state() != xcb::PROPERTY_NEW_VALUE as u8 { continue }; let length = xcb::get_property( &self.getter.connection, false, self.getter.window, property, xcb::ATOM_ANY, 0, 0 ) .get_reply() .map(|reply| reply.bytes_after())?; let reply = xcb::get_property( &self.getter.connection, true, self.getter.window, property, xcb::ATOM_ANY, 0, length ) .get_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(); xcb::convert_selection( &self.getter.connection, self.getter.window, selection, target, property, xcb::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. ); self.getter.connection.flush(); self.process_event(&mut buff, selection, target, property, timeout, false, 0)?; xcb::delete_property(&self.getter.connection, self.getter.window, property); self.getter.connection.flush(); Ok(buff) } /// wait for a new value and load it pub fn load_wait(&self, selection: Atom, target: Atom, property: Atom) -> Result, Error> { let mut buff = Vec::new(); let screen = &self.getter.connection.get_setup().roots() .nth(self.getter.screen as usize) .ok_or(Error::XcbConn(ConnError::ClosedInvalidScreen))?; let xfixes = xcb::query_extension( &self.getter.connection, "XFIXES").get_reply()?; assert!(xfixes.present()); xcb::xfixes::query_version(&self.getter.connection, 5, 0); // Clear selection sources... xcb::xfixes::select_selection_input( &self.getter.connection, screen.root(), self.getter.atoms.primary, 0); xcb::xfixes::select_selection_input( &self.getter.connection, screen.root(), self.getter.atoms.clipboard, 0); // ...and set the one requested now xcb::xfixes::select_selection_input( &self.getter.connection, screen.root(), selection, xcb::xfixes::SELECTION_EVENT_MASK_SET_SELECTION_OWNER | xcb::xfixes::SELECTION_EVENT_MASK_SELECTION_CLIENT_CLOSE | xcb::xfixes::SELECTION_EVENT_MASK_SELECTION_WINDOW_DESTROY); self.getter.connection.flush(); self.process_event(&mut buff, selection, target, property, None, true, xfixes.first_event())?; xcb::delete_property(&self.getter.connection, self.getter.window, property); self.getter.connection.flush(); Ok(buff) } /// store value. pub fn store>>(&self, selection: Atom, target: Atom, value: T) -> Result<(), Error> { self.send.send(selection)?; self.setmap .write() .map_err(|_| Error::Lock)? .insert(selection, (target, value.into())); xcb::set_selection_owner( &self.setter.connection, self.setter.window, selection, xcb::CURRENT_TIME ); self.setter.connection.flush(); if xcb::get_selection_owner(&self.setter.connection, selection) .get_reply() .map(|reply| reply.owner() == self.setter.window) .unwrap_or(false) { Ok(()) } else { Err(Error::Owner) } } } x11-clipboard-0.5.1/src/run.rs010064400017500001750000000113141350515100400142370ustar0000000000000000use std::cmp; use std::sync::Arc; use std::sync::mpsc::Receiver; use std::collections::HashMap; use xcb::{ self, Atom }; use ::{ INCR_CHUNK_SIZE, Context, SetMap }; macro_rules! try_continue { ( $expr:expr ) => { match $expr { Some(val) => val, None => continue } }; } struct IncrState { selection: Atom, requestor: Atom, property: Atom, pos: usize } pub fn run(context: &Arc, setmap: &SetMap, max_length: usize, receiver: &Receiver) { let mut incr_map = HashMap::new(); let mut state_map = HashMap::new(); while let Some(event) = context.connection.wait_for_event() { while let Ok(selection) = receiver.try_recv() { if let Some(property) = incr_map.remove(&selection) { state_map.remove(&property); } } match event.response_type() & !0x80 { xcb::SELECTION_REQUEST => { let event = unsafe { xcb::cast_event::(&event) }; let read_map = try_continue!(setmap.read().ok()); let &(target, ref value) = try_continue!(read_map.get(&event.selection())); if event.target() == context.atoms.targets { xcb::change_property( &context.connection, xcb::PROP_MODE_REPLACE as u8, event.requestor(), event.property(), xcb::ATOM_ATOM, 32, &[context.atoms.targets, target] ); } else if value.len() < max_length - 24 { xcb::change_property( &context.connection, xcb::PROP_MODE_REPLACE as u8, event.requestor(), event.property(), target, 8, value ); } else { xcb::change_window_attributes( &context.connection, event.requestor(), &[(xcb::CW_EVENT_MASK, xcb::EVENT_MASK_PROPERTY_CHANGE)] ); xcb::change_property( &context.connection, xcb::PROP_MODE_REPLACE as u8, event.requestor(), event.property(), context.atoms.incr, 32, &[0u8; 0] ); incr_map.insert(event.selection(), event.property()); state_map.insert( event.property(), IncrState { selection: event.selection(), requestor: event.requestor(), property: event.property(), pos: 0 } ); } xcb::send_event( &context.connection, false, event.requestor(), 0, &xcb::SelectionNotifyEvent::new( event.time(), event.requestor(), event.selection(), event.target(), event.property() ) ); context.connection.flush(); }, xcb::PROPERTY_NOTIFY => { let event = unsafe { xcb::cast_event::(&event) }; if event.state() != xcb::PROPERTY_DELETE as u8 { continue }; let is_end = { let state = try_continue!(state_map.get_mut(&event.atom())); let read_setmap = try_continue!(setmap.read().ok()); let &(target, ref value) = try_continue!(read_setmap.get(&state.selection)); let len = cmp::min(INCR_CHUNK_SIZE, value.len() - state.pos); xcb::change_property( &context.connection, xcb::PROP_MODE_REPLACE as u8, state.requestor, state.property, target, 8, &value[state.pos..][..len] ); state.pos += len; len == 0 }; if is_end { state_map.remove(&event.atom()); } context.connection.flush(); }, xcb::SELECTION_CLEAR => { let event = unsafe { xcb::cast_event::(&event) }; if let Some(property) = incr_map.remove(&event.selection()) { state_map.remove(&property); } if let Ok(mut write_setmap) = setmap.write() { write_setmap.remove(&event.selection()); } }, _ => () } } } x11-clipboard-0.5.1/tests/simple-test.rs010064400017500001750000000022551350515100400162600ustar0000000000000000extern crate x11_clipboard; use std::time::{ Instant, Duration }; use x11_clipboard::Clipboard; #[test] fn it_work() { let data = format!("{:?}", Instant::now()); let clipboard = Clipboard::new().unwrap(); let atom_clipboard = clipboard.setter.atoms.clipboard; let atom_utf8string = clipboard.setter.atoms.utf8_string; let atom_property = clipboard.setter.atoms.property; clipboard.store(atom_clipboard, atom_utf8string, data.as_bytes()).unwrap(); let output = clipboard.load(atom_clipboard, atom_utf8string, atom_property, None).unwrap(); assert_eq!(output, data.as_bytes()); let data = format!("{:?}", Instant::now()); clipboard.store(atom_clipboard, atom_utf8string, data.as_bytes()).unwrap(); let output = clipboard.load(atom_clipboard, atom_utf8string, atom_property, None).unwrap(); assert_eq!(output, data.as_bytes()); let output = clipboard.load(atom_clipboard, atom_utf8string, atom_property, None).unwrap(); assert_eq!(output, data.as_bytes()); let dur = Duration::from_secs(3); let output = clipboard.load(atom_clipboard, atom_utf8string, atom_property, dur).unwrap(); assert_eq!(output, data.as_bytes()); } x11-clipboard-0.5.1/.cargo_vcs_info.json0000644000000001121357076202500135040ustar00{ "git": { "sha1": "73c14a84007976f30801f7e484a297bddb3b6bbe" } } x11-clipboard-0.5.1/Cargo.lock0000644000000027701357076202500114730ustar00# This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] name = "cfg-if" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "libc" version = "0.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "log" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "x11-clipboard" version = "0.5.1" dependencies = [ "xcb 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "xcb" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [metadata] "checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" "checksum libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "6281b86796ba5e4366000be6e9e18bf35580adf9e63fbe2294aadb587613a319" "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" "checksum xcb 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "62056f63138b39116f82a540c983cc11f1c90cd70b3d492a70c25eaa50bd22a6"