X11 clipboard impl work in progress.

This commit is contained in:
Artur Kovacs 2020-08-30 14:21:06 +02:00
parent 28910ef591
commit 1410b4e4e6
2 changed files with 123 additions and 85 deletions

View file

@ -2,11 +2,11 @@ extern crate clipboard;
use clipboard::ClipboardProvider;
#[cfg(target_os = "linux")]
use clipboard::x11_clipboard::{X11ClipboardContext, Primary};
use clipboard::x11_clipboard::{X11ClipboardContext};
#[cfg(target_os = "linux")]
fn main() {
let mut ctx: X11ClipboardContext<Primary> = ClipboardProvider::new().unwrap();
let mut ctx: X11ClipboardContext = ClipboardProvider::new().unwrap();
let the_string = "Hello, world!";

View file

@ -129,6 +129,7 @@ mod locking {
}
// TODO move this back to the manager struct as a regular field.
/// Variable used to wait more time if we've received an INCR
/// notification, which means that we're going to receive large
/// amounts of data from the selection owner.
@ -162,13 +163,6 @@ type BufferPtr = Option<Vec<u8>>;
type Atoms = Vec<xcb::xproto::Atom>;
type NotifyCallback = Option<Arc<dyn (Fn() -> bool) + Send + Sync + 'static>>;
struct SendableConnection {
con: xcb::Connection
}
/// Need to manually `impl Send` because the connection contains pointers,
/// and no pointer is `Send` by default.
unsafe impl Send for SendableConnection {}
lazy_static! {
static ref MANAGER: Option<Mutex<Manager>> = {
@ -180,25 +174,41 @@ lazy_static! {
/// Connection to X11 server
/// This is an `Arc<Mutex<>>` because it's shared between the user's thread and
/// the x11 event processing thread.
static ref CONNECTION: Mutex<SendableConnection> = {
static ref SHARED_STATE: Mutex<SharedState> = {
let connection = xcb::Connection::connect(None).unwrap().0;
Mutex::new(SendableConnection { con: connection })
Mutex::new(SharedState {
conn: connection,
atoms: Default::default(),
common_atoms: Default::default()
})
};
static ref ATOM_HANDLER: Mutex<AtomHandler> = Mutex::new(Default::default());
// Used to wait/notify the arrival of the SelectionNotify event when
// we requested the clipboard content from other selection owner.
static ref CONDVAR: Condvar = Condvar::new();
//static ref ATOM_HANDLER: Mutex<AtomHandler> = Mutex::new(Default::default());
}
/// The name indicates that objects in this struct are shared between
/// the event processing thread and the user tread. However it's important
/// that the `Manager` itself is also shared. So the real reason for splitting these
/// apart from the `Manager` is to conform to Rust's locking and aliasing rules but that is hard to
/// convey in a short name.
struct SharedState {
conn: xcb::Connection,
#[derive(Default)]
struct AtomHandler {
// Cache of known atoms
atoms: BTreeMap<String, xcb::xproto::Atom>,
// Cache of common used atoms by us
common_atoms: Atoms,
}
/// Need to manually `impl Send` because the connection contains pointers,
/// and no pointer is `Send` by default.
unsafe impl Send for SharedState {}
impl AtomHandler {
impl SharedState {
fn get_atom_by_id(&mut self, id: usize) -> xproto::Atom {
if self.common_atoms.is_empty() {
self.common_atoms = self.get_atoms(&COMMON_ATOM_NAMES);
@ -207,7 +217,7 @@ impl AtomHandler {
}
fn get_atoms(&mut self, names: &[&'static str]) -> Atoms {
let connection = CONNECTION.lock().unwrap();
let connection = SHARED_STATE.lock().unwrap();
let mut results = vec![0; names.len()];
let mut cookies = HashMap::with_capacity(names.len());
@ -217,14 +227,14 @@ impl AtomHandler {
if let Some(pair) = found {
*res = *pair.1;
} else {
cookies.insert(name, xproto::intern_atom(&connection.con, false, name));
cookies.insert(name, xproto::intern_atom(&connection.conn, false, name));
}
}
for (res, name) in results.iter_mut().zip(names.iter()) {
if *res == 0 {
let reply = unsafe { xcb::ffi::xproto::xcb_intern_atom_reply(
connection.con.get_raw_conn(),
connection.conn.get_raw_conn(),
cookies.get(name).unwrap().cookie,
std::ptr::null_mut()
)};
@ -241,10 +251,6 @@ impl AtomHandler {
}
}
fn get_atom_by_id(id: usize) -> xproto::Atom {
ATOM_HANDLER.lock().unwrap().get_atom_by_id(id)
}
struct Manager {
/// Original comment: "Access to the whole Manager"
@ -266,15 +272,14 @@ struct Manager {
// all events related about the clipboard in a background thread
window: xcb::xproto::Window,
// Used to wait/notify the arrival of the SelectionNotify event when
// we requested the clipboard content from other selection owner.
condvar: Condvar,
// Thread used to run a background message loop to wait X11 events
// about clipboard. The X11 selection owner will be a hidden window
// created by us just for the clipboard purpose/communication.
thread_handle: Option<std::thread::JoinHandle<()>>,
// WARNING: The callback must not attempt to lock the manager or the shared state.
// (Otherwise the code needs to be restructured slightly)
//
// Internal callback used when a SelectionNotify is received (or the
// whole data content is received by the INCR method). So this
// callback can use the notification by different purposes (e.g. get
@ -337,8 +342,8 @@ impl Manager {
XCB_WINDOW_CLASS_INPUT_OUTPUT,
XCB_CW_EVENT_MASK
};
let connection = CONNECTION.lock().unwrap();
let setup = connection.con.get_setup();
let connection = SHARED_STATE.lock().unwrap();
let setup = connection.conn.get_setup();
if std::ptr::null() == setup.ptr {
return Err("Could not get setup for connection".into());
}
@ -352,10 +357,10 @@ impl Manager {
XCB_EVENT_MASK_PROPERTY_CHANGE |
// To receive DestroyNotify event and stop the message loop.
XCB_EVENT_MASK_STRUCTURE_NOTIFY;
let mut window = connection.con.generate_id();
let window = connection.conn.generate_id();
unsafe {
xcb::ffi::xproto::xcb_create_window(
connection.con.get_raw_conn(),
connection.conn.get_raw_conn(),
0,
window,
(*screen).root,
@ -372,7 +377,6 @@ impl Manager {
Ok(Manager {
//mutex: Mutex::new(()),
window: 0,
condvar: Condvar::new(),
thread_handle: Some(thread_handle),
callback: None,
callback_result: false,
@ -429,12 +433,13 @@ impl Manager {
self.reply_offset += n;
}
fn call_callback(&mut self, reply: *mut xcb_get_property_reply_t) {
// Rust impl: It's strange, the reply attribute is also unused in the original code.
fn call_callback(&mut self, _reply: *mut xcb_get_property_reply_t) {
self.callback_result = false;
if let Some(callback) = &self.callback {
self.callback_result = callback();
}
self.condvar.notify_one();
CONDVAR.notify_one();
self.reply_data = None;
}
@ -453,13 +458,14 @@ impl Drop for Manager {
//xcb::xproto::Window
let mut x11_clipboard_manager = 0;
{
let con = CONNECTION.lock().unwrap();
let mut shared = SHARED_STATE.lock().unwrap();
let clipboard_manager_atom = shared.get_atom_by_id(CLIPBOARD_MANAGER);
let cookie = {
get_selection_owner(&con.con, get_atom_by_id(CLIPBOARD_MANAGER))
get_selection_owner(&shared.conn, clipboard_manager_atom)
};
let reply = {
unsafe { xcb::ffi::xproto::xcb_get_selection_owner_reply(
con.con.get_raw_conn(),
shared.conn.get_raw_conn(),
cookie.cookie,
std::ptr::null_mut()
)}
@ -472,8 +478,14 @@ impl Drop for Manager {
}
}
if x11_clipboard_manager != 0 {
let atoms = vec![get_atom_by_id(SAVE_TARGETS)];
let selection = get_atom_by_id(CLIPBOARD_MANAGER);
let atoms;
let selection;
{
let mut shared = SHARED_STATE.lock().unwrap();
atoms = vec![shared.get_atom_by_id(SAVE_TARGETS)];
selection = shared.get_atom_by_id(CLIPBOARD_MANAGER);
} // let go of the shared state lock before invoking `get_data_from_selection_owner`
// Start the SAVE_TARGETS mechanism so the X11
// CLIPBOARD_MANAGER will save our clipboard data
// from now on.
@ -486,16 +498,16 @@ impl Drop for Manager {
}
if self.window != 0 {
let con = CONNECTION.lock().unwrap();
let con = SHARED_STATE.lock().unwrap();
unsafe { xcb::ffi::xproto::xcb_destroy_window(
con.con.get_raw_conn(),
con.conn.get_raw_conn(),
self.window
)};
con.con.flush();
con.conn.flush();
}
if let Some(handle) = self.thread_handle.take() {
handle.join();
handle.join().ok();
}
// This is not needed because the connection is automatically disconnected when droped
@ -509,7 +521,6 @@ fn process_x11_events() {
XCB_DESTROY_NOTIFY,
XCB_SELECTION_CLEAR,
XCB_SELECTION_REQUEST,
XCB_SELECTION_NOTIFY,
XCB_PROPERTY_NOTIFY
};
@ -517,7 +528,7 @@ fn process_x11_events() {
let mut event;
while !stop {
event = {
let maybe_event = CONNECTION.lock().unwrap().con.wait_for_event();
let maybe_event = SHARED_STATE.lock().unwrap().conn.wait_for_event();
if let Some(e) = maybe_event {
e
} else {
@ -568,7 +579,11 @@ fn process_x11_events() {
fn handle_selection_clear_event(event: *mut xcb_selection_clear_event_t) {
let selection = unsafe { (*event).selection };
if selection == get_atom_by_id(CLIPBOARD) {
let clipboard_atom = {
let mut shared = SHARED_STATE.lock().unwrap();
shared.get_atom_by_id(CLIPBOARD)
};
if selection == clipboard_atom {
if let Some(manager) = &*MANAGER {
manager.lock().unwrap().clear_data();
}
@ -582,7 +597,6 @@ fn handle_selection_request_event(event: *mut xcb_selection_request_event_t) {
} else {
return;
};
let mut conn = CONNECTION.lock().unwrap();
let target;
let requestor;
let property;
@ -595,39 +609,55 @@ fn handle_selection_request_event(event: *mut xcb_selection_request_event_t) {
time = (*event).time;
selection = (*event).selection;
}
if target == get_atom_by_id(TARGETS) {
let targets_atom;
let save_targets_atom;
let multiple_atom;
let atom_atom;
{
let mut shared = SHARED_STATE.lock().unwrap();
targets_atom = shared.get_atom_by_id(TARGETS);
save_targets_atom = shared.get_atom_by_id(SAVE_TARGETS);
multiple_atom = shared.get_atom_by_id(MULTIPLE);
atom_atom = shared.get_atom_by_id(ATOM);
}
if target == targets_atom {
let mut targets = Atoms::with_capacity(4);
targets.push(get_atom_by_id(TARGETS));
targets.push(get_atom_by_id(SAVE_TARGETS));
targets.push(get_atom_by_id(MULTIPLE));
targets.push(targets_atom);
targets.push(save_targets_atom);
targets.push(multiple_atom);
let manager = manager_mutex.lock().unwrap();
for atom in manager.data.keys() {
targets.push(*atom);
}
let shared = SHARED_STATE.lock().unwrap();
// Set the "property" of "requestor" with the clipboard
// formats ("targets", atoms) that we provide.
unsafe { xcb_change_property(
conn.con.get_raw_conn(),
shared.conn.get_raw_conn(),
XCB_PROP_MODE_REPLACE as u8,
requestor,
property,
get_atom_by_id(ATOM),
atom_atom,
8 * std::mem::size_of::<xcb_atom_t>() as u8,
targets.len() as u32,
targets.as_ptr() as *const _
)};
} else if target == get_atom_by_id(SAVE_TARGETS) {
} else if target == save_targets_atom {
// Do nothing
} else if target == get_atom_by_id(MULTIPLE) {
} else if target == multiple_atom {
let mut manager = manager_mutex.lock().unwrap();
let reply = get_and_delete_property(
&mut conn.con,
requestor,
property,
get_atom_by_id(ATOM_PAIR),
false
);
let reply = {
let mut shared = SHARED_STATE.lock().unwrap();
let atom_pair_atom = shared.get_atom_by_id(ATOM_PAIR);
get_and_delete_property(
&mut shared.conn,
requestor,
property,
atom_pair_atom,
false
)
};
if reply != std::ptr::null_mut() {
let mut ptr: *mut xcb_atom_t = unsafe { xcb_get_property_value(reply) } as *mut xcb_atom_t;
let end = unsafe { ptr.offset(
@ -645,8 +675,9 @@ fn handle_selection_request_event(event: *mut xcb_selection_request_event_t) {
if !manager.set_requestor_property_with_clipboard_content(
requestor, property, target
) {
let shared = SHARED_STATE.lock().unwrap();
unsafe { xcb_change_property(
conn.con.get_raw_conn(),
shared.conn.get_raw_conn(),
XCB_PROP_MODE_REPLACE as u8,
requestor,
property,
@ -677,14 +708,15 @@ fn handle_selection_request_event(event: *mut xcb_selection_request_event_t) {
target: target,
property: property,
};
let shared = SHARED_STATE.lock().unwrap();
unsafe { xcb_send_event(
conn.con.get_raw_conn(),
shared.conn.get_raw_conn(),
0,
requestor,
XCB_EVENT_MASK_NO_EVENT,
&notify as *const _ as *const _
)};
conn.con.flush();
shared.conn.flush();
}
fn handle_selection_notify_event(event: *mut xcb_selection_notify_event_t) {
@ -702,18 +734,18 @@ fn handle_selection_notify_event(event: *mut xcb_selection_notify_event_t) {
requestor = (*event).requestor;
property = (*event).property;
}
let mut connection = CONNECTION.lock().unwrap();
let mut shared = SHARED_STATE.lock().unwrap();
let mut manager = manager_mutex.lock().unwrap();
assert_eq!(requestor, manager.window);
if target == get_atom_by_id(TARGETS) {
manager.target_atom = get_atom_by_id(ATOM);
if target == shared.get_atom_by_id(TARGETS) {
manager.target_atom = shared.get_atom_by_id(ATOM);
} else {
manager.target_atom = target;
}
let mut reply = get_and_delete_property(
&mut connection.con,
&mut shared.conn,
requestor,
property,
manager.target_atom,
@ -723,11 +755,12 @@ fn handle_selection_notify_event(event: *mut xcb_selection_notify_event_t) {
let reply_type = unsafe { (*reply).type_ };
// In this case, We're going to receive the clipboard content in
// chunks of data with several PropertyNotify events.
if reply_type == get_atom_by_id(INCR) {
let incr_atom = shared.get_atom_by_id(INCR);
if reply_type == incr_atom {
unsafe { libc::free(reply as *mut _); }
reply = get_and_delete_property(
&mut connection.con,
requestor, property, get_atom_by_id(INCR), true
&mut shared.conn,
requestor, property, incr_atom, true
);
if reply != std::ptr::null_mut() {
if unsafe { xcb_get_property_value_length(reply) } == 4 {
@ -768,10 +801,10 @@ fn handle_property_notify_event(event: *mut xcb_property_notify_event_t) {
window = (*event).window;
}
let mut manager = manager_mutex.lock().unwrap();
let mut conn = CONNECTION.lock().unwrap();
if manager.incr_process && state == XCB_PROPERTY_NEW_VALUE && atom == get_atom_by_id(CLIPBOARD) {
let mut shared = SHARED_STATE.lock().unwrap();
if manager.incr_process && state == XCB_PROPERTY_NEW_VALUE && atom == shared.get_atom_by_id(CLIPBOARD) {
let reply = get_and_delete_property(
&mut conn.con, window, atom, manager.target_atom, true
&mut shared.conn, window, atom, manager.target_atom, true
);
if reply != std::ptr::null_mut() {
INCR_RECEIVED.store(true, Ordering::SeqCst);
@ -830,10 +863,11 @@ fn get_data_from_selection_owner(
} else {
return false;
};
let mut manager = manager_mutex.lock().unwrap();
if selection == 0 {
selection = get_atom_by_id(CLIPBOARD);
let mut shared = SHARED_STATE.lock().unwrap();
selection = shared.get_atom_by_id(CLIPBOARD);
}
let mut manager = manager_mutex.lock().unwrap();
manager.callback = callback;
// Clear data if we are not the selection owner.
@ -845,23 +879,24 @@ fn get_data_from_selection_owner(
// text format/atom.
for atom in atoms.iter() {
{
let connection = CONNECTION.lock().unwrap();
let mut shared = SHARED_STATE.lock().unwrap();
let clipboard_atom = shared.get_atom_by_id(CLIPBOARD);
xproto::convert_selection(
&connection.con,
&shared.conn,
manager.window,
selection,
*atom,
get_atom_by_id(CLIPBOARD),
clipboard_atom,
xcb::base::CURRENT_TIME
);
connection.con.flush();
shared.conn.flush();
}
// We use the "m_incr_received" to wait several timeouts in case
// that we've received the INCR SelectionNotify or
// PropertyNotify events.
'incr_loop: loop {
match manager.condvar.wait_timeout(manager, CV_TIMEOUT) {
match CONDVAR.wait_timeout(manager, CV_TIMEOUT) {
Ok((guard, status)) => {
manager = guard;
if !status.timed_out() {
@ -889,10 +924,13 @@ fn get_data_from_selection_owner(
fn get_x11_selection_owner() -> xcb::xproto::Window {
let mut result = 0;
let conn = CONNECTION.lock().unwrap();
let cookie = xproto::get_selection_owner(&conn.con, get_atom_by_id(CLIPBOARD));
let reply = unsafe {
xcb_get_selection_owner_reply(conn.con.get_raw_conn(), cookie.cookie, std::ptr::null_mut())
let mut shared = SHARED_STATE.lock().unwrap();
let clipboard_atom = shared.get_atom_by_id(CLIPBOARD);
let cookie = xproto::get_selection_owner(&shared.conn, clipboard_atom);
let reply = unsafe {
xcb_get_selection_owner_reply(
shared.conn.get_raw_conn(), cookie.cookie, std::ptr::null_mut()
)
};
if reply != std::ptr::null_mut() {
result = unsafe { (*reply).owner };