use std::{
cell::RefCell,
collections::{hash_map::Entry, HashMap},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread::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,
};
use crate::{common::ScopeGuard, common_linux::into_unknown, Error, LinuxClipboardKind};
#[cfg(feature = "image-data")]
use crate::{common_linux::encode_as_png, ImageData};
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",
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 {
context: Arc<ClipboardContext>,
server_handle: JoinHandle<()>,
}
struct XContext {
conn: RustConnection,
win_id: u32,
}
struct ClipboardContext {
server: XContext,
atoms: Atoms,
clipboard_data: RwLock<Option<ClipboardData>>,
primary_data: RwLock<Option<ClipboardData>>,
secondary_data: RwLock<Option<ClipboardData>>,
handover_state: Mutex<ManagerHandoverState>,
handover_cv: Condvar,
serve_stopped: AtomicBool,
}
impl XContext {
fn new() -> Result<Self> {
let (conn, screen_num): (RustConnection, _) =
RustConnection::connect(None).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(Debug, Clone)]
struct ClipboardData {
bytes: Vec<u8>,
format: Atom,
}
enum ReadSelNotifyResult {
GotData(Vec<u8>),
IncrStarted,
EventNotRecognized,
}
impl ClipboardContext {
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_data: RwLock::default(),
primary_data: RwLock::default(),
secondary_data: RwLock::default(),
handover_state: Mutex::new(ManagerHandoverState::Idle),
handover_cv: Condvar::new(),
serve_stopped: AtomicBool::new(false),
})
}
fn write(&self, data: ClipboardData, selection: LinuxClipboardKind) -> 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)?;
*self.data_of(selection).write() = Some(data);
Ok(())
}
fn read(&self, formats: &[Atom], selection: LinuxClipboardKind) -> Result<ClipboardData> {
if self.is_owner(selection)? {
let data = self.data_of(selection).read();
if let Some(data) = &*data {
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 data_of(&self, selection: LinuxClipboardKind) -> &RwLock<Option<ClipboardData>> {
match selection {
LinuxClipboardKind::Clipboard => &self.clipboard_data,
LinuxClipboardKind::Primary => &self.primary_data,
LinuxClipboardKind::Secondary => &self.secondary_data,
}
}
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.data_of(selection).read();
if let Some(data) = &*data {
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.data_of(selection).read();
if let Some(data) = &*data {
if data.format == event.target {
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)?;
success = true;
} else {
success = 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.data_of(LinuxClipboardKind::Clipboard).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(clipboard: Arc<ClipboardContext>) -> Result<(), Box<dyn std::error::Error>> {
fn handover_finished(
clip: &Arc<ClipboardContext>,
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(|| {
clipboard.serve_stopped.store(true, Ordering::Relaxed);
});
let mut written = false;
let mut notified = false;
loop {
match clipboard.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) = clipboard.kind_of(event.selection) {
let mut data = clipboard.data_of(selection).write();
*data = None
}
}
Event::SelectionRequest(event) => {
trace!(
"SelectionRequest - selection is: {}, target is {}",
clipboard.atom_name_dbg(event.selection),
clipboard.atom_name_dbg(event.target),
);
clipboard.handle_selection_request(event).map_err(into_unknown)?;
let handover_state = clipboard.handover_state.lock();
if *handover_state == ManagerHandoverState::InProgress {
if event.target != clipboard.atoms.TARGETS {
trace!("The contents were written to the clipboard manager.");
written = true;
if notified {
handover_finished(&clipboard, handover_state);
}
}
}
}
Event::SelectionNotify(event) => {
if event.selection != clipboard.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 = clipboard.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(&clipboard, handover_state);
}
}
}
_event => {
}
}
}
}
pub struct X11ClipboardContext {
inner: Arc<ClipboardContext>,
}
impl X11ClipboardContext {
pub 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.context) });
}
let ctx = Arc::new(ClipboardContext::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 { context: Arc::clone(&ctx), server_handle: join_handle });
Ok(Self { inner: ctx })
}
pub fn get_text(&self) -> Result<String> {
self.get_text_with_clipboard(LinuxClipboardKind::Clipboard)
}
pub(crate) fn get_text_with_clipboard(&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 fn set_text(&self, message: String) -> Result<()> {
self.set_text_with_clipboard(message, LinuxClipboardKind::Clipboard)
}
pub(crate) fn set_text_with_clipboard(
&self,
message: String,
selection: LinuxClipboardKind,
) -> Result<()> {
let data =
ClipboardData { bytes: message.into_bytes(), format: self.inner.atoms.UTF8_STRING };
self.inner.write(data, selection)
}
#[cfg(feature = "image-data")]
pub fn get_image(&self) -> Result<ImageData<'static>> {
let formats = [self.inner.atoms.PNG_MIME];
let bytes = self.inner.read(&formats, LinuxClipboardKind::Clipboard)?.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 fn set_image(&self, image: ImageData) -> Result<()> {
let encoded = encode_as_png(&image)?;
let data = ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME };
self.inner.write(data, LinuxClipboardKind::Clipboard)
}
}
impl Drop for X11ClipboardContext {
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 paniced. Panic message: '{}'", message);
} else {
error!("The clipboard server thread paniced.");
}
}
}
}
}
}