use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use x11rb::connection::Connection as _;
use x11rb::protocol::xproto::*;
use x11rb::protocol::Event;
use x11rb::rust_connection::RustConnection;
use x11rb::wrapper::ConnectionExt as _;
pub fn client_base(xid: u32, resource_id_mask: u32) -> u32 {
xid & !resource_id_mask
}
const DEFAULT_SERVE_DURATION: Duration = Duration::from_secs(5);
const GRACE_PERIOD: Duration = Duration::from_millis(200);
type FetchMap = Arc<Mutex<HashMap<u32, u32>>>;
pub struct ClipboardServeHandle {
stop: Arc<AtomicBool>,
skip_grace: Arc<AtomicBool>,
fetches: FetchMap,
resource_id_mask: u32,
#[allow(dead_code)] own_client_base: u32,
handle: Option<std::thread::JoinHandle<()>>,
}
impl ClipboardServeHandle {
pub fn stop_now(&self) {
self.skip_grace.store(true, Ordering::Relaxed);
self.stop.store(true, Ordering::Relaxed);
}
pub fn served_count(&self) -> u32 {
match self.fetches.lock() {
Ok(map) => map.values().sum(),
Err(_) => 0,
}
}
pub fn fetches_by_client(&self, base: u32) -> u32 {
match self.fetches.lock() {
Ok(map) => map.get(&base).copied().unwrap_or(0),
Err(_) => 0,
}
}
pub fn resource_id_mask(&self) -> u32 {
self.resource_id_mask
}
#[cfg(test)]
pub fn own_client_base(&self) -> u32 {
self.own_client_base
}
}
impl Drop for ClipboardServeHandle {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
pub fn x11_clipboard_get() -> Option<String> {
let (conn, screen_num) = x11rb::connect(None).ok()?;
let setup = conn.setup();
let root = setup.roots[screen_num].root;
let window = conn.generate_id().ok()?;
conn.create_window(
0,
window,
root,
0,
0,
1,
1,
0,
WindowClass::INPUT_ONLY,
0,
&CreateWindowAux::new(),
)
.ok()?;
let clipboard = intern(&conn, b"CLIPBOARD")?;
let utf8_string = intern(&conn, b"UTF8_STRING")?;
let property = intern(&conn, b"TALK_RS_SEL")?;
conn.convert_selection(window, clipboard, utf8_string, property, 0u32)
.ok()?;
conn.flush().ok()?;
let deadline = Instant::now() + Duration::from_secs(2);
let result = loop {
if Instant::now() > deadline {
break None;
}
let event = match conn.poll_for_event() {
Ok(Some(e)) => e,
Ok(None) => {
std::thread::sleep(Duration::from_millis(1));
continue;
}
Err(_) => break None,
};
if let Event::SelectionNotify(notify) = event {
if notify.requestor == window && notify.selection == clipboard {
if notify.property == 0u32 {
break None;
}
let prop = conn
.get_property(true, window, property, utf8_string, 0, u32::MAX)
.ok()?
.reply()
.ok()?;
break String::from_utf8(prop.value).ok();
}
}
};
let _ = conn.destroy_window(window);
let _ = conn.flush();
result
}
pub fn x11_clipboard_set(text: &str) -> Option<ClipboardServeHandle> {
let (conn, screen_num) = x11rb::connect(None).ok()?;
let setup = conn.setup();
let root = setup.roots[screen_num].root;
let window = conn.generate_id().ok()?;
conn.create_window(
0,
window,
root,
0,
0,
1,
1,
0,
WindowClass::INPUT_ONLY,
0,
&CreateWindowAux::new(),
)
.ok()?;
let clipboard = intern(&conn, b"CLIPBOARD")?;
let utf8_string = intern(&conn, b"UTF8_STRING")?;
let targets_atom = intern(&conn, b"TARGETS")?;
conn.set_selection_owner(window, clipboard, 0u32).ok()?;
conn.flush().ok()?;
let owner = conn
.get_selection_owner(clipboard)
.ok()?
.reply()
.ok()?
.owner;
if owner != window {
let _ = conn.destroy_window(window);
let _ = conn.flush();
return None;
}
let resource_id_mask = setup.resource_id_mask;
let own_client_base = client_base(setup.resource_id_base, resource_id_mask);
let stop = Arc::new(AtomicBool::new(false));
let skip_grace = Arc::new(AtomicBool::new(false));
let fetches: FetchMap = Arc::new(Mutex::new(HashMap::new()));
let stop2 = Arc::clone(&stop);
let skip2 = Arc::clone(&skip_grace);
let fetches2 = Arc::clone(&fetches);
let text = text.to_string();
let atoms = Atoms {
clipboard,
utf8_string,
targets: targets_atom,
};
let handle = std::thread::spawn(move || {
serve_loop(
&conn,
&atoms,
&text,
&stop2,
&skip2,
&fetches2,
resource_id_mask,
own_client_base,
);
let _ = conn.destroy_window(window);
let _ = conn.flush();
});
Some(ClipboardServeHandle {
stop,
skip_grace,
fetches,
resource_id_mask,
own_client_base,
handle: Some(handle),
})
}
struct Atoms {
clipboard: Atom,
utf8_string: Atom,
targets: Atom,
}
fn intern(conn: &RustConnection, name: &[u8]) -> Option<Atom> {
conn.intern_atom(false, name)
.ok()?
.reply()
.ok()
.map(|r| r.atom)
}
#[allow(clippy::too_many_arguments)] fn serve_loop(
conn: &RustConnection,
atoms: &Atoms,
text: &str,
stop: &AtomicBool,
skip_grace: &AtomicBool,
fetches: &FetchMap,
resource_id_mask: u32,
own_client_base: u32,
) {
let deadline = Instant::now() + DEFAULT_SERVE_DURATION;
loop {
if stop.load(Ordering::Relaxed) || Instant::now() > deadline {
if !skip_grace.load(Ordering::Relaxed) {
let grace_end = Instant::now() + GRACE_PERIOD;
while Instant::now() < grace_end {
poll_and_serve(
conn,
atoms,
text,
fetches,
resource_id_mask,
own_client_base,
);
std::thread::sleep(Duration::from_millis(1));
}
}
break;
}
match conn.poll_for_event() {
Ok(Some(Event::SelectionRequest(req))) if req.selection == atoms.clipboard => {
serve_request(
conn,
&req,
text,
atoms,
fetches,
resource_id_mask,
own_client_base,
);
}
Ok(Some(Event::SelectionClear(ev))) if ev.selection == atoms.clipboard => {
break;
}
Ok(_) => {
std::thread::sleep(Duration::from_millis(1));
}
Err(_) => break,
}
}
}
fn poll_and_serve(
conn: &RustConnection,
atoms: &Atoms,
text: &str,
fetches: &FetchMap,
resource_id_mask: u32,
own_client_base: u32,
) {
if let Ok(Some(Event::SelectionRequest(req))) = conn.poll_for_event() {
if req.selection == atoms.clipboard {
serve_request(
conn,
&req,
text,
atoms,
fetches,
resource_id_mask,
own_client_base,
);
}
}
}
#[allow(clippy::too_many_arguments)] fn serve_request(
conn: &RustConnection,
req: &SelectionRequestEvent,
text: &str,
atoms: &Atoms,
fetches: &FetchMap,
resource_id_mask: u32,
own_client_base: u32,
) {
let prop = if req.property == 0u32 {
req.target
} else {
req.property
};
let ok = if req.target == atoms.targets {
let supported = [atoms.targets, atoms.utf8_string];
conn.change_property32(
PropMode::REPLACE,
req.requestor,
prop,
AtomEnum::ATOM,
&supported,
)
.is_ok()
} else if req.target == atoms.utf8_string {
let wrote = conn
.change_property8(
PropMode::REPLACE,
req.requestor,
prop,
atoms.utf8_string,
text.as_bytes(),
)
.is_ok();
if wrote {
let requestor_base = client_base(req.requestor, resource_id_mask);
if requestor_base != own_client_base {
if let Ok(mut map) = fetches.lock() {
let this_count = {
let entry = map.entry(requestor_base).or_insert(0);
*entry += 1;
*entry
};
let total: u32 = map.values().sum();
log::trace!(
"clipboard serve: answered UTF8_STRING request from requestor {} \
(client-base {:#x}, {} bytes, this-client count={}, total={})",
req.requestor,
requestor_base,
text.len(),
this_count,
total,
);
}
} else {
log::trace!(
"clipboard serve: skipping count for self-directed UTF8_STRING request \
from requestor {} (own client-base {:#x})",
req.requestor,
own_client_base,
);
}
}
wrote
} else {
false
};
let reply_prop = if ok { prop } else { 0u32 };
let notify = SelectionNotifyEvent {
response_type: SELECTION_NOTIFY_EVENT,
sequence: 0,
time: req.time,
requestor: req.requestor,
selection: req.selection,
target: req.target,
property: reply_prop,
};
let _ = conn.send_event(false, req.requestor, EventMask::NO_EVENT, notify);
let _ = conn.flush();
}