mirror of
https://github.com/1Password/arboard.git
synced 2025-12-23 06:01:09 +00:00
X11 clipboard impl work in progress.
This commit is contained in:
parent
28910ef591
commit
1410b4e4e6
2 changed files with 123 additions and 85 deletions
|
|
@ -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!";
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
¬ify 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 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue