use std::{
borrow::Cow,
cell::RefCell,
collections::{hash_map::Entry, HashMap},
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
},
thread::{self, JoinHandle},
thread_local,
time::{Duration, Instant},
usize,
};
use log::{error, trace, warn};
use parking_lot::{Condvar, Mutex, MutexGuard, RwLock};
use x11rb::{
connection::Connection,
protocol::{
xproto::{
Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property,
PropertyNotifyEvent, SelectionNotifyEvent, SelectionRequestEvent, Time, WindowClass,
SELECTION_NOTIFY_EVENT,
},
Event,
},
rust_connection::RustConnection,
wrapper::ConnectionExt as _,
COPY_DEPTH_FROM_PARENT, COPY_FROM_PARENT, NONE,
};
#[cfg(feature = "image-data")]
use super::encode_as_png;
use super::{into_unknown, LinuxClipboardKind};
#[cfg(feature = "image-data")]
use crate::ImageData;
use crate::{common::ScopeGuard, Error};
type Result<T, E = Error> = std::result::Result<T, E>;
static CLIPBOARD: Mutex<Option<GlobalClipboard>> = parking_lot::const_mutex(None);
x11rb::atom_manager! {
pub Atoms: AtomCookies {
CLIPBOARD,
PRIMARY,
SECONDARY,
CLIPBOARD_MANAGER,
SAVE_TARGETS,
TARGETS,
ATOM,
INCR,
UTF8_STRING,
UTF8_MIME_0: b"text/plain;charset=utf-8",
UTF8_MIME_1: b"text/plain;charset=UTF-8",
STRING,
TEXT,
TEXT_MIME_UNKNOWN: b"text/plain",
HTML: b"text/html",
PNG_MIME: b"image/png",
ARBOARD_CLIPBOARD,
}
}
thread_local! {
static ATOM_NAME_CACHE: RefCell<HashMap<Atom, &'static str>> = Default::default();
}
const LONG_TIMEOUT_DUR: Duration = Duration::from_millis(4000);
const SHORT_TIMEOUT_DUR: Duration = Duration::from_millis(10);
#[derive(Debug, PartialEq, Eq)]
enum ManagerHandoverState {
Idle,
InProgress,
Finished,
}
struct GlobalClipboard {
inner: Arc<Inner>,
server_handle: JoinHandle<()>,
}
struct XContext {
conn: RustConnection,
win_id: u32,
}
struct Inner {
server: XContext,
atoms: Atoms,
clipboard: Selection,
primary: Selection,
secondary: Selection,
handover_state: Mutex<ManagerHandoverState>,
handover_cv: Condvar,
serve_stopped: AtomicBool,
}
impl XContext {
fn new() -> Result<Self> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send(RustConnection::connect(None)).ok(); });
let patient_conn = rx.recv_timeout(SHORT_TIMEOUT_DUR).map_err(|_| Error::Unknown {
description: String::from("X11 server connection timed out because it was unreachable"),
})?;
let (conn, screen_num): (RustConnection, _) = patient_conn.map_err(into_unknown)?;
let screen = conn
.setup()
.roots
.get(screen_num)
.ok_or(Error::Unknown { description: String::from("no screen found") })?;
let win_id = conn.generate_id().map_err(into_unknown)?;
let event_mask =
EventMask::PROPERTY_CHANGE |
EventMask::STRUCTURE_NOTIFY;
conn.create_window(
COPY_DEPTH_FROM_PARENT,
win_id,
screen.root,
0,
0,
1,
1,
0,
WindowClass::COPY_FROM_PARENT,
COPY_FROM_PARENT,
&CreateWindowAux::new().event_mask(event_mask),
)
.map_err(into_unknown)?;
conn.flush().map_err(into_unknown)?;
Ok(Self { conn, win_id })
}
}
#[derive(Default)]
struct Selection {
data: RwLock<Option<Vec<ClipboardData>>>,
mutex: Mutex<()>,
data_changed: Condvar,
}
#[derive(Debug, Clone)]
struct ClipboardData {
bytes: Vec<u8>,
format: Atom,
}
enum ReadSelNotifyResult {
GotData(Vec<u8>),
IncrStarted,
EventNotRecognized,
}
impl Inner {
fn new() -> Result<Self> {
let server = XContext::new()?;
let atoms =
Atoms::new(&server.conn).map_err(into_unknown)?.reply().map_err(into_unknown)?;
Ok(Self {
server,
atoms,
clipboard: Selection::default(),
primary: Selection::default(),
secondary: Selection::default(),
handover_state: Mutex::new(ManagerHandoverState::Idle),
handover_cv: Condvar::new(),
serve_stopped: AtomicBool::new(false),
})
}
fn write(
&self,
data: Vec<ClipboardData>,
selection: LinuxClipboardKind,
wait: bool,
) -> Result<()> {
if self.serve_stopped.load(Ordering::Relaxed) {
return Err(Error::Unknown {
description: "The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)".into()
});
}
let server_win = self.server.win_id;
self.server
.conn
.set_selection_owner(server_win, self.atom_of(selection), Time::CURRENT_TIME)
.map_err(|_| Error::ClipboardOccupied)?;
self.server.conn.flush().map_err(into_unknown)?;
let selection = self.selection_of(selection);
let mut data_guard = selection.data.write();
*data_guard = Some(data);
let mut guard = selection.mutex.lock();
selection.data_changed.notify_all();
if wait {
drop(data_guard);
selection.data_changed.wait(&mut guard);
}
Ok(())
}
fn read(&self, formats: &[Atom], selection: LinuxClipboardKind) -> Result<ClipboardData> {
if self.is_owner(selection)? {
let data = self.selection_of(selection).data.read();
if let Some(data_list) = &*data {
for data in data_list {
for format in formats {
if *format == data.format {
return Ok(data.clone());
}
}
}
}
return Err(Error::ContentNotAvailable);
}
let reader = XContext::new()?;
trace!("Trying to get the clipboard data.");
for format in formats {
match self.read_single(&reader, selection, *format) {
Ok(bytes) => {
return Ok(ClipboardData { bytes, format: *format });
}
Err(Error::ContentNotAvailable) => {
continue;
}
Err(e) => return Err(e),
}
}
Err(Error::ContentNotAvailable)
}
fn read_single(
&self,
reader: &XContext,
selection: LinuxClipboardKind,
target_format: Atom,
) -> Result<Vec<u8>> {
reader
.conn
.delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD)
.map_err(into_unknown)?;
reader
.conn
.convert_selection(
reader.win_id,
self.atom_of(selection),
target_format,
self.atoms.ARBOARD_CLIPBOARD,
Time::CURRENT_TIME,
)
.map_err(into_unknown)?;
reader.conn.sync().map_err(into_unknown)?;
trace!("Finished `convert_selection`");
let mut incr_data: Vec<u8> = Vec::new();
let mut using_incr = false;
let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR;
while Instant::now() < timeout_end {
let event = reader.conn.poll_for_event().map_err(into_unknown)?;
let event = match event {
Some(e) => e,
None => {
std::thread::sleep(Duration::from_millis(1));
continue;
}
};
match event {
Event::SelectionNotify(event) => {
trace!("Read SelectionNotify");
let result = self.handle_read_selection_notify(
reader,
target_format,
&mut using_incr,
&mut incr_data,
event,
)?;
match result {
ReadSelNotifyResult::GotData(data) => return Ok(data),
ReadSelNotifyResult::IncrStarted => {
timeout_end += SHORT_TIMEOUT_DUR;
}
ReadSelNotifyResult::EventNotRecognized => (),
}
}
Event::PropertyNotify(event) => {
let result = self.handle_read_property_notify(
reader,
target_format,
using_incr,
&mut incr_data,
&mut timeout_end,
event,
)?;
if result {
return Ok(incr_data);
}
}
_ => log::trace!("An unexpected event arrived while reading the clipboard."),
}
}
log::info!("Time-out hit while reading the clipboard.");
Err(Error::ContentNotAvailable)
}
fn atom_of(&self, selection: LinuxClipboardKind) -> Atom {
match selection {
LinuxClipboardKind::Clipboard => self.atoms.CLIPBOARD,
LinuxClipboardKind::Primary => self.atoms.PRIMARY,
LinuxClipboardKind::Secondary => self.atoms.SECONDARY,
}
}
fn selection_of(&self, selection: LinuxClipboardKind) -> &Selection {
match selection {
LinuxClipboardKind::Clipboard => &self.clipboard,
LinuxClipboardKind::Primary => &self.primary,
LinuxClipboardKind::Secondary => &self.secondary,
}
}
fn kind_of(&self, atom: Atom) -> Option<LinuxClipboardKind> {
match atom {
a if a == self.atoms.CLIPBOARD => Some(LinuxClipboardKind::Clipboard),
a if a == self.atoms.PRIMARY => Some(LinuxClipboardKind::Primary),
a if a == self.atoms.SECONDARY => Some(LinuxClipboardKind::Secondary),
_ => None,
}
}
fn is_owner(&self, selection: LinuxClipboardKind) -> Result<bool> {
let current = self
.server
.conn
.get_selection_owner(self.atom_of(selection))
.map_err(into_unknown)?
.reply()
.map_err(into_unknown)?
.owner;
Ok(current == self.server.win_id)
}
fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result<String> {
String::from_utf8(
self.server
.conn
.get_atom_name(atom)
.map_err(into_unknown)?
.reply()
.map_err(into_unknown)?
.name,
)
.map_err(into_unknown)
}
fn atom_name_dbg(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str {
ATOM_NAME_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
match cache.entry(atom) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let s = self
.atom_name(atom)
.map(|s| Box::leak(s.into_boxed_str()) as &str)
.unwrap_or("FAILED-TO-GET-THE-ATOM-NAME");
entry.insert(s);
s
}
}
})
}
fn handle_read_selection_notify(
&self,
reader: &XContext,
target_format: u32,
using_incr: &mut bool,
incr_data: &mut Vec<u8>,
event: SelectionNotifyEvent,
) -> Result<ReadSelNotifyResult> {
if event.property == NONE || event.target != target_format {
return Err(Error::ContentNotAvailable);
}
if self.kind_of(event.selection).is_none() {
log::info!("Received a SelectionNotify for a selection other than CLIPBOARD, PRIMARY or SECONDARY. This is unexpected.");
return Ok(ReadSelNotifyResult::EventNotRecognized);
}
if *using_incr {
log::warn!("Received a SelectionNotify while already expecting INCR segments.");
return Ok(ReadSelNotifyResult::EventNotRecognized);
}
let mut reply = reader
.conn
.get_property(true, event.requestor, event.property, event.target, 0, u32::MAX / 4)
.map_err(into_unknown)?
.reply()
.map_err(into_unknown)?;
if reply.type_ == target_format {
Ok(ReadSelNotifyResult::GotData(reply.value))
} else if reply.type_ == self.atoms.INCR {
reply = reader
.conn
.get_property(
true,
event.requestor,
event.property,
self.atoms.INCR,
0,
u32::MAX / 4,
)
.map_err(into_unknown)?
.reply()
.map_err(into_unknown)?;
log::trace!("Receiving INCR segments");
*using_incr = true;
if reply.value_len == 4 {
let min_data_len = reply.value32().and_then(|mut vals| vals.next()).unwrap_or(0);
incr_data.reserve(min_data_len as usize);
}
Ok(ReadSelNotifyResult::IncrStarted)
} else {
Err(Error::Unknown {
description: String::from("incorrect type received from clipboard"),
})
}
}
fn handle_read_property_notify(
&self,
reader: &XContext,
target_format: u32,
using_incr: bool,
incr_data: &mut Vec<u8>,
timeout_end: &mut Instant,
event: PropertyNotifyEvent,
) -> Result<bool> {
if event.atom != self.atoms.ARBOARD_CLIPBOARD || event.state != Property::NEW_VALUE {
return Ok(false);
}
if !using_incr {
return Ok(false);
}
let reply = reader
.conn
.get_property(true, event.window, event.atom, target_format, 0, u32::MAX / 4)
.map_err(into_unknown)?
.reply()
.map_err(into_unknown)?;
if reply.value_len == 0 {
return Ok(true);
}
incr_data.extend(reply.value);
*timeout_end = Instant::now() + SHORT_TIMEOUT_DUR;
Ok(false)
}
fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> {
let selection = match self.kind_of(event.selection) {
Some(kind) => kind,
None => {
warn!("Received a selection request to a selection other than the CLIPBOARD, PRIMARY or SECONDARY. This is unexpected.");
return Ok(());
}
};
let success;
if event.target == self.atoms.TARGETS {
trace!("Handling TARGETS, dst property is {}", self.atom_name_dbg(event.property));
let mut targets = Vec::with_capacity(10);
targets.push(self.atoms.TARGETS);
targets.push(self.atoms.SAVE_TARGETS);
let data = self.selection_of(selection).data.read();
if let Some(data_list) = &*data {
for data in data_list {
targets.push(data.format);
if data.format == self.atoms.UTF8_STRING {
targets.push(self.atoms.UTF8_MIME_0);
targets.push(self.atoms.UTF8_MIME_1);
}
}
}
self.server
.conn
.change_property32(
PropMode::REPLACE,
event.requestor,
event.property,
self.atoms.ATOM,
&targets,
)
.map_err(into_unknown)?;
self.server.conn.flush().map_err(into_unknown)?;
success = true;
} else {
trace!("Handling request for (probably) the clipboard contents.");
let data = self.selection_of(selection).data.read();
if let Some(data_list) = &*data {
success = match data_list.iter().find(|d| d.format == event.target) {
Some(data) => {
self.server
.conn
.change_property8(
PropMode::REPLACE,
event.requestor,
event.property,
event.target,
&data.bytes,
)
.map_err(into_unknown)?;
self.server.conn.flush().map_err(into_unknown)?;
true
}
None => false,
};
} else {
success = false;
}
}
let property = if success { event.property } else { AtomEnum::NONE.into() };
self.server
.conn
.send_event(
false,
event.requestor,
EventMask::NO_EVENT,
SelectionNotifyEvent {
response_type: SELECTION_NOTIFY_EVENT,
sequence: event.sequence,
time: event.time,
requestor: event.requestor,
selection: event.selection,
target: event.target,
property,
},
)
.map_err(into_unknown)?;
self.server.conn.flush().map_err(into_unknown)
}
fn ask_clipboard_manager_to_request_our_data(&self) -> Result<()> {
if self.server.win_id == 0 {
error!("The server's window id was 0. This is unexpected");
return Ok(());
}
if !self.is_owner(LinuxClipboardKind::Clipboard)? {
return Ok(());
}
if self.selection_of(LinuxClipboardKind::Clipboard).data.read().is_none() {
return Ok(());
}
let mut handover_state = self.handover_state.lock();
trace!("Sending the data to the clipboard manager");
self.server
.conn
.convert_selection(
self.server.win_id,
self.atoms.CLIPBOARD_MANAGER,
self.atoms.SAVE_TARGETS,
self.atoms.ARBOARD_CLIPBOARD,
Time::CURRENT_TIME,
)
.map_err(into_unknown)?;
self.server.conn.flush().map_err(into_unknown)?;
*handover_state = ManagerHandoverState::InProgress;
let max_handover_duration = Duration::from_millis(100);
let result = self.handover_cv.wait_for(&mut handover_state, max_handover_duration);
if *handover_state == ManagerHandoverState::Finished {
return Ok(());
}
if result.timed_out() {
warn!("Could not hand the clipboard contents over to the clipboard manager. The request timed out.");
return Ok(());
}
Err(Error::Unknown {
description: "The handover was not finished and the condvar didn't time out, yet the condvar wait ended. This should be unreachable.".into()
})
}
}
fn serve_requests(context: Arc<Inner>) -> Result<(), Box<dyn std::error::Error>> {
fn handover_finished(clip: &Arc<Inner>, mut handover_state: MutexGuard<ManagerHandoverState>) {
log::trace!("Finishing clipboard manager handover.");
*handover_state = ManagerHandoverState::Finished;
drop(handover_state);
clip.handover_cv.notify_all();
}
trace!("Started serve requests thread.");
let _guard = ScopeGuard::new(|| {
context.serve_stopped.store(true, Ordering::Relaxed);
});
let mut written = false;
let mut notified = false;
loop {
match context.server.conn.wait_for_event().map_err(into_unknown)? {
Event::DestroyNotify(_) => {
trace!("Clipboard server window is being destroyed x_x");
return Ok(());
}
Event::SelectionClear(event) => {
trace!("Somebody else owns the clipboard now");
if let Some(selection) = context.kind_of(event.selection) {
let selection = context.selection_of(selection);
let mut data_guard = selection.data.write();
*data_guard = None;
let _guard = selection.mutex.lock();
selection.data_changed.notify_all();
}
}
Event::SelectionRequest(event) => {
trace!(
"SelectionRequest - selection is: {}, target is {}",
context.atom_name_dbg(event.selection),
context.atom_name_dbg(event.target),
);
context.handle_selection_request(event).map_err(into_unknown)?;
let handover_state = context.handover_state.lock();
if *handover_state == ManagerHandoverState::InProgress {
if event.target != context.atoms.TARGETS {
trace!("The contents were written to the clipboard manager.");
written = true;
if notified {
handover_finished(&context, handover_state);
}
}
}
}
Event::SelectionNotify(event) => {
if event.selection != context.atoms.CLIPBOARD_MANAGER {
error!("Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread.");
continue;
}
let handover_state = context.handover_state.lock();
if *handover_state == ManagerHandoverState::InProgress {
trace!("The clipboard manager indicated that it's done requesting the contents from us.");
notified = true;
if written {
handover_finished(&context, handover_state);
}
}
}
_event => {
}
}
}
}
pub(crate) struct Clipboard {
inner: Arc<Inner>,
}
impl Clipboard {
pub(crate) fn new() -> Result<Self> {
let mut global_cb = CLIPBOARD.lock();
if let Some(global_cb) = &*global_cb {
return Ok(Self { inner: Arc::clone(&global_cb.inner) });
}
let ctx = Arc::new(Inner::new()?);
let join_handle;
{
let ctx = Arc::clone(&ctx);
join_handle = std::thread::spawn(move || {
if let Err(error) = serve_requests(ctx) {
error!("Worker thread errored with: {}", error);
}
});
}
*global_cb = Some(GlobalClipboard { inner: Arc::clone(&ctx), server_handle: join_handle });
Ok(Self { inner: ctx })
}
pub(crate) fn get_text(&self, selection: LinuxClipboardKind) -> Result<String> {
let formats = [
self.inner.atoms.UTF8_STRING,
self.inner.atoms.UTF8_MIME_0,
self.inner.atoms.UTF8_MIME_1,
self.inner.atoms.STRING,
self.inner.atoms.TEXT,
self.inner.atoms.TEXT_MIME_UNKNOWN,
];
let result = self.inner.read(&formats, selection)?;
if result.format == self.inner.atoms.STRING {
Ok(result.bytes.into_iter().map(|c| c as char).collect())
} else {
String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)
}
}
pub(crate) fn set_text(
&self,
message: Cow<'_, str>,
selection: LinuxClipboardKind,
wait: bool,
) -> Result<()> {
let data = vec![ClipboardData {
bytes: message.into_owned().into_bytes(),
format: self.inner.atoms.UTF8_STRING,
}];
self.inner.write(data, selection, wait)
}
pub(crate) fn set_html(
&self,
html: Cow<'_, str>,
alt: Option<Cow<'_, str>>,
selection: LinuxClipboardKind,
wait: bool,
) -> Result<()> {
let mut data = vec![];
if let Some(alt_text) = alt {
data.push(ClipboardData {
bytes: alt_text.into_owned().into_bytes(),
format: self.inner.atoms.UTF8_STRING,
});
}
data.push(ClipboardData {
bytes: html.into_owned().into_bytes(),
format: self.inner.atoms.HTML,
});
self.inner.write(data, selection, wait)
}
#[cfg(feature = "image-data")]
pub(crate) fn get_image(&self, selection: LinuxClipboardKind) -> Result<ImageData<'static>> {
let formats = [self.inner.atoms.PNG_MIME];
let bytes = self.inner.read(&formats, selection)?.bytes;
let cursor = std::io::Cursor::new(&bytes);
let mut reader = image::io::Reader::new(cursor);
reader.set_format(image::ImageFormat::Png);
let image = match reader.decode() {
Ok(img) => img.into_rgba8(),
Err(_e) => return Err(Error::ConversionFailure),
};
let (w, h) = image.dimensions();
let image_data =
ImageData { width: w as usize, height: h as usize, bytes: image.into_raw().into() };
Ok(image_data)
}
#[cfg(feature = "image-data")]
pub(crate) fn set_image(
&self,
image: ImageData,
selection: LinuxClipboardKind,
wait: bool,
) -> Result<()> {
let encoded = encode_as_png(&image)?;
let data = vec![ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME }];
self.inner.write(data, selection, wait)
}
}
impl Drop for Clipboard {
fn drop(&mut self) {
const MIN_OWNERS: usize = 3;
let mut global_cb = CLIPBOARD.lock();
if Arc::strong_count(&self.inner) == MIN_OWNERS {
if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() {
error!("Could not hand the clipboard data over to the clipboard manager: {}", e);
}
let global_cb = global_cb.take();
if let Err(e) = self.inner.server.conn.destroy_window(self.inner.server.win_id) {
error!("Failed to destroy the clipboard window. Error: {}", e);
return;
}
if let Err(e) = self.inner.server.conn.flush() {
error!("Failed to flush the clipboard window. Error: {}", e);
return;
}
if let Some(global_cb) = global_cb {
if let Err(e) = global_cb.server_handle.join() {
let message;
if let Some(msg) = e.downcast_ref::<&'static str>() {
message = Some((*msg).to_string());
} else if let Some(msg) = e.downcast_ref::<String>() {
message = Some(msg.clone());
} else {
message = None;
}
if let Some(message) = message {
error!(
"The clipboard server thread panicked. Panic message: '{}'",
message,
);
} else {
error!("The clipboard server thread panicked.");
}
}
}
}
}
}